user = model(User::class); $this->student = model(Student::class); $this->studentClass = model(StudentClass::class); $this->enrollment = model(Enrollment::class); $this->config = model(Configuration::class); $this->charges = model(EventCharges::class); $this->event = model(Event::class); $this->medicalCondition = model(StudentMedicalCondition::class); $this->allergy = model(StudentAllergy::class); $this->refund = model(Refund::class); $this->invoice = model(Invoice::class); $this->classSection = model(ClassSection::class); $this->promotion = model(PromotionQueue::class); $this->emergencyContact = model(EmergencyContact::class); $this->authorizedUser = model(AuthorizedUser::class); } public function index() { $page = max(1, (int) ($this->request->getGet('page') ?? 1)); $perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20))); $query = $this->user->newQuery() ->select('users.*') ->join('user_roles', 'user_roles.user_id', '=', 'users.id') ->join('roles', 'roles.id', '=', 'user_roles.role_id') ->where('roles.name', 'parent') ->groupBy('users.id'); $result = $this->paginate($query, $page, $perPage); foreach ($result['data'] as &$parent) { unset($parent['password'], $parent['token']); } return $this->success($result, 'Parents retrieved successfully'); } public function enrollClassesData($id) { try { $parentId = (int) $id; $parent = $this->user->find($parentId); if (!$parent) { return $this->error('Parent not found', Response::HTTP_NOT_FOUND); } $schoolYear = (string) ($this->config->getConfig('school_year')); $semester = (string) ($this->config->getConfig('semester')); $lastDayOfRegistration = (string) ($this->config->getConfig('enrollment_deadline')); $schoolStartDate = (string) ($this->config->getConfig('school_start_date')); $withdrawalDeadline = (string) ($this->config->getConfig('refund_deadline')); $selectedYear = (string) ($this->request->getGet('school_year') ?? $schoolYear); $isEditable = ($selectedYear === $schoolYear); $students = DB::table('students') ->where('parent_id', $parentId) ->get() ->map(fn($row) => (array) $row) ->toArray(); $statusMap = [ 'admission under review' => 'admission under review', 'payment pending' => 'payment pending', 'enrolled' => 'enrolled', 'withdraw under review' => 'withdraw under review', 'refund pending' => 'refund pending', 'withdrawn' => 'withdrawn', 'denied' => 'denied', 'waitlist' => 'waitlist', ]; foreach ($students as &$student) { $studentId = (int) ($student['id'] ?? 0); $classRow = DB::table('student_class') ->select('class_section_id') ->where('student_id', $studentId) ->where('school_year', $selectedYear) ->first(); $classSectionId = $classRow->class_section_id ?? null; $student['class_section'] = $classSectionId ? ($this->classSection->getClassSectionNameBySectionId($classSectionId) ?? 'Class not Assigned') : 'Class not Assigned'; $enr = DB::table('enrollments') ->select('enrollment_status', 'admission_status', 'is_withdrawn') ->where('student_id', $studentId) ->where('school_year', $selectedYear) ->orderByDesc('updated_at') ->first(); if ($enr && isset($enr->admission_status)) { $student['admission_status'] = $enr->admission_status; if ($enr->admission_status === 'denied') { $student['enrollment_status'] = 'denied'; } else { $est = $enr->enrollment_status ?? ''; $student['enrollment_status'] = $statusMap[$est] ?? 'not enrolled'; } } else { $student['admission_status'] = null; $student['enrollment_status'] = 'not enrolled'; } $student['disable_enroll'] = in_array( $student['enrollment_status'], ['admission under review', 'payment pending', 'enrolled', 'withdraw under review', 'denied'], true ); } unset($student); $schoolYears = DB::table('enrollments') ->select('school_year') ->distinct() ->orderByDesc('school_year') ->get() ->map(fn($row) => (array) $row) ->toArray(); if (empty($schoolYears)) { $schoolYears[] = ['school_year' => $schoolYear]; } return $this->success([ 'students' => $students, 'schoolYears' => $schoolYears, 'selectedYear' => $selectedYear, 'isEditable' => $isEditable, 'withdrawalDeadline' => $withdrawalDeadline, 'lastDayOfRegistration'=> $lastDayOfRegistration, 'schoolStartDate' => $schoolStartDate, ], 'Enroll classes data'); } catch (\Throwable $e) { log_message('error', 'enrollClassesData error: ' . $e->getMessage()); return $this->error('Server error', Response::HTTP_INTERNAL_SERVER_ERROR); } } public function updateEnrollments($id) { $parentId = (int) $id; $parent = $this->user->find($parentId); if (!$parent) { return $this->error('Parent not found', Response::HTTP_NOT_FOUND); } $payload = $this->payloadData(); $enroll = array_map('intval', (array) ($payload['enroll'] ?? [])); $withdraw = array_map('intval', (array) ($payload['withdraw'] ?? [])); if (empty($enroll) && empty($withdraw)) { return $this->error('No students selected for enrollment or withdrawal.', Response::HTTP_UNPROCESSABLE_ENTITY); } $result = $this->processEnrollmentUpdate($parentId, $enroll, $withdraw); return $this->success($result, 'Enrollment updates processed'); } public function emergencyContacts($id) { $parentId = (int) $id; $rows = $this->emergencyContact->getEmergencyContactsByParentId($parentId); return $this->success($rows, 'Emergency contacts retrieved'); } public function saveEmergencyContact($id) { $parentId = (int) $id; $data = $this->payloadData(); $payload = [ 'parent_id' => $parentId, 'emergency_contact_name' => trim((string) ($data['emergency_contact_name'] ?? '')), 'cellphone' => trim((string) ($data['cellphone'] ?? '')), 'email' => trim((string) ($data['email'] ?? '')) ?: null, 'relation' => trim((string) ($data['relation'] ?? '')) ?: null, 'semester' => (string) $this->config->getConfig('semester'), 'school_year' => (string) $this->config->getConfig('school_year'), ]; if ($payload['emergency_contact_name'] === '' || $payload['cellphone'] === '') { return $this->error('Name and cellphone are required', Response::HTTP_UNPROCESSABLE_ENTITY); } $idToUpdate = isset($data['id']) ? (int) $data['id'] : 0; if ($idToUpdate > 0) { $row = $this->emergencyContact->find($idToUpdate); if (!$row || (int) $row['parent_id'] !== $parentId) { return $this->error('Contact not found', Response::HTTP_NOT_FOUND); } $this->emergencyContact->update($idToUpdate, $payload); return $this->success(['id' => $idToUpdate], 'Emergency contact updated'); } $newId = $this->emergencyContact->insert($payload); if (!$newId) { return $this->error('Failed to save emergency contact', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success(['id' => $newId], 'Emergency contact created'); } public function authorizedUsers($id) { $parentId = (int) $id; $rows = $this->authorizedUser->newQuery() ->where('user_id', $parentId) ->orderByDesc('created_at') ->get() ->toArray(); return $this->success($rows, 'Authorized users retrieved'); } public function addAuthorizedUser($id) { $parentId = (int) $id; $data = $this->payloadData(); $payload = [ 'user_id' => $parentId, 'firstname' => trim((string) ($data['firstname'] ?? '')), 'lastname' => trim((string) ($data['lastname'] ?? '')), 'email' => trim((string) ($data['email'] ?? '')), 'phone_number' => trim((string) ($data['phone_number'] ?? '')), 'relation_to_user'=> trim((string) ($data['relation_to_user'] ?? '')), 'status' => 'Pending', ]; if ($payload['firstname'] === '' || $payload['lastname'] === '' || $payload['email'] === '' || $payload['phone_number'] === '') { return $this->error('Missing required fields', Response::HTTP_UNPROCESSABLE_ENTITY); } if (!$this->authorizedUser->insert($payload)) { return $this->error('Failed to create authorized user', Response::HTTP_INTERNAL_SERVER_ERROR, $this->authorizedUser->errors()); } return $this->success([], 'Authorized user request created'); } public function createStudent($id) { $parentId = (int) $id; $data = $this->payloadData(); $allowed = [ 'firstname', 'lastname', 'dob', 'age', 'gender', 'registration_grade', 'photo_consent', 'rfid_tag', ]; $payload = array_intersect_key($data, array_flip($allowed)); $payload['parent_id'] = $parentId; $payload['is_new'] = isset($data['is_new']) ? (int) $data['is_new'] : 1; $payload['school_year'] = (string) $this->config->getConfig('school_year'); $payload['semester'] = (string) $this->config->getConfig('semester'); $payload['registration_date'] = Carbon::now('UTC')->format('Y-m-d'); if (empty($payload['firstname']) || empty($payload['lastname']) || empty($payload['gender'])) { return $this->error('firstname, lastname, and gender are required', Response::HTTP_UNPROCESSABLE_ENTITY); } $student = $this->student->create($payload); if (!$student) { return $this->error('Failed to create student', Response::HTTP_INTERNAL_SERVER_ERROR, $this->student->errors()); } return $this->success(['id' => $student->id], 'Student created'); } public function updateStudent($id, $studentId) { $parentId = (int) $id; $studentId = (int) $studentId; $row = $this->student->find($studentId); if (!$row || (int) $row['parent_id'] !== $parentId) { return $this->error('Student not found', Response::HTTP_NOT_FOUND); } $data = $this->payloadData(); $allowed = ['firstname', 'lastname', 'dob', 'age', 'gender', 'registration_grade', 'photo_consent', 'rfid_tag']; $payload = array_intersect_key($data, array_flip($allowed)); if (empty($payload)) { return $this->error('No valid fields to update', Response::HTTP_UNPROCESSABLE_ENTITY); } $ok = $this->student->update($studentId, $payload); if (!$ok) { return $this->error('Failed to update student', Response::HTTP_INTERNAL_SERVER_ERROR, $this->student->errors()); } return $this->success([], 'Student updated'); } public function deleteStudent($id, $studentId) { $parentId = (int) $id; $studentId = (int) $studentId; $row = $this->student->find($studentId); if (!$row || (int) $row['parent_id'] !== $parentId) { return $this->error('Student not found', Response::HTTP_NOT_FOUND); } $ok = $this->student->delete($studentId); if (!$ok) { return $this->error('Failed to delete student', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success([], 'Student deleted'); } private function processEnrollmentUpdate(int $parentId, array $enroll, array $withdraw): array { $schoolYear = (string) $this->config->getConfig('school_year'); $semester = (string) $this->config->getConfig('semester'); $lastDayOfRegistration = (string) $this->config->getConfig('enrollment_deadline'); $today = Carbon::now('UTC')->format('Y-m-d'); if ($lastDayOfRegistration && $today > date('Y-m-d', strtotime($lastDayOfRegistration))) { if (!empty($enroll)) { return [ 'enrolled' => [], 'withdrawn' => [], 'messages' => ['Enrollment deadline has passed. New enrollments are not allowed.'], ]; } } $enrolled = []; $withdrawn = []; $messages = []; foreach ($enroll as $studentId) { $studentId = (int) $studentId; if ($studentId <= 0) { continue; } $existing = $this->enrollment->newQuery() ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->where('semester', $semester) ->first(); if ($existing) { if ((int) ($existing['is_withdrawn'] ?? 0) === 1) { $ok = $this->enrollment->update((int) $existing['id'], [ 'is_withdrawn' => 0, 'withdrawal_date' => null, 'enrollment_status' => 'payment pending', 'updated_at' => Carbon::now('UTC')->toDateTimeString(), ]); if ($ok) { $enrolled[] = $studentId; $messages[] = "Re-enrolled student {$studentId}."; } } else { $messages[] = "Student {$studentId} already enrolled."; } } else { $ok = $this->enrollment->insert([ 'student_id' => $studentId, 'parent_id' => $parentId, 'school_year' => $schoolYear, 'semester' => $semester, 'enrollment_date' => $today, 'is_withdrawn' => 0, 'enrollment_status' => 'admission under review', 'admission_status' => 'pending', 'created_at' => Carbon::now('UTC')->toDateTimeString(), ]); if ($ok) { $enrolled[] = $studentId; $messages[] = "Enrolled student {$studentId}."; } } $this->applyPromotionAssignment($studentId, $schoolYear); } foreach ($withdraw as $studentId) { $studentId = (int) $studentId; if ($studentId <= 0) { continue; } $enrollment = $this->enrollment->newQuery() ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->where('semester', $semester) ->where('is_withdrawn', 0) ->first(); if ($enrollment) { $ok = $this->enrollment->update((int) $enrollment['id'], [ 'withdrawal_date' => $today, 'enrollment_status' => 'withdraw under review', 'updated_at' => Carbon::now('UTC')->toDateTimeString(), ]); if ($ok) { $withdrawn[] = $studentId; $messages[] = "Withdrawn student {$studentId}."; } $invoice = $this->invoice->newQuery() ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->orderByDesc('created_at') ->first(); if ($invoice) { $existingRefund = $this->refund->newQuery() ->where('parent_id', $parentId) ->where('invoice_id', $invoice['id']) ->where('school_year', $schoolYear) ->first(); if ($existingRefund) { $this->refund->update((int) $existingRefund['id'], [ 'reason' => 'Withdrawal under review for student ID ' . $studentId, 'note' => null, 'updated_by' => $this->getCurrentUserId(), ]); } else { $this->refund->insert([ 'parent_id' => $parentId, 'invoice_id' => $invoice['id'], 'requested_at' => Carbon::now('UTC')->toDateTimeString(), 'school_year' => $schoolYear, 'status' => 'Pending review', 'reason' => 'Withdrawal under review for student ID ' . $studentId, 'request' => 'new', 'semester' => $semester, 'refund_paid_amount' => 0.0, ]); } } } } return [ 'enrolled' => $enrolled, 'withdrawn' => $withdrawn, 'messages' => $messages, ]; } private function applyPromotionAssignment(int $studentId, string $year): void { try { $row = $this->promotion->newQuery() ->where('student_id', $studentId) ->where('school_year_to', $year) ->first(); if (!$row) { return; } $targetSectionId = (int) ($row['to_class_section_id'] ?? 0); if ($targetSectionId <= 0) { $base = $this->classSection->getBaseSectionByClassId((int) $row['to_class_id']); if (!$base) { return; } $targetSectionId = (int) ($base['class_section_id'] ?? 0); } if ($targetSectionId <= 0) { return; } $semester = (string) $this->config->getConfig('semester'); $exists = $this->studentClass->newQuery() ->where('student_id', $studentId) ->where('school_year', $year) ->where('semester', $semester) ->first(); $payload = [ 'student_id' => $studentId, 'class_section_id' => $targetSectionId, 'school_year' => $year, 'semester' => $semester, 'updated_by' => $this->getCurrentUserId(), 'updated_at' => Carbon::now('UTC')->toDateTimeString(), ]; if ($exists) { $this->studentClass->update((int) $exists['id'], $payload); } else { $payload['created_at'] = Carbon::now('UTC')->toDateTimeString(); $this->studentClass->insert($payload); } $this->promotion->update((int) $row['id'], [ 'status' => 'applied', 'updated_at' => Carbon::now('UTC')->toDateTimeString(), 'updated_by' => $this->getCurrentUserId(), ]); } catch (\Throwable $e) { log_message('error', 'applyPromotionAssignment (API) failed: ' . $e->getMessage()); } } public function show($id = null) { $parent = $this->user->newQuery() ->select('users.*') ->join('user_roles', 'user_roles.user_id', '=', 'users.id') ->join('roles', 'roles.id', '=', 'user_roles.role_id') ->where('roles.name', 'parent') ->where('users.id', $id) ->first(); if (!$parent) { return $this->error('Parent not found', Response::HTTP_NOT_FOUND); } unset($parent['password'], $parent['token']); return $this->success($parent, 'Parent retrieved successfully'); } public function getStudents($id = null) { $parent = $this->user->find($id); if (!$parent) { return $this->error('Parent not found', Response::HTTP_NOT_FOUND); } $students = $this->student->newQuery() ->where('parent_id', $id) ->get() ->map(fn($row) => (array) $row) ->toArray(); foreach ($students as &$student) { $classInfo = DB::table('student_class') ->select('class_section.*', 'classes.name as class_name') ->join('class_section', 'class_section.id', '=', 'student_class.class_section_id', 'left') ->join('classes', 'classes.id', '=', 'class_section.class_id', 'left') ->where('student_class.student_id', $student['id']) ->orderByDesc('student_class.school_year') ->first(); $student['current_class'] = $classInfo ? (array) $classInfo : null; $student['medical_conditions'] = $this->medicalCondition ->where('student_id', $student['id']) ->findColumn('condition_name') ?? []; $student['allergies'] = $this->allergy ->where('student_id', $student['id']) ->findColumn('allergy') ?? []; } return $this->success($students, 'Students retrieved successfully'); } public function attendance($id) { try { $year = $this->request->getGet('school_year') ?? $this->config->getConfig('school_year'); $records = DB::table('attendance_data') ->select('students.firstname', 'students.lastname', 'attendance_data.date', 'attendance_data.status', 'attendance_data.reason') ->join('students', 'students.id', '=', 'attendance_data.student_id') ->where('students.parent_id', $id) ->where('attendance_data.school_year', $year) ->orderByDesc('attendance_data.date') ->get() ->map(fn($row) => (array) $row) ->toArray(); return $this->success($records, 'Attendance data retrieved successfully'); } catch (Throwable $e) { return $this->error('Error fetching attendance: ' . $e->getMessage()); } } public function payments($id) { $invoices = DB::table('invoices') ->where('parent_id', $id) ->orderByDesc('created_at') ->get() ->map(fn($row) => (array) $row) ->toArray(); return $this->success($invoices, 'Payments retrieved successfully'); } public function enrollments($id) { $schoolYear = $this->request->getGet('school_year') ?? $this->config->getConfig('school_year'); $students = $this->student->where('parent_id', $id)->findAll(); $results = []; foreach ($students as $student) { $enrollment = $this->enrollment->newQuery() ->where('student_id', $student['id']) ->where('school_year', $schoolYear) ->orderByDesc('created_at') ->first(); $student['enrollment'] = $enrollment ?? [ 'enrollment_status' => 'not enrolled', 'admission_status' => null, ]; $results[] = $student; } return $this->success($results, 'Enrollment data retrieved successfully'); } public function enroll($id) { try { $data = $this->payloadData(); if (empty($data['student_ids']) || !is_array($data['student_ids'])) { return $this->error('Invalid student_ids payload', Response::HTTP_BAD_REQUEST); } $parentId = (int) $id; $result = $this->processEnrollmentUpdate($parentId, array_map('intval', $data['student_ids']), []); return $this->success($result, 'Enrollment submitted successfully'); } catch (Throwable $e) { return $this->error('Failed to enroll: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } public function events($id) { $schoolYear = $this->config->getConfig('school_year'); $semester = $this->config->getConfig('semester'); $events = $this->event->getActiveEvents($schoolYear, $semester); $charges = $this->charges->getChargesWithEventInfo($id, $schoolYear); $chargeMap = []; foreach ($charges as $charge) { $chargeMap[$charge['student_id'] . ':' . $charge['event_id']] = $charge; } return $this->success([ 'events' => $events, 'charges' => $chargeMap, ], 'Event participation data retrieved'); } public function updateParticipation($id) { try { $payload = $this->payloadData(); $participations = $payload['participation'] ?? []; $schoolYear = $this->config->getConfig('school_year'); $semester = $this->config->getConfig('semester'); foreach ($participations as $key => $value) { [$studentId, $eventId] = explode(':', $key); $existing = $this->charges->where([ 'parent_id' => $id, 'student_id' => $studentId, 'event_id' => $eventId, ])->first(); if ($value === 'no') { if ($existing) { $this->charges->delete($existing['id']); } continue; } $event = $this->event->getEvent($eventId, $schoolYear); if ($existing) { $this->charges->update($existing['id'], ['participation' => $value]); } else { $this->charges->insert([ 'parent_id' => $id, 'student_id' => $studentId, 'event_id' => $eventId, 'participation' => $value, 'charged' => $event['amount'], 'school_year' => $schoolYear, 'semester' => $semester, ]); } } return $this->success([], 'Participation updated successfully'); } catch (Throwable $e) { return $this->error('Failed to update participation: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } public function profile($id) { $user = $this->user->find($id); if (!$user) { return $this->error('Parent not found', Response::HTTP_NOT_FOUND); } unset($user['password'], $user['token']); return $this->success($user, 'Profile retrieved successfully'); } public function updateProfile($id) { try { $data = $this->payloadData(); if (!$data) { return $this->error('Invalid JSON payload', Response::HTTP_BAD_REQUEST); } $allowed = ['firstname', 'lastname', 'cellphone', 'address_street', 'city', 'state', 'zip']; $update = array_intersect_key($data, array_flip($allowed)); if (empty($update)) { return $this->error('No valid fields to update', Response::HTTP_BAD_REQUEST); } $this->user->update($id, $update); return $this->success([], 'Profile updated successfully'); } catch (Throwable $e) { return $this->error('Failed to update profile: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } }