Files
alrahma_sunday_school_api/app/Services/AttendanceTracking/AttendanceCaseQueryService.php
T
2026-06-09 00:03:03 -04:00

339 lines
12 KiB
PHP

<?php
namespace App\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\AttendanceTracking;
use App\Models\Student;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Throwable;
class AttendanceCaseQueryService
{
public function __construct(
protected Student $studentModel,
protected AttendanceData $attendanceDataModel,
protected AttendanceTracking $attendanceTrackingModel,
protected ViolationRuleEngineService $violationRuleEngine,
protected AttendanceParentLookupService $parentLookupService
) {}
public function getStudentCase(
int $studentId,
?string $codeParam = null,
?string $incidentDate = null,
bool $includeNotified = false,
bool $pendingOnly = false,
?string $schoolYearFilter = null,
?string $semesterFilter = null,
?string $defaultSchoolYear = null,
?string $defaultSemester = null
): array {
$student = $this->studentModel->query()->find($studentId);
if (! $student) {
return [
'success' => false,
'message' => 'Student not found',
'status' => 404,
];
}
$student = $student->toArray();
$student['name'] = trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? ''));
$codeParam = strtoupper((string) ($codeParam ?? ''));
$incidentDate = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $incidentDate) ? (string) $incidentDate : '';
$statusFilter = ['absent', 'late'];
if (str_starts_with($codeParam, 'ABS')) {
$statusFilter = ['absent'];
} elseif (str_starts_with($codeParam, 'LATE')) {
$statusFilter = ['late'];
} elseif (str_starts_with($codeParam, 'MIX')) {
$statusFilter = ['absent', 'late'];
}
$schoolYear = $schoolYearFilter
?? ($incidentDate !== ''
? $this->violationRuleEngine->schoolYearForDate($incidentDate, $defaultSchoolYear)
: (string) $defaultSchoolYear);
$semester = $semesterFilter;
if ($semester === null && $codeParam === '' && $incidentDate === '') {
$semester = (string) $defaultSemester;
}
[$syStart, $syEnd] = $this->violationRuleEngine->deriveSchoolYearBounds($schoolYear ?: date('Y'));
$today = Carbon::today();
$start = Carbon::parse($syStart)->startOfDay();
$rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay();
$windowWeeks = $this->violationRuleEngine->windowWeeksForViolationCode($codeParam);
$endYmd = $incidentDate !== '' ? $incidentDate : date('Y-m-d');
$startYmd = $this->violationRuleEngine->activeWeekWindowStartYmd($endYmd, $windowWeeks, $schoolYear, $semester);
$studentIdValues = [(int) $studentId];
$studentCodeRaw = trim((string) ($student['student_code'] ?? $student['school_id'] ?? ''));
$studentCodeValues = [];
if ($studentCodeRaw !== '') {
if (is_numeric($studentCodeRaw)) {
$studentCodeNum = (int) $studentCodeRaw;
if ($studentCodeNum > 0 && $studentCodeNum !== (int) $studentId) {
$studentIdValues[] = $studentCodeNum;
}
} else {
$studentCodeValues[] = $studentCodeRaw;
}
}
$studentIdValues = array_values(array_unique(array_filter($studentIdValues, fn ($v) => $v > 0)));
$studentCodeValues = array_values(array_unique(array_filter($studentCodeValues, fn ($v) => $v !== '')));
$qb = $this->attendanceDataModel->query()
->select([
'id',
'student_id',
'class_id',
'class_section_id',
'date',
'status',
'reason',
'school_year',
'semester',
'is_reported',
'is_notified',
])
->where(function ($q) use ($studentIdValues, $studentCodeValues) {
$q->whereIn('student_id', $studentIdValues);
if (! empty($studentCodeValues)) {
$q->orWhereIn('student_id', $studentCodeValues);
}
})
->where('date', '>=', max($start->format('Y-m-d'), $startYmd))
->where('date', '<', $this->violationRuleEngine->dayBounds($endYmd)[1]);
if ($statusFilter === ['absent']) {
$qb->whereRaw("(LOWER(TRIM(status)) LIKE 'abs%' OR LOWER(TRIM(status)) = 'a')");
} elseif ($statusFilter === ['late']) {
$qb->whereRaw("(LOWER(TRIM(status)) LIKE 'late%' OR LOWER(TRIM(status)) = 'l')");
} else {
$qb->whereRaw("(LOWER(TRIM(status)) LIKE 'abs%' OR LOWER(TRIM(status)) LIKE 'late%' OR LOWER(TRIM(status)) IN ('a','l'))");
}
if ($pendingOnly) {
$this->violationRuleEngine->applyUnreportedAttendanceFilter($qb, $this->attendanceDataModel->getTable());
if (! $includeNotified) {
$qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))");
}
}
if ($schoolYearFilter !== null) {
$qb->where('school_year', $schoolYearFilter);
}
if ($semesterFilter !== null) {
$qb->where('semester', $semesterFilter);
}
$attendanceRows = $qb->orderByDesc('date')->get()->toArray();
$data = [
'student' => $student,
'attendance' => $attendanceRows,
'violation_code' => $codeParam !== '' ? $codeParam : null,
'violation_date' => $incidentDate,
'violation_notified' => null,
'show_violation_summary' => true,
'school_year' => $schoolYear,
'semester' => $semester,
];
if ($data['violation_date'] === '' && ! empty($data['attendance'][0]['date'])) {
$data['violation_date'] = substr((string) $data['attendance'][0]['date'], 0, 10);
}
if (! empty($data['attendance'][0]['is_notified'])) {
$data['violation_notified'] = $data['attendance'][0]['is_notified'];
}
if ($data['violation_date'] !== '') {
foreach ($attendanceRows as $row) {
$ymd = substr((string) ($row['date'] ?? ''), 0, 10);
if ($ymd === $data['violation_date']) {
$data['show_violation_summary'] = false;
break;
}
}
}
return [
'success' => true,
'data' => $data,
];
}
public function getNotifiedViolations(
?string $schoolYearParam = null,
?string $semesterParam = null,
?string $defaultSchoolYear = null,
?string $defaultSemester = null
): array {
$schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $defaultSchoolYear;
$semester = filled($semesterParam) ? (string) $semesterParam : (string) $defaultSemester;
$trackingData = $this->attendanceTrackingModel->query()
->where('school_year', $schoolYear)
->where('semester', $semester)
->where(function ($q) {
$q->where('is_notified', 1)
->orWhere('is_notified', '1')
->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) = 'yes'");
})
->orderByDesc('date')
->get()
->toArray();
if (empty($trackingData)) {
$latest = $this->attendanceTrackingModel->query()
->select(['school_year', 'semester'])
->where(function ($q) {
$q->where('is_notified', 1)
->orWhere('is_notified', '1')
->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) = 'yes'");
})
->orderByDesc('date')
->orderByDesc('id')
->first();
if ($latest) {
$schoolYear = (string) ($latest->school_year ?? $schoolYear);
$semester = (string) ($latest->semester ?? $semester);
$trackingData = $this->attendanceTrackingModel->query()
->where('school_year', $schoolYear)
->where('semester', $semester)
->where(function ($q) {
$q->where('is_notified', 1)
->orWhere('is_notified', '1')
->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) = 'yes'");
})
->orderByDesc('date')
->get()
->toArray();
}
}
if (empty($trackingData)) {
return [
'students' => [],
'school_year' => $schoolYear,
'semester' => $semester,
'show_actions' => false,
];
}
$studentIds = array_unique(array_map(fn ($r) => (int) ($r['student_id'] ?? 0), $trackingData));
$students = $this->studentModel->query()
->whereIn('id', $studentIds)
->where('is_active', 1)
->get()
->toArray();
$activeIdSet = array_flip(array_map(
static fn ($row) => (int) ($row['id'] ?? 0),
$students
));
$trackingData = array_values(array_filter(
$trackingData,
static fn ($row) => isset($activeIdSet[(int) ($row['student_id'] ?? 0)])
));
$studentIds = array_keys($activeIdSet);
if (empty($trackingData)) {
return [
'students' => [],
'school_year' => $schoolYear,
'semester' => $semester,
'show_actions' => false,
];
}
$studentMap = [];
foreach ($students as $s) {
$studentMap[$s['id']] = $s;
}
$classByStudent = [];
try {
$rows = DB::table('student_class as sc')
->select([
'sc.student_id',
'sc.class_section_id',
'cs.class_section_name',
'cs.class_id',
'c.class_name',
])
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
->leftJoin('classes as c', 'c.id', '=', 'cs.class_id')
->whereIn('sc.student_id', $studentIds)
->where('sc.school_year', $schoolYear)
->orderByDesc('sc.id')
->get();
foreach ($rows as $row) {
$sid = (int) ($row->student_id ?? 0);
if ($sid > 0 && ! isset($classByStudent[$sid])) {
$classByStudent[$sid] = [
'class_section_id' => $row->class_section_id ?? null,
'class_section_name' => (string) ($row->class_section_name ?? ''),
'class_id' => $row->class_id ?? null,
'class_name' => (string) ($row->class_name ?? ''),
];
}
}
} catch (Throwable) {
}
$mergedData = [];
foreach ($trackingData as $row) {
$sid = (int) ($row['student_id'] ?? 0);
if (! isset($studentMap[$sid])) {
continue;
}
$merged = array_merge($studentMap[$sid], $row);
if (isset($classByStudent[$sid])) {
$merged['class_section_name'] = (string) ($classByStudent[$sid]['class_section_name'] ?? '');
$merged['class_name'] = (string) ($classByStudent[$sid]['class_name'] ?? '');
} elseif (! empty($merged['registration_grade'])) {
$merged['grade'] = (string) $merged['registration_grade'];
}
try {
$p = $this->parentLookupService->getPrimaryParentForStudent($sid);
if ($p) {
$merged['parent_name'] = $p['parent_name'] ?? '';
$merged['parent_email'] = $p['email'] ?? '';
$merged['parent_phone'] = $p['phone'] ?? '';
}
} catch (Throwable) {
}
$mergedData[] = $merged;
}
return [
'students' => $mergedData,
'school_year' => $schoolYear,
'semester' => $semester,
];
}
}