Files
alrahma_sunday_school_api/app/Services/Teachers/TeacherAbsenceService.php
T
2026-06-08 23:30:22 -04:00

294 lines
10 KiB
PHP

<?php
namespace App\Services\Teachers;
use App\Models\StaffAttendance;
use App\Models\TeacherClass;
use App\Models\User;
use App\Services\ApplicationUrlService;
use App\Services\SemesterRangeService;
use App\Services\Staff\StaffTimeOffLinkService;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
class TeacherAbsenceService
{
public function __construct(
private TeacherConfigService $configService,
private SemesterRangeService $semesterRangeService,
private StaffTimeOffLinkService $staffTimeOffLinkService,
private ApplicationUrlService $urls,
) {}
public function formData(int $userId): array
{
$context = $this->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 = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;">'
.'<h3 style="margin:0 0 8px;">New TimeOff Request</h3>'
.'<p style="margin:0 0 12px;">A staff time-off request was submitted from the teacher portal.</p>'
.'<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">'
.'<tr><td><strong>Name</strong></td><td>'.e($fullName).'</td></tr>'
.'<tr><td><strong>Email</strong></td><td>'.e($userEmail).'</td></tr>'
.'<tr><td><strong>Role</strong></td><td>'.e((string) $role).'</td></tr>'
.'<tr><td><strong>Semester</strong></td><td>'.e($semester).'</td></tr>'
.'<tr><td><strong>School Year</strong></td><td>'.e($schoolYear).'</td></tr>'
.'<tr><td><strong>Reason Type</strong></td><td>'.e($reasonType ?: '-').'</td></tr>'
.'<tr><td><strong>Reason</strong></td><td>'.e($reasonText).'</td></tr>'
.'<tr><td><strong>Dates</strong></td><td>'.e($dateList ?: '-').'</td></tr>'
.'<tr><td><strong>Assigned Class(es)</strong></td><td>'.e($assignedText).'</td></tr>'
.'<tr><td><strong>Submitted At</strong></td><td>'.e($submittedAt).'</td></tr>'
.'</table>'
.'</div>';
$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 .= '<p style="margin-top:16px;">Click <a href="'.e($notifyUrl).'">Send confirmation email to '
.e($fullName)
.'</a> so the staff member is notified automatically. This link expires in 14 days.</p>';
$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';
}
}