fcm = $fcm; $this->notificationHelper = $notificationHelper; $this->qrLinkService = $qrLinkService; $coordinate = config('coordinate.coordinate', '-7.604032330848524, 112.10176449791652'); [$lat, $lon] = array_map('trim', explode(',', $coordinate)); $this->latOfAttendance = (float) $lat; $this->lonOfAttendance = (float) $lon; // load maximum distance tolerance from config (meters) $this->distanceMaximumTolerance = (int) config('coordinate.max_distance_tolerance', 1000); } public function qrAttendance(Request $request) { /// check academic period active or not. try { // request input validation $request->validate([ 'id_student' => 'required|integer', 'id_class' => 'required|integer', 'qrcode' => 'required|string', 'latitude' => 'required|numeric', 'longitude' => 'required|numeric', ]); // request data variable // $idStudent = $request->id_student; // $idClass = $request->id_class; // $rawCode = $request->qrcode; // get request based on auth user $user = $request->user(); if (!$user) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); } // check if user has student profile $student = $user->student ?? null; if (!$student) { return response()->json(['success' => false, 'message' => 'Student profile not found for user'], 403); } // // use student id from raw body for testing purpose. // $idStudent = $request->id_student; // $student = Student::find($idStudent); // if (!$student) { // return response()->json(['success' => false, 'message' => 'Student not found'], 404); // } $idStudent = $student->id; $idClass = $request->id_class; $rawCode = $request->qrcode; // Coordinate Location from device client $lat = $request->latitude; $lon = $request->longitude; // Center of School Location $schoolLat = $this->latOfAttendance; $schoolLon = $this->lonOfAttendance; // count distance with helper $distance = getDistanceInMeters($lat, $lon, $schoolLat, $schoolLon); if ($distance > $this->distanceMaximumTolerance) { return response()->json([ 'success' => false, 'message' => 'Lokasi di luar jangkauan', ], 403); } // log error_log('distance: '); error_log($distance); // request time // Jika client mengirimkan `date` (format ISO atau yang bisa di-parse oleh Carbon), // gunakan itu sebagai waktu sekarang untuk keperluan testing/override. if ($request->filled('date')) { $now = Carbon::parse($request->input('date'), 'Asia/Jakarta'); } else { $now = Carbon::now('Asia/Jakarta'); // testing using date: 1 April 2026 07:15 WIB (should be valid for schedule 7-8) // $now = Carbon::parse('2026-05-18 07:15:00', 'Asia/Jakarta'); // $now = Carbon::parse('Asia/Jakarta'); } $dayOfWeek = strtolower($now->locale('id')->dayName); // example: "senin" // schedule search (compare with qrcode) => class of student and schedule search $schedule = Schedule::query() ->where('id_class', $idClass) ->where('code', $rawCode) ->whereHas('academicPeriods', function ($q) { $q->where('is_active', 1); }) ->first(); if (!$schedule) { return response()->json([ 'success' => false, 'message' => 'Jadwal tidak ditemukan / QR tidak valid'], 404); } // day validation if ($schedule->day_of_week !== $dayOfWeek) { return response()->json([ 'success' => false, 'message' => 'Absensi tidak sesuai dengan jadwal'], 422); } // period validation using configuration that has been set $periods = config('periods.periods'); // period mapping $currentTime = $now->format('H:i'); $currentPeriod = null; $validPeriod = false; foreach (range($schedule->period_start, $schedule->period_end) as $p) { if ( isset($periods[$p]) && $currentTime >= $periods[$p]['start'] && $currentTime < $periods[$p]['end'] ) { $validPeriod = true; $currentPeriod = $p; break; } } if (!$validPeriod) { return response()->json([ 'success' => false, 'message' => 'jam pelajaran sudah lewat / belum dimulai'], 422); } $lockKey = "attendance:{$idStudent}:{$schedule->id}:" . $now->toDateString(); return Cache::lock($lockKey, 5)->block(3, function () use ( $idStudent, $idClass, $schedule, $lat, $lon, $currentPeriod, $now, $lockKey ) { Log::info('Attendance lock acquired', ['key' => $lockKey]); error_log('Attendance lock acquired: ' . $lockKey); $alreadyAttendance = AttendanceHistory::where('id_student', $idStudent) ->where('id_schedule', $schedule->id) ->where('attendance_date', $now->toDateString()) ->first(); Log::info('Checking for existing attendance record:', [ 'id_student' => $idStudent, 'id_schedule' => $schedule->id, 'date_query' => $now->toDateString(), ]); Log::info('Query result:', [ 'attendance_record' => $alreadyAttendance, ]); if ($alreadyAttendance) { return response()->json([ 'success' => false, 'message' => 'Anda sudah absen untuk mapel ini!' ], 422); } // Dispatch background job to write attendance (quick response to client) $jobData = [ 'id_student' => $idStudent, 'id_schedule' => $schedule->id, 'id_class' => $idClass, 'status' => 'hadir', 'coordinate' => "$lat, $lon", 'period_number' => $currentPeriod, 'attendance_date' => $now->toDateString(), 'created_at' => $now, ]; WriteAttendanceJob::dispatch($jobData); return response()->json([ 'success' => true, 'message' => 'Absensi diterima dan sedang diproses', 'data' => [ 'schedule' => $schedule, ] ], 202); }); } catch (\Illuminate\Validation\ValidationException $e) { // This will catch validation errors and return a 422 status return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $e->errors() ], 422); } catch (LockTimeoutException $e) { return response()->json([ 'success' => false, 'message' => 'Permintaan sedang diproses, silakan coba lagi sebentar', ], 429); } catch (\Exception $e) { // This will catch any other general exceptions return response()->json([ 'success' => false, 'message' => 'Terjadi kesalahan pada server', 'error' => $e->getMessage() ], 500); } } // get attendance report from eloquent AttendanceHistoryDaily from face recognition attendance // based on student and date. public function dailyAttendanceReport(Request $request) { try { $request->validate([ 'date' => 'required|date', ]); $user = $request->user(); $student = $user->student ?? null; if (!$student) { return response()->json(['success' => false, 'message' => 'Student profile not found for user'], 403); } $date = Carbon::parse($request->date)->toDateString(); $attendanceRecords = AttendanceHistoryDaily::query() ->where('id_student', $student->id) ->whereDate('created_at', $date) ->get() ->first(); // if record is empty, return message no attendance record for that date if (!$attendanceRecords) { return response()->json([ 'success' => true, 'message' => 'No attendance record for this date', 'data' => null, ], 200); } return response()->json([ 'success' => true, 'message' => 'Daily attendance report fetched', 'data' => $attendanceRecords, ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } // get attendance report based on schedule, student, and date. // expected result: List Schedule [schedule + attendance status (hadir, izin, sakit, alpa), if in schedule hasnt attendance record just return attendance status: null] // get parameter: id_class, date, id_student (from token) // this is for personal student attendance report, to show the attendance status for each schedule in that day. public function report(Request $request) { try { $request->validate([ 'id_class' => 'required|integer', 'date' => 'required|date', ]); $user = $request->user(); $student = $user->student ?? null; if (!$student) { return response()->json(['success' => false, 'message' => 'Student profile not found for user'], 403); } $idClass = $request->id_class; $date = Carbon::parse($request->date)->toDateString(); $dayName = strtolower(Carbon::parse($date)->locale('id')->dayName); $dayIndex = Carbon::parse($date)->dayOfWeekIso; // 1..7 // fetch schedules for class where academic period is active and day matches $schedules = Schedule::query() ->where('id_class', $idClass) ->whereHas('academicPeriods', function ($q) { $q->where('is_active', 1); }) ->where(function ($q) use ($dayName, $dayIndex) { $q->where('day_of_week', $dayName) ->orWhere('day_of_week', (string) $dayIndex); }) ->with(['subject', 'teacher']) ->orderBy('period_start') ->get(); $result = []; foreach ($schedules as $sch) { $attendance = AttendanceHistory::where('id_student', $student->id) ->where('id_schedule', $sch->id) ->where('attendance_date', $date) ->first(); $result[] = [ 'schedule' => $sch, 'attendance_status' => $attendance ? $attendance->status : null, 'attendance' => $attendance ?? null, ]; } return response()->json([ 'success' => true, 'message' => 'Attendance report fetched', 'data' => $result, ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } // get attendance report history all student in once class filtered by class, and date now. // expected result: List Schedule [schedule + all user in class + attendance status (hadir, izin, sakit, alpa), if in schedule hasnt attendance record, dont display the student in that schedule] public function reportClass(Request $request) { try { $request->validate([ 'id_class' => 'required|integer', 'date' => 'required|date', ]); $idClass = $request->id_class; $date = Carbon::parse($request->date)->toDateString(); $dayName = strtolower(Carbon::parse($date)->locale('id')->dayName); $dayIndex = Carbon::parse($date)->dayOfWeekIso; // 1..7 // schedules for class on that day and active academic period $schedules = Schedule::query() ->where('id_class', $idClass) ->whereHas('academicPeriods', function ($q) { $q->where('is_active', 1); }) ->where(function ($q) use ($dayName, $dayIndex) { $q->where('day_of_week', $dayName) ->orWhere('day_of_week', (string) $dayIndex); }) ->with(['subject', 'teacher', 'class']) ->orderBy('period_start') ->get(); $data = []; foreach ($schedules as $sch) { // get attendance records for that schedule on the date $attendances = AttendanceHistory::query() ->where('id_schedule', $sch->id) ->where('attendance_date', $date) ->with(['student.user']) ->get(); // if there are no attendance records, still include schedule with empty list (per request could be omitted by client) $items = []; foreach ($attendances as $att) { $items[] = [ 'attendance' => $att, // 'student' => $att->student, 'status' => $att->status, ]; } $data[] = [ 'schedule' => $sch, 'attendances' => $items, ]; } return response()->json([ 'success' => true, 'message' => 'Attendance history for class fetched', 'data' => $data, ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } // post permission from student using data public function createPermission(Request $request) { try { $request->validate([ 'reason' => 'required|string', 'information' => 'nullable|string', 'evidence' => 'nullable|file|mimes:jpg,jpeg,png|max:2048', 'date_permission' => 'required|date', // count days 'time_period' => 'required|integer|min:1|max:10', ]); $user = $request->user(); $student = $user->student ?? null; if (!$student) { return response()->json(['success' => false, 'message' => 'Student profile not found for user'], 403); } $permission = new Permission(); $permission->id_student = $student->id; $permission->reason = $request->reason; $permission->information = $request->information; $permission->date_permission = Carbon::parse($request->date_permission)->toDateString(); $permission->time_period = $request->time_period; if ($request->hasFile('evidence')) { $file = $request->file('evidence'); // filename format: {id_student}_{nama_lengkap}_timestamp.extension $displayName = $student->name ?? ($student->user->name ?? 'student'); $safeName = str_replace(' ', '-', strtolower($displayName)); // Example: 12_budi-sudarsono_1771945730.jpg $filename = $student->id . '_' . $safeName . '_' . time() . '.' . $file->getClientOriginalExtension(); Storage::disk('public')->putFileAs('permission/evidence', $file, $filename); $permission->evidence = $filename; } $permission->save(); return response()->json([ 'success' => true, 'message' => 'Permission request created successfully', 'data' => $permission, ], 201); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } public function permissionReport(Request $request) { try { $request->validate([ 'start_date' => 'required|date', 'end_date' => 'required|date|after_or_equal:start_date', ]); $user = $request->user(); $student = $user->student ?? null; if (!$student) { return response()->json(['success' => false, 'message' => 'Student profile not found for user'], 403); } $startDate = Carbon::parse($request->start_date)->toDateString(); $endDate = Carbon::parse($request->end_date)->toDateString(); $permissions = Permission::query() ->where('id_student', $student->id) ->whereDate('created_at', '>=', $startDate) ->whereDate('created_at', '<=', $endDate) ->get(); if ($permissions->isEmpty()) { return response()->json([ 'success' => true, 'message' => 'No permission records for this date', 'data' => null, ], 200); } return response()->json([ 'success' => true, 'message' => 'Permission report fetched', 'data' => $permissions, ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } // get attendance in schedule per day by class id. (teacher only) public function reportClassById(Request $request, $classId, $date) { try { // check bearer token and get user $user = $request->user(); if (!$user) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); } $date = Carbon::parse($date)->toDateString(); $dayName = strtolower(Carbon::parse($date)->locale('id')->dayName); $dayIndex = Carbon::parse($date)->dayOfWeekIso; // 1..7 $students = $this->getStudentsForClass((int) $classId); $schedules = $this->getActiveSchedulesForClassByDate((int) $classId, $dayName, $dayIndex); $data = []; foreach ($schedules as $sch) { $data[] = [ 'schedule' => $sch, 'attendances' => $this->buildAttendanceItemsForSchedule($sch, $date, $students), // fallback to oz ex: 2026-04-22T00:00:00Z, 'date_attendance' => Carbon::parse($date)->toDateString(), ]; } return response()->json([ 'success' => true, 'message' => 'Attendance history for class fetched', 'data' => $data, ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } // daily report attendance for teacher side. public function reportDailyAttendance(Request $request) { try{ // check bearer token and get user $user = $request->user(); if (!$user) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); } // get input request $classId, $date $request->validate([ 'id_class' => 'required|integer', 'date' => 'required|date', ]); $classId = $request->input('id_class'); $date = Carbon::parse($request->input('date'))->toDateString(); // Get class info $class = Classes::find($classId); // Get all students in the class $students = Student::query() ->where('id_class', $classId) ->orderBy('name') ->get(); // Get all attendance records for this class and date $historyReport = AttendanceHistoryDaily::query() ->where('id_class', $classId) ->whereHas('class.schedules.academicPeriods', function ($q) { $q->where('is_active', 1); }) ->whereDate('created_at', $date) ->with(['class', 'student']) ->get(); // Create a map of attendance records by student id $attendanceByStudent = $historyReport->keyBy('id_student'); // Add missing students with status 'none' foreach ($students as $student) { if (!$attendanceByStudent->has($student->id)) { $newRecord = new AttendanceHistoryDaily(); $newRecord->id_student = $student->id; $newRecord->id_class = $classId; $newRecord->status = 'none'; $newRecord->picture = null; $newRecord->created_at = $date; $newRecord->setRelation('student', $student); $newRecord->setRelation('class', $class); $historyReport->push($newRecord); } } if ($historyReport->isEmpty()) { return response()->json([ 'success' => true, 'message' => 'No attendance history for this class and date', 'data' => null, ], 200); } return response()->json([ 'success' => true, 'message' => 'Daily attendance report for class fetched', 'data' => $historyReport, ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } /** * @return \Illuminate\Support\Collection */ private function getStudentsForClass(int $classId) { return Student::query() ->where('id_class', $classId) ->with('user') ->orderBy('name') ->get(); } /** * @return \Illuminate\Support\Collection */ private function getActiveSchedulesForClassByDate(int $classId, string $dayName, int $dayIndex) { return Schedule::query() ->where('id_class', $classId) ->whereHas('academicPeriods', function ($q) { $q->where('is_active', 1); }) ->where(function ($q) use ($dayName, $dayIndex) { $q->where('day_of_week', $dayName) ->orWhere('day_of_week', (string) $dayIndex); }) ->with(['subject', 'teacher', 'class']) ->orderBy('period_start') ->get(); } /** * @param \App\Models\Schedule $schedule * @param string $date * @param \Illuminate\Support\Collection $students * @return array */ private function buildAttendanceItemsForSchedule($schedule, string $date, $students): array { $attendances = AttendanceHistory::query() ->where('id_schedule', $schedule->id) ->where('attendance_date', $date) ->with(['student.user']) ->get(); $attendanceByStudent = $attendances->keyBy('id_student'); $items = []; foreach ($students as $student) { $attendance = $attendanceByStudent->get($student->id); if (!$attendance) { $attendance = new AttendanceHistory([ 'period_number' => $schedule->period_start, 'status' => 'null', 'coordinate' => null, 'attendance_date' => $date, 'id_student' => $student->id, 'id_schedule' => $schedule->id, 'id_class' => $schedule->id_class, ]); $attendance->setRelation('student', $student); } $items[] = [ 'attendance' => $attendance, 'status' => $attendance->status, ]; } return $items; } // permission get by class id. (teacher only) public function permissionReportByClass(Request $request, $classId) { try { // check bearer token and get user $user = $request->user(); if (!$user) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); } $request->validate([ 'start_date' => 'required|string', 'end_date' => 'required|string|after_or_equal:start_date', ]); $startDate = $request->input('start_date'); $endDate = $request->input('end_date'); $permissions = Permission::query() ->whereHas('student', function ($q) use ($classId) { $q->where('id_class', $classId); }) ->whereDate('created_at', '>=', $startDate) ->whereDate('created_at', '<=', $endDate) ->with(['student.user']) ->orderBy('created_at', 'desc') ->get(); return response()->json([ 'success' => true, 'message' => 'Permission report for class fetched', 'data' => $permissions, ], 200); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } // permission accept for teacher only public function permissionAccept(Request $request) { try { // check bearer token and get user $user = $request->user(); if (!$user) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); } $permissionId = $request->input('permission_id'); if (!$permissionId) { return response()->json(['success' => false, 'message' => 'Permission ID is required'], 422); } // find permission by id $permission = Permission::find($permissionId); if (!$permission) { return response()->json(['success' => false, 'message' => 'Permission not found'], 404); } // permission must be processed only, if already accepted or rejected, cannot be processed again. if ($permission->status === 'diterima') { return response()->json(['success' => false, 'message' => 'Permission already accepted'], 422); } if ($permission->status === 'ditolak') { return response()->json(['success' => false, 'message' => 'Permission already rejected'], 422); } $student = Student::find($permission->id_student); if (!$student) { return response()->json(['success' => false, 'message' => 'Student not found'], 404); } DB::transaction(function () use ($permission, $user, $request, $student) { // update status to accepted $permission->status = 'diterima'; $permission->approved_by = $user->id; // set approved by teacher id $permission->feedback = $request->input('feedback', null); // optional feedback message from teacher $permission->save(); // * create attendance record history to related date (only fill with schedule that has active academic period, and match with date permission). $permissionReason = $permission->reason; // contoh: 'sakit', 'izin', 'dispen', 'lainnya' if ($permissionReason === 'lainnya') { $permissionReason = 'izin'; } $permissionDate = Carbon::parse($permission->date_permission)->startOfDay(); $permissionTimePeriod = (int) $permission->time_period; $createdSchoolDays = 0; $currentDate = $permissionDate->copy(); // Count time_period as school days; weekend days are skipped and replaced by the next weekday. while ($createdSchoolDays < $permissionTimePeriod) { if ($currentDate->isWeekend()) { $currentDate->addDay(); continue; } $dayName = strtolower($currentDate->locale('id')->dayName); $dayIndex = $currentDate->dayOfWeekIso; // 1..7 // search related schedule with day_of_week same and active academic period $schedules = Schedule::query() ->where('id_class', $student->id_class) ->whereHas('academicPeriods', function ($q) { $q->where('is_active', 1); }) ->where(function ($q) use ($dayName, $dayIndex) { $q->where('day_of_week', $dayName) ->orWhere('day_of_week', (string) $dayIndex); }) ->get(); foreach ($schedules as $schedule) { AttendanceHistory::updateOrCreate( [ 'id_schedule' => $schedule->id, 'id_student' => $student->id, 'attendance_date' => $currentDate->toDateString(), ], [ 'id_class' => $schedule->id_class, 'period_number' => $schedule->period_start, 'coordinate' => "$this->latOfAttendance, $this->lonOfAttendance", 'status' => $permissionReason, ] ); } // update attendancehistorydaily to 'izin' AttendanceHistoryDaily::updateOrCreate( [ 'id_student' => $student->id, 'id_class' => $student->id_class, 'created_at' => $currentDate->toDateString(), ], [ 'status' => 'izin', 'picture' => '', ] ); $createdSchoolDays++; $currentDate->addDay(); } }); // * notification zone: send notification to student related to the permission that has been accepted $userStudent = $student->user; if (!$userStudent) { return response()->json([ 'success' => false, 'message' => 'User student not found', ], 404); } $fcmToken = $userStudent->device_token; $template = $this->notificationHelper->templatePermissionApproval( $fcmToken ); try { $notificationResult = $this->notificationHelper->send($template); } catch (\Exception $e) { Log::error('Failed to send attendance violation notification', ['exception' => $e]); $notificationResult = [ 'success' => false, 'message' => 'Failed to send notification', 'error' => $e->getMessage(), ]; } $createTemplate = $this->notificationHelper->createTemplateForStudent( (int) $userStudent->id, $template->title, $template->body, NotificationType::PermissionAccepted, // user id of teacher, convert to int. (int) $user->id ); Notification::create($createTemplate->toArray()); // * end of notification zone return response()->json([ 'success' => true, 'message' => 'Izin diterima untuk siswa ' . $student->name, 'data' => [ 'permission' => $permission, 'notification_result' => $notificationResult, ], ], 200); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } public function permissionReject(Request $request) { try { // check bearer token and get user $user = $request->user(); if (!$user) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); } $permissionId = $request->input('permission_id'); if (!$permissionId) { return response()->json(['success' => false, 'message' => 'Permission ID is required'], 422); } // find permission by id $permission = Permission::find($permissionId); if (!$permission) { return response()->json(['success' => false, 'message' => 'Permission not found'], 404); } // permission must be processed only, if already accepted or rejected, cannot be processed again. if ($permission->status === 'diterima') { return response()->json(['success' => false, 'message' => 'Permission already accepted'], 422); } if ($permission->status === 'ditolak') { return response()->json(['success' => false, 'message' => 'Permission already rejected'], 422); } $student = Student::find($permission->id_student); if (!$student) { return response()->json(['success' => false, 'message' => 'Student not found'], 404); } DB::transaction(function () use ($permission, $user, $request) { // update status to rejected $permission->status = 'ditolak'; $permission->approved_by = $user->id; // set approved by teacher id $permission->feedback = $request->input('feedback', null); // optional feedback message from teacher $permission->save(); }); /// send notification while saving to database notification. $fcmToken = $student->user->device_token; $template = $this->notificationHelper->templatePermissionRejection( $fcmToken, $request->input('feedback', null) // optional custom message from teacher feedback ); try { $notificationResult = $this->notificationHelper->send($template); } catch (\Exception $e) { Log::error('Failed to send attendance violation notification', ['exception' => $e]); $notificationResult = [ 'success' => false, 'message' => 'Failed to send notification', 'error' => $e->getMessage(), ]; } $createTemplate = $this->notificationHelper->createTemplateForStudent( (int) $student->user->id, $template->title, $template->body, NotificationType::PermissionRejected, // user id of teacher, convert to int. (int) $user->id ); Notification::create($createTemplate->toArray()); return response()->json([ 'success' => true, 'message' => 'Izin ditolak untuk siswa ' . $student->name, 'data' => [ 'permission' => $permission, 'notification_result' => $notificationResult, ], ], 200); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $e->getMessage(), ], 500); } } public function reportDisrepancyStudentAttendanceByHistoryId(Request $request) { $user = $request->user(); if (!$user) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); } // input validate $request->validate([ 'attendance_history_id' => 'required|integer', 'disrepancy_type' => 'nullable|string|in:terlambat,hp_tidak_tersedia,izin,pulang_awal,alasan_lain', 'description' => 'nullable|string', ]); $attendanceHistoryId = $request->input('attendance_history_id'); $attendanceHistory = AttendanceHistory::query() ->where('id', $attendanceHistoryId) ->with(['schedule']) ->first(); if (!$attendanceHistory) { return response()->json(['success' => false, 'message' => 'Attendance history not found'], 404); } // cek status attendance history if already 'invalid', cannot report disrepancy again. if ($attendanceHistory->status === 'invalid') { return response()->json(['success' => false, 'message' => 'Attendance history already reported as invalid'], 422); } /// logic disrepancy here. DB::transaction(function () use ($request, $attendanceHistory) { $attendanceHistory->status = 'invalid'; $attendanceHistory->save(); // create report discrepancy $report = new Report(); $report->id_attendance_history = $attendanceHistory->id; $report->id_student = $attendanceHistory->id_student; $report->id_class = $attendanceHistory->id_class; $report->student_name = $attendanceHistory->student->name ?? 'Unknown Student'; $report->reported_by = $request->user()->id; $report->disrepancy_type = $request->input('disrepancy_type', 'alasan_lain'); // default to 'alasan_lain' if not provided $report->description = $request->input('description', null); $report->marked_as_resolved = false; $report->attendance_date = $attendanceHistory->attendance_date; $report->save(); }); /// notification zone for report disrepancy, send notification to teacher related to the schedule of attendance history that has been reported. $userStudent = $attendanceHistory->student->user; $fcmToken = $userStudent->device_token; $template = $this->notificationHelper->templateAttendanceViolation( $fcmToken, $request->input('description', null) // optional custom message from student about the disrepancy ); // use try catch to handle notification sending error, but still return success response for reporting disrepancy, because the main process of reporting disrepancy has been done in database transaction. try { $notificationResult = $this->notificationHelper->send($template); } catch (\Exception $e) { Log::error('Failed to send attendance violation notification', ['exception' => $e]); $notificationResult = [ 'success' => false, 'message' => 'Failed to send notification', 'error' => $e->getMessage(), ]; } $createTemplate = $this->notificationHelper->createTemplateForStudent( (int) $userStudent->id, $template->title, $template->body, NotificationType::AttendanceViolation, // user id of system or admin, convert to int. (for example: 0) (int) $user->id ); Notification::create($createTemplate->toArray()); return response()->json([ 'success' => true, 'data' => [ 'report' => Report::query()->where('id_attendance_history', $attendanceHistory->id)->first(), 'notification_result' => $notificationResult, ] ], 200); } // return json response link randomize for public qr code, access from website // use cache 5 minutes for the link, if more than 5 minutes, the link will be expired and generate new link. public function generateLinkQrAttendance(Request $request, $idSchedule) { try { if (!$request->user()) { return response()->json([ 'success' => false, 'message' => 'Unauthorized' ], 401); } $result = $this->qrLinkService->generateLink($idSchedule); return response()->json($result); } catch (\Throwable $th) { return response()->json([ 'success' => false, 'message' => 'Server error', 'error' => $th->getMessage() ], 500); } } }