From a6880ea14f7570e7be18f1b3a16c79ab45f50f62 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 23:27:50 -0500 Subject: [PATCH] update code --- app/Controllers/AdminProgressController.php | 256 +++++++++++++++++- app/Controllers/ClassProgressController.php | 135 ++++++++- app/Controllers/View/AttendanceController.php | 33 ++- .../View/ClassPreparationController.php | 52 ++-- .../View/ParentAttendanceReportController.php | 132 ++++++++- app/Views/admin/class_progress_list.php | 208 +++++++++----- app/Views/parent/report_attendance.php | 98 ++++++- app/Views/teacher/class_progress_history.php | 7 +- app/Views/teacher/class_progress_submit.php | 23 +- app/Views/teacher/class_progress_view.php | 1 + 10 files changed, 807 insertions(+), 138 deletions(-) diff --git a/app/Controllers/AdminProgressController.php b/app/Controllers/AdminProgressController.php index 5f323ad..1878bf4 100644 --- a/app/Controllers/AdminProgressController.php +++ b/app/Controllers/AdminProgressController.php @@ -5,7 +5,10 @@ namespace App\Controllers; use App\Models\ClassProgressReportModel; use App\Models\ClassProgressAttachmentModel; use App\Models\ClassSectionModel; +use App\Models\CalendarModel; +use App\Models\ConfigurationModel; use App\Models\StudentClassModel; +use App\Services\SemesterRangeService; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\ResponseInterface; @@ -15,6 +18,9 @@ class AdminProgressController extends BaseController protected ClassProgressAttachmentModel $attachmentModel; protected ClassSectionModel $classSectionModel; protected StudentClassModel $studentClassModel; + protected CalendarModel $calendarModel; + protected ConfigurationModel $configModel; + protected SemesterRangeService $semesterRangeService; public function __construct() { @@ -23,6 +29,9 @@ class AdminProgressController extends BaseController $this->attachmentModel = new ClassProgressAttachmentModel(); $this->classSectionModel = new ClassSectionModel(); $this->studentClassModel = new StudentClassModel(); + $this->calendarModel = new CalendarModel(); + $this->configModel = new ConfigurationModel(); + $this->semesterRangeService = new SemesterRangeService($this->configModel); } public function index() @@ -53,22 +62,30 @@ class AdminProgressController extends BaseController } $rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray(); - $reportGroups = []; + $reportGroupsBySection = []; foreach ($rows as $row) { $row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; - $key = ($row['week_start'] ?? '') . '_' . ($row['class_section_id'] ?? ''); - if ($key === '_') { + $sectionId = (int) ($row['class_section_id'] ?? 0); + $weekKey = (string) ($row['week_start'] ?? ''); + if ($sectionId === 0 || $weekKey === '') { continue; } - if (! isset($reportGroups[$key])) { - $reportGroups[$key] = [ + if (! isset($reportGroupsBySection[$sectionId])) { + $reportGroupsBySection[$sectionId] = []; + } + if (! isset($reportGroupsBySection[$sectionId][$weekKey])) { + $reportGroupsBySection[$sectionId][$weekKey] = [ 'week_start' => $row['week_start'], 'week_end' => $row['week_end'], 'class_section_name' => $row['class_section_name'] ?? '', 'reports' => [], ]; } - $reportGroups[$key]['reports'][$row['subject']] = $row; + $reportGroupsBySection[$sectionId][$weekKey]['reports'][$row['subject']] = $row; + } + foreach ($reportGroupsBySection as $sectionId => $groups) { + krsort($groups); + $reportGroupsBySection[$sectionId] = $groups; } $classSections = $this->classSectionModel->getClassSections(); @@ -78,12 +95,27 @@ class AdminProgressController extends BaseController return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0; })); + $filterStart = $this->normalizeDate($filters['from'] ?? ''); + $filterEnd = $this->normalizeDate($filters['to'] ?? ''); + [$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet] = $this->buildSemesterDates(); + [$expectedDays, $activeDatesSet] = $this->resolveExpectedDays( + $dateList, + $noSchoolDays, + $totalPassedDays, + $passedDatesSet, + $filterStart, + $filterEnd + ); + $sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays); + return view('admin/class_progress_list', [ - 'reportGroups' => $reportGroups, + 'reportGroupsBySection' => $reportGroupsBySection, 'filters' => $filters, 'classSections' => $filteredSections, 'statusOptions' => ClassProgressController::STATUS_OPTIONS, 'subjectSections' => ClassProgressController::SUBJECT_SECTIONS, + 'sectionStats' => $sectionStats, + 'expectedDays' => $expectedDays, ]); } @@ -211,4 +243,214 @@ class AdminProgressController extends BaseController $flags = json_decode($json, true); return is_array($flags) ? $flags : []; } + + protected function normalizeDate(string $value): string + { + $value = trim($value); + if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { + return ''; + } + [$y, $m, $d] = array_map('intval', explode('-', $value)); + return checkdate($m, $d, $y) ? $value : ''; + } + + protected function buildSemesterDates(): array + { + $schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); + $semester = (string) ($this->configModel->getConfig('semester') ?? ''); + $schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string) ($this->configModel->getConfig('school_year') ?? ''); + [$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYearForRange); + $semesterNorm = $this->semesterRangeService->normalizeSemester($semester); + if ($semesterNorm !== '' && $schoolYearForRange !== '') { + $semRange = $this->semesterRangeService->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 { + $events = $this->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'); + + $passedDatesSet = []; + if (! empty($dateList) && $anchorSundayYmd !== '') { + foreach ($dateList as $d) { + if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) { + $passedDatesSet[$d] = true; + } + } + } + $totalPassedDays = count($passedDatesSet); + + return [$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet]; + } + + protected function resolveExpectedDays( + array $dateList, + array $noSchoolDays, + int $totalPassedDays, + array $passedDatesSet, + string $filterStart, + string $filterEnd + ): array { + $activeDatesSet = []; + if ($filterStart === '' && $filterEnd === '') { + $expectedDays = $totalPassedDays; + if ($expectedDays > 0) { + $activeDatesSet = $passedDatesSet; + } + return [$expectedDays, $activeDatesSet]; + } + + $expectedDays = 0; + foreach ($dateList as $d) { + if ($d === '') { + continue; + } + if ($filterStart !== '' && $d < $filterStart) { + continue; + } + if ($filterEnd !== '' && $d > $filterEnd) { + continue; + } + if (! empty($noSchoolDays[$d])) { + continue; + } + $activeDatesSet[$d] = true; + $expectedDays++; + } + + return [$expectedDays, $activeDatesSet]; + } + + protected function buildSectionSubmissionStats(array $rows, array $activeDatesSet, int $expectedDays): array + { + $requiredSubjects = []; + foreach (ClassProgressController::SUBJECT_SECTIONS as $section) { + $requiredSubjects[] = $section['db_subject'] ?? $section['label'] ?? ''; + } + $requiredSubjects = array_values(array_filter($requiredSubjects)); + + $submittedBySection = []; + foreach ($rows as $row) { + $sectionId = (int) ($row['class_section_id'] ?? 0); + $weekStart = (string) ($row['week_start'] ?? ''); + $subject = (string) ($row['subject'] ?? ''); + if ($sectionId === 0 || $weekStart === '' || $subject === '') { + continue; + } + if (! empty($activeDatesSet) && empty($activeDatesSet[$weekStart])) { + continue; + } + if (! in_array($subject, $requiredSubjects, true)) { + continue; + } + $submittedBySection[$sectionId][$weekStart][$subject] = true; + } + + $stats = []; + foreach ($submittedBySection as $sectionId => $weeks) { + $submitted = 0; + foreach ($weeks as $subjects) { + $all = true; + foreach ($requiredSubjects as $subject) { + if (empty($subjects[$subject])) { + $all = false; + break; + } + } + if ($all) { + $submitted++; + } + } + $stats[$sectionId] = $this->buildSectionStat($submitted, $expectedDays); + } + + return $stats; + } + + protected function buildSectionStat(int $submitted, int $expectedDays): array + { + $percent = $expectedDays > 0 ? round(($submitted * 100) / $expectedDays, 1) : 0.0; + if ($expectedDays === 0) { + $badgeClass = 'bg-secondary'; + $labelClass = 'text-muted'; + } elseif ($percent >= 100) { + $badgeClass = 'bg-success'; + $labelClass = 'text-success'; + } elseif ($percent >= 60) { + $badgeClass = 'bg-warning text-dark'; + $labelClass = 'text-warning'; + } elseif ($percent >= 40) { + $badgeClass = 'bg-orange text-dark'; + $labelClass = 'text-warning'; + } else { + $badgeClass = 'bg-danger'; + $labelClass = 'text-danger'; + } + + return [ + 'submitted' => $submitted, + 'expected' => $expectedDays, + 'percent' => $percent, + 'badgeClass' => $badgeClass, + 'labelClass' => $labelClass, + ]; + } } diff --git a/app/Controllers/ClassProgressController.php b/app/Controllers/ClassProgressController.php index 134596b..b25f03c 100644 --- a/app/Controllers/ClassProgressController.php +++ b/app/Controllers/ClassProgressController.php @@ -7,6 +7,7 @@ use App\Models\ClassProgressAttachmentModel; use App\Models\ConfigurationModel; use App\Models\SubjectCurriculumModel; use App\Models\TeacherClassModel; +use App\Services\SemesterRangeService; use CodeIgniter\Exceptions\PageNotFoundException; class ClassProgressController extends BaseController @@ -56,6 +57,7 @@ class ClassProgressController extends BaseController $classSectionName = $first['class_section_name'] ?? null; $classId = $first['class_id'] ?? null; $sundayOptions = $this->buildSundayOptions(); + $defaultWeekStart = $this->pickDefaultWeekStart($sundayOptions); $subjectCurriculum = []; if ($classId) { foreach (self::SUBJECT_SECTIONS as $slug => $section) { @@ -69,7 +71,7 @@ class ClassProgressController extends BaseController 'classSectionName' => $classSectionName, 'classId' => $classId, 'sundayOptions' => $sundayOptions, - 'defaultWeekStart' => $sundayOptions[0] ?? '', + 'defaultWeekStart' => $defaultWeekStart, ]; return view('teacher/class_progress_submit', $data); } @@ -171,10 +173,16 @@ class ClassProgressController extends BaseController if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) { $selectedSectionId = $validSectionIds[0] ?? null; } + [$semester, $schoolYear] = $this->resolveCurrentTerm(); + $allowedTeacherIds = $this->resolveAssignedTeacherIds($selectedSectionId, $semester, $schoolYear); + if (empty($allowedTeacherIds)) { + $allowedTeacherIds = [$teacherId]; + } $builder = $this->reportModel - ->select('class_progress_reports.*, cs.class_section_name') + ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') - ->where('teacher_id', $teacherId); + ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left') + ->whereIn('teacher_id', $allowedTeacherIds); if ($selectedSectionId) { $builder->where('class_progress_reports.class_section_id', $selectedSectionId); } @@ -216,20 +224,33 @@ class ClassProgressController extends BaseController { $teacherId = (int) session()->get('user_id'); $row = $this->reportModel - ->select('class_progress_reports.*, cs.class_section_name') + ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') - ->where('teacher_id', $teacherId) - ->find((int) $id); + ->join('users u', 'u.id = class_progress_reports.teacher_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.'); + } + $row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $weeklyReports = $this->reportModel - ->select('class_progress_reports.*, cs.class_section_name') + ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') - ->where('teacher_id', $teacherId) + ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left') + ->whereIn('teacher_id', $allowedTeacherIds) ->where('class_progress_reports.class_section_id', $row['class_section_id']) ->where('week_start', $row['week_start']) ->orderBy('subject', 'ASC') @@ -427,6 +448,39 @@ class ClassProgressController extends BaseController } protected function buildSundayOptions(int $count = 12): array + { + $range = $this->resolveProgressDateRange(); + if ($range === null) { + return $this->buildUpcomingSundayOptions($count); + } + [$rangeStart, $rangeEnd] = $range; + + try { + $start = new \DateTime($rangeStart); + $end = new \DateTime($rangeEnd); + } catch (\Exception $e) { + return $this->buildUpcomingSundayOptions($count); + } + + if ($end < $start) { + [$start, $end] = [$end, $start]; + } + + $weekday = (int) $start->format('w'); + if ($weekday !== 0) { + $start->modify('next sunday'); + } + + $options = []; + while ($start <= $end) { + $options[] = $start->format('Y-m-d'); + $start->modify('+7 days'); + } + + return $options; + } + + protected function buildUpcomingSundayOptions(int $count): array { $start = new \DateTime('today'); $weekday = (int) $start->format('w'); @@ -443,6 +497,48 @@ class ClassProgressController extends BaseController return $options; } + protected function resolveProgressDateRange(): ?array + { + $schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); + if ($schoolYear === '') { + return null; + } + + $semesterResolver = new SemesterRangeService($this->configModel); + $semester = $semesterResolver->normalizeSemester((string) ($this->configModel->getConfig('semester') ?? '')); + if ($semester === '') { + $semester = $semesterResolver->getSemesterForDate(); + } + + if ($semester !== '') { + $range = $semesterResolver->getSemesterRange($schoolYear, $semester); + if ($range !== null) { + return $range; + } + } + + return $semesterResolver->getSchoolYearRange($schoolYear); + } + + protected function pickDefaultWeekStart(array $options): string + { + if (empty($options)) { + return ''; + } + + $today = date('Y-m-d'); + $default = ''; + foreach ($options as $option) { + if ($option <= $today) { + $default = $option; + continue; + } + break; + } + + return $default !== '' ? $default : $options[0]; + } + protected function buildWeekEndFromStart(string $weekStart): string { try { @@ -468,4 +564,27 @@ class ClassProgressController extends BaseController mkdir($this->attachmentStoragePath, 0755, true); } } + + protected function resolveCurrentTerm(): array + { + $schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); + $semester = (string) ($this->configModel->getConfig('semester') ?? ''); + return [$semester, $schoolYear]; + } + + protected function resolveAssignedTeacherIds(?int $classSectionId, string $semester, string $schoolYear): array + { + if (! $classSectionId || $schoolYear === '') { + return []; + } + $rows = $this->teacherClassModel->assignedForSectionTerm($classSectionId, $semester, $schoolYear); + $ids = []; + foreach ($rows as $row) { + $id = (int) ($row['teacher_id'] ?? 0); + if ($id > 0) { + $ids[$id] = true; + } + } + return array_keys($ids); + } } diff --git a/app/Controllers/View/AttendanceController.php b/app/Controllers/View/AttendanceController.php index eb0782f..f3b0962 100644 --- a/app/Controllers/View/AttendanceController.php +++ b/app/Controllers/View/AttendanceController.php @@ -797,26 +797,25 @@ public function showUpdateAttendanceForm() } $noSchoolDays = []; - if ($schoolYearForRange !== '') { + $events = []; + try { + $events = $this->calendarModel->getEvents(); + } catch (\Throwable $e) { $events = []; - try { - $events = $this->calendarModel->getEventsBySchoolYearAndSemester( - $schoolYearForRange, - $semesterNorm !== '' ? $semesterNorm : null - ); - } catch (\Throwable $e) { - $events = []; + } + foreach ($events as $event) { + $d = substr((string)($event['date'] ?? ''), 0, 10); + if ($d === '' || empty($event['no_school'])) { + continue; } - foreach ($events as $event) { - $d = substr((string)($event['date'] ?? ''), 0, 10); - if ($d === '' || empty($event['no_school'])) { - continue; - } - if ($d < $rangeStart || $d > $rangeEnd) { - continue; - } - $noSchoolDays[$d] = true; + if ($d < $rangeStart || $d > $rangeEnd) { + continue; } + $eventYear = trim((string)($event['school_year'] ?? '')); + if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) { + continue; + } + $noSchoolDays[$d] = true; } // Total passed attendance days up to this Sunday (inclusive), excluding no-school days. diff --git a/app/Controllers/View/ClassPreparationController.php b/app/Controllers/View/ClassPreparationController.php index 1da0b69..db9456e 100644 --- a/app/Controllers/View/ClassPreparationController.php +++ b/app/Controllers/View/ClassPreparationController.php @@ -60,13 +60,15 @@ class ClassPreparationController extends BaseController $limitToSemester = $this->hasRosterForSemester($schoolYear, $semester); // 1) Get student count per class-section (distinct students, correct semester) - $scQ = $this->studentClassModel - ->select('class_section_id, COUNT(DISTINCT student_id) AS student_count') - ->where('school_year', $schoolYear); + $scQ = $this->db->table('student_class sc') + ->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false) + ->join('students s', 's.id = sc.student_id', 'inner') + ->where('s.is_active', 1) + ->where('sc.school_year', $schoolYear); if ($limitToSemester && $semester !== '') { - $scQ->where('semester', $semester); + $scQ->where('sc.semester', $semester); } - $classSections = $scQ->groupBy('class_section_id')->findAll(); + $classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray(); // 2) Inventory availability — prefer good_qty when present, else condition='good' quantity. $inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed); @@ -298,14 +300,16 @@ class ClassPreparationController extends BaseController $className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; // Distinct student count for this term - $studentQ = $this->studentClassModel - ->select('COUNT(DISTINCT student_id) AS cnt') - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear); + $studentQ = $this->db->table('student_class sc') + ->select('COUNT(DISTINCT sc.student_id) AS cnt', false) + ->join('students s', 's.id = sc.student_id', 'inner') + ->where('s.is_active', 1) + ->where('sc.class_section_id', $classSectionId) + ->where('sc.school_year', $schoolYear); if ($limitToSemester && $semester !== '') { - $studentQ->where('semester', $semester); + $studentQ->where('sc.semester', $semester); } - $studentRow = $studentQ->first(); + $studentRow = $studentQ->get()->getRowArray(); $studentCount = (int)($studentRow['cnt'] ?? 0); // Live calc + adjustments @@ -355,13 +359,15 @@ class ClassPreparationController extends BaseController $limitToSemester = $this->hasRosterForSemester($schoolYear, $semester); // Student counts - $scQ = $this->studentClassModel - ->select('class_section_id, COUNT(DISTINCT student_id) AS student_count') - ->where('school_year', $schoolYear); + $scQ = $this->db->table('student_class sc') + ->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false) + ->join('students s', 's.id = sc.student_id', 'inner') + ->where('s.is_active', 1) + ->where('sc.school_year', $schoolYear); if ($limitToSemester && $semester !== '') { - $scQ->where('semester', $semester); + $scQ->where('sc.semester', $semester); } - $classSections = $scQ->groupBy('class_section_id')->findAll(); + $classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray(); // Build inventory availability maps $inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed); @@ -448,14 +454,16 @@ class ClassPreparationController extends BaseController $now = utc_now(); $count = 0; foreach ($ids as $classSectionId) { - $studentQ = $this->studentClassModel - ->select('COUNT(DISTINCT student_id) AS cnt') - ->where('class_section_id', $classSectionId) - ->where('school_year', $schoolYear); + $studentQ = $this->db->table('student_class sc') + ->select('COUNT(DISTINCT sc.student_id) AS cnt', false) + ->join('students s', 's.id = sc.student_id', 'inner') + ->where('s.is_active', 1) + ->where('sc.class_section_id', $classSectionId) + ->where('sc.school_year', $schoolYear); if ($limitToSemester && $semester !== '') { - $studentQ->where('semester', $semester); + $studentQ->where('sc.semester', $semester); } - $studentRow = $studentQ->first(); + $studentRow = $studentQ->get()->getRowArray(); $studentCount = (int)($studentRow['cnt'] ?? 0); $classLevel = $this->getClassLevelBySection((string)$classSectionId); $className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; diff --git a/app/Controllers/View/ParentAttendanceReportController.php b/app/Controllers/View/ParentAttendanceReportController.php index 268aa85..ec1119f 100644 --- a/app/Controllers/View/ParentAttendanceReportController.php +++ b/app/Controllers/View/ParentAttendanceReportController.php @@ -71,12 +71,15 @@ class ParentAttendanceReportController extends BaseController ->orderBy('s.lastname', 'ASC') ->get()->getResultArray(); + $cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff')); + return view('parent/report_attendance', [ 'students' => $students, 'today' => local_date(utc_now(), 'Y-m-d'), 'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items 'defaultDate' => $defaultDate, // preselected date (upcoming Sunday) 'myReports' => $previewRows, + 'cutoffThreshold' => $cutoffThreshold, ]); } @@ -141,6 +144,17 @@ class ParentAttendanceReportController extends BaseController // Enforce Sunday-only and future-or-today selection $todayCheck = new \DateTime('today'); + $cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff')); + $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); + $tz = null; + $nowTz = null; + try { + $tz = new \DateTimeZone($tzName ?: 'UTC'); + $nowTz = new \DateTime('now', $tz); + } catch (\Throwable $e) { + $tz = null; + $nowTz = null; + } $validDates = []; $cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year')); @@ -150,9 +164,11 @@ class ParentAttendanceReportController extends BaseController $lateTodayStr = $todayCheck->format('Y-m-d'); if ($type === 'late') { try { - $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); - $tz = new \DateTimeZone($tzName ?: 'UTC'); - $lateNow = new \DateTime('now', $tz); + if (!$tz) { + $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); + $tz = new \DateTimeZone($tzName ?: 'UTC'); + } + $lateNow = $nowTz ?: new \DateTime('now', $tz); $cm = $this->configModel; $cutoffStr = (string) ($cm->getConfig('late_report_cutoff') ?: $cm->getConfig('late_cutoff_time') @@ -183,6 +199,17 @@ class ParentAttendanceReportController extends BaseController if ($dt < $todayCheck) { return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.'); } + if ($nowTz && $tz) { + try { + $cutoff = new \DateTime($entry . ' ' . $cutoffThreshold . ':00', $tz); + if ($nowTz >= $cutoff) { + $abbr = $cutoff->format('T'); + return redirect()->back()->withInput()->with('error', 'Reporting closes at ' . $cutoffThreshold . ' ' . $abbr . ' on the report date.'); + } + } catch (\Throwable $e) { + // ignore and continue + } + } if ($dt > $cap) { return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.'); } @@ -800,6 +827,36 @@ class ParentAttendanceReportController extends BaseController $update['reported'] = 1; } $this->attendanceDataModel->update((int) $existing['id'], $update); + return; + } + + if ($existingStatus === '' || $existingStatus === 'present') { + $update = [ + 'status' => $status, + 'reason' => $existing['reason'] ?: $reasonText, + 'updated_at' => utc_now(), + ]; + if ($hasIsReported) { + $update['is_reported'] = 'yes'; + } + if ($hasLegacyReported) { + $update['reported'] = 1; + } + $this->attendanceDataModel->update((int) $existing['id'], $update); + + $classSectionId = (int) ($existing['class_section_id'] ?? $classSectionId ?? 0); + $schoolId = (string) ($existing['school_id'] ?? ''); + if ($classSectionId > 0 && $schoolId !== '') { + $this->adjustAttendanceRecordForStatusChange( + studentId: $studentId, + classSectionId: $classSectionId, + schoolId: $schoolId, + semester: $semester, + schoolYear: $schoolYear, + oldStatus: $existingStatus, + newStatus: $status + ); + } } } @@ -856,6 +913,61 @@ class ParentAttendanceReportController extends BaseController $this->attendanceRecordModel->insert($insert); } + private function adjustAttendanceRecordForStatusChange( + int $studentId, + int $classSectionId, + string $schoolId, + string $semester, + string $schoolYear, + string $oldStatus, + string $newStatus + ): void { + $oldStatus = strtolower($oldStatus); + $newStatus = strtolower($newStatus); + + if ($oldStatus === $newStatus) { + return; + } + + $record = $this->attendanceRecordModel->where([ + 'student_id' => $studentId, + 'semester' => $semester, + 'school_year' => $schoolYear, + ])->first(); + + if (!$record) { + $this->bumpAttendanceRecordFromParent($studentId, $classSectionId, $schoolId, $newStatus, $semester, $schoolYear); + return; + } + + $updates = [ + 'total_presence' => (int) ($record['total_presence'] ?? 0), + 'total_absence' => (int) ($record['total_absence'] ?? 0), + 'total_late' => (int) ($record['total_late'] ?? 0), + 'total_attendance' => (int) ($record['total_attendance'] ?? 0), + 'updated_at' => utc_now(), + 'modified_by' => (int) (session()->get('user_id') ?? 0), + ]; + + if ($oldStatus === 'present') { + $updates['total_presence'] = max(0, $updates['total_presence'] - 1); + } elseif ($oldStatus === 'absent') { + $updates['total_absence'] = max(0, $updates['total_absence'] - 1); + } elseif ($oldStatus === 'late') { + $updates['total_late'] = max(0, $updates['total_late'] - 1); + } + + if ($newStatus === 'present') { + $updates['total_presence']++; + } elseif ($newStatus === 'absent') { + $updates['total_absence']++; + } elseif ($newStatus === 'late') { + $updates['total_late']++; + } + + $this->attendanceRecordModel->update((int) $record['id'], $updates); + } + // GET: staff view to list reports (today & upcoming by default) public function list() { @@ -1503,6 +1615,20 @@ class ParentAttendanceReportController extends BaseController return $userId; // fallback } + private function normalizeCutoffTime(string $raw, string $default = '09:00'): string + { + $raw = trim($raw); + if ($raw === '') { + return $default; + } + if (preg_match('/^(\d{1,2}):(\d{2})$/', $raw, $m)) { + $hh = str_pad((string) ((int) $m[1]), 2, '0', STR_PAD_LEFT); + $mm = $m[2]; + return $hh . ':' . $mm; + } + return $default; + } + /** * Send confirmation/notification emails for parent attendance submissions. */ diff --git a/app/Views/admin/class_progress_list.php b/app/Views/admin/class_progress_list.php index 89337aa..d5a615b 100644 --- a/app/Views/admin/class_progress_list.php +++ b/app/Views/admin/class_progress_list.php @@ -1,8 +1,21 @@ extend('layout/management_layout') ?> section('content') ?>
@@ -56,79 +69,132 @@
-
-
- - - - - - - - - - +
+ + +
No class sections found.
+ +
+ $cs): ?> 0, + 'expected' => 0, + 'percent' => 0, + 'badgeClass' => 'bg-secondary', + 'labelClass' => 'text-muted', + ]; + } else { + $stat = [ + 'submitted' => 0, + 'expected' => $expectedDays, + 'percent' => 0, + 'badgeClass' => 'bg-danger', + 'labelClass' => 'text-danger', + ]; + } + } + $percentLabel = $expectedDays > 0 ? number_format((float) $stat['percent'], 1) . '%' : 'N/A'; + $submissionLabel = $expectedDays > 0 + ? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')') + : 'Submitted: N/A'; ?> - -
- - - - - - - - - - - - -
WeekSubjectsTeacherAction
No reports found.
-
- -
Class:
- -
-
- $section): ?> - -
-
- - -
-
-
- +
+

+ +

+
+
+ +
No reports found for this section.
+ +
+ + + + + + + + + + + + + + + + + + + + +
WeekSubjectsTeacherAction
+
+ $section): ?> + +
+
+ + +
+
+
+ +
+
+ + View + +
-
- - View - -
-
+ +
+
+
+ + + diff --git a/app/Views/parent/report_attendance.php b/app/Views/parent/report_attendance.php index 6582594..024b39e 100644 --- a/app/Views/parent/report_attendance.php +++ b/app/Views/parent/report_attendance.php @@ -122,15 +122,40 @@
+ attendance['timezone'] ?? user_timezone()); + $tz = new DateTimeZone($tzName ?: 'UTC'); + $nowTz = new DateTime('now', $tz); + $cutoffDate = $nowTz->format('Y-m-d'); + $cutoffTime = $nowTz->format('H:i'); + $cutoffTzAbbr = $nowTz->format('T'); + } catch (\Throwable $e) { + $cutoffDate = ''; + $cutoffTime = ''; + $cutoffTzAbbr = ''; + } + ?>
+ data-check-url="" + data-cutoff-date="" + data-cutoff-time="" + data-cutoff-threshold="" + data-cutoff-blocked="0"> +
@@ -352,7 +377,10 @@ const dates = selectedDates(); if (!checkUrl || !dateSel || !typeSel) return; if (!ids.length || !dates.length) { - if (submitBtn) submitBtn.disabled = false; + if (submitBtn) { + const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1'; + submitBtn.disabled = cutoffBlocked; + } return; } @@ -395,7 +423,10 @@ } if (!data || !data.ok) { - if (submitBtn) submitBtn.disabled = false; + if (submitBtn) { + const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1'; + submitBtn.disabled = cutoffBlocked; + } return; } @@ -454,10 +485,16 @@ setAlert('', ''); } - if (submitBtn) submitBtn.disabled = (selectedIds().length === 0); + if (submitBtn) { + const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1'; + submitBtn.disabled = cutoffBlocked || (selectedIds().length === 0); + } } catch (e) { setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.'); - if (submitBtn) submitBtn.disabled = false; + if (submitBtn) { + const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1'; + submitBtn.disabled = cutoffBlocked; + } } } @@ -472,4 +509,55 @@ runCheck(); }); + endSection() ?> diff --git a/app/Views/teacher/class_progress_history.php b/app/Views/teacher/class_progress_history.php index 4ec2740..25ae052 100644 --- a/app/Views/teacher/class_progress_history.php +++ b/app/Views/teacher/class_progress_history.php @@ -3,7 +3,7 @@
-

My Progress Reports

+

Class Progress Reports

Review weekly submissions and compare Islamic Studies with Quran/Arabic.
Submit New Report @@ -78,6 +78,11 @@
+ +
+ Submitted by: +
+
diff --git a/app/Views/teacher/class_progress_submit.php b/app/Views/teacher/class_progress_submit.php index 8133e7d..2a82a4b 100644 --- a/app/Views/teacher/class_progress_submit.php +++ b/app/Views/teacher/class_progress_submit.php @@ -175,10 +175,13 @@
- +
- + +
+
+
@@ -263,10 +266,13 @@ row.className = 'row g-2 align-items-end mb-2 unit-chapter-entry'; row.innerHTML = `
- +
- + +
+
+
`; const inputs = row.querySelectorAll('input'); @@ -338,6 +344,15 @@ }); }); + document.addEventListener('click', event => { + const button = event.target.closest('[data-remove-unit-chapter]'); + if (!button) return; + const row = button.closest('.unit-chapter-entry'); + if (row) { + row.remove(); + } + }); + document.querySelectorAll('[data-custom-entry]').forEach(button => { button.addEventListener('click', event => { event.stopPropagation(); diff --git a/app/Views/teacher/class_progress_view.php b/app/Views/teacher/class_progress_view.php index 57dbfc8..670c0d8 100644 --- a/app/Views/teacher/class_progress_view.php +++ b/app/Views/teacher/class_progress_view.php @@ -37,6 +37,7 @@
No entry submitted for this subject this week.
+
Submitted by:
:
Activities