1887 lines
77 KiB
PHP
1887 lines
77 KiB
PHP
<?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));
|
||
$tokenHash = hash('sha256', $token);
|
||
|
||
// 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' => $tokenHash,
|
||
'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, $semester);
|
||
|
||
// Build a map: "studentId:eventId" => [ 'participation' => ..., 'date' => ... ]
|
||
$charges = [];
|
||
$externalParticipantsByEvent = [];
|
||
foreach ($chargesList as $charge) {
|
||
$studentId = $charge['student_id'] ?? null;
|
||
$eventId = (int) ($charge['event_id'] ?? 0);
|
||
|
||
if (!empty($studentId)) {
|
||
$key = $studentId . ':' . $eventId;
|
||
$charges[$key] = [
|
||
'participation' => $charge['participation'],
|
||
'date' => $charge['updated_at'] ?? $charge['created_at'], // Use updated_at if available
|
||
];
|
||
continue;
|
||
}
|
||
|
||
$externalName = trim((string) ($charge['external_firstname'] ?? '') . ' ' . (string) ($charge['external_lastname'] ?? ''));
|
||
if ($eventId > 0 && $externalName !== '') {
|
||
$externalParticipantsByEvent[$eventId][] = [
|
||
'name' => $externalName,
|
||
'note' => (string) ($charge['external_note'] ?? ''),
|
||
'participation' => (string) ($charge['participation'] ?? ''),
|
||
'event_paid' => !empty($charge['event_paid']),
|
||
'charged' => (float) ($charge['charged'] ?? ($charge['event_amount'] ?? 0)),
|
||
];
|
||
}
|
||
}
|
||
|
||
// Get enrolled students
|
||
$students = $this->enrollmentModel->getEnrolledStudents($parentId, $schoolYear);
|
||
|
||
return view('parent/event_participation', [
|
||
'activeEvents' => $activeEvents,
|
||
'charges' => $charges,
|
||
'externalParticipantsByEvent' => $externalParticipantsByEvent,
|
||
'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');
|
||
}
|
||
}
|