'required|integer', 'class_section_id'=> 'permit_empty|integer', 'school_year' => 'required|max_length[20]', 'is_event_only' => 'permit_empty|in_list[0,1]', 'updated_by' => 'permit_empty|integer', ]; protected $validationMessages = []; protected $skipValidation = false; protected $cleanValidationRules = true; /** * Create a fresh builder for student_class. * * Using a fresh builder prevents WHERE conditions from a previous model * query from leaking into another query. */ private function freshBuilder(): BaseBuilder { return $this->db->table($this->table); } /** * Create a fresh builder scoped to active students. */ private function activeStudentsBuilder(): BaseBuilder { return $this->freshBuilder() ->select('student_class.*') ->join( 'students', 'students.id = student_class.student_id', 'inner' ) ->where('students.is_active', 1); } /** * Apply the active-student scope to the model. * * Prefer the dedicated retrieval methods below for new code because they * use fresh builders and cannot inherit stale model query state. */ public function active(): self { return $this ->select('student_class.*') ->join( 'students', 'students.id = student_class.student_id', 'inner' ) ->where('students.is_active', 1); } /** * Return the most recent class section name assigned to a student. */ public function getClassSectionNameByStudentId( int $studentId, ?string $schoolYear = null ): ?string { $builder = $this->freshBuilder() ->select('cs.class_section_name') ->join( 'classSection cs', 'cs.class_section_id = student_class.class_section_id', 'inner' ) ->where('student_class.student_id', $studentId) ->where( 'student_class.class_section_id IS NOT NULL', null, false ); if ($schoolYear !== null && trim($schoolYear) !== '') { $builder->where( 'student_class.school_year', trim($schoolYear) ); } $row = $builder ->orderBy('student_class.created_at', 'DESC') ->get() ->getRowArray(); if (!$row) { return null; } $name = trim((string) ($row['class_section_name'] ?? '')); return $name !== '' ? $name : null; } /** * Get active students assigned to a class section. * * student_class is scoped by school year only. It has no semester column. */ public function getClassStudents( int $classSectionId, ?string $schoolYear = null ): array { $builder = $this->activeStudentsBuilder() ->where( 'student_class.class_section_id', $classSectionId ) ->where( 'student_class.class_section_id IS NOT NULL', null, false ); if ($schoolYear !== null && trim($schoolYear) !== '') { $builder->where( 'student_class.school_year', trim($schoolYear) ); } return $builder ->orderBy('students.lastname', 'ASC') ->orderBy('students.firstname', 'ASC') ->get() ->getResultArray(); } /** * Return student IDs assigned to a class section for a school year. */ public function getStudentIdsByClassSection( int $classSectionId, string $schoolYear ): array { $schoolYear = trim($schoolYear); $builder = $this->freshBuilder() ->select('student_class.student_id') ->join( 'students', 'students.id = student_class.student_id', 'inner' ) ->where( 'student_class.class_section_id', $classSectionId ) ->where('students.is_active', 1) ->where( 'student_class.class_section_id IS NOT NULL', null, false ); if ($schoolYear !== '') { $builder->where( 'student_class.school_year', $schoolYear ); } $rows = $builder ->groupBy('student_class.student_id') ->get() ->getResultArray(); $studentIds = array_map( static fn(array $row): int => (int) ($row['student_id'] ?? 0), $rows ); return array_values(array_unique(array_filter( $studentIds, static fn(int $studentId): bool => $studentId > 0 ))); } /** * Return class section names assigned to a student for a school year. * * @return array|string */ public function getClassSectionsByStudentId( int|string $studentId, string $schoolYear, bool $asArray = false ): array|string { $rows = $this->freshBuilder() ->select('cs.class_section_name') ->join( 'classSection cs', 'student_class.class_section_id = cs.class_section_id', 'inner' ) ->where('student_class.student_id', $studentId) ->where( 'student_class.class_section_id IS NOT NULL', null, false ) ->where( 'student_class.school_year', trim($schoolYear) ) ->orderBy('cs.class_section_name', 'ASC') ->get() ->getResultArray(); $names = array_map( static fn(array $row): string => trim((string) ($row['class_section_name'] ?? '')), $rows ); $names = array_values(array_unique(array_filter( $names, static fn(string $name): bool => $name !== '' ))); return $asArray ? $names : implode(', ', $names); } /** * Return class section names and indicate event-only assignments. * * @return array|string */ public function getClassSectionsByStudentIdWithFlags( int|string $studentId, string $schoolYear, bool $asArray = false ): array|string { $rows = $this->freshBuilder() ->select( 'cs.class_section_name, student_class.is_event_only' ) ->join( 'classSection cs', 'student_class.class_section_id = cs.class_section_id', 'inner' ) ->where('student_class.student_id', $studentId) ->where( 'student_class.class_section_id IS NOT NULL', null, false ) ->where( 'student_class.school_year', trim($schoolYear) ) ->orderBy('cs.class_section_name', 'ASC') ->get() ->getResultArray(); $names = []; foreach ($rows as $row) { $name = trim( (string) ($row['class_section_name'] ?? '') ); if ($name === '') { continue; } if ((int) ($row['is_event_only'] ?? 0) === 1) { $name .= ' (Event)'; } $names[] = $name; } $names = array_values(array_unique($names)); return $asArray ? $names : implode(', ', $names); } /** * Return class-section IDs assigned to a student for a school year. * * @return int[] */ public function getClassSectionIdsByStudentId( int|string $studentId, string $schoolYear ): array { $rows = $this->freshBuilder() ->select('student_class.class_section_id') ->where('student_class.student_id', $studentId) ->where( 'student_class.class_section_id IS NOT NULL', null, false ) ->where( 'student_class.school_year', trim($schoolYear) ) ->get() ->getResultArray(); $ids = array_map( static fn(array $row): int => (int) ($row['class_section_id'] ?? 0), $rows ); return array_values(array_unique(array_filter( $ids, static fn(int $id): bool => $id > 0 ))); } /** * Return active students for a set of class-section IDs. */ public function getStudentsByClassSectionIds( array $classSectionIds, ?string $schoolYear = null ): array { $classSectionIds = array_values(array_unique(array_filter( array_map('intval', $classSectionIds), static fn(int $id): bool => $id > 0 ))); if ($classSectionIds === []) { return []; } $builder = $this->freshBuilder() ->select([ 'students.id AS student_id', 'students.firstname', 'students.lastname', 'students.school_id', 'students.is_new', 'students.photo_consent', 'students.age', 'student_class.school_year', 'student_class.class_section_id', 'student_class.is_event_only', ]) ->join( 'students', 'students.id = student_class.student_id', 'inner' ) ->whereIn( 'student_class.class_section_id', $classSectionIds ) ->where('students.is_active', 1); if ($schoolYear !== null && trim($schoolYear) !== '') { $builder->where( 'student_class.school_year', trim($schoolYear) ); } return $builder ->orderBy('students.lastname', 'ASC') ->orderBy('students.firstname', 'ASC') ->get() ->getResultArray(); } /** * Return the student's most recent non-event class grade. */ public function getStudentGrade( int $studentId, ?string $schoolYear = null ): string { $builder = $this->freshBuilder() ->select('classSection.class_id') ->join( 'classSection', 'classSection.class_section_id = student_class.class_section_id', 'inner' ) ->where('student_class.student_id', $studentId) ->where('student_class.is_event_only', 0) ->where( 'student_class.class_section_id IS NOT NULL', null, false ); if ($schoolYear !== null && trim($schoolYear) !== '') { $builder->where( 'student_class.school_year', trim($schoolYear) ); } $row = $builder ->orderBy('student_class.created_at', 'DESC') ->get() ->getRowArray(); $classId = trim((string) ($row['class_id'] ?? '')); if ($classId !== '') { return $classId; } log_message( 'warning', 'Student class or section not found for student ID: {studentId}', ['studentId' => $studentId] ); return 'N/A'; } /** * Determine whether a student has a non-event assignment for a year. */ public function hasNonEventAssignment( int $studentId, string $schoolYear ): bool { return $this->freshBuilder() ->select('student_class.id') ->where('student_class.student_id', $studentId) ->where( 'student_class.school_year', trim($schoolYear) ) ->where('student_class.is_event_only', 0) ->limit(1) ->get() ->getRowArray() !== null; } /** * Return active student counts by class section. * * @return array */ public function getStudentCountsBySection( ?string $schoolYear = null ): array { $builder = $this->freshBuilder() ->select( 'student_class.class_section_id, ' . 'COUNT(DISTINCT student_class.student_id) AS total' ) ->join( 'students', 'students.id = student_class.student_id', 'inner' ) ->where('students.is_active', 1) ->where( 'student_class.class_section_id IS NOT NULL', null, false ) ->groupBy('student_class.class_section_id'); if ($schoolYear !== null && trim($schoolYear) !== '') { $builder->where( 'student_class.school_year', trim($schoolYear) ); } $rows = $builder ->get() ->getResultArray(); $counts = []; foreach ($rows as $row) { $classSectionId = (int) ( $row['class_section_id'] ?? 0 ); if ($classSectionId <= 0) { continue; } $counts[$classSectionId] = (int) ( $row['total'] ?? 0 ); } return $counts; } }