query('school_year'); $semester = $request->query('semester'); $query = DB::table('parent_attendance_reports as par') ->select([ 'par.id', 'par.report_date', 'par.dismiss_time', 'par.reason', 'par.status', 'par.school_year', 'par.semester', 'par.class_section_id', 's.firstname', 's.lastname', 'cs.class_section_name', ]) ->join('students as s', 's.id', '=', 'par.student_id') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'par.class_section_id') ->where('par.type', 'early_dismissal') ->orderBy('par.report_date', 'desc') ->orderByRaw("CONCAT(s.firstname, ' ', s.lastname) ASC"); if ($schoolYear) { $query->where('par.school_year', $schoolYear); } if ($semester) { $query->where('par.semester', $semester); } $rows = $query->get(); $groups = []; foreach ($rows as $row) { $date = $row->report_date ?? 'Unknown'; $groups[$date][] = [ 'id' => $row->id, 'firstname' => $row->firstname, 'lastname' => $row->lastname, 'class_section_name' => $row->class_section_name, 'dismiss_time' => $row->dismiss_time, 'reason' => $row->reason, 'status' => $row->status, 'school_year' => $row->school_year, 'semester' => $row->semester, ]; } $dates = array_keys($groups); $signatures = EarlyDismissalSignature::query() ->whereIn('report_date', $dates) ->get(['report_date', 'filename', 'original_name']); $signatureByDate = []; foreach ($signatures as $sig) { $signatureByDate[(string) $sig->report_date] = [ 'filename' => $sig->filename, 'original_name' => $sig->original_name, ]; } return $this->success([ 'groups' => $groups, 'signatureByDate' => $signatureByDate, ]); } /** * GET /api/v1/attendance/early-dismissals/student-options * Returns active students with their current class section. */ public function studentOptions(): JsonResponse { $students = DB::table('students as s') ->select([ 's.id', 's.firstname', 's.lastname', 'cs.class_section_name', ]) ->leftJoin('student_class as sc', function ($j) { $j->on('sc.student_id', '=', 's.id') ->where('sc.school_year', '=', Configuration::getConfigValueByKey('school_year')); }) ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') ->where('s.is_active', 1) ->orderByRaw("CONCAT(s.firstname, ' ', s.lastname) ASC") ->get(); return $this->success(['students' => $students]); } /** * POST /api/v1/attendance/early-dismissals * Creates a new early dismissal record. */ public function store(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'student_id' => ['required', 'integer', 'min:1', 'exists:students,id'], 'date' => ['required', 'date_format:Y-m-d'], 'dismiss_time' => ['required', 'date_format:H:i'], 'reason' => ['nullable', 'string', 'max:2000'], ]); if ($validator->fails()) { return $this->respondValidationError($validator->errors()->toArray()); } $data = $validator->validated(); $schoolYear = (string) Configuration::getConfigValueByKey('school_year'); $semester = (string) Configuration::getConfigValueByKey('semester'); $sc = DB::table('student_class') ->where('student_id', $data['student_id']) ->where('school_year', $schoolYear) ->orderBy('id') ->first(); $classSectionId = $sc?->class_section_id ?? null; $parentId = DB::table('students') ->where('id', $data['student_id']) ->value('parent_id') ?? Auth::id(); DB::table('parent_attendance_reports')->insert([ 'parent_id' => $parentId ?? Auth::id(), 'student_id' => $data['student_id'], 'class_section_id' => $classSectionId, 'report_date' => $data['date'], 'type' => 'early_dismissal', 'dismiss_time' => $data['dismiss_time'], 'reason' => $data['reason'] ?? null, 'semester' => $semester, 'school_year' => $schoolYear, 'status' => 'new', 'created_at' => now(), 'updated_at' => now(), ]); return $this->success([], 'Early dismissal saved.', Response::HTTP_CREATED); } /** * POST /api/v1/attendance/early-dismissals/signature * Uploads (or replaces) the signature file for a report date. */ public function uploadSignature(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'report_date' => ['required', 'date_format:Y-m-d'], 'school_year' => ['nullable', 'string', 'max:16'], 'semester' => ['nullable', 'string', 'max:32'], 'signature_file' => ['required', 'file', 'mimes:jpg,jpeg,png,webp,gif,pdf', 'max:5120'], ]); if ($validator->fails()) { return $this->respondValidationError($validator->errors()->toArray()); } $file = $request->file('signature_file'); $reportDate = $request->input('report_date'); $schoolYear = $request->input('school_year') ?: (string) Configuration::getConfigValueByKey('school_year'); $semester = $request->input('semester') ?: (string) Configuration::getConfigValueByKey('semester'); $dir = storage_path('uploads/early_dismissal_signatures'); if (!is_dir($dir)) { mkdir($dir, 0755, true); } $existing = EarlyDismissalSignature::query() ->where('report_date', $reportDate) ->where('school_year', $schoolYear) ->where('semester', $semester) ->first(); if ($existing?->filename && file_exists($dir . '/' . $existing->filename)) { unlink($dir . '/' . $existing->filename); } $filename = uniqid('sig_', true) . '.' . $file->getClientOriginalExtension(); $file->move($dir, $filename); EarlyDismissalSignature::updateOrCreate( ['report_date' => $reportDate, 'school_year' => $schoolYear, 'semester' => $semester], [ 'filename' => $filename, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getClientMimeType(), 'file_size' => $file->getSize(), 'uploaded_by' => Auth::id(), ] ); return $this->success(['filename' => $filename], 'Signature uploaded.'); } }