reportService = new AttendanceReportService(); $this->authService = new AuthService(); } /** * GET /api/attendance/reports * * Query: * - from_date (YYYY-MM-DD, optional, default: today if both empty) * - to_date (YYYY-MM-DD, optional, default: today if both empty) * - class_id (int, optional) * - student_id (int, optional) * - status (PRESENT,LATE,OUTSIDE_ZONE,NO_SCHEDULE,INVALID_DEVICE; optional) * * Returns recap per hari/per kelas + daftar detail kehadiran. */ public function index(): ResponseInterface { $user = $this->authService->currentUser(); if (! $user) { return $this->errorResponse('Unauthorized', null, null, ResponseInterface::HTTP_UNAUTHORIZED); } $roles = $user['roles'] ?? []; $roleCodes = array_column($roles, 'role_code'); $isAdmin = in_array(Role::CODE_ADMIN, $roleCodes, true); $isGuruBk = in_array(Role::CODE_GURU_BK, $roleCodes, true); $isWali = in_array(Role::CODE_WALI_KELAS, $roleCodes, true); $isGuruMap = in_array(Role::CODE_GURU_MAPEL, $roleCodes, true); // ORANG_TUA pakai Portal Orang Tua, bukan endpoint ini if (! $isAdmin && ! $isGuruBk && ! $isWali && ! $isGuruMap) { return $this->errorResponse('Forbidden', null, null, ResponseInterface::HTTP_FORBIDDEN); } $from = $this->request->getGet('from_date'); $to = $this->request->getGet('to_date'); $today = date('Y-m-d'); if (($from === null || $from === '') && ($to === null || $to === '')) { $from = $today; $to = $today; } if ($from !== null && $from !== '' && ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) { return $this->errorResponse('from_date must be YYYY-MM-DD', null, null, ResponseInterface::HTTP_BAD_REQUEST); } if ($to !== null && $to !== '' && ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) { return $this->errorResponse('to_date must be YYYY-MM-DD', null, null, ResponseInterface::HTTP_BAD_REQUEST); } $classId = (int) $this->request->getGet('class_id'); $studentId = (int) $this->request->getGet('student_id'); $status = $this->request->getGet('status'); if ($status !== null && $status !== '') { $allowedStatus = ['PRESENT', 'LATE', 'OUTSIDE_ZONE', 'NO_SCHEDULE', 'INVALID_DEVICE']; if (! in_array($status, $allowedStatus, true)) { return $this->errorResponse('Invalid status value', null, null, ResponseInterface::HTTP_BAD_REQUEST); } } $db = \Config\Database::connect(); $builder = $db->table('attendance_sessions AS att') ->select( 'att.id, att.attendance_date, att.status, att.checkin_at, ' . 's.id AS student_id, s.nisn, s.name AS student_name, ' . 'c.id AS class_id, c.grade, c.major, c.name AS class_name, ' . 'sch.id AS schedule_id, sub.id AS subject_id, sub.name AS subject_name, ' . 'u.id AS teacher_user_id, u.name AS teacher_name' ) ->join('students AS s', 's.id = att.student_id', 'inner') ->join('classes AS c', 'c.id = s.class_id', 'left') ->join('schedules AS sch', 'sch.id = att.schedule_id', 'left') ->join('subjects AS sub', 'sub.id = sch.subject_id', 'left') ->join('users AS u', 'u.id = sch.teacher_user_id', 'left'); if ($from) { $builder->where('att.attendance_date >=', $from); } if ($to) { $builder->where('att.attendance_date <=', $to); } if ($classId > 0) { $builder->where('c.id', $classId); } if ($studentId > 0) { $builder->where('s.id', $studentId); } if ($status) { $builder->where('att.status', $status); } // RBAC batasan data if ($isWali) { $builder->where('c.wali_user_id', (int) $user['id']); } elseif ($isGuruMap) { $builder->where('sch.teacher_user_id', (int) $user['id']); } elseif (! $isAdmin && ! $isGuruBk) { // Should not reach here, tetapi jaga-jaga return $this->errorResponse('Forbidden', null, null, ResponseInterface::HTTP_FORBIDDEN); } $builder->orderBy('att.attendance_date', 'DESC') ->orderBy('c.grade', 'ASC') ->orderBy('c.major', 'ASC') ->orderBy('c.name', 'ASC') ->orderBy('s.name', 'ASC'); $rows = $builder->get()->getResultArray(); // Detail records $records = array_map(static function (array $r): array { $classLabel = null; if ($r['grade'] !== null || $r['major'] !== null || $r['class_name'] !== null) { $parts = array_filter([ trim((string) ($r['grade'] ?? '')), trim((string) ($r['major'] ?? '')), trim((string) ($r['class_name'] ?? '')), ]); $classLabel = implode(' ', $parts); } return [ 'id' => (int) $r['id'], 'attendance_date' => $r['attendance_date'], 'status' => $r['status'], 'checkin_at' => $r['checkin_at'], 'student_id' => (int) $r['student_id'], 'student_name' => $r['student_name'], 'nisn' => $r['nisn'], 'class_id' => $r['class_id'] !== null ? (int) $r['class_id'] : null, 'class_label' => $classLabel, 'schedule_id' => $r['schedule_id'] !== null ? (int) $r['schedule_id'] : null, 'subject_id' => $r['subject_id'] !== null ? (int) $r['subject_id'] : null, 'subject_name' => $r['subject_name'], 'teacher_user_id' => $r['teacher_user_id'] !== null ? (int) $r['teacher_user_id'] : null, 'teacher_name' => $r['teacher_name'], ]; }, $rows); // Rekap per hari & kelas $summaryMap = []; foreach ($records as $rec) { $date = $rec['attendance_date']; $cid = $rec['class_id'] ?? 0; $label = $rec['class_label'] ?? '-'; $key = $date . '|' . $cid; if (! isset($summaryMap[$key])) { $summaryMap[$key] = [ 'attendance_date' => $date, 'class_id' => $cid, 'class_label' => $label, 'total' => 0, 'present' => 0, 'late' => 0, 'outside_zone' => 0, 'no_schedule' => 0, 'invalid_device' => 0, ]; } $summaryMap[$key]['total']++; switch ($rec['status']) { case 'PRESENT': $summaryMap[$key]['present']++; break; case 'LATE': $summaryMap[$key]['late']++; break; case 'OUTSIDE_ZONE': $summaryMap[$key]['outside_zone']++; break; case 'NO_SCHEDULE': $summaryMap[$key]['no_schedule']++; break; case 'INVALID_DEVICE': $summaryMap[$key]['invalid_device']++; break; } } $summary = array_values($summaryMap); usort($summary, static function (array $a, array $b): int { if ($a['attendance_date'] === $b['attendance_date']) { return strcmp((string) $a['class_label'], (string) $b['class_label']); } return strcmp($a['attendance_date'], $b['attendance_date']); }); $payload = [ 'filters' => [ 'from_date' => $from, 'to_date' => $to, 'class_id' => $classId > 0 ? $classId : null, 'student_id' => $studentId > 0 ? $studentId : null, 'status' => $status ?: null, ], 'summary' => $summary, 'records' => $records, ]; return $this->successResponse($payload, 'Attendance reports'); } /** * GET /api/attendance/report/schedule/{scheduleId}?date=YYYY-MM-DD * * @param int|string $scheduleId From route (:num) * @return ResponseInterface */ public function scheduleReport($scheduleId): ResponseInterface { $scheduleId = (int) $scheduleId; $date = $this->request->getGet('date'); if ($date === null || $date === '') { return $this->errorResponse('Query parameter date (YYYY-MM-DD) is required', null, null, 400); } $date = (string) $date; if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return $this->errorResponse('Parameter date must be YYYY-MM-DD', null, null, 400); } $userContext = $this->authService->currentUser(); $report = $this->reportService->getScheduleAttendanceReport($scheduleId, $date, $userContext); if ($report === null) { return $this->errorResponse('Schedule not found or access denied', null, null, 404); } return $this->successResponse($report, 'Schedule attendance report'); } }