init backend presensi

This commit is contained in:
mwpn
2026-03-05 14:37:36 +07:00
commit b4fda6b9c9
319 changed files with 27261 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
<?php
namespace App\Modules\Attendance\Services;
use App\Modules\Auth\Entities\Role;
use App\Modules\Attendance\Entities\AttendanceSession;
use App\Modules\Academic\Models\ClassModel;
use App\Modules\Academic\Models\ScheduleModel;
use App\Modules\Academic\Models\StudentModel;
use App\Modules\Academic\Models\SubjectModel;
use App\Modules\Attendance\Models\AttendanceSessionModel;
/**
* Attendance Report Service
*
* On-the-fly computation of schedule attendance report (expected, present, late, absent).
* No DB insert for ABSENT. RBAC applied when userContext provided.
*/
class AttendanceReportService
{
protected ScheduleModel $scheduleModel;
protected StudentModel $studentModel;
protected SubjectModel $subjectModel;
protected ClassModel $classModel;
protected AttendanceSessionModel $attendanceModel;
public function __construct()
{
$this->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'));
}
}