add school year model
This commit is contained in:
@@ -48,18 +48,39 @@ class AttendanceManagementService
|
||||
});
|
||||
}
|
||||
|
||||
$rows = $query->orderByDesc('follow_up_required')
|
||||
$schoolYear = trim((string) ($filters['school_year'] ?? ''));
|
||||
$semester = $this->normalizeSemester($filters['semester'] ?? null);
|
||||
$limit = max(1, (int) ($filters['limit'] ?? 250));
|
||||
|
||||
$rowCollection = $query->orderByDesc('follow_up_required')
|
||||
->orderBy('follow_up_completed')
|
||||
->orderBy('person_name')
|
||||
->limit((int) ($filters['limit'] ?? 250))
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->map(fn ($r) => $this->hydrateAcademicContext((array) $r))
|
||||
->filter(function (array $row) use ($schoolYear, $semester): bool {
|
||||
if ($schoolYear !== '' && (string) ($row['school_year'] ?? '') !== $schoolYear) {
|
||||
return false;
|
||||
}
|
||||
if ($semester !== '' && (string) ($row['semester'] ?? '') !== $semester) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$rows = $rowCollection
|
||||
->take($limit)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'date' => $date,
|
||||
'summary' => $this->summaryForDate($date),
|
||||
'filters' => $this->availableFilters(),
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'current_year' => $this->currentSchoolYear(),
|
||||
'current_semester' => $this->currentSemester(),
|
||||
'summary' => $this->summaryForDate($date, $rowCollection->values()->all(), $schoolYear, $semester),
|
||||
'filters' => $this->availableFilters($rows),
|
||||
'rows' => $rows,
|
||||
];
|
||||
}
|
||||
@@ -75,6 +96,7 @@ class AttendanceManagementService
|
||||
$status = $isLate ? self::STATUS_LATE : self::STATUS_PRESENT;
|
||||
$counts = $this->countsFor($person['type'], $person['id'], $entryAt->toDateString(), $status);
|
||||
$badgeExceptionCount = $this->badgeExceptionCount($person['type'], $person['id'], $entryAt->toDateString()) + 1;
|
||||
$academicContext = $this->academicContextForDate($entryAt);
|
||||
|
||||
$row = [
|
||||
'person_type' => $person['type'],
|
||||
@@ -83,6 +105,8 @@ class AttendanceManagementService
|
||||
'role_grade' => $person['role_grade'],
|
||||
'badge_id' => $person['badge_id'],
|
||||
'event_date' => $entryAt->toDateString(),
|
||||
'school_year' => $academicContext['school_year'],
|
||||
'semester' => $academicContext['semester'],
|
||||
'attendance_status' => $status,
|
||||
'report_status' => $reportStatus,
|
||||
'entry_time' => $entryAt,
|
||||
@@ -144,6 +168,7 @@ class AttendanceManagementService
|
||||
|
||||
$status = $entryAt->gt($this->schoolStartFor($entryAt)) ? self::STATUS_LATE : self::STATUS_PRESENT;
|
||||
$counts = $this->countsFor($person['type'], $person['id'], $entryAt->toDateString(), $status);
|
||||
$academicContext = $this->academicContextForDate($entryAt);
|
||||
$row = [
|
||||
'person_type' => $person['type'],
|
||||
'person_id' => $person['id'],
|
||||
@@ -151,6 +176,8 @@ class AttendanceManagementService
|
||||
'role_grade' => $person['role_grade'],
|
||||
'badge_id' => $badge,
|
||||
'event_date' => $entryAt->toDateString(),
|
||||
'school_year' => $academicContext['school_year'],
|
||||
'semester' => $academicContext['semester'],
|
||||
'attendance_status' => $status,
|
||||
'report_status' => $reportStatus,
|
||||
'entry_time' => $entryAt,
|
||||
@@ -189,6 +216,7 @@ class AttendanceManagementService
|
||||
$reportStatus = $authorized ? 'reported' : $this->normalizeReportStatus($data['report_status'] ?? null);
|
||||
$counts = $this->countsFor($person['type'], $person['id'], $exitAt->toDateString(), self::STATUS_EARLY_DISMISSAL);
|
||||
$badgeExceptions = $this->badgeExceptionCount($person['type'], $person['id'], $exitAt->toDateString()) + ((($data['exit_method'] ?? '') === 'manual_exit') ? 1 : 0);
|
||||
$academicContext = $this->academicContextForDate($exitAt);
|
||||
$row = [
|
||||
'person_type' => $person['type'],
|
||||
'person_id' => $person['id'],
|
||||
@@ -196,6 +224,8 @@ class AttendanceManagementService
|
||||
'role_grade' => $person['role_grade'],
|
||||
'badge_id' => $person['badge_id'],
|
||||
'event_date' => $exitAt->toDateString(),
|
||||
'school_year' => $academicContext['school_year'],
|
||||
'semester' => $academicContext['semester'],
|
||||
'attendance_status' => self::STATUS_EARLY_DISMISSAL,
|
||||
'report_status' => $reportStatus,
|
||||
'exit_time' => $exitAt,
|
||||
@@ -233,6 +263,7 @@ class AttendanceManagementService
|
||||
$date = $this->dateOnly($data['date'] ?? now()->toDateString());
|
||||
$reportStatus = $this->normalizeReportStatus($data['report_status'] ?? null);
|
||||
$counts = $this->countsFor($person['type'], $person['id'], $date, self::STATUS_ABSENT);
|
||||
$academicContext = $this->academicContextForDate(Carbon::parse($date));
|
||||
$row = [
|
||||
'person_type' => $person['type'],
|
||||
'person_id' => $person['id'],
|
||||
@@ -240,6 +271,8 @@ class AttendanceManagementService
|
||||
'role_grade' => $person['role_grade'],
|
||||
'badge_id' => $person['badge_id'],
|
||||
'event_date' => $date,
|
||||
'school_year' => $academicContext['school_year'],
|
||||
'semester' => $academicContext['semester'],
|
||||
'attendance_status' => self::STATUS_ABSENT,
|
||||
'report_status' => $reportStatus,
|
||||
'reason' => $data['reason'] ?? null,
|
||||
@@ -290,6 +323,10 @@ class AttendanceManagementService
|
||||
throw new \RuntimeException('Attendance management event not found.');
|
||||
}
|
||||
$count = (int) DB::table('late_slip_reprints')->where('attendance_management_event_id', $eventId)->count() + 1;
|
||||
$academicContext = [
|
||||
'school_year' => (string) ($event['school_year'] ?? $this->currentSchoolYear()),
|
||||
'semester' => (string) ($event['semester'] ?? $this->currentSemester()),
|
||||
];
|
||||
$row = [
|
||||
'attendance_management_event_id' => $eventId,
|
||||
'late_slip_log_id' => $data['late_slip_log_id'] ?? null,
|
||||
@@ -297,6 +334,8 @@ class AttendanceManagementService
|
||||
'student_name' => $event['person_name'] ?? null,
|
||||
'slip_number' => $data['slip_number'] ?? ('AME-'.$eventId),
|
||||
'reprinted_at' => now(),
|
||||
'school_year' => $academicContext['school_year'],
|
||||
'semester' => $academicContext['semester'],
|
||||
'reprinted_by' => $actor?->id,
|
||||
'reason' => $data['reason'] ?? 'Reprint requested',
|
||||
'reprint_count' => $count,
|
||||
@@ -370,8 +409,8 @@ class AttendanceManagementService
|
||||
private function printLateSlip(array $event, ?User $actor): array
|
||||
{
|
||||
$payload = [
|
||||
'school_year' => Configuration::getConfigValueByKey('school_year') ?? '',
|
||||
'semester' => Configuration::getConfigValueByKey('semester') ?? '',
|
||||
'school_year' => (string) ($event['school_year'] ?? $this->currentSchoolYear()),
|
||||
'semester' => (string) ($event['semester'] ?? $this->currentSemester()),
|
||||
'student_name' => $event['person_name'] ?? '',
|
||||
'slip_date' => $event['event_date'] ?? now()->toDateString(),
|
||||
'time_in' => Carbon::parse($event['official_entry_time'] ?? now())->format('H:i:s'),
|
||||
@@ -389,12 +428,15 @@ class AttendanceManagementService
|
||||
|
||||
private function recordBadgeException(int $eventId, array $person, Carbon $date, string $reason, int $count, ?User $actor, ?string $notes): void
|
||||
{
|
||||
$academicContext = $this->academicContextForDate($date);
|
||||
DB::table('badge_exceptions')->insert([
|
||||
'attendance_management_event_id' => $eventId,
|
||||
'person_type' => $person['type'],
|
||||
'person_id' => $person['id'],
|
||||
'person_name' => $person['name'],
|
||||
'exception_date' => $date->toDateString(),
|
||||
'school_year' => $academicContext['school_year'],
|
||||
'semester' => $academicContext['semester'],
|
||||
'exception_type' => 'manual_entry',
|
||||
'badge_status' => $reason,
|
||||
'exception_count' => $count,
|
||||
@@ -431,28 +473,55 @@ class AttendanceManagementService
|
||||
->count();
|
||||
}
|
||||
|
||||
private function summaryForDate(string $date): array
|
||||
private function summaryForDate(string $date, array $rows = [], string $schoolYear = '', string $semester = ''): array
|
||||
{
|
||||
$base = DB::table('attendance_management_events')->whereDate('event_date', $date);
|
||||
$badgeExceptions = DB::table('badge_exceptions')->whereDate('exception_date', $date);
|
||||
$this->applyAcademicFilters($badgeExceptions, $schoolYear, $semester);
|
||||
|
||||
$lateSlipReprints = DB::table('late_slip_reprints')->whereDate('reprinted_at', $date);
|
||||
$this->applyAcademicFilters($lateSlipReprints, $schoolYear, $semester);
|
||||
|
||||
return [
|
||||
'present' => (clone $base)->where('attendance_status', self::STATUS_PRESENT)->count(),
|
||||
'absent' => (clone $base)->where('attendance_status', self::STATUS_ABSENT)->count(),
|
||||
'late' => (clone $base)->where('attendance_status', self::STATUS_LATE)->count(),
|
||||
'early_dismissal' => (clone $base)->where('attendance_status', self::STATUS_EARLY_DISMISSAL)->count(),
|
||||
'not_reported' => (clone $base)->where('report_status', 'not_reported')->count(),
|
||||
'follow_up_required' => (clone $base)->where('follow_up_required', true)->where('follow_up_completed', false)->count(),
|
||||
'badge_exceptions' => DB::table('badge_exceptions')->whereDate('exception_date', $date)->count(),
|
||||
'late_slip_reprints' => DB::table('late_slip_reprints')->whereDate('reprinted_at', $date)->count(),
|
||||
'present' => $this->countRowsByStatus($rows, self::STATUS_PRESENT),
|
||||
'absent' => $this->countRowsByStatus($rows, self::STATUS_ABSENT),
|
||||
'late' => $this->countRowsByStatus($rows, self::STATUS_LATE),
|
||||
'early_dismissal' => $this->countRowsByStatus($rows, self::STATUS_EARLY_DISMISSAL),
|
||||
'not_reported' => $this->countRowsByReportStatus($rows, 'not_reported'),
|
||||
'follow_up_required' => $this->countRowsRequiringFollowUp($rows),
|
||||
'badge_exceptions' => $badgeExceptions->count(),
|
||||
'late_slip_reprints' => $lateSlipReprints->count(),
|
||||
];
|
||||
}
|
||||
|
||||
private function availableFilters(): array
|
||||
private function availableFilters(array $rows = []): array
|
||||
{
|
||||
$schoolYears = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): string => trim((string) ($row['school_year'] ?? '')),
|
||||
$rows,
|
||||
))));
|
||||
rsort($schoolYears);
|
||||
|
||||
$currentYear = $this->currentSchoolYear();
|
||||
if ($currentYear !== '' && ! in_array($currentYear, $schoolYears, true)) {
|
||||
array_unshift($schoolYears, $currentYear);
|
||||
}
|
||||
|
||||
$semesters = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): string => trim((string) ($row['semester'] ?? '')),
|
||||
$rows,
|
||||
))));
|
||||
$currentSemester = $this->currentSemester();
|
||||
if ($currentSemester !== '' && ! in_array($currentSemester, $semesters, true)) {
|
||||
array_unshift($semesters, $currentSemester);
|
||||
}
|
||||
|
||||
return [
|
||||
'attendance_status' => [self::STATUS_PRESENT, self::STATUS_ABSENT, self::STATUS_ABSENT_PENDING, self::STATUS_LATE, self::STATUS_LATE_WITHOUT_SLIP, self::STATUS_EARLY_DISMISSAL],
|
||||
'report_status' => ['reported', 'not_reported', 'pending_clarification'],
|
||||
'risk_level' => ['low', 'medium', 'high', 'very_high', 'critical', 'severe'],
|
||||
'person_type' => ['student', 'staff', 'contractor', 'visitor'],
|
||||
'school_years' => $schoolYears,
|
||||
'semesters' => $semesters,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -462,6 +531,91 @@ class AttendanceManagementService
|
||||
return Carbon::parse($date->toDateString().' '.$configured, $date->timezone);
|
||||
}
|
||||
|
||||
private function hydrateAcademicContext(array $row): array
|
||||
{
|
||||
$date = trim((string) ($row['event_date'] ?? ''));
|
||||
if ($date === '') {
|
||||
return $row;
|
||||
}
|
||||
|
||||
$context = $this->academicContextForDate(Carbon::parse($date));
|
||||
if (trim((string) ($row['school_year'] ?? '')) === '') {
|
||||
$row['school_year'] = $context['school_year'];
|
||||
}
|
||||
if (trim((string) ($row['semester'] ?? '')) === '') {
|
||||
$row['semester'] = $context['semester'];
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function academicContextForDate(Carbon $date): array
|
||||
{
|
||||
$derivedYear = $this->deriveSchoolYearForDate($date);
|
||||
$derivedSemester = $this->deriveSemesterForDate($date);
|
||||
|
||||
if ($date->isSameDay(now())) {
|
||||
return [
|
||||
'school_year' => $this->currentSchoolYear() ?: $derivedYear,
|
||||
'semester' => $this->currentSemester() ?: $derivedSemester,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'school_year' => $derivedYear ?: $this->currentSchoolYear(),
|
||||
'semester' => $derivedSemester ?: $this->currentSemester(),
|
||||
];
|
||||
}
|
||||
|
||||
private function deriveSchoolYearForDate(Carbon $date): string
|
||||
{
|
||||
$startYear = $date->month >= 8 ? $date->year : ($date->year - 1);
|
||||
|
||||
return sprintf('%d-%d', $startYear, $startYear + 1);
|
||||
}
|
||||
|
||||
private function deriveSemesterForDate(Carbon $date): string
|
||||
{
|
||||
return ($date->month >= 8 || $date->month === 1) ? 'Fall' : 'Spring';
|
||||
}
|
||||
|
||||
private function currentSchoolYear(): string
|
||||
{
|
||||
return trim((string) (Configuration::getConfigValueByKey('school_year') ?? ''));
|
||||
}
|
||||
|
||||
private function currentSemester(): string
|
||||
{
|
||||
return $this->normalizeSemester(Configuration::getConfig('semester'));
|
||||
}
|
||||
|
||||
private function applyAcademicFilters($query, string $schoolYear = '', string $semester = ''): void
|
||||
{
|
||||
if ($schoolYear !== '') {
|
||||
$query->where('school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
}
|
||||
|
||||
private function countRowsByStatus(array $rows, string $status): int
|
||||
{
|
||||
return count(array_filter($rows, static fn (array $row): bool => (string) ($row['attendance_status'] ?? '') === $status));
|
||||
}
|
||||
|
||||
private function countRowsByReportStatus(array $rows, string $status): int
|
||||
{
|
||||
return count(array_filter($rows, static fn (array $row): bool => (string) ($row['report_status'] ?? '') === $status));
|
||||
}
|
||||
|
||||
private function countRowsRequiringFollowUp(array $rows): int
|
||||
{
|
||||
return count(array_filter($rows, static function (array $row): bool {
|
||||
return (bool) ($row['follow_up_required'] ?? false) && ! (bool) ($row['follow_up_completed'] ?? false);
|
||||
}));
|
||||
}
|
||||
|
||||
private function dateTime($value): Carbon { return $value instanceof Carbon ? $value : Carbon::parse((string) $value); }
|
||||
private function dateOnly($value): string { return Carbon::parse((string) $value)->toDateString(); }
|
||||
|
||||
@@ -471,6 +625,19 @@ class AttendanceManagementService
|
||||
return in_array($v, ['reported', 'not_reported', 'pending_clarification'], true) ? $v : 'pending_clarification';
|
||||
}
|
||||
|
||||
private function normalizeSemester($value): string
|
||||
{
|
||||
$semester = strtolower(trim((string) $value));
|
||||
if ($semester === 'fall') {
|
||||
return 'Fall';
|
||||
}
|
||||
if ($semester === 'spring') {
|
||||
return 'Spring';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function combinationCode(int $abs, int $late, int $ed, int $badge): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
Reference in New Issue
Block a user