468 lines
14 KiB
PHP
Executable File
468 lines
14 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Models\BaseModel;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
class AttendanceTracking extends BaseModel
|
|
{
|
|
protected $table = 'attendance_tracking';
|
|
protected $primaryKey = 'id';
|
|
public $timestamps = true;
|
|
|
|
const CREATED_AT = 'created_at';
|
|
const UPDATED_AT = 'updated_at';
|
|
protected $fillable = [
|
|
'student_id',
|
|
'date',
|
|
'is_reported',
|
|
'reason',
|
|
'note',
|
|
'is_notified',
|
|
'notif_counter',
|
|
'semester',
|
|
'school_year',
|
|
'created_at',
|
|
'updated_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'student_id' => '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;
|
|
}
|
|
}
|