attributes->get('worker'); // Sanitize job_id: if not a valid existing JobPost ID, treat it as null (job id is not needed/optional) $jobIdInput = $request->input('job_id'); if (empty($jobIdInput) || !is_numeric($jobIdInput) || !\App\Models\JobPost::where('id', $jobIdInput)->exists()) { $request->merge(['job_id' => null]); } $validator = Validator::make($request->all(), [ 'employer_id' => 'required|exists:users,id', 'job_id' => 'nullable|exists:job_posts,id', 'rating' => 'required|integer|min:1|max:5', 'title' => 'nullable|string|max:255', 'comment' => 'required|string|max:1000', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $employerId = $request->employer_id; $jobId = $request->job_id; // 1. Validate confirmed hire and completed joining, and 3-month requirement $hasConfirmedHire = false; $joiningDate = null; // Check standard JobApplication $appQuery = JobApplication::where('worker_id', $worker->id) ->where('status', 'hired') ->whereNotNull('joining_confirmed_at') ->whereHas('jobPost', function ($query) use ($employerId, $jobId) { $query->where('employer_id', $employerId); if ($jobId) { $query->where('id', $jobId); } }); $app = $appQuery->first(); if ($app) { $hasConfirmedHire = true; $joiningDate = Carbon::parse($app->joining_confirmed_at); } else { // Check direct JobOffer $offerQuery = JobOffer::where('worker_id', $worker->id) ->where('employer_id', $employerId) ->where('status', 'accepted'); $offer = $offerQuery->first(); if ($offer) { $hasConfirmedHire = true; // For direct offers, we treat work_date or updated_at as joining date $joiningDate = $offer->work_date ? Carbon::parse($offer->work_date) : $offer->updated_at; } } if (!$hasConfirmedHire) { return response()->json([ 'success' => false, 'message' => 'You can only review employers with a confirmed hire and completed joining.' ], 403); } // Verify if completed configured months from joining date $eligibilityMonths = (int) config('reminders.review_eligibility_months', 3); if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < $eligibilityMonths) { return response()->json([ 'success' => false, 'message' => "You can write a review only after {$eligibilityMonths} months from the joining date." ], 403); } // 2. Prevent duplicate reviews for the same job/employer combination $existing = EmployerReview::where('worker_id', $worker->id) ->where('employer_id', $employerId) ->where('job_id', $jobId) ->first(); if ($existing) { return response()->json([ 'success' => false, 'message' => 'You have already submitted a review for this job/employer.' ], 422); } // 3. Create the review $review = EmployerReview::create([ 'worker_id' => $worker->id, 'employer_id' => $employerId, 'job_id' => $jobId, 'rating' => $request->rating, 'title' => $request->title, 'comment' => $request->comment, ]); return response()->json([ 'success' => true, 'message' => 'Review submitted successfully.', 'data' => [ 'review' => $review ] ], 201); } catch (\Exception $e) { logger()->error('Worker Add Employer Review Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while adding the review.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * Edit/update an existing review. * PUT /api/workers/reviews/{id} */ public function editReview(Request $request, $id) { /** @var Worker $worker */ $worker = $request->attributes->get('worker'); $validator = Validator::make($request->all(), [ 'rating' => 'required|integer|min:1|max:5', 'title' => 'nullable|string|max:255', 'comment' => 'required|string|max:1000', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $review = EmployerReview::where('id', $id) ->where('worker_id', $worker->id) ->first(); if (!$review) { return response()->json([ 'success' => false, 'message' => 'Review not found.' ], 404); } // Check if the edit window has expired $editPeriodDays = (int) config('reminders.review_edit_period_days', 7); if ($review->created_at->addDays($editPeriodDays)->isPast()) { return response()->json([ 'success' => false, 'message' => "The {$editPeriodDays}-day edit window for this review has expired." ], 403); } $review->update([ 'rating' => $request->rating, 'title' => $request->title, 'comment' => $request->comment, ]); return response()->json([ 'success' => true, 'message' => 'Review updated successfully.', 'data' => [ 'review' => $review ] ], 200); } catch (\Exception $e) { logger()->error('Worker Edit Employer Review Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while updating the review.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * View a single review. * GET /api/workers/reviews/{id} */ public function viewReview(Request $request, $id) { /** @var Worker $worker */ $worker = $request->attributes->get('worker'); try { $review = EmployerReview::where('id', $id) ->where('worker_id', $worker->id) ->with(['employer.employerProfile', 'jobPost']) ->first(); if (!$review) { return response()->json([ 'success' => false, 'message' => 'Review not found.' ], 404); } $editPeriodDays = (int) config('reminders.review_edit_period_days', 7); $editable = !$review->created_at->addDays($editPeriodDays)->isPast(); return response()->json([ 'success' => true, 'data' => [ 'review' => [ 'id' => $review->id, 'employer_id' => $review->employer_id, 'employer_name' => $review->employer->name ?? 'Employer', 'job_id' => $review->job_id, 'job_title' => $review->jobPost->title ?? null, 'rating' => $review->rating, 'title' => $review->title, 'comment' => $review->comment, 'editable' => $editable, 'created_at' => $review->created_at->toIso8601String(), 'updated_at' => $review->updated_at->toIso8601String(), ] ] ], 200); } catch (\Exception $e) { logger()->error('Worker View Employer Review Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while fetching the review.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * Get reviews written by this worker. * GET /api/workers/reviews */ public function getReviews(Request $request) { /** @var Worker $worker */ $worker = $request->attributes->get('worker'); try { $page = (int) $request->input('page', 1); $perPage = (int) $request->input('per_page', 15); $query = EmployerReview::where('worker_id', $worker->id) ->with(['employer.employerProfile', 'jobPost']) ->latest(); $total = $query->count(); $offset = ($page - 1) * $perPage; $reviews = $query->skip($offset)->take($perPage)->get() ->map(function ($rev) { $editPeriodDays = (int) config('reminders.review_edit_period_days', 7); $editable = !$rev->created_at->addDays($editPeriodDays)->isPast(); return [ 'id' => $rev->id, 'employer_id' => $rev->employer_id, 'employer_name' => $rev->employer->name ?? 'Employer', 'job_id' => $rev->job_id, 'job_title' => $rev->jobPost->title ?? null, 'rating' => $rev->rating, 'title' => $rev->title, 'comment' => $rev->comment, 'editable' => $editable, 'created_at' => $rev->created_at->toIso8601String(), 'updated_at' => $rev->updated_at->toIso8601String(), ]; }); return response()->json([ 'success' => true, 'data' => [ 'reviews' => $reviews, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int) ceil($total / $perPage)), ] ] ], 200); } catch (\Exception $e) { logger()->error('Worker Get Employer Reviews Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while fetching reviews.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } }