339 lines
12 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|