'integer', 'is_reported' => 'integer', 'is_notified' => 'integer', 'notif_counter' => 'integer', 'date' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /* |-------------------------------------------------------------------------- | Relationships |-------------------------------------------------------------------------- */ public function student() { return $this->belongsTo(Student::class, 'student_id'); } /* |-------------------------------------------------------------------------- | INTERNAL HELPERS (converted from CI) |-------------------------------------------------------------------------- */ private function normalizeDateTime(string $date): string { if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $date)) { return $date; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return $date . ' 00:00:00'; } return Carbon::parse($date)->toDateTimeString(); } private function applyTermFilters($query, ?string $semester, ?string $schoolYear) { if (!empty($semester)) { $query->where('semester', $semester); } if (!empty($schoolYear)) { $query->where('school_year', $schoolYear); } return $query; } private function deriveTermFromDate(string $ymd): array { $d = Carbon::parse(substr($ymd, 0, 10)); $month = $d->month; $year = $d->year; $semester = ($month >= 8 && $month <= 12) ? 'Fall' : 'Spring'; $startY = ($month >= 8) ? $year : $year - 1; $endY = $startY + 1; return [$semester, "{$startY}-{$endY}"]; } /* |-------------------------------------------------------------------------- | Main CRUD-ish API (converted from CI) |-------------------------------------------------------------------------- */ public function recordAbsence( int $studentId, string $date, ?string $semester = null, ?string $schoolYear = null, bool $isExcused = false, ?string $reason = null ) { $dt = $this->normalizeDateTime($date); if (!$semester || !$schoolYear) { [$derivedSemester, $derivedYear] = $this->deriveTermFromDate($dt); $semester = $semester ?? $derivedSemester; $schoolYear = $schoolYear ?? $derivedYear; } return self::create([ 'student_id' => $studentId, 'date' => $dt, 'is_reported' => $isExcused ? 1 : 0, 'reason' => $reason, 'is_notified' => 0, 'notif_counter' => 0, 'semester' => $semester, 'school_year' => $schoolYear, ])->id; } public function getStudentAbsences( int $studentId, ?string $startDate = null, ?string $endDate = null, ?string $semester = null, ?string $schoolYear = null ): array { $query = self::where('student_id', $studentId); if ($startDate) { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { $query->whereDate('date', '>=', $startDate); } else { $query->where('date', '>=', $this->normalizeDateTime($startDate)); } } if ($endDate) { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) { $query->whereDate('date', '<=', $endDate); } else { $query->where('date', '<=', $this->normalizeDateTime($endDate)); } } $this->applyTermFilters($query, $semester, $schoolYear); return $query->orderBy('date', 'DESC')->get()->toArray(); } public function getUnnotifiedUnexcusedAbsences( ?string $date = null, ?string $semester = null, ?string $schoolYear = null ): array { $query = self::where('is_reported', 0)->where('is_notified', 0); if ($date) { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { $query->whereDate('date', $date); } else { $query->where('date', $this->normalizeDateTime($date)); } } $this->applyTermFilters($query, $semester, $schoolYear); return $query->orderBy('date', 'DESC')->get()->toArray(); } public function hasConsecutiveAbsences( int $studentId, int $days, ?string $semester = null, ?string $schoolYear = null ): bool { $start = Carbon::now()->subDays($days)->toDateString(); $query = self::where('student_id', $studentId) ->whereDate('date', '>=', $start); $this->applyTermFilters($query, $semester, $schoolYear); return $query->count() >= $days; } public function checkConsecutiveAbsences( int $studentId, int $threshold, ?string $semester = null, ?string $schoolYear = null ): bool { $query = self::where('student_id', $studentId) ->where('is_reported', 0) ->where('is_notified', 0); $this->applyTermFilters($query, $semester, $schoolYear); $absences = $query->orderBy('date', 'DESC') ->limit($threshold) ->get() ->toArray(); if (count($absences) < $threshold) { return false; } for ($i = 0; $i < $threshold - 1; $i++) { $d1 = Carbon::parse($absences[$i]['date']); $d2 = Carbon::parse($absences[$i + 1]['date']); if ($d2->diffInDays($d1) !== 7) { return false; } } return true; } public function checkAbsencesInStreak( int $studentId, int $absencesThreshold, int $weeksStreak, ?string $semester = null, ?string $schoolYear = null ): bool { $query = self::where('student_id', $studentId) ->where('is_reported', 0) ->where('is_notified', 0); $this->applyTermFilters($query, $semester, $schoolYear); $records = $query->orderBy('date', 'DESC') ->limit($weeksStreak) ->get() ->toArray(); if (count($records) < $weeksStreak) { return false; } $count = collect($records)->filter(function ($r) { return $r['is_reported'] == 0 && $r['is_notified'] == 0; })->count(); return $count >= $absencesThreshold; } public function getStudentAttendanceStats( int $studentId, ?string $semester = null, ?string $schoolYear = null ): array { $base = self::where('student_id', $studentId); $this->applyTermFilters($base, $semester, $schoolYear); $totalAbs = (clone $base)->count(); $unexcusedAbs = (clone $base)->where('is_reported', 0)->count(); $last = (clone $base)->orderBy('date', 'DESC')->first(); return [ 'total_absences' => $totalAbs, 'unexcused_absences' => $unexcusedAbs, 'last_absence' => $last ? $last->toArray() : null, ]; } public function getAttendanceBySemester( ?string $semester = null, ?string $schoolYear = null, int $limit = 0 ): array { $query = self::query(); $this->applyTermFilters($query, $semester, $schoolYear); if ($limit > 0) { $query->limit($limit); } return $query->orderBy('date', 'DESC')->get()->toArray(); } public function getSemesterSummary(?string $semester = null, ?string $schoolYear = null): array { $query = self::query(); $this->applyTermFilters($query, $semester, $schoolYear); $total = (clone $query)->count(); $unexcused = (clone $query)->where('is_reported', 0)->count(); $students = (clone $query)->distinct()->count('student_id'); return [ 'total_absences' => $total, 'unexcused_absences' => $unexcused, 'students_with_absences' => $students, ]; } /* |-------------------------------------------------------------------------- | Violation Update System |-------------------------------------------------------------------------- */ public function updateAttendanceViolations(array $studentsWithViolations, string $semester, string $schoolYear): array { $results = ['added' => 0, 'updated' => 0, 'skipped' => 0, 'notified' => 0]; foreach ($studentsWithViolations as $student) { if (empty($student['absences'])) { $results['skipped']++; continue; } foreach ($student['absences'] as $absence) { $ymd = substr($absence['date'] ?? now()->format('Y-m-d'), 0, 10); $dt = $this->normalizeDateTime($ymd); $data = [ 'student_id' => (int)$student['id'], 'date' => $dt, 'is_reported' => 0, 'reason' => $student['violation'] ?? '', 'is_notified' => 0, 'notif_counter' => 0, 'semester' => $semester, 'school_year' => $schoolYear, ]; $existing = self::where('student_id', $data['student_id']) ->whereDate('date', $ymd) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); if ($existing) { if ($existing->reason !== $data['reason']) { $existing->update($data); $results['updated']++; if ($existing->is_notified) { $existing->update(['is_notified' => 0, 'notif_counter' => 0]); $results['notified']++; } } else { $results['skipped']++; } } else { $new = self::create($data); $results['added']++; if (preg_match('/\b4\b/', $data['reason'])) { $new->update(['is_notified' => 1, 'notif_counter' => 1]); $results['notified']++; } } } } return $results; } /* |-------------------------------------------------------------------------- | Notification operations |-------------------------------------------------------------------------- */ public function markAsNotified(int $id): bool { self::where('id', $id)->increment('notif_counter'); return self::where('id', $id)->update(['is_notified' => 1]) > 0; } public function resetNotification(int $id): bool { return self::where('id', $id)->update([ 'is_notified' => 0, 'notif_counter' => 0 ]) > 0; } public function markRuleAsNotified( int $studentId, string $code, string $ymd, ?string $semester = null, ?string $schoolYear = null ): bool { $semester = $semester ?? config('school.semester'); $schoolYear = $schoolYear ?? config('school.year'); $start = Carbon::parse($ymd)->startOfDay(); $end = Carbon::parse($ymd)->addDay()->startOfDay(); $row = self::where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereBetween('date', [$start, $end]) ->where('reason', 'like', "%{$code}%") ->orderBy('date', 'DESC') ->first(); if (!$row) { $row = self::create([ 'student_id' => $studentId, 'date' => $start, 'is_reported' => 0, 'reason' => $code, 'is_notified' => 1, 'notif_counter' => 1, 'semester' => $semester, 'school_year' => $schoolYear, ]); return true; } $count = ($row->notif_counter ?? 0) + 1; return $row->update([ 'is_notified' => 1, 'notif_counter' => $count, 'is_reported' => 0, 'updated_at' => now(), ]); } public function markNotified( int $studentId, string $schoolYear, ?string $semester, string $code, ?string $lastDate = null ): int { $query = self::where('student_id', $studentId) ->where('school_year', $schoolYear) ->where('reason', 'like', "%{$code}%"); if ($semester) { $query->where('semester', $semester); } if ($lastDate) { $query->where('date', '<=', $lastDate); } $row = $query->orderBy('date', 'DESC')->first(); if (!$row) { return 0; } $counter = ($row->notif_counter ?? 0) + 1; return $row->update([ 'is_notified' => 1, 'notif_counter' => $counter, 'updated_at' => now(), ]) ? 1 : 0; } }