configService->context(); $semester = (string) ($context['semester'] ?? ''); $schoolYear = (string) ($context['school_year'] ?? ''); $teacher = User::query()->find($userId); $displayName = $teacher ? trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? '')) : 'Teacher'; $existing = StaffAttendance::query() ->where('user_id', $userId) ->when($semester !== '', fn ($q) => $q->where('semester', $semester)) ->when($schoolYear !== '', fn ($q) => $q->where('school_year', $schoolYear)) ->orderByDesc('date') ->get() ->toArray(); return [ 'teacher_name' => $displayName, 'semester' => $semester, 'school_year' => $schoolYear, 'existing' => $existing, 'available_dates' => $this->allowedAbsenceDates($schoolYear), ]; } public function submit(int $userId, array $payload): array { $context = $this->configService->context(); $semester = (string) ($context['semester'] ?? ''); $schoolYear = (string) ($context['school_year'] ?? ''); if ($schoolYear === '') { return [ 'ok' => false, 'message' => 'Semester or school year not configured.', 'status' => 422, ]; } $dates = (array) ($payload['dates'] ?? []); $reasonType = trim((string) ($payload['reason_type'] ?? '')); $reasonText = trim((string) ($payload['reason'] ?? '')); if ($reasonText === '') { return [ 'ok' => false, 'message' => 'Reason is required.', 'status' => 422, ]; } $reasonBase = $reasonType !== '' ? strtolower($reasonType) : ''; $reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText; $allowedSet = array_fill_keys($this->allowedAbsenceDates($schoolYear), true); $roleName = User::getUserRoleName($userId) ?: null; $saved = 0; $invalid = []; $savedDates = []; $dates = array_values(array_unique(array_map('strval', $dates))); foreach ($dates as $date) { $date = trim($date); if ($date === '') { continue; } try { $parsed = Carbon::createFromFormat('Y-m-d', $date); if ($parsed->format('Y-m-d') !== $date || empty($allowedSet[$date])) { $invalid[] = $date; continue; } } catch (\Throwable) { $invalid[] = $date; continue; } $semesterForDate = $this->semesterRangeService->getSemesterForDate($date); if ($semesterForDate === '') { $semesterForDate = $semester; } $record = StaffAttendance::upsertOne( userId: $userId, roleName: $roleName, date: $date, semester: $semesterForDate, schoolYear: $schoolYear, status: StaffAttendance::STATUS_ABSENT, reason: $reason, editorId: $userId ); if ($record) { $saved++; $savedDates[] = $date; } } if (!empty($invalid)) { return [ 'ok' => false, 'message' => 'Invalid dates: ' . implode(', ', $invalid), 'status' => 422, ]; } $this->sendPrincipalNotification( userId: $userId, roleName: $roleName, semester: $semester, schoolYear: $schoolYear, reasonType: $reasonType, reasonText: $reasonText, dates: $dates, savedDates: $savedDates ); return [ 'ok' => true, 'message' => $saved . ' day(s) saved as absent.', 'saved' => $saved, 'dates' => $savedDates, 'status' => 200, ]; } private function allowedAbsenceDates(string $schoolYear): array { $today = Carbon::today(); $startYear = null; $endYear = null; if (preg_match('/^(\d{4})\D+(\d{4})$/', $schoolYear, $m)) { $startYear = (int) $m[1]; $endYear = (int) $m[2]; } else { $cy = (int) now()->format('Y'); $cm = (int) now()->format('n'); if ($cm >= 9) { $startYear = $cy; $endYear = $cy + 1; } else { $startYear = $cy - 1; $endYear = $cy; } } try { $start = Carbon::create($startYear, 9, 1)->startOfDay(); $end = Carbon::create($endYear, 5, 31)->startOfDay(); } catch (\Throwable) { return []; } if ($start->lt($today)) { $start = $today->copy(); } $dates = []; $cursor = $start->copy(); while ($cursor->lte($end)) { if ($cursor->dayOfWeek === Carbon::SUNDAY) { $dates[] = $cursor->format('Y-m-d'); } $cursor->addDay(); } return $dates; } private function sendPrincipalNotification( int $userId, ?string $roleName, string $semester, string $schoolYear, string $reasonType, string $reasonText, array $dates, array $savedDates ): void { try { $user = User::query()->find($userId); $fullName = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) ?: 'Teacher'; $userEmail = $user->email ?? ''; $role = $roleName ?: 'teacher'; $dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); $subject = sprintf( 'TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string) $role), $dateList ?: 'No dates' ); $submittedAt = now()->toDateTimeString(); $assignedText = $this->formatAssignedClasses($userId, $schoolYear, $semester); $body = '
' . '

New TimeOff Request

' . '

A staff time-off request was submitted from the teacher portal.

' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '
Name' . e($fullName) . '
Email' . e($userEmail) . '
Role' . e((string) $role) . '
Semester' . e($semester) . '
School Year' . e($schoolYear) . '
Reason Type' . e($reasonType ?: '-') . '
Reason' . e($reasonText) . '
Dates' . e($dateList ?: '-') . '
Assigned Class(es)' . e($assignedText) . '
Submitted At' . e($submittedAt) . '
' . '
'; $token = $this->staffTimeOffLinkService->createToken([ 'uid' => $userId, 'email' => $userEmail, 'name' => $fullName, 'role' => $role, 'dates' => $dateList ?: '-', 'reason' => $reasonText, 'reason_type' => $reasonType, 'submitted_at' => $submittedAt, 'origin' => 'teacher portal', ]); $notifyUrl = $this->urls->timeoffNotifyUrl($token); $body .= '

Click Send confirmation email to ' . e($fullName) . ' so the staff member is notified automatically. This link expires in 14 days.

'; $principalEmail = env('PRINCIPAL_EMAIL', 'principal@alrahmaisgl.org'); Mail::html($body, function ($message) use ($principalEmail, $subject) { $message->to($principalEmail) ->subject($subject); }); } catch (\Throwable) { // Email failures should not block absence submissions. } } private function formatAssignedClasses(int $userId, string $schoolYear, string $semester): string { $assignments = TeacherClass::getClassAssignmentsByUserId($userId, $schoolYear, $semester); if (empty($assignments)) { return 'No assigned class'; } $parts = []; foreach ($assignments as $assignment) { $name = (string) ($assignment['class_section_name'] ?? ''); $role = (string) ($assignment['teacher_role'] ?? ''); if ($name !== '') { $parts[] = $role !== '' ? ($name . ' (' . $role . ')') : $name; } } return !empty($parts) ? implode(', ', $parts) : 'No assigned class'; } }