File manager - Edit - /var/www/payraty/hris/app/Http/Controllers/AppraisalReviewController.php
Back
<?php namespace App\Http\Controllers; use App\Models\AppraisalFinalScore; use App\Models\User; use App\Models\Employee; use App\Models\Department; use App\Models\RatingType; use App\Models\SimpleGoal; use App\Mail\PeerAddedMail; use App\Models\Competencies; use Illuminate\Http\Request; use App\Models\AppraisalReview; use App\Models\RatingTypeMetric; use App\Models\WeightedCriteria; use App\Models\FinalRatingAction; use App\Mail\AppraisalCreatedMail; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Mail; use App\Models\AppraisalReviewFeedback; use App\Mail\AppraisalReviewReminderMail; use Maatwebsite\Excel\Facades\Excel; use App\Exports\CompensationOutcomesExport; class AppraisalReviewController extends Controller { public function index() { $user = Auth::user(); $orgId = $user->organisation_id; $userIsAdmin = $user->can('manage goals'); // Initialize a base query for Appraisal Reviews, eager load employee data $query = AppraisalReview::with('employee'); // Initialize a flag for employee status $isEmployee = false; if ($userIsAdmin) { // Admins: see all appraisal reviews within their organization $query->whereHas('employee', function ($q) use ($orgId) { $q->where('organisation_id', $orgId); }); } else { // For non-admin users, we need to determine their specific context (employee, manager, peer) $employeeRecord = Employee::where('user_id', $user->id)->first(); if ($employeeRecord) { // Set the isEmployee flag $isEmployee = true; $isManager = false; // Check if the current user manages any departments $managedDepartmentIds = Department::where('manager_id', $employeeRecord->id) ->pluck('id') ->toArray(); if (!empty($managedDepartmentIds)) { $isManager = true; } // Build the main WHERE clause using a nested function to group OR conditions $query->where(function ($q) use ($employeeRecord, $isManager, $managedDepartmentIds) { // Condition 1: Include appraisals where the current user is the employee being reviewed $q->where('employee_id', $employeeRecord->id); // Condition 2: If the user is a manager, include appraisals for employees in their managed departments if ($isManager) { $managedEmployeeIds = Employee::whereIn('department_id', $managedDepartmentIds) ->pluck('id') ->toArray(); $q->orWhereIn('employee_id', $managedEmployeeIds); } // Condition 3: Include appraisals where the current user is a peer $q->orWhereHas('peers', function ($qPeer) use ($employeeRecord) { $qPeer->where('appraisal_review_peers.employee_id', $employeeRecord->id); }); }); } else { // If the user is not an admin and has no employee record, they shouldn't see any appraisals. $query->whereRaw('1 = 0'); } } // Apply ordering and pagination $appraisals = $query->latest()->paginate(20); // Pass the new variable to the view return view('appraisal_reviews.index', compact('appraisals', 'userIsAdmin', 'isEmployee')); } public function compensationOutcome() { $user = Auth::user(); $orgId = $user->organisation_id; $userIsAdmin = $user->can('manage goals'); // Initialize a base query for Appraisal Reviews, eager load employee data // Initialize a flag for employee status $isEmployee = false; $outcomes = AppraisalFinalScore::with(['appraisal_review.employee'])->paginate(20); // Pass the new variable to the view return view('appraisal_reviews.outcomes', compact('outcomes', 'userIsAdmin', 'isEmployee')); } public function export() { return Excel::download(new CompensationOutcomesExport, 'compensation_outcomes.xlsx'); } public function create() { $currentUser = Auth::user(); $orgId = Auth::user()->organisation_id; $employees = Employee::where('organisation_id', $orgId)->pluck('name', 'id'); $goals = SimpleGoal::where('organisation_id', $orgId)->pluck('title', 'id'); $competencies = Competencies::where('organisation_id', $orgId)->pluck('name', 'id'); $ratingTypes = RatingType::pluck('name', 'id'); return view('appraisal_reviews.create', compact('employees', 'goals', 'ratingTypes', 'competencies')); } public function store(Request $r) { $data = $r->validate([ 'appraisal_date' => 'required', 'employee_id' => 'required|exists:employees,id', 'rating_type_id' => 'required|exists:rating_types,id', 'goal_ids' => 'array', 'allow_goal_update' => 'boolean', 'review_type' => 'required|in:90,180,360', ]); $data['status'] = 'on_going'; $data['review_type'] = $r->review_type; $data['created_by'] = Auth::id(); $data['organisation_id'] = Auth::user()->organisation_id; $appraisal = AppraisalReview::create($data); if ($appraisal->employee && $appraisal->employee->user) { try { Mail::to($appraisal->employee->user->email)->send(new AppraisalCreatedMail($appraisal)); } catch (\Exception $e) { Log::error("Failed to send appraisal creation email to {$appraisal->employee->user->email}: " . $e->getMessage()); } } // attach selected goals $goalIds = $r->input('goal_ids', []); $pivotData = []; $compentencyPivotData = []; foreach ($goalIds as $goalId) { $pivotData[$goalId] = ['organisation_id' => Auth::user()->organisation_id]; } // attach competencies $competencyIds = $r->input('competencies', []); Log::info('Competency IDs: ', $competencyIds); foreach ($competencyIds as $competencyId) { $compentencyPivotData[$competencyId] = [ 'weight' => 0, // default weight 'rating' => null, // default rating 'comment' => null, // default comment 'organisation_id' => Auth::user()->organisation_id, ]; } $appraisal->competencies()->sync($compentencyPivotData); $appraisal->goals()->sync($pivotData); /* ------------------------------------------------------ 5) insert feedback rows expected payload shape: ratings[<goalId>][manager_rating|manager_comment|employee_rating|...] ------------------------------------------------------ */ $feedbackInput = $r->input('ratings', []); // array|null $rows = []; foreach ($feedbackInput as $goalId => $values) { foreach (['manager', 'employee', 'admin'] as $role) { $rating = $values["{$role}_rating"] ?? null; $comment = $values["{$role}_comment"] ?? null; // skip completely empty rows if ($rating === null && $comment === null) continue; $rows[] = [ 'appraisal_review_id' => $appraisal->id, 'simple_goal_id' => $goalId, 'giver_role' => $role, // enum('manager','employee','admin') 'rating_value' => $rating, 'comment' => $comment, 'organisation_id' => $data['organisation_id'], 'created_at' => now(), 'updated_at' => now(), ]; } } if ($rows) { AppraisalReviewFeedback::insert($rows); } return redirect()->route('appraisal_reviews.index')->with('success', __('Appraisal created.')); } public function edit(AppraisalReview $appraisalReview) { $this->authorizeOrg($appraisalReview); $currentUser = Auth::user(); $orgId = $currentUser->organisation_id; $employees = Employee::where('organisation_id', $orgId)->pluck('name', 'id'); $availableGoals = SimpleGoal::where('organisation_id', $orgId)->where('is_active', true)->pluck('title', 'id'); $availableCompetencies = Competencies::where('organisation_id', $orgId)->get(['id', 'name']); $ratingTypes = RatingType::where('organisation_id', $orgId)->pluck('name', 'id'); $ratingTypesWithConfig = RatingType::where('organisation_id', $orgId)->pluck('config', 'id')->map(function ($item) { return json_decode($item, true); }); $isManager = false; $employeeRecord = Employee::where('user_id', $currentUser->id)->first(); $userIsAdmin = $currentUser->can('manage goals'); $allowEditAfterFinalScore = ($appraisalReview->final_score === null); //Employee who owns the review $employee = $appraisalReview->employee; $isEmployee = $employeeRecord->id === $appraisalReview->employee_id; //Fetch if employee is a manager if ($employeeRecord) { $managedDepartmentIds = Department::where('manager_id', $employeeRecord->id) ->pluck('id') ->toArray(); $isManager = in_array($employee->department_id, $managedDepartmentIds); } //Check if user is a peer $isPeer = ($appraisalReview->review_type === '360' && $appraisalReview->peers->contains($employeeRecord->id)); $preloadedGoals = $appraisalReview->goals->map(fn($g) => [ 'id' => $g->id, 'title' => $g->title, 'due_date' => $g->due_date, ])->values(); $preloadedCompetencies = $appraisalReview->competencies()->get(); $potentialPeers = collect(); $selectedPeerIds = []; if (($userIsAdmin || $isManager) && $appraisalReview->review_type === '360') { $potentialPeers = User::where('organisation_id', $orgId) ->whereHas('employee') //Only users who have an associated employee record ->when($employee, function ($query) use ($employee) { $query->where('id', '!=', $employee->user_id); }) ->where('id', '!=', $currentUser->id) ->get() ->map(function ($user) { if ($user->employee) { return [ 'id' => $user->employee->id, 'name' => $user->employee->name . ' - ' . (optional($user->employee->department)->name ?? 'N/A') ]; } // Fallback if no employee record (shouldn't happen with whereHas('employee')) return [ 'id' => $user->id, 'name' => $user->name . ' - N/A' ]; }); $selectedPeerIds = $appraisalReview->peers->pluck('id')->toArray(); } // --- NEW: Fetch single peer feedback for the peer form --- $peerFeedback = null; if ($isPeer) { $peerFeedback = AppraisalReviewFeedback::where('appraisal_review_id', $appraisalReview->id) ->where('giver_role', 'peer') ->where('feedback_by', $currentUser->id) ->whereNull('simple_goal_id') ->whereNull('competency_id') ->first(); } $allPeerFeedbacks = []; // Only fetch if the user is not a peer (i.e. manager, admin, employee being reviewed) if (!$isPeer) { $allPeerFeedbacks = AppraisalReviewFeedback::where('appraisal_review_id', $appraisalReview->id) ->where('giver_role', 'peer') ->whereNull('simple_goal_id') ->whereNull('competency_id') ->with('giverUser.employee') ->get() ->map(fn($fb) => [ 'rating_value' => $fb->rating_value, 'comment' => $fb->comment, 'giver_name' => $fb->giverUser->employee->name ?? $fb->giverUser->name ?? 'Unknown Peer', ]); } $feedback = AppraisalReviewFeedback::where('appraisal_review_id', $appraisalReview->id) ->with('giverUser.employee') ->get(); $feedbackByGoalRole = []; $feedbackByCompetencyRole = []; foreach ($feedback as $fb) { $itemId = $fb->simple_goal_id ?? $fb->competency_id; $itemType = $fb->simple_goal_id ? 'goal' : 'competency'; // We only need to process manager/employee/admin feedback here, // as peer feedback is now handled separately. if ($fb->giver_role !== 'peer') { if ($itemType === 'goal' && !isset($feedbackByGoalRole[$itemId])) { $feedbackByGoalRole[$itemId] = []; } elseif ($itemType === 'competency' && !isset($feedbackByCompetencyRole[$itemId])) { $feedbackByCompetencyRole[$itemId] = []; } $feedbackData = [ 'rating_value' => $fb->rating_value, 'comment' => $fb->comment, ]; if ($itemType === 'goal') { $feedbackByGoalRole[$itemId][$fb->giver_role] = $feedbackData; } else { $feedbackByCompetencyRole[$itemId][$fb->giver_role] = $feedbackData; } } } // Log::info('Manager', ['isManager' => $isManager, 'isEmployee' => $isEmployee, 'isPeer' => $isPeer, 'userIsAdmin' => $userIsAdmin, 'currentUser' => $currentUser->id]); Log::info('peers', ['SP' => $selectedPeerIds, 'PP' => $potentialPeers, 'isEmployee' => $isEmployee]); // // Log::info('feedbackByCompetencyRole', $peerFeedbackData); return view('appraisal_reviews.edit', compact( 'appraisalReview', 'employees', 'availableGoals', 'availableCompetencies', 'ratingTypes', 'ratingTypesWithConfig', 'preloadedGoals', 'feedbackByGoalRole', 'employee', 'isEmployee', 'isManager', 'isPeer', 'userIsAdmin', 'preloadedCompetencies', 'feedbackByCompetencyRole', 'allowEditAfterFinalScore', 'potentialPeers', 'selectedPeerIds', 'peerFeedback', 'allPeerFeedbacks' )); } public function update(Request $r, AppraisalReview $appraisalReview) { $this->authorizeOrg($appraisalReview); $currentUser = Auth::user(); $currentUserEmployeeRecord = Employee::where('user_id', $currentUser->id)->first(); // Determine user roles using the same, corrected logic as the edit method $employeeBeingReviewed = $appraisalReview->employee; // Employee who owns the review $isEmployee = ($currentUserEmployeeRecord && $currentUserEmployeeRecord->id === $employeeBeingReviewed->id); $isManager = false; if ($currentUserEmployeeRecord && $employeeBeingReviewed) { $managedDepartmentIds = Department::where('manager_id', $currentUserEmployeeRecord->id) ->pluck('id') ->toArray(); // Current user manages the department the employee being reviewed is in $isManager = in_array($employeeBeingReviewed->department_id, $managedDepartmentIds); } $userIsAdmin = $currentUser->can('manage goals'); // Or 'manage appraisals' $isPeer = ($appraisalReview->review_type === '360' && $appraisalReview->peers->contains($currentUserEmployeeRecord->id)); $data = $r->all(); $appraisalReview->update($data); // This updates the main appraisal review fields (like status, review_type, etc.) // Sync goals and competencies first, as they define the items for feedback // Attach selected goals $goalIds = $r->input('goal_ids', []); $pivotData = []; foreach ($goalIds as $goalId) { $pivotData[$goalId] = ['organisation_id' => Auth::user()->organisation_id]; } $appraisalReview->goals()->sync($pivotData); // Sync selected competencies $competencyIds = $r->input('competency_ids', []); $pivotDataCompetencies = []; foreach ($competencyIds as $competencyId) { $pivotDataCompetencies[$competencyId] = ['organisation_id' => Auth::user()->organisation_id]; } $appraisalReview->competencies()->sync($pivotDataCompetencies); $ratingsInput = $r->input('ratings', []); $originalPeerIds = $appraisalReview->peers->pluck('id')->toArray(); // Only allow Admin or Manager to sync peers, and only if review type is 360 if (($userIsAdmin || $isManager) && $appraisalReview->review_type === '360') { $peerEmployeeIds = $r->peer_ids ?? []; $appraisalReview->peers()->sync( collect($peerEmployeeIds) ->mapWithKeys(fn($id) => [$id => ['organisation_id' => Auth::user()->organisation_id]]) ); $newPeerIds = array_diff($peerEmployeeIds, $originalPeerIds); if (!empty($newPeerIds)) { $newPeers = Employee::whereIn('id', $newPeerIds)->with('user')->get(); foreach ($newPeers as $peer) { if ($peer->user) { try { Mail::to($peer->user->email)->send(new PeerAddedMail($appraisalReview, $peer)); } catch (\Exception $e) { Log::error("Failed to send peer added email to {$peer->user->email}: " . $e->getMessage()); } } } } } elseif ($appraisalReview->review_type !== '360') { // If review type is changed from 360, clear existing peers $appraisalReview->peers()->detach(); } // Process Goal Feedback if (isset($ratingsInput['goal'])) { foreach ($ratingsInput['goal'] as $goalId => $values) { // Handle Manager, Employee, Admin feedback for goals foreach (['manager', 'employee', 'admin'] as $role) { // Check if current user has permission to update this role's feedback if ( ($role === 'manager' && !$isManager) || ($role === 'employee' && !$isEmployee) || ($role === 'admin' && !$userIsAdmin) ) { continue; // Skip if user doesn't have permission for this role's feedback } $rating = $values["{$role}_rating"] ?? null; $comment = $values["{$role}_comment"] ?? null; // If both rating and comment are empty, and there's no existing record, we don't create one. // If they are empty and there IS an existing record, we update it with nulls or remove it. // For manager/employee/admin, typically we'd update with nulls if cleared. if ($rating === null && $comment === null) { // Optionally: Delete the feedback if cleared AppraisalReviewFeedback::where('appraisal_review_id', $appraisalReview->id) ->where('simple_goal_id', $goalId) ->where('giver_role', $role) ->delete(); continue; // Move to next role/item } AppraisalReviewFeedback::updateOrCreate( [ 'appraisal_review_id' => $appraisalReview->id, 'simple_goal_id' => $goalId, 'competency_id' => null, // Ensure correct column is null 'giver_role' => $role, 'feedback_by' => null, // Standard roles usually don't have a specific giver_id 'organisation_id' => Auth::user()->organisation_id, ], [ 'rating_value' => $rating, 'comment' => $comment, // created_at/updated_at handled by Laravel's Eloquent ] ); } } } // Process Competency Feedback (similar logic as goals) if (isset($ratingsInput['competency'])) { foreach ($ratingsInput['competency'] as $competencyId => $values) { // Handle Manager, Employee, Admin feedback for competencies foreach (['manager', 'employee', 'admin'] as $role) { // Check if current user has permission to update this role's feedback if ( ($role === 'manager' && !$isManager) || ($role === 'employee' && !$isEmployee) || ($role === 'admin' && !$userIsAdmin) ) { continue; // Skip if user doesn't have permission for this role's feedback } // return [$ratingsInput['competency'], $role, $competencyId]; $rating = $values["{$role}_rating"] ?? null; $comment = $values["{$role}_comment"] ?? null; if ($rating === null && $comment === null) { // Optionally: Delete the feedback if cleared AppraisalReviewFeedback::where('appraisal_review_id', $appraisalReview->id) ->where('competency_id', $competencyId) ->where('giver_role', $role) ->delete(); continue; } AppraisalReviewFeedback::updateOrCreate( [ 'appraisal_review_id' => $appraisalReview->id, 'simple_goal_id' => null, 'competency_id' => $competencyId, 'giver_role' => $role, 'feedback_by' => null, 'organisation_id' => Auth::user()->organisation_id, ], [ 'rating_value' => $rating, 'comment' => $comment, ] ); } } } if ($isPeer && $appraisalReview->review_type === '360') { $rating = $r->input('peer_rating') ?? null; $comment = $r->input('peer_comment') ?? null; // This is the crucial update: we update a single record AppraisalReviewFeedback::updateOrCreate( [ 'appraisal_review_id' => $appraisalReview->id, 'giver_role' => 'peer', 'feedback_by' => $currentUser->id, 'organisation_id' => Auth::user()->organisation_id, 'simple_goal_id' => null, // Ensure this record is not tied to a goal 'competency_id' => null, // Ensure this record is not tied to a competency ], [ 'rating_value' => $rating, 'comment' => $comment, ] ); } // Log::info('All Feedback Rows: ', $allFeedbackRows); // No longer needed as we're not batch inserting // if ($allFeedbackRows) { // AppraisalReviewFeedback::insert($allFeedbackRows); // REMOVED // } return redirect() ->route('appraisal_reviews.index') ->with('success', __('Appraisal updated.')); } public function destroy(AppraisalReview $appraisalReview) { $this->authorizeOrg($appraisalReview); $appraisalReview->delete(); // pivot rows cascade if FK with cascadeOnDelete return redirect() ->route('appraisal_reviews.index') ->with('success', __('Appraisal deleted.')); } /* ---------------------------------------------------- | Helper: ensure appraisal reviews belongs to same organisation * ---------------------------------------------------- */ protected function authorizeOrg(AppraisalReview $appraisal): void { $orgId = Auth::user()->organisation_id; if ($appraisal->employee->organisation_id !== $orgId) { abort(403, __('Permission denied.')); } } public function getGoalsForEmployee($employeeId) { $orgId = Auth::user()->organisation_id; $goals = SimpleGoal::where('organisation_id', $orgId) ->whereHas('employees', function ($q) use ($employeeId) { $q->where('simple_goal_employees.employee_id', $employeeId); }) ->select('id', 'title', 'due_date', 'status') ->get(); return response()->json($goals); } public function closeAppraisal(AppraisalReview $appraisalReview) { $this->authorizeOrg($appraisalReview); $user = Auth::user(); $orgId = $user->organisation_id; if ($appraisalReview->status !== 'on_going') { return redirect()->back()->with('error', __('Appraisal review cannot be closed as it is not in an "on-going" status.')); } $finalScore = 'ungraded'; // Ensure only authorized users can finalize the review if (!Auth::user()->can('manage goals')) { return redirect()->back()->with('error', 'You are not authorized to finalize this appraisal.'); } // Get the manager's goal feedback ratings $managerGoalFeedbacks = $appraisalReview->feedback() ->where('giver_role', 'manager') ->whereNotNull('simple_goal_id') ->whereNull('competency_id') ->with('simpleGoal') ->get(); $totalScore = 0; $totalWeight = 0; // Use the rating type associated with the appraisal review itself $ratingType = $appraisalReview->ratingType; $ratingTypeId = $ratingType ? $ratingType->id : null; if (!$ratingTypeId) { return redirect()->back()->with('error', 'The appraisal review does not have a rating type assigned.'); } DB::beginTransaction(); try { foreach ($managerGoalFeedbacks as $feedback) { $ratingValue = (float) $feedback->rating_value; $goalWeight = (float) $feedback->simpleGoal->weight; $metric = null; // Handle different rating types if ($ratingType->type === 'numeric' || $ratingType->type === 'percentage') { // For range-based ratings, find the metric where the rating value falls within the range $metrics = RatingTypeMetric::where('rating_type_id', $ratingTypeId)->get(); foreach ($metrics as $m) { [$min, $max] = explode('-', $m->rating_value); if ($ratingValue >= (float) $min && $ratingValue <= (float) $max) { $metric = $m; break; } } if (!$metric) { $metric = $ratingValue; } } else { // For text-based ratings, find the metric by direct match $metric = RatingTypeMetric::where('rating_type_id', $ratingTypeId) ->where('rating_value', $feedback->rating_value) ->where('organisation_id', $orgId) ->first(); } // If a metric is found, calculate the score for this goal if ($metric) { // Goal Score = (Metric Value / 100) * Goal Weight $goalScore = ($metric->metric_value ?? $metric / 100) * $goalWeight; $totalScore += $goalScore; $totalWeight += $goalWeight; } else { Log::warning("No metric found for rating_type_id: {$ratingTypeId} and rating_value: {$ratingValue}"); } } // return $totalWeight; $finalScore = ($totalWeight > 0) ? ($totalScore / $totalWeight) * 100 : 0; $finalScore = round($finalScore, 2); $appraisalReview->update([ 'status' => 'closed', 'final_score' => $finalScore, 'allow_goal_update' => false, ]); DB::commit(); return redirect()->route('appraisal_reviews.index')->with('success', 'Appraisal finalized and final score calculated.'); } catch (\Exception $e) { DB::rollback(); Log::error("Failed to finalize appraisal: " . $e->getMessage()); return redirect()->back()->with('error', 'An error occurred while finalizing the appraisal.'); } } public function processFinalScore(AppraisalReview $appraisalReview) { // Find the matching final rating action based on the calculated score $finalRatingAction = FinalRatingAction::where('min_score', '<=', $appraisalReview->final_score) ->where('max_score', '>=', $appraisalReview->final_score) ->first(); if (!$finalRatingAction) { return response()->json(['error' => 'No final action policy found for this score.'], 404); } return view('appraisal_final_scores.create', compact('appraisalReview', 'finalRatingAction')); } public function createNotificationModal() { $user = Auth::user(); $userIsAdmin = $user->can('manage goals'); $isManager = false; if ($userIsAdmin) { $departments = Department::where('organisation_id', $user->organisation_id)->pluck('name', 'id'); return view('appraisal_reviews.notify_modal', compact('userIsAdmin', 'departments', 'isManager')); } $employeeRecord = Employee::where('user_id', $user->id)->first(); if ($employeeRecord && $employeeRecord->department) { $isManager = Department::where('manager_id', $employeeRecord->id)->exists(); if ($isManager) { $department = $employeeRecord->department; $employees = Employee::where('department_id', $department->id)->get(); return view('appraisal_reviews.notify_modal', compact('isManager', 'department', 'employees', 'userIsAdmin')); } } return response()->view('appraisal_reviews.notify_modal', compact('userIsAdmin', 'isManager'), 403); } /** * Send appraisal review reminder notifications. */ public function sendNotifications(Request $request) { $user = Auth::user(); $userIsAdmin = $user->can('manage goals'); $recipients = collect(); if ($userIsAdmin) { $type = $request->input('type'); if ($type === 'all_employees') { $recipients = Employee::where('organisation_id', $user->organisation_id)->get(); } elseif ($type === 'departments' && $request->has('departments')) { $departments = $request->input('departments'); $recipients = Employee::whereIn('department_id', $departments)->get(); } } $employeeRecord = Employee::where('user_id', $user->id)->first(); if ($employeeRecord && $employeeRecord->department) { $isManager = Department::where('manager_id', $employeeRecord->id)->exists(); if ($isManager) { $recipients = Employee::where('department_id', $employeeRecord->department->id)->get(); } } if ($recipients->isEmpty()) { return redirect()->back()->with('error', 'No employees were selected to receive notifications.'); } foreach ($recipients as $employee) { if ($employee->user) { try { Mail::to($employee->user->email)->send(new AppraisalReviewReminderMail($employee)); Log::info('Mail Sent', [$employee]); } catch (\Exception $e) { \Log::error("Failed to send review reminder to {$employee->user->email}: " . $e->getMessage()); } } } return redirect()->back()->with('success', 'Appraisal review reminders have been sent successfully!'); } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.30 | Generation time: 0.51 |
proxy
|
phpinfo
|
Settings