257 lines
9.4 KiB
PHP
257 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\AttendanceTracking;
|
|
|
|
use App\Models\AttendanceData;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Throwable;
|
|
|
|
class AttendancePendingViolationService
|
|
{
|
|
public function __construct(
|
|
protected AttendanceData $attendanceDataModel,
|
|
protected ViolationRuleEngineService $violationRuleEngine,
|
|
protected AttendanceViolationStudentResolverService $studentResolverService
|
|
) {}
|
|
|
|
public function getPendingViolations(
|
|
string $defaultSchoolYear,
|
|
?string $schoolYearParam = null,
|
|
?string $semesterParam = null
|
|
): array {
|
|
$schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $defaultSchoolYear;
|
|
$semester = filled($semesterParam) ? (string) $semesterParam : null;
|
|
|
|
$debugInfo = [
|
|
'school_year_param' => $schoolYear,
|
|
'semester_param' => $semester,
|
|
'class_students' => 0,
|
|
'student_ids' => 0,
|
|
'attendance_rows' => 0,
|
|
'filtered_rows' => 0,
|
|
'violations' => 0,
|
|
'active_weeks' => 0,
|
|
'abs_last5' => 0,
|
|
'late_last5' => 0,
|
|
];
|
|
|
|
$resolved = $this->studentResolverService->resolveForSchoolYear($schoolYear, $semester);
|
|
|
|
$schoolYear = (string) ($resolved['school_year'] ?? $schoolYear);
|
|
$semester = $resolved['semester'] ?? $semester;
|
|
$studentIds = $resolved['student_ids'] ?? [];
|
|
$studentCodes = $resolved['student_codes'] ?? [];
|
|
$students = $resolved['students'] ?? [];
|
|
$studentCodeToId = $resolved['student_code_to_id'] ?? [];
|
|
$existingIds = $resolved['existing_ids'] ?? [];
|
|
$resolverDebug = $resolved['debug'] ?? [];
|
|
|
|
$debugInfo['class_students'] = $resolverDebug['class_students'] ?? 0;
|
|
$debugInfo['student_ids'] = $resolverDebug['student_ids'] ?? 0;
|
|
|
|
if (empty($studentIds) && empty($studentCodes)) {
|
|
return [
|
|
'students' => [],
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'error' => 'No student identifiers found for the current school year.',
|
|
'debug' => $debugInfo,
|
|
];
|
|
}
|
|
|
|
if (empty($students)) {
|
|
return [
|
|
'students' => [],
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'error' => 'No students found matching attendance records.',
|
|
'debug' => $debugInfo,
|
|
];
|
|
}
|
|
|
|
$today = Carbon::today();
|
|
[$syStart, $syEnd] = $this->violationRuleEngine->deriveSchoolYearBounds($schoolYear);
|
|
$start = Carbon::parse($syStart)->startOfDay();
|
|
$rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay();
|
|
$endEx = $rangeEnd->copy()->addDay();
|
|
|
|
$qb = $this->attendanceDataModel->query()
|
|
->select([
|
|
'id',
|
|
'student_id',
|
|
'class_id',
|
|
'class_section_id',
|
|
'date',
|
|
'status',
|
|
'reason',
|
|
'school_year',
|
|
'semester',
|
|
'is_reported',
|
|
'is_notified',
|
|
])
|
|
->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
|
->when(! empty($semester), fn ($q) => $q->where('semester', $semester))
|
|
->where(function ($q) use ($studentIds, $studentCodes) {
|
|
if (! empty($studentIds)) {
|
|
$q->whereIn('student_id', $studentIds);
|
|
}
|
|
if (! empty($studentCodes)) {
|
|
if (! empty($studentIds)) {
|
|
$q->orWhereIn('student_id', $studentCodes);
|
|
} else {
|
|
$q->whereIn('student_id', $studentCodes);
|
|
}
|
|
}
|
|
});
|
|
|
|
$this->violationRuleEngine->applyUnreportedAttendanceFilter($qb, $this->attendanceDataModel->getTable());
|
|
|
|
$qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))")
|
|
->whereRaw("LOWER(TRIM(status)) IN ('absent','late')")
|
|
->where('date', '>=', $start->format('Y-m-d'))
|
|
->where('date', '<', $endEx->format('Y-m-d'));
|
|
|
|
$termRows = $qb->orderByDesc('date')->get()->toArray();
|
|
|
|
if (empty($termRows)) {
|
|
$latestAttendanceTerm = DB::table($this->attendanceDataModel->getTable())
|
|
->select('school_year', 'semester', 'date')
|
|
->when(! empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds))
|
|
->orderByDesc('date')
|
|
->first();
|
|
|
|
if ($latestAttendanceTerm) {
|
|
$schoolYear = ! empty($latestAttendanceTerm->school_year)
|
|
? (string) $latestAttendanceTerm->school_year
|
|
: $schoolYear;
|
|
|
|
$semester = ! empty($latestAttendanceTerm->semester)
|
|
? (string) $latestAttendanceTerm->semester
|
|
: $semester;
|
|
|
|
[$syStart, $syEnd] = $this->violationRuleEngine->deriveSchoolYearBounds($schoolYear ?: $defaultSchoolYear);
|
|
$start = Carbon::parse($syStart)->startOfDay();
|
|
$rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay();
|
|
$endEx = $rangeEnd->copy()->addDay();
|
|
|
|
$qb = $this->attendanceDataModel->query()
|
|
->select([
|
|
'id',
|
|
'student_id',
|
|
'class_id',
|
|
'class_section_id',
|
|
'date',
|
|
'status',
|
|
'reason',
|
|
'school_year',
|
|
'semester',
|
|
'is_reported',
|
|
'is_notified',
|
|
])
|
|
->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
|
->when(! empty($semester), fn ($q) => $q->where('semester', $semester))
|
|
->where(function ($q) use ($studentIds, $studentCodes) {
|
|
if (! empty($studentIds)) {
|
|
$q->whereIn('student_id', $studentIds);
|
|
}
|
|
if (! empty($studentCodes)) {
|
|
if (! empty($studentIds)) {
|
|
$q->orWhereIn('student_id', $studentCodes);
|
|
} else {
|
|
$q->whereIn('student_id', $studentCodes);
|
|
}
|
|
}
|
|
});
|
|
|
|
$this->violationRuleEngine->applyUnreportedAttendanceFilter($qb, $this->attendanceDataModel->getTable());
|
|
|
|
$qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))")
|
|
->whereRaw("LOWER(TRIM(status)) IN ('absent','late')")
|
|
->where('date', '>=', $start->format('Y-m-d'))
|
|
->where('date', '<', $endEx->format('Y-m-d'));
|
|
|
|
$termRows = $qb->orderByDesc('date')->get()->toArray();
|
|
}
|
|
|
|
if (empty($termRows)) {
|
|
return [
|
|
'students' => [],
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'debug' => $debugInfo,
|
|
];
|
|
}
|
|
}
|
|
|
|
$attendanceRows = [];
|
|
foreach ($termRows as $row) {
|
|
$dStr = $row['date'] ?? null;
|
|
if (! $dStr) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$d = Carbon::parse($dStr);
|
|
} catch (Throwable) {
|
|
continue;
|
|
}
|
|
|
|
if ($d->lt($start) || ! $d->lt($endEx)) {
|
|
continue;
|
|
}
|
|
|
|
$sid = $this->studentResolverService->ensureAttendanceStudentExists(
|
|
$students,
|
|
$studentCodeToId,
|
|
$existingIds,
|
|
$row['student_id'] ?? null
|
|
);
|
|
|
|
if ($sid === null || $sid <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$row['student_id'] = $sid;
|
|
$row['status'] = strtolower(trim((string) ($row['status'] ?? '')));
|
|
$row['date'] = $d->format('Y-m-d');
|
|
|
|
$attendanceRows[] = $row;
|
|
}
|
|
|
|
if (empty($attendanceRows)) {
|
|
return [
|
|
'students' => [],
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'debug' => $debugInfo,
|
|
];
|
|
}
|
|
|
|
$debugInfo['attendance_rows'] = count($termRows);
|
|
$debugInfo['filtered_rows'] = count($attendanceRows);
|
|
|
|
$violations = $this->violationRuleEngine->computeViolations(
|
|
$students,
|
|
$attendanceRows,
|
|
$schoolYear,
|
|
null,
|
|
$attendanceRows
|
|
);
|
|
|
|
$ruleDebug = $this->violationRuleEngine->getDebugCompute();
|
|
$debugInfo['violations'] = count($violations);
|
|
$debugInfo['active_weeks'] = $ruleDebug['active_weeks'] ?? 0;
|
|
$debugInfo['by_student'] = $ruleDebug['by_student'] ?? 0;
|
|
$debugInfo['abs_last5'] = $ruleDebug['abs_last5'] ?? 0;
|
|
$debugInfo['late_last5'] = $ruleDebug['late_last5'] ?? 0;
|
|
|
|
return [
|
|
'students' => $violations,
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'debug' => $debugInfo,
|
|
];
|
|
}
|
|
}
|