Files
root 38644b32ae
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 48s
Tests / PHPUnit (push) Failing after 1m19s
fix invoice and enrollment fees
2026-08-27 18:34:36 -04:00

1865 lines
98 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('styles') ?>
<style>
.enrollment-start-panel {
border: 1px solid #d8e7dc;
border-radius: 8px;
background: #f8fbf8;
padding: 1rem;
}
.enrollment-stepper {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: .5rem;
}
.enrollment-step {
border-bottom: 3px solid #dee2e6;
color: #6c757d;
font-size: .85rem;
line-height: 1.2;
overflow-wrap: anywhere;
padding: .35rem 0;
text-align: center;
}
.enrollment-step.is-active {
border-color: #198754;
color: #198754;
font-weight: 700;
}
.student-select-card {
border: 1px solid #dee2e6;
border-radius: 8px;
cursor: pointer;
padding: .85rem;
transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease;
}
.student-select-card.is-selected {
background: #eef8f0;
border-color: #198754;
box-shadow: 0 0 0 .15rem rgba(25, 135, 84, .14);
}
.student-select-card.is-disabled {
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
}
.student-select-icon {
align-items: center;
border: 1px solid #adb5bd;
border-radius: 50%;
display: inline-flex;
height: 1.65rem;
justify-content: center;
width: 1.65rem;
}
.student-select-card.is-selected .student-select-icon {
background: #198754;
border-color: #198754;
color: #fff;
}
.enrollment-policy-frame {
border: 1px solid #dee2e6;
border-radius: 8px;
height: clamp(520px, 72vh, 900px);
width: 100%;
}
.enrollment-policy-ack {
align-items: flex-start;
background: #fff8e1;
border: 2px solid #ffc107;
border-radius: 8px;
bottom: 0;
box-shadow: 0 -.35rem 1rem rgba(33, 37, 41, .08);
display: flex;
gap: .85rem;
margin-top: 1rem;
padding: 1rem;
position: sticky;
z-index: 2;
}
.enrollment-policy-ack.is-accepted {
background: #eef8f0;
border-color: #198754;
}
.enrollment-policy-ack .form-check-input {
border: 2px solid #212529;
flex: 0 0 auto;
height: 1.6rem;
margin: .1rem 0 0;
width: 1.6rem;
}
.enrollment-policy-ack .form-check-input:focus {
box-shadow: 0 0 0 .25rem rgba(255, 193, 7, .35);
}
.enrollment-policy-ack .form-check-input:checked {
background-color: #198754;
border-color: #198754;
}
.enrollment-policy-ack-label {
color: #212529;
cursor: pointer;
font-size: 1rem;
line-height: 1.35;
}
.enrollment-policy-ack-label strong {
display: block;
}
.enrollment-policy-ack-label span {
color: #6c757d;
display: block;
font-size: .875rem;
margin-top: .15rem;
}
@media (max-width: 767.98px) {
.enrollment-policy-frame {
height: 72vh;
min-height: 480px;
}
.enrollment-policy-ack {
padding: .85rem;
}
}
.enrollment-withdraw-control {
align-items: center;
display: inline-flex;
gap: .5rem;
justify-content: flex-end;
min-width: max-content;
padding-left: 0;
white-space: nowrap;
}
.enrollment-withdraw-control .form-check-input {
flex: 0 0 auto;
margin-left: 0;
}
.enrollment-modal-footer {
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;
}
.enrollment-step {
font-size: .72rem;
}
.enrollment-modal-footer {
align-items: stretch;
flex-direction: column;
}
.enrollment-modal-footer .btn {
width: 100%;
}
.student-select-card {
padding: .75rem;
}
.enrollment-status-table,
.enrollment-status-table thead,
.enrollment-status-table tbody,
.enrollment-status-table tr,
.enrollment-status-table th,
.enrollment-status-table td {
display: block;
width: 100%;
}
.enrollment-status-table thead {
display: none;
}
.enrollment-status-table {
border: 0;
}
.enrollment-status-table tr {
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: .85rem;
overflow: hidden;
}
.enrollment-status-table td {
align-items: flex-start;
border-bottom: 1px solid #eef1f3;
display: flex;
gap: .75rem;
justify-content: space-between;
padding: .75rem;
text-align: right;
}
.enrollment-status-table td:last-child {
border-bottom: 0;
}
.enrollment-status-table td::before {
color: #6c757d;
content: attr(data-label);
flex: 0 0 42%;
font-size: .8rem;
font-weight: 700;
text-align: left;
}
.enrollment-status-table td > * {
max-width: 58%;
}
.enrollment-status-table td[data-label="Withdraw"] {
align-items: center;
}
.enrollment-status-table td[data-label="Withdraw"] > * {
max-width: none;
}
.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() ?>
<?= $this->section('content') ?>
<?php
$tz = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$deadlineObj = (new DateTime($lastDayOfRegistration, new DateTimeZone($tz)))->setTime(23, 59, 59);
$nowObj = new DateTime('now', new DateTimeZone($tz));
$deadlinePassed = $nowObj > $deadlineObj;
$hasAcceptedSchoolPolicy = (bool) ($hasAcceptedSchoolPolicy ?? false);
$familyFinancialSummary = is_array($familyFinancialSummary ?? null) ? $familyFinancialSummary : [];
$enrollmentFeeSchedule = is_array($enrollmentFeeSchedule ?? null) ? $enrollmentFeeSchedule : [];
$money = static function ($amount) use ($familyFinancialSummary): string {
$currency = (string) ($familyFinancialSummary['currency'] ?? '$');
return $currency . number_format((float) $amount, 2);
};
$statusBadge = static function (?string $status): string {
$status = strtolower(trim((string) $status));
return match ($status) {
'admission under review' => '<span class="badge bg-primary">admission under review</span>',
'review & decision' => '<span class="badge bg-secondary">review &amp; decision</span>',
'payment pending' => '<span class="badge bg-warning text-dark">payment pending</span>',
'enrolled' => '<span class="badge bg-success">enrolled</span>',
'withdraw under review' => '<span class="badge bg-warning text-dark">withdraw under review</span>',
'refund pending' => '<span class="badge bg-info text-dark">refund pending</span>',
'withdrawn' => '<span class="badge bg-danger">withdrawn</span>',
'waitlist' => '<span class="badge bg-secondary">waitlist</span>',
'denied' => '<span class="badge bg-danger">denied</span>',
'not enrolled' => '<span class="badge bg-light text-dark">not enrolled</span>',
default => '<span class="badge bg-light text-dark">unknown</span>',
};
};
$alreadyEnrolledMessage = static fn (?string $status): string => \App\Support\Enrollment\EnrollmentEligibility::alreadyEnrolledMessage($status);
$alreadyEnrolledTitle = static fn (?string $status): string => \App\Support\Enrollment\EnrollmentEligibility::alreadyEnrolledTitle($status);
$hasSettledEnrollmentStatus = static function (?string $status, ?string $admissionStatus = ''): bool {
if (strtolower(trim((string) $admissionStatus)) === 'accepted') {
return true;
}
return in_array(strtolower(trim((string) $status)), [
'admission under review',
'review & decision',
'payment pending',
'enrolled',
'waitlist',
'withdraw under review',
'refund pending',
], true);
};
$enrollableCount = 0;
$withdrawableCount = 0;
$currentYearTuitionStudentCount = 0;
foreach (($students ?? []) as $student) {
$studentStatus = strtolower(trim((string) ($student['enrollment_status'] ?? '')));
$studentAdmissionStatus = strtolower(trim((string) ($student['admission_status'] ?? '')));
if (
! in_array($studentStatus, ['withdrawn', 'withdraw under review', 'refund pending', 'denied', 'not enrolled'], true)
&& (
in_array($studentStatus, ['admission under review', 'review & decision', 'payment pending', 'enrolled', 'waitlist'], true)
|| $studentAdmissionStatus === 'accepted'
)
) {
$currentYearTuitionStudentCount++;
}
$evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : [];
if (
(($evaluation['can_enroll'] ?? false) === true || ($evaluation['parent_enrollment_allowed'] ?? false) === true)
&& !$deadlinePassed
&& $isEditable
) {
$enrollableCount++;
}
if (($student['enrollment_status'] ?? '') === 'enrolled' && $isEditable) {
$withdrawableCount++;
}
}
$studentCount = count($students ?? []);
?>
<div class="container my-4">
<div class="text-center mb-4">
<h3 class="text-success">Enroll in Classes</h3>
<?php if (!empty($selectedYear)): ?>
<div class="text-muted small">School Year: <?= esc($selectedYear) ?></div>
<?php endif; ?>
</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>Payments are processed on the first day of school: <strong><?= esc(local_date($schoolStartDate, 'm-d-Y')) ?></strong>.</li>
</ul>
</div>
<?php if (!empty($students)): ?>
<form action="<?= base_url('/parent/enroll_classes_handler') ?>" method="post" id="enrollmentFlowForm">
<?= csrf_field() ?>
<input type="hidden" id="accept_school_policy_input" name="accept_school_policy" value="<?= $hasAcceptedSchoolPolicy ? '1' : '0' ?>">
<div class="enrollment-start-panel mb-4">
<div class="row g-3 align-items-center">
<div class="col-lg">
<div class="fw-semibold">Enrollment process</div>
<div class="text-muted small">
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">
<button type="button"
class="btn btn-success btn-lg w-100"
id="startEnrollmentButton"
<?= (!$isEditable || $studentCount === 0 || $deadlinePassed) ? 'disabled' : '' ?>>
Start Enrollment
</button>
</div>
</div>
<?php if ($deadlinePassed): ?>
<div class="small text-danger mt-2">Enrollment closed on <?= esc($deadlineObj->format('m-d-Y')) ?>.</div>
<?php elseif ($studentCount === 0): ?>
<div class="small text-muted mt-2">No students are currently linked to this account.</div>
<?php elseif ($enrollableCount === 0): ?>
<div class="small text-muted mt-2">No students are currently eligible for enrollment. You may still click Start Enrollment to review the reason.</div>
<?php endif; ?>
</div>
<div class="table-responsive mb-4">
<table class="table table-striped table-bordered align-middle enrollment-status-table">
<thead>
<tr>
<th>Student</th>
<th>Grade</th>
<th>Decision</th>
<th>Required Action</th>
<th>Status</th>
<th>Eligibility</th>
<th>Withdraw</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<?php
$studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''));
$studentName = $studentName !== '' ? $studentName : 'Student';
$section = $student['class_section'] ?? null;
$gradeLabel = $section
? preg_replace_callback('/\byouth\b/i', fn($m) => ucfirst(strtolower($m[0])), (string) $section)
: 'Not Assigned';
?>
<tr>
<td data-label="Student">
<div class="fw-semibold"><?= esc($studentName) ?></div>
<div class="text-muted small">School ID: <?= esc($student['school_id'] ?? 'N/A') ?></div>
</td>
<td data-label="Grade"><?= esc($gradeLabel) ?></td>
<td data-label="Decision"><?= esc($student['transition_evaluation']['decision_label'] ?? 'Pending') ?></td>
<td data-label="Required Action"><?= esc($student['parent_enrollment_state'] ?? $student['required_action_label'] ?? 'Contact administration') ?></td>
<td data-label="Status"><?= $statusBadge($student['enrollment_status'] ?? '') ?></td>
<td data-label="Eligibility">
<?php
$evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : [];
if ($hasSettledEnrollmentStatus($student['enrollment_status'] ?? '', $student['admission_status'] ?? '')) {
echo '<span class="text-muted small">' . esc($alreadyEnrolledMessage((string) ($student['enrollment_status'] ?? ''))) . '</span>';
} elseif (($evaluation['can_enroll'] ?? false) === true || ($evaluation['parent_enrollment_allowed'] ?? false) === true) {
echo '<span class="text-success small">Eligible</span>';
} elseif (! empty($evaluation['primary_parent_message'])) {
echo '<span class="text-danger small">' . esc($evaluation['primary_parent_message']) . '</span>';
} elseif (! empty($student['enrollment_eligibility_message']['message'])) {
echo '<span class="text-danger small">' . esc($student['enrollment_eligibility_message']['message']) . '</span>';
} else {
echo '<span class="text-muted small">Not eligible</span>';
}
?>
</td>
<td data-label="Withdraw">
<?php if (($student['enrollment_status'] ?? '') === 'enrolled'): ?>
<div class="form-check form-switch m-0 enrollment-withdraw-control">
<input class="form-check-input" type="checkbox" name="withdraw[]" value="<?= esc($student['id']) ?>" id="withdraw-<?= esc($student['id']) ?>" <?= !$isEditable ? 'disabled' : '' ?>>
<label class="form-check-label small" for="withdraw-<?= esc($student['id']) ?>">Request</label>
</div>
<?php elseif (($student['enrollment_status'] ?? '') === 'withdrawn'): ?>
<span class="text-muted small">Withdrawn</span>
<?php else: ?>
<span class="text-muted small">Unavailable</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($withdrawableCount > 0): ?>
<div class="d-flex justify-content-center mb-4">
<button type="submit" class="btn btn-outline-danger" id="withdrawSubmitButton">Submit Withdrawal Request</button>
</div>
<?php endif; ?>
<div class="modal fade" id="enrollmentFlowModal" tabindex="-1" aria-labelledby="enrollmentFlowModalLabel" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-xl modal-dialog-scrollable modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="enrollmentFlowModalLabel">Submit Enrollment</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<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">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">
<h6 class="fw-semibold">Review student information</h6>
<div class="text-muted small mb-3">Select each student you want to enroll for <?= esc($selectedYear ?? 'the selected school year') ?>. Photo consent, medical conditions, and allergies can be updated below.</div>
<?php
$enrollmentMedicalOptions = [
'None',
'ADHD (Attention-Deficit/Hyperactivity Disorder)',
'Anxiety or Emotional Disorders',
'Asthma',
'Autism Spectrum Disorder (ASD)',
'Behavioral or Conduct Disorders',
'Blindness / Vision Impairment',
'Celiac Disease (Gluten Intolerance)',
'Cerebral Palsy',
'Cystic Fibrosis',
'Depression',
'Diabetes (Type 1 or Type 2)',
'Down Syndrome',
'Dyslexia or Learning Disabilities',
'Eating Disorders',
'Eczema / Severe Skin Conditions',
'Epilepsy / Seizure Disorders',
'Hearing Impairments / Deafness',
'Heart Conditions (congenital or acquired)',
'Hemophilia / Bleeding Disorders',
'Kidney Disease',
'Migraines / Chronic Headaches',
'Obsessive-Compulsive Disorder (OCD)',
'Physical Disabilities / Mobility Impairments',
'PTSD (Post-Traumatic Stress Disorder)',
'Sickle Cell Anemia',
'Speech and Language Disorders',
'Thyroid Disorders',
'Tourette Syndrome',
'Traumatic Brain Injury (TBI)',
'Rheumatic diseases',
'Ulcerative Colitis / Crohns Disease',
'Other',
];
$enrollmentAllergyOptions = [
'None',
'Animal Dander (cats, dogs, etc.)',
'Antibiotics',
'Bee stings',
'Cockroach',
'Corn',
'Dust Mites',
'Egg',
'Fire ant stings',
'Fish',
'Fragrances / Perfumes',
'Latex',
'Milk / Dairy',
'Mold',
'Mosquito bites',
'Peanut',
'Pollen (grass, tree, weed)',
'Sesame',
'Shellfish (shrimp, crab, lobster, etc.)',
'Soy',
'Tree Nuts (almond, cashew, walnut, etc.)',
'Wasp stings',
'Wheat / Gluten',
'Other',
];
?>
<div class="d-grid gap-2">
<?php foreach ($students as $student): ?>
<?php
$eligibilityMessage = is_array($student['enrollment_eligibility_message'] ?? null) ? $student['enrollment_eligibility_message'] : ['message' => '', 'blocking' => false, 'level' => 'info'];
$evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : [];
$studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''));
$studentName = $studentName !== '' ? $studentName : 'Student';
$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 = $canEnroll
? ''
: ($hasSettledEnrollment
? $alreadyEnrolledMessage((string) ($student['enrollment_status'] ?? ''))
: (string) ($evaluation['primary_parent_message'] ?? $eligibilityMessage['message'] ?? ''));
$blockTitle = $hasSettledEnrollment
? $alreadyEnrolledTitle((string) ($student['enrollment_status'] ?? ''))
: 'Enrollment Not Available';
$section = $student['class_section'] ?? null;
$gradeLabel = $section
? preg_replace_callback('/\byouth\b/i', fn($m) => ucfirst(strtolower($m[0])), (string) $section)
: 'Not Assigned';
$medicalSelected = is_array($student['medical_conditions'] ?? null)
? array_values(array_filter(array_map('trim', $student['medical_conditions'])))
: array_values(array_filter(array_map('trim', preg_split('/[,\n;]+/', (string) ($student['medical_conditions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [])));
$allergySelected = is_array($student['allergies'] ?? null)
? array_values(array_filter(array_map('trim', $student['allergies'])))
: array_values(array_filter(array_map('trim', preg_split('/[,\n;]+/', (string) ($student['allergies'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [])));
$medicalOther = implode(', ', array_diff($medicalSelected, $enrollmentMedicalOptions));
$allergyOther = implode(', ', array_diff($allergySelected, $enrollmentAllergyOptions));
if ($medicalOther !== '') {
$medicalSelected[] = 'Other';
}
if ($allergyOther !== '') {
$allergySelected[] = 'Other';
}
$dobDisplay = !empty($student['dob']) ? local_date($student['dob'], 'm-d-Y') : 'N/A';
$photoConsentValue = (string) ($student['photo_consent'] ?? '');
if ($photoConsentValue !== '0' && $photoConsentValue !== '1') {
$photoConsentValue = !empty($student['photo_consent']) ? '1' : '';
}
?>
<div class="student-select-card <?= $canEnroll ? '' : 'is-disabled' ?>"
data-student-card
data-student-id="<?= esc($student['id']) ?>"
data-student-name="<?= esc($studentName) ?>"
data-requested-student="<?= $isPreselectedStudent ? '1' : '0' ?>"
data-school-id="<?= esc($student['school_id'] ?? 'N/A') ?>"
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-counts-tuition="<?= $hasSettledEnrollment ? '1' : '0' ?>"
data-selectable="<?= $canEnroll ? '1' : '0' ?>"
data-already-enrolled="<?= $hasSettledEnrollment ? '1' : '0' ?>"
data-block-title="<?= esc($blockTitle) ?>"
data-block-message="<?= esc($blockMessage) ?>">
<div class="d-flex gap-3 align-items-start">
<span class="student-select-icon" aria-hidden="true"><i class="bi bi-check2"></i></span>
<div class="flex-grow-1">
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="fw-semibold"><?= esc($studentName) ?></div>
<?= $statusBadge($student['enrollment_status'] ?? '') ?>
</div>
<div class="small text-muted mt-1">
<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>
<?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): ?>
<input type="hidden" name="student_info[<?= esc($student['id']) ?>][firstname]" value="<?= esc($student['firstname'] ?? '') ?>">
<input type="hidden" name="student_info[<?= esc($student['id']) ?>][lastname]" value="<?= esc($student['lastname'] ?? '') ?>">
<input type="hidden" name="student_info[<?= esc($student['id']) ?>][dob]" value="<?= esc($student['dob'] ?? '') ?>">
<input type="hidden" name="student_info[<?= esc($student['id']) ?>][gender]" value="<?= esc($student['gender'] ?? '') ?>">
<input type="hidden" name="student_info[<?= esc($student['id']) ?>][registration_grade]" value="<?= esc($student['registration_grade'] ?? '') ?>">
<div class="row g-2 mt-3" data-student-edit-fields>
<div class="col-12">
<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>
<select class="form-select form-select-sm" id="student-photo-consent-<?= esc($student['id']) ?>" name="student_info[<?= esc($student['id']) ?>][photo_consent]" required disabled data-student-edit-input>
<option value="">Select</option>
<option value="1" <?= $photoConsentValue === '1' ? 'selected' : '' ?>>Yes</option>
<option value="0" <?= $photoConsentValue === '0' ? 'selected' : '' ?>>No</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Medical conditions</label>
<div class="border rounded p-2 bg-white small" style="max-height: 180px; overflow-y: auto;" data-health-group="medical_conditions">
<?php foreach ($enrollmentMedicalOptions as $opt): ?>
<?php $optId = 'student-medical-' . $student['id'] . '-' . md5($opt); ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="<?= esc($optId) ?>" name="student_info[<?= esc($student['id']) ?>][medical_conditions][]" value="<?= esc($opt) ?>" <?= in_array($opt, $medicalSelected, true) ? 'checked' : '' ?> disabled data-student-edit-input data-health-option="<?= $opt === 'Other' ? 'other' : ($opt === 'None' ? 'none' : 'item') ?>">
<label class="form-check-label" for="<?= esc($optId) ?>"><?= esc($opt) ?></label>
</div>
<?php endforeach; ?>
</div>
<input type="text" class="form-control form-control-sm mt-2 <?= $medicalOther === '' ? 'd-none' : '' ?>" name="student_info[<?= esc($student['id']) ?>][medical_condition_other]" value="<?= esc($medicalOther) ?>" placeholder="Please specify if Other is selected" disabled data-student-edit-input data-other-input="medical_conditions" maxlength="100">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Allergies</label>
<div class="border rounded p-2 bg-white small" style="max-height: 180px; overflow-y: auto;" data-health-group="allergies">
<?php foreach ($enrollmentAllergyOptions as $opt): ?>
<?php $optId = 'student-allergy-' . $student['id'] . '-' . md5($opt); ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="<?= esc($optId) ?>" name="student_info[<?= esc($student['id']) ?>][allergies][]" value="<?= esc($opt) ?>" <?= in_array($opt, $allergySelected, true) ? 'checked' : '' ?> disabled data-student-edit-input data-health-option="<?= $opt === 'Other' ? 'other' : ($opt === 'None' ? 'none' : 'item') ?>">
<label class="form-check-label" for="<?= esc($optId) ?>"><?= esc($opt) ?></label>
</div>
<?php endforeach; ?>
</div>
<input type="text" class="form-control form-control-sm mt-2 <?= $allergyOther === '' ? 'd-none' : '' ?>" name="student_info[<?= esc($student['id']) ?>][allergy_other]" value="<?= esc($allergyOther) ?>" placeholder="Please specify if Other is selected" disabled data-student-edit-input data-other-input="allergies" maxlength="100">
</div>
</div>
<?php endif; ?>
<?php if (($eligibilityMessage['message'] ?? '') !== ''): ?>
<div class="alert alert-<?= esc($eligibilityMessage['level'] ?? 'info') ?> py-2 px-3 mt-2 mb-0 small">
<?= esc($eligibilityMessage['message']) ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($canEnroll): ?>
<input class="d-none" type="checkbox" name="enroll[]" value="<?= esc($student['id']) ?>" data-enroll-input>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</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"
autocomplete="tel"
title="Enter a valid 10-digit phone number (e.g., 123-456-7890)"
required>
<div class="form-text text-muted">10-digit US phone number (e.g., 123-456-7890).</div>
<div id="parent-contact-cellphone-error" class="invalid-feedback"></div>
</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="50"
autocomplete="address-line1"
title="250 characters. Letters, numbers, spaces, periods, and hyphens only."
required>
<div class="form-text text-muted">250 characters. Letters, numbers, spaces, periods, and hyphens only.</div>
<div id="parent-contact-street-error" class="invalid-feedback"></div>
</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"
autocomplete="address-line2"
title="Optional. Letters, numbers, spaces, periods, and hyphens only.">
<div class="form-text text-muted">Optional. Max 15 characters.</div>
<div id="parent-contact-apt-error" class="invalid-feedback"></div>
</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="30"
autocomplete="address-level2"
title="230 characters. Letters, spaces, periods, apostrophes, and hyphens only."
required>
<div class="form-text text-muted">230 characters. Letters and spaces allowed.</div>
<div id="parent-contact-city-error" class="invalid-feedback"></div>
</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"
pattern="\d{5}"
autocomplete="postal-code"
title="Please enter a 5-digit ZIP code"
required>
<div class="form-text text-muted">5-digit US ZIP code only.</div>
<div id="parent-contact-zip-error" class="invalid-feedback"></div>
</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 id="parent-contact-state-error" class="invalid-feedback"></div>
</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>
<div class="enrollment-policy-ack <?= $hasAcceptedSchoolPolicy ? 'is-accepted' : '' ?>" id="schoolPolicyAcceptedPanel">
<input class="form-check-input" type="checkbox" value="1" id="schoolPolicyAcceptedCheckbox" <?= $hasAcceptedSchoolPolicy ? 'checked disabled' : '' ?>>
<label class="enrollment-policy-ack-label" for="schoolPolicyAcceptedCheckbox">
<strong>I have read and accept all school policies.</strong>
<span>This acknowledgement is required before enrollment can continue.</span>
</label>
</div>
</div>
<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 !== []): ?>
<div class="border rounded p-3 bg-light">
<div class="row g-2">
<div class="col-md-4"><span class="text-muted">Previous-year carry-over balance:</span> <strong data-financial-value="carry_over_balance"><?= esc($money($familyFinancialSummary['carry_over_balance'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Total currently due:</span> <strong data-financial-value="amount_due"><?= esc($money($familyFinancialSummary['amount_due'] ?? 0)) ?></strong></div>
</div>
</div>
<?php else: ?>
<div class="alert alert-warning mb-0">Family account information is not available. Please contact the administration if you have questions about tuition or balance.</div>
<?php endif; ?>
</div>
<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>
<div class="mb-3">
<div class="fw-semibold mb-2">Summary</div>
<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">
<div class="fw-semibold mb-2">School dates</div>
<div class="small"><span class="text-muted">First school day:</span> <strong><?= esc(!empty($schoolStartDate) ? local_date($schoolStartDate, 'm-d-Y') : 'TBD') ?></strong></div>
<div class="small"><span class="text-muted">Make-up exam date:</span> <strong><?= esc(!empty($fallMakeupExamOn) ? local_date($fallMakeupExamOn, 'm-d-Y') : 'No make-up exam date currently listed') ?></strong></div>
<div class="small"><span class="text-muted">Enrollment deadline:</span> <strong><?= esc(local_date($lastDayOfRegistration, 'm-d-Y')) ?></strong></div>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<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">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">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>
<?php endif; ?>
</div>
</div>
</div>
<div class="alert alert-warning mb-0 small">
<div class="fw-semibold mb-1">Important information</div>
<ul class="mb-0 ps-3">
<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>
<?php endif; ?>
<li>Contact administration if any student information, fee, or placement detail looks incorrect.</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer enrollment-modal-footer">
<button type="button" class="btn btn-outline-secondary" id="enrollmentBackButton">Back</button>
<button type="button" class="btn btn-success" id="enrollmentNextButton">Next</button>
<button type="submit" class="btn btn-success d-none" id="enrollmentSubmitButton">Submit Enrollment</button>
</div>
</div>
</div>
</div>
</form>
<?php else: ?>
<p>No students found for the selected school year. Please register your kids first.</p>
<?php endif; ?>
</div>
<div class="modal fade" id="enrollmentBlockModal" tabindex="-1" aria-labelledby="enrollmentBlockModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="enrollmentBlockModalLabel">Enrollment Not Available</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="enrollmentBlockModalBody">
Enrollment cannot continue at this time. Please contact school administration.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-labelledby="deadlineModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deadlineModalLabel">Enrollment Closed</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Enrollment for the selected school year closed on <strong><?= esc($deadlineObj->format('m-d-Y')) ?></strong>.
You can still request a withdrawal if applicable.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
const deadlinePassed = <?= $deadlinePassed ? 'true' : 'false' ?>;
const form = document.getElementById('enrollmentFlowForm');
const startButton = document.getElementById('startEnrollmentButton');
const flowModalEl = document.getElementById('enrollmentFlowModal');
const deadlineModalEl = document.getElementById('deadlineModal');
const blockModalEl = document.getElementById('enrollmentBlockModal');
const blockModalTitle = document.getElementById('enrollmentBlockModalLabel');
const blockModalHeader = blockModalEl ? blockModalEl.querySelector('.modal-header') : null;
const blockModalBody = document.getElementById('enrollmentBlockModalBody');
const flowModal = flowModalEl ? new bootstrap.Modal(flowModalEl) : null;
const deadlineModal = deadlineModalEl ? new bootstrap.Modal(deadlineModalEl) : null;
const blockModal = blockModalEl ? new bootstrap.Modal(blockModalEl) : null;
const eligibilityRefreshUrl = <?= json_encode(base_url('/parent/enrollment_eligibility_refresh'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
let latestEligibility = {};
const policyAcceptedInput = document.getElementById('accept_school_policy_input');
const policyAcceptedCheckbox = document.getElementById('schoolPolicyAcceptedCheckbox');
const policyAcceptedPanel = document.getElementById('schoolPolicyAcceptedPanel');
const backButton = document.getElementById('enrollmentBackButton');
const nextButton = document.getElementById('enrollmentNextButton');
const submitButton = document.getElementById('enrollmentSubmitButton');
const feeReviewList = document.getElementById('enrollmentFeeReviewList');
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),
'secondStudentFee' => (float) ($enrollmentFeeSchedule['second_student_fee'] ?? 0),
'tuitionDueAtRegistration' => (float) ($enrollmentFeeSchedule['tuition_due_at_registration'] ?? 0),
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
const familyFinancial = <?= json_encode([
'carryOverBalance' => (float) ($familyFinancialSummary['carry_forward_balance'] ?? $familyFinancialSummary['carry_over_balance'] ?? 0),
'currentBalance' => (float) ($familyFinancialSummary['current_year_balance'] ?? $familyFinancialSummary['current_balance'] ?? 0),
'amountDue' => (float) ($familyFinancialSummary['total_enrollment_due'] ?? $familyFinancialSummary['amount_due'] ?? 0),
'currentYearTuitionStudentCount' => (int) ($currentYearTuitionStudentCount ?? 0),
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
let currentStep = 0;
let hasAcceptedSchoolPolicy = <?= $hasAcceptedSchoolPolicy ? 'true' : 'false' ?>;
const selectedEnrollInputs = () => 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));
document.querySelectorAll('[data-step-panel]').forEach(panel => {
panel.classList.toggle('d-none', Number(panel.dataset.stepPanel) !== currentStep);
});
document.querySelectorAll('[data-step-indicator]').forEach(indicator => {
indicator.classList.toggle('is-active', Number(indicator.dataset.stepIndicator) === currentStep);
});
backButton.classList.toggle('d-none', currentStep === 0);
nextButton.classList.toggle('d-none', currentStep === finalStep);
submitButton.classList.toggle('d-none', currentStep !== finalStep);
if (currentStep >= 3) {
updateFinancialReview();
}
if (currentStep === finalStep) {
renderReview();
}
}
function parseGradeRank(value) {
let text = String(value || '').trim().toUpperCase().replace(/\s+/g, ' ');
text = text.replace(/[._-]/g, ' ');
if (['PK', 'P K', 'PREK', 'PRE K', 'PRE KINDER', 'PREKINDER'].includes(text)) return -1;
if (['K', 'KG', 'K G', 'KINDER', 'KINDERGARTEN'].includes(text)) return 1;
const youth = text.match(/^Y(?:OUTH)?\s*(\d+)?$/);
if (youth) return 9 + (youth[1] ? Math.max(1, Number(youth[1])) : 1);
const grade = text.match(/^(?:GR?ADE\s*)?(\d{1,2})\s*([A-Z]*)$/);
if (grade) return Number(grade[1]);
const match = text.match(/\d+/);
return match ? Number(match[0]) : 999;
}
function cardFeeGrade(card) {
return card?.dataset.expectedPlacement || card?.dataset.currentGrade || '';
}
function billableTuitionCards() {
const cardsByStudentId = new Map();
studentCards().forEach(card => {
if (card.dataset.countsTuition === '1') {
cardsByStudentId.set(String(card.dataset.studentId || ''), card);
}
});
selectedEnrollInputs().forEach(input => {
const card = input.closest('[data-student-card]');
if (card) {
cardsByStudentId.set(String(card.dataset.studentId || ''), card);
}
});
return Array.from(cardsByStudentId.values()).sort((left, right) => {
const rankDiff = parseGradeRank(cardFeeGrade(left)) - parseGradeRank(cardFeeGrade(right));
if (rankDiff !== 0) return rankDiff;
return String(left.dataset.studentName || '').localeCompare(String(right.dataset.studentName || ''));
});
}
function tuitionFeeByStudentId() {
const fees = new Map();
billableTuitionCards().forEach((card, index) => {
fees.set(
String(card.dataset.studentId || ''),
index === 0 ? Number(feeSchedule.firstStudentFee || 0) : Number(feeSchedule.secondStudentFee || 0)
);
});
return fees;
}
function calculateSelectedTuition() {
const fees = tuitionFeeByStudentId();
return selectedEnrollInputs().reduce((total, input) => {
const card = input.closest('[data-student-card]');
return total + Number(fees.get(String(card?.dataset.studentId || '')) || 0);
}, 0);
}
function updateFinancialReview() {
const selectedCount = selectedEnrollInputs().length;
const tuitionDue = selectedCount > 0 ? calculateSelectedTuition() : 0;
const carryOver = Number(familyFinancial.carryOverBalance || 0);
const currentBalance = Number(familyFinancial.currentBalance || 0);
const amountDue = Math.max(0, carryOver) + Math.max(0, currentBalance) + tuitionDue;
const values = {
carry_over_balance: carryOver,
tuition_due_at_registration: tuitionDue,
current_balance: currentBalance,
amount_due: amountDue,
};
Object.keys(values).forEach(key => {
document.querySelectorAll('[data-financial-value="' + key + '"]').forEach(node => {
node.textContent = formatMoney(values[key]);
});
});
}
function renderReview() {
updateFinancialReview();
const cards = selectedEnrollInputs().map(input => {
const card = input.closest('[data-student-card]');
const firstName = card ? card.querySelector("input[name$='[firstname]']")?.value?.trim() : '';
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("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';
return {
studentId: card?.dataset.studentId || '',
name,
schoolId,
dob,
gender,
registrationGrade: isNewStudent ? grade : '',
currentGrade,
expectedPlacement,
requiredAction,
};
});
if (!feeReviewList) {
return;
}
if (!cards.length) {
feeReviewList.innerHTML = '<div class="alert alert-warning mb-0">No students selected.</div>';
renderParentContactReview();
return;
}
const fees = tuitionFeeByStudentId();
feeReviewList.innerHTML = cards.map((student) => {
const tuitionFee = fees.get(String(student.studentId || '')) || 0;
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) {
return String(value).replace(/[&<>"']/g, function(char) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[char];
});
}
function formatMoney(amount) {
const numericAmount = Number(amount || 0);
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 titleCaseContact(value) {
return String(value || '')
.trim()
.replace(/\s+/g, ' ')
.toLowerCase()
.replace(/(^|[\s\-'])([a-z])/g, function(_, prefix, letter) {
return prefix + letter.toUpperCase();
});
}
const parentContactRules = {
phone: /^\(?\d{3}\)?[-\s]?\d{3}[-\s]?\d{4}$/,
street: /^[A-Za-z0-9.\s-]{2,50}$/,
apt: /^[A-Za-z0-9.\s-]{0,15}$/,
city: /^[A-Za-z\s.'-]{2,30}$/,
zip: /^\d{5}$/,
};
function setParentContactError(input, message) {
if (!input) {
return;
}
const error = document.getElementById(input.id + '-error');
input.classList.toggle('is-invalid', !!message);
if (error) {
error.textContent = message || '';
}
}
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 validateParentContactField(input) {
if (!input) {
return true;
}
const value = input.value.trim();
let message = '';
switch (input.id) {
case 'parent-contact-cellphone':
if (!parentContactRules.phone.test(value) || digitsOnly(value).length !== 10) {
message = 'Please enter a valid 10-digit phone number.';
}
break;
case 'parent-contact-street':
if (!parentContactRules.street.test(value)) {
message = 'Street address must be 250 characters and may contain only letters, numbers, spaces, periods, and hyphens.';
}
break;
case 'parent-contact-apt':
if (value && !parentContactRules.apt.test(value)) {
message = 'Apartment or unit may contain only letters, numbers, spaces, periods, and hyphens.';
}
break;
case 'parent-contact-city':
if (!parentContactRules.city.test(value)) {
message = 'City must be 230 characters and may contain only letters, spaces, periods, apostrophes, and hyphens.';
}
break;
case 'parent-contact-zip':
if (!parentContactRules.zip.test(digitsOnly(value))) {
message = 'Please enter a valid 5-digit ZIP code.';
}
break;
case 'parent-contact-state':
if (!value) {
message = 'Please select your state.';
}
break;
}
setParentContactError(input, message);
return message === '';
}
function validateParentContact() {
const fields = [
parentPhoneInput,
parentStreetInput,
parentAptInput,
parentCityInput,
parentZipInput,
parentStateInput,
];
fields.forEach(shapeParentContactField);
renderParentContactReview();
let firstInvalid = null;
fields.forEach(field => {
if (!validateParentContactField(field) && !firstInvalid) {
firstInvalid = field;
}
});
if (firstInvalid) {
firstInvalid.focus();
return false;
}
return true;
}
function shapeParentContactField(input) {
if (!input) {
return;
}
if (input === parentPhoneInput) {
input.value = formatPhoneDisplay(input.value);
return;
}
if (input === parentZipInput) {
input.value = digitsOnly(input.value).substring(0, 5);
return;
}
if (input === parentStreetInput || input === parentCityInput) {
input.value = titleCaseContact(input.value);
return;
}
if (input === parentAptInput) {
input.value = input.value.trim().replace(/\s+/g, ' ').toUpperCase();
}
}
function selectStudentsForEnrollment(ids) {
const wanted = Array.isArray(ids) ? ids.map(Number).filter(id => id > 0) : [];
const cards = wanted.length ? requestedStudentCards() : studentCards();
cards.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) {
policyAcceptedInput.value = hasAcceptedSchoolPolicy ? '1' : '0';
}
if (policyAcceptedPanel) {
policyAcceptedPanel.classList.toggle('is-accepted', hasAcceptedSchoolPolicy);
}
}
function setStudentFieldsEnabled(card, enabled) {
card.querySelectorAll('[data-student-edit-input]').forEach(field => {
field.disabled = !enabled;
});
syncHealthGroup(card, 'medical_conditions');
syncHealthGroup(card, 'allergies');
}
function syncHealthGroup(card, groupName) {
const selected = !!card.querySelector('[data-enroll-input]')?.checked;
const group = card.querySelector('[data-health-group="' + groupName + '"]');
const otherInput = card.querySelector('[data-other-input="' + groupName + '"]');
if (!group) {
return;
}
const noneChecked = !!group.querySelector('[data-health-option="none"]:checked');
const otherChecked = !!group.querySelector('[data-health-option="other"]:checked');
group.querySelectorAll('input[type="checkbox"]').forEach(input => {
if (!selected) {
input.disabled = true;
return;
}
if (input.dataset.healthOption === 'none') {
input.disabled = false;
return;
}
if (noneChecked) {
input.checked = false;
input.disabled = true;
return;
}
input.disabled = false;
});
if (otherInput) {
otherInput.classList.toggle('d-none', !otherChecked);
otherInput.disabled = !selected || !otherChecked;
if (!otherChecked) {
otherInput.value = '';
}
}
}
function validateSelectedStudentHealth() {
for (const input of selectedEnrollInputs()) {
const card = input.closest('[data-student-card]');
if (!card) {
continue;
}
const name = card.querySelector('.fw-semibold')?.textContent?.trim() || 'Student';
const photo = card.querySelector("select[name$='[photo_consent]']");
if (!photo || photo.value === '') {
alert('Please select photo consent for ' + name + '.');
return false;
}
const medicalChecked = card.querySelectorAll('[data-health-group="medical_conditions"] input[type="checkbox"]:checked');
if (!medicalChecked.length) {
alert('Please select medical conditions for ' + name + '.');
return false;
}
const medicalOther = card.querySelector('[data-other-input="medical_conditions"]');
if (card.querySelector('[data-health-group="medical_conditions"] [data-health-option="other"]:checked') && (!medicalOther || !medicalOther.value.trim())) {
alert('Please specify the other medical condition for ' + name + '.');
return false;
}
const allergyChecked = card.querySelectorAll('[data-health-group="allergies"] input[type="checkbox"]:checked');
if (!allergyChecked.length) {
alert('Please select allergies for ' + name + '.');
return false;
}
const allergyOther = card.querySelector('[data-other-input="allergies"]');
if (card.querySelector('[data-health-group="allergies"] [data-health-option="other"]:checked') && (!allergyOther || !allergyOther.value.trim())) {
alert('Please specify the other allergy for ' + name + '.');
return false;
}
}
return true;
}
studentCards().forEach(card => {
card.querySelectorAll('[data-health-group]').forEach(group => {
group.addEventListener('change', function(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement) || target.type !== 'checkbox') {
return;
}
const groupName = group.getAttribute('data-health-group');
if (!groupName) {
return;
}
if (target.dataset.healthOption === 'none' && target.checked) {
group.querySelectorAll('[data-health-option="item"], [data-health-option="other"]').forEach(input => {
input.checked = false;
});
}
if ((target.dataset.healthOption === 'item' || target.dataset.healthOption === 'other') && target.checked) {
const noneInput = group.querySelector('[data-health-option="none"]');
if (noneInput) {
noneInput.checked = false;
}
}
syncHealthGroup(card, groupName);
});
});
});
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 && !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) {
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();
}
}
function blockDetailsForCard(card) {
const studentId = String(card?.dataset?.studentId || '');
const row = latestEligibility[studentId] || {};
const alreadyEnrolled = row.decision === 'ALREADY_ENROLLED' || card.dataset.alreadyEnrolled === '1';
return {
alreadyEnrolled,
title: row.block_title || card.dataset.blockTitle || (alreadyEnrolled ? 'Already Enrolled' : 'Enrollment Not Available'),
message: row.primary_parent_message || card.dataset.blockMessage || '',
};
}
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' },
credentials: 'same-origin',
});
if (!response.ok) {
throw new Error('Unable to refresh eligibility.');
}
const payload = await response.json();
latestEligibility = {};
(payload.students || []).forEach(row => {
latestEligibility[String(row.student_id)] = row;
});
if (payload.financial_summary) {
familyFinancial.carryOverBalance = Number(payload.financial_summary.carry_forward_balance || 0);
familyFinancial.currentBalance = Number(payload.financial_summary.current_year_balance || 0);
familyFinancial.amountDue = Number(payload.financial_summary.total_enrollment_due || 0);
updateFinancialReview();
}
applyEligibilityToCards();
return latestEligibility;
}
function applyEligibilityToCards() {
let shouldReload = false;
studentCards().forEach(card => {
const studentId = String(card.dataset.studentId || '');
const row = latestEligibility[studentId];
if (!row) {
return;
}
const wasBlocked = card.dataset.selectable !== '1';
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) {
card.dataset.blockMessage = row.primary_parent_message;
}
if (row.decision === 'ALREADY_ENROLLED') {
card.dataset.alreadyEnrolled = '1';
card.dataset.blockTitle = row.block_title || 'Already Enrolled';
}
if (wasBlocked && canEnroll && !card.querySelector('[data-enroll-input]')) {
shouldReload = true;
}
});
if (shouldReload) {
window.location.reload();
}
}
if (startButton && flowModal) {
startButton.addEventListener('click', async function() {
if (deadlinePassed) {
if (deadlineModal) deadlineModal.show();
return;
}
try {
await refreshEligibility();
} catch (error) {
alert(error.message || 'Unable to refresh enrollment eligibility.');
return;
}
const cardsToConsider = requestedStudentCards();
const eligibleCards = cardsToConsider.filter(card => {
const studentId = String(card.dataset.studentId || '');
const row = latestEligibility[studentId] || {};
return row.can_enroll === true || row.parent_enrollment_allowed === true;
});
if (eligibleCards.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;
}
setStep(0);
flowModal.show();
});
}
studentCards().forEach(card => {
card.addEventListener('click', async function(event) {
if (event.target.closest('input, select, textarea, label')) {
return;
}
if (this.dataset.selectable !== '1') {
try {
await refreshEligibility();
} catch (error) {
// Fall back to the last known server-rendered message.
}
if (this.dataset.selectable !== '1') {
const details = blockDetailsForCard(this);
showBlockModal(details.message, details);
return;
}
}
const input = this.querySelector('[data-enroll-input]');
if (!input) {
return;
}
input.checked = !input.checked;
this.classList.toggle('is-selected', input.checked);
setStudentFieldsEnabled(this, input.checked);
});
});
if (policyAcceptedCheckbox) {
policyAcceptedCheckbox.addEventListener('change', syncPolicyAccepted);
}
if (backButton) {
backButton.addEventListener('click', function() {
setStep(currentStep - 1);
});
}
if (nextButton) {
nextButton.addEventListener('click', function() {
if (currentStep === 0 && selectedEnrollInputs().length === 0) {
alert('Please select at least one student to enroll.');
return;
}
if (currentStep === 0 && !validateSelectedStudentHealth()) {
return;
}
if (currentStep === 1 && !validateParentContact()) {
return;
}
if (currentStep === 2) {
syncPolicyAccepted();
if (!hasAcceptedSchoolPolicy) {
alert('Please read and accept the school policies before continuing.');
return;
}
}
setStep(currentStep + 1);
});
}
if (form) {
form.addEventListener('submit', function(e) {
if (e.submitter && e.submitter.id === 'withdrawSubmitButton') {
selectedEnrollInputs().forEach(input => {
input.checked = false;
const card = input.closest('[data-student-card]');
card?.classList.remove('is-selected');
if (card) {
setStudentFieldsEnabled(card, false);
}
});
}
if (e.submitter && e.submitter.id === 'enrollmentSubmitButton') {
selectedWithdrawInputs().forEach(input => {
input.checked = false;
});
}
const anyEnroll = selectedEnrollInputs().length > 0;
const anyWithdraw = selectedWithdrawInputs().length > 0;
syncPolicyAccepted();
if (!anyEnroll && !anyWithdraw) {
e.preventDefault();
alert('Please select at least one student to enroll or withdraw before submitting.');
return;
}
if (deadlinePassed && anyEnroll) {
e.preventDefault();
if (deadlineModal) deadlineModal.show();
return;
}
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) {
e.preventDefault();
}
}
});
}
[
parentPhoneInput,
parentStreetInput,
parentAptInput,
parentCityInput,
parentZipInput,
parentStateInput,
].forEach(input => {
if (!input) {
return;
}
input.addEventListener('input', function() {
if (input === parentPhoneInput) {
input.value = formatPhoneDisplay(input.value);
}
if (input === parentZipInput) {
input.value = digitsOnly(input.value).substring(0, 5);
}
validateParentContactField(input);
renderParentContactReview();
});
input.addEventListener('blur', function() {
shapeParentContactField(input);
validateParentContactField(input);
renderParentContactReview();
});
});
refreshEligibility().then(() => {
if (enrollmentStartStep >= 1 && startButton && !startButton.disabled && !<?= $flashError ? 'true' : 'false' ?>) {
openEnrollmentAtStep(enrollmentStartStep - 1);
}
}).catch(() => {
if (enrollmentStartStep >= 1 && startButton && !startButton.disabled && !<?= $flashError ? 'true' : 'false' ?>) {
openEnrollmentAtStep(enrollmentStartStep - 1);
}
// Keep server-rendered eligibility if refresh fails.
});
});
</script>
<?= $this->endSection() ?>