directoryService->assignmentData($request->validated()['school_year'] ?? null); return response()->json([ 'ok' => true, 'students' => StudentAssignmentResource::collection($data['students']), 'classes' => StudentClassSectionResource::collection($data['classes']), 'schoolYears' => $data['schoolYears'], 'selectedYear' => $data['selectedYear'], 'currentYear' => $data['currentYear'], 'isCurrentYear' => $data['isCurrentYear'], ]); } public function index(Request $request): JsonResponse { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ 'school_year' => ['nullable', 'string', 'max:50'], 'parent_id' => ['required', 'integer', 'min:1'], 'parent_ids' => ['nullable', 'array'], 'parent_ids.*' => ['integer', 'min:1'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $parentIds = []; if (!empty($payload['parent_ids'])) { $parentIds = array_values(array_unique(array_map('intval', $payload['parent_ids']))); } if (!empty($payload['parent_id'])) { $parentIds[] = (int) $payload['parent_id']; } $parentIds = array_values(array_unique(array_filter($parentIds))); $data = $this->directoryService->listStudents($payload['school_year'] ?? null, $parentIds ?: null); return response()->json([ 'ok' => true, 'students' => $data['students'], 'school_year' => $data['schoolYear'], 'current_year' => $data['currentYear'], ]); } public function scoreCardSelectable(Request $request): JsonResponse { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ 'q' => ['nullable', 'string', 'max:255'], 'limit' => ['nullable', 'integer', 'min:1', 'max:1000'], 'class_section_id' => ['nullable', 'integer', 'min:1'], 'school_year' => ['nullable', 'string', 'max:32'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $query = trim((string) ($payload['q'] ?? '')); $limit = (int) ($payload['limit'] ?? ($query !== '' ? 50 : 1000)); $classSectionId = isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null; $schoolYear = isset($payload['school_year']) ? trim((string) $payload['school_year']) : null; $user = Auth::user(); if (! $user instanceof User) { return response()->json([ 'ok' => false, 'message' => 'Unauthorized.', ], 401); } $students = $this->scoreCardService->scoreCardSelectableStudents( $user, $query, $limit, $classSectionId > 0 ? $classSectionId : null, $schoolYear !== '' ? $schoolYear : null, ); return response()->json([ 'ok' => true, 'students' => $students, 'selectable' => $students, ]); } public function show(int $studentId): JsonResponse { $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } return response()->json([ 'ok' => true, 'student' => $student->only([ 'id', 'school_id', 'firstname', 'lastname', 'dob', 'age', 'gender', 'is_active', 'registration_grade', 'is_new', 'photo_consent', 'parent_id', 'registration_date', 'tuition_paid', 'semester', 'year_of_registration', 'school_year', ]), ]); } public function store(Request $request): JsonResponse { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $parentId = (int) ($data['parent_id'] ?? 0); $isFirstForParent = $parentId > 0 && Student::query()->where('parent_id', $parentId)->count() === 0; if (empty($data['school_id'])) { $nextId = (int) Student::query()->max('id') + 1; $data['school_id'] = $this->generateSchoolId($nextId); } $rules = [ 'school_id' => ['nullable', 'string', 'max:50'], 'firstname' => ['required', 'string', 'max:150'], 'lastname' => ['required', 'string', 'max:150'], 'dob' => ['nullable', 'date'], 'age' => ['required', 'integer', 'min:0'], 'gender' => ['required', 'string', 'max:20'], 'is_active' => ['nullable', 'boolean'], 'registration_grade' => ['nullable', 'string', 'max:50'], 'is_new' => ['nullable', 'boolean'], 'photo_consent' => ['required', 'boolean'], 'parent_id' => ['required', 'integer', 'min:1'], 'registration_date' => ['nullable', 'date'], 'tuition_paid' => ['nullable', 'boolean'], 'semester' => ['nullable', 'string', 'max:50'], 'year_of_registration' => ['required', 'integer', 'min:1900'], 'school_year' => ['required', 'string', 'max:50'], 'medical_conditions' => ['nullable', 'string'], 'allergies' => ['nullable', 'string'], ]; if ($isFirstForParent) { $rules = array_merge($rules, [ 'name' => ['required', 'string', 'max:150'], 'cellphone' => ['required', 'string', 'max:30'], 'email' => ['required', 'email', 'max:150'], 'relation' => ['nullable', 'string', 'max:80'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); } $validator = Validator::make($data, $rules); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $studentPayload = collect($payload)->only([ 'school_id', 'firstname', 'lastname', 'dob', 'age', 'gender', 'is_active', 'registration_grade', 'is_new', 'photo_consent', 'parent_id', 'registration_date', 'tuition_paid', 'semester', 'year_of_registration', 'school_year', ])->all(); $student = Student::query()->create($studentPayload); if (empty($student->school_id)) { $student->school_id = $this->generateSchoolId($student->id); $student->save(); } $healthPayload = []; if (array_key_exists('medical_conditions', $payload)) { $healthPayload['medical_conditions'] = $payload['medical_conditions']; } if (array_key_exists('allergies', $payload)) { $healthPayload['allergies'] = $payload['allergies']; } if (!empty($healthPayload)) { $this->profileService->updateStudent($student->id, $healthPayload); } if ($isFirstForParent) { $contactPayload = [ 'parent_id' => $parentId, 'emergency_contact_name' => $payload['name'], 'cellphone' => $payload['cellphone'], 'email' => $payload['email'], 'relation' => $payload['relation'] ?? null, 'semester' => $payload['semester'] ?? null, 'school_year' => $payload['school_year'] ?? null, ]; if (Schema::hasColumn('emergency_contacts', 'student_id')) { $contactPayload['student_id'] = $student->id; } EmergencyContact::query()->create($contactPayload); } return response()->json(['ok' => true, 'student' => $student], 201); } public function assignClass(StudentAssignClassRequest $request): JsonResponse { $payload = $request->validated(); $result = $this->assignmentService->assignClasses( (int) $payload['student_id'], $payload['class_section_id'], (bool) ($payload['is_event_only'] ?? false), (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function removeClass(StudentRemoveClassRequest $request): JsonResponse { $payload = $request->validated(); $result = $this->assignmentService->removeClass( (int) $payload['student_id'], (int) $payload['class_section_id'], (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function removed(StudentAssignmentListRequest $request): JsonResponse { $data = $this->directoryService->removedStudents($request->validated()['school_year'] ?? null); return response()->json([ 'ok' => true, 'active_students' => StudentRemovedResource::collection($data['active_students']), 'removed_students' => StudentRemovedResource::collection($data['removed_students']), 'school_year' => $data['school_year'], ]); } public function setActive(StudentSetActiveRequest $request): JsonResponse { $payload = $request->validated(); $result = $this->statusService->setActive( (int) $payload['student_id'], (bool) $payload['is_active'], (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function autoDistribute(StudentAutoDistributeRequest $request): JsonResponse { $payload = $request->validated(); $result = $this->autoDistributionService->autoDistribute( (int) ($payload['class_id'] ?? 0), (int) $payload['students_per_section'], $payload['school_year'] ?? null, (int) ($payload['class_section_id'] ?? 0), (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function promotionTotals(StudentPromotionTotalsRequest $request): JsonResponse { $data = $this->autoDistributionService->promotionTotals($request->validated()['school_year'] ?? null); return response()->json([ 'ok' => true, 'year' => $data['year'], 'rows' => $data['rows'], ]); } public function update(StudentUpdateRequest $request, int $studentId): JsonResponse { $result = $this->profileService->updateStudent($studentId, $request->validated()); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function destroy(int $studentId): JsonResponse { $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $student->update(['is_active' => 0]); return response()->json(['ok' => true]); } public function classes(Request $request, int $studentId): JsonResponse { $validator = Validator::make($request->query->all(), [ 'school_year' => ['nullable', 'string', 'max:50'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $schoolYear = $validator->validated()['school_year'] ?? null; $rows = StudentClass::query() ->select('student_class.*', 'cs.class_section_name') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'student_class.class_section_id') ->where('student_class.student_id', $studentId) ->when($schoolYear !== null && $schoolYear !== '', fn ($q) => $q->where('student_class.school_year', $schoolYear)) ->orderBy('cs.class_section_name') ->get() ->toArray(); return response()->json([ 'ok' => true, 'student_id' => $studentId, 'school_year' => $schoolYear, 'classes' => $rows, ]); } public function assignClassForStudent(Request $request, int $studentId): JsonResponse { $data = array_merge($request->all(), $request->json()->all()); $validator = Validator::make($data, [ 'student_id' => ['required', 'integer', 'min:1'], 'class_section_id' => ['nullable', 'integer', 'min:1'], 'class_section_ids' => ['nullable', 'array'], 'class_section_ids.*' => ['integer', 'min:1'], 'is_event_only' => ['nullable', 'boolean'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); if ((int) $payload['student_id'] !== $studentId) { return response()->json([ 'message' => 'Validation failed.', 'errors' => ['student_id' => ['student_id does not match the route studentId.']], ], 422); } $ids = $payload['class_section_ids'] ?? []; if (empty($ids) && isset($payload['class_section_id'])) { $ids = [(int) $payload['class_section_id']]; } $result = $this->assignmentService->assignClasses( $studentId, $ids, (bool) ($payload['is_event_only'] ?? false), (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function removeClassForStudent(int $studentId, int $classSectionId): JsonResponse { $result = $this->assignmentService->removeClass( $studentId, $classSectionId, (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function promote(Request $request, int $studentId): JsonResponse { $data = array_merge($request->all(), $request->json()->all()); $validator = Validator::make($data, [ 'class_section_id' => ['required', 'integer', 'min:1'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $result = $this->assignmentService->assignClasses( $studentId, [(int) $payload['class_section_id']], false, (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; return response()->json($result + ['ok' => (bool) $result['ok']], $status); } public function attendance(Request $request, int $studentId): JsonResponse { $validator = Validator::make($request->query->all(), [ 'school_year' => ['nullable', 'string', 'max:50'], 'semester' => ['nullable', 'string', 'max:50'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $rows = AttendanceRecord::query() ->where('student_id', $studentId) ->when(!empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) ->when(!empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) ->orderBy('school_year', 'desc') ->orderBy('semester', 'desc') ->get() ->toArray(); return response()->json([ 'ok' => true, 'student_id' => $studentId, 'rows' => $rows, ]); } public function incidents(Request $request, int $studentId): JsonResponse { $validator = Validator::make($request->query->all(), [ 'school_year' => ['nullable', 'string', 'max:50'], 'semester' => ['nullable', 'string', 'max:50'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $rows = Incident::query() ->where('student_id', $studentId) ->when(!empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) ->when(!empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) ->orderByDesc('incident_datetime') ->get() ->toArray(); return response()->json([ 'ok' => true, 'student_id' => $studentId, 'rows' => $rows, ]); } public function scores(Request $request, int $studentId): JsonResponse { $validator = Validator::make($request->query->all(), [ 'school_year' => ['nullable', 'string', 'max:50'], 'semester' => ['nullable', 'string', 'max:50'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $rows = SemesterScore::query() ->where('student_id', $studentId) ->when(!empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) ->when(!empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) ->orderBy('school_year', 'desc') ->orderBy('semester', 'desc') ->get() ->toArray(); return response()->json([ 'ok' => true, 'student_id' => $studentId, 'rows' => $rows, ]); } public function notes(int $studentId): JsonResponse { return response()->json([ 'ok' => true, 'student_id' => $studentId, 'rows' => [], ]); } public function parents(int $studentId): JsonResponse { $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $parent = null; if (!empty($student->parent_id)) { $parent = User::query() ->select(['id', 'firstname', 'lastname', 'email', 'cellphone']) ->find((int) $student->parent_id); } return response()->json([ 'ok' => true, 'student_id' => $studentId, 'parent' => $parent, ]); } public function emergencyContacts(int $studentId): JsonResponse { $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $query = EmergencyContact::query(); if (Schema::hasColumn('emergency_contacts', 'student_id')) { $query->where('student_id', $studentId); } if (!empty($student->parent_id)) { $query->orWhere('parent_id', $student->parent_id); } $rows = $query ->orderBy('emergency_contact_name') ->get() ->toArray(); return response()->json([ 'ok' => true, 'student_id' => $studentId, 'rows' => $rows, ]); } public function addEmergencyContact(Request $request, int $studentId): JsonResponse { $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $data = array_merge($request->all(), $request->json()->all()); $validator = Validator::make($data, [ 'name' => ['required', 'string', 'max:150'], 'cellphone' => ['required', 'string', 'max:30'], 'email' => ['nullable', 'email', 'max:150'], 'relation' => ['nullable', 'string', 'max:80'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $payload = $validator->validated(); $create = [ 'parent_id' => $student->parent_id, 'emergency_contact_name' => $payload['name'], 'cellphone' => $payload['cellphone'], 'email' => $payload['email'] ?? null, 'relation' => $payload['relation'] ?? null, 'semester' => $payload['semester'] ?? null, 'school_year' => $payload['school_year'] ?? null, ]; if (Schema::hasColumn('emergency_contacts', 'student_id')) { $create['student_id'] = $studentId; } $row = EmergencyContact::query()->create($create); return response()->json(['ok' => true, 'row' => $row], 201); } public function updateEmergencyContact(Request $request, int $studentId, int $contactId): JsonResponse { $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $data = array_merge($request->all(), $request->json()->all()); $validator = Validator::make($data, [ 'name' => ['nullable', 'string', 'max:150'], 'cellphone' => ['nullable', 'string', 'max:30'], 'email' => ['nullable', 'email', 'max:150'], 'relation' => ['nullable', 'string', 'max:80'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $contact = EmergencyContact::query()->find($contactId); if (!$contact) { return response()->json(['ok' => false, 'message' => 'Contact not found.'], 404); } $payload = $validator->validated(); $contact->update(array_filter([ 'emergency_contact_name' => $payload['name'] ?? null, 'cellphone' => $payload['cellphone'] ?? null, 'email' => $payload['email'] ?? null, 'relation' => $payload['relation'] ?? null, 'semester' => $payload['semester'] ?? null, 'school_year' => $payload['school_year'] ?? null, ], static fn ($v) => $v !== null)); return response()->json(['ok' => true, 'row' => $contact]); } public function updateBadgeScan(Request $request, int $studentId): JsonResponse { $validator = Validator::make($request->all(), [ 'badge_scan' => ['required', 'string', 'max:100'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $student->update(['rfid_tag' => $validator->validated()['badge_scan']]); return response()->json(['ok' => true]); } public function uploadPhoto(Request $request, int $studentId): JsonResponse { $validator = Validator::make($request->all(), [ 'photo' => ['required', 'file', 'image', 'max:5120'], ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } $student = Student::query()->find($studentId); if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $file = $request->file('photo'); $ext = $file->getClientOriginalExtension() ?: 'jpg'; $path = "student-photos/{$studentId}." . strtolower($ext); Storage::disk('public')->putFileAs('student-photos', $file, "{$studentId}." . strtolower($ext)); return response()->json([ 'ok' => true, 'photo_url' => Storage::disk('public')->url($path), ]); } public function photo(int $studentId): JsonResponse { $base = "student-photos/{$studentId}"; $candidates = ['jpg', 'jpeg', 'png', 'gif', 'webp']; $disk = Storage::disk('public'); foreach ($candidates as $ext) { $path = $base . '.' . $ext; if ($disk->exists($path)) { return response()->json([ 'ok' => true, 'photo_url' => $disk->url($path), ]); } } return response()->json(['ok' => false, 'message' => 'Photo not found.'], 404); } private function generateSchoolId(int $studentId): string { $year = (int) date('y'); return 'STU' . $year . str_pad((string) $studentId, 4, '0', STR_PAD_LEFT); } public function scoreCard(int $studentId): JsonResponse { $result = $this->scoreCardService->scoreCard($studentId); if (!$result['ok']) { return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Not found.'], 404); } return response()->json([ 'ok' => true, 'student' => $result['student'], 'rows' => StudentScoreCardRowResource::collection($result['rows']), ]); } }