Files
alrahma_sunday_school/app/Controllers/View/ParentController.php
T
2026-02-10 22:11:06 -05:00

1867 lines
76 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\UserModel;
use App\Models\StudentModel;
use App\Models\ClassSectionModel;
use App\Models\StudentClassModel;
use App\Models\AuthorizedUserModel;
use App\Models\EmergencyContactModel;
use App\Models\ConfigurationModel;
use App\Models\StudentMedicalConditionModel;
use App\Models\StudentAllergyModel;
use \App\Models\EnrollmentModel;
use \App\Models\EventChargesModel;
use \App\Models\EventModel;
use CodeIgniter\Events\Events;
use App\Services\SchoolIdService;
use App\Services\FeeCalculationService;
use App\Services\PhoneFormatterService;
use InvalidArgumentException;
use DateTimeImmutable;
use DateTimeZone;
use Throwable;
use DateTime;
use Exception;
use CodeIgniter\Database\Exceptions\DatabaseException;
class ParentController extends BaseController
{
protected $userModel;
protected $studentModel;
protected $db;
protected $classSectionModel;
protected $studentClassModel;
protected $configModel;
protected $lastDayOfRegistration;
protected $withdrawalDeadline;
protected $schoolYear;
protected $semester;
protected $dateAgeReference;
protected $enrollmentModel;
protected $maxChilds;
protected $maxEmergency;
protected $chargesModel;
protected $eventModel;
protected $eventController;
protected $emergencyContactModel;
protected $medicalConditionModel;
protected $allergyModel;
protected $authorizedUsersModel;
protected $schoolStartDate;
protected $ageDateRefernce;
public function __construct()
{
$this->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 (230 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');
}
}