520 lines
20 KiB
PHP
Executable File
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');
|
|
}
|
|
}
|