fix is_new issue with enrollment fixes
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 48s
Tests / PHPUnit (push) Failing after 1m22s

This commit is contained in:
root
2026-08-20 19:57:25 -04:00
parent 127098b87c
commit 889c037660
29 changed files with 77306 additions and 293 deletions
+12
View File
@@ -492,4 +492,16 @@ class Services extends BaseService
(string) getSemester()
);
}
public static function studentYearStatus(bool $getShared = true): \App\Services\StudentYearStatusService
{
if ($getShared) {
return static::getSharedInstance('studentYearStatus');
}
return new \App\Services\StudentYearStatusService(
\Config\Database::connect(),
model(\App\Models\StudentYearStatusModel::class)
);
}
}
+6 -9
View File
@@ -26,8 +26,9 @@ class ClassPrepController extends PrintablesBaseController
$isGrade5 = static fn(string $g) => (bool) preg_match('/^5(\-|$)/', trim($g));
$b = $this->db->table('student_class sc')
->select('sc.student_id, s.firstname, s.lastname, s.is_new, cs.class_section_name AS grade_label')
->select('sc.student_id, s.firstname, s.lastname, COALESCE(sys.is_new, s.is_new, 1) AS is_new, cs.class_section_name AS grade_label', false)
->join('students s', 's.id = sc.student_id', 'inner')
->join('student_year_status sys', 'sys.student_id = s.id AND sys.school_year = sc.school_year', 'left')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'inner')
->join('classes c', 'c.id = cs.class_id', 'inner')
->where('sc.school_year', $schoolYear);
@@ -122,14 +123,9 @@ class ClassPrepController extends PrintablesBaseController
// --- Pull roster: student_class -> classSection -> classes -> students ---
$builder = $this->db->table('student_class sc')
->select([
'sc.student_id',
's.firstname',
's.lastname',
's.is_new',
'cs.class_section_name AS grade_label',
])
->select('sc.student_id, s.firstname, s.lastname, COALESCE(sys.is_new, s.is_new, 1) AS is_new, cs.class_section_name AS grade_label', false)
->join('students s', 's.id = sc.student_id')
->join('student_year_status sys', 'sys.student_id = s.id AND sys.school_year = sc.school_year', 'left')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
->join('classes c', 'c.id = cs.class_id')
->where('sc.school_year', $schoolYear);
@@ -217,8 +213,9 @@ class ClassPrepController extends PrintablesBaseController
$isGrade5 = static fn($g) => (bool) preg_match('/^5(\-|$)/', trim($g));
$b = $this->db->table('student_class sc')
->select('sc.student_id, s.firstname, s.lastname, s.is_new, cs.class_section_name AS grade_label')
->select('sc.student_id, s.firstname, s.lastname, COALESCE(sys.is_new, s.is_new, 1) AS is_new, cs.class_section_name AS grade_label', false)
->join('students s', 's.id = sc.student_id')
->join('student_year_status sys', 'sys.student_id = s.id AND sys.school_year = sc.school_year', 'left')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
->join('classes c', 'c.id = cs.class_id')
->where('sc.school_year', $schoolYear)
+212 -15
View File
@@ -83,7 +83,8 @@ class ParentController extends BaseController
$this->ageDateRefernce = $this->configModel->getConfig('date_age_reference');
$this->lastDayOfRegistration = $this->configModel->getConfig('enrollment_deadline');
$this->schoolStartDate = $this->configModel->getConfig('fall_semester_start');
$this->schoolStartDate = $this->configModel->getConfig('first_day_of_school')
?: $this->configModel->getConfig('fall_semester_start');
$this->withdrawalDeadline = $this->configModel->getConfig('refund_deadline');
$this->schoolYear = $this->configModel->getConfig('school_year');
$this->semester = getSemester();
@@ -263,7 +264,7 @@ class ParentController extends BaseController
// Verify user type is "parent" from the `users` table
$userData = $this->db->table('users')
->select('user_type, accept_school_policy')
->select('user_type, accept_school_policy, firstname, lastname, cellphone, address_street, apt, city, state, zip')
->where('id', $parentId)
->get()
->getRowArray();
@@ -385,6 +386,30 @@ class ParentController extends BaseController
$student['parent_enrollment_state'] = $this->parentEnrollmentState($student);
}
}
unset($student);
service('studentYearStatus')->attachToStudents($students, $selectedYear);
foreach ($students as &$student) {
$studentId = (int) ($student['id'] ?? 0);
if ($studentId > 0 && $this->isReturningReEnrollmentStudent($studentId, $selectedYear)) {
$student['is_new'] = 0;
}
}
unset($student);
$enrollmentStartStep = (int) $this->request->getGet('start');
if ($enrollmentStartStep < 1 || $enrollmentStartStep > 5) {
$enrollmentStartStep = 0;
}
$preselectedStudentIds = [];
$studentsParam = trim((string) $this->request->getGet('students'));
if ($studentsParam !== '') {
$preselectedStudentIds = array_values(array_unique(array_filter(
array_map('intval', explode(',', $studentsParam)),
static fn (int $id): bool => $id > 0
)));
}
// Render view
return view('/parent/enroll_classes', [
@@ -399,6 +424,9 @@ class ParentController extends BaseController
'hasAcceptedSchoolPolicy' => $this->hasAcceptedPolicyForYear((int) $parentId, $selectedYear),
'familyFinancialSummary' => $this->familyFinancialSummary((int) $parentId, $previousSchoolYear, $selectedYear, $students),
'enrollmentFeeSchedule' => $this->enrollmentFeeSchedule($selectedYear),
'parentContact' => $this->parentContactForEnrollment($userData),
'enrollmentStartStep' => $enrollmentStartStep,
'preselectedStudentIds' => $preselectedStudentIds,
]);
} catch (Exception $e) {
log_message('error', 'An error occurred in enrollClasses: ' . $e->getMessage());
@@ -452,6 +480,7 @@ class ParentController extends BaseController
// Handle enrollments
$studentData = [];
$enrollmentResultMessages = [];
if (!empty($enroll)) {
$selectedYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
@@ -476,6 +505,11 @@ class ParentController extends BaseController
return redirect()->back()->withInput()->with('error', implode(' ', $studentInfoErrors));
}
$parentContactErrors = $this->updateEnrollmentParentContact((int) $parentId);
if ($parentContactErrors !== []) {
return redirect()->back()->withInput()->with('error', implode(' ', $parentContactErrors));
}
foreach ($submittedStudentIds as $studentId) {
try {
$evaluation = $transitionService->evaluateForParent((int) $parentId, $studentId, $previousSchoolYear, $selectedYear, 'parent');
@@ -539,12 +573,13 @@ class ParentController extends BaseController
$studentName = trim((string) ($studentData[$studentId]['firstname'] ?? '') . ' ' . (string) ($studentData[$studentId]['lastname'] ?? '')) ?: 'Student ID ' . $studentId;
$isReturningReEnrollment = $this->isReturningReEnrollmentStudent((int)$studentId, $selectedYear);
$submittedEnrollmentContexts[$studentId] = $isReturningReEnrollment ? 're_enrollment' : 'first_enrollment';
$isNewForSelectedYear = ! $isReturningReEnrollment
&& service('studentYearStatus')->isNew($studentId, $selectedYear);
$submittedEnrollmentContexts[$studentId] = $isNewForSelectedYear ? 'first_enrollment' : 're_enrollment';
$targetEnrollmentStatus = $isNewForSelectedYear ? 'admission under review' : 'payment pending';
$targetAdmissionStatus = $isNewForSelectedYear ? 'pending' : 'accepted';
if ($existingEnrollment) {
$targetEnrollmentStatus = $isReturningReEnrollment ? 'payment pending' : 'admission under review';
$targetAdmissionStatus = $isReturningReEnrollment ? 'accepted' : 'pending';
$update = $this->enrollmentPayloadFromEvaluation($evaluation, [
'is_withdrawn' => 0,
'withdrawal_date' => null,
@@ -595,10 +630,8 @@ class ParentController extends BaseController
$this->enrollmentAuditPayload($update, $evaluation),
implode(' ', array_map('strval', $evaluation['rule_codes'] ?? []))
);
$enrollmentResultMessages[] = $studentName . ': ' . $targetEnrollmentStatus;
} else {
$targetEnrollmentStatus = $isReturningReEnrollment ? 'payment pending' : 'admission under review';
$targetAdmissionStatus = $isReturningReEnrollment ? 'accepted' : 'pending';
// If no enrollment record exists, insert a new enrollment record
$payload = $this->enrollmentPayloadFromEvaluation($evaluation, [
'student_id' => $studentId,
@@ -636,6 +669,7 @@ class ParentController extends BaseController
$this->enrollmentAuditPayload($payload, $evaluation),
implode(' ', array_map('strval', $evaluation['rule_codes'] ?? []))
);
$enrollmentResultMessages[] = $studentName . ': ' . $targetEnrollmentStatus;
}
}
$this->db->transComplete();
@@ -649,8 +683,11 @@ class ParentController extends BaseController
// $studentData now holds info for all students processed
// Handle withdrawals
$withdrawalResultMessages = [];
$withdrawalErrors = [];
if (!empty($withdraw)) {
foreach ($withdraw as $studentId) {
$studentId = (int) $studentId;
$enrollment = $this->enrollmentModel
->where('student_id', $studentId)
->where('school_year', $this->schoolYear)
@@ -673,6 +710,7 @@ class ParentController extends BaseController
'updated_at' => utc_now()
], (int) $parentId, 'parent_withdrawal_requested');
log_message('info', "Student ID $studentId has been withdrawn from enrollment ID {$enrollment['id']}.");
$withdrawalResultMessages[] = 'Student ID ' . $studentId . ': withdraw under review';
// === Trigger refund process ===
// Find the related invoice (you may need to adjust this based on your DB structure)
@@ -752,6 +790,7 @@ class ParentController extends BaseController
}
} else {
log_message('error', "No active enrollment found for student ID $studentId.");
$withdrawalErrors[] = 'Student ID ' . $studentId . ': no active enrollment was found.';
}
}
}
@@ -761,13 +800,32 @@ class ParentController extends BaseController
// 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');
$redirect = redirect()->to('/parent/enroll_classes');
if ($withdrawalResultMessages !== []) {
$redirect = $redirect->with('success', 'Withdrawal request submitted. ' . implode(' ', $withdrawalResultMessages));
}
if ($withdrawalErrors !== []) {
$redirect = $redirect->with('error', 'Some withdrawal requests failed. ' . implode(' ', $withdrawalErrors));
}
return $redirect;
} else {
// Redirect to enrollment success page if there are enrollments
$contextValues = array_values(array_unique($submittedEnrollmentContexts ?? []));
$parentData['enrollment_context'] = count($contextValues) === 1 ? $contextValues[0] : 'mixed';
Events::trigger('admissionUnderReview', $parentData, $studentData); //send notification for enrolled student
return redirect()->to('/parent/enroll_classes');
try {
Events::trigger('admissionUnderReview', $parentData, $studentData);
} catch (Throwable $e) {
log_message('error', 'Enrollment notification failed after successful submission: {message}', [
'message' => $e->getMessage(),
]);
}
$successMessage = 'Enrollment submitted successfully.';
if ($enrollmentResultMessages !== []) {
$successMessage .= ' ' . implode(' ', $enrollmentResultMessages);
}
return redirect()->to('/parent/enroll_success')->with('success', $successMessage);
}
}
@@ -887,6 +945,107 @@ class ParentController extends BaseController
return $errors;
}
/**
* @param array<string, mixed> $user
* @return array<string, string>
*/
private function parentContactForEnrollment(array $user): array
{
$posted = old('parent_contact');
$posted = is_array($posted) ? $posted : [];
$phoneDigits = preg_replace('/\D/', '', (string) ($posted['cellphone'] ?? $user['cellphone'] ?? '')) ?? '';
$phoneDisplay = strlen($phoneDigits) === 10
? substr($phoneDigits, 0, 3) . '-' . substr($phoneDigits, 3, 3) . '-' . substr($phoneDigits, 6)
: trim((string) ($posted['cellphone'] ?? $user['cellphone'] ?? ''));
return [
'firstname' => trim((string) ($user['firstname'] ?? '')),
'lastname' => trim((string) ($user['lastname'] ?? '')),
'cellphone' => $phoneDisplay,
'address_street' => trim((string) ($posted['address_street'] ?? $user['address_street'] ?? '')),
'apt' => trim((string) ($posted['apt'] ?? $user['apt'] ?? '')),
'city' => trim((string) ($posted['city'] ?? $user['city'] ?? '')),
'state' => strtoupper(trim((string) ($posted['state'] ?? $user['state'] ?? ''))),
'zip' => trim((string) ($posted['zip'] ?? $user['zip'] ?? '')),
];
}
/**
* @return list<string>
*/
private function updateEnrollmentParentContact(int $parentId): array
{
$fields = $this->request->getPost('parent_contact');
$normalized = $this->normalizeEnrollmentParentContact(is_array($fields) ? $fields : []);
if ($normalized['errors'] !== []) {
return $normalized['errors'];
}
if (! $this->userModel->update($parentId, $normalized['data'])) {
return ['Parent contact information could not be updated.'];
}
return [];
}
/**
* @param array<string, mixed> $fields
* @return array{errors: list<string>, data: array<string, string>}
*/
private function normalizeEnrollmentParentContact(array $fields): array
{
$phoneDigits = preg_replace('/\D/', '', (string) ($fields['cellphone'] ?? '')) ?? '';
$street = trim((string) ($fields['address_street'] ?? ''));
$apt = trim((string) ($fields['apt'] ?? ''));
$city = trim((string) ($fields['city'] ?? ''));
$state = strtoupper(trim((string) ($fields['state'] ?? '')));
$zip = trim((string) ($fields['zip'] ?? ''));
$errors = [];
if (strlen($phoneDigits) !== 10) {
$errors[] = 'A valid 10-digit home/cell phone number is required.';
}
if (strlen($street) < 5 || strlen($street) > 255) {
$errors[] = 'Home street address is required.';
}
if ($apt !== '' && strlen($apt) > 15) {
$errors[] = 'Apartment or unit must be 15 characters or fewer.';
}
if (strlen($city) < 2 || strlen($city) > 100) {
$errors[] = 'City is required.';
}
if (! preg_match('/^[A-Z]{2}$/', $state)) {
$errors[] = 'State is required.';
}
if (! preg_match('/^\d{5}$/', $zip)) {
$errors[] = 'A valid 5-digit ZIP code is required.';
}
if ($errors !== []) {
return ['errors' => $errors, 'data' => []];
}
$formattedPhone = (new PhoneFormatterService())->formatPhoneNumber($phoneDigits);
return [
'errors' => [],
'data' => [
'cellphone' => $formattedPhone ?: $phoneDigits,
'address_street' => $street,
'apt' => $apt,
'city' => ucfirst(strtolower($city)),
'state' => $state,
'zip' => $zip,
'updated_at' => utc_now(),
],
];
}
/**
* @param mixed $selected
* @return list<string>
@@ -1968,6 +2127,11 @@ class ParentController extends BaseController
return ((int) $matches[1] - 1) . '-' . ((int) $matches[2] - 1);
}
public function enrollSuccess()
{
return view('/parent/enroll_success');
}
public function enrollFailure()
{
echo view('/parent/enroll_failure');
@@ -2446,6 +2610,7 @@ class ParentController extends BaseController
$this->db->transStart();
$studentAdded = false;
$registeredStudentIds = [];
// Gather all POST data for last year flags
$rawPost = $this->request->getPost();
@@ -2472,6 +2637,9 @@ class ParentController extends BaseController
if ($result) {
$studentAdded = true;
if (is_numeric($result) && (int) $result > 0) {
$registeredStudentIds[] = (int) $result;
}
}
}
}
@@ -2492,6 +2660,16 @@ class ParentController extends BaseController
throw new \Exception('DB transaction failed.');
}
if ($studentAdded) {
$enrollmentQuery = ['start' => 2];
if ($registeredStudentIds !== []) {
$enrollmentQuery['students'] = implode(',', array_values(array_unique($registeredStudentIds)));
}
return redirect()->to(base_url('/parent/enroll_classes?' . http_build_query($enrollmentQuery)))
->with('success', 'Registration successful! Continue enrollment by confirming your home address and phone number.');
}
return redirect()->to(base_url('/parent/child_register'))->with('success', 'Registration successful!');
} catch (\Throwable $e) {
log_message('error', $e->getMessage());
@@ -2564,8 +2742,15 @@ class ParentController extends BaseController
->findColumn('condition_name') ?? [];
$kid['enrollment'] = isset($enrollmentMap[$studentId]['id']) && !empty($enrollmentMap[$studentId]['id']) ? 1 : 0;
}
unset($kid);
service('studentYearStatus')->attachToStudents($kids, $selectedSchoolYear);
foreach ($kids as &$kid) {
$kid['can_delete'] = $this->canParentDeleteStudent($kid, $parentId);
}
unset($kid);
$emergencies = $this->emergencyContactModel->where('parent_id', $parentId)->findAll();
@@ -2693,6 +2878,10 @@ $existing = $this->studentModel
}
}
if (! is_null($isNew) && (int) $studentId > 0) {
service('studentYearStatus')->upsert((int) $studentId, (string) $schoolYear, (bool) $isNew);
}
// ---------- SAVE MEDICAL CONDITIONS ----------
$this->medicalConditionModel->where('student_id', $studentId)->delete();
foreach ((array) $conditions as $c) {
@@ -2710,14 +2899,14 @@ $existing = $this->studentModel
foreach ((array) $allergies as $a) {
$a = trim($a);
if ($a !== '') {
$this->allergyModel->insert([
$this->allergyModel->insert([
'student_id' => $studentId,
'allergy' => $a,
]);
}
}
return true;
return (int) $studentId > 0 ? (int) $studentId : true;
}
@@ -3099,7 +3288,15 @@ $existing = $this->studentModel
return false;
}
if ((string) ($student['is_new'] ?? '1') === '0') {
$statusYear = trim((string) ($student['school_year'] ?? ''));
if ($statusYear === '') {
$statusYear = (string) (service('studentYearStatus')->activeSchoolYear() ?? '');
}
$isNew = $statusYear !== ''
? service('studentYearStatus')->isNew($studentId, $statusYear)
: ((string) ($student['is_new'] ?? '1') === '1');
if (! $isNew) {
return false;
}
+11 -3
View File
@@ -542,7 +542,7 @@ class StudentController extends BaseController
// Retrieve students with an enrollment in the selected year (any status)
$students = $this->studentModel
->select('students.id, students.firstname, students.lastname, students.registration_date, students.is_new, students.age, students.parent_id, students.registration_grade')
->select('students.id, students.firstname, students.lastname, students.registration_date, students.age, students.parent_id, students.registration_grade')
->join('enrollments e', 'e.student_id = students.id', 'inner')
->where('e.school_year', $selectedYear)
->groupBy('students.id')
@@ -552,11 +552,13 @@ class StudentController extends BaseController
// Fallback: if none found (data inconsistency), include students even without an enrollment row
if (empty($students)) {
$students = $this->studentModel
->select('students.id, students.firstname, students.lastname, students.registration_date, students.is_new, students.age, students.parent_id, students.registration_grade')
->select('students.id, students.firstname, students.lastname, students.registration_date, students.age, students.parent_id, students.registration_grade')
->orderBy('students.lastname', 'ASC')
->findAll();
}
service('studentYearStatus')->attachToStudents($students, $selectedYear);
$studentData = [];
foreach ($students as $student) {
$sectionNames = $this->studentClassModel->getClassSectionsByStudentIdWithFlags((int)$student['id'], $selectedYear, true);
@@ -2608,7 +2610,6 @@ class StudentController extends BaseController
'school_year' => $in['school_year'] ?: null,
'rfid_tag' => $in['rfid_tag'] ?: null,
'semester' => $in['semester'] ?: null,
'is_new' => (int) ($in['is_new'] === '1'),
];
// Normalize health lists (server-side safety)
@@ -2631,6 +2632,13 @@ class StudentController extends BaseController
->with('errors', $this->studentModel->errors() ?: []);
}
$statusYear = $in['school_year'] !== ''
? $in['school_year']
: (string) ($this->currentSchoolYearName((string) ($this->schoolYear ?? '')) ?: '');
if ($statusYear !== '') {
service('studentYearStatus')->upsert($id, $statusYear, $in['is_new'] === '1');
}
// Only sync health lists when user actually changed them (touched=1)
// Fallback: if no touch flag but a non-empty value was explicitly posted, also sync.
$medTouched = ($in['medical_touched'] === '1');
@@ -0,0 +1,212 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateStudentYearStatus extends Migration
{
public function up(): void
{
if ($this->db->tableExists('student_year_status')) {
$this->backfillExistingStatus();
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'student_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => false,
],
'is_new' => [
'type' => 'TINYINT',
'constraint' => 1,
'null' => false,
'default' => 1,
'comment' => '1 = new student for this school year, 0 = returning',
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['student_id', 'school_year'], 'uq_student_year_status');
$this->forge->addKey('school_year');
$this->forge->addKey('student_id');
$this->forge->createTable('student_year_status', true);
$this->backfillExistingStatus();
}
public function down(): void
{
$this->forge->dropTable('student_year_status', true);
}
private function backfillExistingStatus(): void
{
if (! $this->db->tableExists('student_year_status') || ! $this->db->tableExists('students')) {
return;
}
$now = date('Y-m-d H:i:s');
$hasStudentSchoolYear = $this->db->fieldExists('school_year', 'students');
$select = ['id', 'is_new'];
if ($hasStudentSchoolYear) {
$select[] = 'school_year';
}
$students = $this->db->table('students')
->select($select)
->get()
->getResultArray();
if ($hasStudentSchoolYear) {
foreach ($students as $student) {
$studentId = (int) ($student['id'] ?? 0);
if ($studentId <= 0) {
continue;
}
$schoolYear = trim((string) ($student['school_year'] ?? ''));
if (preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
$this->upsertStatus(
$studentId,
$schoolYear,
(int) ($student['is_new'] ?? 1) === 1 ? 1 : 0,
$now
);
}
}
}
$activeYear = $this->configuredSchoolYear();
if ($activeYear === null) {
return;
}
$activeStart = $this->schoolYearStartYear($activeYear);
$returningIds = $activeStart === null ? [] : $this->priorYearStudentIds($activeStart);
foreach ($students as $student) {
$studentId = (int) ($student['id'] ?? 0);
if ($studentId <= 0) {
continue;
}
$isNew = isset($returningIds[$studentId]) ? 0 : ((int) ($student['is_new'] ?? 1) === 1 ? 1 : 0);
$this->upsertStatus($studentId, $activeYear, $isNew, $now);
}
}
private function upsertStatus(int $studentId, string $schoolYear, int $isNew, string $now): void
{
$existing = $this->db->table('student_year_status')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->get(1)
->getRowArray();
if ($existing !== null) {
return;
}
$this->db->table('student_year_status')->insert([
'student_id' => $studentId,
'school_year' => $schoolYear,
'is_new' => $isNew,
'created_at' => $now,
'updated_at' => $now,
]);
}
/**
* @return array<int, true>
*/
private function priorYearStudentIds(int $selectedStartYear): array
{
$studentIds = [];
foreach (['enrollments', 'student_class'] as $table) {
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
continue;
}
$rows = $this->db->table($table)
->select('student_id, school_year')
->where('student_id IS NOT NULL', null, false)
->where('school_year IS NOT NULL', null, false)
->get()
->getResultArray();
foreach ($rows as $row) {
$rowStartYear = $this->schoolYearStartYear((string) ($row['school_year'] ?? ''));
$studentId = (int) ($row['student_id'] ?? 0);
if ($studentId > 0 && $rowStartYear !== null && $rowStartYear < $selectedStartYear) {
$studentIds[$studentId] = true;
}
}
}
return $studentIds;
}
private function schoolYearStartYear(string $schoolYear): ?int
{
if (! preg_match('/^(\d{4})-\d{4}$/', trim($schoolYear), $matches)) {
return null;
}
return (int) $matches[1];
}
private function configuredSchoolYear(): ?string
{
if ($this->db->tableExists('school_years')) {
$row = $this->db->table('school_years')
->select('name')
->where('status', 'active')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$name = trim((string) ($row['name'] ?? ''));
if (preg_match('/^\d{4}-\d{4}$/', $name)) {
return $name;
}
}
if (! $this->db->tableExists('configuration')) {
return null;
}
$row = $this->db->table('configuration')
->select('config_value')
->where('config_key', 'school_year')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$name = trim((string) ($row['config_value'] ?? ''));
return preg_match('/^\d{4}-\d{4}$/', $name) ? $name : null;
}
}
+4 -2
View File
@@ -9,7 +9,7 @@ if (!function_exists('student_normalized_enrollment_status')) {
$status = 'withdrawn';
}
if ($status === '' && $studentId > 0) {
if ($studentId > 0) {
$schoolYear = trim((string)($schoolYear ?? ''));
if ($schoolYear === '') {
try {
@@ -44,7 +44,9 @@ if (!function_exists('student_normalized_enrollment_status')) {
}
}
}
$status = $statusCache[$cacheKey];
if (($statusCache[$cacheKey] ?? '') !== '') {
$status = $statusCache[$cacheKey];
}
}
return $status;
+36 -12
View File
@@ -78,6 +78,23 @@ class StudentClassModel extends Model
return $builder->groupEnd();
}
private function latestEnrollmentJoinCondition(string $schoolYear): string
{
$schoolYear = trim($schoolYear);
$yearCondition = $schoolYear !== ''
? 'e_latest.school_year = ' . $this->db->escape($schoolYear)
: 'e_latest.school_year = student_class.school_year';
return 'enrollments.id = (
SELECT e_latest.id
FROM enrollments e_latest
WHERE e_latest.student_id = student_class.student_id
AND ' . $yearCondition . '
ORDER BY e_latest.updated_at DESC, e_latest.enrollment_date DESC, e_latest.id DESC
LIMIT 1
)';
}
/**
* Create a fresh builder scoped to active students.
*/
@@ -174,10 +191,9 @@ class StudentClassModel extends Model
)
->join(
'enrollments',
$schoolYear !== ''
? 'enrollments.student_id = student_class.student_id AND enrollments.school_year = ' . $this->db->escape($schoolYear)
: 'enrollments.student_id = student_class.student_id',
'left'
$this->latestEnrollmentJoinCondition($schoolYear),
'left',
false
)
->where(
'student_class.class_section_id',
@@ -249,8 +265,9 @@ class StudentClassModel extends Model
)
->join(
'enrollments',
'enrollments.student_id = student_class.student_id AND enrollments.school_year = ' . $this->db->escape($schoolYear),
'left'
$this->latestEnrollmentJoinCondition($schoolYear),
'left',
false
);
}
@@ -436,7 +453,7 @@ class StudentClassModel extends Model
'students.lastname',
'students.school_id',
'students.is_active',
'students.is_new',
'COALESCE(sys.is_new, 1) AS is_new',
'students.photo_consent',
'students.age',
'student_class.school_year',
@@ -451,12 +468,18 @@ class StudentClassModel extends Model
'inner'
)
->join(
'enrollments',
'student_year_status sys',
$schoolYear !== ''
? 'enrollments.student_id = student_class.student_id AND enrollments.school_year = ' . $this->db->escape($schoolYear)
: 'enrollments.student_id = student_class.student_id',
? 'sys.student_id = students.id AND sys.school_year = ' . $this->db->escape($schoolYear)
: 'sys.student_id = students.id AND sys.school_year = student_class.school_year',
'left'
)
->join(
'enrollments',
$this->latestEnrollmentJoinCondition($schoolYear),
'left',
false
)
->whereIn(
'student_class.class_section_id',
$classSectionIds
@@ -580,8 +603,9 @@ class StudentClassModel extends Model
)
->join(
'enrollments',
'enrollments.student_id = student_class.student_id AND enrollments.school_year = ' . $this->db->escape($schoolYear),
'left'
$this->latestEnrollmentJoinCondition($schoolYear),
'left',
false
);
} else {
$schoolYear = '';
+139 -39
View File
@@ -17,7 +17,6 @@ class StudentModel extends Model
'gender',
'registration_grade',
'photo_consent',
'is_new',
'parent_id',
'registration_date',
'tuition_paid',
@@ -27,24 +26,38 @@ class StudentModel extends Model
];
protected $useTimestamps = false;
// File: app/Models/StudentModel.php
/**
* Get all students marked as new (is_new = 1).
*
* @return array
* Get students marked as new for a school year.
*/
public function getNewStudents(): array
public function getNewStudents(string $schoolYear): array
{
return $this->where('is_new', 1)
->orderBy('lastname', 'ASC')
->orderBy('firstname', 'ASC')
$schoolYear = trim($schoolYear);
if (! preg_match('/^\d{4}-\d{4}$/', $schoolYear) || ! $this->db->tableExists('student_year_status')) {
return [];
}
return $this->select('students.*, COALESCE(sys.is_new, 1) AS is_new')
->join(
'student_year_status sys',
'sys.student_id = students.id AND sys.school_year = ' . $this->db->escape($schoolYear),
'inner'
)
->where('COALESCE(sys.is_new, 1)', 1, false)
->orderBy('students.lastname', 'ASC')
->orderBy('students.firstname', 'ASC')
->findAll();
}
public function getNewStudentsWithParents(): array
public function getNewStudentsWithParents(string $schoolYear): array
{
$schoolYear = trim($schoolYear);
if (! preg_match('/^\d{4}-\d{4}$/', $schoolYear) || ! $this->db->tableExists('student_year_status')) {
return [];
}
return $this->select([
'students.*',
'COALESCE(sys.is_new, 1) AS is_new',
'u.id AS parent_id',
'u.firstname AS parent_firstname',
'u.lastname AS parent_lastname',
@@ -52,55 +65,91 @@ class StudentModel extends Model
'u.cellphone AS parent_phone',
])
->join('users u', 'u.id = students.parent_id', 'left')
->where('students.is_new', 1)
->join(
'student_year_status sys',
'sys.student_id = students.id AND sys.school_year = ' . $this->db->escape($schoolYear),
'inner'
)
->where('COALESCE(sys.is_new, 1)', 1, false)
->orderBy('students.lastname', 'ASC')
->orderBy('students.firstname', 'ASC')
->findAll();
}
/**
* Fetch only new students (is_new=1) with parent info and a single emergency contact.
* - Parent fields: parent_firstname, parent_lastname, parent_email, parent_phone
* - Emergency fields: emergency_name, emergency_relationship, emergency_phone
* Fetch only new students for a school year with parent info and a single emergency contact.
*/
public function getNewStudentsWithParentsAndEmergency(): array
public function getNewStudentsWithParentsAndEmergency(string $schoolYear): array
{
$schoolYear = trim($schoolYear);
if (! preg_match('/^\d{4}-\d{4}$/', $schoolYear) || ! $this->db->tableExists('student_year_status')) {
return [];
}
return $this->select([
'students.*',
'COALESCE(sys.is_new, 1) AS is_new',
'u.firstname AS parent_firstname',
'u.lastname AS parent_lastname',
'u.email AS parent_email',
'u.cellphone AS parent_phone',
// Pick one emergency contact (MIN() works fine if you group by student)
'MIN(ec.emergency_contact_name) AS emergency_name',
'MIN(ec.relation) AS emergency_relationship',
'MIN(ec.cellphone) AS emergency_phone',
])
->join('users u', 'u.id = students.parent_id', 'left')
->join('emergency_contacts ec', 'ec.parent_id = students.parent_id', 'left')
->where('students.is_new', 1) // strictly 1; change to 'Yes' if your data uses Yes/No
->join(
'student_year_status sys',
'sys.student_id = students.id AND sys.school_year = ' . $this->db->escape($schoolYear),
'inner'
)
->where('COALESCE(sys.is_new, 1)', 1, false)
->groupBy('students.id')
->orderBy('students.lastname', 'ASC')
->orderBy('students.firstname', 'ASC')
->findAll();
}
/**
* Fetch students with parent + one emergency contact.
* @param null|int $isNew Pass 1 for only new, 0 for only not-new, null for both.
*/
/**
* Fetch students with parent + one emergency contact.
*
* @param null|int $isNew 1 = only new, 0 = only not-new, null = both
* @param null|string $schoolYear e.g. "2025-2026"; pass null or "all" for no filter
* @param null|int $isNew 1 = only new, 0 = only not-new, null for both
* @param null|string $schoolYear e.g. "2025-2026"; pass null or "all" for no year filter
* @return array
*/
public function getStudentsWithParentsAndEmergency(?string $schoolYear = null, ?int $isNew = null): array
{
$builder = $this->select([
'students.*',
$filterByYear = $schoolYear !== null && strtolower((string) $schoolYear) !== 'all';
$statusYear = $filterByYear ? trim((string) $schoolYear) : '';
if ($statusYear === '' && ($isNew === 0 || $isNew === 1)) {
$statusYear = (string) (service('studentYearStatus')->activeSchoolYear() ?? '');
}
$useYearScopedIsNew = $statusYear !== ''
&& preg_match('/^\d{4}-\d{4}$/', $statusYear)
&& $this->db->tableExists('student_year_status');
$schoolYearSelect = $statusYear !== ''
? $this->db->escape($statusYear) . ' AS school_year'
: 'NULL AS school_year';
$select = [
'students.id',
'students.school_id',
'students.firstname',
'students.lastname',
'students.dob',
'students.age',
'students.gender',
'students.registration_grade',
'students.photo_consent',
'students.parent_id',
'students.registration_date',
'students.tuition_paid',
'students.year_of_registration',
$schoolYearSelect,
'students.rfid_tag',
'NULL AS semester',
'students.is_active',
'u.firstname AS parent_firstname',
'u.lastname AS parent_lastname',
'u.email AS parent_email',
@@ -108,30 +157,47 @@ class StudentModel extends Model
'MIN(ec.emergency_contact_name) AS emergency_name',
'MIN(ec.relation) AS emergency_relationship',
'MIN(ec.cellphone) AS emergency_phone',
])
];
if ($useYearScopedIsNew) {
$select[] = 'COALESCE(sys.is_new, 1) AS is_new';
}
$builder = $this->select($select)
->join('users u', 'u.id = students.parent_id', 'left')
->join('emergency_contacts ec', 'ec.parent_id = students.parent_id', 'left');
// is_new filter
if ($isNew === 0 || $isNew === 1) {
$builder->where('students.is_new', $isNew);
} else {
$builder->whereIn('students.is_new', [0, 1]); // both
if ($useYearScopedIsNew) {
$builder->join(
'student_year_status sys',
'sys.student_id = students.id AND sys.school_year = ' . $this->db->escape($statusYear),
'left'
);
}
// school_year filter (skip if null or "all")
if ($schoolYear !== null && strtolower($schoolYear) !== 'all') {
if ($isNew === 0 || $isNew === 1) {
if (! $useYearScopedIsNew) {
return [];
}
$builder->where('COALESCE(sys.is_new, 1)', $isNew, false);
}
if ($filterByYear) {
$builder
->join('student_class sc_filter', 'sc_filter.student_id = students.id', 'inner')
->where('sc_filter.school_year', $schoolYear);
}
return $builder
$builder = $builder
->groupBy('students.id')
->orderBy('students.is_new', 'DESC') // new first
->orderBy('students.lastname', 'ASC')
->orderBy('students.firstname', 'ASC')
->findAll();
->orderBy('students.firstname', 'ASC');
if ($useYearScopedIsNew) {
$builder->orderBy('COALESCE(sys.is_new, 1)', 'DESC', false);
}
return $builder->findAll();
}
@@ -407,6 +473,15 @@ class StudentModel extends Model
$studentClassJoin = 'student_class.student_id = students.id';
$enrollmentJoin = 'enrollments.student_id = students.id';
$selectedYearFilter = '';
$useYearScopedIsNew = $schoolYear !== ''
&& preg_match('/^\d{4}-\d{4}$/', $schoolYear)
&& $this->db->tableExists('student_year_status');
$schoolYearSelect = $schoolYear !== ''
? $this->db->escape($schoolYear) . ' AS school_year'
: 'NULL AS school_year';
$isNewSelect = $useYearScopedIsNew
? 'COALESCE(sys.is_new, 1) AS is_new'
: '1 AS is_new';
if ($schoolYear !== '') {
$escapedSchoolYear = $this->db->escape($schoolYear);
@@ -433,7 +508,24 @@ class StudentModel extends Model
}
$builder = $this->select('
students.*,
students.id,
students.school_id,
students.firstname,
students.lastname,
students.dob,
students.age,
students.gender,
students.registration_grade,
students.photo_consent,
students.parent_id,
students.registration_date,
students.tuition_paid,
students.year_of_registration,
' . $schoolYearSelect . ',
students.rfid_tag,
NULL AS semester,
students.is_active,
' . $isNewSelect . ',
student_class.class_section_id,
enrollments.enrollment_status,
enrollments.admission_status,
@@ -444,6 +536,14 @@ class StudentModel extends Model
->join('enrollments', $enrollmentJoin, 'left')
->join('users u', 'u.id = students.parent_id', 'left');
if ($useYearScopedIsNew) {
$builder->join(
'student_year_status sys',
'sys.student_id = students.id AND sys.school_year = ' . $this->db->escape($schoolYear),
'left'
);
}
if ($selectedYearFilter !== '') {
$builder->where($selectedYearFilter, null, false);
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use App\Models\Concerns\SchoolYearAutoFillTrait;
use CodeIgniter\Model;
class StudentYearStatusModel extends Model
{
use SchoolYearAutoFillTrait;
protected $table = 'student_year_status';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useAutoIncrement = true;
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $allowedFields = [
'student_id',
'school_year',
'is_new',
];
protected $validationRules = [
'student_id' => 'required|is_natural_no_zero',
'school_year' => 'required|regex_match[/^\d{4}-\d{4}$/]',
'is_new' => 'required|in_list[0,1]',
];
}
@@ -110,6 +110,10 @@ public function studentProfiles(string $selectedYear): array
->getResultArray();
}
if ($selectedYear !== '') {
service('studentYearStatus')->attachToStudents($students, $selectedYear);
}
$enrollmentStatusByStudentId = [];
$studentIds = array_values(array_unique(array_filter(array_map(
static fn (array $row): int => (int) ($row['id'] ?? 0),
+5 -8
View File
@@ -58,7 +58,7 @@ public function buildRoster(string $selectedYear, string $semester): array
$students = $this->studentModel->getStudentsWithClassAndEnrollment($selectedYear);
$removedPriorStatuses = $this->removedPriorYearStudentStatuses($selectedYear);
$returningStudentIds = $this->priorYearStudentIds($selectedYear);
service('studentYearStatus')->attachToStudents($students, $selectedYear);
foreach ($students as &$s) {
// ===== Ensure IDs needed by the modal =====
@@ -93,11 +93,8 @@ public function buildRoster(string $selectedYear, string $semester): array
$s['parent_sort'] = 'ZZZ Unknown Parent';
}
// ===== New-student flags =====
$s['is_new'] = (int) ($s['is_new'] ?? 0);
if (isset($returningStudentIds[$s['student_id']])) {
$s['is_new'] = 0;
}
// ===== New-student flags (year-scoped) =====
$s['is_new'] = (int) ($s['is_new'] ?? 1) === 1 ? 1 : 0;
$s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';
// ===== Admission override =====
@@ -158,7 +155,7 @@ public function buildRoster(string $selectedYear, string $semester): array
*/
public function newStudents(string $schoolYear): array
{
$rows = $this->studentModel->getStudentsWithParentsAndEmergency($schoolYear);
$rows = $this->studentModel->getStudentsWithParentsAndEmergency($schoolYear, 1);
$newStudents = [];
foreach ($rows as $r) {
@@ -170,7 +167,7 @@ public function newStudents(string $schoolYear): array
? $classSection
: 'Class not Assigned';
$r['is_new'] = (int) $r['is_new'];
$r['is_new'] = (int) ($r['is_new'] ?? 1) === 1 ? 1 : 0;
$r['new_student'] = $r['is_new'] === 1 ? "Yes" : "No";
$r['modalIdContact'] = 'contact_' . (int)($r['id'] ?? 0);
$r['enrollment_status'] = $enrollmentstatus;
+39 -20
View File
@@ -603,14 +603,6 @@ final class SchoolYearClosingService
->where('sc.school_year', $schoolYear)
->where('sc.class_section_id IS NOT NULL', null, false);
if ($hasEnrollments && ($hasEnrollmentStatus || $hasEnrollmentWithdrawn)) {
$builder->join(
'enrollments e',
'e.student_id = sc.student_id AND e.school_year = ' . $this->db->escape($schoolYear),
'left'
);
}
if ($hasDob) {
$builder->select('s.dob');
}
@@ -630,18 +622,12 @@ final class SchoolYearClosingService
$builder->where('s.is_active', 1);
}
if ($hasEnrollmentStatus) {
$inactiveStatuses = implode(',', array_map([$this->db, 'escape'], EnrollmentStatusService::INACTIVE_STATUSES));
$builder->where(
'(e.enrollment_status IS NULL OR LOWER(TRIM(e.enrollment_status)) NOT IN (' . $inactiveStatuses . '))',
null,
false
);
}
if ($hasEnrollmentWithdrawn) {
$builder->where('(e.is_withdrawn IS NULL OR e.is_withdrawn != 1)', null, false);
}
$this->excludeWithdrawnStudentsFromClosingBlockers(
$builder,
$schoolYear,
$hasEnrollmentStatus,
$hasEnrollmentWithdrawn
);
$assignmentRows = $builder
->orderBy('sc.student_id', 'ASC')
@@ -783,6 +769,39 @@ final class SchoolYearClosingService
];
}
private function excludeWithdrawnStudentsFromClosingBlockers(
$builder,
string $schoolYear,
bool $hasEnrollmentStatus,
bool $hasEnrollmentWithdrawn
): void {
if (! $hasEnrollmentStatus && ! $hasEnrollmentWithdrawn) {
return;
}
$conditions = [];
if ($hasEnrollmentStatus) {
$inactiveStatuses = implode(',', array_map([$this->db, 'escape'], EnrollmentStatusService::INACTIVE_STATUSES));
$conditions[] = 'LOWER(TRIM(e.enrollment_status)) IN (' . $inactiveStatuses . ')';
}
if ($hasEnrollmentWithdrawn) {
$conditions[] = 'e.is_withdrawn = 1';
}
$builder->where(
'NOT EXISTS (
SELECT 1
FROM enrollments e
WHERE e.student_id = sc.student_id
AND e.school_year = ' . $this->db->escape($schoolYear) . '
AND (' . implode(' OR ', $conditions) . ')
)',
null,
false
);
}
private function isKgStudent(array $student): bool
{
foreach (['class_name', 'class_section_name'] as $field) {
+178
View File
@@ -0,0 +1,178 @@
<?php
namespace App\Services;
use App\Models\StudentYearStatusModel;
use CodeIgniter\Database\BaseConnection;
class StudentYearStatusService
{
public function __construct(
private BaseConnection $db,
private StudentYearStatusModel $yearStatusModel,
) {
}
public function isNew(int $studentId, string $schoolYear): bool
{
if ($studentId <= 0) {
return true;
}
$schoolYear = trim($schoolYear);
if (! preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
return true;
}
if (! $this->db->tableExists('student_year_status')) {
return true;
}
$row = $this->yearStatusModel
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->first();
if (! is_array($row)) {
return true;
}
return (int) ($row['is_new'] ?? 1) === 1;
}
public function upsert(int $studentId, string $schoolYear, bool $isNew): void
{
if ($studentId <= 0) {
return;
}
$schoolYear = trim($schoolYear);
if (! preg_match('/^\d{4}-\d{4}$/', $schoolYear) || ! $this->db->tableExists('student_year_status')) {
return;
}
$flag = $isNew ? 1 : 0;
$existing = $this->yearStatusModel
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->first();
if (is_array($existing) && isset($existing['id'])) {
$this->yearStatusModel->update((int) $existing['id'], [
'is_new' => $flag,
]);
return;
}
$this->yearStatusModel->insert([
'student_id' => $studentId,
'school_year' => $schoolYear,
'is_new' => $flag,
]);
}
/**
* @param list<array<string, mixed>> $students
*/
public function attachToStudents(array &$students, string $schoolYear): void
{
$schoolYear = trim($schoolYear);
if ($students === [] || ! preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
return;
}
$ids = [];
foreach ($students as $student) {
$id = (int) ($student['id'] ?? $student['student_id'] ?? 0);
if ($id > 0) {
$ids[$id] = true;
}
}
$flags = $this->flagsForStudents(array_keys($ids), $schoolYear);
foreach ($students as &$student) {
$id = (int) ($student['id'] ?? $student['student_id'] ?? 0);
if ($id <= 0) {
continue;
}
$student['is_new'] = $flags[$id] ?? 1;
}
unset($student);
}
/**
* @param list<int> $studentIds
* @return array<int, int>
*/
public function flagsForStudents(array $studentIds, string $schoolYear): array
{
$studentIds = array_values(array_unique(array_filter(
array_map('intval', $studentIds),
static fn(int $id): bool => $id > 0
)));
$schoolYear = trim($schoolYear);
if ($studentIds === [] || ! preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
return [];
}
$flags = [];
foreach ($studentIds as $studentId) {
$flags[$studentId] = 1;
}
if (! $this->db->tableExists('student_year_status')) {
return $flags;
}
$rows = $this->yearStatusModel
->select('student_id, is_new')
->whereIn('student_id', $studentIds)
->where('school_year', $schoolYear)
->findAll();
foreach ($rows as $row) {
$id = (int) ($row['student_id'] ?? 0);
if ($id > 0) {
$flags[$id] = (int) ($row['is_new'] ?? 1) === 1 ? 1 : 0;
}
}
return $flags;
}
public function activeSchoolYear(): ?string
{
if ($this->db->tableExists('school_years')) {
$row = $this->db->table('school_years')
->select('name')
->where('status', 'active')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$name = trim((string) ($row['name'] ?? ''));
if (preg_match('/^\d{4}-\d{4}$/', $name)) {
return $name;
}
}
if (! $this->db->tableExists('configuration')) {
return null;
}
$row = $this->db->table('configuration')
->select('config_value')
->where('config_key', 'school_year')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$name = trim((string) ($row['config_value'] ?? ''));
return preg_match('/^\d{4}-\d{4}$/', $name) ? $name : null;
}
}
@@ -59,6 +59,7 @@ final class SchoolYearTableRegistry
'staff_attendance',
'student_class',
'student_decisions',
'student_year_status',
'teacher_attendance_data',
'teacher_class',
'teacher_submission_notification_history',
@@ -38,9 +38,9 @@
<th>Registration Date</th>
<th>Parent/Guardian</th>
<th>Student Name</th>
<th>School ID</th>
<th>Age</th>
<th>New Student</th>
<th>Removed (Prior Years)</th>
<th>Current Class</th>
<th>Actual Status</th>
<th>Update Enrollment Status</th>
@@ -79,6 +79,11 @@
<?php endif; ?>
</td>
<!-- School ID -->
<td>
<?= esc($student['school_id'] ?? '-') ?>
</td>
<!-- Age -->
<td class="text-center">
<?= isset($student['age']) && $student['age'] !== '' && $student['age'] !== null ? esc((string)(int)$student['age']) : '-' ?>
@@ -95,15 +100,6 @@
<?php endif; ?>
</td>
<!-- Removed Previous Year -->
<td>
<?php if (($student['removed_previous_year'] ?? 'No') === 'Yes'): ?>
<span class="badge bg-danger text-light">Yes</span>
<?php else: ?>
<span class="badge bg-secondary">No</span>
<?php endif; ?>
</td>
<!-- Class -->
<td><?= esc($student['class_section'] ?? 'Class not Assigned') ?></td>
+348 -41
View File
@@ -11,7 +11,7 @@
.enrollment-stepper {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: .5rem;
}
@@ -19,6 +19,8 @@
border-bottom: 3px solid #dee2e6;
color: #6c757d;
font-size: .85rem;
line-height: 1.2;
overflow-wrap: anywhere;
padding: .35rem 0;
text-align: center;
}
@@ -92,6 +94,38 @@
gap: .5rem;
}
.enrollment-summary-card {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
}
.enrollment-summary-lines {
display: grid;
gap: .35rem;
}
.enrollment-summary-line {
align-items: baseline;
display: flex;
gap: .5rem;
justify-content: space-between;
}
.enrollment-summary-label {
color: #6c757d;
flex: 0 0 auto;
font-size: .875rem;
}
.enrollment-summary-value {
font-size: .875rem;
font-weight: 600;
min-width: 0;
overflow-wrap: anywhere;
text-align: right;
}
@media (max-width: 575.98px) {
.enrollment-stepper {
gap: .25rem;
@@ -177,6 +211,16 @@
.enrollment-status-table td[data-label="Withdraw"] .enrollment-withdraw-control {
margin-left: auto;
}
.enrollment-summary-line {
align-items: flex-start;
display: block;
}
.enrollment-summary-value {
display: block;
text-align: left;
}
}
</style>
<?= $this->endSection() ?>
@@ -231,15 +275,21 @@ foreach (($students ?? []) as $student) {
<?php endif; ?>
</div>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php
$flashError = session()->getFlashdata('error');
$flashSuccess = session()->getFlashdata('success');
?>
<?php if ($flashError): ?>
<div class="alert alert-danger"><?= esc($flashError) ?></div>
<?php endif; ?>
<?php if ($flashSuccess): ?>
<div class="alert alert-success"><?= esc($flashSuccess) ?></div>
<?php endif; ?>
<div class="alert alert-info mb-3">
<p>Enrollment is the process of officially signing up your child for the upcoming school year.</p>
<ul class="mb-0">
<li>Last Day for Enrollment is <strong><?= esc(local_date($lastDayOfRegistration, 'm-d-Y')) ?></strong>.</li>
<li>After submit, enrollment status changes to <strong>admission under review</strong>.</li>
<li>Payments are processed on the first day of school: <strong><?= esc(local_date($schoolStartDate, 'm-d-Y')) ?></strong>.</li>
</ul>
</div>
@@ -254,7 +304,7 @@ foreach (($students ?? []) as $student) {
<div class="col-lg">
<div class="fw-semibold">Enrollment process</div>
<div class="text-muted small">
Review student information, acknowledge policies, review tuition and balances, then submit enrollment.
Review student information, confirm your home address and phone, acknowledge policies, review tuition and balances, then submit enrollment.
</div>
</div>
<div class="col-lg-auto">
@@ -338,9 +388,10 @@ foreach (($students ?? []) as $student) {
<div class="modal-body">
<div class="enrollment-stepper mb-3" aria-label="Enrollment steps">
<div class="enrollment-step is-active" data-step-indicator="0">Student Info</div>
<div class="enrollment-step" data-step-indicator="1">Policies</div>
<div class="enrollment-step" data-step-indicator="2">Tuition</div>
<div class="enrollment-step" data-step-indicator="3">Submit</div>
<div class="enrollment-step" data-step-indicator="1">Contact</div>
<div class="enrollment-step" data-step-indicator="2">Policies</div>
<div class="enrollment-step" data-step-indicator="3">Tuition</div>
<div class="enrollment-step" data-step-indicator="4">Submit</div>
</div>
<div data-step-panel="0">
@@ -450,6 +501,7 @@ foreach (($students ?? []) as $student) {
data-current-grade="<?= esc($gradeLabel) ?>"
data-required-action="<?= esc($student['required_action_label'] ?? 'Contact administration') ?>"
data-expected-placement="<?= esc($student['expected_placement_label'] ?? $gradeLabel) ?>"
data-is-new="<?= (string) ($student['is_new'] ?? '1') === '1' ? '1' : '0' ?>"
data-selectable="<?= $canEnroll ? '1' : '0' ?>">
<div class="d-flex gap-3 align-items-start">
<span class="student-select-icon" aria-hidden="true"><i class="bi bi-check2"></i></span>
@@ -463,10 +515,13 @@ foreach (($students ?? []) as $student) {
<div>School ID: <?= esc($student['school_id'] ?? 'N/A') ?></div>
<div>Date of birth: <?= esc($dobDisplay) ?><?php if (isset($student['age'])): ?> &middot; Age: <?= esc($student['age']) ?><?php endif; ?></div>
<div>Gender: <?= esc($student['gender'] ?? 'N/A') ?></div>
<div>Registration grade: <?= esc($student['registration_grade'] ?? 'N/A') ?></div>
<div>Current grade: <?= esc($gradeLabel) ?></div>
<div>Expected placement: <?= esc($student['expected_placement_label'] ?? $gradeLabel) ?></div>
<div>Required action: <?= esc($student['required_action_label'] ?? 'Contact administration') ?></div>
<?php if ((string) ($student['is_new'] ?? '1') === '1'): ?>
<div>Registration grade: <?= esc($student['registration_grade'] ?? 'N/A') ?></div>
<?php endif; ?>
<?php if ((string) ($student['is_new'] ?? '0') === '0'): ?>
<div>Expected placement: <?= esc($student['expected_placement_label'] ?? $gradeLabel) ?></div>
<?php endif; ?>
</div>
<?php if ($canEnroll): ?>
@@ -478,7 +533,7 @@ foreach (($students ?? []) as $student) {
<div class="row g-2 mt-3" data-student-edit-fields>
<div class="col-12">
<div class="fw-semibold small mb-1">Editable health information</div>
<div class="fw-semibold small mb-1">Editable information</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="student-photo-consent-<?= esc($student['id']) ?>">Photo consent</label>
@@ -533,6 +588,95 @@ foreach (($students ?? []) as $student) {
</div>
<div class="d-none" data-step-panel="1">
<?php
$parentContact = is_array($parentContact ?? null) ? $parentContact : [];
$parentContactStates = [
'CT' => 'Connecticut',
'ME' => 'Maine',
'MA' => 'Massachusetts',
'NH' => 'New Hampshire',
'NY' => 'New York',
'RI' => 'Rhode Island',
'VT' => 'Vermont',
];
$currentParentState = strtoupper(trim((string) ($parentContact['state'] ?? '')));
if ($currentParentState !== '' && ! isset($parentContactStates[$currentParentState])) {
$parentContactStates[$currentParentState] = $currentParentState;
}
$parentDisplayName = trim((string) ($parentContact['firstname'] ?? '') . ' ' . (string) ($parentContact['lastname'] ?? ''));
?>
<h6 class="fw-semibold">Confirm home address and phone</h6>
<div class="text-muted small mb-3">
Please review and update your contact information before continuing enrollment.
<?php if ($parentDisplayName !== ''): ?>
This account is for <?= esc($parentDisplayName) ?>.
<?php endif; ?>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-semibold" for="parent-contact-cellphone">Phone</label>
<input type="tel"
class="form-control"
id="parent-contact-cellphone"
name="parent_contact[cellphone]"
value="<?= esc($parentContact['cellphone'] ?? '') ?>"
maxlength="12"
inputmode="numeric"
required>
</div>
<div class="col-md-8">
<label class="form-label small fw-semibold" for="parent-contact-street">Home street address</label>
<input type="text"
class="form-control"
id="parent-contact-street"
name="parent_contact[address_street]"
value="<?= esc($parentContact['address_street'] ?? '') ?>"
maxlength="255"
required>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="parent-contact-apt">Apt / Unit</label>
<input type="text"
class="form-control"
id="parent-contact-apt"
name="parent_contact[apt]"
value="<?= esc($parentContact['apt'] ?? '') ?>"
maxlength="15">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="parent-contact-city">City</label>
<input type="text"
class="form-control"
id="parent-contact-city"
name="parent_contact[city]"
value="<?= esc($parentContact['city'] ?? '') ?>"
maxlength="100"
required>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="parent-contact-zip">ZIP code</label>
<input type="text"
class="form-control"
id="parent-contact-zip"
name="parent_contact[zip]"
value="<?= esc($parentContact['zip'] ?? '') ?>"
maxlength="5"
inputmode="numeric"
required>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="parent-contact-state">State</label>
<select class="form-select" id="parent-contact-state" name="parent_contact[state]" required>
<option value="">Select</option>
<?php foreach ($parentContactStates as $abbr => $stateName): ?>
<option value="<?= esc($abbr) ?>" <?= $currentParentState === $abbr ? 'selected' : '' ?>><?= esc($stateName) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="d-none" data-step-panel="2">
<h6 class="fw-semibold">Acknowledge school policies</h6>
<div class="text-muted small mb-3">Review the current school policies before submitting enrollment.</div>
<iframe src="<?= base_url('policy/school_policy') ?>" class="enrollment-policy-frame" frameborder="0" title="School Policies"></iframe>
@@ -544,7 +688,7 @@ foreach (($students ?? []) as $student) {
</div>
</div>
<div class="d-none" data-step-panel="2">
<div class="d-none" data-step-panel="3">
<h6 class="fw-semibold">Review tuition, fees and balance</h6>
<div class="text-muted small mb-3">Review the family account information before submitting enrollment.</div>
<?php if ($familyFinancialSummary !== []): ?>
@@ -559,7 +703,7 @@ foreach (($students ?? []) as $student) {
<?php endif; ?>
</div>
<div class="d-none" data-step-panel="3">
<div class="d-none" data-step-panel="4">
<h6 class="fw-semibold">Submit enrollment</h6>
<div class="text-muted small mb-3">Review the enrollment summary before submitting.</div>
@@ -568,6 +712,20 @@ foreach (($students ?? []) as $student) {
<div id="enrollmentFeeReviewList" class="d-grid gap-2"></div>
</div>
<div class="enrollment-summary-card mb-3" id="parentContactReviewCard">
<div class="fw-semibold mb-2">Parent contact</div>
<div class="enrollment-summary-lines">
<div class="enrollment-summary-line">
<span class="enrollment-summary-label">Phone:</span>
<span class="enrollment-summary-value" data-parent-review="cellphone"></span>
</div>
<div class="enrollment-summary-line">
<span class="enrollment-summary-label">Home address:</span>
<span class="enrollment-summary-value" data-parent-review="address"></span>
</div>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="border rounded p-3 h-100">
@@ -582,10 +740,7 @@ foreach (($students ?? []) as $student) {
<div class="fw-semibold mb-2">Financial information</div>
<?php if ($familyFinancialSummary !== []): ?>
<div class="small"><span class="text-muted">Carry-over balance:</span> <strong data-financial-value="carry_over_balance"><?= esc($money($familyFinancialSummary['carry_over_balance'] ?? 0)) ?></strong></div>
<div class="small"><span class="text-muted">Registration fee:</span> <strong data-financial-value="registration_fee"><?= esc($money($familyFinancialSummary['registration_fee'] ?? 0)) ?></strong></div>
<div class="small"><span class="text-muted">Tuition due now:</span> <strong data-financial-value="tuition_due_at_registration"><?= esc($money($familyFinancialSummary['tuition_due_at_registration'] ?? 0)) ?></strong></div>
<div class="small"><span class="text-muted">Mandatory fees:</span> <strong data-financial-value="mandatory_fees"><?= esc($money($familyFinancialSummary['mandatory_fees'] ?? 0)) ?></strong></div>
<div class="small"><span class="text-muted">Current-year account balance:</span> <strong data-financial-value="current_balance"><?= esc($money($familyFinancialSummary['current_balance'] ?? 0)) ?></strong></div>
<div class="small"><span class="text-muted">Total currently due:</span> <strong data-financial-value="amount_due"><?= esc($money($familyFinancialSummary['amount_due'] ?? 0)) ?></strong></div>
<?php else: ?>
<div class="small text-muted">Financial information is not available.</div>
@@ -597,7 +752,8 @@ foreach (($students ?? []) as $student) {
<div class="alert alert-warning mb-0 small">
<div class="fw-semibold mb-1">Important information</div>
<ul class="mb-0 ps-3">
<li>For newly registered students, enrollment status changes to admission under review after submitting the request.</li>
<li>Submitting sends the selected enrollment(s) for newly registered students to Admissions for review.</li>
<li>Returning students who passed last year grade are automatically granted admission.</li>
<li>Payments are processed on the first day of school.</li>
<?php if (!empty($fallMakeupExamOn)): ?>
<li>Students with a make-up exam decision may initially remain in the same grade until the make-up exam result is confirmed.</li>
@@ -656,7 +812,15 @@ foreach (($students ?? []) as $student) {
const nextButton = document.getElementById('enrollmentNextButton');
const submitButton = document.getElementById('enrollmentSubmitButton');
const feeReviewList = document.getElementById('enrollmentFeeReviewList');
const finalStep = 3;
const parentPhoneInput = document.getElementById('parent-contact-cellphone');
const parentStreetInput = document.getElementById('parent-contact-street');
const parentAptInput = document.getElementById('parent-contact-apt');
const parentCityInput = document.getElementById('parent-contact-city');
const parentStateInput = document.getElementById('parent-contact-state');
const parentZipInput = document.getElementById('parent-contact-zip');
const finalStep = 4;
const enrollmentStartStep = <?= (int) ($enrollmentStartStep ?? 0) ?>;
const preselectedStudentIds = <?= json_encode(array_values(array_map('intval', $preselectedStudentIds ?? [])), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
const feeSchedule = <?= json_encode([
'currency' => (string) ($enrollmentFeeSchedule['currency'] ?? '$'),
'firstStudentFee' => (float) ($enrollmentFeeSchedule['first_student_fee'] ?? 0),
@@ -690,7 +854,7 @@ foreach (($students ?? []) as $student) {
backButton.classList.toggle('d-none', currentStep === 0);
nextButton.classList.toggle('d-none', currentStep === finalStep);
submitButton.classList.toggle('d-none', currentStep !== finalStep);
if (currentStep >= 2) {
if (currentStep >= 3) {
updateFinancialReview();
}
if (currentStep === finalStep) {
@@ -736,20 +900,19 @@ foreach (($students ?? []) as $student) {
const lastName = card ? card.querySelector("input[name$='[lastname]']")?.value?.trim() : '';
const dob = card ? card.querySelector("input[name$='[dob]']")?.value?.trim() : '';
const grade = card ? card.querySelector("input[name$='[registration_grade]']")?.value?.trim() : '';
const gender = card ? card.querySelector("select[name$='[gender]']")?.value?.trim() : '';
const gender = card ? card.querySelector("input[name$='[gender]']")?.value?.trim() : '';
const schoolId = card?.dataset.schoolId || 'N/A';
const currentGrade = card?.dataset.currentGrade || '';
const expectedPlacement = card?.dataset.expectedPlacement || '';
const requiredAction = card?.dataset.requiredAction || '';
const isNewStudent = card?.dataset.isNew === '1';
const name = (firstName + ' ' + lastName).trim() || card?.querySelector('.fw-semibold')?.textContent?.trim() || 'Student';
const metaParts = [];
if (grade) metaParts.push('Registration grade: ' + grade);
if (dob) metaParts.push('DOB: ' + dob);
if (gender) metaParts.push('Gender: ' + gender);
if (schoolId) metaParts.push('School ID: ' + schoolId);
return {
name,
meta: metaParts.join(' - '),
schoolId,
dob,
gender,
registrationGrade: isNewStudent ? grade : '',
currentGrade,
expectedPlacement,
requiredAction,
@@ -762,26 +925,35 @@ foreach (($students ?? []) as $student) {
if (!cards.length) {
feeReviewList.innerHTML = '<div class="alert alert-warning mb-0">No students selected.</div>';
renderParentContactReview();
return;
}
feeReviewList.innerHTML = cards.map((student, index) => {
const tuitionFee = index === 0 ? feeSchedule.firstStudentFee : feeSchedule.secondStudentFee;
const tier = index === 0 ? 'First student tuition tier' : 'Additional student tuition tier';
return '<div class="border rounded p-3">' +
'<div class="d-flex flex-wrap justify-content-between gap-2">' +
'<div><div class="fw-semibold">' + escapeHtml(student.name) + '</div>' +
'<div class="small text-muted">' + escapeHtml(student.meta) + '</div></div>' +
'<div class="text-md-end"><div class="fw-semibold">' + escapeHtml(formatMoney(tuitionFee)) + '</div>' +
'<div class="small text-muted">' + escapeHtml(tier) + '</div></div>' +
'</div>' +
'<div class="row g-2 small mt-2">' +
'<div class="col-md-4"><span class="text-muted">Current grade:</span> ' + escapeHtml(student.currentGrade || 'N/A') + '</div>' +
'<div class="col-md-4"><span class="text-muted">Expected placement:</span> ' + escapeHtml(student.expectedPlacement || 'Pending') + '</div>' +
'<div class="col-md-4"><span class="text-muted">Required action:</span> ' + escapeHtml(student.requiredAction || 'Contact administration') + '</div>' +
const lines = [
['Student', student.name],
['School ID', student.schoolId || 'N/A'],
['Date of birth', student.dob || 'N/A'],
['Gender', student.gender || 'N/A'],
['Tuition', formatMoney(tuitionFee)],
['Current grade', student.currentGrade || 'N/A'],
['Expected placement', student.expectedPlacement || 'Pending'],
];
return '<div class="enrollment-summary-card">' +
'<div class="enrollment-summary-lines">' +
lines.map(([label, value]) => {
return '<div class="enrollment-summary-line">' +
'<span class="enrollment-summary-label">' + escapeHtml(label) + ':</span>' +
'<span class="enrollment-summary-value">' + escapeHtml(value) + '</span>' +
'</div>';
}).join('') +
'</div>' +
'</div>';
}).join('');
renderParentContactReview();
}
function escapeHtml(value) {
@@ -795,6 +967,108 @@ foreach (($students ?? []) as $student) {
return String(feeSchedule.currency || '$') + numericAmount.toFixed(2);
}
function digitsOnly(value) {
return String(value || '').replace(/\D/g, '');
}
function formatPhoneDisplay(value) {
const digits = digitsOnly(value).substring(0, 10);
if (digits.length > 6) {
return digits.substring(0, 3) + '-' + digits.substring(3, 6) + '-' + digits.substring(6);
}
if (digits.length > 3) {
return digits.substring(0, 3) + '-' + digits.substring(3);
}
return digits;
}
function parentContactAddress() {
const street = parentStreetInput?.value?.trim() || '';
const apt = parentAptInput?.value?.trim() || '';
const city = parentCityInput?.value?.trim() || '';
const state = parentStateInput?.value?.trim() || '';
const zip = parentZipInput?.value?.trim() || '';
const line = [street, apt].filter(Boolean).join(', ');
const cityLine = [city, state, zip].filter(Boolean).join(' ');
return [line, cityLine].filter(Boolean).join(', ') || 'N/A';
}
function renderParentContactReview() {
const phoneNode = document.querySelector('[data-parent-review="cellphone"]');
const addressNode = document.querySelector('[data-parent-review="address"]');
if (phoneNode) {
phoneNode.textContent = formatPhoneDisplay(parentPhoneInput?.value || '') || 'N/A';
}
if (addressNode) {
addressNode.textContent = parentContactAddress();
}
}
function validateParentContact() {
const phone = digitsOnly(parentPhoneInput?.value || '');
if (phone.length !== 10) {
alert('Please enter a valid 10-digit phone number.');
parentPhoneInput?.focus();
return false;
}
if (!parentStreetInput || parentStreetInput.value.trim().length < 5) {
alert('Please enter your home street address.');
parentStreetInput?.focus();
return false;
}
if (!parentCityInput || parentCityInput.value.trim().length < 2) {
alert('Please enter your city.');
parentCityInput?.focus();
return false;
}
if (!parentStateInput || parentStateInput.value.trim() === '') {
alert('Please select your state.');
parentStateInput?.focus();
return false;
}
if (!parentZipInput || digitsOnly(parentZipInput.value).length !== 5) {
alert('Please enter a valid 5-digit ZIP code.');
parentZipInput?.focus();
return false;
}
return true;
}
function selectStudentsForEnrollment(ids) {
const wanted = Array.isArray(ids) ? ids.map(Number).filter(id => id > 0) : [];
document.querySelectorAll('[data-student-card]').forEach(card => {
if (card.dataset.selectable !== '1') {
return;
}
const studentId = Number(card.dataset.studentId || 0);
if (wanted.length && !wanted.includes(studentId)) {
return;
}
const input = card.querySelector('[data-enroll-input]');
if (!input) {
return;
}
input.checked = true;
card.classList.add('is-selected');
setStudentFieldsEnabled(card, true);
});
}
function openEnrollmentAtStep(step) {
if (!flowModal || deadlinePassed) {
return false;
}
if (selectedEnrollInputs().length === 0) {
selectStudentsForEnrollment(preselectedStudentIds);
}
if (selectedEnrollInputs().length === 0) {
return false;
}
setStep(step);
flowModal.show();
return true;
}
function syncPolicyAccepted() {
hasAcceptedSchoolPolicy = !!(policyAcceptedCheckbox && policyAcceptedCheckbox.checked);
if (policyAcceptedInput) {
@@ -966,7 +1240,10 @@ foreach (($students ?? []) as $student) {
if (currentStep === 0 && !validateSelectedStudentHealth()) {
return;
}
if (currentStep === 1) {
if (currentStep === 1 && !validateParentContact()) {
return;
}
if (currentStep === 2) {
syncPolicyAccepted();
if (!hasAcceptedSchoolPolicy) {
alert('Please read and accept the school policies before continuing.');
@@ -1013,13 +1290,27 @@ foreach (($students ?? []) as $student) {
return;
}
if (anyEnroll && !hasAcceptedSchoolPolicy) {
if (anyEnroll && !validateSelectedStudentHealth()) {
e.preventDefault();
setStep(0);
if (flowModal) flowModal.show();
return;
}
if (anyEnroll && !validateParentContact()) {
e.preventDefault();
setStep(1);
if (flowModal) flowModal.show();
return;
}
if (anyEnroll && !hasAcceptedSchoolPolicy) {
e.preventDefault();
setStep(2);
if (flowModal) flowModal.show();
return;
}
if (!anyEnroll && anyWithdraw) {
const ok = confirm('Confirm withdrawal request for the selected student(s)?');
if (!ok) {
@@ -1028,6 +1319,22 @@ foreach (($students ?? []) as $student) {
}
});
}
if (parentPhoneInput) {
parentPhoneInput.addEventListener('input', function() {
this.value = formatPhoneDisplay(this.value);
});
}
if (parentZipInput) {
parentZipInput.addEventListener('input', function() {
this.value = digitsOnly(this.value).substring(0, 5);
});
}
if (enrollmentStartStep >= 2 && startButton && !startButton.disabled && !<?= $flashError ? 'true' : 'false' ?>) {
openEnrollmentAtStep(enrollmentStartStep - 1);
}
});
</script>
<?= $this->endSection() ?>
+9 -11
View File
@@ -1,15 +1,13 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="row">
<main class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<div class="alert alert-success mt-4">
<h4 class="alert-heading">Success!</h4>
<p>The student(s) has/have been successfully enrolled.</p>
<p>You will be redirected to the enroll page shortly...</p>
</div>
</main>
<?php $successMessage = session()->getFlashdata('success'); ?>
<div class="container my-4">
<div class="alert alert-success">
<h4 class="alert-heading">Success!</h4>
<p><?= esc($successMessage ?: 'The student(s) has/have been successfully enrolled.') ?></p>
<p class="mb-0">You will be redirected to the enroll page shortly...</p>
</div>
<a href="<?= base_url('/parent/enroll_classes') ?>" class="btn btn-success">Return to Enrollment</a>
</div>
<?= $this->endSection() ?>
@@ -17,6 +15,6 @@
<script>
setTimeout(function() {
window.location.href = "<?= base_url('/parent/enroll_classes'); ?>";
}, 5000); // 2500 milliseconds = 2.5 seconds
}, 5000);
</script>
<?= $this->endSection() ?>
<?= $this->endSection() ?>