db = $db; $this->configModel = $configModel; $this->studentClassModel = $studentClassModel; $this->classSectionModel = $classSectionModel; $this->userModel = $userModel; } public function buildReport(string $semester, string $schoolYear, array $lowProgressSectionIds = []): array { $this->schoolYear = $schoolYear; $this->semester = $semester; $semesterResolver = new SemesterRangeService($this->configModel); $semesterNorm = $semesterResolver->normalizeSemester($semester); $semesterFilter = $semesterNorm !== '' ? $semesterNorm : $semester; $semesterCandidates = $this->buildSemesterCandidates($semesterFilter); $scoreComments = new ScoreCommentModel(); $semesterScores = new SemesterScoreModel(); $attendanceDays = new AttendanceDayModel(); $examDrafts = new ExamDraftModel(); $homeworkModel = new HomeworkModel(); $historyModel = new TeacherSubmissionNotificationHistoryModel(); $assignmentQuery = $this->db->table('teacher_class tc') ->select([ 'tc.class_section_id', 'cs.class_section_name', 'tc.teacher_id', 'u.firstname', 'u.lastname', 'tc.position', ]) ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left') ->join('users u', 'u.id = tc.teacher_id', 'left') ->orderBy('cs.class_section_name', 'ASC'); // teacher_class assignments are scoped by school year only. // The table has no semester column; semester filtering belongs on // semester-specific records such as scores, comments, attendance, // homework, and exam drafts. if ($schoolYear !== '') { $assignmentQuery->where('tc.school_year', $schoolYear); } $assignmentRows = $assignmentQuery->get()->getResultArray(); $studentCounts = $this->studentClassModel->getStudentCountsBySection($schoolYear !== '' ? $schoolYear : null); $sectionRows = $this->classSectionModel ->select('class_section_id, class_section_name') ->orderBy('class_section_name', 'ASC') ->findAll(); $sectionMap = []; foreach ($sectionRows as $sectionRow) { $sectionId = (int) ($sectionRow['class_section_id'] ?? 0); if ($sectionId <= 0) { continue; } if (empty($studentCounts[$sectionId])) { continue; } $sectionMap[$sectionId] = $sectionRow['class_section_name'] ?? "Section {$sectionId}"; } $sectionIds = array_keys($sectionMap); [$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds); $examDraftCounts = []; $examDraftDeadline = $this->resolveTeacherDashboardExamDraftDeadline($semester, $schoolYear); $examDraftDeadlineConfig = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? '')); $examDraftDeadlineFormatted = ''; if ($examDraftDeadlineConfig !== '') { $parsedUi = $this->parseExamDraftDeadlineConfigValue(); $examDraftDeadlineFormatted = $parsedUi !== null ? $parsedUi->format('M j, Y') : ''; } $homeworkCounts = []; if (! empty($sectionIds)) { $draftBuilder = $examDrafts ->select('class_section_id') ->whereIn('class_section_id', $sectionIds); if ($schoolYear !== '') { $draftBuilder->where('school_year', $schoolYear); } if (!empty($semesterCandidates)) { $draftBuilder->whereIn('semester', $semesterCandidates); } if ($this->db->fieldExists('is_legacy', 'exam_drafts')) { $draftBuilder->where('is_legacy', 0); } $draftRows = $draftBuilder->findAll(); foreach ($draftRows as $draft) { $sectionId = (int) ($draft['class_section_id'] ?? 0); if ($sectionId <= 0) { continue; } $examDraftCounts[$sectionId] = ($examDraftCounts[$sectionId] ?? 0) + 1; } $homeworkBuilder = $homeworkModel ->select('class_section_id, homework_index') ->whereIn('class_section_id', $sectionIds); if ($schoolYear !== '') { $homeworkBuilder->where('school_year', $schoolYear); } if (!empty($semesterCandidates)) { $homeworkBuilder->whereIn('semester', $semesterCandidates); } $homeworkRows = $homeworkBuilder ->where('score IS NOT NULL', null, false) ->where('score !=', '') ->groupBy('class_section_id, homework_index') ->findAll(); foreach ($homeworkRows as $row) { $sectionId = (int) ($row['class_section_id'] ?? 0); if ($sectionId <= 0) { continue; } $homeworkCounts[$sectionId] = ($homeworkCounts[$sectionId] ?? 0) + 1; } } if (empty($lowProgressSectionIds)) { $lowProgressSectionIds = $this->resolveLowProgressSectionIds($sectionIds); } $teachersBySection = []; foreach ($assignmentRows as $assignment) { $sectionId = (int)($assignment['class_section_id'] ?? 0); if ($sectionId <= 0) { continue; } $positionKey = strtolower(trim((string)($assignment['position'] ?? ''))); $roleKey = $positionKey !== '' ? $positionKey : 'teacher'; $positionLabel = match ($roleKey) { 'ta' => 'TA', 'main' => 'Main', default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher', }; $teacherFullName = trim(($assignment['firstname'] ?? '') . ' ' . ($assignment['lastname'] ?? '')); $teacherId = (int)($assignment['teacher_id'] ?? 0); if ($teacherFullName === '' || $teacherId <= 0) { continue; } $entry = &$teachersBySection[$sectionId]; if (!isset($entry)) { $entry = [ 'class_section' => $assignment['class_section_name'] ?? ($sectionMap[$sectionId] ?? "Section {$sectionId}"), 'teachers' => [], ]; } $entry['teachers'][] = [ 'id' => $teacherId, 'label' => "{$positionLabel}: {$teacherFullName}", 'role_key' => $roleKey, ]; unset($entry); } $today = (new \DateTimeImmutable('now', new \DateTimeZone(date_default_timezone_get() ?: 'UTC')))->format('Y-m-d'); $rows = []; $totalStatuses = 0; $missingItemCount = 0; $allTeacherIds = []; $allClassSectionIds = []; $examTerm = $this->resolveExamTermLabel($semester); $examScoreField = $examTerm === 'final' ? 'final_exam_score' : 'midterm_exam_score'; foreach ($sectionMap as $classSectionId => $sectionName) { $classSectionId = (int)$classSectionId; if ($classSectionId <= 0) { continue; } $studentEntries = $this->db->table('student_class') ->select('student_id') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->get() ->getResultArray(); if (empty($studentEntries)) { $studentEntries = $this->studentClassModel ->select('student_id') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->findAll(); } $studentIds = array_filter(array_map(static fn($entry) => (int)($entry['student_id'] ?? 0), $studentEntries)); $expected = count($studentIds); $midtermStudents = []; $participationStudents = []; if ($classSectionId > 0) { $scoreQuery = $semesterScores ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear); if (!empty($semesterCandidates)) { $scoreQuery->whereIn('semester', $semesterCandidates); } $scoreRecords = $scoreQuery->findAll(); foreach ($scoreRecords as $score) { $sid = (int)($score['student_id'] ?? 0); if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) { continue; } $midtermValue = trim((string)($score[$examScoreField] ?? '')); if ($midtermValue !== '') { $midtermStudents[$sid] = true; } $participationValue = trim((string)($score['participation_score'] ?? '')); if ($participationValue !== '') { $participationStudents[$sid] = true; } } } $midtermCommentStudents = []; $ptapCommentStudents = []; if (!empty($studentIds)) { $commentQuery = $scoreComments ->select('student_id, score_type, comment') ->whereIn('student_id', $studentIds) ->where('school_year', $schoolYear) ->whereIn('score_type', [$examTerm, 'ptap']); if (!empty($semesterCandidates)) { $commentQuery->whereIn('semester', $semesterCandidates); } $comments = $commentQuery->findAll(); foreach ($comments as $comment) { $sid = (int)($comment['student_id'] ?? 0); if ($sid <= 0) { continue; } $text = trim((string)($comment['comment'] ?? '')); if ($text === '') { continue; } $type = strtolower(trim((string)($comment['score_type'] ?? ''))); if ($type === $examTerm) { $midtermCommentStudents[$sid] = true; } if ($type === 'ptap') { $ptapCommentStudents[$sid] = true; } } } $attendanceQuery = $attendanceDays ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('date', $today); if (!empty($semesterCandidates)) { $attendanceQuery->whereIn('semester', $semesterCandidates); } $attendanceRow = $attendanceQuery->first(); $attendanceSubmitted = $attendanceRow && in_array(strtolower((string)($attendanceRow['status'] ?? '')), ['submitted', 'published', 'finalized'], true); $section = $teachersBySection[$classSectionId] ?? ['teachers' => []]; $teacherList = $section['teachers'] ?? []; if (!empty($teacherList)) { usort($teacherList, function ($a, $b) { return $this->teacherRolePriority($a['role_key'] ?? 'teacher') <=> $this->teacherRolePriority($b['role_key'] ?? 'teacher'); }); $teacherList = array_values($teacherList); } foreach ($teacherList as $teacherEntry) { if (!empty($teacherEntry['id'])) { $allTeacherIds[] = $teacherEntry['id']; } } $allClassSectionIds[] = $classSectionId; $midtermScoreStatus = $this->submissionStatus(count($midtermStudents), $expected); $midtermCommentStatus = $this->submissionStatus(count($midtermCommentStudents), $expected); $participationStatus = $this->submissionStatus(count($participationStudents), $expected); $ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected); $attendanceStatus = $this->attendanceStatus($attendanceSubmitted); $progressSubmitted = (int) ($progressSubmittedBySection[$classSectionId] ?? 0); $classProgressStatus = $this->progressStatus($progressSubmitted, $progressExpectedWeeks); $draftSubmitted = (int) ($examDraftCounts[$classSectionId] ?? 0); $examDraftStatus = $this->draftStatus($draftSubmitted, $examDraftDeadline); $homeworkSubmitted = (int) ($homeworkCounts[$classSectionId] ?? 0); $homeworkStatus = $this->homeworkStatus($homeworkSubmitted); $statusDetails = [ 'midterm_score_status' => $midtermScoreStatus, 'midterm_comment_status' => $midtermCommentStatus, 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, 'class_progress_status' => $classProgressStatus, 'exam_draft_status' => $examDraftStatus, 'homework_status' => $homeworkStatus, ]; $missingItemsForSection = $this->buildMissingItems($statusDetails, $semester); $missingItemCount += count($missingItemsForSection); $totalStatuses += count($statusDetails); $rows[] = [ 'class_section' => $sectionMap[$classSectionId] ?? ($section['class_section'] ?? "Section {$classSectionId}"), 'class_section_id' => $classSectionId, 'teachers' => $teacherList, 'midterm_score_status' => $midtermScoreStatus, 'midterm_comment_status' => $midtermCommentStatus, 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, 'attendance_status' => $attendanceStatus, 'class_progress_status' => $classProgressStatus, 'exam_draft_status' => $examDraftStatus, 'homework_status' => $homeworkStatus, 'missing_items' => $missingItemsForSection, 'student_count' => $expected, ]; } $historyMap = []; $teacherIds = array_values(array_unique($allTeacherIds)); $classSectionIds = array_values(array_unique($allClassSectionIds)); if (!empty($teacherIds) && !empty($classSectionIds)) { $historyRecords = $historyModel ->select('teacher_submission_notification_history.*, u.firstname, u.lastname') ->join('users u', 'u.id = teacher_submission_notification_history.admin_id', 'left') ->where('notification_category', 'teacher_submissions') ->whereIn('teacher_submission_notification_history.teacher_id', $teacherIds) ->whereIn('teacher_submission_notification_history.class_section_id', $classSectionIds) ->orderBy('sent_at', 'DESC') ->findAll(); foreach ($historyRecords as $record) { $sectionId = (int)($record['class_section_id'] ?? 0); $teacherId = (int)($record['teacher_id'] ?? 0); if ($sectionId <= 0 || $teacherId <= 0) { continue; } $sentAt = $record['sent_at'] ?? null; $sentAtText = $sentAt ? local_datetime($sentAt, 'M j, Y g:i A') : ''; $adminName = trim(($record['firstname'] ?? '') . ' ' . ($record['lastname'] ?? '')); if ($adminName === '') { $adminName = 'Administrator'; } $historyMap[$sectionId][$teacherId][] = [ 'sent_at_text' => $sentAtText, 'admin_name' => $adminName, 'status' => strtolower((string)($record['status'] ?? 'sent')), ]; } foreach ($historyMap as &$teachersHistory) { foreach ($teachersHistory as &$entries) { $entries = array_slice($entries, 0, 3); } unset($entries); } unset($teachersHistory); } $summary = [ 'total_items' => $totalStatuses, 'missing_items' => $missingItemCount, 'submitted_items' => max(0, $totalStatuses - $missingItemCount), 'submission_percentage' => $totalStatuses > 0 ? (int)round((($totalStatuses - $missingItemCount) / $totalStatuses) * 100) : 100, ]; return [ 'rows' => $rows, 'semester' => $semester, 'schoolYear' => $schoolYear, 'notificationHistory' => $historyMap, 'summary' => $summary, 'lowProgressSectionIds' => $lowProgressSectionIds, 'examDraftDeadlineConfig' => $examDraftDeadlineConfig, 'examDraftDeadlineFormatted' => $examDraftDeadlineFormatted, ]; } public function sendNotifications(array $post, string $semester, int $adminId): array { $notify = $post['notify'] ?? null; if (!is_array($notify)) { return ['redirect' => 'back', 'type' => 'info', 'message' => 'Select at least one teacher to notify.']; } $semester = (string)(getSemester() ?? $this->semester ?? ''); $missingItemsPayload = $post['missing_items'] ?? []; $homeworkNotifyAll = (bool) ($post['homework_notify_all'] ?? false); $examTerm = $this->resolveExamTermLabel($semester); $examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores'; $examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments'; $forcedItems = []; if (!empty($post['notify_midterm_score'])) { $forcedItems[] = $examScoreLabel; } if (!empty($post['notify_midterm_comment'])) { $forcedItems[] = $examCommentLabel; } if (!empty($post['notify_participation'])) { $forcedItems[] = 'participation'; } if (!empty($post['notify_ptap_comment'])) { $forcedItems[] = 'PTAP comments'; } if (!empty($post['notify_class_progress'])) { $forcedItems[] = 'class progress'; } if (!empty($post['notify_exam_draft'])) { $forcedItems[] = 'exam draft'; } $targets = []; foreach ($notify as $sectionIdRaw => $teachers) { $sectionId = (int)$sectionIdRaw; if ($sectionId <= 0 || !is_array($teachers)) { continue; } foreach ($teachers as $teacherIdRaw => $value) { $teacherId = (int)$teacherIdRaw; if ($teacherId <= 0 || $value === null || $value === '') { continue; } $key = "{$sectionId}_{$teacherId}"; $targets[$key] = [ 'class_section_id' => $sectionId, 'teacher_id' => $teacherId, ]; } } if (empty($targets)) { return ['redirect' => 'back', 'type' => 'info', 'message' => 'Select at least one teacher to notify.']; } $targets = array_values($targets); $teacherIds = array_values(array_unique(array_column($targets, 'teacher_id'))); $classSectionIds = array_values(array_unique(array_column($targets, 'class_section_id'))); $classSections = $this->classSectionModel ->select('class_section_id, class_section_name') ->whereIn('class_section_id', $classSectionIds) ->findAll(); $classSectionMap = []; foreach ($classSections as $section) { $classSectionMap[(int)($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? ''; } $teachers = $this->userModel ->select('id, firstname, lastname, email') ->whereIn('id', $teacherIds) ->findAll(); $teacherLookup = []; foreach ($teachers as $teacher) { $teacherLookup[(int)$teacher['id']] = $teacher; } $mailer = new EmailController(); if ($adminId <= 0) { return ['redirect' => 'login', 'type' => 'error', 'message' => '']; } $adminUser = $this->userModel->find($adminId); $adminName = trim(($adminUser['firstname'] ?? '') . ' ' . ($adminUser['lastname'] ?? '')) ?: 'Administrator'; $historyModel = new TeacherSubmissionNotificationHistoryModel(); $scoreUrl = site_url('/'); $progressUrl = site_url('teacher/progress/history'); $examDraftUrl = site_url('teacher/exam-drafts'); $homeworkUrl = site_url('teacher/addHomework'); $examDraftDeadlineEmailHtml = $this->buildExamDraftDeadlineEmailHtml(); $sentCount = 0; $failCount = 0; foreach ($targets as $target) { $classSectionId = (int)$target['class_section_id']; $teacherId = (int)$target['teacher_id']; $teacher = $teacherLookup[$teacherId] ?? null; $sectionName = $classSectionMap[$classSectionId] ?? "Section {$classSectionId}"; $teacherName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : ''; if ($teacherName === '') { $teacherName = 'Teacher'; } $subject = "Reminder: Complete submissions for {$sectionName}"; $missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? ''; $missingItems = $this->parseMissingItemsPayload((string)$missingPayload); $selectedItems = $forcedItems; if ($homeworkNotifyAll && !in_array('homework', $selectedItems, true)) { $selectedItems[] = 'homework'; } if (!empty($selectedItems)) { $missingItems = array_values(array_unique($selectedItems)); } if (!empty($missingItems)) { $missingText = htmlspecialchars( $this->formatMissingItemsText($missingItems), ENT_QUOTES, 'UTF-8' ); $missingNote = "
Outstanding items: {$missingText}.
"; } else { $missingNote = "Our records show no outstanding submissions for this section, but please verify if anything still needs attention.
"; } $subject = "Reminder: Complete submissions for {$sectionName}"; $progressNote = ''; if (in_array('class progress', $missingItems, true)) { $progressNote = "Class progress submissions can be updated at Teacher Progress History.
"; } $examDraftNote = ''; if (in_array('exam draft', $missingItems, true)) { $semesterLabel = strtolower(trim((string) $semester)); if ($semesterLabel === 'fall') { $draftLabel = 'midterm exam draft'; } elseif ($semesterLabel === 'spring') { $draftLabel = 'final exam draft'; } else { $draftLabel = 'exam draft'; } $examDraftNote = "" . ucfirst($draftLabel) . " submissions can be updated at Teacher Exam Drafts.
" . $examDraftDeadlineEmailHtml; } $homeworkNote = ''; if (in_array('homework', $missingItems, true)) { $homeworkNote = "Homework scores can be submitted at Teacher Homework.
"; } $hasScoreItems = (bool) array_intersect($missingItems, [ 'midterm scores', 'midterm comments', 'final scores', 'final comments', 'participation', 'PTAP comments', 'homework', ]); $nonScoreOnly = ! empty($missingItems) && ! $hasScoreItems; $body = "Dear {$teacherName},
" . "Administration is gently reminding you to wrap up any remaining " . ($nonScoreOnly ? "submissions for {$sectionName}." : "score submissions, comments, and related items for {$sectionName}.") . "
" . $missingNote . $progressNote . $examDraftNote . $homeworkNote . ($nonScoreOnly ? '' : "Visit Teacher Score Submission to address any remaining items.
") . "Thank you,
Al Rahma Administration
Exam draft submission deadline (exam_draft_deadline): '
. "{$display}"
. ($parsed !== null && $rawEsc !== $display ? " (configured value: {$rawEsc})" : '')
. '.