diff --git a/app/Commands/SendExamDraftDeadlineReminders.php b/app/Commands/SendExamDraftDeadlineReminders.php new file mode 100644 index 0000000..7adfe80 --- /dev/null +++ b/app/Commands/SendExamDraftDeadlineReminders.php @@ -0,0 +1,197 @@ +getConfig('school_year') ?? ''); + $semester = (string) ($configModel->getConfig('semester') ?? ''); + $deadlineValue = trim((string) ($configModel->getConfig('exam_draft_deadline') ?? '')); + if ($deadlineValue === '') { + CLI::write('exam_draft_deadline is not configured.', 'yellow'); + return; + } + + $tz = new \DateTimeZone(config('App')->appTimezone ?? 'UTC'); + try { + $deadline = new \DateTimeImmutable($deadlineValue, $tz); + } catch (\Throwable $e) { + CLI::write('Invalid exam_draft_deadline value.', 'red'); + return; + } + $deadline = $deadline->setTime(0, 0, 0); + $today = new \DateTimeImmutable('now', $tz); + $today = $today->setTime(0, 0, 0); + + if ($today > $deadline) { + CLI::write('Deadline has passed. No reminders sent.', 'yellow'); + return; + } + + $daysToDeadline = (int) $today->diff($deadline)->format('%r%a'); + if (!$this->shouldSendOnDay($daysToDeadline)) { + CLI::write('No reminder scheduled for today.', 'yellow'); + return; + } + + $db = Database::connect(); + $teacherClassRows = $db->table('teacher_class') + ->select('teacher_id, class_section_id, school_year, semester') + ->when($schoolYear !== '', static function ($builder) use ($schoolYear) { + return $builder->where('school_year', $schoolYear); + }) + ->when($semester !== '', static function ($builder) use ($semester) { + return $builder->where('semester', $semester); + }) + ->get() + ->getResultArray(); + + if (empty($teacherClassRows)) { + CLI::write('No teacher assignments found.', 'yellow'); + return; + } + + $draftTable = 'exam_drafts'; + $fields = $db->getFieldNames($draftTable); + $teacherColumn = in_array('teacher_id', $fields, true) ? 'teacher_id' : 'author_id'; + $hasStatusColumn = in_array('status', $fields, true); + $hasIsLegacyColumn = in_array('is_legacy', $fields, true); + + $draftsQuery = $db->table($draftTable) + ->select("{$teacherColumn} AS teacher_id, class_section_id") + ->when($schoolYear !== '', static function ($builder) use ($schoolYear) { + return $builder->where('school_year', $schoolYear); + }) + ->when($semester !== '', static function ($builder) use ($semester) { + return $builder->where('semester', $semester); + }); + + if ($hasStatusColumn) { + $draftsQuery = $draftsQuery->where('status !=', 'legacy'); + } + if ($hasIsLegacyColumn) { + $draftsQuery = $draftsQuery->where('is_legacy', 0); + } + + $draftRows = $draftsQuery->get()->getResultArray(); + $submittedMap = []; + foreach ($draftRows as $row) { + $key = (int) ($row['teacher_id'] ?? 0) . '|' . (int) ($row['class_section_id'] ?? 0); + $submittedMap[$key] = true; + } + + $missingByTeacher = []; + foreach ($teacherClassRows as $row) { + $teacherId = (int) ($row['teacher_id'] ?? 0); + $classSectionId = (int) ($row['class_section_id'] ?? 0); + if ($teacherId <= 0 || $classSectionId <= 0) { + continue; + } + $key = $teacherId . '|' . $classSectionId; + if (!isset($submittedMap[$key])) { + $missingByTeacher[$teacherId][] = $classSectionId; + } + } + + if (empty($missingByTeacher)) { + CLI::write('All teachers have submitted drafts.', 'green'); + return; + } + + $userModel = new UserModel(); + $classSectionModel = new ClassSectionModel(); + $historyModel = new TeacherSubmissionNotificationHistoryModel(); + $mailer = new EmailController(); + + $classSections = $classSectionModel + ->select('class_section_id, class_section_name') + ->findAll(); + $classLookup = []; + foreach ($classSections as $section) { + $classLookup[(int) ($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? ''; + } + + $todayStamp = $today->format('Y-m-d'); + foreach ($missingByTeacher as $teacherId => $sectionIds) { + $teacher = $userModel->find($teacherId); + $email = $teacher['email'] ?? ''; + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + continue; + } + + $alreadySent = $historyModel + ->where('teacher_id', $teacherId) + ->where('notification_category', 'exam_draft_deadline') + ->where('school_year', $schoolYear) + ->where('semester', $semester) + ->like('sent_at', $todayStamp) + ->countAllResults() > 0; + if ($alreadySent) { + continue; + } + + $classNames = array_map(static function ($id) use ($classLookup) { + return $classLookup[(int) $id] ?? "Class {$id}"; + }, $sectionIds); + $classList = implode(', ', $classNames); + $teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')); + if ($teacherName === '') { + $teacherName = 'Teacher'; + } + + $subject = 'Reminder: Exam draft submission deadline'; + $body = '

Dear ' . esc($teacherName) . ',

' + . '

This is a reminder to submit your exam draft(s) for: ' . esc($classList) . '.

' + . '

Deadline: ' . esc($deadline->format('Y-m-d')) . '

' + . '

Please submit your draft at Teacher Exam Drafts.

' + . '

Thank you.

'; + + $status = 'failed'; + if ($mailer->sendEmail($email, $subject, $body, 'notifications')) { + $status = 'sent'; + CLI::write("Sent reminder to {$email}", 'green'); + } + + $historyModel->insert([ + 'teacher_id' => $teacherId, + 'class_section_id' => 0, + 'admin_id' => null, + 'notification_category' => 'exam_draft_deadline', + 'message' => strip_tags($body), + 'status' => $status, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'sent_at' => utc_now(), + ]); + } + } + + private function shouldSendOnDay(int $daysToDeadline): bool + { + if ($daysToDeadline < 0) { + return false; + } + if ($daysToDeadline <= 4) { + return true; + } + return in_array($daysToDeadline, [28, 21, 14, 7], true); + } +}