From 9676261d65cb6062534d40048cc333c9dd3ffef2 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 26 Aug 2026 21:36:03 -0400 Subject: [PATCH] fix registration enrollment of students --- app/Controllers/View/ParentController.php | 52 +++++++- app/Models/StudentModel.php | 1 - .../EnrollmentRegistrationEmailService.php | 4 +- app/Services/EnrollmentTransitionService.php | 42 ++++-- app/Services/StudentYearStatusService.php | 28 +++- app/Views/parent/enroll_classes.php | 120 ++++++++++++++---- app/Views/parent/register_student.php | 14 +- 7 files changed, 208 insertions(+), 53 deletions(-) diff --git a/app/Controllers/View/ParentController.php b/app/Controllers/View/ParentController.php index b7c5df3..dc4ec1e 100644 --- a/app/Controllers/View/ParentController.php +++ b/app/Controllers/View/ParentController.php @@ -380,6 +380,7 @@ class ParentController extends BaseController } unset($student); + $this->ensureStudentYearStatusRows($students, $selectedYear); service('studentYearStatus')->attachToStudents($students, $selectedYear); foreach ($students as &$student) { $studentId = (int) ($student['id'] ?? 0); @@ -528,7 +529,7 @@ class ParentController extends BaseController } $studentName = trim((string) ($studentInfo['firstname'] ?? '') . ' ' . (string) ($studentInfo['lastname'] ?? '')) ?: 'Student ID ' . $studentId; - if (empty($evaluation['can_enroll'])) { + if (empty($evaluation['can_enroll']) && empty($evaluation['parent_enrollment_allowed'])) { $transitionService->logEnrollmentBlock($evaluation, 'parent_enroll_submit', (int) $parentId, (int) $parentId); $messages = array_values(array_filter(array_map('trim', array_map('strval', $evaluation['blockers'] ?? [])))); $codes = array_values(array_filter(array_map('strval', array_merge($evaluation['blocking_rule_codes'] ?? [], $evaluation['review_rule_codes'] ?? [])))); @@ -1951,6 +1952,7 @@ class ParentController extends BaseController if ($this->hasSettledParentEnrollmentStatus($existingStatus, $existingAdmissionStatus)) { $payload[] = [ 'student_id' => $studentId, + 'student_name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student ID ' . $studentId, 'can_enroll' => false, 'primary_block_reason' => 'ALREADY_ENROLLED', 'primary_parent_message' => EnrollmentEligibility::alreadyEnrolledMessage($existingStatus), @@ -1969,7 +1971,10 @@ class ParentController extends BaseController ); $payload[] = [ 'student_id' => $studentId, + 'student_name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student ID ' . $studentId, 'can_enroll' => (bool) ($evaluation['can_enroll'] ?? false), + 'parent_enrollment_allowed' => (bool) ($evaluation['parent_enrollment_allowed'] ?? $evaluation['can_enroll'] ?? false), + 'first_enrollment' => (bool) ($evaluation['first_enrollment'] ?? false), 'primary_block_reason' => $evaluation['primary_block_reason'] ?? null, 'primary_parent_message' => $evaluation['primary_parent_message'] ?? null, 'blocking_rule_codes' => array_values(array_map('strval', $evaluation['blocking_rule_codes'] ?? [])), @@ -2008,7 +2013,10 @@ class ParentController extends BaseController foreach ($students as $student) { $evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : []; - if (($evaluation['can_enroll'] ?? false) !== true) { + if ( + ($evaluation['can_enroll'] ?? false) !== true + && ($evaluation['parent_enrollment_allowed'] ?? false) !== true + ) { continue; } @@ -2123,7 +2131,7 @@ class ParentController extends BaseController 'parent' ); - if (($evaluation['can_enroll'] ?? false) === true) { + if (($evaluation['can_enroll'] ?? false) === true || ($evaluation['parent_enrollment_allowed'] ?? false) === true) { return; } @@ -2800,6 +2808,7 @@ class ParentController extends BaseController } unset($kid); + $this->ensureStudentYearStatusRows($kids, $selectedSchoolYear); service('studentYearStatus')->attachToStudents($kids, $selectedSchoolYear); foreach ($kids as &$kid) { @@ -2821,6 +2830,33 @@ class ParentController extends BaseController ]; } + /** + * @param list> $students + */ + private function ensureStudentYearStatusRows(array $students, string $schoolYear): void + { + $schoolYear = trim($schoolYear); + if ($students === [] || ! preg_match('/^\d{4}-\d{4}$/', $schoolYear)) { + return; + } + + $studentYearStatus = service('studentYearStatus'); + foreach ($students as $student) { + $studentId = (int) ($student['id'] ?? $student['student_id'] ?? 0); + if ($studentId <= 0 || $studentYearStatus->hasStatus($studentId, $schoolYear)) { + continue; + } + + $isNew = (int) ($student['is_new'] ?? 1) === 1; + if (! $studentYearStatus->upsert($studentId, $schoolYear, $isNew)) { + log_message('error', 'Unable to repair student_year_status for student_id={studentId}, school_year={schoolYear}', [ + 'studentId' => $studentId, + 'schoolYear' => $schoolYear, + ]); + } + } + } + private function validateAndSaveOrUpdateStudent($idx, $parentId, $semester, $schoolYear, $schoolIdService, $isNew = null, $studentId = null) { $firstName = $this->request->getPost('studentFirstName')[$idx] ?? null; @@ -2883,8 +2919,10 @@ class ParentController extends BaseController 'photo_consent' => $photoConsent, 'parent_id' => (int) $parentId, 'year_of_registration' => date('Y'), - 'school_year' => $schoolYear, ]; + if ($this->db->fieldExists('school_year', 'students')) { + $studentData['school_year'] = $schoolYear; + } if (!is_null($isNew)) { $studentData['is_new'] = $isNew ? 1 : 0; @@ -2950,7 +2988,11 @@ class ParentController extends BaseController } if (! is_null($isNew) && (int) $studentId > 0) { - service('studentYearStatus')->upsert((int) $studentId, (string) $schoolYear, (bool) $isNew); + $studentYearStatus = service('studentYearStatus'); + $statusSaved = $studentYearStatus->upsert((int) $studentId, (string) $schoolYear, (bool) $isNew); + if (! $statusSaved || ! $studentYearStatus->hasStatus((int) $studentId, (string) $schoolYear)) { + throw new \RuntimeException('Student year status could not be saved for student ID ' . (int) $studentId . ' and school year ' . (string) $schoolYear . '.'); + } } // ---------- SAVE MEDICAL CONDITIONS ---------- diff --git a/app/Models/StudentModel.php b/app/Models/StudentModel.php index 7c2ebee..fd5a70a 100644 --- a/app/Models/StudentModel.php +++ b/app/Models/StudentModel.php @@ -22,7 +22,6 @@ class StudentModel extends Model 'tuition_paid', 'year_of_registration', 'rfid_tag', - 'school_year', 'is_active' ]; protected $useTimestamps = false; diff --git a/app/Services/EnrollmentRegistrationEmailService.php b/app/Services/EnrollmentRegistrationEmailService.php index 7e33a88..bdbaca1 100644 --- a/app/Services/EnrollmentRegistrationEmailService.php +++ b/app/Services/EnrollmentRegistrationEmailService.php @@ -302,7 +302,7 @@ final class EnrollmentRegistrationEmailService return 'KG students may complete registration now. ' . $name . ' is eligible for re-enrollment even though no final deliberation decision is recorded.' . $placementText; } - if (($evaluation['can_enroll'] ?? false) === false && ! empty($evaluation['primary_parent_message'])) { + if (! $this->canCompleteParentReEnrollment($evaluation) && ! empty($evaluation['primary_parent_message'])) { return (string) $evaluation['primary_parent_message']; } @@ -655,7 +655,7 @@ final class EnrollmentRegistrationEmailService return $placement; } - return ($evaluation['can_enroll'] ?? false) === true ? 'Pending' : ''; + return $this->canCompleteParentReEnrollment($evaluation) ? 'Pending' : ''; } private function requiredAction(array $evaluation, string $deadline, string $name = 'The student'): string diff --git a/app/Services/EnrollmentTransitionService.php b/app/Services/EnrollmentTransitionService.php index 6b28193..8468484 100644 --- a/app/Services/EnrollmentTransitionService.php +++ b/app/Services/EnrollmentTransitionService.php @@ -707,7 +707,35 @@ final class EnrollmentTransitionService } } - return trim((string) ($student['school_year'] ?? '')) === $targetSchoolYear; + if ($this->db->fieldExists('school_year', 'students')) { + return trim((string) ($student['school_year'] ?? '')) === $targetSchoolYear; + } + + return ! $this->studentHasPriorSchoolHistory($studentId, $targetSchoolYear); + } + + private function studentHasPriorSchoolHistory(int $studentId, string $targetSchoolYear): bool + { + if ($studentId <= 0) { + return false; + } + + foreach (['student_class', 'enrollments', 'student_decisions'] as $table) { + if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) { + continue; + } + + $count = $this->db->table($table) + ->where('student_id', $studentId) + ->where('school_year !=', $targetSchoolYear) + ->countAllResults(); + + if ($count > 0) { + return true; + } + } + + return false; } private function schoolYearByName(string $schoolYear): ?array @@ -2009,19 +2037,7 @@ final class EnrollmentTransitionService private function applyHouseholdLastNameRule(array &$evaluation, int $parentId, ?string $sourceSchoolYear = null): void { - if (! empty($evaluation['first_enrollment'])) { - $evaluation['family_name_check_ok'] = true; - return; - } - $students = $this->linkedStudentsForParent($parentId); - $sourceSchoolYear = trim((string) $sourceSchoolYear); - if ($sourceSchoolYear !== '') { - $students = array_values(array_filter( - $students, - fn (array $student): bool => $this->sourceAssignment((int) ($student['id'] ?? 0), $sourceSchoolYear) !== null - )); - } if (count($students) <= 1) { $evaluation['family_name_check_ok'] = true; diff --git a/app/Services/StudentYearStatusService.php b/app/Services/StudentYearStatusService.php index e445df6..83e3916 100644 --- a/app/Services/StudentYearStatusService.php +++ b/app/Services/StudentYearStatusService.php @@ -40,15 +40,15 @@ class StudentYearStatusService return (int) ($row['is_new'] ?? 1) === 1; } - public function upsert(int $studentId, string $schoolYear, bool $isNew): void + public function upsert(int $studentId, string $schoolYear, bool $isNew): bool { if ($studentId <= 0) { - return; + return false; } $schoolYear = trim($schoolYear); if (! preg_match('/^\d{4}-\d{4}$/', $schoolYear) || ! $this->db->tableExists('student_year_status')) { - return; + return false; } $flag = $isNew ? 1 : 0; @@ -59,19 +59,35 @@ class StudentYearStatusService ->first(); if (is_array($existing) && isset($existing['id'])) { - $this->yearStatusModel->update((int) $existing['id'], [ + return (bool) $this->yearStatusModel->update((int) $existing['id'], [ 'is_new' => $flag, ]); - return; } - $this->yearStatusModel->insert([ + return (bool) $this->yearStatusModel->insert([ 'student_id' => $studentId, 'school_year' => $schoolYear, 'is_new' => $flag, ]); } + public function hasStatus(int $studentId, string $schoolYear): bool + { + if ($studentId <= 0) { + return false; + } + + $schoolYear = trim($schoolYear); + if (! preg_match('/^\d{4}-\d{4}$/', $schoolYear) || ! $this->db->tableExists('student_year_status')) { + return false; + } + + return $this->yearStatusModel + ->where('student_id', $studentId) + ->where('school_year', $schoolYear) + ->countAllResults() > 0; + } + /** * Mark students who existed in a source school year as returning in the target year. * diff --git a/app/Views/parent/enroll_classes.php b/app/Views/parent/enroll_classes.php index 5f4ed46..b9d3080 100644 --- a/app/Views/parent/enroll_classes.php +++ b/app/Views/parent/enroll_classes.php @@ -281,7 +281,11 @@ $enrollableCount = 0; $withdrawableCount = 0; foreach (($students ?? []) as $student) { $evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : []; - if (($evaluation['can_enroll'] ?? false) === true && !$deadlinePassed && $isEditable) { + if ( + (($evaluation['can_enroll'] ?? false) === true || ($evaluation['parent_enrollment_allowed'] ?? false) === true) + && !$deadlinePassed + && $isEditable + ) { $enrollableCount++; } if (($student['enrollment_status'] ?? '') === 'enrolled' && $isEditable) { @@ -386,7 +390,7 @@ $studentCount = count($students ?? []); $evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : []; if ($hasSettledEnrollmentStatus($student['enrollment_status'] ?? '', $student['admission_status'] ?? '')) { echo '' . esc($alreadyEnrolledMessage((string) ($student['enrollment_status'] ?? ''))) . ''; - } elseif (($evaluation['can_enroll'] ?? false) === true) { + } elseif (($evaluation['can_enroll'] ?? false) === true || ($evaluation['parent_enrollment_allowed'] ?? false) === true) { echo 'Eligible'; } elseif (! empty($evaluation['primary_parent_message'])) { echo '' . esc($evaluation['primary_parent_message']) . ''; @@ -510,13 +514,20 @@ $studentCount = count($students ?? []); $evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : []; $studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')); $studentName = $studentName !== '' ? $studentName : 'Student'; - $canEnroll = ($evaluation['can_enroll'] ?? false) === true + $studentId = (int) ($student['id'] ?? 0); + $isPreselectedStudent = in_array($studentId, array_map('intval', $preselectedStudentIds ?? []), true); + $canEnroll = ( + ($evaluation['can_enroll'] ?? false) === true + || ($evaluation['parent_enrollment_allowed'] ?? false) === true + ) && !$deadlinePassed && $isEditable; $hasSettledEnrollment = $hasSettledEnrollmentStatus($student['enrollment_status'] ?? '', $student['admission_status'] ?? ''); - $blockMessage = $hasSettledEnrollment + $blockMessage = $canEnroll + ? '' + : ($hasSettledEnrollment ? $alreadyEnrolledMessage((string) ($student['enrollment_status'] ?? '')) - : (string) ($evaluation['primary_parent_message'] ?? $eligibilityMessage['message'] ?? ''); + : (string) ($evaluation['primary_parent_message'] ?? $eligibilityMessage['message'] ?? '')); $blockTitle = $hasSettledEnrollment ? $alreadyEnrolledTitle((string) ($student['enrollment_status'] ?? '')) : 'Enrollment Not Available'; @@ -547,6 +558,8 @@ $studentCount = count($students ?? []);
form ? Array.from(form.querySelectorAll("input[name='enroll[]']:checked")) : []; const selectedWithdrawInputs = () => form ? Array.from(form.querySelectorAll("input[name='withdraw[]']:checked")) : []; + const studentCards = () => Array.from(document.querySelectorAll('[data-student-card]')); + const requestedStudentCards = () => { + const requested = studentCards().filter(card => card.dataset.requestedStudent === '1'); + return requested.length ? requested : studentCards(); + }; function setStep(step) { currentStep = Math.max(0, Math.min(finalStep, step)); @@ -1214,7 +1232,8 @@ $studentCount = count($students ?? []); function selectStudentsForEnrollment(ids) { const wanted = Array.isArray(ids) ? ids.map(Number).filter(id => id > 0) : []; - document.querySelectorAll('[data-student-card]').forEach(card => { + const cards = wanted.length ? requestedStudentCards() : studentCards(); + cards.forEach(card => { if (card.dataset.selectable !== '1') { return; } @@ -1340,7 +1359,7 @@ $studentCount = count($students ?? []); return true; } - document.querySelectorAll('[data-student-card]').forEach(card => { + studentCards().forEach(card => { card.querySelectorAll('[data-health-group]').forEach(group => { group.addEventListener('change', function(event) { const target = event.target; @@ -1372,17 +1391,41 @@ $studentCount = count($students ?? []); function showBlockModal(message, options = {}) { const alreadyEnrolled = options.alreadyEnrolled === true; + const warning = options.warning === true; if (blockModalTitle) { blockModalTitle.textContent = options.title || (alreadyEnrolled ? 'Already Enrolled' : 'Enrollment Not Available'); } if (blockModalHeader) { - blockModalHeader.classList.toggle('bg-danger', !alreadyEnrolled); - blockModalHeader.classList.toggle('bg-success', alreadyEnrolled); + blockModalHeader.classList.toggle('bg-danger', !alreadyEnrolled && !warning); + blockModalHeader.classList.toggle('bg-success', alreadyEnrolled && !warning); + blockModalHeader.classList.toggle('bg-warning', warning); + blockModalHeader.classList.toggle('text-dark', warning); + blockModalHeader.classList.toggle('text-white', !warning); } if (blockModalBody) { - blockModalBody.textContent = message || (alreadyEnrolled - ? 'This student is already enrolled for the selected school year.' - : 'Enrollment cannot continue at this time. Please contact school administration.'); + const messages = Array.isArray(options.messages) ? options.messages.filter(Boolean) : []; + if (messages.length > 1) { + const list = document.createElement('ul'); + list.className = 'mb-0 ps-4'; + messages.forEach(item => { + const li = document.createElement('li'); + const separatorIndex = item.indexOf(': '); + if (separatorIndex > 0) { + const name = document.createElement('strong'); + name.textContent = item.substring(0, separatorIndex); + li.appendChild(name); + li.appendChild(document.createTextNode(item.substring(separatorIndex))); + } else { + li.textContent = item; + } + list.appendChild(li); + }); + blockModalBody.replaceChildren(list); + } else { + blockModalBody.textContent = messages[0] || message || (alreadyEnrolled + ? 'This student is already enrolled for the selected school year.' + : 'Enrollment cannot continue at this time. Please contact school administration.'); + } } if (blockModal) { blockModal.show(); @@ -1400,6 +1443,24 @@ $studentCount = count($students ?? []); }; } + function blockMessagesForCards(cards) { + return cards + .map(card => { + const studentId = String(card.dataset.studentId || ''); + const row = latestEligibility[studentId] || {}; + const message = row.primary_parent_message + || (row.blocking_rule_codes || []).join(', ') + || card.dataset.blockMessage + || ''; + if (!message) { + return ''; + } + const name = row.student_name || card.dataset.studentName || 'Student'; + return name + ': ' + message; + }) + .filter(Boolean); + } + async function refreshEligibility() { const response = await fetch(eligibilityRefreshUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, @@ -1426,7 +1487,7 @@ $studentCount = count($students ?? []); function applyEligibilityToCards() { let shouldReload = false; - document.querySelectorAll('[data-student-card]').forEach(card => { + studentCards().forEach(card => { const studentId = String(card.dataset.studentId || ''); const row = latestEligibility[studentId]; if (!row) { @@ -1434,7 +1495,7 @@ $studentCount = count($students ?? []); } const wasBlocked = card.dataset.selectable !== '1'; - const canEnroll = row.can_enroll === true && !deadlinePassed; + const canEnroll = (row.can_enroll === true || row.parent_enrollment_allowed === true) && !deadlinePassed; card.dataset.selectable = canEnroll ? '1' : '0'; card.classList.toggle('is-disabled', !canEnroll); if (row.primary_parent_message) { @@ -1468,19 +1529,29 @@ $studentCount = count($students ?? []); return; } - const eligibleCards = Array.from(document.querySelectorAll('[data-student-card]')).filter(card => { + const cardsToConsider = requestedStudentCards(); + const eligibleCards = cardsToConsider.filter(card => { const studentId = String(card.dataset.studentId || ''); - return latestEligibility[studentId]?.can_enroll === true; + const row = latestEligibility[studentId] || {}; + return row.can_enroll === true || row.parent_enrollment_allowed === true; }); if (eligibleCards.length === 0) { - const firstBlocked = Object.values(latestEligibility).find(row => row.primary_parent_message || (row.blocking_rule_codes || []).length > 0); + const otherBlockedCards = studentCards().filter(card => !cardsToConsider.includes(card)); + const blockedMessages = blockMessagesForCards(cardsToConsider) + .concat(blockMessagesForCards(otherBlockedCards)); + const firstBlocked = cardsToConsider + .map(card => latestEligibility[String(card.dataset.studentId || '')]) + .find(row => row && (row.primary_parent_message || (row.blocking_rule_codes || []).length > 0)) + || Object.values(latestEligibility).find(row => row.primary_parent_message || (row.blocking_rule_codes || []).length > 0); const alreadyEnrolled = firstBlocked?.decision === 'ALREADY_ENROLLED'; const blockReason = firstBlocked?.primary_parent_message || (firstBlocked?.blocking_rule_codes || []).join(', ') || 'No students are currently eligible for enrollment.'; showBlockModal(blockReason, { alreadyEnrolled, + messages: blockedMessages, + warning: alreadyEnrolled && blockedMessages.length > 1, title: firstBlocked?.block_title || (alreadyEnrolled ? 'Already Enrolled' : 'Enrollment Not Available'), }); return; @@ -1491,7 +1562,7 @@ $studentCount = count($students ?? []); }); } - document.querySelectorAll('[data-student-card]').forEach(card => { + studentCards().forEach(card => { card.addEventListener('click', async function(event) { if (event.target.closest('input, select, textarea, label')) { return; @@ -1645,11 +1716,14 @@ $studentCount = count($students ?? []); }); }); - if (enrollmentStartStep >= 2 && startButton && !startButton.disabled && !) { - openEnrollmentAtStep(enrollmentStartStep - 1); - } - - refreshEligibility().catch(() => { + refreshEligibility().then(() => { + if (enrollmentStartStep >= 2 && startButton && !startButton.disabled && !) { + openEnrollmentAtStep(enrollmentStartStep - 1); + } + }).catch(() => { + if (enrollmentStartStep >= 2 && startButton && !startButton.disabled && !) { + openEnrollmentAtStep(enrollmentStartStep - 1); + } // Keep server-rendered eligibility if refresh fails. }); }); diff --git a/app/Views/parent/register_student.php b/app/Views/parent/register_student.php index de59e17..df45792 100644 --- a/app/Views/parent/register_student.php +++ b/app/Views/parent/register_student.php @@ -100,14 +100,22 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
0) { + $unenrolledStudentIds[] = $studentId; + } } } + usort($unenrolledStudentIds, static fn (int $a, int $b): int => $b <=> $a); + $enrollmentUrl = base_url('/parent/enroll_classes?' . http_build_query([ + 'start' => 2, + 'students' => implode(',', $unenrolledStudentIds), + ])); ?> @@ -118,7 +126,7 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;

Please complete their enrollment using the button below to ensure they are able to attend classes.

-