464 lines
14 KiB
PHP
Executable File
464 lines
14 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use CodeIgniter\Model;
|
|
|
|
class AttendanceDataModel extends Model
|
|
{
|
|
protected $table = 'attendance_data';
|
|
protected $primaryKey = 'id';
|
|
protected $useAutoIncrement = true;
|
|
|
|
protected $allowedFields = [
|
|
'class_id',
|
|
'class_section_id',
|
|
'school_id',
|
|
'student_id',
|
|
'date',
|
|
'status',
|
|
'reason',
|
|
'reported',
|
|
'is_reported',
|
|
'is_notified',
|
|
'semester',
|
|
'school_year',
|
|
'modified_by',
|
|
'created_at',
|
|
'updated_at'
|
|
];
|
|
|
|
protected $useTimestamps = true;
|
|
protected $createdField = 'created_at';
|
|
protected $updatedField = 'updated_at';
|
|
|
|
private ?array $reportedColumns = null;
|
|
|
|
protected $validationRules = [
|
|
'class_id' => '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);
|
|
}
|
|
}
|
|
|
|
|
|
}
|