userModel = new UserModel(); $this->configModel = new ConfigurationModel(); $this->studentModel = new StudentModel(); $this->classSectionModel = new ClassSectionModel(); $this->teacherClassModel = new TeacherClassModel(); $this->studentClassModel = new StudentClassModel(); $this->semester = getSemester(); $this->schoolYear = $this->configModel->getConfig('school_year'); } public function index() { $data = [ 'classSections' => [] ]; // Apply school year filter (default to current config) but avoid semester filtering so the full year is visible $selectedSemester = (string)($this->request->getGet('semester') ?? $this->semester ?? ''); $year = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? '')); if ($year === '') { $year = $this->currentSchoolYearName((string)($this->schoolYear ?? '')); } $this->applyPendingDistributionDraftsForEnrolledStudents($year); $this->repairMissingEnrollmentClassAssignments($year); $distributedSectionIds = array_values(array_unique(array_merge( $this->distributedClassSectionIds($year), $this->enrollmentAssignedClassSectionIds($year) ))); $classSectionsAll = []; if (!empty($distributedSectionIds)) { $classSectionsAll = $this->classSectionModel ->select('id, class_section_id, class_section_name, school_year') ->where('school_year', $year) ->whereIn('class_section_id', $distributedSectionIds) ->orderBy('class_section_name', 'ASC') ->findAll(); } $classSectionById = []; foreach ($classSectionsAll as $section) { $sectionId = (int)($section['class_section_id'] ?? 0); if ($sectionId > 0) { $classSectionById[$sectionId] = $section; } } $tcQ = $this->teacherClassModel; if ($year !== '') { $tcQ = $tcQ->where('school_year', $year); } $teacherClassesAll = $tcQ->findAll(); $scQ = $this->studentClassModel->active(); if ($year !== '') { $scQ = $scQ->where('student_class.school_year', $year); } $studentClassesAll = $scQ->findAll(); // Group teacher and student classes by section $teacherBySection = []; foreach ($teacherClassesAll as $tc) { $teacherBySection[$tc['class_section_id']][] = $tc; } $studentsBySection = []; foreach ($studentClassesAll as $sc) { $studentsBySection[$sc['class_section_id']][] = $sc; } $allSectionIds = array_unique(array_merge(array_keys($classSectionById), array_keys($teacherBySection), array_keys($studentsBySection))); $distributedSectionSet = array_fill_keys($distributedSectionIds, true); foreach ($allSectionIds as $classSectionId) { if (!isset($distributedSectionSet[(int)$classSectionId])) { continue; } $teacherClasses = $teacherBySection[$classSectionId] ?? []; $studentClasses = $studentsBySection[$classSectionId] ?? []; $hasTeacher = !empty($teacherClasses); $hasStudents = !empty($studentClasses); $hasClassSection = isset($classSectionById[(int)$classSectionId]); if (!$hasClassSection && !$hasTeacher && !$hasStudents) { continue; } $classSectionName = (string) ($classSectionById[(int)$classSectionId]['class_section_name'] ?? $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? ''); $mainTeachers = []; $teacherAssistants = []; $sectionSemester = ''; $sectionSchoolYear = ''; $description = ''; if ($hasTeacher) { foreach ($teacherClasses as $teacherClass) { $teacher = $this->userModel->find($teacherClass['teacher_id']); if (!$teacher) continue; $teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')); if ($teacherName === '') continue; if (($teacherClass['position'] ?? '') === 'main') { $mainTeachers[] = $teacherName; } elseif (($teacherClass['position'] ?? '') === 'ta') { $teacherAssistants[] = $teacherName; } if ($sectionSemester === '' && !empty($teacherClass['semester'])) { $sectionSemester = (string)$teacherClass['semester']; } if ($sectionSchoolYear === '' && !empty($teacherClass['school_year'])) { $sectionSchoolYear = (string)$teacherClass['school_year']; } if ($description === '' && !empty($teacherClass['description'])) { $description = (string)$teacherClass['description']; } } } $students = []; $seenStudentIds = []; foreach ($studentClasses as $studentClass) { $sid = (int)($studentClass['student_id'] ?? 0); if ($sid <= 0 || isset($seenStudentIds[$sid])) { continue; } if ($sectionSemester === '' && !empty($studentClass['semester'])) { $sectionSemester = (string)$studentClass['semester']; } if ($sectionSchoolYear === '' && !empty($studentClass['school_year'])) { $sectionSchoolYear = (string)$studentClass['school_year']; } if ($description === '' && !empty($studentClass['description'])) { $description = (string)$studentClass['description']; } $student = $this->studentModel ->where('id', $studentClass['student_id']) ->where('is_active', 1) ->first(); if (!$student) { continue; } $calculatedAge = EnrollmentEligibility::ageOnSeptemberFirst($student['dob'] ?? null, $year); $students[] = [ 'id' => (int)$student['id'], 'firstname' => esc($student['firstname']), 'lastname' => esc($student['lastname']), 'age' => esc((string)($calculatedAge ?? ($student['age'] ?? ''))), 'gender' => esc($student['gender']), 'registration_grade' => esc($student['registration_grade']), 'photo_consent' => esc($student['photo_consent'] ? 'Yes' : 'No'), 'tuition_paid' => esc($student['tuition_paid'] ? 'Yes' : 'No'), 'school_id' => esc($student['school_id']), ]; $seenStudentIds[$sid] = true; } $sectionSemesterDisplay = $sectionSemester !== '' ? $sectionSemester : ((string)($this->semester ?? '')); $sectionSchoolYearDisplay = $sectionSchoolYear !== '' ? $sectionSchoolYear : ((string)($this->schoolYear ?? '')); $data['classSections'][] = [ 'class_section_id' => $classSectionId, 'class_section_name' => $classSectionName, 'main_teachers' => $mainTeachers, 'teacher_assistants' => $teacherAssistants, 'students' => $students, 'semester' => $sectionSemesterDisplay, 'school_year' => $sectionSchoolYearDisplay, 'description' => $description, ]; } $schoolYearsList = []; try { $db = Database::connect(); $yearsQuery = $db->table('classSection') ->select('DISTINCT school_year', false) ->where('school_year IS NOT NULL', null, false) ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); $studentYearsQuery = $db->table('student_class') ->select('DISTINCT school_year', false) ->where('school_year IS NOT NULL', null, false) ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); $yearsQuery = array_merge($yearsQuery, $studentYearsQuery); foreach ($yearsQuery as $row) { $val = (string)($row['school_year'] ?? ''); if ($val !== '' && !in_array($val, $schoolYearsList, true)) { $schoolYearsList[] = $val; } } } catch (\Throwable $e) { // ignore fallback below } if ($this->schoolYear !== null && $this->schoolYear !== '' && !in_array((string)$this->schoolYear, $schoolYearsList, true)) { array_unshift($schoolYearsList, (string)$this->schoolYear); } // Sort sections usort($data['classSections'], fn($a, $b) => strnatcasecmp((string) $a['class_section_name'], (string) $b['class_section_name'])); $data['schoolYears'] = $schoolYearsList; $data['schoolYear'] = $year; $data['selectedYear'] = $year; $data['selectedSemester'] = $selectedSemester; return view('administrator/class_assignment', $data); } private function distributedClassSectionIds(string $year): array { $year = trim($year); if ($year === '') { return []; } try { $db = Database::connect(); if (! $db->tableExists('student_section_distribution_drafts')) { return []; } $baseRows = $db->table('classSection') ->select('class_id, class_section_id, class_section_name') ->where('school_year', $year) ->where("class_section_name NOT LIKE '%-%'", null, false) ->orderBy('class_id', 'ASC') ->get() ->getResultArray(); $draftRows = $db->table('student_section_distribution_drafts d') ->select('d.class_id, d.class_section_id, COALESCE(cs.class_section_name, d.class_section_id) AS class_section_name', false) ->join( 'classSection cs', 'cs.class_section_id = d.class_section_id AND cs.school_year = d.school_year', 'left', false ) ->where('d.school_year', $year) ->where('d.class_section_id >', 0) ->whereIn('status', ['pending', 'applied']) ->get() ->getResultArray(); $draftsByClassId = []; foreach ($draftRows as $row) { $classId = (int)($row['class_id'] ?? 0); $sectionId = (int)($row['class_section_id'] ?? 0); if ($classId <= 0 || $sectionId <= 0) { continue; } $draftsByClassId[$classId][$sectionId] = [ 'class_section_id' => $sectionId, 'class_section_name' => (string)($row['class_section_name'] ?? ''), ]; } $allowed = []; $seenClassIds = []; foreach ($baseRows as $row) { $classId = (int)($row['class_id'] ?? 0); $baseSectionId = (int)($row['class_section_id'] ?? 0); $baseName = trim((string)($row['class_section_name'] ?? '')); if ($classId <= 0 || $baseSectionId <= 0 || $baseName === '') { continue; } $normalized = strtolower($baseName); $isAutoDistributeBase = $normalized === 'youth' || (ctype_digit($normalized) && (int)$normalized >= 1 && (int)$normalized <= 10); if (!$isAutoDistributeBase) { continue; } $seenClassIds[$classId] = true; $classDrafts = $draftsByClassId[$classId] ?? []; $hasBaseDraft = false; $letteredDraftIds = []; foreach ($classDrafts as $draft) { $draftSectionId = (int)($draft['class_section_id'] ?? 0); $draftName = trim((string)($draft['class_section_name'] ?? '')); if ($draftSectionId === $baseSectionId || $draftName === '' || strpos($draftName, '-') === false) { $hasBaseDraft = true; continue; } $letteredDraftIds[] = $draftSectionId; } if ($hasBaseDraft || empty($letteredDraftIds)) { $allowed[] = $baseSectionId; continue; } foreach ($letteredDraftIds as $draftSectionId) { $allowed[] = $draftSectionId; } } foreach ($draftsByClassId as $classId => $classDrafts) { if (isset($seenClassIds[$classId])) { continue; } foreach ($classDrafts as $draft) { $sectionId = (int)($draft['class_section_id'] ?? 0); if ($sectionId > 0) { $allowed[] = $sectionId; } } } return array_values(array_unique(array_filter( $allowed, static fn(int $sectionId): bool => $sectionId > 0 ))); } catch (\Throwable $e) { log_message('error', 'distributedClassSectionIds failed: ' . $e->getMessage()); return []; } } private function enrollmentAssignedClassSectionIds(string $year): array { $year = trim($year); if ($year === '') { return []; } try { $db = Database::connect(); $allowedStatuses = ['admission under review', 'review & decision', 'payment pending', 'enrolled']; $ids = []; if ($db->tableExists('enrollments')) { $builder = $db->table('enrollments e') ->select('e.class_section_id') ->join('students s', 's.id = e.student_id', 'inner') ->where('e.school_year', $year) ->whereIn('e.enrollment_status', $allowedStatuses) ->where('e.class_section_id IS NOT NULL', null, false) ->where('e.class_section_id >', 0); if ($db->fieldExists('is_active', 'students')) { $builder->where('s.is_active', 1); } if ($db->fieldExists('is_withdrawn', 'enrollments')) { $builder->groupStart() ->where('e.is_withdrawn', 0) ->orWhere('e.is_withdrawn', null) ->groupEnd(); } foreach ($builder->groupBy('e.class_section_id')->get()->getResultArray() as $row) { $sectionId = (int)($row['class_section_id'] ?? 0); if ($sectionId > 0) { $ids[] = $sectionId; } } } if ($db->tableExists('student_class') && $db->tableExists('enrollments')) { $builder = $db->table('student_class sc') ->select('sc.class_section_id') ->join( 'enrollments e', 'e.student_id = sc.student_id AND e.school_year = sc.school_year', 'inner', false ) ->join('students s', 's.id = sc.student_id', 'inner') ->where('sc.school_year', $year) ->whereIn('e.enrollment_status', $allowedStatuses) ->where('sc.class_section_id IS NOT NULL', null, false) ->where('sc.class_section_id >', 0); if ($db->fieldExists('is_active', 'students')) { $builder->where('s.is_active', 1); } if ($db->fieldExists('is_event_only', 'student_class')) { $builder->groupStart() ->where('sc.is_event_only', 0) ->orWhere('sc.is_event_only', null) ->groupEnd(); } if ($db->fieldExists('is_withdrawn', 'enrollments')) { $builder->groupStart() ->where('e.is_withdrawn', 0) ->orWhere('e.is_withdrawn', null) ->groupEnd(); } foreach ($builder->groupBy('sc.class_section_id')->get()->getResultArray() as $row) { $sectionId = (int)($row['class_section_id'] ?? 0); if ($sectionId > 0) { $ids[] = $sectionId; } } } return array_values(array_unique($ids)); } catch (\Throwable $e) { log_message('error', 'enrollmentAssignedClassSectionIds failed: ' . $e->getMessage()); return []; } } private function applyPendingDistributionDraftsForEnrolledStudents(string $year): void { if ($year === '') { return; } try { $db = Database::connect(); if (! $db->tableExists('student_section_distribution_drafts')) { return; } $rows = $db->table('student_section_distribution_drafts d') ->select('d.id, d.student_id, d.class_section_id') ->join( 'enrollments e', 'e.student_id = d.student_id AND e.school_year = d.school_year', 'inner' ) ->where('d.school_year', $year) ->where('d.status', 'pending') ->whereIn('e.enrollment_status', ['admission under review', 'payment pending', 'enrolled']) ->groupBy('d.id, d.student_id, d.class_section_id') ->get() ->getResultArray(); if (empty($rows)) { return; } $now = utc_now(); $updatedBy = (int)(session()->get('user_id') ?? 0) ?: null; $db->transStart(); foreach ($rows as $row) { $studentId = (int)($row['student_id'] ?? 0); $sectionId = (int)($row['class_section_id'] ?? 0); if ($studentId <= 0 || $sectionId <= 0) { continue; } $existing = $db->table('student_class') ->select('id') ->where('student_id', $studentId) ->where('school_year', $year) ->get() ->getRowArray(); $payload = [ 'student_id' => $studentId, 'class_section_id' => $sectionId, 'school_year' => $year, 'updated_by' => $updatedBy, 'updated_at' => $now, ]; if ($existing) { $db->table('student_class') ->where('id', (int)$existing['id']) ->update($payload); } else { $payload['created_at'] = $now; $db->table('student_class')->insert($payload); } $db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $year) ->whereIn('enrollment_status', ['admission under review', 'payment pending', 'enrolled']) ->update([ 'class_section_id' => $sectionId, 'assigned_class_section_id' => $sectionId, 'placement_status' => 'automatic_distribution_applied', 'updated_at' => $now, ]); $db->table('student_section_distribution_drafts') ->where('id', (int)$row['id']) ->update([ 'status' => 'applied', 'applied_at' => $now, 'updated_at' => $now, ]); } $db->transComplete(); } catch (\Throwable $e) { log_message('error', 'applyPendingDistributionDraftsForEnrolledStudents failed: ' . $e->getMessage()); } } private function repairMissingEnrollmentClassAssignments(string $year): void { if ($year === '') { return; } try { $db = Database::connect(); $this->ensureClassSectionsForYear($db, $year); if (! $db->tableExists('enrollments') || ! $db->tableExists('student_class')) { return; } $previousYear = $this->previousSchoolYearName($year); if ($previousYear === null) { return; } $rows = $db->table('enrollments e') ->select('e.student_id, e.school_year, e.enrollment_status, e.class_section_id, sc.id AS student_class_id') ->join('student_class sc', 'sc.student_id = e.student_id AND sc.school_year = e.school_year', 'left') ->where('e.school_year', $year) ->whereIn('e.enrollment_status', ['admission under review', 'payment pending', 'enrolled']) ->groupStart() ->where('e.class_section_id', null) ->orWhere('e.class_section_id', 0) ->orWhere('sc.id', null) ->groupEnd() ->groupBy('e.student_id, e.school_year, e.enrollment_status, e.class_section_id, sc.id') ->get() ->getResultArray(); foreach ($rows as $row) { $studentId = (int)($row['student_id'] ?? 0); if ($studentId <= 0) { continue; } $evaluation = service('enrollmentTransition')->evaluate($studentId, $previousYear, $year, 'admin'); if (!($evaluation['academic_eligible'] ?? false) || ($evaluation['blockers'] ?? []) !== []) { continue; } $targetSectionId = (int)($evaluation['assigned_class_section_id'] ?? 0); if ($targetSectionId <= 0 && (int)($evaluation['assigned_grade_id'] ?? 0) > 0) { $base = $this->baseSectionForClassYear((int)$evaluation['assigned_grade_id'], $year); $targetSectionId = (int)($base['class_section_id'] ?? 0); } if ($targetSectionId <= 0) { continue; } $placementStatus = match ((string)($evaluation['placement_status'] ?? '')) { 'automatic_distribution_pending' => 'base_section_pending_distribution', 'same_class_assigned', 'temporary_same_grade' => (string)$evaluation['placement_status'], default => 'manual_class_assigned', }; $existing = $db->table('student_class') ->select('id') ->where('student_id', $studentId) ->where('school_year', $year) ->orderBy('id', 'DESC') ->limit(1) ->get() ->getRowArray(); $studentClassPayload = [ 'student_id' => $studentId, 'class_section_id' => $targetSectionId, 'school_year' => $year, 'updated_by' => (int)(session()->get('user_id') ?? 0) ?: null, 'updated_at' => utc_now(), ]; if ($existing !== null) { $db->table('student_class')->where('id', (int)$existing['id'])->update($studentClassPayload); } else { $studentClassPayload['created_at'] = utc_now(); $db->table('student_class')->insert($studentClassPayload); } $db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $year) ->whereIn('enrollment_status', ['admission under review', 'payment pending', 'enrolled']) ->update([ 'class_section_id' => $targetSectionId, 'assigned_class_section_id' => $targetSectionId, 'placement_status' => $placementStatus, 'updated_at' => utc_now(), ]); } } catch (\Throwable $e) { log_message('error', 'repairMissingEnrollmentClassAssignments failed: ' . $e->getMessage()); } } private function baseSectionForClassYear(int $classId, string $schoolYear): ?array { $db = Database::connect(); $this->ensureClassSectionsForYear($db, $schoolYear); if ($classId <= 0 || $schoolYear === '' || ! $db->tableExists('classSection')) { return null; } $builder = $db->table('classSection') ->select('class_section_id, class_section_name, class_id') ->where('class_id', $classId) ->where("class_section_name NOT LIKE '%-%'", null, false) ->orderBy('id', 'ASC') ->limit(1); if ($db->fieldExists('school_year', 'classSection')) { $builder->where('school_year', $schoolYear); } $row = $builder->get()->getRowArray(); return $row !== null && (int)($row['class_section_id'] ?? 0) > 0 ? $row : null; } private function ensureClassSectionsForYear($db, string $targetSchoolYear): void { $targetSchoolYear = trim($targetSchoolYear); if ($targetSchoolYear === '' || ! $db->tableExists('classSection') || ! $db->fieldExists('school_year', 'classSection')) { return; } if ($db->table('classSection')->where('school_year', $targetSchoolYear)->countAllResults() > 0) { return; } $sourceSchoolYear = $this->previousSchoolYearName($targetSchoolYear); if ($sourceSchoolYear === null) { return; } $sourceRows = $db->table('classSection') ->select('class_id, class_section_id, class_section_name') ->where('school_year', $sourceSchoolYear) ->orderBy('id', 'ASC') ->get() ->getResultArray(); $now = utc_now(); foreach ($sourceRows as $row) { $classSectionId = (int)($row['class_section_id'] ?? 0); if ($classSectionId <= 0) { continue; } $exists = $db->table('classSection') ->where('school_year', $targetSchoolYear) ->where('class_section_id', $classSectionId) ->countAllResults(); if ($exists > 0) { continue; } $db->table('classSection')->insert([ 'class_id' => (int)($row['class_id'] ?? 0), 'class_section_id' => $classSectionId, 'class_section_name' => (string)($row['class_section_name'] ?? ''), 'school_year' => $targetSchoolYear, 'created_at' => $now, 'updated_at' => $now, ]); } } private function previousSchoolYearName(string $schoolYear): ?string { $schoolYear = trim($schoolYear); if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $matches)) { return null; } return ((int)$matches[1] - 1) . '-' . ((int)$matches[2] - 1); } public function save() { $data = [ 'student_id' => $this->request->getPost('student_id'), 'class_section_id' => $this->request->getPost('class_section_id'), 'semester' => $this->request->getPost('semester'), 'school_year' => $this->request->getPost('school_year'), 'description' => $this->request->getPost('description'), 'updated_by' => session()->get('user_id'), ]; $this->studentClassModel->save($data); return redirect()->to('/assignments')->with('message', 'Assignment saved successfully'); } // API: JSON payload for Classes List page public function classAssignmentData() { $year = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? '')); if ($year === '') { $year = $this->currentSchoolYearName((string)($this->schoolYear ?? '')); } $this->applyPendingDistributionDraftsForEnrolledStudents($year); $this->repairMissingEnrollmentClassAssignments($year); $distributedSectionIds = array_values(array_unique(array_merge( $this->distributedClassSectionIds($year), $this->enrollmentAssignedClassSectionIds($year) ))); $classSectionsAll = []; if (!empty($distributedSectionIds)) { $classSectionsAll = $this->classSectionModel ->select('id, class_section_id, class_section_name, school_year') ->where('school_year', $year) ->whereIn('class_section_id', $distributedSectionIds) ->orderBy('class_section_name', 'ASC') ->findAll(); } $classSectionById = []; foreach ($classSectionsAll as $section) { $sectionId = (int)($section['class_section_id'] ?? 0); if ($sectionId > 0) { $classSectionById[$sectionId] = $section; } } $tcQ = $this->teacherClassModel; if ($year !== '') { $tcQ = $tcQ->where('school_year', $year); } $teacherClassesAll = $tcQ->findAll(); $scQ = $this->studentClassModel->active(); if ($year !== '') { $scQ = $scQ->where('student_class.school_year', $year); } $studentClassesAll = $scQ->findAll(); // Group by section $teacherBySection = []; foreach ($teacherClassesAll as $tc) { $secId = (int)($tc['class_section_id'] ?? 0); if ($secId) $teacherBySection[$secId][] = $tc; } $studentsBySection = []; foreach ($studentClassesAll as $sc) { $secId = (int)($sc['class_section_id'] ?? 0); if ($secId) $studentsBySection[$secId][] = $sc; } $allSectionIds = array_values(array_unique(array_merge(array_keys($classSectionById), array_keys($teacherBySection), array_keys($studentsBySection)))); $distributedSectionSet = array_fill_keys($distributedSectionIds, true); $classSections = []; foreach ($allSectionIds as $classSectionId) { if (!isset($distributedSectionSet[(int)$classSectionId])) { continue; } $hasTeacher = !empty($teacherBySection[$classSectionId]); $hasStudents = !empty($studentsBySection[$classSectionId]); $hasClassSection = isset($classSectionById[(int)$classSectionId]); if (!$hasClassSection && !$hasTeacher && !$hasStudents) continue; $classSectionName = (string) ($classSectionById[(int)$classSectionId]['class_section_name'] ?? $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? ''); $mainTeachers = []; $teacherAssistants = []; $semesterMeta = ''; $schoolYearMeta = ''; $descriptionMeta = ''; if ($hasTeacher) { foreach ($teacherBySection[$classSectionId] as $teacherClass) { $teacher = $this->userModel->find((int)$teacherClass['teacher_id']); if ($teacher) { $tname = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')); if (($teacherClass['position'] ?? '') === 'main') { $mainTeachers[] = $tname; } elseif (($teacherClass['position'] ?? '') === 'ta') { $teacherAssistants[] = $tname; } } // assign meta (same for all rows in a section) if ($semesterMeta === '' && !empty($teacherClass['semester'])) { $semesterMeta = (string)$teacherClass['semester']; } if ($schoolYearMeta === '' && !empty($teacherClass['school_year'])) { $schoolYearMeta = (string)$teacherClass['school_year']; } if ($descriptionMeta === '' && !empty($teacherClass['description'])) { $descriptionMeta = (string)$teacherClass['description']; } } } // Load students for the section $students = []; $seenStudentIds = []; foreach ($studentsBySection[$classSectionId] ?? [] as $studentClass) { $sid = (int)($studentClass['student_id'] ?? 0); if ($sid <= 0 || isset($seenStudentIds[$sid])) { continue; } $stu = $this->studentModel ->where('id', $sid) ->where('is_active', 1) ->first(); if (!$stu) continue; $students[] = [ 'id' => (int)$stu['id'], 'firstname' => (string)($stu['firstname'] ?? ''), 'lastname' => (string)($stu['lastname'] ?? ''), 'age' => $stu['age'] ?? null, 'gender' => (string)($stu['gender'] ?? ''), 'registration_grade' => (string)($stu['registration_grade'] ?? ''), 'photo_consent' => (bool)($stu['photo_consent'] ?? false), 'tuition_paid' => (bool)($stu['tuition_paid'] ?? false), 'school_id' => (string)($stu['school_id'] ?? ''), ]; $seenStudentIds[$sid] = true; } $classSections[] = [ 'class_section_id' => (int)$classSectionId, 'class_section_name' => $classSectionName, 'main_teachers' => array_values(array_unique($mainTeachers)), 'teacher_assistants' => array_values(array_unique($teacherAssistants)), 'students' => $students, 'semester' => $semesterMeta ?: (string)$this->semester, 'school_year' => $schoolYearMeta ?: (string)$this->schoolYear, 'description' => $descriptionMeta, ]; } // Sort by class_section_name usort($classSections, fn($a, $b) => strnatcasecmp((string)($a['class_section_name'] ?? ''), (string)($b['class_section_name'] ?? ''))); return $this->response->setJSON([ 'classSections' => $classSections, 'csrfHash' => csrf_hash(), 'semester' => (string)$this->semester, 'school_year' => (string)$this->schoolYear, ]); } }