293 lines
10 KiB
PHP
293 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Teachers;
|
|
|
|
use App\Services\ApplicationUrlService;
|
|
use App\Services\Staff\StaffTimeOffLinkService;
|
|
use App\Models\StaffAttendance;
|
|
use App\Models\TeacherClass;
|
|
use App\Models\User;
|
|
use App\Services\SemesterRangeService;
|
|
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';
|
|
}
|
|
}
|