261 lines
10 KiB
PHP
261 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Attendance\Controllers;
|
|
|
|
use App\Core\BaseApiController;
|
|
use App\Modules\Auth\Entities\Role;
|
|
use App\Modules\Auth\Services\AuthService;
|
|
use App\Modules\Attendance\Services\AttendanceReportService;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
/**
|
|
* Attendance Report Controller
|
|
*
|
|
* On-the-fly schedule attendance reports (expected, present, late, absent).
|
|
*/
|
|
class AttendanceReportController extends BaseApiController
|
|
{
|
|
protected AttendanceReportService $reportService;
|
|
protected AuthService $authService;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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');
|
|
}
|
|
}
|