Files
alrahma_sunday_school_api/app/Services/Attendance/AttendanceQueryService.php
T
2026-03-08 16:33:24 -04:00

604 lines
24 KiB
PHP

<?php
namespace App\Services\Attendance;
use App\Models\AttendanceData;
use App\Models\AttendanceDay;
use App\Models\AttendanceRecord;
use App\Models\Calendar;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\Student;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use App\Models\User;
use App\Models\UserRole;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use RuntimeException;
class AttendanceQueryService
{
public function __construct(
protected Configuration $configuration,
protected AttendanceData $attendanceData,
protected AttendanceDay $attendanceDay,
protected AttendanceRecord $attendanceRecord,
protected Student $student,
protected StudentClass $studentClass,
protected ClassSection $classSection,
protected TeacherClass $teacherClass,
protected Calendar $calendar,
protected User $user,
protected UserRole $userRole,
protected AttendanceService $attendanceService,
protected SemesterRangeService $semesterRangeService,
) {}
public function teacherGrid(string $semester, string $schoolYear, string $date, int $sectionCode): array
{
$bySection = $this->teacherClass->assignedBySectionForTerm($semester, $schoolYear);
$sectionCodes = array_keys($bySection);
$labels = [];
if (!empty($sectionCodes)) {
$rows = $this->classSection
->query()
->select(['class_section_id', 'class_section_name'])
->whereIn('class_section_id', $sectionCodes)
->orderBy('class_section_id')
->get()
->toArray();
foreach ($rows as $row) {
$code = (int)$row['class_section_id'];
$name = trim((string)($row['class_section_name'] ?? ''));
$labels[$code] = $name !== '' ? $name : 'Section #' . $code;
}
foreach ($sectionCodes as $code) {
$labels[$code] ??= 'Section #' . $code;
}
}
$grid = [];
$locked = false;
if ($sectionCode > 0) {
$assigned = $this->teacherClass->assignedForSectionTerm($sectionCode, $semester, $schoolYear);
$teacherIds = array_map(fn($r) => (int)$r['teacher_id'], $assigned);
$statusMap = [];
if (!empty($teacherIds)) {
$rows = DB::table('staff_attendance as sa')
->select('sa.user_id', 'sa.status', 'sa.reason')
->where('sa.semester', $semester)
->where('sa.school_year', $schoolYear)
->whereDate('sa.date', $date)
->whereIn('sa.user_id', $teacherIds)
->get();
foreach ($rows as $row) {
$statusMap[(int)$row->user_id] = [
'status' => $row->status,
'reason' => $row->reason,
];
}
}
foreach ($assigned as $teacher) {
$teacherId = (int)$teacher['teacher_id'];
$position = strtolower((string)($teacher['position'] ?? 'main'));
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
$name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
$cell = $statusMap[$teacherId] ?? ['status' => null, 'reason' => null];
$grid[] = [
'teacher_id' => $teacherId,
'name' => $name !== '' ? $name : 'User#' . $teacherId,
'position' => $position,
'status' => $cell['status'],
'reason' => $cell['reason'],
];
}
try {
$locked = method_exists($this->attendanceDay, 'isFinalized')
? $this->attendanceDay->isFinalized($sectionCode, $date, $semester, $schoolYear)
: false;
} catch (\Throwable) {
$locked = false;
}
}
return [
'semester' => $semester,
'school_year' => $schoolYear,
'date' => $date,
'class_section_id' => $sectionCode,
'sections' => $labels,
'grid' => $grid,
'locked' => $locked,
];
}
public function teacherAttendanceFormData(?int $userId, int $requestedSectionId = 0): array
{
if (!$userId) {
throw new RuntimeException('User not logged in.');
}
$teacher = $this->user->find($userId);
$teacherName = $teacher
? trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? ''))
: 'Unknown Teacher';
$semester = $this->attendanceService->currentSemester();
$schoolYear = $this->attendanceService->currentSchoolYear();
$assignments = $this->teacherClass->getClassAssignmentsByUserId($userId, $schoolYear, $semester);
if (empty($assignments)) {
throw new RuntimeException('You do not have an assigned class yet.');
}
$activeSection = null;
foreach ($assignments as $assignment) {
$cid = (int)($assignment['class_section_id'] ?? 0);
$pk = (int)($assignment['class_section_pk'] ?? 0);
if ($requestedSectionId > 0 && ($requestedSectionId === $cid || $requestedSectionId === $pk)) {
$activeSection = $assignment;
break;
}
}
$activeSection ??= $assignments[0];
$classSectionCode = (int)($activeSection['class_section_id'] ?? 0);
$classSectionPk = (int)($activeSection['class_section_pk'] ?? 0);
$classSectionId = $classSectionCode > 0 ? $classSectionCode : $classSectionPk;
if ($classSectionId <= 0) {
throw new RuntimeException('Unable to determine your class section.');
}
$today = Carbon::today();
$anchorSunday = $today->dayOfWeek === Carbon::SUNDAY ? $today->copy() : $today->copy()->next(Carbon::SUNDAY);
$sundayDates = [
$anchorSunday->copy()->subWeeks(2)->toDateString(),
$anchorSunday->copy()->subWeek()->toDateString(),
$anchorSunday->toDateString(),
];
$currentSunday = $sundayDates[2];
$students = $this->student->getByClassAndYear($classSectionId, $schoolYear, $semester);
if (empty($students) && $classSectionPk > 0 && $classSectionPk !== $classSectionId) {
$students = $this->student->getByClassAndYear($classSectionPk, $schoolYear, $semester);
}
$attendanceRows = [];
$lockedByStudent = [];
foreach ($students as $student) {
$studentId = (int)$student['id'];
$rows = AttendanceData::query()
->select(['date', 'status', 'is_reported', 'reason', 'modified_by'])
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->where(function ($q) use ($classSectionId) {
$q->where('class_section_id', $classSectionId)
->orWhere('class_section_id', (int)$classSectionId);
})
->whereIn('date', $sundayDates)
->get()
->map(fn($row) => (array)$row)
->all();
$rows = $this->attendanceService->normalizeAttendanceEntries($rows);
$recentRows = AttendanceData::query()
->select(['date', 'status'])
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->where(function ($q) use ($classSectionId) {
$q->where('class_section_id', $classSectionId)
->orWhere('class_section_id', (int)$classSectionId);
})
->orderByDesc('date')
->limit(3)
->get()
->toArray();
$recentHistory = [];
foreach ($recentRows as $row) {
$recentHistory[] = [
'date' => substr((string)($row['date'] ?? ''), 0, 10),
'status' => strtolower((string)($row['status'] ?? '')),
];
}
$snapshot = [];
$rowsByDate = [];
foreach ($rows as $entry) {
$d = (string)($entry['date'] ?? '');
$snapshot[$d] = $entry['status'] ?? null;
$rowsByDate[$d] = $entry;
}
$statusHistory = [];
foreach ($sundayDates as $date) {
$statusHistory[$date] = array_key_exists($date, $snapshot)
? $snapshot[$date]
: ($date === $currentSunday ? null : 'N/A');
}
$todayRow = $rowsByDate[$currentSunday] ?? null;
$lockInfo = $this->attendanceService->detectExternalSubmission($todayRow);
$todayReason = $todayRow['reason'] ?? null;
$attendanceRows[] = [
'student' => $student,
'sunday_statuses' => $statusHistory,
'today' => $statusHistory[$currentSunday] ?? null,
'today_reason' => $todayReason,
'locked' => $lockInfo['locked'],
'locked_source' => $lockInfo['source'],
'recent_three' => $recentHistory,
];
$lockedByStudent[$studentId] = [
'locked' => $lockInfo['locked'],
'source' => $lockInfo['source'],
'reason' => $todayReason,
'status' => $statusHistory[$currentSunday] ?? null,
];
}
$assigned = $this->teacherClass->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
$teacherRows = [];
if (!empty($assigned)) {
$teacherIds = array_map(static fn($r) => (int)$r['teacher_id'], $assigned);
$statusMap = [];
if (!empty($teacherIds)) {
$rows = DB::table('staff_attendance as sa')
->select('sa.user_id', 'sa.date', 'sa.status', 'sa.reason')
->where('sa.semester', $semester)
->where('sa.school_year', $schoolYear)
->whereIn('sa.user_id', $teacherIds)
->whereIn('sa.date', $sundayDates)
->get();
foreach ($rows as $row) {
$statusMap[(int)$row->user_id][(string)$row->date] = [
'status' => $row->status,
'reason' => $row->reason,
];
}
}
foreach ($assigned as $teacher) {
$teacherId = (int)$teacher['teacher_id'];
$position = strtolower((string)($teacher['position'] ?? 'main'));
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
$name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
$history = [];
foreach ($sundayDates as $date) {
$history[$date] = isset($statusMap[$teacherId][$date]['status'])
? $statusMap[$teacherId][$date]['status']
: ($date === $currentSunday ? null : 'N/A');
}
$teacherRows[] = [
'teacher_id' => $teacherId,
'position' => $position,
'name' => $name !== '' ? $name : 'User#' . $teacherId,
'sunday_statuses' => $history,
'today' => $history[$currentSunday] ?? null,
];
}
}
$noSchoolDays = [];
$calendarRows = $this->calendar->query()
->where('no_school', 1)
->whereIn('date', $sundayDates)
->where('semester', $semester)
->where('school_year', $schoolYear)
->get()
->toArray();
foreach ($calendarRows as $row) {
$d = (string)($row['date'] ?? '');
$noSchoolDays[$d] = [
'title' => $row['title'] ?? 'No School',
'description' => $row['description'] ?? '',
];
}
return [
'teacher_name' => $teacherName,
'students' => $attendanceRows,
'teachers' => $teacherRows,
'class_section_id' => $classSectionId,
'sunday_dates' => $sundayDates,
'current_sunday' => $currentSunday,
'enable_attendance' => (int)$this->configuration->getConfig('enable_attendance'),
'can_edit' => ((int)$this->configuration->getConfig('enable_attendance') === 1),
'class_id' => (int)$this->classSection->getClassId($classSectionId),
'no_school_days' => $noSchoolDays,
'today' => $currentSunday,
'locked_by_student' => $lockedByStudent,
];
}
public function buildDailyAttendanceData(string $termSemester, string $termYear): array
{
$classSections = $this->classSection
->query()
->select('classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name')
->join('student_class as sc', 'sc.class_section_id', '=', 'classSection.class_section_id')
->where('sc.school_year', $termYear)
->groupBy('classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name')
->get()
->map(fn($row) => (array)$row)
->all();
$attendanceData = [];
$attendanceRecord = [];
$studentsBySection = [];
$grades = [];
$datesBySection = [];
$studentSchoolMap = [];
$sectionCounts = $this->studentClass->getStudentCountsBySection($termYear);
foreach ($classSections as $classSection) {
$secPk = (int)($classSection['id'] ?? 0);
$secCodeRaw = (string)($classSection['class_section_id'] ?? '');
$secCode = $secCodeRaw !== '' ? $secCodeRaw : (string)$secPk;
$classId = (int)($classSection['class_id'] ?? 0);
$studentsBySection[$secCode] = [];
$datesBySection[$secCode] = [];
$attendanceData[$secCode] = [];
$attendanceRecord[$secCode] = [];
$countForCode = (int)($sectionCounts[$secCode] ?? 0);
$countForPk = (int)($sectionCounts[$secPk] ?? 0);
$sectionHasStudents = ($countForCode + $countForPk) > 0;
$students = $this->studentClass->getClassStudents($secCode, $termYear);
if (!$students && $secPk && (string)$secPk !== (string)$secCode) {
$students = $this->studentClass->getClassStudents($secPk, $termYear);
}
if (!$sectionHasStudents || !$students) {
continue;
}
$hasRoster = false;
foreach ($students as $sc) {
$studentId = (int)$sc['student_id'];
$student = $this->student
->query()
->select(['id', 'firstname', 'lastname', 'school_id'])
->where('id', $studentId)
->where('is_active', 1)
->first();
if (!$student) {
continue;
}
$student = $student->toArray();
$studentsBySection[$secCode][] = $student;
$studentSchoolMap[$studentId] = (string)($student['school_id'] ?? '');
$hasRoster = true;
$entries = AttendanceData::query()
->where('student_id', $studentId)
->where(function ($q) use ($secCode, $secPk) {
$q->where('class_section_id', $secCode);
if ($secPk && (string)$secPk !== (string)$secCode) {
$q->orWhere('class_section_id', $secPk);
}
})
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
->when($termYear !== '', fn($q) => $q->where('school_year', $termYear))
->orderBy('date')
->get()
->map(fn($row) => $row->toArray())
->all();
$entries = $this->attendanceService->normalizeAttendanceEntries($entries);
$attendanceData[$secCode][$studentId] = $entries;
foreach ($entries as $entry) {
$d = (string)($entry['date'] ?? '');
if ($d !== '') {
$datesBySection[$secCode][$d] = true;
}
}
$summary = AttendanceRecord::query()
->where('student_id', $studentId)
->where(function ($q) use ($secCode, $secPk) {
$q->where('class_section_id', $secCode);
if ($secPk && (string)$secPk !== (string)$secCode) {
$q->orWhere('class_section_id', $secPk);
}
})
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
->when($termYear !== '', fn($q) => $q->where('school_year', $termYear))
->first();
$attendanceRecord[$secCode][$studentId] = $summary?->toArray() ?? [
'total_presence' => 0,
'total_late' => 0,
'total_absence' => 0,
'total_attendance' => 0,
];
}
if (!$hasRoster) {
unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]);
continue;
}
$grades[$classId][] = $classSection;
}
foreach ($attendanceData as $secId => &$studentEntries) {
foreach ($studentEntries as &$entries) {
usort($entries, static fn($a, $b) => strcmp((string)($a['date'] ?? ''), (string)($b['date'] ?? '')));
}
}
unset($studentEntries, $entries);
foreach ($datesBySection as $sec => $set) {
$arr = array_keys($set);
sort($arr, SORT_STRING);
$datesBySection[$sec] = $arr;
}
ksort($grades, SORT_NUMERIC);
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($termYear);
$semesterNorm = $this->semesterRangeService->normalizeSemester($termSemester);
if ($semesterNorm !== '') {
$semRange = $this->semesterRangeService->getSemesterRange($termYear, $semesterNorm);
if ($semRange) {
[$rangeStart, $rangeEnd] = $semRange;
}
}
$dateList = $this->semesterRangeService->buildSundayList($rangeStart, $rangeEnd);
$noSchoolDays = [];
$events = $this->calendar->query()
->where('no_school', 1)
->where('date', '>=', $rangeStart)
->where('date', '<=', $rangeEnd)
->get()
->toArray();
foreach ($events as $event) {
$d = substr((string)($event['date'] ?? ''), 0, 10);
if ($d !== '') {
$noSchoolDays[$d] = true;
}
}
$anchorSunday = now()->dayOfWeek === Carbon::SUNDAY
? now()->toDateString()
: now()->next(Carbon::SUNDAY)->toDateString();
$passedDatesSet = [];
foreach ($dateList as $d) {
if ($d <= $anchorSunday && empty($noSchoolDays[$d])) {
$passedDatesSet[$d] = true;
}
}
$totalPassedDays = count($passedDatesSet);
if ($totalPassedDays > 0 && $termYear !== '' && $termSemester !== '') {
foreach ($attendanceData as $secCode => $studentEntries) {
foreach ($studentEntries as $studentId => $entries) {
$statusByDate = [];
foreach ($entries as $entry) {
$d = substr((string)($entry['date'] ?? ''), 0, 10);
if ($d === '' || empty($passedDatesSet[$d])) {
continue;
}
$st = strtolower(trim((string)($entry['status'] ?? '')));
if (!in_array($st, ['present', 'absent', 'late'], true)) {
continue;
}
$statusByDate[$d] = $st;
}
$p = 0;
$l = 0;
$a = 0;
foreach ($statusByDate as $st) {
if ($st === 'present') {
$p++;
} elseif ($st === 'late') {
$l++;
} elseif ($st === 'absent') {
$a++;
}
}
$sum = $p + $l + $a;
$record = $attendanceRecord[$secCode][$studentId] ?? [];
$storedSum = (int)($record['total_presence'] ?? 0)
+ (int)($record['total_late'] ?? 0)
+ (int)($record['total_absence'] ?? 0);
if ($storedSum !== $totalPassedDays && $storedSum !== $sum) {
DB::table('attendance_record')
->updateOrInsert(
[
'student_id' => $studentId,
'semester' => $termSemester,
'school_year' => $termYear,
],
[
'class_section_id' => $secCode,
'school_id' => $studentSchoolMap[$studentId] ?? '',
'total_presence' => $p,
'total_late' => $l,
'total_absence' => $a,
'total_attendance' => $sum,
'modified_by' => auth()->id(),
'updated_at' => now(),
'created_at' => now(),
]
);
$attendanceRecord[$secCode][$studentId]['total_presence'] = $p;
$attendanceRecord[$secCode][$studentId]['total_late'] = $l;
$attendanceRecord[$secCode][$studentId]['total_absence'] = $a;
$attendanceRecord[$secCode][$studentId]['total_attendance'] = $sum;
}
}
}
}
$adminName = '';
if (auth()->id()) {
$u = $this->user->query()->select('firstname', 'lastname')->find(auth()->id());
if ($u) {
$adminName = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? ''));
}
}
return [
'attendanceData' => $attendanceData,
'attendanceRecord' => $attendanceRecord,
'studentsBySection' => $studentsBySection,
'grades' => $grades,
'datesBySection' => $datesBySection,
'dateList' => $dateList,
'noSchoolDays' => $noSchoolDays,
'totalPassedDays' => $totalPassedDays,
'semester' => $termSemester,
'schoolYear' => $termYear,
'currentAdminName' => $adminName,
];
}
}