init backend presensi
This commit is contained in:
189
app/Modules/Attendance/Services/AttendanceReportService.php
Normal file
189
app/Modules/Attendance/Services/AttendanceReportService.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user