Files
presensi/app/Modules/Attendance/Controllers/AttendanceReportController.php
2026-03-05 14:37:36 +07:00

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');
}
}