scheduleModel = new ScheduleModel(); $this->studentModel = new StudentModel(); $this->subjectModel = new SubjectModel(); $this->classModel = new ClassModel(); $this->attendanceModel = new AttendanceSessionModel(); } /** * Get schedule attendance report for a given date. * Absent = expected students minus those with PRESENT/LATE record (no insert). * * @param int $scheduleId * @param string $dateYmd Y-m-d * @param array|null $userContext { id, name, email, roles: [ { role_code } ] } for RBAC * @return array|null Report array or null if schedule not found / no access */ public function getScheduleAttendanceReport(int $scheduleId, string $dateYmd, ?array $userContext = null): ?array { $schedule = $this->scheduleModel->getScheduleWithSlot($scheduleId); if ($schedule === null) { return null; } if ($userContext !== null && !$this->canAccessSchedule($schedule, $userContext)) { return null; } $classId = (int) $schedule['class_id']; $expectedStudents = $this->studentModel->where('class_id', $classId)->findAll(); $expectedIds = array_map(fn ($s) => (int) $s->id, $expectedStudents); $expectedTotal = count($expectedIds); $db = \Config\Database::connect(); $builder = $db->table('attendance_sessions AS a'); $builder->select('a.student_id, s.nisn, s.name, a.status, a.checkin_at'); $builder->join('students AS s', 's.id = a.student_id', 'inner'); $builder->where('a.schedule_id', $scheduleId); $builder->where('a.attendance_date', $dateYmd); $builder->whereIn('a.status', [AttendanceSession::STATUS_PRESENT, AttendanceSession::STATUS_LATE]); $builder->orderBy('s.name', 'ASC'); $rows = $builder->get()->getResultArray(); $presentList = []; $presentIds = []; $lateTotal = 0; foreach ($rows as $row) { $sid = (int) $row['student_id']; $presentIds[] = $sid; $presentList[] = [ 'student_id' => $sid, 'nisn' => (string) ($row['nisn'] ?? ''), 'name' => (string) ($row['name'] ?? ''), 'status' => (string) $row['status'], 'checkin_at' => (string) $row['checkin_at'], ]; if ($row['status'] === AttendanceSession::STATUS_LATE) { $lateTotal++; } } $presentTotal = count($presentList); $absentIds = array_diff($expectedIds, $presentIds); $absentList = []; foreach ($expectedStudents as $s) { if (in_array((int) $s->id, $absentIds, true)) { $absentList[] = [ 'student_id' => (int) $s->id, 'nisn' => (string) ($s->nisn ?? ''), 'name' => (string) ($s->name ?? ''), ]; } } usort($absentList, fn ($a, $b) => strcmp($a['name'], $b['name'])); $absentTotal = count($absentList); $subject = $this->subjectModel->find($schedule['subject_id']); $subjectName = $subject ? (string) $subject->name : '-'; $classEntity = $this->classModel->find($classId); $className = $classEntity ? (string) $classEntity->name : '-'; $schedulePayload = [ 'id' => (int) $schedule['id'], 'class_id' => (int) $schedule['class_id'], 'class_name' => $className, 'subject' => $subjectName, 'teacher' => (string) ($schedule['teacher_name'] ?? ''), 'start_time' => (string) $schedule['start_time'], 'end_time' => (string) $schedule['end_time'], 'day_of_week' => (int) $schedule['day_of_week'], ]; return [ 'schedule' => $schedulePayload, 'summary' => [ 'expected_total' => $expectedTotal, 'present_total' => $presentTotal, 'late_total' => $lateTotal, 'absent_total' => $absentTotal, ], 'present' => $presentList, 'absent' => $absentList, ]; } /** * RBAC: whether user can access this schedule. * @param array|object $schedule Schedule row (array from getScheduleWithSlot) or entity with class_id, teacher_user_id */ protected function canAccessSchedule(array|object $schedule, array $user): bool { $roles = $user['roles'] ?? []; $roleCodes = array_column($roles, 'role_code'); if (in_array(Role::CODE_ADMIN, $roleCodes, true) || in_array(Role::CODE_GURU_BK, $roleCodes, true)) { return true; } $classId = (int) (is_array($schedule) ? $schedule['class_id'] : $schedule->class_id); if (in_array(Role::CODE_WALI_KELAS, $roleCodes, true)) { $db = \Config\Database::connect(); $row = $db->table('classes')->select('id')->where('id', $classId)->where('wali_user_id', $user['id'])->get()->getRow(); return $row !== null; } if (in_array(Role::CODE_GURU_MAPEL, $roleCodes, true)) { $teacherUserId = is_array($schedule) ? (isset($schedule['teacher_user_id']) ? (int) $schedule['teacher_user_id'] : 0) : (isset($schedule->teacher_user_id) ? (int) $schedule->teacher_user_id : 0); return $teacherUserId === (int) $user['id']; } if (in_array(Role::CODE_ORANG_TUA, $roleCodes, true)) { $studentIds = $this->getStudentIdsForParent((int) $user['id']); if ($studentIds === []) { return false; } $db = \Config\Database::connect(); $overlap = $db->table('students') ->where('class_id', $classId) ->whereIn('id', $studentIds) ->countAllResults(); return $overlap > 0; } return false; } protected function getStudentIdsForParent(int $userId): array { $db = \Config\Database::connect(); $rows = $db->table('student_parents AS sp') ->select('sp.student_id') ->join('parents AS p', 'p.id = sp.parent_id', 'inner') ->where('p.user_id', $userId) ->get() ->getResultArray(); return array_map('intval', array_column($rows, 'student_id')); } }