attendanceModel = new AttendanceSessionModel(); $this->deviceModel = new DeviceModel(); $this->studentModel = new StudentModel(); } /** * Get today's attendance summary * * @return array{total_students: int, present_today: int, late_today: int, outside_zone_today: int, no_schedule_today: int, invalid_device_today: int} */ public function getSummary(): array { $db = \Config\Database::connect(); $totalStudents = (int) $this->studentModel->countAll(); // Count by status for today only (date part of checkin_at = today) $today = date('Y-m-d'); $builder = $db->table('attendance_sessions'); $builder->select('status, COUNT(*) as cnt'); $builder->where('DATE(checkin_at)', $today); $builder->groupBy('status'); $rows = $builder->get()->getResultArray(); $counts = [ 'PRESENT' => 0, 'LATE' => 0, 'OUTSIDE_ZONE' => 0, 'NO_SCHEDULE' => 0, 'INVALID_DEVICE' => 0, ]; foreach ($rows as $row) { if (isset($counts[$row['status']])) { $counts[$row['status']] = (int) $row['cnt']; } } return [ 'total_students' => $totalStudents, 'present_today' => $counts['PRESENT'], 'late_today' => $counts['LATE'], 'outside_zone_today' => $counts['OUTSIDE_ZONE'], 'no_schedule_today' => $counts['NO_SCHEDULE'], 'invalid_device_today' => $counts['INVALID_DEVICE'], ]; } /** * Get last N attendance check-ins with student, class, subject, device info * * @param int $limit * @return array */ public function getRealtimeCheckins(int $limit = 20): array { $db = \Config\Database::connect(); $builder = $db->table('attendance_sessions AS a'); $builder->select(' s.name AS student_name, c.name AS class_name, COALESCE(sub.name, "-") AS subject, a.checkin_at, a.status, d.device_code '); $builder->join('students AS s', 's.id = a.student_id', 'left'); $builder->join('classes AS c', 'c.id = s.class_id', 'left'); $builder->join('schedules AS sch', 'sch.id = a.schedule_id', 'left'); $builder->join('subjects AS sub', 'sub.id = sch.subject_id', 'left'); $builder->join('devices AS d', 'd.id = a.device_id', 'left'); $builder->orderBy('a.checkin_at', 'DESC'); $builder->limit($limit); $rows = $builder->get()->getResultArray(); $out = []; foreach ($rows as $row) { $out[] = [ 'student_name' => (string) ($row['student_name'] ?? '-'), 'class_name' => (string) ($row['class_name'] ?? '-'), 'subject' => (string) ($row['subject'] ?? '-'), 'checkin_at' => (string) $row['checkin_at'], 'status' => (string) $row['status'], 'device_code' => (string) ($row['device_code'] ?? '-'), ]; } return $out; } /** * Get devices with online status (online if last_seen_at within threshold minutes) * * @return array */ public function getDevices(): array { $devices = $this->deviceModel->findAll(); $thresholdSeconds = $this->deviceOnlineThresholdMinutes * 60; $now = time(); $out = []; foreach ($devices as $d) { $lastSeenAt = $d->last_seen_at; $lastSeenStr = null; $lastSeenTs = null; if ($lastSeenAt !== null) { $lastSeenStr = is_object($lastSeenAt) && method_exists($lastSeenAt, 'format') ? $lastSeenAt->format('Y-m-d H:i:s') : (string) $lastSeenAt; $lastSeenTs = strtotime($lastSeenStr); } $onlineStatus = 'offline'; if ($lastSeenTs !== null && ($now - $lastSeenTs) < $thresholdSeconds) { $onlineStatus = 'online'; } $out[] = [ 'id' => (int) $d->id, 'device_code' => $d->device_code, 'device_name' => (string) ($d->device_name ?? ''), 'is_active' => (bool) $d->is_active, 'last_seen_at' => $lastSeenStr, 'online_status' => $onlineStatus, 'latitude' => $d->latitude !== null ? (float) $d->latitude : null, 'longitude' => $d->longitude !== null ? (float) $d->longitude : null, 'radius_meters' => $d->radius_meters !== null ? (int) $d->radius_meters : null, ]; } return $out; } }