attendance = model(AttendanceData::class); $this->studentClass = model(StudentClass::class); $this->classSection = model(ClassSection::class); $this->config = model(Configuration::class); $this->teacherClass = model(TeacherClass::class); $this->staffAttendance = model(StaffAttendance::class); $this->attendanceDay = model(AttendanceDay::class); $this->student = model(Student::class); $this->calendar = model(Calendar::class); $this->user = model(User::class); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); $this->enableAttendance = (int) ($this->config->getConfig('enable_attendance') ?? 0); } public function index() { $page = (int) ($this->request->getGet('page') ?? 1); $perPage = min(100, (int) ($this->request->getGet('per_page') ?? 20)); $studentId = $this->request->getGet('student_id'); $date = $this->request->getGet('date'); $builder = $this->attendance; if ($studentId) { $builder = $builder->where('student_id', $studentId); } if ($date) { $builder = $builder->where('date', $date); } $data = $builder->paginate($perPage, 'default', $page); $pager = $builder->pager; return $this->respondSuccess([ 'data' => $data, 'pagination' => [ 'current_page' => $pager->getCurrentPage(), 'per_page' => $pager->getPerPage(), 'total' => $pager->getTotal(), 'total_pages' => $pager->getPageCount(), ], ], 'Attendance records retrieved successfully'); } public function show($id = null) { $record = $this->attendance->find($id); if (!$record) { return $this->respondError('Attendance record not found', Response::HTTP_NOT_FOUND); } return $this->respondSuccess($record, 'Attendance record retrieved successfully'); } public function store() { $data = $this->request->getJSON(true); $rules = [ 'student_id' => 'required|integer', 'date' => 'required|valid_date[Y-m-d]', 'status' => 'required|in_list[present,absent,late]', 'notes' => 'permit_empty|string', ]; if (!$this->validate($rules)) { return $this->respondValidationError($this->validator->getErrors()); } $existing = $this->attendance ->where('student_id', $data['student_id']) ->where('date', $data['date']) ->first(); if ($existing) { return $this->respondError('Attendance already recorded for this student and date', 409); } $this->attendance->insert($data); $id = $this->attendance->getInsertID(); return $this->respondCreated([ 'id' => $id, 'data' => $data, ], 'Attendance record created successfully'); } public function update($id = null) { $record = $this->attendance->find($id); if (!$record) { return $this->respondError('Attendance record not found', Response::HTTP_NOT_FOUND); } $data = $this->request->getJSON(true); $rules = [ 'status' => 'permit_empty|in_list[present,absent,late]', 'notes' => 'permit_empty|string', ]; if (!$this->validate($rules)) { return $this->respondValidationError($this->validator->getErrors()); } $this->attendance->update($id, $data); return $this->respondSuccess($this->attendance->find($id), 'Attendance record updated successfully'); } public function delete($id = null) { $record = $this->attendance->find($id); if (!$record) { return $this->respondError('Attendance record not found', Response::HTTP_NOT_FOUND); } $this->attendance->delete($id); return $this->respondDeleted(null, 'Attendance record deleted successfully'); } public function student($id = null) { $records = $this->attendance ->where('student_id', $id) ->orderBy('date', 'DESC') ->findAll(); return $this->respondSuccess($records, 'Student attendance retrieved successfully'); } public function classRoster($classSectionId) { $date = $this->request->getGet('date') ?: date('Y-m-d'); $db = $this->getDatabaseConnection(); $rows = $db->table('student_class sc') ->select('s.id AS student_id, s.firstname, s.lastname') ->join('students s', 's.id = sc.student_id', 'inner') ->where('sc.class_section_id', (int)$classSectionId) ->where('sc.school_year', $this->schoolYear) ->orderBy('s.lastname', 'ASC')->orderBy('s.firstname', 'ASC') ->get()->getResultArray(); $list = []; foreach ($rows as $r) { $att = $this->attendance->getAttendance((int)$r['student_id'], (int)$classSectionId, $date); $list[] = [ 'student_id' => (int)$r['student_id'], 'firstname' => $r['firstname'], 'lastname' => $r['lastname'], 'status' => $att['status'] ?? null, 'reason' => $att['reason'] ?? null, ]; } return $this->success([ 'class_section_id' => (int)$classSectionId, 'date' => $date, 'students' => $list, ], 'Class roster attendance retrieved'); } public function recordClass($classSectionId) { $payload = $this->request->getJSON(true) ?? []; $date = isset($payload['date']) ? trim((string)$payload['date']) : ''; $records = isset($payload['records']) && is_array($payload['records']) ? $payload['records'] : []; if ($date === '' || empty($records)) { return $this->respondError('Invalid payload: date and records are required.', Response::HTTP_BAD_REQUEST); } $allowedStatuses = ['present', 'absent', 'late']; $now = utc_now(); DB::beginTransaction(); try { foreach ($records as $record) { $studentId = (int) ($record['student_id'] ?? 0); $status = strtolower(trim((string) ($record['status'] ?? ''))); $reason = isset($record['reason']) ? trim((string) $record['reason']) : null; if ($studentId <= 0 || !in_array($status, $allowedStatuses, true)) { DB::rollBack(); return $this->respondError('Invalid record provided.', Response::HTTP_UNPROCESSABLE_ENTITY); } $existing = $this->attendance ->where('student_id', $studentId) ->where('class_section_id', (int)$classSectionId) ->where('date', $date) ->first(); $payload = [ 'class_section_id' => (int) $classSectionId, 'student_id' => $studentId, 'status' => $status, 'reason' => $reason ?: null, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'date' => $date, 'updated_at' => $now, ]; if ($existing) { $payload['modified_by'] = $this->getCurrentUserId(); $this->attendance->update($existing['id'], $payload); } else { $payload['created_at'] = $now; $payload['modified_by'] = $this->getCurrentUserId(); $this->attendance->insert($payload); } } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); return $this->respondError('Failed to record attendance.', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success(null, 'Class attendance recorded successfully'); } public function teacherSectionGrid(): JsonResponse { $semester = (string) ($this->request->get('semester') ?? $this->semester); $schoolYear = (string) ($this->request->get('school_year') ?? $this->schoolYear); $date = (string) ($this->request->get('date') ?? local_date(utc_now(), 'Y-m-d')); $sectionCode = (int) ($this->request->get('class_section_id') ?? 0); $assignedBySection = (array) ($this->teacherClass->assignedBySectionForTerm($semester, $schoolYear) ?? []); $sectionCodes = array_keys($assignedBySection); $labels = []; if (!empty($sectionCodes)) { $labelRows = $this->classSection ->select('class_section_id, class_section_name') ->whereIn('class_section_id', $sectionCodes) ->orderBy('class_section_id', 'ASC') ->findAll(); foreach ($labelRows as $row) { $code = (int) ($row['class_section_id'] ?? 0); if ($code <= 0) continue; $name = trim((string) ($row['class_section_name'] ?? '')); $labels[$code] = $name !== '' ? $name : ('Section #' . $code); } foreach ($sectionCodes as $code) { if (!isset($labels[$code])) { $labels[$code] = 'Section #' . $code; } } } $grid = []; $locked = false; if ($sectionCode > 0) { $assigned = (array) ($this->teacherClass->assignedForSectionTerm($sectionCode, $semester, $schoolYear) ?? []); $teacherIds = array_map(static fn($row) => (int) ($row['teacher_id'] ?? 0), $assigned); $teacherIds = array_values(array_filter($teacherIds)); $statusMap = []; if (!empty($teacherIds)) { $rows = DB::table('staff_attendance as sa') ->select('sa.user_id', 'sa.status', 'sa.reason') ->where('sa.semester', $semester) ->where('sa.school_year', $schoolYear) ->where('sa.date', $date) ->whereIn('sa.user_id', $teacherIds) ->get(); foreach ($rows as $row) { $statusMap[(int) $row->user_id] = [ 'status' => $row->status ?? null, 'reason' => $row->reason ?? null, ]; } } foreach ($assigned as $teacherRow) { $tid = (int) ($teacherRow['teacher_id'] ?? 0); $name = trim(($teacherRow['firstname'] ?? '') . ' ' . ($teacherRow['lastname'] ?? '')); $name = $name !== '' ? $name : 'User #' . $tid; $posRaw = strtolower((string) ($teacherRow['position'] ?? 'main')); $pos = in_array($posRaw, ['main', 'ta'], true) ? $posRaw : 'main'; $cell = $statusMap[$tid] ?? ['status' => null, 'reason' => null]; $grid[] = [ 'teacher_id' => $tid, 'name' => $name, 'position' => $pos, 'status' => $cell['status'], 'reason' => $cell['reason'], ]; } try { $locked = (bool) $this->attendanceDay->isFinalized($sectionCode, $date, $semester, $schoolYear); } catch (\Throwable $e) { $locked = false; } } return $this->success([ 'semester' => $semester, 'school_year' => $schoolYear, 'date' => $date, 'class_section_id' => $sectionCode, 'sections' => $labels, 'grid' => $grid, 'locked' => $locked, ], 'Teacher attendance grid retrieved'); } public function teacherUpdateData(): JsonResponse { $teacherId = (int) ($this->request->get('teacher_id') ?? $this->getCurrentUserId() ?? 0); if ($teacherId <= 0) { return $this->respondError('Teacher not found or unauthorized.', Response::HTTP_UNAUTHORIZED); } $classSectionParam = (int) ($this->request->get('class_section_id') ?? 0); if ($classSectionParam <= 0) { $classSection = $this->teacherClass->getClassSectionIdByUserId($teacherId); if (empty($classSection)) { return $this->respondError('No class assigned to this teacher.', Response::HTTP_BAD_REQUEST); } $classSectionId = (int) ($classSection['class_section_id'] ?? 0); } else { $classSectionId = $classSectionParam; } if ($classSectionId <= 0) { return $this->respondError('Invalid class section.', Response::HTTP_BAD_REQUEST); } $semester = (string) ($this->request->get('semester') ?? $this->semester); $schoolYear = (string) ($this->request->get('school_year') ?? $this->schoolYear); $today = local_date(utc_now(), 'Y-m-d'); $recentDatesRows = $this->attendance ->select('date') ->distinct() ->orderBy('date', 'DESC') ->limit(3) ->findAll(); $recentDates = array_map(static fn($row) => (string) ($row['date'] ?? ''), $recentDatesRows); $recentDates = array_values(array_filter($recentDates, static fn($d) => $d !== '')); $allDates = array_values(array_unique(array_merge([$today], $recentDates))); $dateSlots = [ 'day_before_before_yesterday' => $allDates[3] ?? null, 'day_before_yesterday' => $allDates[2] ?? null, 'yesterday' => $allDates[1] ?? null, 'today' => $allDates[0] ?? $today, ]; $teacher = $this->user->find($teacherId); $teacherName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : 'Teacher'; $students = (array) ($this->student->getByClassAndYear($classSectionId, $schoolYear) ?? []); $studentIds = array_map(static fn($row) => (int) ($row['id'] ?? 0), $students); $studentIds = array_values(array_filter($studentIds)); $studentAttendance = []; if (!empty($studentIds) && !empty($allDates)) { $rows = $this->attendance ->newQuery() ->whereIn('student_id', $studentIds) ->whereIn('date', $allDates) ->get() ->map(fn($row) => (array) $row) ->toArray(); foreach ($rows as $row) { $studentAttendance[(int) $row['student_id']][$row['date']] = $row['status'] ?? null; } } $studentsPayload = []; foreach ($students as $student) { $sid = (int) ($student['id'] ?? 0); $slots = []; foreach ($dateSlots as $key => $date) { $slots[$key] = $date ? ($studentAttendance[$sid][$date] ?? 'N/A') : 'N/A'; } $studentsPayload[] = [ 'id' => $sid, 'firstname' => $student['firstname'] ?? '', 'lastname' => $student['lastname'] ?? '', 'school_id' => $student['school_id'] ?? '', 'statuses' => $slots, ]; } $assignedTeachers = (array) ($this->teacherClass->assignedForSectionTerm($classSectionId, $semester, $schoolYear) ?? []); $teacherIds = array_map(static fn($row) => (int) ($row['teacher_id'] ?? 0), $assignedTeachers); $teacherIds = array_values(array_filter($teacherIds)); $teacherStatusMap = []; if (!empty($teacherIds) && !empty($allDates)) { $rows = DB::table('staff_attendance') ->select('user_id', 'date', 'status') ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('user_id', $teacherIds) ->whereIn('date', $allDates) ->get(); foreach ($rows as $row) { $teacherStatusMap[(int) $row->user_id][$row->date] = $row->status ?? null; } } $teacherRows = []; foreach ($assignedTeachers as $row) { $tid = (int) ($row['teacher_id'] ?? 0); $posRaw = strtolower((string) ($row['position'] ?? 'main')); $position = in_array($posRaw, ['main', 'ta'], true) ? $posRaw : 'main'; $name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')); if ($name === '') { $name = 'User #' . $tid; } $slots = []; foreach ($dateSlots as $key => $date) { $slots[$key] = $date ? ($teacherStatusMap[$tid][$date] ?? 'N/A') : 'N/A'; } $teacherRows[] = [ 'teacher_id' => $tid, 'name' => $name, 'position' => $position, 'statuses' => $slots, ]; } $wantedDates = array_values(array_filter(array_unique(array_filter($dateSlots)))); $noSchool = []; if (!empty($wantedDates)) { $events = $this->calendar ->where('no_school', 1) ->whereIn('date', $wantedDates) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); foreach ($events as $event) { $d = (string) ($event['date'] ?? ''); if ($d === '') continue; $noSchool[$d] = [ 'title' => $event['title'] ?? 'No School', 'description' => $event['description'] ?? '', ]; } } return $this->success([ 'teacher' => [ 'id' => $teacherId, 'name' => $teacherName, ], 'class_section_id' => $classSectionId, 'semester' => $semester, 'school_year' => $schoolYear, 'dates' => $dateSlots, 'students' => $studentsPayload, 'teachers' => $teacherRows, 'no_school_days' => $noSchool, 'enable_attendance'=> $this->enableAttendance, ], 'Teacher attendance update data retrieved'); } }