Files
alrahma_sunday_school_api/app/Http/Controllers/Api/AttendanceController.php
T
2026-03-05 12:29:37 -05:00

520 lines
20 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Api;
use App\Models\AttendanceData;
use App\Models\AttendanceDay;
use App\Models\Calendar;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\StaffAttendance;
use App\Models\StudentClass;
use App\Models\Student;
use App\Models\TeacherClass;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
class AttendanceController extends BaseApiController
{
protected AttendanceData $attendance;
protected StudentClass $studentClass;
protected ClassSection $classSection;
protected Configuration $config;
protected TeacherClass $teacherClass;
protected StaffAttendance $staffAttendance;
protected AttendanceDay $attendanceDay;
protected Student $student;
protected Calendar $calendar;
protected User $user;
protected string $schoolYear;
protected string $semester;
protected int $enableAttendance;
public function __construct()
{
$this->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');
}
}