'required|is_natural_no_zero', 'class_section_id'=> 'required|is_natural_no_zero', 'student_id' => 'required|is_natural_no_zero', 'status' => 'required|in_list[present,absent,late]', 'is_reported' => 'required|in_list[no,yes]', 'is_notified' => 'required|in_list[no,yes]', 'date' => 'required|valid_date[Y-m-d]', 'semester' => 'required', 'school_year' => 'required', ]; /** * Retrieve attendance by class and section for a specific date. * * @param int $classId * @param int $classSectionId * @param string $date * @return array */ public function getAttendanceByClass($classId, $classSectionId, $date) { return $this->where([ 'class_id' => $classId, 'class_section_id' => $classSectionId, 'date' => $date ])->findAll(); } public function unreportedTermRows(array $studentIds, string $schoolYear, ?string $semester): array { $qb = $this->builder() ->select('id, student_id, class_id, class_section_id, date, status, reason, school_year, semester, is_reported') ->where('school_year', $schoolYear); if ($semester !== null && $semester !== '') { $qb->where('semester', $semester); } if (!empty($studentIds)) { $qb->whereIn('student_id', $studentIds); } $this->applyUnreportedFilter($qb); return $qb->orderBy('date', 'DESC')->get()->getResultArray(); } /** * Update attendance record. * * @param int $attendanceId * @param array $data * @return bool */ public function updateAttendance($attendanceId, $data) { return $this->update($attendanceId, $data); } /** * Retrieve attendance for a specific student, class section, and date. * * @param int $student_id * @param int $classSectionId * @param string $date * @return array|null */ public function getAttendance($student_id, $classSectionId, $date) { return $this->where([ 'student_id' => $student_id, 'class_section_id' => $classSectionId, 'date' => $date ])->first(); } /** * Retrieve reported attendance for a specific date or range. * * @param string $startDate * @param string|null $endDate * @return array */ public function getReportedAttendance($startDate, $endDate = null) { $builder = $this->where(['is_reported' => 1]); if ($endDate) { $builder->where('date >=', $startDate)->where('date <=', $endDate); } else { $builder->where('date', $startDate); } return $builder->findAll(); } public function getTodayAbsentees($classSectionId = null, $schoolYear = null, $semester = null) { $builder = $this->where('status', 'Absent') ->where('date', date('Y-m-d')); if ($classSectionId !== null) { $builder->where('class_section_id', $classSectionId); } if ($schoolYear !== null) { $builder->where('school_year', $schoolYear); } if ($semester !== null) { $builder->where('semester', $semester); } return $builder->findAll(); } public function getTodayLates($classSectionId = null, $schoolYear = null, $semester = null) { $builder = $this->where('status', 'Late') ->where('date', date('Y-m-d')); if ($classSectionId !== null) { $builder->where('class_section_id', $classSectionId); } if ($schoolYear !== null) { $builder->where('school_year', $schoolYear); } if ($semester !== null) { $builder->where('semester', $semester); } return $builder->findAll(); } public function getAbsencesByClassSection($classSectionId, $schoolYear, $semester) { return $this->where('status', 'absent') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('semester', $semester) ->orderBy('date', 'DESC') ->findAll(); } /** * Fetch attendance rows for a student, optionally filtered by date range. * * @param int $studentId * @param string|null $startDate 'YYYY-MM-DD' inclusive * @param string|null $endDate 'YYYY-MM-DD' inclusive * @param string|null $schoolYear optional filter * @param string|null $semester optional filter * @return array */ public function getStudentAttendance( int $studentId, ?string $startDate = null, ?string $endDate = null, ?string $schoolYear = null, ?string $semester = null ): array { $qb = $this->asArray()->where('student_id', $studentId); if (!empty($startDate)) { $qb->where('date >=', $startDate); } if (!empty($endDate)) { $qb->where('date <=', $endDate); } if (!empty($schoolYear)) { $qb->where('school_year', $schoolYear); } if (!empty($semester)) { $qb->where('semester', $semester); } return $qb->orderBy('date', 'DESC')->findAll(); } /** * Convenience: attendance for an exact date. */ public function getStudentAttendanceOnDate(int $studentId, string $date): ?array { return $this->asArray() ->where(['student_id' => $studentId, 'date' => $date]) ->first(); // returns null if none } /** * NOT-REPORTED absences/lates for ALL students in optional date/term filters. * - Not reported: is_reported = 0 OR '0' OR NULL * - Status: ABS, ABSENT, LATE, A, L, plus prefixes like ABS_3 / LATE_2 * - Date inclusive: >= start AND < (end + 1 day) to handle DATE/DATETIME columns */ public function getUnreportedAbsencesAndLates( ?string $schoolYear = null, ?string $semester = null ): array { $b = $this->db->table($this->table); $b->select('id, student_id, class_id, class_section_id, date, status, reason, school_year, semester, is_reported') // Not reported: 0 / '0' / NULL (supports legacy "reported" column) ; $this->applyUnreportedFilter($b); // Not notified yet: exclude rows already marked notified (handles enum/string/numeric) $b->groupStart() ->where('is_notified', 0) ->orWhere('is_notified', '0') ->orWhere('is_notified', null) ->orWhere("LOWER(TRIM(COALESCE(is_notified, ''))) !=", 'yes') ->groupEnd(); // Optional term filters (no date constraints) if ($schoolYear !== null && $schoolYear !== '') { $b->where("{$this->table}.school_year", $schoolYear); } if ($semester !== null && $semester !== '') { $b->where("{$this->table}.semester", $semester); } // Status filter: exact + prefixes (ABS_3, LATE_2, etc.) $b->groupStart() ->whereIn("{$this->table}.status", ['ABS','ABSENT','LATE','A','L']) ->orLike("{$this->table}.status", 'ABS', 'after') ->orLike("{$this->table}.status", 'LATE', 'after') ->groupEnd(); // Sort for readability $b->orderBy("{$this->table}.student_id", 'ASC') ->orderBy("{$this->table}.date", 'DESC'); $rows = $b->get()->getResultArray(); // Group per student (dates included in each record) $out = []; foreach ($rows as $r) { $sid = (int) $r['student_id']; if (!isset($out[$sid])) { $out[$sid] = [ 'absences' => [], 'lates' => [], 'counts' => ['absences' => 0, 'lates' => 0, 'total' => 0], ]; } $st = strtoupper((string)($r['status'] ?? '')); $isAbs = ($st === 'ABS' || $st === 'ABSENT' || $st === 'A' || str_starts_with($st, 'ABS')); $isLate = ($st === 'LATE' || $st === 'L' || str_starts_with($st, 'LATE')); $rec = [ 'id' => $r['id'], 'date' => $r['date'], 'status' => $r['status'], 'reason' => $r['reason'], 'class_id' => $r['class_id'], 'class_section_id' => $r['class_section_id'], 'school_year' => $r['school_year'], 'semester' => $r['semester'], ]; if ($isAbs) { $out[$sid]['absences'][] = $rec; $out[$sid]['counts']['absences']++; } elseif ($isLate) { $out[$sid]['lates'][] = $rec; $out[$sid]['counts']['lates']++; } $out[$sid]['counts']['total'] = $out[$sid]['counts']['absences'] + $out[$sid]['counts']['lates']; } return $out; } public function markRuleAsNotifiedData( int $studentId, string $date, ?string $semester = null, ?string $schoolYear = null ): bool { $day = substr($date, 0, 10); $start = $day . ' 00:00:00'; $endEx = date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00'; $builder = $this->db->table($this->table); $builder->where('student_id', $studentId) ->where('date >=', $start) ->where('date <', $endEx); if (!empty($semester)) { $builder->where('semester', $semester); } if (!empty($schoolYear)) { $builder->where('school_year', $schoolYear); } // ✅ Update ENUM field properly (compatible with legacy numeric flags) $builder->set('is_notified', 'yes'); $builder->set('updated_at', utc_now()); return $builder->update(); } /** * Mark specific attendance dates as reported + notified for a student. * * @param int $studentId * @param array $dates Array of YYYY-MM-DD strings */ public function markReportedAndNotified(int $studentId, array $dates, ?string $semester = null, ?string $schoolYear = null): bool { $dates = array_values(array_unique(array_filter($dates, static function ($d) { return is_string($d) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); }))); if (empty($dates)) { return true; } $builder = $this->db->table($this->table) ->where('student_id', $studentId) ->whereIn('date', $dates) ->where("LOWER(TRIM(status)) IN ('absent','late')", null, false); if (!empty($semester)) { $builder->where('semester', $semester); } if (!empty($schoolYear)) { $builder->where('school_year', $schoolYear); } $cols = $this->reportedColumns(); if (!empty($cols['is_reported'])) { $builder->set('is_reported', 'yes'); } if (!empty($cols['reported'])) { $builder->set('reported', 1); } if ($this->db->fieldExists('is_notified', $this->table)) { $builder->set('is_notified', 'yes'); } return (bool) $builder ->set('updated_at', utc_now()) ->update(); } /** * Fetch unreported dates for a student within N days for the given status set. * * @param int $studentId * @param string[] $statuses ['absent','late'] * @param string $incidentDate YYYY-MM-DD reference (defaults today) * @param int $lookbackDays days window backward from incidentDate (inclusive) */ public function getRecentUnreportedDates( int $studentId, array $statuses, ?string $incidentDate = null, int $lookbackDays = 35, ?string $semester = null, ?string $schoolYear = null ): array { if ($studentId <= 0 || empty($statuses)) { return []; } $statuses = array_map('strtolower', $statuses); $day = $incidentDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $incidentDate) ? $incidentDate : date('Y-m-d'); $start = date('Y-m-d', strtotime($day . ' -' . max(0, $lookbackDays) . ' days')); $qb = $this->db->table($this->table) ->select('date') ->where('student_id', $studentId) ->where('date >=', $start) ->where('date <=', $day) ->whereIn('LOWER(TRIM(status))', $statuses); $this->applyUnreportedFilter($qb); if (!empty($semester)) { $qb->where('semester', $semester); } if (!empty($schoolYear)) { $qb->where('school_year', $schoolYear); } $rows = $qb->orderBy('date', 'DESC')->get()->getResultArray(); return array_values(array_unique(array_map(static function ($r) { return substr((string)($r['date'] ?? ''), 0, 10); }, $rows))); } private function reportedColumns(): array { if ($this->reportedColumns !== null) { return $this->reportedColumns; } $this->reportedColumns = [ 'is_reported' => $this->db->fieldExists('is_reported', $this->table), 'reported' => $this->db->fieldExists('reported', $this->table), ]; return $this->reportedColumns; } private function applyUnreportedFilter($qb): void { $cols = $this->reportedColumns(); if (!empty($cols['is_reported'])) { $qb->groupStart() ->where('is_reported', 'no') ->orWhere('is_reported', 0) ->orWhere('is_reported', '0') ->orWhere('is_reported', null) ->groupEnd(); } if (!empty($cols['reported'])) { $qb->where("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))", null, false); } } }