172 lines
5.8 KiB
PHP
172 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Dashboard\Services;
|
|
|
|
use App\Modules\Academic\Models\StudentModel;
|
|
use App\Modules\Attendance\Models\AttendanceSessionModel;
|
|
use App\Modules\Devices\Models\DeviceModel;
|
|
|
|
/**
|
|
* Dashboard Service
|
|
*
|
|
* Aggregates data for dashboard monitoring endpoints.
|
|
*/
|
|
class DashboardService
|
|
{
|
|
protected AttendanceSessionModel $attendanceModel;
|
|
protected DeviceModel $deviceModel;
|
|
protected StudentModel $studentModel;
|
|
|
|
/**
|
|
* Minutes threshold for device "online" status
|
|
*/
|
|
protected int $deviceOnlineThresholdMinutes = 2;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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<int, array{student_name: string, class_name: string, subject: string, checkin_at: string, status: string, device_code: string}>
|
|
*/
|
|
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<int, array{
|
|
* id: int,
|
|
* device_code: string,
|
|
* device_name: string,
|
|
* is_active: bool,
|
|
* last_seen_at: string|null,
|
|
* online_status: string,
|
|
* latitude: float|null,
|
|
* longitude: float|null,
|
|
* radius_meters: int|null
|
|
* }>
|
|
*/
|
|
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;
|
|
}
|
|
}
|