json()->all(); if (!is_array($data) || $data === []) { $data = $request->all(); } $badgeCode = trim((string) ($data['badgeCode'] ?? $data['BadgeCode'] ?? $data['badge_code'] ?? $data['barcode'] ?? $data['Barcode'] ?? '')); $stationId = trim((string) ($data['stationId'] ?? $data['StationId'] ?? $data['station_id'] ?? $data['deviceId'] ?? $data['DeviceId'] ?? '')); $action = strtolower(trim((string) ($data['action'] ?? 'attendance'))); $scanStatus = strtolower(trim((string) ($data['status'] ?? 'present'))); $scannedAt = $this->normalizeScannedAt((string) ($data['scannedAt'] ?? $data['ScannedAt'] ?? $data['scanned_at'] ?? '')); $operatorId = (int) (auth()->id() ?? 0); if ($badgeCode === '') { $this->logScan($badgeCode, null, null, $action, 'rejected', $operatorId, $stationId, 'Badge code is required.', $scannedAt); return response()->json($this->failure('Badge code is required.'), 400); } if (!in_array($scanStatus, ['present', 'late'], true)) { $scanStatus = 'present'; } $student = $this->findStudentByBadge($badgeCode); if ($student) { return $this->processStudentScan($student, $badgeCode, $stationId, $action, $scanStatus, $scannedAt, $operatorId); } $staffUser = $this->findStaffUserByBadge($badgeCode); if ($staffUser) { return $this->processStaffScan($staffUser, $badgeCode, $stationId, $action, $scanStatus, $scannedAt, $operatorId); } $this->logScan($badgeCode, null, null, $action, 'not_found', $operatorId, $stationId, 'Badge not found.', $scannedAt); return response()->json($this->failure('Badge not found.'), 404); } private function processStudentScan(Student $student, string $badgeCode, string $stationId, string $action, string $status, string $scannedAt, int $operatorId): JsonResponse { $term = $this->currentTerm($student->toArray()); $schoolNow = $this->schoolLocalDateTime($scannedAt); $date = $schoolNow->format('Y-m-d'); $lateCutoff = $this->lateCutoffForDate($schoolNow); $autoLate = $this->isAfterLateCutoff($schoolNow, $lateCutoff); if ($autoLate) { $status = 'late'; } $assignment = $this->studentAssignment((int) $student->id, $term['semester'], $term['school_year']); if (!$assignment) { $this->logScan($badgeCode, 'student', (int) $student->id, $action, 'rejected', $operatorId, $stationId, 'Student has no class assignment for current term.', $scannedAt); return response()->json($this->failure('Student has no class assignment for current term.'), 409); } $classSectionId = (int) $assignment['class_section_id']; $classId = (int) ($assignment['class_id'] ?? 0); $existing = $this->findAttendanceRow((int) $student->id, (string) ($student->school_id ?? ''), $date, $term['semester'], $term['school_year']); $oldStatus = $existing?->status; $now = utc_now(); $row = [ 'class_id' => $classId, 'class_section_id' => $classSectionId, 'student_id' => (int) $student->id, 'school_id' => (string) ($student->school_id ?? ''), 'date' => $date, 'status' => $status, 'reason' => null, 'semester' => $term['semester'], 'school_year' => $term['school_year'], 'modified_by' => $operatorId, 'updated_at' => $now, ]; if (Schema::hasColumn('attendance_data', 'is_reported')) { $row['is_reported'] = 'no'; } if (Schema::hasColumn('attendance_data', 'reported')) { $row['reported'] = 0; } if (Schema::hasColumn('attendance_data', 'is_notified')) { $row['is_notified'] = 'no'; } if ($existing) { $existing->fill($row); $existing->save(); if (strtolower((string) $oldStatus) !== $status) { $this->adjustAttendanceRecord( $classSectionId, (int) $student->id, (string) ($student->school_id ?? ''), $status, $term['semester'], $term['school_year'], $oldStatus, $operatorId ); } } else { $row['created_at'] = $now; AttendanceData::query()->create($row); $this->addAttendanceRecord($classSectionId, (int) $student->id, (string) ($student->school_id ?? ''), $status, $term['semester'], $term['school_year'], $operatorId); } $this->ensureAttendanceDay($classSectionId, $date, $term['semester'], $term['school_year']); $message = $status === 'late' ? 'Student late attendance recorded.' : 'Student attendance recorded.'; $this->logScan($badgeCode, 'student', (int) $student->id, $action, 'accepted', $operatorId, $stationId, $message, $scannedAt); $studentSummary = $this->studentSummary((int) $student->id, (int) ($student->parent_id ?? 0), $term['semester'], $term['school_year']); $person = $this->studentPayload($student->toArray(), $assignment); $lateSlip = null; $warnings = []; if ($autoLate) { $lateSlip = $this->createLateSlipPayload( $student->toArray(), $assignment, $term, $schoolNow, $lateCutoff, $operatorId, $existing ? (string) ($oldStatus ?? '') : null ); $warnings[] = 'Student arrived after ' . $lateCutoff->format('h:i A') . '. Late slip receipt generated.'; } return response()->json($this->successPayload([ 'personType' => 'student', 'person' => $person, 'Person' => $this->clientPersonPayload($person, 'student', true), 'attendance' => [ 'status' => $status, 'date' => $date, 'scannedAt' => $scannedAt, 'duplicate' => (bool) $existing, ], 'Attendance' => $this->clientAttendancePayload($status, $message, $scannedAt), 'studentSummary' => $studentSummary, 'StudentSummary' => $this->clientStudentSummaryPayload($studentSummary), 'lateSlip' => $lateSlip, 'LateSlip' => $lateSlip, 'warnings' => $warnings, 'Warnings' => $warnings, ], $message)); } private function processStaffScan(User $user, string $badgeCode, string $stationId, string $action, string $status, string $scannedAt, int $operatorId): JsonResponse { $term = $this->currentTerm($user->toArray()); $date = substr($scannedAt, 0, 10); $roleName = $this->primaryRoleName((int) $user->id); StaffAttendance::upsertOne( (int) $user->id, $roleName, $date, $term['semester'], $term['school_year'], $status, null, $operatorId ); $this->logScan($badgeCode, 'staff', (int) $user->id, $action, 'accepted', $operatorId, $stationId, 'Staff attendance recorded.', $scannedAt); $person = [ 'id' => (int) $user->id, 'name' => trim((string) ($user->firstname ?? '') . ' ' . (string) ($user->lastname ?? '')), 'email' => $user->email, 'role' => $roleName, 'badgeCode' => $badgeCode, ]; return response()->json($this->successPayload([ 'personType' => 'staff', 'person' => $person, 'Person' => $this->clientPersonPayload($person, 'staff', true), 'attendance' => [ 'status' => $status, 'date' => $date, 'scannedAt' => $scannedAt, ], 'Attendance' => $this->clientAttendancePayload($status, 'Staff attendance recorded.', $scannedAt), 'studentSummary' => null, 'StudentSummary' => null, 'warnings' => [], 'Warnings' => [], ], 'Staff attendance recorded.')); } private function findStudentByBadge(string $badgeCode): ?Student { return Student::query() ->where(function ($query) use ($badgeCode) { $query->where('rfid_tag', $badgeCode) ->orWhere('school_id', $badgeCode); }) ->first(); } private function findStaffUserByBadge(string $badgeCode): ?User { $user = User::query() ->where(function ($query) use ($badgeCode) { $query->where('rfid_tag', $badgeCode); if (ctype_digit($badgeCode)) { $query->orWhere('school_id', (int) $badgeCode); } }) ->first(); if (!$user || !$this->isStaffUser((int) $user->id)) { return null; } return $user; } private function isStaffUser(int $userId): bool { $roles = DB::table('user_roles as ur') ->selectRaw('LOWER(r.name) AS role_name') ->leftJoin('roles as r', 'r.id', '=', 'ur.role_id') ->where('ur.user_id', $userId) ->get(); foreach ($roles as $role) { $name = trim((string) ($role->role_name ?? '')); if ($name !== '' && !in_array($name, ['parent', 'guest', 'student'], true)) { return true; } } return false; } private function primaryRoleName(int $userId): ?string { return DB::table('user_roles as ur') ->leftJoin('roles as r', 'r.id', '=', 'ur.role_id') ->where('ur.user_id', $userId) ->orderBy('r.name') ->value('r.name'); } private function studentAssignment(int $studentId, string $semester, string $schoolYear): ?array { $builder = DB::table('student_class as sc') ->select('sc.class_section_id', 'cs.class_id', 'cs.class_section_name') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') ->where('sc.student_id', $studentId); if (Schema::hasColumn('student_class', 'school_year')) { $builder->where('sc.school_year', $schoolYear); } if (Schema::hasColumn('student_class', 'semester')) { $builder->where('sc.semester', $semester); } $row = $builder->orderByDesc('sc.id')->first(); if (!$row && Schema::hasColumn('student_class', 'school_year')) { $row = DB::table('student_class as sc') ->select('sc.class_section_id', 'cs.class_id', 'cs.class_section_name') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') ->where('sc.student_id', $studentId) ->where('sc.school_year', $schoolYear) ->orderByDesc('sc.id') ->first(); } return $row ? (array) $row : null; } private function findAttendanceRow(int $studentId, string $schoolId, string $date, string $semester, string $schoolYear): ?AttendanceData { $row = AttendanceData::query() ->where('student_id', $studentId) ->whereDate('date', $date) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); if ($row) { return $row; } return AttendanceData::query() ->where('school_id', $schoolId) ->whereDate('date', $date) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); } private function adjustAttendanceRecord(int $classSectionId, int $studentId, string $schoolId, string $newStatus, string $semester, string $schoolYear, ?string $oldStatus, int $operatorId): void { $record = AttendanceRecord::query() ->where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); if (!$record) { $this->addAttendanceRecord($classSectionId, $studentId, $schoolId, $newStatus, $semester, $schoolYear, $operatorId); return; } $presence = (int) ($record->total_presence ?? 0); $absence = (int) ($record->total_absence ?? 0); $late = (int) ($record->total_late ?? 0); if ($oldStatus === 'present') { $presence--; } elseif ($oldStatus === 'absent') { $absence--; } elseif ($oldStatus === 'late') { $late--; } if ($newStatus === 'present') { $presence++; } elseif ($newStatus === 'absent') { $absence++; } elseif ($newStatus === 'late') { $late++; } $record->fill([ 'class_section_id' => $classSectionId, 'total_presence' => max(0, $presence), 'total_absence' => max(0, $absence), 'total_late' => max(0, $late), 'modified_by' => $operatorId, 'updated_at' => utc_now(), ]); $record->save(); } private function addAttendanceRecord(int $classSectionId, int $studentId, string $schoolId, string $status, string $semester, string $schoolYear, int $operatorId): void { $record = AttendanceRecord::query() ->where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); $field = $status === 'late' ? 'total_late' : ($status === 'absent' ? 'total_absence' : 'total_presence'); if ($record) { $record->fill([ 'class_section_id' => $classSectionId, $field => (int) ($record->{$field} ?? 0) + 1, 'total_attendance' => (int) ($record->total_attendance ?? 0) + 1, 'modified_by' => $operatorId, 'updated_at' => utc_now(), ]); $record->save(); return; } AttendanceRecord::query()->create([ 'class_section_id' => $classSectionId, 'student_id' => $studentId, 'school_id' => $schoolId, 'total_presence' => $status === 'present' ? 1 : 0, 'total_absence' => $status === 'absent' ? 1 : 0, 'total_late' => $status === 'late' ? 1 : 0, 'total_attendance' => 1, 'semester' => $semester, 'school_year' => $schoolYear, 'modified_by' => $operatorId, 'created_at' => utc_now(), 'updated_at' => utc_now(), ]); } private function ensureAttendanceDay(int $classSectionId, string $date, string $semester, string $schoolYear): void { if (AttendanceDay::findBySectionDate($classSectionId, $date, $semester, $schoolYear)) { return; } AttendanceDay::query()->create([ 'class_section_id' => $classSectionId, 'date' => $date, 'status' => 'draft', 'semester' => $semester, 'school_year' => $schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now(), ]); } private function studentPayload(array $student, array $assignment): array { return [ 'id' => (int) ($student['id'] ?? 0), 'schoolId' => $student['school_id'] ?? null, 'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')), 'firstName' => $student['firstname'] ?? null, 'lastName' => $student['lastname'] ?? null, 'grade' => $student['registration_grade'] ?? null, 'classSectionId' => isset($assignment['class_section_id']) ? (int) $assignment['class_section_id'] : null, 'classSectionName' => $assignment['class_section_name'] ?? null, 'badgeCode' => !empty($student['rfid_tag']) ? $student['rfid_tag'] : ($student['school_id'] ?? null), ]; } private function studentSummary(int $studentId, int $parentId, string $semester, string $schoolYear): array { $attendance = AttendanceRecord::query() ->where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); $score = SemesterScore::query() ->where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->orderByDesc('id') ->first(); $flags = CurrentFlag::query() ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->whereIn('flag_state', ['open', 'Opened', 'Open']) ->orderByDesc('flag_datetime') ->limit(10) ->get(); $payments = Payment::query() ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->orderByDesc('payment_date') ->limit(5) ->get(); return [ 'attendance' => [ 'present' => (int) ($attendance->total_presence ?? 0), 'late' => (int) ($attendance->total_late ?? 0), 'absent' => (int) ($attendance->total_absence ?? 0), 'total' => (int) ($attendance->total_attendance ?? 0), 'score' => isset($score?->attendance_score) ? (float) $score->attendance_score : null, ], 'grading' => [ 'homeworkAvg' => isset($score?->homework_avg) ? (float) $score->homework_avg : null, 'quizAvg' => isset($score?->quiz_avg) ? (float) $score->quiz_avg : null, 'projectAvg' => isset($score?->project_avg) ? (float) $score->project_avg : null, 'midtermExamScore' => isset($score?->midterm_exam_score) ? (float) $score->midterm_exam_score : null, 'finalExamScore' => isset($score?->final_exam_score) ? (float) $score->final_exam_score : null, 'semesterScore' => isset($score?->semester_score) ? (float) $score->semester_score : null, ], 'discipline' => $flags->map(static fn (CurrentFlag $flag): array => [ 'id' => (int) $flag->id, 'flag' => $flag->flag, 'state' => $flag->flag_state, 'description' => $flag->open_description, 'date' => $flag->flag_datetime?->toDateTimeString(), ])->all(), 'payments' => $payments->map(static fn (Payment $payment): array => [ 'id' => (int) $payment->id, 'invoiceId' => isset($payment->invoice_id) ? (int) $payment->invoice_id : null, 'totalAmount' => isset($payment->total_amount) ? (float) $payment->total_amount : null, 'paidAmount' => isset($payment->paid_amount) ? (float) $payment->paid_amount : null, 'balance' => isset($payment->balance) ? (float) $payment->balance : null, 'status' => $payment->status, 'date' => $payment->payment_date?->toDateTimeString(), ])->all(), ]; } private function clientPersonPayload(array $person, string $type, bool $active): array { return [ 'id' => (int) ($person['id'] ?? 0), 'name' => (string) ($person['name'] ?? ''), 'type' => $type, 'barcode' => (string) ($person['badgeCode'] ?? $person['schoolId'] ?? ''), 'active' => $active, ]; } private function clientAttendancePayload(string $status, string $message, string $scannedAt): array { return [ 'status' => $status, 'message' => $message, 'time' => gmdate('c', strtotime($scannedAt) ?: time()), ]; } private function clientStudentSummaryPayload(array $summary): array { $attendance = $summary['attendance'] ?? []; $payments = $summary['payments'] ?? []; $lastAttendance = $attendance['lastAttendance'] ?? null; $outstandingBalance = 0.0; $paymentStatus = 'Unknown'; if (!empty($payments)) { $latest = $payments[0]; $paymentStatus = (string) ($latest['status'] ?? 'Unknown'); $outstandingBalance = (float) ($latest['balance'] ?? 0); } $issues = $this->scanIssues($summary); return [ 'lastAttendance' => $lastAttendance, 'attendanceCount' => (int) ($attendance['total'] ?? 0), 'paymentStatus' => $paymentStatus, 'outstandingBalance' => $outstandingBalance, 'packageStatus' => $outstandingBalance > 0 ? 'Balance due' : 'Clear', 'issueLevel' => $this->highestIssueLevel($issues), 'issues' => $issues, ]; } private function scanIssues(array $summary): array { $issues = []; $payments = $summary['payments'] ?? []; if (!empty($payments)) { $latest = $payments[0]; $balance = (float) ($latest['balance'] ?? 0); $status = strtolower(trim((string) ($latest['status'] ?? ''))); if ($balance > 0) { $issues[] = [ 'category' => 'payment', 'severity' => 'critical', 'title' => 'Payment balance due', 'detail' => '$' . number_format($balance, 2) . ' outstanding', ]; } elseif ($status !== '' && !in_array($status, ['paid', 'full', 'full payment', 'complete', 'completed'], true)) { $issues[] = [ 'category' => 'payment', 'severity' => 'warning', 'title' => 'Payment status needs review', 'detail' => (string) ($latest['status'] ?? ''), ]; } } $grading = $summary['grading'] ?? []; $semesterScore = $grading['semesterScore'] ?? null; if ($semesterScore !== null && (float) $semesterScore < 70) { $issues[] = [ 'category' => 'grades', 'severity' => (float) $semesterScore < 60 ? 'critical' : 'warning', 'title' => 'Grade concern', 'detail' => 'Semester score ' . number_format((float) $semesterScore, 1), ]; } foreach (['homeworkAvg' => 'Homework', 'quizAvg' => 'Quiz', 'projectAvg' => 'Project'] as $key => $label) { if (isset($grading[$key]) && $grading[$key] !== null && (float) $grading[$key] < 70) { $issues[] = [ 'category' => 'grades', 'severity' => 'warning', 'title' => $label . ' average below target', 'detail' => number_format((float) $grading[$key], 1), ]; } } $attendance = $summary['attendance'] ?? []; if ((int) ($attendance['late'] ?? 0) >= 3) { $issues[] = [ 'category' => 'attendance', 'severity' => 'warning', 'title' => 'Repeated lateness', 'detail' => (int) $attendance['late'] . ' late attendance records', ]; } foreach (($summary['discipline'] ?? []) as $flag) { $flagType = strtolower(trim((string) ($flag['flag'] ?? ''))); $category = $this->scanIssueCategory($flagType); $issues[] = [ 'category' => $category, 'severity' => in_array($category, ['behavior', 'discipline'], true) ? 'critical' : 'warning', 'title' => $this->scanIssueTitle($flagType, $category), 'detail' => trim((string) ($flag['description'] ?? '')) ?: null, ]; } return $issues; } private function scanIssueCategory(string $flagType): string { if (str_contains($flagType, 'payment')) { return 'payment'; } if (str_contains($flagType, 'grade')) { return 'grades'; } if (str_contains($flagType, 'attendance') || str_contains($flagType, 'tard')) { return 'attendance'; } if ( str_contains($flagType, 'behavior') || str_contains($flagType, 'defiance') || str_contains($flagType, 'disrespect') || str_contains($flagType, 'bullying') || str_contains($flagType, 'fighting') || str_contains($flagType, 'threat') || str_contains($flagType, 'harassment') ) { return 'behavior'; } return 'discipline'; } private function scanIssueTitle(string $flagType, string $category): string { $label = trim(str_replace('_', ' ', $flagType)); if ($label !== '') { return ucwords($label) . ' flag'; } return match ($category) { 'payment' => 'Payment flag', 'grades' => 'Grade flag', 'attendance' => 'Attendance flag', 'behavior' => 'Behavior flag', default => 'Discipline flag', }; } private function highestIssueLevel(array $issues): string { $level = 'clear'; foreach ($issues as $issue) { $severity = strtolower((string) ($issue['severity'] ?? '')); if ($severity === 'critical') { return 'critical'; } if ($severity === 'warning') { $level = 'warning'; } elseif ($severity === 'info' && $level === 'clear') { $level = 'info'; } } return $level; } private function createLateSlipPayload(array $student, array $assignment, array $term, \DateTimeImmutable $schoolNow, \DateTimeImmutable $lateCutoff, int $operatorId, ?string $previousStatus): array { $studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')); $grade = (string) ($student['registration_grade'] ?? $assignment['class_section_name'] ?? ''); $cutoffLabel = $lateCutoff->format('h:i A'); $reason = 'Arrived after ' . $cutoffLabel; $adminName = $this->operatorName($operatorId); $slip = [ 'school_year' => $term['school_year'], 'semester' => $term['semester'], 'student_name' => $studentName, 'slip_date' => $schoolNow->format('Y-m-d'), 'time_in' => $schoolNow->format('H:i:s'), 'grade' => $grade, 'reason' => $reason, 'admin_name' => $adminName, ]; $logId = null; $logged = false; if ($previousStatus !== 'late') { try { $created = LateSlipLog::query()->create([ 'school_year' => $slip['school_year'], 'semester' => $slip['semester'], 'student_name' => $slip['student_name'], 'slip_date' => $slip['slip_date'], 'time_in' => $slip['time_in'], 'grade' => $slip['grade'], 'reason' => $slip['reason'], 'admin_name' => $slip['admin_name'], 'printed_by' => $operatorId ?: null, 'printed_at' => utc_now(), ]); $logId = (int) ($created->id ?? 0); $logged = $logId > 0; } catch (\Throwable $e) { Log::error('Badge scan late slip log failed: ' . $e->getMessage()); } } $displayDate = $schoolNow->format('m/d/Y'); $displayTime = $schoolNow->format('h:i A'); return [ 'required' => true, 'logged' => $logged, 'logId' => $logId ?: null, 'logUrl' => url('/administrator/late_slips'), 'schoolYear' => $slip['school_year'], 'semester' => $slip['semester'], 'studentName' => $studentName, 'date' => $displayDate, 'timeIn' => $displayTime, 'grade' => $grade, 'reason' => $reason, 'adminName' => $adminName, 'cutoffTime' => $cutoffLabel, 'message' => 'Late slip receipt generated for arrival after ' . $cutoffLabel . '.', 'text' => implode("\n", [ 'Al Rahma School at ISGL', 'School Year: ' . $slip['school_year'], 'Student Name: ' . $studentName, 'Date: ' . $displayDate . ' Time In: ' . $displayTime, 'Grade: ' . $grade, 'Reason: ' . $reason, 'Admin: ' . $adminName, ]), ]; } private function isAfterLateCutoff(\DateTimeImmutable $schoolNow, \DateTimeImmutable $cutoff): bool { return $schoolNow >= $cutoff; } private function lateCutoffForDate(\DateTimeImmutable $schoolNow): \DateTimeImmutable { $configured = trim((string) (Configuration::getConfig(self::LATE_CUTOFF_CONFIG_KEY) ?: self::DEFAULT_LATE_CUTOFF)); $timezone = $schoolNow->getTimezone(); $date = $schoolNow->format('Y-m-d'); foreach (['H:i:s', 'H:i', 'g:i A', 'h:i A', 'g:i a', 'h:i a'] as $format) { $parsed = \DateTimeImmutable::createFromFormat('!' . $format, $configured, $timezone); if ($parsed instanceof \DateTimeImmutable) { return $schoolNow->setTime((int) $parsed->format('H'), (int) $parsed->format('i'), (int) $parsed->format('s')); } } $timestamp = strtotime($date . ' ' . $configured); if ($timestamp !== false) { return (new \DateTimeImmutable('@' . $timestamp))->setTimezone($timezone); } Log::warning(self::LATE_CUTOFF_CONFIG_KEY . ' has invalid value "' . $configured . '"; using ' . self::DEFAULT_LATE_CUTOFF); return $schoolNow->setTime(10, 0, 0); } private function schoolLocalDateTime(string $scannedAt): \DateTimeImmutable { $timezone = new \DateTimeZone('America/New_York'); try { $utc = new \DateTimeZone('UTC'); return (new \DateTimeImmutable($scannedAt, $utc))->setTimezone($timezone); } catch (\Throwable) { return new \DateTimeImmutable('now', $timezone); } } private function operatorName(int $operatorId): string { if ($operatorId > 0) { try { $user = User::query()->select('firstname', 'lastname', 'email')->find($operatorId); if ($user) { $name = trim((string) ($user->firstname ?? '') . ' ' . (string) ($user->lastname ?? '')); if ($name !== '') { return $name; } return (string) ($user->email ?? ''); } } catch (\Throwable $e) { Log::error('Badge scan operator lookup failed: ' . $e->getMessage()); } } return ''; } private function currentTerm(array $row): array { $semester = (string) (Configuration::getConfig('semester') ?: ($row['semester'] ?? '')); $schoolYear = (string) (Configuration::getConfig('school_year') ?: ($row['school_year'] ?? '')); return [ 'semester' => $semester, 'school_year' => $schoolYear, ]; } private function normalizeScannedAt(string $value): string { if ($value !== '') { $timestamp = strtotime($value); if ($timestamp !== false) { return date('Y-m-d H:i:s', $timestamp); } } return utc_now(); } private function logScan(string $badgeCode, ?string $personType, ?int $personId, string $action, string $status, int $operatorId, string $stationId, string $message, string $scannedAt): void { try { if (!Schema::hasTable('badge_scan_logs')) { return; } DB::table('badge_scan_logs')->insert([ 'badge_code' => $badgeCode, 'person_type' => $personType, 'person_id' => $personId, 'scan_action' => $action, 'scan_status' => $status, 'operator_user_id' => $operatorId ?: null, 'station_id' => $stationId ?: null, 'message' => $message, 'scanned_at' => $scannedAt, 'created_at' => utc_now(), ]); } catch (\Throwable $e) { Log::error('Badge scan log failed: ' . $e->getMessage()); } } private function successPayload(array $payload, string $message): array { return [ 'ok' => true, 'status' => true, 'success' => true, 'message' => $message, ] + $payload; } private function failure(string $message): array { return [ 'ok' => false, 'status' => false, 'success' => false, 'message' => $message, ]; } }