db = \Config\Database::connect(); $this->userModel = new UserModel(); $this->studentModel = new StudentModel(); $this->classSectionModel = new ClassSectionModel(); $this->studentClassModel = new StudentClassModel(); $this->configModel = new ConfigurationModel(); $this->enrollmentModel = new EnrollmentModel(); $this->chargesModel = new EventChargesModel(); $this->eventModel = new EventModel(); $this->eventController = new \App\Controllers\View\InvoiceController(); $this->emergencyContactModel = new EmergencyContactModel(); $this->medicalConditionModel = new StudentMedicalConditionModel(); $this->allergyModel = new StudentAllergyModel(); $this->authorizedUsersModel = new AuthorizedUserModel(); $this->ageDateRefernce = $this->configModel->getConfig('date_age_reference'); $this->lastDayOfRegistration = $this->configModel->getConfig('enrollment_deadline'); $this->schoolStartDate = $this->configModel->getConfig('fall_semester_start'); $this->withdrawalDeadline = $this->configModel->getConfig('refund_deadline'); $this->schoolYear = $this->configModel->getConfig('school_year'); $this->semester = $this->configModel->getConfig('semester'); $this->dateAgeReference = $this->configModel->getConfig('date_age_reference'); $this->maxChilds = (int) $this->configModel->getConfig('max_kids') ?? 0; $this->maxEmergency = (int) $this->configModel->getConfig('max_emergency') ?? 0; helper(['url', 'form']); if (!session()->get('is_logged_in')) { return redirect()->to('/login'); } // Add more role-specific checks if needed if (session()->get('role') !== 'parent') { return redirect()->to('/access_denied'); } } public function index() { // Retrieve all parents from the database where role is parent $parents = $this->userModel->where('role', 'parent')->findAll(); // Pass the parents to the view return view('administrator/parent', ['parents' => $parents]); } public function create() { // Show the form to create a new parent return view('administrator/create_parent'); } public function store() { // Handle the form submission to create a new parent $data = [ 'firstname' => $this->request->getPost('firstname'), 'lastname' => $this->request->getPost('lastname'), 'email' => strtolower($this->request->getPost('email')), 'password' => password_hash($this->request->getPost('password'), PASSWORD_DEFAULT), 'role' => 'parent', ]; $this->userModel->insert($data); return redirect()->to('/administrator/parent'); } public function edit($id) { // Retrieve the parent details to edit $parent = $this->userModel->find($id); return view('administrator/edit_parent', ['parent' => $parent]); } public function update($id) { // Handle the form submission to update an existing parent $data = [ 'firstname' => $this->request->getPost('firstname'), 'lastname' => $this->request->getPost('lastname'), 'email' => strtolower($this->request->getPost('email')), ]; if ($this->request->getPost('password')) { $data['password'] = password_hash($this->request->getPost('password'), PASSWORD_DEFAULT); } $this->userModel->update($id, $data); return redirect()->to('/administrator/parent'); } public function destroy($id) { // Delete the parent $this->userModel->delete($id); return redirect()->to('/administrator/parent'); } public function attendance() { try { // Get parent ID from session $parentId = session()->get('user_id'); if (!$parentId) { return redirect()->back()->with('error', 'Parent session not found.'); } // Get current school year from config $currentSchoolYear = $this->configModel->getConfig('school_year'); // Get selected school year (no semester filter on parent view) $selectedYear = $this->request->getVar('school_year') ?? $currentSchoolYear; // Build query to retrieve attendance $builder = $this->db->table('attendance_data'); $builder->select('students.firstname, students.lastname, attendance_data.date, attendance_data.status, attendance_data.reason'); $builder->join('students', 'students.id = attendance_data.student_id'); $builder->where('attendance_data.school_year', $selectedYear); // No semester filter so both semesters show for the selected year $builder->where('students.parent_id', $parentId); $query = $builder->get(); $attendanceResults = $query->getResultArray(); // Get list of available school years $schoolYears = $this->db->table('attendance_data') ->select('school_year') ->distinct() ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); // If no records found, set a flag to show message in the view if (empty($attendanceResults)) { return view('/parent/attendance', [ 'attendance' => null, 'schoolYears' => $schoolYears, 'selectedYear' => $selectedYear, 'selectedSemester' => null, //'error' => 'No attendance records found for the selected school year and semester.' ]); } // Return view with attendance results return view('/parent/attendance', [ 'attendance' => $attendanceResults, 'schoolYears' => $schoolYears, 'selectedYear' => $selectedYear, 'selectedSemester' => null, 'error' => null ]); } catch (Exception $e) { log_message('error', 'Failed to retrieve attendance data: ' . $e->getMessage()); return view('/parent/attendance', [ 'attendance' => null, 'schoolYears' => [], 'selectedYear' => $selectedYear ?? null, 'selectedSemester' => null, 'error' => 'Failed to retrieve attendance data. Please try again later.' ]); } } public function viewPayments() { $parentId = session()->get('user_id'); // Log the user ID for debugging log_message('info', 'Logged in user ID: ' . $parentId); if (!$parentId) { log_message('error', 'No user ID found in session.'); return view('/parent/payment', ['invoices' => [], 'error' => 'No user ID found in session.']); } // Fetch invoices for the logged-in user based on parent IDs $builder = $this->db->table('invoices'); $builder->select('*'); $builder->groupStart() ->where('parent_id', $parentId) //->orWhere('secondparent_user_id', $userId) ->groupEnd(); $query = $builder->get(); $invoices = $query->getResultArray(); // Log the query and results for debugging log_message('info', 'Query executed: ' . $builder->getCompiledSelect()); log_message('info', 'invoices: ' . print_r($invoices, true)); if (empty($invoices)) { log_message('info', 'No invoices found for user ID: ' . $parentId); } else { log_message('info', count($invoices) . ' payment(s) found for user ID: ' . $parentId); } // Pass the data to the view return view('/parent/payment', [ 'invoices' => $invoices ]); } public function enrollClasses() { try { // Log session data for debugging log_message('info', 'Session Data: ' . print_r(session()->get(), true)); // Get deadlines and school year from config if (!$this->schoolYear) { log_message('error', 'Current school year not found in configuration.'); return redirect()->back()->with('error', 'Configuration error: School year missing.'); } // Get selected school year from request or fallback $selectedYear = $this->request->getGet('school_year') ?? $this->schoolYear; $isEditable = ($selectedYear === $this->schoolYear); // Get parent ID from session $parentId = session()->get('user_id'); if (!$parentId) { log_message('error', 'User ID not found in session.'); return redirect()->back()->with('error', 'User session error. Please log in again.'); } // Verify user type is "parent" from the `users` table $userData = $this->db->table('users') ->select('user_type') ->where('id', $parentId) ->get() ->getRowArray(); if (!$userData || $userData['user_type'] !== 'primary') { return redirect()->back()->with('error', 'Invalid user type. Only primary parents can enroll.'); } // Fetch students with parent_id = current logged in user $students = $this->db->table('students') ->where('parent_id', $parentId) ->get() ->getResultArray(); if (empty($students)) { log_message('error', 'No students found for Parent ID: ' . $parentId); return redirect()->to('/no-kids')->with('error', 'No students found. Please register your child first.'); } // Map enrollment statuses $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' // ✅ Add this line ]; // Attach enrollment and class section data to each student foreach ($students as &$student) { $studentId = $student['id']; // Get class section info (can be multiple sections like Grade + Arabic) $classSections = $this->studentClassModel->getClassSectionsByStudentId($studentId, $selectedYear, true); $student['class_section'] = !empty($classSections) ? implode(', ', $classSections) : 'Class not Assigned'; $isArabicClass = !empty($classSections) && array_reduce( $classSections, static fn($carry, $name) => $carry || (is_string($name) && stripos($name, 'arabic') === 0), false ); // ✅ Get enrollment status AND admission status $enrollment = $this->db->table('enrollments') ->select('enrollment_status, admission_status') ->where('student_id', $studentId) ->where('school_year', $selectedYear) ->get() ->getRowArray(); // ✅ Apply admission status override logic if ($enrollment && isset($enrollment['admission_status'])) { $student['admission_status'] = $enrollment['admission_status']; if ($enrollment['admission_status'] === 'denied') { // Force enrollment_status to denied if admission is denied $student['enrollment_status'] = 'denied'; } else { // Use enrollment_status from DB if admission is accepted/pending $student['enrollment_status'] = $enrollment && isset($statusMap[$enrollment['enrollment_status']]) ? $statusMap[$enrollment['enrollment_status']] : 'not enrolled'; } } else { // No enrollment record found $student['admission_status'] = null; $student['enrollment_status'] = 'not enrolled'; } // If assigned to Arabic class without an enrollment record, display as enrolled. if ($student['enrollment_status'] === 'not enrolled' && $isArabicClass) { $student['enrollment_status'] = 'enrolled'; } // ✅ Updated disable logic to include denied status $student['disable_enroll'] = in_array( $student['enrollment_status'], ['admission under review', 'payment pending', 'enrolled', 'withdraw under review', 'denied'] ); } // Fetch available school years $schoolYears = $this->db->table('enrollments') ->select('school_year') ->distinct() ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); if (empty($schoolYears)) { $schoolYears[] = ['school_year' => $this->schoolYear]; } // Render view return view('/parent/enroll_classes', [ 'students' => $students, 'schoolYears' => $schoolYears, 'selectedYear' => $selectedYear, 'isEditable' => $isEditable, 'withdrawalDeadline' => $this->withdrawalDeadline, 'lastDayOfRegistration' => $this->lastDayOfRegistration, 'schoolStartDate' => $this->schoolStartDate ]); } catch (Exception $e) { log_message('error', 'An error occurred in enrollClasses: ' . $e->getMessage()); return view('errors/html/error_500'); } } public function noKids() { return view('/parent/no_kids_registred'); } public function enrollClassesHandler() { // Call enrollClasses() function at the start of this method $this->enrollClasses(); $refundService = new FeeCalculationService(); // Retrieve enrollment and withdrawal data from the POST request $enroll = $this->request->getPost('enroll'); // Selected students for enrollment $withdraw = $this->request->getPost('withdraw'); // Selected students for withdrawal $parentId = session()->get('user_id'); // Parent ID from session if ($this->lastDayOfRegistration && local_date(utc_now(), 'Y-m-d') > date('Y-m-d', strtotime($this->lastDayOfRegistration))) { if (!empty($enroll)) { return redirect()->back()->with('error', 'Enrollment deadline has passed. New enrollments are not allowed.'); } } // Ensure there are students selected for either enrollment or withdrawal if (empty($enroll) && empty($withdraw)) { return redirect()->back()->with('error', 'No students selected for enrollment or withdrawal.'); } // Handle enrollments $studentData = []; if (!empty($enroll)) { foreach ($enroll as $studentId) { // Get student full name (supports both string return or array with firstname/lastname) $studentInfo = $this->studentModel->getFullNameById($studentId); if (empty($studentName)) { $studentName = "Student ID $studentId"; log_message('warning', "Name for student ID $studentId not found in students table."); } // Save student info into $studentData $studentData[$studentId] = $studentInfo; // raw return from getFullName() // Check if the student already has an enrollment record for the current school year and semester $existingEnrollment = $this->enrollmentModel ->where('student_id', $studentId) ->where('school_year', $this->schoolYear) ->where('semester', $this->semester) ->get() ->getRowArray(); $studentSchoolId = $this->studentModel->getStudentSchoolIdByStudentId($studentId); if (!$studentSchoolId) { return redirect()->back()->with('error', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']}: Student school ID not found."); } if ($existingEnrollment) { if ($existingEnrollment['is_withdrawn'] == 1) { // Reactivate the enrollment if the student was previously withdrawn $this->enrollmentModel->where('id', $existingEnrollment['id'])->update([ 'is_withdrawn' => 0, 'withdrawal_date' => null, 'enrollment_status' => 'payment pending', 'updated_at' => utc_now() ]); log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) has been re-enrolled in enrollment ID {$existingEnrollment['id']}."); // Apply promotion-based class placement for the upcoming year $this->applyPromotionAssignment((int)$studentId, (string)$this->schoolYear); } else { log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) is already actively enrolled."); } } else { // If no enrollment record exists, insert a new enrollment record $result = $this->enrollmentModel->insert([ 'student_id' => $studentId, 'parent_id' => $parentId, 'school_year' => $this->schoolYear, 'semester' => $this->semester, 'enrollment_date' => local_date(utc_now(), 'Y-m-d'), 'is_withdrawn' => 0, 'enrollment_status' => 'admission under review', 'admission_status' => 'pending', 'created_at' => utc_now() ]); if (!$result) { dd($this->enrollmentModel->errors()); } else { log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) has been newly enrolled."); // Apply promotion-based class placement for the upcoming year $this->applyPromotionAssignment((int)$studentId, (string)$this->schoolYear); } } } } // $studentData now holds info for all students processed // Handle withdrawals if (!empty($withdraw)) { foreach ($withdraw as $studentId) { $enrollment = $this->enrollmentModel ->where('student_id', $studentId) ->where('school_year', $this->schoolYear) ->where('semester', $this->semester) ->where('is_withdrawn', 0) // Only withdraw active enrollments ->get() ->getRowArray(); if ($enrollment !== null) { // Update enrollment as withdrawn $this->enrollmentModel->where('id', $enrollment['id'])->update([ 'withdrawal_date' => local_date(utc_now(), 'Y-m-d'), 'enrollment_status' => 'withdraw under review', // Withdrawal needs review 'updated_at' => utc_now() ]); log_message('info', "Student ID $studentId has been withdrawn from enrollment ID {$enrollment['id']}."); // === Trigger refund process === // Find the related invoice (you may need to adjust this based on your DB structure) $invoice = $this->db->table('invoices') ->where('parent_id', $parentId) ->where('school_year', $this->schoolYear) ->orderBy('created_at', 'DESC') ->get() ->getRowArray(); if ($invoice !== null) { $invoiceId = $invoice['id']; $refundTable = $this->db->table('refunds'); $existingRefund = $refundTable ->where('parent_id', $parentId) ->where('invoice_id', $invoiceId) ->where('school_year', $this->schoolYear) ->get() ->getRow(); if ($existingRefund) { // Only update the fields that should change $updateData = [ 'reason' => 'Withdrawal under review for student ID ' . $studentId, 'note' => null, 'updated_by' => session()->get('user_id'), // optionally track updates // Add other fields if *and only if* they must be changed ]; $refundTable ->where('id', $existingRefund->id) ->update($updateData); log_message('info', "Refund record updated for invoice ID {$invoiceId}, student ID {$studentId}."); } else { // Only set these once, for new entries $insertData = [ 'parent_id' => $parentId, 'invoice_id' => $invoiceId, 'requested_at' => utc_now(), 'school_year' => $this->schoolYear, 'status' => 'Pending review', 'reason' => 'Withdrawal under review for student ID ' . $studentId, 'request' => 'new', 'semester' => $this->semester, 'refund_paid_amount' => 0.0, ]; $refundTable->insert($insertData); log_message('info', "Refund record created for invoice ID {$invoiceId}, student ID {$studentId}."); } } else { log_message('error', "No invoice found for parent ID {$parentId}, student ID {$studentId}."); } } else { log_message('error', "No active enrollment found for student ID $studentId."); } } } $parentData = $this->userModel->getUserInfoById($parentId); $parentData['user_id'] = $parentId; // Redirect to the success page after processing enrollment and withdrawal if (!empty($withdraw)) { // Redirect to withdrawal success page if there are withdrawals //parent/enroll_classes return redirect()->to('/parent/enroll_classes'); } else { // Redirect to enrollment success page if there are enrollments Events::trigger('admissionUnderReview', $parentData, $studentData); //send notification for enrolled student return redirect()->to('/parent/enroll_classes'); } } /** * If a promotion_queue record exists for this student and the given school year, * create/update student_class row accordingly (base section until distribution), * and mark the queue row as applied. */ private function applyPromotionAssignment(int $studentId, string $year): void { try { $promo = new \App\Models\PromotionQueueModel(); $classSectionModel = new \App\Models\ClassSectionModel(); $studentClass = new \App\Models\StudentClassModel(); $row = $promo->where('student_id', $studentId) ->where('school_year_to', $year) ->first(); if (!$row) return; // nothing to do $targetSectionId = (int)($row['to_class_section_id'] ?? 0); if ($targetSectionId <= 0) { // Resolve base section for target class (e.g., '3') $base = $classSectionModel->getBaseSectionByClassId((int)$row['to_class_id']); if (!$base) { log_message('warning', 'applyPromotionAssignment: base section not found for class_id=' . (int)$row['to_class_id']); return; } $targetSectionId = (int)($base['class_section_id'] ?? 0); } if ($targetSectionId <= 0) return; // Upsert into student_class for the target school year $exists = $studentClass->where('student_id', $studentId) ->where('school_year', $year) ->first(); $payload = [ 'student_id' => $studentId, 'class_section_id' => $targetSectionId, 'school_year' => $year, 'updated_by' => (int)(session()->get('user_id') ?? 0) ?: null, 'updated_at' => utc_now(), ]; if ($exists) { $studentClass->update((int)$exists['id'], $payload); } else { $payload['created_at'] = utc_now(); $studentClass->insert($payload); } // Mark promotion as applied $promo->update((int)$row['id'], [ 'status' => 'applied', 'updated_at' => utc_now(), 'updated_by' => (int)(session()->get('user_id') ?? 0) ?: null, ]); } catch (\Throwable $e) { log_message('error', 'applyPromotionAssignment failed: ' . $e->getMessage()); } } public function enrollFailure() { echo view('/parent/enroll_failure'); } public function payment() { // Get the logged-in user's ID $userId = session()->get('user_id'); // Fetch invoices records where the user is either the first or second parent $invoices = $this->db->table('invoices') ->select('invoices.*, registeredKids') // Assuming 'registeredKids' is a field ->where('parent_id', $userId) //->orWhere('secondparent_user_id', $userId) ->get() ->getResultArray(); // Pass the invoices data to the view return view('/parent/payment', [ 'invoices' => $invoices ]); } public function paymentHandler() { $parentId = session()->get('user_id'); // Update all students for this parent to mark tuition as paid $this->db->table('students') ->where('parent_id', $parentId) ->update(['tuition_paid' => 1]); // Clear the total tuition fee from the session session()->remove('total_tuition_fee'); // Set success message in session session()->setFlashdata('success', 'Tuition fees paid successfully'); return redirect()->to('/parent/payment_success'); } public function messageParent() { return view('/parent/parent_message'); } public function successMessage() { // Load the success message view return view('/parent/success_message'); } public function addSecondParent() { return view('/parent/add_second_parent'); } public function createUserFromParentOrAuthorizedUser($userData, $relationToStudent) { $schoolIdService = new SchoolIdService(); // Step 1: Generate a secure token for email verification $token = bin2hex(random_bytes(48)); // Step 2: Determine user type based on relationship $userType = in_array(strtolower($relationToStudent), ['wife', 'husband']) ? 'Secondary' : 'Tertiary'; // Step 3: Validate incoming user data $validation = \Config\Services::validation(); $validation->setRules([ 'firstname' => [ 'label' => 'First Name', 'rules' => 'required|min_length[2]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]', 'errors' => [ 'regex_match' => 'First name may only contain letters, spaces, and dashes.' ] ], 'lastname' => [ 'label' => 'Last Name', 'rules' => 'required|min_length[2]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]', 'errors' => [ 'regex_match' => 'Last name may only contain letters, spaces, and dashes.' ] ], 'email' => [ 'label' => 'Email Address', 'rules' => 'required|valid_email|max_length[150]|is_unique[users.email]', 'errors' => [ 'is_unique' => 'This email is already registered.' ] ], 'cellphone' => [ 'label' => 'Cell Phone', 'rules' => 'required|regex_match[/^\d{10}$/]', 'errors' => [ 'regex_match' => 'Phone number must be exactly 10 digits.' ] ], 'gender' => 'required|in_list[Male,Female]', 'city' => 'required|max_length[100]', 'state' => 'required|max_length[100]', 'zip' => 'required|regex_match[/^\d{5}$/]', ]); if (!$validation->run($userData)) { log_message('error', 'User creation failed due to invalid data: ' . json_encode($validation->getErrors())); return false; } // Step 4: Prepare sanitized user data $userEntry = [ 'firstname' => ucfirst(strtolower($userData['firstname'])), 'lastname' => ucfirst(strtolower($userData['lastname'])), 'gender' => $userData['gender'], 'cellphone' => $userData['cellphone'], 'email' => strtolower($userData['email']), 'address_street' => $userData['address_street'] ?? '', 'apt' => $userData['apt'] ?? null, 'city' => ucfirst(strtolower($userData['city'])), 'state' => strtoupper($userData['state']), 'zip' => $userData['zip'], 'accept_school_policy' => $userData['accept_school_policy'] ?? 0, 'token' => $token, 'is_verified' => 0, 'status' => 'Inactive', 'user_type' => $userType, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'school_id' => $schoolIdService->generateUserSchoolId(), ]; try { // Step 5: Insert user into database if (!$this->userModel->insert($userEntry)) { log_message('error', 'Failed to insert user: ' . print_r($this->userModel->errors(), true)); return false; } $userId = $this->userModel->getInsertID(); // Step 6: Send activation email $this->sendActivationEmail($userData['email'], $token); log_message('info', "User with ID $userId created successfully and activation email sent."); return $userId; } catch (Exception $e) { log_message('error', 'Exception during user creation: ' . $e->getMessage()); return false; } } private function sendActivationEmail($email, $token) { $emailController = new \App\Controllers\View\EmailController(); $subject = 'Activate Your Account'; $activationLink = site_url('/user/confirm/' . $token); $message = "Please click the following link to confirm your email and set your password: $activationLink"; if ($emailController->sendEmail($email, $subject, $message)) { log_message('info', 'Activation email sent successfully to ' . $email); } else { log_message('error', 'Failed to send activation email to ' . $email); } } /** * Updates or inserts an authorized user record based on user ID and email. * * @param int $userId The ID of the parent/user who is authorizing access. * @param array $data Contains 'email', 'name', and any other required fields. */ protected function updateAuthorizedUsers($userId, $data) { $validation = \Config\Services::validation(); // Step 1: Define validation rules for authorized user fields $validation->setRules([ 'email' => [ 'label' => 'Email', 'rules' => 'required|valid_email|max_length[150]', 'errors' => [ 'required' => 'Email is required.', 'valid_email' => 'Please provide a valid email address.', 'max_length' => 'Email must be less than 150 characters.' ] ], 'name' => [ 'label' => 'Name', 'rules' => 'required|min_length[3]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]', 'errors' => [ 'required' => 'Name is required.', 'regex_match' => 'Name can only contain letters, spaces, and dashes.', 'min_length' => 'Name must be at least 3 characters long.', 'max_length' => 'Name must be less than 100 characters.' ] ] ]); // Step 2: Run validation if (!$validation->run($data)) { log_message('error', 'Invalid authorized user data: ' . json_encode($validation->getErrors())); return; // Skip update/insert if validation fails } // Step 3: Check if an authorized user with same email/user ID exists $existingAuthorizedUser = $this->authorizedUsersModel ->where('user_id', $userId) ->where('email', $data['email']) ->first(); if ($existingAuthorizedUser) { // Step 4: Update existing authorized user $this->authorizedUsersModel->update($existingAuthorizedUser['id'], $data); } else { // Step 5: Insert new authorized user with pending status $data['user_id'] = $userId; $data['status'] = 'Pending'; // default status for new records $this->authorizedUsersModel->insert($data); } } public function profile($id) { // Fetch the user's data based on the given ID $user = $this->userModel->find($id); // Check if user exists if (!$user) { return redirect()->to('/')->with('error', 'User not found'); } // Load the profile view with the user's data return view('/profile', ['user' => $user]); } public function updateProfile($id) { $user = $this->userModel->find($id); // Step 1: Check if user exists if (!$user) { return redirect()->to('/')->with('error', 'User not found'); } // Step 2: Define validation rules $validation = \Config\Services::validation(); $validation->setRules([ 'firstname' => [ 'label' => 'First Name', 'rules' => 'required|min_length[3]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]', 'errors' => [ 'regex_match' => 'First name must only contain letters, spaces, and dashes.' ] ], 'lastname' => [ 'label' => 'Last Name', 'rules' => 'required|min_length[3]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]', 'errors' => [ 'regex_match' => 'Last name must only contain letters, spaces, and dashes.' ] ], 'cellphone' => [ 'label' => 'Cell Phone', 'rules' => 'required|regex_match[/^\d{10}$/]', 'errors' => [ 'regex_match' => 'Cell phone must be exactly 10 digits.' ] ], 'address_street' => [ 'label' => 'Street Address', 'rules' => 'required|min_length[5]|max_length[255]' ], 'city' => [ 'label' => 'City', 'rules' => 'required|min_length[2]|max_length[100]' ], 'state' => [ 'label' => 'State', 'rules' => 'required|min_length[2]|max_length[100]' ], 'zip' => [ 'label' => 'ZIP Code', 'rules' => 'required|regex_match[/^\d{5}$/]', 'errors' => [ 'regex_match' => 'ZIP code must be exactly 5 digits.' ] ] ]); // Step 3: Validate input; reload view with error messages if validation fails if (!$this->validate($validation->getRules())) { return view('/profile', [ 'user' => $user, 'validation' => $this->validator, ]); } // Step 4: Prepare cleaned and validated data for update $updatedData = [ 'firstname' => ucfirst(strtolower($this->request->getPost('firstname'))), 'lastname' => ucfirst(strtolower($this->request->getPost('lastname'))), 'cellphone' => $this->request->getPost('cellphone'), 'address_street' => $this->request->getPost('address_street'), 'city' => ucfirst(strtolower($this->request->getPost('city'))), 'state' => strtoupper($this->request->getPost('state')), 'zip' => $this->request->getPost('zip'), ]; // Step 5: Update user profile in the database $this->userModel->update($id, $updatedData); // Step 6: Redirect with success message session()->setFlashdata('success', 'Profile updated successfully'); return redirect()->to('/profile/' . $id); } public function isEmailUnique(string $email): bool { $tablesAndColumns = [ 'users' => 'email', 'emergency_contacts' => 'email', ]; foreach ($tablesAndColumns as $table => $column) { // Skip duplicate table lookups with different columns $builder = $this->db->table($table); $builder->where($column, $email); $exists = $builder->countAllResults(); if ($exists > 0) { return false; // Email already exists } } return true; // Email is unique } //form to save student and emergency contact data public function saveStudentRegistration() { log_message('debug', print_r($this->request->getPost(), true)); $schoolIdService = new SchoolIdService(); $parentId = session()->get('user_id'); if (!$this->lastDayOfRegistration || !strtotime($this->lastDayOfRegistration)) { throw new \Exception('Invalid enrollment deadline date.'); } if (strtolower($this->request->getMethod()) !== 'post') { return redirect()->back()->with('error', 'Invalid request method.'); } try { // ✅ 1. Get existing data $data = $this->getRegistrationData($parentId); $existingKids = $data['existingKids']; $existingECs = $data['emergencies']; $maxChilds = $data['maxChilds']; $maxEmergency = $data['maxEmergency']; $existingKidsCount = count($existingKids); $existingECCount = count($existingECs); $incomingFirstNames = $this->request->getPost('studentFirstName') ?? []; $incomingLastNames = $this->request->getPost('studentLastName') ?? []; $incomingDOBs = $this->request->getPost('dob') ?? []; $newStudentCount = count(array_filter($incomingFirstNames)); // ✅ 2. Duplicate student check against existing records foreach ($incomingFirstNames as $i => $firstName) { $lastName = trim($incomingLastNames[$i] ?? ''); $dob = trim($incomingDOBs[$i] ?? ''); if (empty($firstName) || empty($lastName) || empty($dob)) continue; foreach ($existingKids as $kid) { if ( strtolower($kid['firstname']) === strtolower($firstName) && strtolower($kid['lastname']) === strtolower($lastName) && $kid['dob'] === $dob ) { return redirect()->back()->withInput()->with('error', "Duplicate student detected: {$firstName} {$lastName} with DOB {$dob} already exists."); } } } // ✅ 3. Duplicate student check within incoming data $seenStudents = []; foreach ($incomingFirstNames as $i => $firstName) { $lastName = trim($incomingLastNames[$i] ?? ''); $dob = trim($incomingDOBs[$i] ?? ''); if (empty($firstName) || empty($lastName) || empty($dob)) continue; $key = strtolower($firstName . '|' . $lastName . '|' . $dob); if (isset($seenStudents[$key])) { return redirect()->back()->withInput()->with('error', "Duplicate student entry in the form: {$firstName} {$lastName} with DOB {$dob}."); } $seenStudents[$key] = true; } // ✅ 4. Emergency contact validation $incomingECFirst = $this->request->getPost('emergency_firstname') ?? []; $incomingECLast = $this->request->getPost('emergency_lastname') ?? []; $incomingECPhones = $this->request->getPost('emergency_phone') ?? []; $incomingECEmails = $this->request->getPost('emergency_email') ?? []; $newECCount = count(array_filter($incomingECFirst)); // Against existing foreach ($incomingECFirst as $i => $first) { $last = trim($incomingECLast[$i] ?? ''); $phone = preg_replace('/\D/', '', $incomingECPhones[$i] ?? ''); $email = strtolower(trim($incomingECEmails[$i] ?? '')); if (empty($first) || empty($last)) continue; foreach ($existingECs as $contact) { $existingPhone = preg_replace('/\D/', '', $contact['cellphone']); $existingEmail = strtolower($contact['email']); if ( strtolower($contact['emergency_contact_name']) === strtolower(trim($first . ' ' . $last)) || ($phone && $phone === $existingPhone) || ($email && $email === $existingEmail) ) { return redirect()->back()->withInput()->with('error', "Duplicate emergency contact: {$first} {$last} already exists."); } } } // Within incoming data $seenContacts = []; foreach ($incomingECFirst as $i => $first) { $last = trim($incomingECLast[$i] ?? ''); $phone = preg_replace('/\D/', '', $incomingECPhones[$i] ?? ''); $email = strtolower(trim($incomingECEmails[$i] ?? '')); if (empty($first) || empty($last)) continue; $key = strtolower($first . '|' . $last . '|' . $phone . '|' . $email); if (isset($seenContacts[$key])) { return redirect()->back()->withInput()->with('error', "Duplicate emergency contact entry in the form: {$first} {$last}."); } $seenContacts[$key] = true; } // ✅ 5. Check limits if (($existingKidsCount + $newStudentCount) > $maxChilds) { return redirect()->back()->withInput()->with('error', "Student limit exceeded. You have $existingKidsCount and tried to add $newStudentCount (limit: $maxChilds)."); } if (($existingECCount + $newECCount) > $maxEmergency) { return redirect()->back()->withInput()->with('error', "Emergency contact limit exceeded. You have $existingECCount and tried to add $newECCount (limit: $maxEmergency)."); } // ✅ 6. Start transaction and save $this->db->transStart(); $studentAdded = false; // Gather all POST data for last year flags $rawPost = $this->request->getPost(); // Loop through students foreach ($incomingFirstNames as $idx => $firstName) { if (!empty($firstName)) { // ✅ 7. Extract is_new flag (convert to boolean) $lastYearKey = 'last_year_' . $idx; $lastYearVal = strtolower(trim($rawPost[$lastYearKey] ?? '')); $isNew = match ($lastYearVal) { 'yes' => false, // was with us last year => not new 'no' => true, // was not with us => new student default => null // missing or invalid }; if (!isset($isNew)) { return redirect()->back()->withInput()->with('error', "Please select if student #" . ($idx + 1) . " was with us last year."); } // ✅ 8. Pass $isNew to the student save function $result = $this->validateAndSaveOrUpdateStudent($idx, $parentId, $this->semester, $this->schoolYear, $schoolIdService, $isNew, null, null); if ($result) { $studentAdded = true; } } } $hasEC = !empty($incomingECFirst[0]) || !empty(($this->request->getPost('emergency_lastname')[0] ?? '')); if ($hasEC) { $this->saveEmergencyContact($parentId, $this->semester, $this->schoolYear); } if (!$studentAdded && !$hasEC) { throw new \Exception('Please fill in at least a form before submitting.'); } $this->db->transComplete(); if ($this->db->transStatus() === false) { throw new \Exception('DB transaction failed.'); } return redirect()->to(base_url('/parent/child_register'))->with('success', 'Registration successful!'); } catch (\Throwable $e) { log_message('error', $e->getMessage()); $this->db->transRollback(); return redirect()->back()->withInput()->with('error', $e->getMessage()); } } // Function to check if the parent has registered kids and redirect accordingly public function registerKidCheck() { $parentId = session()->get('user_id'); if (!$parentId) { return redirect()->back()->with('error', 'Session expired. Please log in again.'); } try { $data = $this->getRegistrationData($parentId); } catch (\RuntimeException $e) { return redirect()->back()->with('error', $e->getMessage()); } catch (\Throwable $e) { log_message('error', "Failed to fetch registration data: " . $e->getMessage()); return redirect()->back()->with('error', 'Failed to retrieve registration info.'); } $data['lastDayOfRegistration'] = $this->lastDayOfRegistration; $data['registrationAgeDeadline'] = $this->dateAgeReference; return view('parent/register_student', $data); } private function getEnrollmentsByParent($parentId, $schoolYear) { return $this->enrollmentModel ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->orderBy('enrollment_date', 'DESC') ->findAll(); } private function getRegistrationData(int $parentId): array { $enrollments = $this->getEnrollmentsByParent($parentId, $this->schoolYear); $enrollmentMap = []; if (!empty($enrollments)) { foreach ($enrollments as $enroll) { $enrollmentMap[$enroll['student_id']] = $enroll; } } $user = $this->userModel->find($parentId); if (!$user || $user['user_type'] !== 'primary') { throw new \RuntimeException('Only primary parents are allowed to register children.'); } $kids = $this->studentModel->where('parent_id', $parentId)->findAll(); foreach ($kids as &$kid) { $kid['allergies'] = $this->allergyModel ->where('student_id', $kid['id']) ->findColumn('allergy') ?? []; $kid['medical_conditions'] = $this->medicalConditionModel ->where('student_id', $kid['id']) ->findColumn('condition_name') ?? []; $kid['enrollment'] = isset($enrollmentMap[$kid['id']]['id']) && !empty($enrollmentMap[$kid['id']]['id']) ? 1 : 0; } $emergencies = $this->emergencyContactModel->where('parent_id', $parentId)->findAll(); return [ 'existingKids' => $kids, 'emergencies' => $emergencies, 'parent' => $user, 'maxChilds' => $this->maxChilds, 'maxEmergency' => $this->maxEmergency, 'enrollments' => $enrollments, ]; } private function validateAndSaveOrUpdateStudent($idx, $parentId, $semester, $schoolYear, $schoolIdService, $isNew = null, $studentId = null) { $firstName = $this->request->getPost('studentFirstName')[$idx] ?? null; $lastName = $this->request->getPost('studentLastName')[$idx] ?? null; $dob = $this->request->getPost('dob')[$idx] ?? null; $gender = $this->request->getPost('gender')[$idx] ?? null; $grade = $this->request->getPost('registration_grade')[$idx] ?? null; $conditions = $this->request->getPost('medical_conditions')[$idx] ?? []; $allergies = $this->request->getPost('allergies')[$idx] ?? []; $photoRaw = $this->request->getPost('photo_consent')[$idx] ?? ''; if (!$firstName || !$lastName || !$dob || !$gender || !$grade) { return false; } // Normalize names: trim, collapse internal spaces, ucfirst each word $normalize = function (string $s): string { $s = trim($s); $s = preg_replace('/\s+/', ' ', $s); // collapse whitespace // Keep your existing formatName if you prefer; this is a safe default: return mb_convert_case($s, MB_CASE_TITLE, 'UTF-8'); // "ahmed ali" -> "Ahmed Ali" }; $firstName = $normalize($firstName); $lastName = $normalize($lastName); // Still do your stricter name validation if desired $this->validateNames($firstName); $this->validateNames($lastName); $dobObj = new \DateTime($dob); $objregistrationAgeDeadline = new \DateTime($this->ageDateRefernce); $age = $dobObj->diff($objregistrationAgeDeadline)->y; $validation = $this->validateDobAge($dob, $this->ageDateRefernce); if (!$validation['isValid']) { $displayDeadline = (new DateTime($this->ageDateRefernce))->format('m-d-Y'); session()->setFlashdata( 'error', "Student '{$firstName} {$lastName}' {$validation['message']}. Age at deadline would be: $displayDeadline." ); return false; } $photoConsent = strtolower($photoRaw) === 'yes' ? 1 : 0; $studentData = [ 'firstname' => $firstName, 'lastname' => $lastName, 'age' => $age, 'dob' => $dobObj->format('Y-m-d'), 'gender' => $gender, 'registration_grade' => $grade, 'photo_consent' => $photoConsent, 'parent_id' => (int) $parentId, 'year_of_registration' => date('Y'), 'school_year' => $schoolYear, 'semester' => $semester, ]; if (!is_null($isNew)) { $studentData['is_new'] = $isNew ? 1 : 0; } // ---------- DUPLICATE CHECK (case/space-insensitive) ---------- // Using LOWER(TRIM(...)) bound safely; CI4 will bind the value. $fnKey = mb_strtolower(trim(preg_replace('/\s+/', ' ', $firstName)), 'UTF-8'); $lnKey = mb_strtolower(trim(preg_replace('/\s+/', ' ', $lastName)), 'UTF-8'); $dobStr = $dobObj->format('Y-m-d'); $existing = $this->studentModel ->where('school_year', $schoolYear) ->where('dob', $dobStr) ->where('firstname', $firstName) // already normalized above ->where('lastname', $lastName) // already normalized above ->first(); if (!$studentId && $existing) { session()->setFlashdata( 'error', "Student '{$firstName} {$lastName}' with the same birthdate is already registered for $schoolYear." ); return false; } // ---------- UPDATE OR INSERT ---------- if ($studentId) { $this->studentModel->update($studentId, $studentData); } else { $studentData['registration_date'] = utc_now(); $studentData['tuition_paid'] = 0; $studentData['school_id'] = $schoolIdService->generateStudentSchoolId(); try { $studentId = $this->studentModel->insert($studentData, true); } catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) { // If DB unique index (below) triggers 1062 duplicate-key, show a friendly message if (strpos($e->getMessage(), '1062') !== false) { session()->setFlashdata('error', "Student '{$firstName} {$lastName}' with the same birthdate is already registered for $schoolYear."); return false; } throw $e; // other DB errors bubble up } } // ---------- SAVE MEDICAL CONDITIONS ---------- $this->medicalConditionModel->where('student_id', $studentId)->delete(); foreach ((array) $conditions as $c) { $c = trim($c); if ($c !== '') { $this->medicalConditionModel->insert([ 'student_id' => $studentId, 'condition_name' => $c, ]); } } // ---------- SAVE ALLERGIES ---------- $this->allergyModel->where('student_id', $studentId)->delete(); foreach ((array) $allergies as $a) { $a = trim($a); if ($a !== '') { $this->allergyModel->insert([ 'student_id' => $studentId, 'allergy' => $a, ]); } } return true; } /** * Validates if a date of birth falls within required age range by a deadline * * @param string $dob Date of birth (YYYY-MM-DD format) * @param string $registrationAgeDeadline Deadline date (YYYY-MM-DD format) * @param int $minAge Minimum required age (default: 5) * @param int $maxAge Maximum allowed age (default: 18) * @return array ['isValid' => bool, 'message' => string] */ function validateDobAge(string $dob, string $registrationAgeDeadline, int $minAge = 5, int $maxAge = 18): array { $response = ['isValid' => false, 'message' => '', 'age' => null]; // -- Use a stable timezone for all comparisons (UTC is safest) $tz = new DateTimeZone('UTC'); // 1) Empty? $dob = trim($dob); if ($dob === '') { $response['message'] = 'Date of birth is required'; return $response; } // 2) Strict-parse DOB as a date-only at 00:00:00 (use leading "!" to zero unspecified fields) $birthDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $tz); $errs = DateTimeImmutable::getLastErrors(); if ($birthDate === false || ($errs['warning_count'] ?? 0) > 0 || ($errs['error_count'] ?? 0) > 0) { $response['message'] = 'Invalid date format (Use YYYY-MM-DD)'; return $response; } // 3) Parse deadline; if it's date-only, we set it to end-of-day to be inclusive try { $deadline = new DateTimeImmutable($registrationAgeDeadline, $tz); } catch (Throwable $e) { $deadline = new DateTimeImmutable('now', $tz); } // Inclusive of the deadline date $deadline = $deadline->setTime(23, 59, 59); // 4) Age at deadline (now that times are normalized) $ageAtDeadline = $birthDate->diff($deadline)->y; $response['age'] = $ageAtDeadline; // 5) Allowed birthdate window (inclusive) // - Earliest birthdate = deadline - maxAge years (exactly maxAge on deadline is OK) // - Latest birthdate = deadline - minAge years (exactly minAge on deadline is OK) $minBirthDate = $deadline->modify("-{$maxAge} years")->setTime(0, 0, 0); // earliest allowed $maxBirthDate = $deadline->modify("-{$minAge} years")->setTime(23, 59, 59); // latest allowed // 6) Validate $isValid = ($birthDate >= $minBirthDate) && ($birthDate <= $maxBirthDate); $response['isValid'] = $isValid; if (!$isValid) { $response['message'] = sprintf( 'Must be %d-%d years old by %s. Current age would be: %d', $minAge, $maxAge, $deadline->format('m-d-Y'), $ageAtDeadline ); } return $response; } private function validateNames($name) { if (!preg_match('/^[A-Za-z\s\-]{2,30}$/', $name)) { throw new InvalidArgumentException("Invalid name format: Only letters, spaces, or dashes (2–30 chars) allowed."); } } private function saveEmergencyContact($parentId, $semester, $schoolYear, $single = null, $id = null) { $phoneFormatter = new PhoneFormatterService(); // Load the formatter service // ✅ SINGLE CONTACT MODE if ($single !== null) { $firstName = $this->formatName($single['first_name'] ?? ''); $lastName = $this->formatName($single['last_name'] ?? ''); $relation = trim($single['relation'] ?? ''); $phone = $phoneFormatter->formatPhoneNumber($single['cellphone'] ?? ''); $email = strtolower(trim($single['email'] ?? '')); if ($firstName === '' && $lastName === '' && $phone === '(000)-000-0000' && $email === '' && $relation === '') { return; } $this->validateNames($firstName); $this->validateNames($lastName); if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \Exception('Invalid email format for emergency contact.'); } $fullName = $firstName . ' ' . $lastName; $data = [ 'parent_id' => $parentId, 'emergency_contact_name' => $fullName, 'cellphone' => $phone, 'email' => $email, 'relation' => $relation, 'semester' => $semester, 'school_year' => $schoolYear, 'updated_at' => utc_now(), ]; if ($id !== null) { $duplicate = $this->emergencyContactModel ->where('parent_id', $parentId) ->where('emergency_contact_name', $fullName) ->where('cellphone', $phone) ->where('email', $email) ->where('relation', $relation) ->where('id !=', $id) ->first(); if ($duplicate) { session()->setFlashdata('error', 'Another emergency contact with the same information already exists.'); return redirect()->back()->withInput(); } $this->emergencyContactModel->update($id, $data); } else { $exists = $this->emergencyContactModel->where([ 'parent_id' => $parentId, 'emergency_contact_name' => $fullName, 'cellphone' => $phone, 'email' => $email, 'relation' => $relation, ])->first(); if ($exists) { session()->setFlashdata('error', 'This emergency contact is already registered.'); return redirect()->back()->withInput(); } $this->emergencyContactModel->insert($data); } } // ✅ BULK MODE $firstNames = $this->request->getPost('emergency_firstname') ?? []; $lastNames = $this->request->getPost('emergency_lastname') ?? []; $relations = $this->request->getPost('emergency_relation') ?? []; $phones = $this->request->getPost('emergency_phone') ?? []; $emails = $this->request->getPost('emergency_email') ?? []; foreach ($firstNames as $idx => $first) { $firstName = $this->formatName($first ?? ''); $lastName = $this->formatName($lastNames[$idx] ?? ''); $relation = trim($relations[$idx] ?? ''); $phone = $phoneFormatter->formatPhoneNumber($phones[$idx] ?? ''); $email = strtolower(trim($emails[$idx] ?? '')); if ($firstName === '' && $lastName === '' && $phone === '(000)-000-0000' && $email === '' && $relation === '') { continue; } if ($phone === '(000)-000-0000') { throw new \Exception('Invalid phone number.'); } if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \Exception('Invalid email format for emergency contact.'); } $fullName = $firstName . ' ' . $lastName; $exists = $this->emergencyContactModel->where([ 'parent_id' => $parentId, 'emergency_contact_name' => $fullName, 'cellphone' => $phone, 'email' => $email, 'relation' => $relation, ])->first(); if (!$exists) { $this->emergencyContactModel->insert([ 'parent_id' => $parentId, 'emergency_contact_name' => $fullName, 'cellphone' => $phone, 'email' => $email, 'relation' => $relation, 'semester' => $semester, 'school_year' => $schoolYear, ]); } } } public function editEmergencyContact($id = null) { if ($id === null) { $parentId = session()->get('user_id'); $contacts = $this->emergencyContactModel->where('parent_id', $parentId)->findAll(); return view('/parent/edit_emergency_contact', ['contacts' => $contacts]); } if (strtolower($this->request->getMethod()) === 'post') { $rules = [ 'emergency_first_name' => [ 'label' => 'First Name', 'rules' => 'required|min_length[2]|max_length[50]|regex_match[/^[-A-Za-z ]+$/]', 'errors' => [ 'regex_match' => 'First name may only contain letters, spaces, and dashes.' ] ], 'emergency_last_name' => [ 'label' => 'Last Name', 'rules' => 'required|min_length[2]|max_length[50]|regex_match[/^[-A-Za-z ]+$/]', 'errors' => [ 'regex_match' => 'Last name may only contain letters, spaces, and dashes.' ] ], 'cellphone' => [ 'label' => 'Cell Phone', 'rules' => 'required|regex_match[/^\(?\d{3}\)?[-\s]?\d{3}[-\s]?\d{4}$/]', 'errors' => [ 'regex_match' => 'Please enter a valid phone number (e.g., 123-456-7890 or (123) 456 7890).' ] ], 'relation' => [ 'label' => 'Relation', 'rules' => 'required|min_length[3]|max_length[50]' ], 'email' => [ 'label' => 'Email', 'rules' => 'permit_empty|valid_email|max_length[150]' ] ]; if (!$this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $parentId = session()->get('user_id'); $semester = session()->get('active_semester'); $schoolYear = session()->get('active_school_year'); $this->saveEmergencyContact($parentId, $semester, $schoolYear, [ 'first_name' => $this->request->getPost('emergency_first_name'), 'last_name' => $this->request->getPost('emergency_last_name'), 'cellphone' => $this->request->getPost('cellphone'), 'email' => $this->request->getPost('email'), 'relation' => $this->request->getPost('relation'), ], $id); return redirect()->to('/parent/child_register') ->with('message', 'Emergency contact updated successfully.'); } } public function editStudent($id) { $schoolIdService = new \App\Services\SchoolIdService(); if (strtolower($this->request->getMethod()) !== 'post') { return redirect()->back()->with('error', 'Invalid request.'); } $rules = [ 'firstname' => [ 'label' => 'First Name', 'rules' => 'required|min_length[2]|max_length[255]|regex_match[/^[A-Za-z\s\-]+$/]', 'errors' => ['regex_match' => 'Only letters, spaces, and dashes allowed.'] ], 'lastname' => [ 'label' => 'Last Name', 'rules' => 'required|min_length[2]|max_length[255]|regex_match[/^[A-Za-z\s\-]+$/]', 'errors' => ['regex_match' => 'Only letters, spaces, and dashes allowed.'] ], 'dob' => 'required|valid_date', 'gender' => 'required|in_list[Male,Female]', 'photo_consent' => 'required|in_list[0,1]', 'registration_grade' => 'permit_empty|max_length[50]', ]; // ✅ Validate first, keep scalar post data for proper error messages if (!$this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $parentId = session()->get('user_id'); // Prepare formData only AFTER validation $formData = [ 'studentFirstName' => [$this->request->getPost('firstname')], 'studentLastName' => [$this->request->getPost('lastname')], 'dob' => [$this->request->getPost('dob')], 'gender' => [$this->request->getPost('gender')], 'registration_grade' => [$this->request->getPost('registration_grade')], 'photo_consent' => [$this->request->getPost('photo_consent')], 'allergies' => [explode(',', $this->request->getPost('allergies') ?? '')], 'medical_conditions' => [explode(',', $this->request->getPost('medical_conditions') ?? '')], ]; // Inject temporarily only for update logic, not before validation $this->request->setGlobal('post', $formData); // Save/update $this->validateAndSaveOrUpdateStudent(0, $parentId, $this->semester, $this->schoolYear, $schoolIdService, $id, null, null); return redirect()->to('/parent/child_register')->with('success', 'Student updated!'); } public function deleteStudent($id) { if (strtolower($this->request->getMethod()) !== 'post') { return redirect()->back()->with('error', 'Invalid request method.'); } $parentId = session()->get('user_id'); // Confirm student exists and belongs to the parent $student = $this->studentModel ->where('id', $id) ->where('parent_id', $parentId) ->first(); if (!$student) { return redirect()->back()->with('error', 'Student not found or unauthorized.'); } // Perform deletion if (!$this->studentModel->delete($id)) { return redirect()->back()->with('error', 'Failed to delete student.'); } // ✅ Check if parent has any students left $remainingStudents = $this->studentModel ->where('parent_id', $parentId) ->countAllResults(); if ($remainingStudents === 0) { // Delete all emergency contacts linked to this parent $this->emergencyContactModel->where('parent_id', $parentId)->delete(); } return redirect()->to('/parent/child_register')->with('success', 'Student deleted successfully.'); } private function formatName(string $name): string { $name = trim($name); $name = strtolower($name); $name = ucwords($name, ' '); $name = implode('-', array_map('ucfirst', explode('-', $name))); return $name; } public function addStudentForm() { $session = session(); $count = $session->get('registeredStudentsCount') ?? 0; return view('partials/student_form', [ 'registeredStudentsCount' => $count ]); } private function calculateAge($dob) { try { $parts = explode('-', $this->schoolYear); $endYear = isset($parts[1]) ? trim($parts[1]) : date('Y'); $registrationAgeDeadline = "$endYear-12-31"; // Convert both dates into DateTime objects $deadlineObj = \DateTime::createFromFormat('Y-m-d', $registrationAgeDeadline); $dobObj = \DateTime::createFromFormat('Y-m-d', $dob); if (!$deadlineObj || !$dobObj) { throw new \Exception('Invalid date format for DOB or Enrollment_deadline.'); } // For display or logging purposes (if needed) $deadlineForDisplay = $deadlineObj->format('Y-m-d'); $dobForDisplay = $dobObj->format('Y-m-d'); // Compare dates if ($dobObj > $deadlineObj) { log_message('debug', "DOB ($dobForDisplay) is after deadline ($deadlineForDisplay)"); return -1; // Invalid age case } // Calculate age $ageInterval = $dobObj->diff($deadlineObj); return $ageInterval->y; } catch (\Exception $e) { log_message('error', 'Invalid DOB or deadline in calculateAge(): ' . $e->getMessage()); return -1; } } public function parentEventPage() { $schoolYear = session()->get('school_year'); $semester = session()->get('semester'); $parentId = session()->get('user_id'); // Get active events $activeEvents = $this->eventModel->getActiveEvents($schoolYear, $semester); $activeEventCount = is_array($activeEvents) ? count($activeEvents) : 0; // Get charges (participation info) $chargesList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear); // Build a map: "studentId:eventId" => [ 'participation' => ..., 'date' => ... ] $charges = []; foreach ($chargesList as $charge) { $key = $charge['student_id'] . ':' . $charge['event_id']; $charges[$key] = [ 'participation' => $charge['participation'], 'date' => $charge['updated_at'] ?? $charge['created_at'] // Use updated_at if available ]; } // Get enrolled students $students = $this->enrollmentModel->getEnrolledStudents($parentId, $schoolYear); return view('parent/event_participation', [ 'activeEvents' => $activeEvents, 'charges' => $charges, 'yourStudents' => $students, 'activeEventCount' => $activeEventCount, ]); } public function updateParticipation() { $participations = $this->request->getPost('participation'); // ['student_id:event_id' => 'yes'|'no'] $parentId = session()->get('user_id'); foreach ($participations as $key => $value) { [$studentId, $eventId] = explode(':', $key); $existing = $this->chargesModel->where([ 'parent_id' => $parentId, 'student_id' => $studentId, 'event_id' => $eventId, ])->first(); if ($value === 'no') { if ($existing) { $this->chargesModel->delete($existing['id']); } continue; // skip to next entry } // value is 'yes' if ($existing) { $this->chargesModel->update($existing['id'], ['participation' => $value]); } else { $event = $this->eventModel->getEvent($eventId, $this->schoolYear); $this->chargesModel->insert([ 'parent_id' => $parentId, 'student_id' => $studentId, 'event_id' => $eventId, 'participation' => $value, 'charged' => $event['amount'], 'school_year' => $this->schoolYear, 'semester' => $this->semester, 'updated_by' => $parentId ]); } } //recalculate the invoice $this->eventController->generateInvoice($parentId); return redirect()->back()->with('success', 'Participation updated'); } }