reconstruction of the project
This commit is contained in:
+342
-240
@@ -4,15 +4,15 @@ namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceTracking extends BaseModel
|
||||
{
|
||||
protected $table = 'attendance_tracking';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'date',
|
||||
@@ -28,272 +28,313 @@ class AttendanceTracking extends BaseModel
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'is_reported' => 'integer',
|
||||
'is_notified' => 'integer',
|
||||
'notif_counter' => 'integer',
|
||||
'date' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'student_id' => 'integer',
|
||||
'date' => 'datetime', // CI dateFormat = datetime
|
||||
'is_reported' => 'integer',
|
||||
'is_notified' => 'integer',
|
||||
'notif_counter' => 'integer',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
/* ----------------------- Helpers ----------------------- */
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| INTERNAL HELPERS (converted from CI)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
private function normalizeDateTime(string $date): string
|
||||
/** Normalize Y-m-d (or Y-m-d H:i:s) into a full 'Y-m-d H:i:s' at midnight if time missing. */
|
||||
private static 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();
|
||||
try {
|
||||
return Carbon::parse($date, 'UTC')->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
return Carbon::now('UTC')->startOfDay()->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
|
||||
private function applyTermFilters($query, ?string $semester, ?string $schoolYear)
|
||||
/** Apply semester/schoolYear filters if provided. */
|
||||
private static function applyTermFilters($q, ?string $semester, ?string $schoolYear)
|
||||
{
|
||||
if (!empty($semester)) {
|
||||
$query->where('semester', $semester);
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
if (!empty($schoolYear)) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
if ($schoolYear !== null && $schoolYear !== '') {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
return $query;
|
||||
return $q;
|
||||
}
|
||||
|
||||
private function deriveTermFromDate(string $ymd): array
|
||||
/** Derive (semester, school_year) from a Y-m-d date (Fall=Aug–Dec; Spring=Jan–Jul). */
|
||||
private static function deriveTermFromDate(string $ymd): array
|
||||
{
|
||||
$d = Carbon::parse(substr($ymd, 0, 10));
|
||||
try {
|
||||
$d = Carbon::createFromFormat('Y-m-d', substr($ymd, 0, 10), 'UTC');
|
||||
} catch (\Throwable $e) {
|
||||
$d = Carbon::now('UTC');
|
||||
}
|
||||
|
||||
$month = $d->month;
|
||||
$year = $d->year;
|
||||
$m = (int) $d->format('n');
|
||||
$y = (int) $d->format('Y');
|
||||
|
||||
$semester = ($month >= 8 && $month <= 12) ? 'Fall' : 'Spring';
|
||||
$startY = ($month >= 8) ? $year : $year - 1;
|
||||
$semester = ($m >= 8 && $m <= 12) ? 'Fall' : 'Spring';
|
||||
$startY = ($m >= 8) ? $y : $y - 1;
|
||||
$endY = $startY + 1;
|
||||
|
||||
return [$semester, "{$startY}-{$endY}"];
|
||||
return [$semester, sprintf('%d-%d', $startY, $endY)];
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Main CRUD-ish API (converted from CI)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/** Day bounds for DATETIME searches (index-friendly). */
|
||||
private static function dayRange(string $ymd): array
|
||||
{
|
||||
$day = substr($ymd, 0, 10);
|
||||
$start = Carbon::parse($day, 'UTC')->startOfDay();
|
||||
$endEx = Carbon::parse($day, 'UTC')->addDay()->startOfDay(); // exclusive
|
||||
return [$start, $endEx];
|
||||
}
|
||||
|
||||
public function recordAbsence(
|
||||
/* ----------------------- CRUD-ish APIs ----------------------- */
|
||||
|
||||
/**
|
||||
* Records a student absence (stores time at 00:00:00 if only a day is given).
|
||||
* NOTE: $semester/$schoolYear are optional; will be derived from $date if omitted.
|
||||
* Returns inserted id.
|
||||
*/
|
||||
public static function recordAbsence(
|
||||
int $studentId,
|
||||
string $date,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null,
|
||||
bool $isExcused = false,
|
||||
?string $reason = null
|
||||
) {
|
||||
$dt = $this->normalizeDateTime($date);
|
||||
): int {
|
||||
$dt = static::normalizeDateTime($date);
|
||||
|
||||
if (!$semester || !$schoolYear) {
|
||||
[$derivedSemester, $derivedYear] = $this->deriveTermFromDate($dt);
|
||||
$semester = $semester ?? $derivedSemester;
|
||||
$schoolYear = $schoolYear ?? $derivedYear;
|
||||
if ($semester === null || $schoolYear === null) {
|
||||
[$sem, $yr] = static::deriveTermFromDate($dt);
|
||||
$semester = $semester ?? $sem;
|
||||
$schoolYear = $schoolYear ?? $yr;
|
||||
}
|
||||
|
||||
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;
|
||||
$row = static::create([
|
||||
'student_id' => $studentId,
|
||||
'date' => $dt,
|
||||
'is_reported' => $isExcused ? 1 : 0,
|
||||
'reason' => $reason,
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
|
||||
return (int) $row->id;
|
||||
}
|
||||
|
||||
public function getStudentAbsences(
|
||||
/**
|
||||
* All absences for a student; start/end accept Y-m-d or Y-m-d H:i:s.
|
||||
* For day-only ranges, we use DATETIME bounds (>= startOfDay, < nextDay) for performance.
|
||||
*/
|
||||
public static function getStudentAbsences(
|
||||
int $studentId,
|
||||
?string $startDate = null,
|
||||
?string $endDate = null,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$query = self::where('student_id', $studentId);
|
||||
) {
|
||||
$q = static::query()->where('student_id', $studentId);
|
||||
|
||||
if ($startDate) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||
$query->whereDate('date', '>=', $startDate);
|
||||
[$start, ] = static::dayRange($startDate);
|
||||
$q->where('date', '>=', $start);
|
||||
} else {
|
||||
$query->where('date', '>=', $this->normalizeDateTime($startDate));
|
||||
$q->where('date', '>=', static::normalizeDateTime($startDate));
|
||||
}
|
||||
}
|
||||
|
||||
if ($endDate) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
|
||||
$query->whereDate('date', '<=', $endDate);
|
||||
[, $endEx] = static::dayRange($endDate);
|
||||
$q->where('date', '<', $endEx);
|
||||
} else {
|
||||
$query->where('date', '<=', $this->normalizeDateTime($endDate));
|
||||
$q->where('date', '<=', static::normalizeDateTime($endDate));
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
return $query->orderBy('date', 'DESC')->get()->toArray();
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
public function getUnnotifiedUnexcusedAbsences(
|
||||
/**
|
||||
* Unexcused + unnotified absences (optionally for a specific day).
|
||||
*/
|
||||
public static function getUnnotifiedUnexcusedAbsences(
|
||||
?string $date = null,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$query = self::where('is_reported', 0)->where('is_notified', 0);
|
||||
) {
|
||||
$q = static::query()
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
if ($date) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
$query->whereDate('date', $date);
|
||||
[$start, $endEx] = static::dayRange($date);
|
||||
$q->where('date', '>=', $start)->where('date', '<', $endEx);
|
||||
} else {
|
||||
$query->where('date', $this->normalizeDateTime($date));
|
||||
$q->where('date', static::normalizeDateTime($date));
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
return $query->orderBy('date', 'DESC')->get()->toArray();
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
public function hasConsecutiveAbsences(
|
||||
/**
|
||||
* Has at least N absences in the last N days (rough heuristic).
|
||||
*/
|
||||
public static function hasConsecutiveAbsences(
|
||||
int $studentId,
|
||||
int $days,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$start = Carbon::now()->subDays($days)->toDateString();
|
||||
$startDay = Carbon::now('UTC')->subDays($days)->toDateString();
|
||||
[$start, ] = static::dayRange($startDay);
|
||||
|
||||
$query = self::where('student_id', $studentId)
|
||||
->whereDate('date', '>=', $start);
|
||||
$q = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('date', '>=', $start);
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
return $query->count() >= $days;
|
||||
return $q->count() >= $days;
|
||||
}
|
||||
|
||||
public function checkConsecutiveAbsences(
|
||||
/**
|
||||
* TRUE if last $threshold absences are exactly weekly (7-day spacing).
|
||||
*/
|
||||
public static function checkConsecutiveAbsences(
|
||||
int $studentId,
|
||||
int $threshold,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$query = self::where('student_id', $studentId)
|
||||
$q = static::query()
|
||||
->select(['date', 'is_reported', 'is_notified'])
|
||||
->where('student_id', $studentId)
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
$absences = $query->orderBy('date', 'DESC')
|
||||
->limit($threshold)
|
||||
->get()
|
||||
->toArray();
|
||||
$absences = $q->orderByDesc('date')->limit($threshold)->get()->toArray();
|
||||
|
||||
if (count($absences) < $threshold) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
$d1 = new \DateTime($absences[$i]['date']);
|
||||
$d2 = new \DateTime($absences[$i + 1]['date']);
|
||||
$diffDays = (int) $d2->diff($d1)->format('%a');
|
||||
if ($diffDays !== 7) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkAbsencesInStreak(
|
||||
/**
|
||||
* TRUE if at least $absencesThreshold absences in the last $weeksStreak weeks.
|
||||
* (Mirrors your CI behavior: count of last N fetched records)
|
||||
*/
|
||||
public static function checkAbsencesInStreak(
|
||||
int $studentId,
|
||||
int $absencesThreshold,
|
||||
int $weeksStreak,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$query = self::where('student_id', $studentId)
|
||||
$q = static::query()
|
||||
->select(['date', 'is_reported', 'is_notified'])
|
||||
->where('student_id', $studentId)
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
$records = $query->orderBy('date', 'DESC')
|
||||
->limit($weeksStreak)
|
||||
->get()
|
||||
->toArray();
|
||||
$records = $q->orderByDesc('date')->limit($weeksStreak)->get()->toArray();
|
||||
|
||||
if (count($records) < $weeksStreak) {
|
||||
return false;
|
||||
if (count($records) < $weeksStreak) return false;
|
||||
|
||||
$count = 0;
|
||||
foreach ($records as $r) {
|
||||
if ((int)($r['is_reported'] ?? 0) === 0 && (int)($r['is_notified'] ?? 0) === 0) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$count = collect($records)->filter(function ($r) {
|
||||
return $r['is_reported'] == 0 && $r['is_notified'] == 0;
|
||||
})->count();
|
||||
|
||||
return $count >= $absencesThreshold;
|
||||
}
|
||||
|
||||
public function getStudentAttendanceStats(
|
||||
/**
|
||||
* Summarize a student's status this term.
|
||||
*/
|
||||
public static function getStudentAttendanceStats(
|
||||
int $studentId,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$base = self::where('student_id', $studentId);
|
||||
$this->applyTermFilters($base, $semester, $schoolYear);
|
||||
$q1 = static::query()->where('student_id', $studentId);
|
||||
static::applyTermFilters($q1, $semester, $schoolYear);
|
||||
$total = (int) $q1->count();
|
||||
|
||||
$totalAbs = (clone $base)->count();
|
||||
$unexcusedAbs = (clone $base)->where('is_reported', 0)->count();
|
||||
$last = (clone $base)->orderBy('date', 'DESC')->first();
|
||||
$q2 = static::query()->where('student_id', $studentId)->where('is_reported', 0);
|
||||
static::applyTermFilters($q2, $semester, $schoolYear);
|
||||
$unexcused = (int) $q2->count();
|
||||
|
||||
$q3 = static::query()->where('student_id', $studentId);
|
||||
static::applyTermFilters($q3, $semester, $schoolYear);
|
||||
$last = $q3->orderByDesc('date')->first();
|
||||
|
||||
return [
|
||||
'total_absences' => $totalAbs,
|
||||
'unexcused_absences' => $unexcusedAbs,
|
||||
'last_absence' => $last ? $last->toArray() : null,
|
||||
'total_absences' => $total,
|
||||
'unexcused_absences' => $unexcused,
|
||||
'last_absence' => $last ? $last->toArray() : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function getAttendanceBySemester(
|
||||
/**
|
||||
* Fetch records by semester/schoolYear (with optional limit).
|
||||
*/
|
||||
public static 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();
|
||||
) {
|
||||
$q = static::query();
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
if ($limit > 0) $q->limit($limit);
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
public function getSemesterSummary(?string $semester = null, ?string $schoolYear = null): array
|
||||
/**
|
||||
* Summary counts for a term.
|
||||
*/
|
||||
public static function getSemesterSummary(?string $semester = null, ?string $schoolYear = null): array
|
||||
{
|
||||
$query = self::query();
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
$q1 = static::query();
|
||||
static::applyTermFilters($q1, $semester, $schoolYear);
|
||||
$total = (int) $q1->count();
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$unexcused = (clone $query)->where('is_reported', 0)->count();
|
||||
$students = (clone $query)->distinct()->count('student_id');
|
||||
$q2 = static::query()->where('is_reported', 0);
|
||||
static::applyTermFilters($q2, $semester, $schoolYear);
|
||||
$unexcused = (int) $q2->count();
|
||||
|
||||
$q3 = static::query();
|
||||
static::applyTermFilters($q3, $semester, $schoolYear);
|
||||
$students = (int) $q3->distinct('student_id')->count('student_id');
|
||||
|
||||
return [
|
||||
'total_absences' => $total,
|
||||
@@ -302,13 +343,11 @@ class AttendanceTracking extends BaseModel
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Violation Update System
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function updateAttendanceViolations(array $studentsWithViolations, string $semester, string $schoolYear): array
|
||||
/**
|
||||
* Update/create violation rows (matching by student + day-range(date) + term).
|
||||
* Expects input rows like: ['id'=>..., 'violation'=>..., 'absences'=>[['date'=>'Y-m-d'], ...]]
|
||||
*/
|
||||
public static function updateAttendanceViolations(array $studentsWithViolations, string $semester, string $schoolYear): array
|
||||
{
|
||||
$results = ['added' => 0, 'updated' => 0, 'skipped' => 0, 'notified' => 0];
|
||||
|
||||
@@ -319,46 +358,61 @@ class AttendanceTracking extends BaseModel
|
||||
}
|
||||
|
||||
foreach ($student['absences'] as $absence) {
|
||||
$ymd = substr($absence['date'] ?? now()->format('Y-m-d'), 0, 10);
|
||||
$dt = $this->normalizeDateTime($ymd);
|
||||
$ymd = substr((string)($absence['date'] ?? now()->toDateString()), 0, 10);
|
||||
[$start, $endEx] = static::dayRange($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,
|
||||
'student_id' => (int)($student['id'] ?? 0),
|
||||
'date' => static::normalizeDateTime($ymd),
|
||||
'is_reported' => 0,
|
||||
'reason' => (string)($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 ($data['student_id'] <= 0) {
|
||||
$results['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
if ($existing->reason !== $data['reason']) {
|
||||
$existing->update($data);
|
||||
$results['updated']++;
|
||||
try {
|
||||
// Match by student + day range + term
|
||||
$existing = static::query()
|
||||
->where('student_id', $data['student_id'])
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
if ($existing->is_notified) {
|
||||
$existing->update(['is_notified' => 0, 'notif_counter' => 0]);
|
||||
$results['notified']++;
|
||||
if ($existing) {
|
||||
if (($existing->reason ?? '') !== $data['reason']) {
|
||||
$existing->fill($data)->save();
|
||||
$results['updated']++;
|
||||
|
||||
if (!empty($existing->is_notified)) {
|
||||
static::resetNotification((int)$existing->id);
|
||||
$results['notified']++;
|
||||
}
|
||||
} else {
|
||||
$results['skipped']++;
|
||||
}
|
||||
} else {
|
||||
$results['skipped']++;
|
||||
}
|
||||
} else {
|
||||
$new = self::create($data);
|
||||
$results['added']++;
|
||||
$new = static::create($data);
|
||||
$results['added']++;
|
||||
|
||||
if (preg_match('/\b4\b/', $data['reason'])) {
|
||||
$new->update(['is_notified' => 1, 'notif_counter' => 1]);
|
||||
$results['notified']++;
|
||||
// Only match standalone "4"
|
||||
if (preg_match('/\b4\b/', $data['reason'])) {
|
||||
static::markAsNotified((int)$new->id);
|
||||
$results['notified']++;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// keep behavior: skip on error
|
||||
$results['skipped']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,102 +420,150 @@ class AttendanceTracking extends BaseModel
|
||||
return $results;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Notification operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/* ----------------------- Notification ops ----------------------- */
|
||||
|
||||
public function markAsNotified(int $id): bool
|
||||
public static function markAsNotified(int $id): bool
|
||||
{
|
||||
self::where('id', $id)->increment('notif_counter');
|
||||
return self::where('id', $id)->update(['is_notified' => 1]) > 0;
|
||||
// atomic increment + set flag
|
||||
$affected = static::query()
|
||||
->whereKey($id)
|
||||
->update([
|
||||
'notif_counter' => DB::raw('COALESCE(notif_counter,0)+1'),
|
||||
'is_notified' => 1,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
public function resetNotification(int $id): bool
|
||||
public static function resetNotification(int $id): bool
|
||||
{
|
||||
return self::where('id', $id)->update([
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0
|
||||
]) > 0;
|
||||
$affected = static::query()
|
||||
->whereKey($id)
|
||||
->update([
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected >= 0;
|
||||
}
|
||||
|
||||
public function markRuleAsNotified(
|
||||
/**
|
||||
* Mark rule as notified by matching latest row for that day + term.
|
||||
* $code is the rule code stored in "reason" (ABS_2, etc).
|
||||
* If no row exists, insert one.
|
||||
*/
|
||||
public static 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;
|
||||
// If you want to keep the old dependency on ConfigurationModel, you can inject it elsewhere.
|
||||
// Here we require term to be passed or derived from date.
|
||||
if ($semester === null || $schoolYear === null) {
|
||||
[$sem, $yr] = static::deriveTermFromDate($ymd);
|
||||
$semester = $semester ?? $sem;
|
||||
$schoolYear = $schoolYear ?? $yr;
|
||||
}
|
||||
|
||||
$count = ($row->notif_counter ?? 0) + 1;
|
||||
[$start, $endEx] = static::dayRange($ymd);
|
||||
|
||||
return $row->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $count,
|
||||
'is_reported' => 0,
|
||||
'updated_at' => now(),
|
||||
// 1) Match by code + day (preferred)
|
||||
$row = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->where('reason', 'like', "%{$code}%")
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
// 2) Fallback: match by day only
|
||||
if (!$row) {
|
||||
$row = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($row) {
|
||||
$counter = ((int)($row->notif_counter ?? 0)) + 1;
|
||||
|
||||
$affected = static::query()
|
||||
->whereKey((int)$row->id)
|
||||
->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $counter,
|
||||
'is_reported' => 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
// Insert a row so future runs see it as notified
|
||||
$created = static::create([
|
||||
'student_id' => $studentId,
|
||||
'date' => static::normalizeDateTime($ymd),
|
||||
'is_reported' => 0,
|
||||
'reason' => $code,
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => 1,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'note' => null,
|
||||
]);
|
||||
|
||||
return !empty($created->id);
|
||||
}
|
||||
|
||||
public function markNotified(
|
||||
/**
|
||||
* Mark the latest matching tracking row as notified and increment counter.
|
||||
* Optionally bound by $lastDate (DATETIME string).
|
||||
*/
|
||||
public static 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}%");
|
||||
$q = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($semester) {
|
||||
$query->where('semester', $semester);
|
||||
if (!empty($semester)) {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
|
||||
if ($lastDate) {
|
||||
$query->where('date', '<=', $lastDate);
|
||||
if (!empty($lastDate)) {
|
||||
$q->where('date', '<=', static::normalizeDateTime($lastDate));
|
||||
}
|
||||
|
||||
$row = $query->orderBy('date', 'DESC')->first();
|
||||
$row = $q->where('reason', 'like', "%{$code}%")
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
return 0;
|
||||
}
|
||||
if (!$row) return 0;
|
||||
|
||||
$counter = ($row->notif_counter ?? 0) + 1;
|
||||
$counter = ((int)($row->notif_counter ?? 0)) + 1;
|
||||
|
||||
return $row->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $counter,
|
||||
'updated_at' => now(),
|
||||
]) ? 1 : 0;
|
||||
$affected = static::query()
|
||||
->whereKey((int)$row->id)
|
||||
->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $counter,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user