diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 1b3eb7c..57a2926 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -405,6 +405,8 @@ $routes->get('teacher/progress/submit', 'ClassProgressController::create', ['fil $routes->post('teacher/progress/store', 'ClassProgressController::store', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/history', 'ClassProgressController::history', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/view/(:num)', 'ClassProgressController::view/$1', ['filter' => 'auth:teacher,teacher_assistant']); +$routes->get('teacher/progress/edit/(:num)', 'ClassProgressController::edit/$1', ['filter' => 'auth:teacher,teacher_assistant']); +$routes->post('teacher/progress/update/(:num)', 'ClassProgressController::update/$1', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/attachment/(:num)', 'ClassProgressController::attachment/$1', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/attachment-file/(:num)', 'ClassProgressController::attachmentFile/$1', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('parent/progress', 'ParentProgressController::index', ['filter' => 'auth:parent']); diff --git a/app/Controllers/AdminProgressController.php b/app/Controllers/AdminProgressController.php index 965d9fe..6ea6d37 100644 --- a/app/Controllers/AdminProgressController.php +++ b/app/Controllers/AdminProgressController.php @@ -111,6 +111,23 @@ class AdminProgressController extends BaseController ); $sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays); $sectionSubjectCounts = $this->buildSectionSubjectCounts($rows); + $lowProgressSectionIds = []; + if ($expectedDays > 0) { + foreach ($filteredSections as $section) { + $sectionId = (int) ($section['class_section_id'] ?? 0); + if ($sectionId === 0) { + continue; + } + $stat = $sectionStats[$sectionId] ?? null; + $percent = $stat['percent'] ?? 0; + if ($stat === null) { + $percent = 0; + } + if ($percent < 50) { + $lowProgressSectionIds[] = $sectionId; + } + } + } return view('admin/class_progress_list', [ 'reportGroupsBySection' => $reportGroupsBySection, @@ -121,6 +138,7 @@ class AdminProgressController extends BaseController 'sectionStats' => $sectionStats, 'sectionSubjectCounts' => $sectionSubjectCounts, 'expectedDays' => $expectedDays, + 'lowProgressSectionIds' => $lowProgressSectionIds, ]); } diff --git a/app/Controllers/ClassProgressController.php b/app/Controllers/ClassProgressController.php index b25f03c..f8a7c47 100644 --- a/app/Controllers/ClassProgressController.php +++ b/app/Controllers/ClassProgressController.php @@ -119,6 +119,32 @@ class ClassProgressController extends BaseController return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.'); } + $confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite'); + $existingReports = $this->reportModel + ->select('id') + ->where('class_section_id', $classSectionId) + ->where('week_start', $weekStart) + ->where('teacher_id', $teacherId) + ->findAll(); + + if (! $confirmOverwrite && ! empty($existingReports)) { + return redirect()->back() + ->withInput() + ->with('warning', 'A progress report already exists for this week, are you sure you want to override it?') + ->with('confirm_overwrite', true); + } + + if ($confirmOverwrite && ! empty($existingReports)) { + $existingIds = array_values(array_filter(array_map( + static fn (array $row): int => (int) ($row['id'] ?? 0), + $existingReports + ))); + if (! empty($existingIds)) { + $this->attachmentModel->whereIn('report_id', $existingIds)->delete(); + $this->reportModel->whereIn('id', $existingIds)->delete(); + } + } + $status = self::DEFAULT_STATUS; $reportsCreated = 0; @@ -276,6 +302,227 @@ class ClassProgressController extends BaseController ]); } + public function edit($id) + { + $teacherId = (int) session()->get('user_id'); + $row = $this->reportModel + ->select('class_progress_reports.*, cs.class_section_name') + ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') + ->where('class_progress_reports.id', (int) $id) + ->first(); + + if (! $row) { + throw new PageNotFoundException('Progress report not found.'); + } + + [$semester, $schoolYear] = $this->resolveCurrentTerm(); + $allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear); + if (empty($allowedTeacherIds)) { + if ($teacherId !== (int) $row['teacher_id']) { + throw new PageNotFoundException('Progress report not found.'); + } + $allowedTeacherIds = [(int) $row['teacher_id']]; + } elseif (! in_array($teacherId, $allowedTeacherIds, true)) { + throw new PageNotFoundException('Progress report not found.'); + } + + $weeklyReports = $this->reportModel + ->select('class_progress_reports.*') + ->whereIn('teacher_id', $allowedTeacherIds) + ->where('class_section_id', $row['class_section_id']) + ->where('week_start', $row['week_start']) + ->orderBy('subject', 'ASC') + ->findAll(); + + $reportMap = []; + foreach ($weeklyReports as $report) { + $subject = (string) ($report['subject'] ?? ''); + if ($subject === '') { + continue; + } + $reportMap[$subject] = $report; + } + + $subjectReports = []; + foreach (self::SUBJECT_SECTIONS as $slug => $section) { + $subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; + $report = $reportMap[$subjectName] ?? null; + if (! $report) { + continue; + } + $parsed = $this->parseUnitChapterSummary((string) ($report['unit_title'] ?? '')); + $subjectReports[$slug] = [ + 'report_id' => (int) ($report['id'] ?? 0), + 'covered' => $report['covered'] ?? '', + 'homework' => $report['homework'] ?? '', + 'unit_title' => $report['unit_title'] ?? '', + 'unit_values' => $parsed['units'], + 'chapter_values' => $parsed['chapters'], + ]; + } + + $assignments = $this->loadTeacherSections($teacherId); + $classId = null; + $classSectionName = $row['class_section_name'] ?? ''; + foreach ($assignments as $assignment) { + if ((int) ($assignment['class_section_id'] ?? 0) === (int) $row['class_section_id']) { + $classId = $assignment['class_id'] ?? null; + $classSectionName = $assignment['class_section_name'] ?? $classSectionName; + break; + } + } + + $subjectCurriculum = []; + if ($classId) { + foreach (self::SUBJECT_SECTIONS as $slug => $section) { + $subjectCurriculum[$slug] = $this->curriculumModel->getOptionsForClass((int) $classId, $slug); + } + } + + return view('teacher/class_progress_submit', [ + 'subjectSections' => self::SUBJECT_SECTIONS, + 'subjectCurriculum' => $subjectCurriculum, + 'classSectionId' => $row['class_section_id'], + 'classSectionName' => $classSectionName, + 'classId' => $classId, + 'sundayOptions' => [$row['week_start']], + 'defaultWeekStart' => $row['week_start'], + 'existingWeekEnd' => $row['week_end'], + 'existingReports' => $subjectReports, + 'isEdit' => true, + 'formAction' => base_url('teacher/progress/update/' . (int) $row['id']), + 'submitLabel' => 'Update Progress', + ]); + } + + public function update($id) + { + $teacherId = (int) session()->get('user_id'); + $row = $this->reportModel->find((int) $id); + if (! $row) { + throw new PageNotFoundException('Progress report not found.'); + } + + [$semester, $schoolYear] = $this->resolveCurrentTerm(); + $allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear); + if (empty($allowedTeacherIds)) { + if ($teacherId !== (int) $row['teacher_id']) { + throw new PageNotFoundException('Progress report not found.'); + } + $allowedTeacherIds = [(int) $row['teacher_id']]; + } elseif (! in_array($teacherId, $allowedTeacherIds, true)) { + throw new PageNotFoundException('Progress report not found.'); + } + + $subjectSections = self::SUBJECT_SECTIONS; + $rules = [ + 'class_section_id' => 'required|integer', + 'week_start' => 'required|valid_date[Y-m-d]', + 'week_end' => 'required|valid_date[Y-m-d]', + ]; + foreach ($subjectSections as $slug => $section) { + $rules["covered_$slug"] = 'required|string'; + $rules["homework_$slug"] = 'permit_empty|string'; + $rules["unit_{$slug}.*"] = 'permit_empty|string|max_length[120]'; + $rules["chapter_{$slug}.*"] = 'permit_empty|string|max_length[120]'; + } + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + $attachmentErrors = $this->validateAttachmentFiles($subjectSections); + if (! empty($attachmentErrors)) { + return redirect()->back()->withInput()->with('errors', $attachmentErrors); + } + + $weekStart = (string) $this->request->getPost('week_start'); + $weekEnd = (string) $this->request->getPost('week_end'); + if ($weekStart && ! $weekEnd) { + $weekEnd = $this->buildWeekEndFromStart($weekStart); + } + if ($weekStart && $weekEnd && strtotime($weekEnd) < strtotime($weekStart)) { + return redirect()->back()->withInput()->with('error', 'Week end must be the same as or after the week start.'); + } + + $classSectionId = (int) ($row['class_section_id'] ?? 0); + if ($classSectionId === 0) { + return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.'); + } + + $weeklyReports = $this->reportModel + ->select('class_progress_reports.*') + ->whereIn('teacher_id', $allowedTeacherIds) + ->where('class_section_id', $classSectionId) + ->where('week_start', $row['week_start']) + ->orderBy('subject', 'ASC') + ->findAll(); + + $reportMap = []; + foreach ($weeklyReports as $report) { + $subject = (string) ($report['subject'] ?? ''); + if ($subject === '') { + continue; + } + $reportMap[$subject] = $report; + } + + $reportsUpdated = 0; + $flagsInput = $this->request->getPost('flags'); + foreach ($subjectSections as $slug => $section) { + $covered = trim((string) $this->request->getPost("covered_$slug")); + if ($covered === '') { + continue; + } + $homework = trim((string) $this->request->getPost("homework_$slug")); + $unitTitle = $this->buildUnitChapterSummary($slug); + $subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; + $existing = $reportMap[$subjectName] ?? null; + if ($unitTitle === null && $existing) { + $unitTitle = $existing['unit_title'] ?? null; + } + + $data = [ + 'class_section_id' => $classSectionId, + 'week_start' => $weekStart, + 'week_end' => $weekEnd, + 'subject' => $subjectName, + 'unit_title' => $unitTitle, + 'covered' => $covered, + 'homework' => $homework ?: null, + ]; + if ($flagsInput !== null) { + $data['flags_json'] = $this->normalizeFlags($flagsInput); + } + + if ($existing) { + $this->reportModel->update((int) $existing['id'], $data); + $reportId = (int) $existing['id']; + } else { + $data['teacher_id'] = $teacherId; + $data['status'] = self::DEFAULT_STATUS; + $reportId = (int) $this->reportModel->insert($data, true); + } + + $attachmentField = "attachment_$slug"; + $attachments = $this->request->getFileMultiple($attachmentField) ?? []; + $storedAttachments = $this->storeAttachments($reportId, $attachments); + if (! empty($storedAttachments)) { + $this->attachmentModel->insertBatch($storedAttachments); + if (empty($existing['attachment_path'] ?? '')) { + $this->reportModel->update($reportId, ['attachment_path' => $storedAttachments[0]['file_path']]); + } + } + $reportsUpdated++; + } + + if ($reportsUpdated === 0) { + return redirect()->back()->withInput()->with('error', 'Please provide progress for at least one subject.'); + } + + return redirect()->to('teacher/progress/history')->with('success', 'Progress reports updated.'); + } + public function attachment($id) { $row = $this->reportModel->find((int)$id); @@ -447,6 +694,34 @@ class ClassProgressController extends BaseController return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary; } + protected function parseUnitChapterSummary(string $summary): array + { + $summary = trim($summary); + if ($summary === '') { + return ['units' => [], 'chapters' => []]; + } + + $units = []; + $chapters = []; + $segments = preg_split('/\s*;\s*/', $summary, -1, PREG_SPLIT_NO_EMPTY); + foreach ($segments as $segment) { + $segment = trim($segment); + if ($segment === '') { + continue; + } + $parts = preg_split('/\s*\/\s*/', $segment, 2); + if (count($parts) === 2) { + $units[] = trim($parts[0]); + $chapters[] = trim($parts[1]); + } else { + $units[] = $segment; + $chapters[] = ''; + } + } + + return ['units' => $units, 'chapters' => $chapters]; + } + protected function buildSundayOptions(int $count = 12): array { $range = $this->resolveProgressDateRange(); diff --git a/app/Controllers/View/AdministratorController.php b/app/Controllers/View/AdministratorController.php index 6991201..61fb1be 100644 --- a/app/Controllers/View/AdministratorController.php +++ b/app/Controllers/View/AdministratorController.php @@ -28,6 +28,8 @@ use App\Models\ScoreCommentModel; use App\Models\SemesterScoreModel; use App\Models\TeacherClassModel; use App\Models\TeacherSubmissionNotificationHistoryModel; +use App\Models\ExamDraftModel; +use App\Models\HomeworkModel; use App\Services\SemesterRangeService; use CodeIgniter\Events\Events; @@ -696,15 +698,26 @@ class AdministratorController extends BaseController public function teacherSubmissionsReport() { - $semester = (string)($this->semester ?? ''); - $schoolYear = (string)($this->schoolYear ?? ''); + $semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? ''); + $schoolYear = (string)($this->configModel->getConfig('school_year') ?? $this->schoolYear ?? ''); + $semesterResolver = new SemesterRangeService($this->configModel); + $semesterNorm = $semesterResolver->normalizeSemester($semester); + $semesterFilter = $semesterNorm !== '' ? $semesterNorm : $semester; + $semesterCandidates = $this->buildSemesterCandidates($semesterFilter); + $lowProgressRaw = (string) $this->request->getGet('low_progress_sections'); + $lowProgressSectionIds = array_values(array_unique(array_filter(array_map( + 'intval', + preg_split('/\s*,\s*/', $lowProgressRaw, -1, PREG_SPLIT_NO_EMPTY) + )))); $scoreComments = new ScoreCommentModel(); $semesterScores = new SemesterScoreModel(); $attendanceDays = new AttendanceDayModel(); + $examDrafts = new ExamDraftModel(); + $homeworkModel = new HomeworkModel(); $historyModel = new TeacherSubmissionNotificationHistoryModel(); - $assignmentRows = $this->db->table('teacher_class tc') + $assignmentQuery = $this->db->table('teacher_class tc') ->select([ 'tc.class_section_id', 'cs.class_section_name', @@ -715,11 +728,91 @@ class AdministratorController extends BaseController ]) ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left') ->join('users u', 'u.id = tc.teacher_id', 'left') - ->where('tc.school_year', $schoolYear) - ->where('tc.semester', $semester) - ->orderBy('cs.class_section_name', 'ASC') - ->get() - ->getResultArray(); + ->orderBy('cs.class_section_name', 'ASC'); + + $filteredQuery = clone $assignmentQuery; + if ($schoolYear !== '') { + $filteredQuery = $filteredQuery->where('tc.school_year', $schoolYear); + } + if (!empty($semesterCandidates)) { + $filteredQuery = $filteredQuery->whereIn('tc.semester', $semesterCandidates); + } + + $assignmentRows = $filteredQuery->get()->getResultArray(); + if (empty($assignmentRows) && ($schoolYear !== '' || $semester !== '')) { + $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->resolveExamDraftDeadline($semester, $schoolYear); + $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) { @@ -745,7 +838,7 @@ class AdministratorController extends BaseController $entry = &$teachersBySection[$sectionId]; if (!isset($entry)) { $entry = [ - 'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}", + 'class_section' => $assignment['class_section_name'] ?? ($sectionMap[$sectionId] ?? "Section {$sectionId}"), 'teachers' => [], ]; } @@ -765,35 +858,49 @@ class AdministratorController extends BaseController $missingItemCount = 0; $allTeacherIds = []; $allClassSectionIds = []; - foreach ($teachersBySection as $classSectionId => $section) { + $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->studentClassModel + $studentQuery = $this->studentClassModel ->select('student_id') ->where('class_section_id', $classSectionId) - ->where('semester', $semester) - ->where('school_year', $schoolYear) - ->findAll(); + ->where('school_year', $schoolYear); + if (!empty($semesterCandidates)) { + $studentQuery->whereIn('semester', $semesterCandidates); + } + $studentEntries = $studentQuery->findAll(); + 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) { - $scoreRecords = $semesterScores + $scoreQuery = $semesterScores ->where('class_section_id', $classSectionId) - ->where('semester', $semester) - ->where('school_year', $schoolYear) - ->findAll(); + ->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['midterm_exam_score'] ?? '')); + $midtermValue = trim((string)($score[$examScoreField] ?? '')); if ($midtermValue !== '') { $midtermStudents[$sid] = true; } @@ -807,13 +914,15 @@ class AdministratorController extends BaseController $midtermCommentStudents = []; $ptapCommentStudents = []; if (!empty($studentIds)) { - $comments = $scoreComments + $commentQuery = $scoreComments ->select('student_id, score_type, comment') ->whereIn('student_id', $studentIds) - ->where('semester', $semester) ->where('school_year', $schoolYear) - ->whereIn('score_type', ['midterm', 'ptap']) - ->findAll(); + ->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) { @@ -824,7 +933,7 @@ class AdministratorController extends BaseController continue; } $type = strtolower(trim((string)($comment['score_type'] ?? ''))); - if ($type === 'midterm') { + if ($type === $examTerm) { $midtermCommentStudents[$sid] = true; } if ($type === 'ptap') { @@ -833,14 +942,17 @@ class AdministratorController extends BaseController } } - $attendanceRow = $attendanceDays + $attendanceQuery = $attendanceDays ->where('class_section_id', $classSectionId) - ->where('semester', $semester) ->where('school_year', $schoolYear) - ->where('date', $today) - ->first(); + ->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) { @@ -861,18 +973,27 @@ class AdministratorController extends BaseController $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); + $missingItemsForSection = $this->buildMissingItems($statusDetails, $semester); $missingItemCount += count($missingItemsForSection); $totalStatuses += count($statusDetails); $rows[] = [ - 'class_section' => $section['class_section'] ?? "Section {$classSectionId}", + 'class_section' => $sectionMap[$classSectionId] ?? ($section['class_section'] ?? "Section {$classSectionId}"), 'class_section_id' => $classSectionId, 'teachers' => $teacherList, 'midterm_score_status' => $midtermScoreStatus, @@ -880,6 +1001,9 @@ class AdministratorController extends BaseController '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, ]; @@ -941,15 +1065,181 @@ class AdministratorController extends BaseController 'schoolYear' => $schoolYear, 'notificationHistory' => $historyMap, 'summary' => $summary, + 'lowProgressSectionIds' => $lowProgressSectionIds, ]); } + private function resolveLowProgressSectionIds(array $sectionIds): array + { + [$expectedWeeks, $submittedBySection] = $this->buildClassProgressStats($sectionIds); + if ($expectedWeeks <= 0) { + return []; + } + + $lowProgressSectionIds = []; + foreach ($sectionIds as $sectionId) { + $submitted = (int) ($submittedBySection[$sectionId] ?? 0); + $percent = ($submitted / $expectedWeeks) * 100; + if ($percent < 50) { + $lowProgressSectionIds[] = $sectionId; + } + } + + return $lowProgressSectionIds; + } + + private function buildClassProgressStats(array $sectionIds): array + { + $sectionIds = array_values(array_unique(array_filter(array_map('intval', $sectionIds)))); + if (empty($sectionIds)) { + return [0, []]; + } + + $semesterResolver = new SemesterRangeService($this->configModel); + $schoolYear = (string)($this->configModel->getConfig('school_year') ?? ''); + $semester = (string)($this->configModel->getConfig('semester') ?? ''); + $schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string)($this->configModel->getConfig('school_year') ?? ''); + [$rangeStart, $rangeEnd] = $semesterResolver->getSchoolYearRange($schoolYearForRange); + $semesterNorm = $semesterResolver->normalizeSemester($semester); + if ($semesterNorm !== '' && $schoolYearForRange !== '') { + $semRange = $semesterResolver->getSemesterRange($schoolYearForRange, $semesterNorm); + if ($semRange) { + [$rangeStart, $rangeEnd] = $semRange; + } + } + + $dateList = []; + try { + $start = new \DateTimeImmutable($rangeStart); + $end = new \DateTimeImmutable($rangeEnd); + $cursor = $start; + $w = (int) $cursor->format('w'); + if ($w !== 0) { + $cursor = $cursor->modify('next sunday'); + } + while ($cursor <= $end) { + $dateList[] = $cursor->format('Y-m-d'); + $cursor = $cursor->modify('+7 days'); + } + } catch (\Throwable $e) { + $dateList = []; + } + + $noSchoolDays = []; + $events = []; + try { + $calendarModel = new \App\Models\CalendarModel(); + $events = $calendarModel->getEvents(); + } catch (\Throwable $e) { + $events = []; + } + foreach ($events as $event) { + $d = substr((string) ($event['date'] ?? ''), 0, 10); + if ($d === '' || empty($event['no_school'])) { + continue; + } + if ($d < $rangeStart || $d > $rangeEnd) { + continue; + } + $eventYear = trim((string) ($event['school_year'] ?? '')); + if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) { + continue; + } + $noSchoolDays[$d] = true; + } + + $anchorSundayYmd = ''; + try { + $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); + $tzObj = new \DateTimeZone($tzName ?: 'UTC'); + } catch (\Throwable $e) { + try { + $tzObj = new \DateTimeZone(user_timezone() ?: 'UTC'); + } catch (\Throwable $e2) { + $tzObj = new \DateTimeZone('UTC'); + } + } + try { + $nowDate = new \DateTime('now', $tzObj); + } catch (\Throwable $e) { + $nowDate = new \DateTime('now'); + } + $weekday = (int) $nowDate->format('w'); + $anchorSundayYmd = $weekday === 0 + ? $nowDate->format('Y-m-d') + : $nowDate->modify('next sunday')->format('Y-m-d'); + + $activeDatesSet = []; + if (! empty($dateList) && $anchorSundayYmd !== '') { + foreach ($dateList as $d) { + if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) { + $activeDatesSet[$d] = true; + } + } + } + $expectedWeeks = count($activeDatesSet); + if ($expectedWeeks === 0) { + return [0, []]; + } + + $builder = $this->db->table('class_progress_reports') + ->select('class_section_id, week_start') + ->whereIn('class_section_id', $sectionIds); + if (! empty($activeDatesSet)) { + $builder->whereIn('week_start', array_keys($activeDatesSet)); + } + $rows = $builder->get()->getResultArray(); + + $submittedBySection = []; + foreach ($rows as $row) { + $sectionId = (int) ($row['class_section_id'] ?? 0); + $weekStart = (string) ($row['week_start'] ?? ''); + if ($sectionId === 0 || $weekStart === '' || empty($activeDatesSet[$weekStart])) { + continue; + } + $submittedBySection[$sectionId][$weekStart] = true; + } + + $counts = []; + foreach ($sectionIds as $sectionId) { + $counts[$sectionId] = isset($submittedBySection[$sectionId]) + ? count($submittedBySection[$sectionId]) + : 0; + } + + return [$expectedWeeks, $counts]; + } + public function sendTeacherSubmissionNotifications() {$notify = $this->request->getPost('notify'); if (!is_array($notify)) { return redirect()->back()->with('info', 'Select at least one teacher to notify.'); } + $semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? ''); $missingItemsPayload = $this->request->getPost('missing_items') ?? []; + $homeworkNotifyAll = (bool) $this->request->getPost('homework_notify_all'); + $examTerm = $this->resolveExamTermLabel($semester); + $examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores'; + $examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments'; + $forcedItems = []; + if ($this->request->getPost('notify_midterm_score')) { + $forcedItems[] = $examScoreLabel; + } + if ($this->request->getPost('notify_midterm_comment')) { + $forcedItems[] = $examCommentLabel; + } + if ($this->request->getPost('notify_participation')) { + $forcedItems[] = 'participation'; + } + if ($this->request->getPost('notify_ptap_comment')) { + $forcedItems[] = 'PTAP comments'; + } + if ($this->request->getPost('notify_class_progress')) { + $forcedItems[] = 'class progress'; + } + if ($this->request->getPost('notify_exam_draft')) { + $forcedItems[] = 'exam draft'; + } $targets = []; foreach ($notify as $sectionIdRaw => $teachers) { @@ -1006,6 +1296,9 @@ class AdministratorController extends BaseController $historyModel = new TeacherSubmissionNotificationHistoryModel(); $scoreUrl = site_url('/'); + $progressUrl = site_url('teacher/progress/history'); + $examDraftUrl = site_url('teacher/exam-drafts'); + $homeworkUrl = site_url('teacher/addHomework'); $sentCount = 0; $failCount = 0; @@ -1021,6 +1314,13 @@ class AdministratorController extends BaseController $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), @@ -1033,10 +1333,45 @@ class AdministratorController extends BaseController } $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.
"; + } + $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 score submissions and/or comments for {$sectionName}.
" + . "Administration is gently reminding you to wrap up any remaining " + . ($nonScoreOnly ? "submissions for {$sectionName}." : "score submissions, comments, and related items for {$sectionName}.") + . "
" . $missingNote - . "Visit Teacher Score Submission to address any remaining items.
" + . $progressNote + . $examDraftNote + . $homeworkNote + . ($nonScoreOnly ? '' : "Visit Teacher Score Submission to address any remaining items.
") . "Thank you,
Al Rahma Administration