fix school year and enrollment steps
Tests / PHPUnit (push) Failing after 58s

This commit is contained in:
root
2026-08-15 21:15:05 -04:00
parent 4603d9ced2
commit 3cbfc40934
18 changed files with 1142 additions and 163 deletions
@@ -62,7 +62,7 @@ class SchoolYearClosingController extends BaseController
} }
service('schoolYearClosing')->start($id, $targetId, $this->userId()); service('schoolYearClosing')->start($id, $targetId, $this->userId());
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'End-year process started.'); return redirect()->to('/administrator/school-years/' . $id . '/closing/preview?close_flow=1')->with('success', 'End-year process started.');
} catch (Throwable $e) { } catch (Throwable $e) {
return redirect()->back()->with('error', $e->getMessage()); return redirect()->back()->with('error', $e->getMessage());
} }
@@ -72,7 +72,7 @@ class SchoolYearClosingController extends BaseController
{ {
try { try {
service('schoolYearClosing')->execute($id, $this->userId()); service('schoolYearClosing')->execute($id, $this->userId());
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'Carry-forward confirmed.'); return redirect()->to('/administrator/school-years/' . $id . '/closing/preview?close_flow=1')->with('success', 'Carry-forward confirmed.');
} catch (Throwable $e) { } catch (Throwable $e) {
return redirect()->back()->with('error', $e->getMessage()); return redirect()->back()->with('error', $e->getMessage());
} }
@@ -82,7 +82,7 @@ class SchoolYearClosingController extends BaseController
{ {
try { try {
service('schoolYearClosing')->complete($id, $this->userId()); service('schoolYearClosing')->complete($id, $this->userId());
return redirect()->to('/administrator/school-years')->with('success', 'School year ended and closed.'); return redirect()->to('/administrator/school-years/' . $id . '/closing/preview?close_flow=1')->with('success', 'School year ended and closed.');
} catch (Throwable $e) { } catch (Throwable $e) {
return redirect()->back()->with('error', $e->getMessage()); return redirect()->back()->with('error', $e->getMessage());
} }
@@ -52,6 +52,7 @@ class ParentReportCardController extends BaseController
$studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students))); $studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students)));
$ackMap = []; $ackMap = [];
$reportAvailableMap = $this->reportAvailabilityMap($studentIds, $schoolYear, $semester);
if (! empty($studentIds)) { if (! empty($studentIds)) {
$rows = $this->ackModel $rows = $this->ackModel
->where('parent_id', $parentId) ->where('parent_id', $parentId)
@@ -67,6 +68,7 @@ class ParentReportCardController extends BaseController
return view('parent/report_cards', [ return view('parent/report_cards', [
'students' => $students, 'students' => $students,
'ackMap' => $ackMap, 'ackMap' => $ackMap,
'reportAvailableMap' => $reportAvailableMap,
'schoolYear' => $schoolYear, 'schoolYear' => $schoolYear,
'semester' => $semester, 'semester' => $semester,
'isEditable' => ! $schoolYearContext->isReadonly(), 'isEditable' => ! $schoolYearContext->isReadonly(),
@@ -89,6 +91,11 @@ class ParentReportCardController extends BaseController
$schoolYear = trim($schoolYearContext->yearName()); $schoolYear = trim($schoolYearContext->yearName());
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? '')); $semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
if (! $this->reportExists((int) $studentId, $schoolYear, $semester)) {
return redirect()->to(site_url('parent/report-cards'))
->with('error', 'No report card exists for the selected school year and semester.');
}
if (! $schoolYearContext->isReadonly()) { if (! $schoolYearContext->isReadonly()) {
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [ $this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
'viewed_at' => date('Y-m-d H:i:s'), 'viewed_at' => date('Y-m-d H:i:s'),
@@ -136,6 +143,12 @@ class ParentReportCardController extends BaseController
$schoolYear = trim($schoolYearContext->yearName()); $schoolYear = trim($schoolYearContext->yearName());
$semester = trim((string) ($this->configModel->getConfig('semester') ?? '')); $semester = trim((string) ($this->configModel->getConfig('semester') ?? ''));
if (! $this->reportExists((int) $studentId, $schoolYear, $semester)) {
return redirect()->to(site_url('parent/report-cards'))
->with('error', 'No report card exists for the selected school year and semester.');
}
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [ $this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
'viewed_at' => $now, 'viewed_at' => $now,
@@ -173,6 +186,80 @@ class ParentReportCardController extends BaseController
return $parentId ?: null; return $parentId ?: null;
} }
protected function reportAvailabilityMap(array $studentIds, string $schoolYear, string $semester): array
{
$studentIds = array_values(array_filter(array_map('intval', $studentIds), static fn ($id) => $id > 0));
if (empty($studentIds) || $schoolYear === '') {
return [];
}
$builder = $this->db->table('semester_scores')
->select('student_id')
->whereIn('student_id', $studentIds)
->where('school_year', $schoolYear);
$semesterVariants = $this->semesterVariants($semester);
if (! empty($semesterVariants)) {
$builder->whereIn('semester', $semesterVariants);
}
$rows = $builder
->groupBy('student_id')
->get()
->getResultArray();
$map = [];
foreach ($rows as $row) {
$sid = (int) ($row['student_id'] ?? 0);
if ($sid > 0) {
$map[$sid] = true;
}
}
return $map;
}
protected function reportExists(int $studentId, string $schoolYear, string $semester): bool
{
if ($studentId <= 0 || $schoolYear === '') {
return false;
}
$builder = $this->db->table('semester_scores')
->select('id')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->limit(1);
$semesterVariants = $this->semesterVariants($semester);
if (! empty($semesterVariants)) {
$builder->whereIn('semester', $semesterVariants);
}
return (bool) $builder->get()->getRowArray();
}
protected function semesterVariants(string $semester): array
{
$raw = trim($semester);
if ($raw === '') {
return [];
}
$normalized = strtolower($raw);
if ($normalized === 'fall' || $normalized === 'first' || str_contains($normalized, 'fall') || str_contains($normalized, '1')) {
$values = ['Fall', 'fall', 'First', 'first', 'Semester 1', 'semester 1', 'Semester1', 'semester1', 'Sem 1', 'sem 1', '1', '01', 'S1', 's1'];
} elseif ($normalized === 'spring' || $normalized === 'second' || str_contains($normalized, 'spring') || str_contains($normalized, '2')) {
$values = ['Spring', 'spring', 'Second', 'second', 'Semester 2', 'semester 2', 'Semester2', 'semester2', 'Sem 2', 'sem 2', '2', '02', 'S2', 's2'];
} else {
$values = [$raw];
}
$values[] = $raw;
return array_values(array_unique($values));
}
protected function touchAcknowledgement( protected function touchAcknowledgement(
int $parentId, int $parentId,
int $studentId, int $studentId,
+172 -6
View File
@@ -391,7 +391,8 @@ class ParentController extends BaseController
'lastDayOfRegistration' => $this->lastDayOfRegistration, 'lastDayOfRegistration' => $this->lastDayOfRegistration,
'schoolStartDate' => $this->schoolStartDate, 'schoolStartDate' => $this->schoolStartDate,
'hasAcceptedSchoolPolicy' => $this->hasAcceptedPolicyForYear((int) $parentId, $selectedYear), 'hasAcceptedSchoolPolicy' => $this->hasAcceptedPolicyForYear((int) $parentId, $selectedYear),
'familyFinancialSummary' => $this->familyFinancialSummary((int) $parentId, $previousSchoolYear, $selectedYear), 'familyFinancialSummary' => $this->familyFinancialSummary((int) $parentId, $previousSchoolYear, $selectedYear, $students),
'enrollmentFeeSchedule' => $this->enrollmentFeeSchedule($selectedYear),
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
log_message('error', 'An error occurred in enrollClasses: ' . $e->getMessage()); log_message('error', 'An error occurred in enrollClasses: ' . $e->getMessage());
@@ -462,6 +463,12 @@ class ParentController extends BaseController
$submittedStudentIds = array_values(array_unique(array_filter(array_map('intval', (array) $enroll), static fn (int $id): bool => $id > 0))); $submittedStudentIds = array_values(array_unique(array_filter(array_map('intval', (array) $enroll), static fn (int $id): bool => $id > 0)));
$evaluations = []; $evaluations = [];
$errors = []; $errors = [];
$submittedEnrollmentContexts = [];
$studentInfoErrors = $this->updateEnrollmentStudentInfo($submittedStudentIds, (int) $parentId, $selectedYear);
if ($studentInfoErrors !== []) {
return redirect()->back()->withInput()->with('error', implode(' ', $studentInfoErrors));
}
foreach ($submittedStudentIds as $studentId) { foreach ($submittedStudentIds as $studentId) {
try { try {
@@ -524,8 +531,10 @@ class ParentController extends BaseController
->getRowArray(); ->getRowArray();
$studentName = trim((string) ($studentData[$studentId]['firstname'] ?? '') . ' ' . (string) ($studentData[$studentId]['lastname'] ?? '')) ?: 'Student ID ' . $studentId; $studentName = trim((string) ($studentData[$studentId]['firstname'] ?? '') . ' ' . (string) ($studentData[$studentId]['lastname'] ?? '')) ?: 'Student ID ' . $studentId;
if ($existingEnrollment) {
$isReturningReEnrollment = $this->isReturningReEnrollmentStudent((int)$studentId, $selectedYear); $isReturningReEnrollment = $this->isReturningReEnrollmentStudent((int)$studentId, $selectedYear);
$submittedEnrollmentContexts[$studentId] = $isReturningReEnrollment ? 're_enrollment' : 'first_enrollment';
if ($existingEnrollment) {
$targetEnrollmentStatus = $isReturningReEnrollment ? 'payment pending' : 'admission under review'; $targetEnrollmentStatus = $isReturningReEnrollment ? 'payment pending' : 'admission under review';
$targetAdmissionStatus = $isReturningReEnrollment ? 'accepted' : 'pending'; $targetAdmissionStatus = $isReturningReEnrollment ? 'accepted' : 'pending';
@@ -568,7 +577,6 @@ class ParentController extends BaseController
implode(' ', array_map('strval', $evaluation['rule_codes'] ?? [])) implode(' ', array_map('strval', $evaluation['rule_codes'] ?? []))
); );
} else { } else {
$isReturningReEnrollment = $this->isReturningReEnrollmentStudent((int)$studentId, $selectedYear);
$targetEnrollmentStatus = $isReturningReEnrollment ? 'payment pending' : 'admission under review'; $targetEnrollmentStatus = $isReturningReEnrollment ? 'payment pending' : 'admission under review';
$targetAdmissionStatus = $isReturningReEnrollment ? 'accepted' : 'pending'; $targetAdmissionStatus = $isReturningReEnrollment ? 'accepted' : 'pending';
@@ -616,6 +624,7 @@ class ParentController extends BaseController
if (! $this->db->transStatus()) { if (! $this->db->transStatus()) {
return redirect()->back()->withInput()->with('error', 'A database error occurred while submitting enrollment.'); return redirect()->back()->withInput()->with('error', 'A database error occurred while submitting enrollment.');
} }
} }
// $studentData now holds info for all students processed // $studentData now holds info for all students processed
@@ -710,6 +719,8 @@ class ParentController extends BaseController
return redirect()->to('/parent/enroll_classes'); return redirect()->to('/parent/enroll_classes');
} else { } else {
// Redirect to enrollment success page if there are enrollments // 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 Events::trigger('admissionUnderReview', $parentData, $studentData); //send notification for enrolled student
return redirect()->to('/parent/enroll_classes'); return redirect()->to('/parent/enroll_classes');
} }
@@ -739,6 +750,115 @@ class ParentController extends BaseController
return $this->filterEnrollmentPayloadByColumns($payload); return $this->filterEnrollmentPayloadByColumns($payload);
} }
private function updateEnrollmentStudentInfo(array $studentIds, int $parentId, string $schoolYear): array
{
$studentInfo = $this->request->getPost('student_info');
if (! is_array($studentInfo)) {
return [];
}
$submittedIds = array_fill_keys(array_map('intval', $studentIds), true);
$errors = [];
$updates = [];
foreach ($studentInfo as $studentId => $fields) {
$studentId = (int) $studentId;
if ($studentId <= 0 || ! isset($submittedIds[$studentId]) || ! is_array($fields)) {
continue;
}
$existing = $this->studentModel
->where('id', $studentId)
->where('parent_id', $parentId)
->first();
if (! is_array($existing)) {
$errors[] = 'Student ID ' . $studentId . ': student record was not found.';
continue;
}
$firstName = $this->normalizeEnrollmentStudentName((string) ($fields['firstname'] ?? ''));
$lastName = $this->normalizeEnrollmentStudentName((string) ($fields['lastname'] ?? ''));
$dob = trim((string) ($fields['dob'] ?? ''));
$gender = trim((string) ($fields['gender'] ?? ''));
$registrationGrade = trim((string) ($fields['registration_grade'] ?? ''));
$photoConsent = (string) ($fields['photo_consent'] ?? '');
$studentLabel = trim($firstName . ' ' . $lastName) ?: 'Student ID ' . $studentId;
try {
$this->validateNames($firstName);
$this->validateNames($lastName);
} catch (InvalidArgumentException $e) {
$errors[] = $studentLabel . ': ' . $e->getMessage();
continue;
}
if (! in_array($gender, ['Male', 'Female'], true)) {
$errors[] = $studentLabel . ': gender is required.';
continue;
}
if ($photoConsent !== '0' && $photoConsent !== '1') {
$errors[] = $studentLabel . ': photo consent is required.';
continue;
}
if ($registrationGrade === '' || mb_strlen($registrationGrade) > 50) {
$errors[] = $studentLabel . ': registration grade is required.';
continue;
}
$dobObj = \DateTimeImmutable::createFromFormat('!Y-m-d', $dob, new \DateTimeZone('UTC'));
$dobErrors = \DateTimeImmutable::getLastErrors();
$dobWarningCount = is_array($dobErrors) ? (int) ($dobErrors['warning_count'] ?? 0) : 0;
$dobErrorCount = is_array($dobErrors) ? (int) ($dobErrors['error_count'] ?? 0) : 0;
if ($dobObj === false || $dobWarningCount > 0 || $dobErrorCount > 0) {
$errors[] = $studentLabel . ': date of birth must use YYYY-MM-DD format.';
continue;
}
$validation = $this->validateDobAge(
$dob,
$this->registrationMinimumAgeDeadline($schoolYear),
5,
18,
$this->schoolYearAgeDeadline($schoolYear)
);
if (! $validation['isValid']) {
$errors[] = $studentLabel . ': ' . $validation['message'] . '.';
continue;
}
$updates[$studentId] = [
'firstname' => $firstName,
'lastname' => $lastName,
'dob' => $dobObj->format('Y-m-d'),
'age' => $this->calculateAgeAsOfSchoolYearStartYear($dobObj->format('Y-m-d'), $schoolYear),
'gender' => $gender,
'registration_grade' => $registrationGrade,
'photo_consent' => (int) $photoConsent,
];
}
if ($errors !== []) {
return $errors;
}
foreach ($updates as $studentId => $payload) {
if (! $this->studentModel->update($studentId, $payload)) {
$errors[] = 'Student ID ' . $studentId . ': student information could not be updated.';
}
}
return $errors;
}
private function normalizeEnrollmentStudentName(string $name): string
{
$name = trim(preg_replace('/\s+/', ' ', $name) ?? '');
return mb_convert_case($name, MB_CASE_TITLE, 'UTF-8');
}
private function exceptionReasonFromEvaluation(array $evaluation): ?string private function exceptionReasonFromEvaluation(array $evaluation): ?string
{ {
if (! empty($evaluation['admin_exception'])) { if (! empty($evaluation['admin_exception'])) {
@@ -1625,16 +1745,16 @@ class ParentController extends BaseController
return 'Contact administration'; return 'Contact administration';
} }
private function familyFinancialSummary(int $parentId, ?string $previousSchoolYear, string $selectedYear): array private function familyFinancialSummary(int $parentId, ?string $previousSchoolYear, string $selectedYear, array $students = []): array
{ {
$schoolYearConfig = $this->schoolYearConfig($selectedYear); $schoolYearConfig = $this->schoolYearConfig($selectedYear);
$carryOver = $previousSchoolYear !== null ? $this->invoiceBalanceForParent($parentId, $previousSchoolYear) : 0.0; $carryOver = $previousSchoolYear !== null ? $this->invoiceBalanceForParent($parentId, $previousSchoolYear) : 0.0;
$currentBalance = $this->invoiceBalanceForParent($parentId, $selectedYear); $currentBalance = $this->invoiceBalanceForParent($parentId, $selectedYear);
$registrationFee = round((float) ($schoolYearConfig['registration_fee'] ?? 0), 2); $registrationFee = round((float) ($schoolYearConfig['registration_fee'] ?? 0), 2);
$tuitionDue = round((float) ($schoolYearConfig['tuition_due_at_registration'] ?? 0), 2); $tuitionDue = $this->enrollmentTuitionDue($students);
$mandatoryFees = round((float) ($schoolYearConfig['mandatory_fees'] ?? 0), 2); $mandatoryFees = round((float) ($schoolYearConfig['mandatory_fees'] ?? 0), 2);
$behavior = (string) ($schoolYearConfig['carry_over_balance_behavior'] ?? 'submission_blocked_until_payment'); $behavior = (string) ($schoolYearConfig['carry_over_balance_behavior'] ?? 'submission_blocked_until_payment');
$amountDue = max(0.0, $carryOver) + $registrationFee + $tuitionDue + $mandatoryFees; $amountDue = max(0.0, $carryOver) + max(0.0, $currentBalance) + $registrationFee + $tuitionDue + $mandatoryFees;
return [ return [
'currency' => '$', 'currency' => '$',
@@ -1650,6 +1770,52 @@ class ParentController extends BaseController
]; ];
} }
private function enrollmentTuitionDue(array $students): float
{
$tuitionStudents = [];
foreach ($students as $student) {
$status = strtolower(trim((string) ($student['enrollment_status'] ?? '')));
$eligibilityMessage = is_array($student['enrollment_eligibility_message'] ?? null)
? $student['enrollment_eligibility_message']
: ['blocking' => false];
if ($status !== 'not enrolled' || ! empty($eligibilityMessage['blocking'])) {
continue;
}
$evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : [];
$tuitionStudents[] = [
'student_id' => (int) ($student['id'] ?? $student['student_id'] ?? 0),
'class_section_id' => (int) (
$evaluation['assigned_class_section_id']
?? $student['class_section_id']
?? 0
),
];
}
if ($tuitionStudents === []) {
return 0.0;
}
return (new FeeCalculationService())->calculateEnrollmentTuition($tuitionStudents);
}
private function enrollmentFeeSchedule(string $selectedYear): array
{
$schoolYearConfig = $this->schoolYearConfig($selectedYear);
return [
'currency' => '$',
'first_student_fee' => round((float) ($this->configModel->getConfig('first_student_fee') ?? 380), 2),
'second_student_fee' => round((float) ($this->configModel->getConfig('second_student_fee') ?? 280), 2),
'registration_fee' => round((float) ($schoolYearConfig['registration_fee'] ?? 0), 2),
'tuition_due_at_registration' => round((float) ($schoolYearConfig['tuition_due_at_registration'] ?? 0), 2),
'mandatory_fees' => round((float) ($schoolYearConfig['mandatory_fees'] ?? 0), 2),
];
}
private function financialPolicyMessage(string $behavior, string $configured): string private function financialPolicyMessage(string $behavior, string $configured): string
{ {
$configured = trim($configured); $configured = trim($configured);
@@ -35,6 +35,7 @@ class ParentFinancialAidController extends BaseController
'students' => $students, 'students' => $students,
'requests' => $requests, 'requests' => $requests,
'openRequest' => $model->openRequestForParent($parentId, $schoolYear), 'openRequest' => $model->openRequestForParent($parentId, $schoolYear),
'existingRequest' => $model->requestForParentYear($parentId, $schoolYear),
]); ]);
} }
@@ -47,8 +48,8 @@ class ParentFinancialAidController extends BaseController
$schoolYear = (string) ((new ConfigurationModel())->getConfig('school_year') ?? ''); $schoolYear = (string) ((new ConfigurationModel())->getConfig('school_year') ?? '');
$model = new FinancialAidRequestModel(); $model = new FinancialAidRequestModel();
if ($model->openRequestForParent($parentId, $schoolYear) !== null) { if ($model->requestForParentYear($parentId, $schoolYear) !== null) {
return redirect()->back()->with('error', 'You already have an open financial aid request for this school year.'); return redirect()->back()->with('error', 'You can submit only one financial aid application per school year.');
} }
$studentIds = array_values(array_unique(array_filter(array_map('intval', (array) $this->request->getPost('student_ids'))))); $studentIds = array_values(array_unique(array_filter(array_map('intval', (array) $this->request->getPost('student_ids')))));
+22 -14
View File
@@ -1224,12 +1224,6 @@ $scoresEndY = $pdf->GetY();
public function generateSingleReport($studentId) public function generateSingleReport($studentId)
{ {
// Prevent Debug Toolbar from corrupting PDF
if (ENVIRONMENT !== 'production') {
// Turn off toolbar manually by unsetting the collector
unset(service('toolbar')->collectors['CodeIgniter\Debug\Toolbar\Collectors\Timers']);
}
$download = $this->request->getGet('download') === '1'; $download = $this->request->getGet('download') === '1';
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
$semester = $this->request->getGet('semester') ?? $this->semester; $semester = $this->request->getGet('semester') ?? $this->semester;
@@ -1237,7 +1231,7 @@ $scoresEndY = $pdf->GetY();
$data = $this->prepareStudentReportData((int)$studentId, (string)$schoolYear, (string)$semester); $data = $this->prepareStudentReportData((int)$studentId, (string)$schoolYear, (string)$semester);
if (!$data) { if (!$data) {
return "Student not found or missing scores."; return $this->missingReportResponse((int)$studentId, (string)$schoolYear, (string)$semester);
} }
$data['report_date'] = $reportDate['iso']; $data['report_date'] = $reportDate['iso'];
$data['report_date_display'] = $reportDate['display']; $data['report_date_display'] = $reportDate['display'];
@@ -1264,12 +1258,6 @@ $scoresEndY = $pdf->GetY();
public function generateClassReport($classSectionId) public function generateClassReport($classSectionId)
{ {
// Disable toolbar output to prevent PDF corruption
if (ENVIRONMENT !== 'production') {
unset(service('toolbar')->collectors['CodeIgniter\Debug\Toolbar\Collectors\Timers']);
}
$download = $this->request->getGet('download') === '1'; $download = $this->request->getGet('download') === '1';
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
$semester = $this->request->getGet('semester') ?? $this->semester; $semester = $this->request->getGet('semester') ?? $this->semester;
@@ -1290,10 +1278,11 @@ $scoresEndY = $pdf->GetY();
->findAll(); ->findAll();
if (!$scores) { if (!$scores) {
return "No students found for this class section."; return $this->missingReportResponse(null, (string)$schoolYear, (string)$semester, (int)$classSectionId);
} }
$pdf = new \FPDF('P', 'mm', 'Letter'); $pdf = new \FPDF('P', 'mm', 'Letter');
$generatedCount = 0;
foreach ($scores as $score) { foreach ($scores as $score) {
$studentId = $score['student_id']; $studentId = $score['student_id'];
@@ -1304,9 +1293,14 @@ $scoresEndY = $pdf->GetY();
$data['report_date'] = $reportDate['iso']; $data['report_date'] = $reportDate['iso'];
$data['report_date_display'] = $reportDate['display']; $data['report_date_display'] = $reportDate['display'];
$this->formatReportPDF($pdf, $data); $this->formatReportPDF($pdf, $data);
$generatedCount++;
} }
} }
if ($generatedCount === 0) {
return $this->missingReportResponse(null, (string)$schoolYear, (string)$semester, (int)$classSectionId);
}
$filename = 'ClassReport_Section_' . $classSectionId . '.pdf'; $filename = 'ClassReport_Section_' . $classSectionId . '.pdf';
// Clean any existing output buffer // Clean any existing output buffer
@@ -1324,6 +1318,20 @@ $scoresEndY = $pdf->GetY();
->setBody($pdfContent); ->setBody($pdfContent);
} }
protected function missingReportResponse(?int $studentId, string $schoolYear, string $semester, ?int $classSectionId = null)
{
$target = $studentId !== null
? 'student #' . $studentId
: 'class section #' . (int)$classSectionId;
$period = trim($schoolYear . ($semester !== '' ? ' ' . $semester : ''));
$message = 'No report card exists for ' . $target . ($period !== '' ? ' for ' . $period : '') . '.';
return $this->response
->setStatusCode(404)
->setHeader('Content-Type', 'text/plain; charset=UTF-8')
->setBody($message . "\nPlease select a school year and semester that has saved scores before viewing or signing the report card.");
}
protected function normalizeSemester(?string $input): string protected function normalizeSemester(?string $input): string
{ {
@@ -0,0 +1,61 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddUniqueFinancialAidParentYear extends Migration
{
private string $table = 'financial_aid_requests';
private string $index = 'uniq_financial_aid_parent_year';
public function up()
{
if (! $this->db->tableExists($this->table) || $this->indexExists()) {
return;
}
$duplicate = $this->db->table($this->table)
->select('parent_id, school_year, COUNT(*) AS total', false)
->groupBy(['parent_id', 'school_year'])
->having('total >', 1)
->limit(1)
->get()
->getRowArray();
if ($duplicate !== null) {
log_message('warning', 'Skipped unique financial aid parent/year index because duplicate requests already exist.');
return;
}
$this->db->query(sprintf(
'CREATE UNIQUE INDEX %s ON %s (parent_id, school_year)',
$this->db->escapeIdentifiers($this->index),
$this->db->escapeIdentifiers($this->table)
));
}
public function down()
{
if (! $this->db->tableExists($this->table) || ! $this->indexExists()) {
return;
}
$this->db->query(sprintf(
'DROP INDEX %s ON %s',
$this->db->escapeIdentifiers($this->index),
$this->db->escapeIdentifiers($this->table)
));
}
private function indexExists(): bool
{
foreach ($this->db->getIndexData($this->table) as $index) {
if (($index->name ?? '') === $this->index) {
return true;
}
}
return false;
}
}
+19 -4
View File
@@ -78,6 +78,20 @@ class SchoolEventListener
public function handleAdmissionUnderReview(array $data, array $studentdata) public function handleAdmissionUnderReview(array $data, array $studentdata)
{ {
$parentName = trim(($data['firstname'] ?? '') . ' ' . ($data['lastname'] ?? '')); $parentName = trim(($data['firstname'] ?? '') . ' ' . ($data['lastname'] ?? ''));
$enrollmentContext = (string) ($data['enrollment_context'] ?? 'first_enrollment');
$isReEnrollment = $enrollmentContext === 're_enrollment';
$isMixedEnrollment = $enrollmentContext === 'mixed';
$emailSubject = $isReEnrollment
? 'Re-Enrollment Submission Received'
: ($isMixedEnrollment ? 'Enrollment Submission Received' : 'Admission Application Under Review');
$notificationTitle = $isReEnrollment
? 'Re-Enrollment Received'
: ($isMixedEnrollment ? 'Enrollment Submission Received' : 'Admission Under Review');
$notificationMessage = $isReEnrollment
? 'Your re-enrollment submission has been received. Please check your email for details.'
: ($isMixedEnrollment
? 'Your enrollment submission has been received. Please check your email for details.'
: 'Your admission application is under review. We will notify you once it is approved.');
$studentNames = []; $studentNames = [];
foreach ($studentdata as $student) { foreach ($studentdata as $student) {
@@ -89,15 +103,16 @@ class SchoolEventListener
$emailData = [ $emailData = [
'parentName' => $parentName, 'parentName' => $parentName,
'studentName' => $studentNameData, 'studentName' => $studentNameData,
'enrollmentContext' => $enrollmentContext,
'portalLink' => base_url('/login'), 'portalLink' => base_url('/login'),
'title' => 'Admission Application Under Review', // used by layout <title>, optional 'title' => $emailSubject,
]; ];
// In-app notification (unchanged) // In-app notification (unchanged)
$this->notifyUser( $this->notifyUser(
$data['user_id'], $data['user_id'],
'Admission Under Review', $notificationTitle,
'Your admission application is under review. We will notify you once it is approved.', $notificationMessage,
['in_app'], ['in_app'],
'registration' 'registration'
); );
@@ -109,7 +124,7 @@ class SchoolEventListener
// Send via EmailService (sets HTML + alt body) // Send via EmailService (sets HTML + alt body)
$ok = $this->emailService->send( $ok = $this->emailService->send(
$data['email'], $data['email'],
'Admission Application Under Review', $emailSubject,
$emailBody, $emailBody,
'registration' 'registration'
); );
+8
View File
@@ -38,4 +38,12 @@ class FinancialAidRequestModel extends Model
->orderBy('id', 'DESC') ->orderBy('id', 'DESC')
->first(); ->first();
} }
public function requestForParentYear(int $parentId, string $schoolYear): ?array
{
return $this->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->orderBy('id', 'DESC')
->first();
}
} }
@@ -8,7 +8,7 @@ use DateTimeInterface;
final class EnrollmentRegistrationEmailService final class EnrollmentRegistrationEmailService
{ {
public const TEMPLATE_VERSION = 'phase5_consolidated_v1'; public const TEMPLATE_VERSION = 'phase5_consolidated_v2';
public function __construct( public function __construct(
private readonly BaseConnection $db, private readonly BaseConnection $db,
@@ -149,7 +149,7 @@ final class EnrollmentRegistrationEmailService
$previousYear = $this->previousSchoolYearName($schoolYearName); $previousYear = $this->previousSchoolYearName($schoolYearName);
$deadline = $this->dateText($schoolYear['registration_deadline_at'] ?? $schoolYear['registration_ends_on'] ?? null); $deadline = $this->dateText($schoolYear['registration_deadline_at'] ?? $schoolYear['registration_ends_on'] ?? null);
$opens = $this->dateText($schoolYear['registration_opens_at'] ?? $schoolYear['registration_starts_on'] ?? null); $opens = $this->dateText($schoolYear['registration_opens_at'] ?? $schoolYear['registration_starts_on'] ?? null);
$students = []; $studentRows = [];
$studentIds = []; $studentIds = [];
foreach ($family['students'] as $student) { foreach ($family['students'] as $student) {
@@ -159,20 +159,19 @@ final class EnrollmentRegistrationEmailService
} }
$evaluation = $this->transitionService->evaluate($studentId, $previousYear, $schoolYearName, 'parent'); $evaluation = $this->transitionService->evaluate($studentId, $previousYear, $schoolYearName, 'parent');
$students[] = $this->studentSection($student, $evaluation, $opens, $deadline); $studentRows[] = $this->studentRow($student, $evaluation, $opens, $deadline);
$studentIds[] = $studentId; $studentIds[] = $studentId;
} }
$financial = $this->financialSection((int) $family['parent_user_id'], $schoolYear, $previousYear); $financial = $this->financialSection((int) $family['parent_user_id'], $schoolYear, $previousYear);
$subject = 'Registration for ' . $schoolYearName . ' Is Now Open'; $subject = 'Registration for ' . $schoolYearName . ' Is Now Open';
$bodyHtml = '<p>Dear ' . esc($family['name']) . ',</p>' $bodyHtml = '<p>Dear ' . esc($family['name']) . ',</p>'
. '<p>We are pleased to welcome your family to the registration process for the ' . esc($schoolYearName) . ' school year.</p>' . '<p>Registration for the ' . esc($schoolYearName) . ' school year is now open. Please review the student summary below and complete the portal steps before ' . esc($deadline) . '.</p>'
. '<p>Registration opens on ' . esc($opens) . ' and closes on ' . esc($deadline) . '.</p>' . $this->studentsTable($studentRows)
. '<p>Please review the information below for each of your children, including the deliberation decision, registration eligibility, expected academic placement, and any required action.</p>'
. implode('', $students)
. $financial . $financial
. '<p>To complete registration for each eligible student:</p>' . '<h3>Registration Steps</h3>'
. '<ol><li>Sign in to the parent portal.</li><li>Review and update student information.</li><li>Upload required documents.</li><li>Review and acknowledge school policies.</li><li>Review tuition, fees, and any carry-over balance.</li><li>Submit registration before ' . esc($deadline) . '.</li></ol>' . '<p>Re-enrollment opens on ' . esc($opens) . ' and closes on ' . esc($deadline) . '. Complete re-enrollment for each eligible student before the deadline to secure their enrollment for the upcoming school year.</p>'
. '<ol><li>Sign in to the parent portal.</li><li>Review student information and upload any required documents.</li><li>Acknowledge school policies.</li><li>Review tuition, fees, and any carry-over balance.</li><li>Submit Enrollment.</li></ol>'
. '<p>Registration portal: <a href="' . esc(site_url('parent/enroll_classes')) . '">' . esc(site_url('parent/enroll_classes')) . '</a></p>' . '<p>Registration portal: <a href="' . esc(site_url('parent/enroll_classes')) . '">' . esc(site_url('parent/enroll_classes')) . '</a></p>'
. '<p>For assistance, please contact the school administration.</p>' . '<p>For assistance, please contact the school administration.</p>'
. '<p>Sincerely,<br>Al Rahma Sunday School<br>School Administration</p>'; . '<p>Sincerely,<br>Al Rahma Sunday School<br>School Administration</p>';
@@ -190,7 +189,19 @@ final class EnrollmentRegistrationEmailService
]; ];
} }
private function studentSection(array $student, array $evaluation, string $opens, string $deadline): string private function studentsTable(array $studentRows): string
{
if ($studentRows === []) {
return '<p>No eligible linked students were found for this email.</p>';
}
return '<table style="width:100%; border-collapse:collapse; margin:12px 0 18px; font-size:14px;">'
. '<tbody>'
. implode('', $studentRows)
. '</tbody></table>';
}
private function studentRow(array $student, array $evaluation, string $opens, string $deadline): string
{ {
$name = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student'; $name = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student';
$decision = DeliberationDecision::display((string) ($evaluation['deliberation_decision'] ?? '')); $decision = DeliberationDecision::display((string) ($evaluation['deliberation_decision'] ?? ''));
@@ -199,12 +210,12 @@ final class EnrollmentRegistrationEmailService
$requiredAction = $this->requiredAction($evaluation, $deadline); $requiredAction = $this->requiredAction($evaluation, $deadline);
$message = $this->decisionMessage($name, $evaluation, $opens, $deadline); $message = $this->decisionMessage($name, $evaluation, $opens, $deadline);
return '<h3>' . esc($name) . '</h3>' return '<tr>'
. '<p><strong>Deliberation decision:</strong> ' . esc($decision ?: 'Pending') . '<br>' . '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;">' . esc($name) . '</td>'
. '<strong>Registration status:</strong> ' . esc($status) . '<br>' . '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;">' . esc($decision ?: 'Pending') . '</td>'
. '<strong>Expected placement:</strong> ' . esc($placement) . '</p>' . '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;"><strong>' . esc($status) . '</strong><br>' . esc($placement) . '</td>'
. '<p>' . esc($message) . '</p>' . '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;">' . esc($message) . '<br><strong>' . esc($requiredAction) . '</strong></td>'
. '<p><strong>Required action:</strong> ' . esc($requiredAction) . '</p>'; . '</tr>';
} }
private function decisionMessage(string $name, array $evaluation, string $opens, string $deadline): string private function decisionMessage(string $name, array $evaluation, string $opens, string $deadline): string
@@ -212,9 +223,7 @@ final class EnrollmentRegistrationEmailService
if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) { if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) {
$grade = $this->currentGradeText($evaluation); $grade = $this->currentGradeText($evaluation);
return 'We are pleased to inform you that ' . $name . ' has successfully passed ' . $grade . '. ' return $name . ' has successfully passed ' . $grade . '.';
. 'Re-enrollment for the new school year will open on ' . $opens . '. Please make sure to re-enroll your child before ' . $deadline . ' to secure their enrollment for the upcoming school year. '
. 'To complete the process, sign in to the parent portal, review the students information, submit the required documents, acknowledge the school policies, and complete any applicable payment steps.';
} }
if (($evaluation['blockers'] ?? []) !== []) { if (($evaluation['blockers'] ?? []) !== []) {
+6 -1
View File
@@ -8,6 +8,11 @@ use App\Models\ClassSectionModel;
class FeeCalculationService class FeeCalculationService
{ {
public function calculateEnrollmentTuition(array $students): float
{
return round($this->calculateTotalTuitionFee($students), 2);
}
public function calculateRefund(array $students, int $parentId): float public function calculateRefund(array $students, int $parentId): float
{ {
$configModel = new ConfigurationModel(); $configModel = new ConfigurationModel();
@@ -145,7 +150,7 @@ class FeeCalculationService
// ✅ Pre-fetch and assign grade/class section names before sorting // ✅ Pre-fetch and assign grade/class section names before sorting
foreach ($students as &$student) { foreach ($students as &$student) {
$gradeName = $classSectionModel->getClassSectionNameBySectionId($student['class_section_id']); $gradeName = $classSectionModel->getClassSectionNameBySectionId((int) ($student['class_section_id'] ?? 0));
$student['grade'] = strtoupper(trim($gradeName)); $student['grade'] = strtoupper(trim($gradeName));
} }
unset($student); // break reference unset($student); // break reference
+39 -2
View File
@@ -1,12 +1,27 @@
<?= $this->extend('layout/email_layout') ?> <?= $this->extend('layout/email_layout') ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<h2 style="font-size:22px; font-weight:bold;">Admission Under Review</h2> <?php
$enrollmentContext = (string) ($enrollmentContext ?? 'first_enrollment');
$isReEnrollment = $enrollmentContext === 're_enrollment';
$isMixedEnrollment = $enrollmentContext === 'mixed';
$studentCount = is_array($studentName) ? count($studentName) : 1;
$applicationWord = $studentCount === 1 ? 'submission' : 'submissions';
?>
<h2 style="font-size:22px; font-weight:bold;">
<?= $isReEnrollment ? 'Re-Enrollment Received' : ($isMixedEnrollment ? 'Enrollment Submission Received' : 'Admission Under Review') ?>
</h2>
<p style="font-size:16px;">Dear <?= $parentName ?>,</p> <p style="font-size:16px;">Dear <?= $parentName ?>,</p>
<?php if (is_array($studentName)): ?> <?php if (is_array($studentName)): ?>
<p style="font-size:16px;"> <p style="font-size:16px;">
<?php if ($isReEnrollment): ?>
Thank you for submitting re-enrollment for the following <?= count($studentName) ?> students:
<?php elseif ($isMixedEnrollment): ?>
Thank you for submitting enrollment updates for the following <?= count($studentName) ?> students:
<?php else: ?>
Thank you for submitting the enrollment application for the following <?= count($studentName) ?> students: Thank you for submitting the enrollment application for the following <?= count($studentName) ?> students:
<?php endif; ?>
</p> </p>
<ul style="font-size:16px;"> <ul style="font-size:16px;">
<?php foreach ($studentName as $student): ?> <?php foreach ($studentName as $student): ?>
@@ -15,18 +30,40 @@
</ul> </ul>
<?php else: ?> <?php else: ?>
<p style="font-size:16px;"> <p style="font-size:16px;">
<?php if ($isReEnrollment): ?>
Thank you for submitting re-enrollment for <?= $studentName ?>.
<?php elseif ($isMixedEnrollment): ?>
Thank you for submitting the enrollment update for <?= $studentName ?>.
<?php else: ?>
Thank you for submitting the enrollment application for <?= $studentName ?>. Thank you for submitting the enrollment application for <?= $studentName ?>.
<?php endif; ?>
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if ($isReEnrollment): ?>
<p style="font-size:16px;">
Your re-enrollment <?= $applicationWord ?> <?= $studentCount === 1 ? 'has' : 'have' ?> been received. The <?= $studentCount === 1 ? 'student is' : 'students are' ?> approved to continue, and the next step is to complete any required payment or follow-up items shown in the parent portal.
</p>
<p style="font-size:16px;">
You will be notified if any additional information is needed.
</p>
<?php elseif ($isMixedEnrollment): ?>
<p style="font-size:16px;">
Your enrollment <?= $applicationWord ?> <?= $studentCount === 1 ? 'is' : 'are' ?> being processed. New student applications may require admissions review, while returning student re-enrollment may move directly to the next required portal step.
</p>
<p style="font-size:16px;">
You will be notified immediately once an update is available.
</p>
<?php else: ?>
<p style="font-size:16px;"> <p style="font-size:16px;">
Your application<?= is_array($studentName) ? 's are' : ' is' ?> currently under review by our admissions committee. This process typically takes 12 business days. Your application<?= is_array($studentName) ? 's are' : ' is' ?> currently under review by our admissions committee. This process typically takes 12 business days.
</p> </p>
<p style="font-size:16px;"> <p style="font-size:16px;">
You will be notified immediately once a decision has been made. You will be notified immediately once a decision has been made.
</p> </p>
<?php endif; ?>
<p style="font-size:16px;"> <p style="font-size:16px;">
You can check your application status at any time through the parent portal. You can check your <?= $isReEnrollment ? 're-enrollment' : 'application' ?> status at any time through the parent portal.
</p> </p>
<p> <p>
<!-- make sure helper('url') is loaded --> <!-- make sure helper('url') is loaded -->
+25 -3
View File
@@ -111,9 +111,31 @@
.custom-navbar .dropdown-item:hover { color: var(--app-primary) !important; } .custom-navbar .dropdown-item:hover { color: var(--app-primary) !important; }
.custom-navbar .dropdown-menu { z-index: 2050 !important; } .custom-navbar .dropdown-menu { z-index: 2050 !important; }
/* Improve toggler contrast on dark menus */ .custom-navbar .navbar-toggler {
body[data-app-menu-mode="dark"] .custom-navbar .navbar-toggler { border-color: var(--mgmt-menu-text); } background: #fff;
body[data-app-menu-mode="dark"] .custom-navbar .navbar-toggler-icon { filter: invert(1) brightness(2); } border: 1px solid rgba(15, 23, 42, 0.18);
border-radius: 0.5rem;
padding: 0.35rem 0.55rem;
}
.custom-navbar .navbar-toggler-icon {
position: relative;
width: 1.65rem;
height: 1.35rem;
background-image: none !important;
filter: none !important;
}
.custom-navbar .navbar-toggler-icon::before {
content: "";
position: absolute;
left: 0.1rem;
right: 0.1rem;
top: 50%;
height: 0.18rem;
border-radius: 999px;
background: #0f172a;
transform: translateY(-50%);
box-shadow: 0 -0.45rem 0 #0f172a, 0 0.45rem 0 #0f172a;
}
</style> </style>
<!-- Calendar JS (optional early load) --> <!-- Calendar JS (optional early load) -->
+266 -32
View File
@@ -11,7 +11,7 @@
.enrollment-stepper { .enrollment-stepper {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: .5rem; gap: .5rem;
} }
@@ -162,6 +162,7 @@ $nowObj = new DateTime('now', new DateTimeZone($tz));
$deadlinePassed = $nowObj > $deadlineObj; $deadlinePassed = $nowObj > $deadlineObj;
$hasAcceptedSchoolPolicy = (bool) ($hasAcceptedSchoolPolicy ?? false); $hasAcceptedSchoolPolicy = (bool) ($hasAcceptedSchoolPolicy ?? false);
$familyFinancialSummary = is_array($familyFinancialSummary ?? null) ? $familyFinancialSummary : []; $familyFinancialSummary = is_array($familyFinancialSummary ?? null) ? $familyFinancialSummary : [];
$enrollmentFeeSchedule = is_array($enrollmentFeeSchedule ?? null) ? $enrollmentFeeSchedule : [];
$money = static function ($amount) use ($familyFinancialSummary): string { $money = static function ($amount) use ($familyFinancialSummary): string {
$currency = (string) ($familyFinancialSummary['currency'] ?? '$'); $currency = (string) ($familyFinancialSummary['currency'] ?? '$');
return $currency . number_format((float) $amount, 2); return $currency . number_format((float) $amount, 2);
@@ -220,12 +221,12 @@ foreach (($students ?? []) as $student) {
<div class="border rounded p-3 mb-3 bg-light"> <div class="border rounded p-3 mb-3 bg-light">
<div class="fw-semibold mb-2">Family Account Information</div> <div class="fw-semibold mb-2">Family Account Information</div>
<div class="row g-2"> <div class="row g-2">
<div class="col-md-4"><span class="text-muted">Previous-year carry-over balance:</span> <strong><?= esc($money($familyFinancialSummary['carry_over_balance'] ?? 0)) ?></strong></div> <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">Registration fee:</span> <strong><?= esc($money($familyFinancialSummary['registration_fee'] ?? 0)) ?></strong></div> <div class="col-md-4"><span class="text-muted">Registration fee:</span> <strong data-financial-value="registration_fee"><?= esc($money($familyFinancialSummary['registration_fee'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Tuition due now:</span> <strong><?= esc($money($familyFinancialSummary['tuition_due_at_registration'] ?? 0)) ?></strong></div> <div class="col-md-4"><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="col-md-4"><span class="text-muted">Mandatory fees:</span> <strong><?= esc($money($familyFinancialSummary['mandatory_fees'] ?? 0)) ?></strong></div> <div class="col-md-4"><span class="text-muted">Mandatory fees:</span> <strong data-financial-value="mandatory_fees"><?= esc($money($familyFinancialSummary['mandatory_fees'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Current-year account balance:</span> <strong><?= esc($money($familyFinancialSummary['current_balance'] ?? 0)) ?></strong></div> <div class="col-md-4"><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="col-md-4"><span class="text-muted">Total currently due:</span> <strong><?= esc($money($familyFinancialSummary['amount_due'] ?? 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 if (!empty($familyFinancialSummary['policy_message'])): ?> <?php if (!empty($familyFinancialSummary['policy_message'])): ?>
<div class="small text-muted mt-2"><?= esc($familyFinancialSummary['policy_message']) ?></div> <div class="small text-muted mt-2"><?= esc($familyFinancialSummary['policy_message']) ?></div>
@@ -244,7 +245,7 @@ foreach (($students ?? []) as $student) {
<div class="col-lg"> <div class="col-lg">
<div class="fw-semibold">Enrollment process</div> <div class="fw-semibold">Enrollment process</div>
<div class="text-muted small"> <div class="text-muted small">
Select students, review policy information, then submit enrollment. Review student information, acknowledge policies, review tuition and balances, then submit enrollment.
</div> </div>
</div> </div>
<div class="col-lg-auto"> <div class="col-lg-auto">
@@ -322,19 +323,20 @@ foreach (($students ?? []) as $student) {
<div class="modal-dialog modal-xl modal-dialog-scrollable modal-fullscreen-sm-down"> <div class="modal-dialog modal-xl modal-dialog-scrollable modal-fullscreen-sm-down">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-success text-white"> <div class="modal-header bg-success text-white">
<h5 class="modal-title" id="enrollmentFlowModalLabel">Enroll your childs</h5> <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> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="enrollment-stepper mb-3" aria-label="Enrollment steps"> <div class="enrollment-stepper mb-3" aria-label="Enrollment steps">
<div class="enrollment-step is-active" data-step-indicator="0">Students</div> <div class="enrollment-step is-active" data-step-indicator="0">Student Info</div>
<div class="enrollment-step" data-step-indicator="1">Policy</div> <div class="enrollment-step" data-step-indicator="1">Policies</div>
<div class="enrollment-step" data-step-indicator="2">Submit</div> <div class="enrollment-step" data-step-indicator="2">Tuition</div>
<div class="enrollment-step" data-step-indicator="3">Submit</div>
</div> </div>
<div data-step-panel="0"> <div data-step-panel="0">
<h6 class="fw-semibold">Select student names</h6> <h6 class="fw-semibold">Review student information</h6>
<div class="text-muted small mb-3">Tap each student you want to enroll for <?= esc($selectedYear ?? 'the selected school year') ?>.</div> <div class="text-muted small mb-3">Select each student you want to enroll for <?= esc($selectedYear ?? 'the selected school year') ?> and update their information if needed.</div>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<?php foreach ($students as $student): ?> <?php foreach ($students as $student): ?>
<?php <?php
@@ -353,6 +355,10 @@ foreach (($students ?? []) as $student) {
<div class="student-select-card <?= $canEnroll ? '' : 'is-disabled' ?>" <div class="student-select-card <?= $canEnroll ? '' : 'is-disabled' ?>"
data-student-card data-student-card
data-student-id="<?= esc($student['id']) ?>" data-student-id="<?= esc($student['id']) ?>"
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-selectable="<?= $canEnroll ? '1' : '0' ?>"> data-selectable="<?= $canEnroll ? '1' : '0' ?>">
<div class="d-flex gap-3 align-items-start"> <div class="d-flex gap-3 align-items-start">
<span class="student-select-icon" aria-hidden="true"><i class="bi bi-check2"></i></span> <span class="student-select-icon" aria-hidden="true"><i class="bi bi-check2"></i></span>
@@ -361,7 +367,55 @@ foreach (($students ?? []) as $student) {
<div class="fw-semibold"><?= esc($studentName) ?></div> <div class="fw-semibold"><?= esc($studentName) ?></div>
<?= $statusBadge($student['enrollment_status'] ?? '') ?> <?= $statusBadge($student['enrollment_status'] ?? '') ?>
</div> </div>
<div class="small text-muted">Grade: <?= esc($gradeLabel) ?> &middot; School ID: <?= esc($student['school_id'] ?? 'N/A') ?></div> <div class="small text-muted">Current grade: <?= esc($gradeLabel) ?> &middot; School ID: <?= esc($student['school_id'] ?? 'N/A') ?></div>
<?php if ($canEnroll): ?>
<div class="row g-2 mt-2" data-student-edit-fields>
<div class="col-md-6">
<label class="form-label small fw-semibold" for="student-firstname-<?= esc($student['id']) ?>">First name</label>
<input class="form-control form-control-sm" type="text" id="student-firstname-<?= esc($student['id']) ?>" name="student_info[<?= esc($student['id']) ?>][firstname]" value="<?= esc($student['firstname'] ?? '') ?>" required maxlength="30" disabled data-student-edit-input>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold" for="student-lastname-<?= esc($student['id']) ?>">Last name</label>
<input class="form-control form-control-sm" type="text" id="student-lastname-<?= esc($student['id']) ?>" name="student_info[<?= esc($student['id']) ?>][lastname]" value="<?= esc($student['lastname'] ?? '') ?>" required maxlength="30" disabled data-student-edit-input>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="student-dob-<?= esc($student['id']) ?>">Date of birth</label>
<input class="form-control form-control-sm" type="date" id="student-dob-<?= esc($student['id']) ?>" name="student_info[<?= esc($student['id']) ?>][dob]" value="<?= esc($student['dob'] ?? '') ?>" required disabled data-student-edit-input>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="student-gender-<?= esc($student['id']) ?>">Gender</label>
<select class="form-select form-select-sm" id="student-gender-<?= esc($student['id']) ?>" name="student_info[<?= esc($student['id']) ?>][gender]" required disabled data-student-edit-input>
<option value="">Select</option>
<option value="Male" <?= ($student['gender'] ?? '') === 'Male' ? 'selected' : '' ?>>Male</option>
<option value="Female" <?= ($student['gender'] ?? '') === 'Female' ? 'selected' : '' ?>>Female</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold" for="student-grade-<?= esc($student['id']) ?>">Registration grade</label>
<input class="form-control form-control-sm" type="text" id="student-grade-<?= esc($student['id']) ?>" name="student_info[<?= esc($student['id']) ?>][registration_grade]" value="<?= esc($student['registration_grade'] ?? '') ?>" required maxlength="50" disabled data-student-edit-input>
</div>
<div class="col-md-6">
<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" <?= (string) ($student['photo_consent'] ?? '') === '1' ? 'selected' : '' ?>>Yes</option>
<option value="0" <?= (string) ($student['photo_consent'] ?? '') === '0' ? 'selected' : '' ?>>No</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Expected placement</label>
<div class="form-control form-control-sm bg-light"><?= esc($student['expected_placement_label'] ?? $gradeLabel) ?></div>
</div>
</div>
<?php else: ?>
<div class="small text-muted">
Date of birth: <?= esc(!empty($student['dob']) ? local_date($student['dob'], 'm-d-Y') : 'N/A') ?>
<?php if (isset($student['age'])): ?>
&middot; Age: <?= esc($student['age']) ?>
<?php endif; ?>
</div>
<div class="small text-muted">Expected placement: <?= esc($student['expected_placement_label'] ?? $gradeLabel) ?></div>
<?php endif; ?>
<div class="small">Required action: <?= esc($student['required_action_label'] ?? 'Contact administration') ?></div> <div class="small">Required action: <?= esc($student['required_action_label'] ?? 'Contact administration') ?></div>
<?php if (($eligibilityMessage['message'] ?? '') !== ''): ?> <?php if (($eligibilityMessage['message'] ?? '') !== ''): ?>
<div class="alert alert-<?= esc($eligibilityMessage['level'] ?? 'info') ?> py-2 px-3 mt-2 mb-0 small"> <div class="alert alert-<?= esc($eligibilityMessage['level'] ?? 'info') ?> py-2 px-3 mt-2 mb-0 small">
@@ -379,7 +433,7 @@ foreach (($students ?? []) as $student) {
</div> </div>
<div class="d-none" data-step-panel="1"> <div class="d-none" data-step-panel="1">
<h6 class="fw-semibold">Policy info update</h6> <h6 class="fw-semibold">Acknowledge school policies</h6>
<div class="text-muted small mb-3">Review the current school policies before submitting enrollment.</div> <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> <iframe src="<?= base_url('policy/school_policy') ?>" class="enrollment-policy-frame" frameborder="0" title="School Policies"></iframe>
<div class="form-check mt-3"> <div class="form-check mt-3">
@@ -391,11 +445,76 @@ foreach (($students ?? []) as $student) {
</div> </div>
<div class="d-none" data-step-panel="2"> <div class="d-none" data-step-panel="2">
<h6 class="fw-semibold">Enroll submit</h6> <h6 class="fw-semibold">Review tuition, fees, and balance</h6>
<div class="text-muted small mb-3">Review the selected students, then submit enrollment.</div> <div class="text-muted small mb-3">Review the family account information before submitting enrollment.</div>
<div id="enrollmentReviewList" class="d-grid gap-2"></div> <?php if ($familyFinancialSummary !== []): ?>
<div class="alert alert-warning mt-3 mb-0 small"> <div class="border rounded p-3 bg-light">
Submitting sends the selected enrollment(s) to admission review. <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">Registration fee:</span> <strong data-financial-value="registration_fee"><?= esc($money($familyFinancialSummary['registration_fee'] ?? 0)) ?></strong></div>
<div class="col-md-4"><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="col-md-4"><span class="text-muted">Mandatory fees:</span> <strong data-financial-value="mandatory_fees"><?= esc($money($familyFinancialSummary['mandatory_fees'] ?? 0)) ?></strong></div>
<div class="col-md-4"><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="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>
<?php if (!empty($familyFinancialSummary['policy_message'])): ?>
<div class="small text-muted mt-2"><?= esc($familyFinancialSummary['policy_message']) ?></div>
<?php endif; ?>
<div class="small mt-2"><a href="<?= site_url('parent/financial-aid') ?>">Request financial aid</a></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="3">
<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">Fees by student</div>
<div id="enrollmentFeeReviewList" class="d-grid gap-2"></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">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>
<?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) to admission review.</li>
<li>Payments are processed on the first day of school.</li>
<?php if (!empty($familyFinancialSummary['policy_message'])): ?>
<li><?= esc($familyFinancialSummary['policy_message']) ?></li>
<?php endif; ?>
<?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>
</div> </div>
@@ -447,7 +566,24 @@ foreach (($students ?? []) as $student) {
const backButton = document.getElementById('enrollmentBackButton'); const backButton = document.getElementById('enrollmentBackButton');
const nextButton = document.getElementById('enrollmentNextButton'); const nextButton = document.getElementById('enrollmentNextButton');
const submitButton = document.getElementById('enrollmentSubmitButton'); const submitButton = document.getElementById('enrollmentSubmitButton');
const reviewList = document.getElementById('enrollmentReviewList'); const feeReviewList = document.getElementById('enrollmentFeeReviewList');
const finalStep = 3;
const feeSchedule = <?= json_encode([
'currency' => (string) ($enrollmentFeeSchedule['currency'] ?? '$'),
'firstStudentFee' => (float) ($enrollmentFeeSchedule['first_student_fee'] ?? 0),
'secondStudentFee' => (float) ($enrollmentFeeSchedule['second_student_fee'] ?? 0),
'registrationFee' => (float) ($enrollmentFeeSchedule['registration_fee'] ?? 0),
'tuitionDueAtRegistration' => (float) ($enrollmentFeeSchedule['tuition_due_at_registration'] ?? 0),
'mandatoryFees' => (float) ($enrollmentFeeSchedule['mandatory_fees'] ?? 0),
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
const familyFinancial = <?= json_encode([
'carryOverBalance' => (float) ($familyFinancialSummary['carry_over_balance'] ?? 0),
'registrationFee' => (float) ($familyFinancialSummary['registration_fee'] ?? 0),
'tuitionDueAtRegistration' => (float) ($familyFinancialSummary['tuition_due_at_registration'] ?? 0),
'mandatoryFees' => (float) ($familyFinancialSummary['mandatory_fees'] ?? 0),
'currentBalance' => (float) ($familyFinancialSummary['current_balance'] ?? 0),
'amountDue' => (float) ($familyFinancialSummary['amount_due'] ?? 0),
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
let currentStep = 0; let currentStep = 0;
let hasAcceptedSchoolPolicy = <?= $hasAcceptedSchoolPolicy ? 'true' : 'false' ?>; let hasAcceptedSchoolPolicy = <?= $hasAcceptedSchoolPolicy ? 'true' : 'false' ?>;
@@ -455,7 +591,7 @@ foreach (($students ?? []) as $student) {
const selectedWithdrawInputs = () => form ? Array.from(form.querySelectorAll("input[name='withdraw[]']:checked")) : []; const selectedWithdrawInputs = () => form ? Array.from(form.querySelectorAll("input[name='withdraw[]']:checked")) : [];
function setStep(step) { function setStep(step) {
currentStep = Math.max(0, Math.min(2, step)); currentStep = Math.max(0, Math.min(finalStep, step));
document.querySelectorAll('[data-step-panel]').forEach(panel => { document.querySelectorAll('[data-step-panel]').forEach(panel => {
panel.classList.toggle('d-none', Number(panel.dataset.stepPanel) !== currentStep); panel.classList.toggle('d-none', Number(panel.dataset.stepPanel) !== currentStep);
}); });
@@ -463,21 +599,100 @@ foreach (($students ?? []) as $student) {
indicator.classList.toggle('is-active', Number(indicator.dataset.stepIndicator) === currentStep); indicator.classList.toggle('is-active', Number(indicator.dataset.stepIndicator) === currentStep);
}); });
backButton.classList.toggle('d-none', currentStep === 0); backButton.classList.toggle('d-none', currentStep === 0);
nextButton.classList.toggle('d-none', currentStep === 2); nextButton.classList.toggle('d-none', currentStep === finalStep);
submitButton.classList.toggle('d-none', currentStep !== 2); submitButton.classList.toggle('d-none', currentStep !== finalStep);
if (currentStep === 2) { if (currentStep >= 2) {
updateFinancialReview();
}
if (currentStep === finalStep) {
renderReview(); renderReview();
} }
} }
function calculateSelectedTuition() {
return selectedEnrollInputs().reduce((total, input, index) => {
return total + (index === 0 ? Number(feeSchedule.firstStudentFee || 0) : Number(feeSchedule.secondStudentFee || 0));
}, 0);
}
function updateFinancialReview() {
const selectedCount = selectedEnrollInputs().length;
const tuitionDue = selectedCount > 0 ? calculateSelectedTuition() : 0;
const carryOver = Number(familyFinancial.carryOverBalance || 0);
const registrationFee = Number(familyFinancial.registrationFee || feeSchedule.registrationFee || 0);
const mandatoryFees = Number(familyFinancial.mandatoryFees || feeSchedule.mandatoryFees || 0);
const currentBalance = Number(familyFinancial.currentBalance || 0);
const amountDue = Math.max(0, carryOver) + Math.max(0, currentBalance) + registrationFee + tuitionDue + mandatoryFees;
const values = {
carry_over_balance: carryOver,
registration_fee: registrationFee,
tuition_due_at_registration: tuitionDue,
mandatory_fees: mandatoryFees,
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() { function renderReview() {
updateFinancialReview();
const cards = selectedEnrollInputs().map(input => { const cards = selectedEnrollInputs().map(input => {
const card = input.closest('[data-student-card]'); const card = input.closest('[data-student-card]');
const name = card ? card.querySelector('.fw-semibold')?.textContent?.trim() : 'Student'; const firstName = card ? card.querySelector("input[name$='[firstname]']")?.value?.trim() : '';
const meta = card ? card.querySelector('.small.text-muted')?.textContent?.trim() : ''; const lastName = card ? card.querySelector("input[name$='[lastname]']")?.value?.trim() : '';
return '<div class="border rounded p-3"><div class="fw-semibold">' + escapeHtml(name || 'Student') + '</div><div class="small text-muted">' + escapeHtml(meta || '') + '</div></div>'; 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 schoolId = card?.dataset.schoolId || 'N/A';
const currentGrade = card?.dataset.currentGrade || '';
const expectedPlacement = card?.dataset.expectedPlacement || '';
const requiredAction = card?.dataset.requiredAction || '';
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(' - '),
currentGrade,
expectedPlacement,
requiredAction,
};
}); });
reviewList.innerHTML = cards.length ? cards.join('') : '<div class="alert alert-warning mb-0">No students selected.</div>';
if (!feeReviewList) {
return;
}
if (!cards.length) {
feeReviewList.innerHTML = '<div class="alert alert-warning mb-0">No students selected.</div>';
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>' +
'</div>' +
'</div>';
}).join('');
} }
function escapeHtml(value) { function escapeHtml(value) {
@@ -486,6 +701,11 @@ foreach (($students ?? []) as $student) {
}); });
} }
function formatMoney(amount) {
const numericAmount = Number(amount || 0);
return String(feeSchedule.currency || '$') + numericAmount.toFixed(2);
}
function syncPolicyAccepted() { function syncPolicyAccepted() {
hasAcceptedSchoolPolicy = !!(policyAcceptedCheckbox && policyAcceptedCheckbox.checked); hasAcceptedSchoolPolicy = !!(policyAcceptedCheckbox && policyAcceptedCheckbox.checked);
if (policyAcceptedInput) { if (policyAcceptedInput) {
@@ -493,6 +713,12 @@ foreach (($students ?? []) as $student) {
} }
} }
function setStudentFieldsEnabled(card, enabled) {
card.querySelectorAll('[data-student-edit-input]').forEach(field => {
field.disabled = !enabled;
});
}
if (startButton && flowModal) { if (startButton && flowModal) {
startButton.addEventListener('click', function() { startButton.addEventListener('click', function() {
if (deadlinePassed) { if (deadlinePassed) {
@@ -505,7 +731,10 @@ foreach (($students ?? []) as $student) {
} }
document.querySelectorAll('[data-student-card]').forEach(card => { document.querySelectorAll('[data-student-card]').forEach(card => {
card.addEventListener('click', function() { card.addEventListener('click', function(event) {
if (event.target.closest('input, select, textarea, label')) {
return;
}
if (this.dataset.selectable !== '1') { if (this.dataset.selectable !== '1') {
return; return;
} }
@@ -515,6 +744,7 @@ foreach (($students ?? []) as $student) {
} }
input.checked = !input.checked; input.checked = !input.checked;
this.classList.toggle('is-selected', input.checked); this.classList.toggle('is-selected', input.checked);
setStudentFieldsEnabled(this, input.checked);
}); });
}); });
@@ -550,7 +780,11 @@ foreach (($students ?? []) as $student) {
if (e.submitter && e.submitter.id === 'withdrawSubmitButton') { if (e.submitter && e.submitter.id === 'withdrawSubmitButton') {
selectedEnrollInputs().forEach(input => { selectedEnrollInputs().forEach(input => {
input.checked = false; input.checked = false;
input.closest('[data-student-card]')?.classList.remove('is-selected'); const card = input.closest('[data-student-card]');
card?.classList.remove('is-selected');
if (card) {
setStudentFieldsEnabled(card, false);
}
}); });
} }
+6 -2
View File
@@ -58,7 +58,7 @@
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (empty($openRequest)): ?> <?php if (empty($existingRequest)): ?>
<form method="post" action="<?= site_url('parent/financial-aid') ?>" class="border rounded p-3 bg-light"> <form method="post" action="<?= site_url('parent/financial-aid') ?>" class="border rounded p-3 bg-light">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="mb-3"> <div class="mb-3">
@@ -87,7 +87,11 @@
<button class="btn btn-success" type="submit">Submit request</button> <button class="btn btn-success" type="submit">Submit request</button>
</form> </form>
<?php else: ?> <?php else: ?>
<div class="alert alert-info">You already have an open request. Administration will update it after review. Remember to bring recent pay stubs and last years tax return to the school office.</div> <div class="alert alert-info">
You already submitted a financial aid application for <?= esc($schoolYear ?: 'this school year') ?>.
Parents can submit only one application per school year. Administration will update your request after review.
Remember to bring recent pay stubs and last years tax return to the school office.
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?= $this->endSection() ?> <?= $this->endSection() ?>
+6 -1
View File
@@ -48,6 +48,7 @@
$viewedAt = $ack['viewed_at'] ?? ''; $viewedAt = $ack['viewed_at'] ?? '';
$signedAt = $ack['signed_at'] ?? ''; $signedAt = $ack['signed_at'] ?? '';
$signedName = $ack['signed_name'] ?? ''; $signedName = $ack['signed_name'] ?? '';
$hasReport = !empty(($reportAvailableMap ?? [])[$sid]);
?> ?>
<tr> <tr>
<td><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td> <td><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td>
@@ -62,8 +63,12 @@
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="text-end"> <td class="text-end">
<?php if ($hasReport): ?>
<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('parent/report-cards/view/' . $sid) ?>">View Report</a> <a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('parent/report-cards/view/' . $sid) ?>">View Report</a>
<?php if (! $signedAt): ?> <?php else: ?>
<button class="btn btn-sm btn-outline-secondary" type="button" disabled>No report available</button>
<?php endif; ?>
<?php if ($hasReport && ! $signedAt): ?>
<form class="d-inline-flex align-items-center gap-2 ms-2" method="post" action="<?= base_url('parent/report-cards/sign/' . $sid) ?>"> <form class="d-inline-flex align-items-center gap-2 ms-2" method="post" action="<?= base_url('parent/report-cards/sign/' . $sid) ?>">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="text" name="signed_name" class="form-control form-control-sm" placeholder="Full name" required<?= $disabledAttr ?>> <input type="text" name="signed_name" class="form-control form-control-sm" placeholder="Full name" required<?= $disabledAttr ?>>
+356 -35
View File
@@ -13,11 +13,51 @@
$blockers = $preview['blockers'] ?? []; $blockers = $preview['blockers'] ?? [];
$warnings = $preview['warnings'] ?? []; $warnings = $preview['warnings'] ?? [];
$carryForward = $preview['carry_forward'] ?? []; $carryForward = $preview['carry_forward'] ?? [];
$carryForwardTotal = array_reduce(
$carryForward,
static fn (float $total, array $row): float => $total + (float) ($row['carry_forward_amount'] ?? 0),
0.0
);
$status = (string) ($source['status'] ?? ''); $status = (string) ($source['status'] ?? '');
$batchStatus = (string) ($latestBatch['status'] ?? ''); $batchStatus = (string) ($latestBatch['status'] ?? '');
$missingCarryForwardInvoices = (int) ($missingCarryForwardInvoices ?? 0); $missingCarryForwardInvoices = (int) ($missingCarryForwardInvoices ?? 0);
$money = static fn ($value): string => '$' . number_format((float) $value, 2); $money = static fn ($value): string => '$' . number_format((float) $value, 2);
$score = static fn ($value): string => is_numeric($value) ? number_format((float) $value, 2) : '-'; $score = static fn ($value): string => is_numeric($value) ? number_format((float) $value, 2) : '-';
$sourceId = (int) ($source['id'] ?? 0);
$targetId = (int) ($target['id'] ?? 0);
$flowStep = 1;
if ($status === 'closing' && $batchStatus === 'started') {
$flowStep = 3;
} elseif ($status === 'closing' && $batchStatus === 'executed') {
$flowStep = 4;
} elseif ($status === 'closed' || $batchStatus === 'completed') {
$flowStep = 5;
} elseif ($status === 'active') {
$flowStep = $blockers === [] && $target ? 2 : 1;
}
$initialModalStep = $flowStep === 2 ? 1 : $flowStep;
$flowSteps = [
1 => [
'title' => 'Review checklist',
'description' => 'Confirm promotion decisions, blocking issues, warnings, and financial totals before the year is locked.',
],
2 => [
'title' => 'Start close',
'description' => 'Lock the current year into end-year review and connect it to the selected next school year.',
],
3 => [
'title' => 'Carry forward',
'description' => 'Create or update target-year balances from the family balances shown in this checklist.',
],
4 => [
'title' => 'Finish close',
'description' => 'Mark the current school year closed and activate the next school year.',
],
5 => [
'title' => 'Closed',
'description' => 'The end-year process is complete. The closed year remains available for reporting and audit review.',
],
];
$ageBySchoolYearSecondSeptember = static function (array $row) use ($source): string { $ageBySchoolYearSecondSeptember = static function (array $row) use ($source): string {
$dob = trim((string) ($row['dob'] ?? '')); $dob = trim((string) ($row['dob'] ?? ''));
$schoolYear = (string) ($source['name'] ?? ''); $schoolYear = (string) ($source['name'] ?? '');
@@ -94,16 +134,12 @@
<span>Generated: <?= esc($preview['generated_at'] ?? '') ?></span> <span>Generated: <?= esc($preview['generated_at'] ?? '') ?></span>
</div> </div>
</div> </div>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#closeSchoolYearFlowModal">
Continue End-Year Flow
</button>
<a class="btn btn-outline-secondary" href="<?= site_url('administrator/school-years') ?>">Back to School Years</a> <a class="btn btn-outline-secondary" href="<?= site_url('administrator/school-years') ?>">Back to School Years</a>
</div> </div>
<div class="border rounded bg-white p-3 mb-4">
<h5 class="mb-2">How to End This Year</h5>
<ol class="mb-0">
<li>Clear all blocking issues below.</li>
<li>Start the end-year process to lock the checklist.</li>
<li>Confirm carry-forward balances, then mark this school year closed.</li>
</ol>
</div> </div>
<form class="row g-2 align-items-end mb-4" method="get" action="<?= site_url('administrator/school-years/' . (int) ($source['id'] ?? 0) . '/closing/preview') ?>"> <form class="row g-2 align-items-end mb-4" method="get" action="<?= site_url('administrator/school-years/' . (int) ($source['id'] ?? 0) . '/closing/preview') ?>">
@@ -342,55 +378,218 @@
</div> </div>
</div> </div>
<div class="d-flex flex-wrap gap-2 mb-4"> </div>
<?php if ($status === 'active' && $target && $blockers === []): ?>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/start') ?>" method="post"> <div
class="modal fade"
id="closeSchoolYearFlowModal"
tabindex="-1"
aria-labelledby="closeSchoolYearFlowModalLabel"
aria-hidden="true"
data-initial-step="<?= esc((string) $initialModalStep, 'attr') ?>"
data-max-step="<?= esc((string) $flowStep, 'attr') ?>"
>
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title" id="closeSchoolYearFlowModalLabel">Close <?= esc($source['name'] ?? '') ?></h5>
<div class="text-muted small">
<?php if ($target): ?>
Next school year: <span class="fw-semibold"><?= esc($target['name'] ?? '') ?></span>
<?php else: ?>
Select or create a next school year before closing can start.
<?php endif; ?>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-3 mb-4">
<?php foreach ($flowSteps as $stepNumber => $step): ?>
<?php
$stepClass = $stepNumber < $flowStep
? 'border-success bg-light'
: ($stepNumber === $flowStep ? 'border-primary' : 'border-light bg-light');
$badgeClass = $stepNumber < $flowStep
? 'bg-success'
: ($stepNumber === $flowStep ? 'bg-primary' : 'bg-secondary');
?>
<div class="col-lg col-md-4 col-sm-6">
<div class="border rounded p-3 h-100 <?= esc($stepClass) ?>" data-step-indicator="<?= esc((string) $stepNumber, 'attr') ?>">
<span class="badge <?= esc($badgeClass) ?> mb-2"><?= esc((string) $stepNumber) ?></span>
<div class="fw-semibold"><?= esc($step['title']) ?></div>
<div class="text-muted small"><?= esc($step['description']) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="close-school-year-step" data-step-pane="1">
<h6 class="mb-3">Review checklist details</h6>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="border rounded p-3 h-100">
<div class="text-muted small">Students reviewed</div>
<div class="fs-5 fw-semibold"><?= esc((string) ($overview['students'] ?? 0)) ?></div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded p-3 h-100">
<div class="text-muted small">Pending decisions</div>
<div class="fs-5 fw-semibold <?= (int) ($promotionSummary['pending_decision'] ?? 0) > 0 ? 'text-danger' : '' ?>">
<?= esc((string) ($promotionSummary['pending_decision'] ?? 0)) ?>
</div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded p-3 h-100">
<div class="text-muted small">Blocking issues</div>
<div class="fs-5 fw-semibold <?= $blockers === [] ? 'text-success' : 'text-danger' ?>"><?= esc((string) count($blockers)) ?></div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded p-3 h-100">
<div class="text-muted small">Net carry-forward</div>
<div class="fs-5 fw-semibold"><?= esc($money($carryForwardTotal)) ?></div>
</div>
</div>
</div>
<?php if ($blockers !== []): ?>
<div class="alert alert-danger">
<div class="fw-semibold mb-2">Resolve these blockers before the year can be closed.</div>
<ul class="mb-0">
<?php foreach ($blockers as $finding): ?>
<li><strong><?= esc($finding['title']) ?>:</strong> <?= esc($finding['detail']) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php elseif (! $target && $status === 'active'): ?>
<div class="alert alert-warning mb-0">
A next school year is required. Use the selector on this page to choose a target year, then refresh the checklist.
</div>
<?php else: ?>
<div class="alert alert-success mb-0">
Checklist review is ready. Continue to the next step to start closing this school year.
</div>
<?php endif; ?>
<?php if ($warnings !== []): ?>
<div class="border rounded p-3 mt-3">
<div class="fw-semibold mb-2">Warnings to review</div>
<ul class="mb-0">
<?php foreach ($warnings as $finding): ?>
<li><strong><?= esc($finding['title']) ?>:</strong> <?= esc($finding['detail']) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
<div class="close-school-year-step d-none" data-step-pane="2">
<h6 class="mb-3">Start the close</h6>
<div class="alert alert-warning mb-3">
Starting the close locks <?= esc($source['name'] ?? 'this school year') ?> into end-year review. Normal edits for this year stop while carry-forward is prepared.
</div>
<dl class="row mb-0">
<dt class="col-sm-4">Current school year</dt>
<dd class="col-sm-8"><?= esc($source['name'] ?? '') ?></dd>
<dt class="col-sm-4">Next school year</dt>
<dd class="col-sm-8"><?= esc($target['name'] ?? 'Not selected') ?></dd>
<dt class="col-sm-4">Blocking issues</dt>
<dd class="col-sm-8"><?= esc((string) count($blockers)) ?></dd>
</dl>
</div>
<div class="close-school-year-step d-none" data-step-pane="3">
<h6 class="mb-3">Confirm carry-forward</h6>
<div class="alert alert-primary mb-3">
Carry-forward will create or update next-year invoices from the balances shown in the Carry-Forward Families table.
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<div class="text-muted small">Families with carry-forward rows</div>
<div class="fs-5 fw-semibold"><?= esc((string) count($carryForward)) ?></div>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<div class="text-muted small">Net carry-forward</div>
<div class="fs-5 fw-semibold"><?= esc($money($carryForwardTotal)) ?></div>
</div>
</div>
</div>
</div>
<div class="close-school-year-step d-none" data-step-pane="4">
<h6 class="mb-3">Finish the close</h6>
<div class="alert alert-success mb-0">
Carry-forward is complete. Finishing the close will mark <?= esc($source['name'] ?? 'this school year') ?> closed and activate <?= esc($target['name'] ?? 'the next school year') ?>.
</div>
</div>
<div class="close-school-year-step d-none" data-step-pane="5">
<h6 class="mb-3">Closed</h6>
<div class="alert alert-secondary mb-0">
This close flow is complete. Use the report tables on this page for audit review.
</div>
</div>
</div>
<div class="modal-footer justify-content-between">
<div>
<?php if ($status === 'closing' && ! in_array($batchStatus, ['executed', 'completed'], true)): ?>
<form action="<?= site_url('administrator/school-years/' . $sourceId . '/closing/cancel') ?>" method="post" data-action-step="3">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="hidden" name="target_school_year_id" value="<?= (int) $target['id'] ?>"> <button class="btn btn-outline-secondary" type="submit">Cancel End-Year Process</button>
<button class="btn btn-warning" type="submit">Close Year</button> </form>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Review Page</button>
<button type="button" class="btn btn-outline-primary" data-step-back>Back</button>
<button type="button" class="btn btn-primary" data-step-next>Next</button>
<?php if ($status === 'active' && $target && $blockers === []): ?>
<form action="<?= site_url('administrator/school-years/' . $sourceId . '/closing/start') ?>" method="post" data-action-step="2">
<?= csrf_field() ?>
<input type="hidden" name="target_school_year_id" value="<?= $targetId ?>">
<button class="btn btn-warning" type="submit">Next</button>
</form> </form>
<?php elseif ($status === 'active'): ?> <?php elseif ($status === 'active'): ?>
<button class="btn btn-warning" type="button" disabled>Close Year</button> <button class="btn btn-warning" type="button" disabled data-action-step="1">Next</button>
<span class="align-self-center text-muted small">
<?php if (! $target): ?>
Create or select the next school year first.
<?php elseif ($blockers !== []): ?>
Resolve the blocking issues above first.
<?php endif; ?>
</span>
<?php endif; ?> <?php endif; ?>
<?php if ($status === 'closing' && $batchStatus === 'started'): ?> <?php if ($status === 'closing' && $batchStatus === 'started'): ?>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/execute') ?>" method="post"> <form action="<?= site_url('administrator/school-years/' . $sourceId . '/closing/execute') ?>" method="post" data-action-step="3">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="btn btn-primary" type="submit">Confirm Carry-Forward</button> <button class="btn btn-primary" type="submit">Next</button>
</form> </form>
<?php endif; ?> <?php endif; ?>
<?php if ($status === 'closing' && $batchStatus === 'executed'): ?> <?php if ($status === 'closing' && $batchStatus === 'executed'): ?>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/complete') ?>" method="post"> <form action="<?= site_url('administrator/school-years/' . $sourceId . '/closing/complete') ?>" method="post" data-action-step="4">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="btn btn-success" type="submit">Close Year</button> <button class="btn btn-success" type="submit">Finish Close</button>
</form> </form>
<?php endif; ?> <?php endif; ?>
<?php if (in_array($batchStatus, ['executed', 'completed'], true)): ?> <?php if ($status === 'closed' || $batchStatus === 'completed'): ?>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/execute') ?>" method="post"> <a class="btn btn-success" href="<?= site_url('administrator/school-years') ?>" data-action-step="5">Finish</a>
<?php elseif ($batchStatus === 'executed'): ?>
<form action="<?= site_url('administrator/school-years/' . $sourceId . '/closing/execute') ?>" method="post" data-action-step="4">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="btn btn-primary" type="submit"> <button class="btn btn-outline-primary" type="submit">
<?= $missingCarryForwardInvoices > 0 <?= $missingCarryForwardInvoices > 0
? 'Repair Carry-Forward Invoices (' . $missingCarryForwardInvoices . ')' ? 'Repair Carry-Forward Invoices (' . $missingCarryForwardInvoices . ')'
: 'Re-run Carry-Forward Sync' ?> : 'Re-run Carry-Forward Sync' ?>
</button> </button>
</form> </form>
<?php endif; ?> <?php endif; ?>
</div>
<?php if ($status === 'closing' && ! in_array($batchStatus, ['executed', 'completed'], true)): ?> </div>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/cancel') ?>" method="post"> </div>
<?= csrf_field() ?>
<button class="btn btn-outline-secondary" type="submit">Cancel End-Year Process</button>
</form>
<?php endif; ?>
</div> </div>
</div> </div>
@@ -399,6 +598,128 @@
<?= $this->section('scripts') ?> <?= $this->section('scripts') ?>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var closeFlowModal = document.getElementById('closeSchoolYearFlowModal');
var setupCloseFlowModal = function () {
if (!closeFlowModal) {
return;
}
var currentStep = parseInt(closeFlowModal.getAttribute('data-initial-step') || '1', 10);
var maxStep = parseInt(closeFlowModal.getAttribute('data-max-step') || '1', 10);
var backButton = closeFlowModal.querySelector('[data-step-back]');
var nextButton = closeFlowModal.querySelector('[data-step-next]');
var panes = Array.prototype.slice.call(closeFlowModal.querySelectorAll('[data-step-pane]'));
var indicators = Array.prototype.slice.call(closeFlowModal.querySelectorAll('[data-step-indicator]'));
var actions = Array.prototype.slice.call(closeFlowModal.querySelectorAll('[data-action-step]'));
var actionForms = actions.filter(function (action) {
return action.tagName && action.tagName.toLowerCase() === 'form';
});
var showStep = function (step) {
currentStep = Math.max(1, Math.min(maxStep, step));
panes.forEach(function (pane) {
pane.classList.toggle('d-none', parseInt(pane.getAttribute('data-step-pane') || '0', 10) !== currentStep);
});
indicators.forEach(function (indicator) {
var indicatorStep = parseInt(indicator.getAttribute('data-step-indicator') || '0', 10);
var badge = indicator.querySelector('.badge');
indicator.classList.remove('border-success', 'border-primary', 'border-light', 'bg-light');
if (badge) {
badge.classList.remove('bg-success', 'bg-primary', 'bg-secondary');
}
if (indicatorStep < currentStep) {
indicator.classList.add('border-success', 'bg-light');
if (badge) badge.classList.add('bg-success');
} else if (indicatorStep === currentStep) {
indicator.classList.add('border-primary');
if (badge) badge.classList.add('bg-primary');
} else {
indicator.classList.add('border-light', 'bg-light');
if (badge) badge.classList.add('bg-secondary');
}
});
actions.forEach(function (action) {
action.classList.toggle('d-none', parseInt(action.getAttribute('data-action-step') || '0', 10) !== currentStep);
});
if (backButton) {
backButton.classList.toggle('d-none', currentStep <= 1);
}
if (nextButton) {
nextButton.classList.toggle('d-none', currentStep >= maxStep);
}
};
if (backButton) {
backButton.addEventListener('click', function () {
showStep(currentStep - 1);
});
}
if (nextButton) {
nextButton.addEventListener('click', function () {
showStep(currentStep + 1);
});
}
actionForms.forEach(function (form) {
form.addEventListener('submit', function (event) {
var submitButton = form.querySelector('button[type="submit"]');
event.preventDefault();
if (submitButton) {
submitButton.disabled = true;
}
fetch(form.action, {
method: form.method || 'POST',
body: new FormData(form),
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function (response) {
return response.text();
})
.then(function (html) {
var parsed = new DOMParser().parseFromString(html, 'text/html');
var updatedModal = parsed.getElementById('closeSchoolYearFlowModal');
if (!updatedModal) {
window.location.reload();
return;
}
closeFlowModal.setAttribute('data-initial-step', updatedModal.getAttribute('data-initial-step') || '1');
closeFlowModal.setAttribute('data-max-step', updatedModal.getAttribute('data-max-step') || '1');
closeFlowModal.querySelector('.modal-content').innerHTML = updatedModal.querySelector('.modal-content').innerHTML;
setupCloseFlowModal();
})
.catch(function () {
form.submit();
})
.finally(function () {
if (submitButton) {
submitButton.disabled = false;
}
});
});
});
showStep(currentStep);
};
if (closeFlowModal) {
setupCloseFlowModal();
if (new URLSearchParams(window.location.search).get('close_flow') === '1' && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(closeFlowModal).show();
}
}
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) { if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return; return;
} }
-4
View File
@@ -181,10 +181,6 @@
<button class="dropdown-item text-danger" type="submit">Delete draft</button> <button class="dropdown-item text-danger" type="submit">Delete draft</button>
</form> </form>
</li> </li>
<?php elseif ($status === 'active'): ?>
<li>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">Close year</a>
</li>
<?php elseif ($status === 'closing'): ?> <?php elseif ($status === 'closing'): ?>
<li> <li>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">Finish end-year checklist</a> <a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">Finish end-year checklist</a>
@@ -12,7 +12,7 @@ use PHPUnit\Framework\TestCase;
final class EnrollmentRegistrationEmailServiceTest extends TestCase final class EnrollmentRegistrationEmailServiceTest extends TestCase
{ {
public function testPassedStudentUsesReEnrollmentOpeningLanguage(): void public function testPassedStudentMessageStaysConcise(): void
{ {
$service = $this->service(); $service = $this->service();
@@ -32,10 +32,10 @@ final class EnrollmentRegistrationEmailServiceTest extends TestCase
$action = $this->invoke($service, 'requiredAction', [$evaluation, 'August 31, 2026']); $action = $this->invoke($service, 'requiredAction', [$evaluation, 'August 31, 2026']);
$this->assertSame('Eligible', $status); $this->assertSame('Eligible', $status);
$this->assertStringContainsString('We are pleased to inform you that Student Name has successfully passed Grade 8.', $message); $this->assertStringContainsString('Student Name has successfully passed Grade 8.', $message);
$this->assertStringContainsString('Re-enrollment for the new school year will open on August 1, 2026.', $message); $this->assertStringNotContainsString('Re-enrollment for the new school year will open on August 1, 2026.', $message);
$this->assertStringContainsString('Please make sure to re-enroll your child before August 31, 2026', $message); $this->assertStringNotContainsString('Please make sure to re-enroll your child before August 31, 2026', $message);
$this->assertStringContainsString('sign in to the parent portal', $message); $this->assertStringNotContainsString('sign in to the parent portal', $message);
$this->assertStringContainsString('Complete re-enrollment before August 31, 2026.', $action); $this->assertStringContainsString('Complete re-enrollment before August 31, 2026.', $action);
$this->assertStringNotContainsString('Not Eligible', $status); $this->assertStringNotContainsString('Not Eligible', $status);
$this->assertStringNotContainsString('Registration for the new school year has not opened yet', $message); $this->assertStringNotContainsString('Registration for the new school year has not opened yet', $message);