fix enrollment for the new school-year
Tests / PHPUnit (push) Successful in 1m26s

This commit is contained in:
root
2026-08-07 23:43:31 -04:00
parent b6f3b14e7b
commit 1ef2800f12
66 changed files with 8997 additions and 498 deletions
@@ -0,0 +1,336 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid py-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h2 class="mb-1">Enrollment Administration</h2>
<div class="text-muted">Review school-year transition flags, exceptions, and placement follow-up.</div>
</div>
</div>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="border rounded p-3 mb-3 bg-light">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<div>
<div class="fw-semibold">Registration Email Launch</div>
<?php if (!empty($launchState['approved'])): ?>
<div class="text-success small">Approved at <?= esc(local_datetime($launchState['approved_at'], 'm-d-Y H:i')) ?></div>
<?php elseif (!empty($launchState['missing'])): ?>
<div class="text-danger small">Missing: <?= esc(implode(', ', $launchState['missing'])) ?></div>
<?php else: ?>
<div class="text-muted small">Configuration is ready for launch approval.</div>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2 justify-content-end">
<?php if (!empty($previewParentId)): ?>
<a class="btn btn-outline-secondary" target="_blank" href="<?= site_url('administrator/enrollment-admin/email-preview?school_year=' . rawurlencode((string) $schoolYear) . '&parent_id=' . (int) $previewParentId) ?>">Preview Email</a>
<?php endif; ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/approve-launch') ?>">
<?= csrf_field() ?>
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<button type="submit" class="btn btn-success" <?= !empty($launchState['missing']) ? 'disabled' : '' ?>>Approve Launch</button>
</form>
<form method="post" action="<?= site_url('administrator/enrollment-admin/send-registration-emails') ?>" onsubmit="return confirm('Send real registration emails to parents for <?= esc((string) ($schoolYear ?? ''), 'js') ?>?');">
<?= csrf_field() ?>
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="force_resend" name="force_resend" value="1">
<label class="form-check-label small" for="force_resend">Resend sent</label>
</div>
<button type="submit" class="btn btn-danger" <?= empty($launchState['approved']) || !empty($launchState['missing']) || empty($emailExamples) ? 'disabled' : '' ?>>Send Real Emails</button>
</form>
</div>
</div>
</div>
<h4 class="mb-3">Decision Email Examples</h4>
<div class="table-responsive mb-4">
<table class="table table-sm table-bordered align-middle" data-no-mgmt-sticky>
<thead>
<tr>
<th>Parent</th>
<th>Recipient</th>
<th>Students</th>
<th>Subject</th>
<th>Delivery</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<?php if (!empty($emailExamples)): ?>
<?php foreach ($emailExamples as $example): ?>
<?php
$deliveryStatus = (string) ($example['delivery_status'] ?? 'not sent');
$badgeClass = match ($deliveryStatus) {
'sent' => 'bg-success',
'failed' => 'bg-danger',
'generated' => 'bg-info text-dark',
default => 'bg-secondary',
};
?>
<tr>
<td><?= esc($example['parent_name'] ?? '') ?></td>
<td><?= esc(implode(', ', $example['recipients'] ?? [])) ?></td>
<td><?= esc(implode(', ', $example['student_names'] ?? [])) ?></td>
<td><?= esc($example['subject'] ?? '') ?></td>
<td>
<span class="badge <?= esc($badgeClass) ?>"><?= esc($deliveryStatus) ?></span>
<?php if (!empty($example['sent_at'])): ?>
<div class="small text-muted"><?= esc(local_datetime($example['sent_at'], 'm-d-Y H:i')) ?></div>
<?php endif; ?>
<?php if (!empty($example['failure_reason'])): ?>
<div class="small text-danger"><?= esc($example['failure_reason']) ?></div>
<?php endif; ?>
</td>
<td>
<a class="btn btn-sm btn-outline-secondary" target="_blank" href="<?= site_url('administrator/enrollment-admin/email-preview?school_year=' . rawurlencode((string) $schoolYear) . '&parent_id=' . (int) ($example['parent_user_id'] ?? 0)) ?>">View Example</a>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="text-center text-muted">No parent decision email examples found for this school year.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<form method="get" action="<?= site_url('administrator/enrollment-admin') ?>" class="row g-2 align-items-end mb-3">
<div class="col-md-3">
<label class="form-label" for="school_year">School Year</label>
<select class="form-select" id="school_year" name="school_year">
<?php foreach (($schoolYears ?? []) as $year): ?>
<?php $name = (string) ($year['name'] ?? ''); ?>
<option value="<?= esc($name) ?>" <?= $name === ($schoolYear ?? '') ? 'selected' : '' ?>><?= esc($name) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="status">Status</label>
<select class="form-select" id="status" name="status">
<?php foreach (['open' => 'Open', 'resolved' => 'Resolved'] as $value => $label): ?>
<option value="<?= esc($value) ?>" <?= $value === ($status ?? '') ? 'selected' : '' ?>><?= esc($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="flag_type">Flag Type</label>
<select class="form-select" id="flag_type" name="flag_type">
<option value="">All flag types</option>
<?php foreach (($flagTypes ?? []) as $type): ?>
<option value="<?= esc($type) ?>" <?= $type === ($flagType ?? '') ? 'selected' : '' ?>><?= esc($type) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="assigned_to">Assigned To</label>
<select class="form-select" id="assigned_to" name="assigned_to">
<option value="">Anyone</option>
<?php foreach (($admins ?? []) as $admin): ?>
<?php
$adminId = (int) ($admin['id'] ?? 0);
$adminName = trim((string) ($admin['firstname'] ?? '') . ' ' . (string) ($admin['lastname'] ?? '')) ?: 'User #' . $adminId;
?>
<option value="<?= $adminId ?>" <?= (string) $adminId === (string) ($assignedTo ?? '') ? 'selected' : '' ?>><?= esc($adminName) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Filter</button>
</div>
</form>
<h4 class="mb-3">Enrollment Follow-up</h4>
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle" data-no-mgmt-sticky>
<thead>
<tr>
<th>Student</th>
<th>School ID</th>
<th>Enrollment Status</th>
<th>Decision</th>
<th>Placement</th>
<th>Class</th>
<th>Exception / Reason</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<?php if (!empty($enrollmentFollowups)): ?>
<?php foreach ($enrollmentFollowups as $row): ?>
<?php
$statusValue = (string) ($row['enrollment_status'] ?? '');
$placementValue = (string) ($row['placement_status'] ?? '');
$decisionValue = (string) ($row['deliberation_decision'] ?? '');
$statusClass = match (strtolower($statusValue)) {
'review & decision' => 'bg-warning text-dark',
'waitlist' => 'bg-info text-dark',
'denied' => 'bg-danger',
default => 'bg-secondary',
};
?>
<tr>
<td><?= esc($row['student_name'] ?? '') ?></td>
<td><?= esc($row['school_id'] ?? '') ?></td>
<td><span class="badge <?= esc($statusClass) ?>"><?= esc($statusValue) ?></span></td>
<td><?= esc($decisionValue !== '' ? $decisionValue : 'Pending') ?></td>
<td><?= esc($placementValue !== '' ? $placementValue : 'Pending') ?></td>
<td><?= esc($row['class_section_name'] ?? '') ?></td>
<td>
<?php if (!empty($row['exception_required'])): ?>
<span class="badge bg-warning text-dark">Exception</span>
<?php endif; ?>
<div class="small"><?= esc($row['exception_reason'] ?? '') ?></div>
</td>
<td><?= esc(!empty($row['updated_at']) ? local_datetime($row['updated_at'], 'm-d-Y H:i') : '') ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="text-center text-muted">No enrollment records need follow-up for this school year.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<h4 class="mb-3">Enrollment Flags</h4>
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle" data-no-mgmt-sticky>
<thead>
<tr>
<th>Student</th>
<th>School ID</th>
<th>Flag Type</th>
<th>Priority</th>
<th>Details</th>
<th>Assigned</th>
<th>Created</th>
<th style="min-width: 280px;">Action</th>
</tr>
</thead>
<tbody>
<?php if (!empty($flags)): ?>
<?php foreach ($flags as $flag): ?>
<?php
$flagId = (int) ($flag['id'] ?? 0);
$flagTypeValue = (string) ($flag['flag_type'] ?? '');
$details = is_array($flag['details'] ?? null) ? $flag['details'] : [];
?>
<tr>
<td><?= esc($flag['student_name'] ?? '') ?></td>
<td><?= esc($flag['school_id'] ?? '') ?></td>
<td><span class="badge bg-secondary"><?= esc($flagTypeValue) ?></span></td>
<td><?= esc($flag['priority'] ?? 'normal') ?></td>
<td>
<?php if ($details !== []): ?>
<?php foreach ($details as $key => $value): ?>
<div><span class="text-muted"><?= esc((string) $key) ?>:</span> <?= esc(is_scalar($value) ? (string) $value : json_encode($value)) ?></div>
<?php endforeach; ?>
<?php else: ?>
<span class="text-muted">No details</span>
<?php endif; ?>
</td>
<td><?= esc($flag['assignee_name'] ?: '') ?></td>
<td><?= esc(!empty($flag['created_at']) ? local_datetime($flag['created_at'], 'm-d-Y H:i') : '') ?></td>
<td>
<?php if (($flag['status'] ?? '') === 'open'): ?>
<?php if ($flagTypeValue === 'CLASS_REASSIGNMENT_REQUIRED'): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/assign-class') ?>" class="mb-2">
<?= csrf_field() ?>
<select class="form-select form-select-sm mb-2" name="class_section_id" required>
<option value="">Select class section</option>
<?php foreach (($classSections ?? []) as $section): ?>
<option value="<?= (int) ($section['class_section_id'] ?? 0) ?>"><?= esc($section['class_section_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
<input class="form-control form-control-sm mb-2" name="resolution_notes" placeholder="Reason / notes" required>
<button class="btn btn-sm btn-primary" type="submit">Assign Class</button>
</form>
<?php elseif ($flagTypeValue === 'PENDING_MAKE_UP_EXAM_PROMOTION'): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/makeup-promotion') ?>" class="mb-2">
<?= csrf_field() ?>
<select class="form-select form-select-sm mb-2" name="exam_result" required>
<option value="">Exam result</option>
<option value="passed">Passed</option>
<option value="failed">Failed</option>
</select>
<select class="form-select form-select-sm mb-2" name="class_section_id">
<option value="">Promoted class section if passed</option>
<?php foreach (($classSections ?? []) as $section): ?>
<option value="<?= (int) ($section['class_section_id'] ?? 0) ?>"><?= esc($section['class_section_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
<input class="form-control form-control-sm mb-2" name="resolution_notes" placeholder="Resolution notes" required>
<button class="btn btn-sm btn-primary" type="submit">Resolve Exam</button>
</form>
<?php elseif (in_array($flagTypeValue, ['AGE_EXCEPTION_REQUIRED', 'LATE_REGISTRATION_EXCEPTION', 'FINANCIAL_REVIEW_REQUIRED', 'CLASS_CAPACITY_EXCEPTION_REQUIRED'], true)): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/approve-exception') ?>" class="mb-2">
<?= csrf_field() ?>
<input class="form-control form-control-sm mb-2" name="reason" placeholder="Approval reason" required>
<button class="btn btn-sm btn-warning" type="submit">Approve Exception</button>
</form>
<?php endif; ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/resolve') ?>">
<?= csrf_field() ?>
<input class="form-control form-control-sm mb-2" name="resolution_notes" placeholder="Resolution notes" required>
<button class="btn btn-sm btn-outline-secondary" type="submit">Resolve Manually</button>
</form>
<?php else: ?>
<div class="text-muted small"><?= esc($flag['resolution_notes'] ?? '') ?></div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="text-center text-muted">No formal enrollment flags found. Check Enrollment Follow-up above for status-based items.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<h4 class="mb-3">Recent Enrollment Audit</h4>
<div class="table-responsive">
<table class="table table-sm table-bordered align-middle" data-no-mgmt-sticky>
<thead>
<tr>
<th>When</th>
<th>Student</th>
<th>Action</th>
<th>Administrator</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
<?php if (!empty($auditRows)): ?>
<?php foreach ($auditRows as $row): ?>
<tr>
<td><?= esc(!empty($row['created_at']) ? local_datetime($row['created_at'], 'm-d-Y H:i') : '') ?></td>
<td><?= esc($row['student_name'] ?? '') ?></td>
<td><?= esc($row['action'] ?? '') ?></td>
<td><?= esc($row['performed_by_name'] ?? '') ?></td>
<td><?= esc($row['reason'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="5" class="text-center text-muted">No enrollment audit records found. Audit rows appear after actions are completed from this dashboard or through the transition service.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?= $this->endSection() ?>
@@ -0,0 +1,30 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<style>
.enrollment-email-preview-frame .email-container {
max-width: 1280px;
width: min(1280px, 100%);
}
</style>
<div class="container-fluid py-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h2 class="mb-1">Registration Email Preview</h2>
<div class="text-muted"><?= esc($schoolYear ?? '') ?> · Parent #<?= esc((string) ($parentId ?? '')) ?></div>
</div>
<a class="btn btn-secondary" href="<?= site_url('administrator/enrollment-admin?school_year=' . rawurlencode((string) ($schoolYear ?? ''))) ?>">Back</a>
</div>
<div class="mb-3">
<label class="form-label">Subject</label>
<input class="form-control" value="<?= esc($subject ?? '') ?>" readonly>
</div>
<div class="border rounded bg-white p-3 enrollment-email-preview-frame">
<?= $body ?? '' ?>
</div>
</div>
<?= $this->endSection() ?>
@@ -3,30 +3,44 @@
<?= $this->section('styles') ?>
<style>
#classesTable {
table-layout: fixed;
table-layout: auto;
}
#classesTable th:nth-child(1),
#classesTable td:nth-child(1) {
width: 10rem;
width: 1%;
white-space: nowrap;
}
#classesTable th:nth-child(2),
#classesTable td:nth-child(2),
#classesTable td:nth-child(2) {
width: 1%;
white-space: nowrap;
}
#classesTable th:nth-child(3),
#classesTable td:nth-child(3) {
width: 6rem;
}
#classesTable th:nth-child(4),
#classesTable td:nth-child(4) {
width: 10rem;
min-width: 34rem;
}
.distribution-cell {
min-width: 28rem;
min-width: 34rem;
}
.distribution-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: .5rem;
}
.distribution-class-roster {
border: 1px solid #ced4da;
border-radius: .375rem;
background: #f8f9fa;
padding: .5rem;
margin-bottom: .5rem;
}
.distribution-class-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
margin-bottom: .35rem;
}
.distribution-section {
border: 1px solid #dee2e6;
border-radius: .375rem;
@@ -53,14 +67,78 @@
max-height: 8.5rem;
overflow: auto;
margin: 0;
padding-left: 1.1rem;
padding-left: 0;
list-style: none;
counter-reset: distribution-student;
font-size: .8125rem;
line-height: 1.35;
}
.distribution-students li {
counter-increment: distribution-student;
}
.distribution-class-roster .distribution-students {
max-height: 14rem;
}
.distribution-students-wrap {
min-width: 0;
}
.distribution-students-header {
display: grid;
grid-template-columns: 2.25rem minmax(12rem, 1fr) 3.5rem 5rem minmax(8rem, 10rem) minmax(12rem, 16rem);
gap: .5rem;
padding: 0 .25rem .2rem 0;
font-size: .72rem;
font-weight: 700;
color: #6c757d;
text-transform: uppercase;
}
.distribution-students-header span:nth-child(3) {
text-align: end;
font-variant-numeric: tabular-nums;
}
.distribution-student-row {
display: grid;
grid-template-columns: 2.25rem minmax(12rem, 1fr) 3.5rem 5rem minmax(8rem, 10rem) minmax(12rem, 16rem);
gap: .5rem;
align-items: center;
margin-bottom: .25rem;
}
.student-row-number {
color: #6c757d;
text-align: end;
font-variant-numeric: tabular-nums;
}
.student-row-number::before {
content: counter(distribution-student) ". ";
}
.student-age-cell {
text-align: end;
color: #495057;
font-variant-numeric: tabular-nums;
}
.student-gender-cell {
color: #495057;
}
.student-last-year-cell {
color: #495057;
min-width: 0;
}
.student-assignment-cell {
min-width: 0;
}
.student-assignment-cell .form-select {
display: inline-block;
width: fit-content;
min-width: 5rem;
max-width: 100%;
}
.distribution-empty {
color: #6c757d;
font-size: .875rem;
}
.distribution-search-summary {
min-height: 1.25rem;
}
</style>
<?= $this->endSection() ?>
@@ -104,7 +182,7 @@
$name = trim((string)($c['class_section_name'] ?? ''));
$isBase = ($name !== '' && strpos($name, '-') === false);
$lowerName = strtolower($name);
$isStandard = ($lowerName === 'kg' || $lowerName === 'youth' || (ctype_digit($lowerName) && (int)$lowerName >= 1 && (int)$lowerName <= 9));
$isStandard = ($lowerName === 'kg' || $lowerName === 'youth' || (ctype_digit($lowerName) && (int)$lowerName >= 1 && (int)$lowerName <= 10));
if (!$isBase || $isStandard) continue;
?>
<option value="<?= (int)($c['class_id'] ?? 0) ?>"><?= esc($name) ?></option>
@@ -119,6 +197,15 @@
<div id="includedClasses" class="d-flex flex-wrap gap-2"></div>
</div>
</div>
<div class="row g-3 align-items-end mb-3">
<div class="col-md-4">
<label for="studentSearch" class="form-label">Search Students</label>
<input type="search" id="studentSearch" class="form-control" placeholder="Search by student name..." autocomplete="off" />
</div>
<div class="col-md-8">
<div id="studentSearchSummary" class="small text-muted distribution-search-summary"></div>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle no-mgmt-sticky" id="classesTable">
@@ -126,8 +213,6 @@
<tr id="tblHeader">
<th>Class</th>
<th>Total</th>
<th>Sections</th>
<th>Actions</th>
<th>Distribution</th>
</tr>
</thead>
@@ -151,6 +236,15 @@
const selectedYear = '<?= esc($selectedYear ?? '') ?>';
const totalsBaseUrl = '<?= site_url('administrator/sections/promotion-totals') ?>';
const distUrl = '<?= site_url('administrator/sections/auto-distribute') ?>';
const updateDraftUrl = '<?= site_url('administrator/sections/distribution-draft/update') ?>';
const updateCandidateUrl = '<?= site_url('administrator/sections/distribution-candidate/update') ?>';
const classSections = <?= json_encode(array_values(array_map(static function ($c) {
return [
'class_id' => (int)($c['class_id'] ?? 0),
'class_section_id' => (int)($c['class_section_id'] ?? 0),
'class_section_name' => (string)($c['class_section_name'] ?? ''),
];
}, $classes ?? [])), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
const tblBody = document.getElementById('tblBody');
const sectionCountInput = document.getElementById('sectionCount');
@@ -161,9 +255,13 @@
const additionalClassSelect = document.getElementById('additionalClassSelect');
const addClassBtn = document.getElementById('addClassBtn');
const includedClassesEl = document.getElementById('includedClasses');
const studentSearchInput = document.getElementById('studentSearch');
const studentSearchSummary = document.getElementById('studentSearchSummary');
let rowIndexByClassId = {}; // mapping to locate rows
let includedClassIds = [];
const baseClasses = buildBaseClassOptions();
const sectionsByClassId = buildSectionsByClassId();
function totalsUrl() {
const params = new URLSearchParams();
@@ -172,9 +270,111 @@
return totalsBaseUrl + '?' + params.toString();
}
function selectedSectionCount() {
const count = parseInt(sectionCountInput.value || '0', 10);
return count > 0 ? count : '';
function buildBaseClassOptions() {
const seen = {};
return classSections
.filter(row => row.class_id > 0 && row.class_section_name && row.class_section_name.indexOf('-') < 0)
.filter(function(row){
if (seen[row.class_id]) return false;
seen[row.class_id] = true;
return true;
})
.sort((a, b) => String(a.class_section_name).localeCompare(String(b.class_section_name), undefined, { numeric: true }));
}
function buildSectionsByClassId() {
const out = {};
classSections.forEach(function(row){
if (!row.class_id || !row.class_section_id || !row.class_section_name || row.class_section_name.indexOf('-') < 0) return;
if (!out[row.class_id]) out[row.class_id] = [];
out[row.class_id].push({
id: row.class_section_id,
name: row.class_section_name
});
});
Object.keys(out).forEach(function(classId){
out[classId].sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { numeric: true }));
});
return out;
}
function classNameById(classId) {
const row = baseClasses.find(c => c.class_id === parseInt(classId || '0', 10));
return row ? row.class_section_name : ('Class #' + classId);
}
function isStandardBaseClassName(name) {
const normalized = String(name || '').trim().toLowerCase();
if (normalized === 'kg' || normalized === 'youth') return true;
if (!/^\d+$/.test(normalized)) return false;
const n = parseInt(normalized, 10);
return n >= 1 && n <= 10;
}
function ensureIncludedClassVisible(classId) {
classId = parseInt(classId || '0', 10);
if (!classId) return;
const className = classNameById(classId);
if (isStandardBaseClassName(className)) return;
if (includedClassIds.indexOf(classId) < 0) {
includedClassIds.push(classId);
renderIncludedClasses();
}
}
function populateDestinationSelect(select, selectedSectionId, selectedClassId) {
select.innerHTML = '';
selectedClassId = parseInt(selectedClassId || '0', 10);
let resolvedClassId = 0;
baseClasses.forEach(function(baseClass){
const classOption = document.createElement('option');
classOption.value = String(baseClass.class_section_id);
classOption.dataset.classId = String(baseClass.class_id);
classOption.textContent = baseClass.class_section_name;
if (baseClass.class_section_id === parseInt(selectedSectionId || '0', 10) || (!parseInt(selectedSectionId || '0', 10) && selectedClassId === baseClass.class_id)) {
classOption.selected = true;
resolvedClassId = baseClass.class_id;
}
select.appendChild(classOption);
const sections = sectionsByClassId[String(baseClass.class_id)] || [];
sections.forEach(function(section){
const opt = document.createElement('option');
opt.value = String(section.id);
opt.dataset.classId = String(baseClass.class_id);
opt.textContent = section.name;
if (section.id === parseInt(selectedSectionId || '0', 10)) {
opt.selected = true;
resolvedClassId = baseClass.class_id;
}
select.appendChild(opt);
});
});
const selectedOption = select.options[select.selectedIndex] || null;
if (!resolvedClassId && selectedOption) {
resolvedClassId = parseInt(selectedOption.dataset.classId || '0', 10);
}
return {
classId: resolvedClassId,
sectionId: selectedOption ? parseInt(selectedOption.value || '0', 10) : 0
};
}
function selectedDestination(select) {
const selectedOption = select && select.options ? select.options[select.selectedIndex] : null;
return {
classId: selectedOption ? parseInt(selectedOption.dataset.classId || '0', 10) : 0,
sectionId: selectedOption ? parseInt(selectedOption.value || '0', 10) : 0
};
}
function assignmentLabel(assignment) {
if (assignment && assignment.class_section_name) return assignment.class_section_name;
const classId = parseInt((assignment && assignment.class_id) || '0', 10);
return classId > 0 ? classNameById(classId) : '-';
}
function buildInitialTable(rows) {
@@ -196,25 +396,16 @@
tdTotal.textContent = r.total;
tr.appendChild(tdTotal);
const tdNeed = document.createElement('td');
tdNeed.className = 'text-end need-cell';
tdNeed.textContent = selectedSectionCount();
tr.appendChild(tdNeed);
const tdAct = document.createElement('td');
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary';
btn.textContent = 'Generate Sections';
btn.addEventListener('click', function(){ runDistribution(r.class_section_id, r.class_section_name); });
tdAct.appendChild(btn);
tr.appendChild(tdAct);
const tdResults = document.createElement('td');
tdResults.className = 'distribution-cell';
const empty = document.createElement('span');
empty.className = 'distribution-empty';
empty.textContent = 'Not generated';
tdResults.appendChild(empty);
if (Array.isArray(r.students) && r.students.length) {
tdResults.appendChild(renderClassRoster(tr, r.students));
} else {
const empty = document.createElement('span');
empty.className = 'distribution-empty';
empty.textContent = 'No students';
tdResults.appendChild(empty);
}
tr.appendChild(tdResults);
tblBody.appendChild(tr);
@@ -223,16 +414,10 @@
rows.forEach(function(r){
if (Array.isArray(r.sections) && r.sections.length) {
renderSectionsForRow(r.class_section_id, r.sections);
renderSectionsForRow(r.class_section_id, r.sections, r.students || []);
}
});
}
function updateNeeds() {
document.querySelectorAll('#tblBody tr').forEach(function(tr){
const needCell = tr.querySelector('.need-cell');
if (needCell) needCell.textContent = selectedSectionCount();
});
applyStudentSearch();
}
function runDistribution(baseSectionId, baseName) {
@@ -273,12 +458,12 @@
msgEl.textContent = res && res.message ? res.message : 'Completed.';
renderSectionsForRow(baseSectionId, res.sections);
renderSectionsForRow(baseSectionId, res.sections, []);
})
.catch(() => { msgEl.textContent = 'Failed to distribute. Please try again.'; });
}
function renderSectionsForRow(baseSectionId, sections) {
function renderSectionsForRow(baseSectionId, sections, unassignedAssignments) {
const tr = Array.from(document.querySelectorAll('#tblBody tr')).find(function(_tr){
return String(_tr.dataset.classSectionId || '') === String(baseSectionId || '');
});
@@ -288,11 +473,54 @@
if (!td) return;
td.innerHTML = '';
const unassigned = Array.isArray(unassignedAssignments) ? unassignedAssignments : (tr._unassignedAssignments || []);
tr._unassignedAssignments = unassigned;
const allSections = Array.isArray(sections) ? sections.slice() : [];
const baseSections = allSections.filter(function(section){
const name = String(section.class_section_name || '');
return name === String(tr.dataset.className || '') || name.indexOf('-') < 0;
});
const visibleSections = allSections.filter(function(section){
return baseSections.indexOf(section) < 0;
});
const baseAssignments = assignmentsFromSections(baseSections);
const rosterAssignments = unassigned.concat(baseAssignments);
if (rosterAssignments.length) {
td.appendChild(renderClassRoster(tr, rosterAssignments));
}
const grid = document.createElement('div');
grid.className = 'distribution-grid';
const allAssignments = assignmentsFromSections(visibleSections);
sections.forEach(function(s){
const studentNames = Array.isArray(s.student_names) ? s.student_names : [];
if (!visibleSections.length && !rosterAssignments.length) {
td.appendChild(renderClassRoster(tr, allAssignments));
}
visibleSections.forEach(function(s){
const studentAssignments = Array.isArray(s.student_assignments) && s.student_assignments.length
? s.student_assignments.map(function(assignment){
return {
...assignment,
class_id: assignment.class_id || s.class_id,
class_section_id: assignment.class_section_id || s.class_section_id,
class_section_name: assignment.class_section_name || s.class_section_name || ''
};
})
: (Array.isArray(s.student_names) ? s.student_names : []).map(function(studentName){
return {
draft_id: 0,
student_id: 0,
student_name: studentName,
age_at_reference: null,
gender: '',
last_year_class_section: '',
class_id: s.class_id,
class_section_id: s.class_section_id,
class_section_name: s.class_section_name || ''
};
});
const studentNames = studentAssignments.map(a => a.student_name).filter(Boolean);
const block = document.createElement('div');
block.className = 'distribution-section';
@@ -327,14 +555,7 @@
if (meta.children.length) block.appendChild(meta);
if (studentNames.length) {
const list = document.createElement('ol');
list.className = 'distribution-students';
studentNames.forEach(function(studentName){
const item = document.createElement('li');
item.textContent = studentName;
list.appendChild(item);
});
block.appendChild(list);
block.appendChild(renderAssignmentList(studentAssignments, true));
} else {
const empty = document.createElement('div');
empty.className = 'distribution-empty';
@@ -346,6 +567,344 @@
});
td.appendChild(grid);
renderAddSectionButton(td, tr, baseSectionId, visibleSections);
applyStudentSearch();
}
function assignmentsFromSections(sections) {
const out = [];
const seenDrafts = {};
const seenFallback = {};
sections.forEach(function(section){
const assignments = Array.isArray(section.student_assignments) && section.student_assignments.length
? section.student_assignments
: (Array.isArray(section.student_names) ? section.student_names : []).map(function(studentName, idx){
return {
draft_id: 0,
student_id: 0,
student_name: studentName,
age_at_reference: null,
gender: '',
last_year_class_section: '',
class_id: section.class_id,
class_section_id: section.class_section_id,
class_section_name: section.class_section_name,
fallback_key: String(section.class_section_id || '') + ':' + idx + ':' + String(studentName || '')
};
});
assignments.forEach(function(assignment){
const draftId = parseInt(assignment.draft_id || '0', 10);
if (draftId > 0) {
if (seenDrafts[draftId]) return;
seenDrafts[draftId] = true;
} else {
const key = assignment.fallback_key || String(assignment.class_section_id || section.class_section_id || '') + ':' + String(assignment.student_name || '');
if (seenFallback[key]) return;
seenFallback[key] = true;
}
out.push({
draft_id: draftId,
student_id: parseInt(assignment.student_id || '0', 10),
student_name: assignment.student_name || 'Student',
age_at_reference: assignment.age_at_reference ?? null,
gender: assignment.gender || '',
last_year_class_section: assignment.last_year_class_section || '',
class_id: parseInt(assignment.class_id || section.class_id || '0', 10),
class_section_id: parseInt(assignment.class_section_id || section.class_section_id || '0', 10),
class_section_name: assignment.class_section_name || section.class_section_name || ''
});
});
});
out.sort((a, b) => String(a.student_name).localeCompare(String(b.student_name), undefined, { numeric: true }));
return out;
}
function renderClassRoster(tr, assignments) {
const wrap = document.createElement('div');
wrap.className = 'distribution-class-roster';
const title = document.createElement('div');
title.className = 'distribution-class-title';
const name = document.createElement('div');
name.className = 'fw-semibold';
name.textContent = 'Students in ' + (tr.dataset.className || 'class');
title.appendChild(name);
const count = document.createElement('div');
count.className = 'badge bg-secondary';
count.textContent = assignments.length + ' students';
title.appendChild(count);
wrap.appendChild(title);
if (!assignments.length) {
const empty = document.createElement('div');
empty.className = 'distribution-empty';
empty.textContent = 'No students assigned';
wrap.appendChild(empty);
return wrap;
}
wrap.appendChild(renderAssignmentList(assignments, true));
return wrap;
}
function renderAssignmentList(assignments, editable) {
const wrap = document.createElement('div');
wrap.className = 'distribution-students-wrap';
const header = document.createElement('div');
header.className = 'distribution-students-header';
['', 'Student', 'Age', 'Gender', 'Last Year', 'Assignment'].forEach(function(label){
const cell = document.createElement('span');
cell.textContent = label;
header.appendChild(cell);
});
wrap.appendChild(header);
const list = document.createElement('ol');
list.className = 'distribution-students';
const sortedAssignments = Array.isArray(assignments)
? assignments.slice().sort(function(a, b){
return String(a.student_name || '').localeCompare(String(b.student_name || ''), undefined, { numeric: true });
})
: [];
sortedAssignments.forEach(function(assignment){
const item = document.createElement('li');
item.dataset.searchText = [
assignment.student_name || '',
assignment.age_at_reference ?? '',
assignment.gender || '',
assignment.last_year_class_section || '',
assignmentLabel(assignment)
].join(' ').toLowerCase();
const row = document.createElement('div');
row.className = 'distribution-student-row';
const rowNumber = document.createElement('span');
rowNumber.className = 'student-row-number';
row.appendChild(rowNumber);
const studentLabel = document.createElement('span');
studentLabel.textContent = assignment.student_name || 'Student';
row.appendChild(studentLabel);
const ageCell = document.createElement('span');
ageCell.className = 'student-age-cell';
ageCell.textContent = assignment.age_at_reference === null || assignment.age_at_reference === undefined || assignment.age_at_reference === ''
? '-'
: assignment.age_at_reference;
row.appendChild(ageCell);
const genderCell = document.createElement('span');
genderCell.className = 'student-gender-cell';
genderCell.textContent = assignment.gender || '-';
row.appendChild(genderCell);
const lastYearCell = document.createElement('span');
lastYearCell.className = 'student-last-year-cell';
lastYearCell.textContent = assignment.last_year_class_section || '-';
row.appendChild(lastYearCell);
const assignmentCell = document.createElement('span');
assignmentCell.className = 'student-assignment-cell';
const draftId = parseInt(assignment.draft_id || '0', 10);
const studentId = parseInt(assignment.student_id || '0', 10);
if (editable && (draftId > 0 || studentId > 0) && baseClasses.length) {
const destinationSelect = document.createElement('select');
destinationSelect.className = 'form-select form-select-sm';
destinationSelect.setAttribute('aria-label', 'Change class and section for ' + (assignment.student_name || 'student'));
populateDestinationSelect(
destinationSelect,
parseInt(assignment.class_section_id || '0', 10),
parseInt(assignment.class_id || '0', 10)
);
destinationSelect.dataset.previousValue = destinationSelect.value;
destinationSelect.addEventListener('change', function(){
const target = selectedDestination(destinationSelect);
if (draftId > 0) {
updateDraftAssignment(draftId, target.classId, target.sectionId, destinationSelect);
} else {
updateCandidateAssignment(studentId, target.classId, target.sectionId, destinationSelect);
}
});
assignmentCell.appendChild(destinationSelect);
} else {
assignmentCell.textContent = assignmentLabel(assignment);
}
row.appendChild(assignmentCell);
item.appendChild(row);
list.appendChild(item);
});
wrap.appendChild(list);
return wrap;
}
function applyStudentSearch() {
const query = String((studentSearchInput && studentSearchInput.value) || '').trim().toLowerCase();
let totalStudents = 0;
let visibleStudents = 0;
document.querySelectorAll('.distribution-students li').forEach(function(item){
totalStudents++;
const matches = !query || String(item.dataset.searchText || '').indexOf(query) >= 0;
item.hidden = !matches;
if (matches) visibleStudents++;
});
document.querySelectorAll('.distribution-section, .distribution-class-roster').forEach(function(block){
const items = Array.from(block.querySelectorAll('.distribution-students li'));
block.hidden = !!query && items.length > 0 && !items.some(item => !item.hidden);
});
document.querySelectorAll('#tblBody tr').forEach(function(tr){
const items = Array.from(tr.querySelectorAll('.distribution-students li'));
tr.hidden = !!query && items.length > 0 && !items.some(item => !item.hidden);
});
if (studentSearchSummary) {
studentSearchSummary.textContent = query
? visibleStudents + ' of ' + totalStudents + ' students shown'
: '';
}
}
function renderAddSectionButton(container, tr, baseSectionId, visibleSections) {
const baseClassId = parseInt(tr.dataset.classId || '0', 10);
const allSections = sectionsByClassId[String(baseClassId)] || [];
const visibleSectionIds = visibleSections.map(section => parseInt(section.class_section_id || '0', 10));
const nextSection = allSections.find(section => visibleSectionIds.indexOf(section.id) < 0);
if (!nextSection) return;
const addWrap = document.createElement('div');
addWrap.className = 'mt-2';
const addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = 'btn btn-sm btn-outline-secondary';
addBtn.textContent = 'Add Section';
addBtn.addEventListener('click', function(){
const nextVisibleSections = visibleSections.concat([{
class_id: baseClassId,
class_section_id: nextSection.id,
class_section_name: nextSection.name,
total: 0,
student_names: [],
student_assignments: []
}]);
renderSectionsForRow(baseSectionId, nextVisibleSections, tr._unassignedAssignments || []);
});
addWrap.appendChild(addBtn);
container.appendChild(addWrap);
}
function updateDraftAssignment(draftId, classId, classSectionId, selectEl) {
if (!draftId || !classSectionId) {
msgEl.textContent = 'Select a valid target section.';
return;
}
const previousValue = selectEl ? selectEl.dataset.previousValue || selectEl.defaultValue || '' : '';
if (selectEl) selectEl.disabled = true;
const fd = new FormData();
fd.append('draft_id', String(draftId));
fd.append('class_section_id', String(classSectionId));
const csrfNameEl = document.getElementById('csrfName');
const csrfValueEl= document.getElementById('csrfValue');
if (csrfNameEl && csrfValueEl) {
fd.append(csrfNameEl.value, csrfValueEl.value);
}
msgEl.textContent = 'Updating draft assignment...';
fetch(updateDraftUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
.then(r => r.json())
.then(res => {
if (res && res.csrfTokenName && res.csrfHash) {
if (csrfNameEl) csrfNameEl.value = res.csrfTokenName;
if (csrfValueEl) csrfValueEl.value = res.csrfHash;
}
if (!res || !res.ok) {
if (selectEl && previousValue) selectEl.value = previousValue;
msgEl.textContent = res && res.message ? res.message : 'Draft assignment could not be updated.';
return;
}
msgEl.textContent = res.message || 'Draft assignment updated.';
if (selectEl) selectEl.dataset.previousValue = String(classSectionId);
ensureIncludedClassVisible(classId);
loadTotals();
})
.catch(() => {
if (selectEl && previousValue) selectEl.value = previousValue;
msgEl.textContent = 'Draft assignment could not be updated.';
})
.finally(() => {
if (selectEl) selectEl.disabled = false;
});
}
function updateCandidateAssignment(studentId, classId, classSectionId, selectEl) {
if (!studentId || !classSectionId) {
msgEl.textContent = 'Select a valid student and target assignment.';
return;
}
const previousValue = selectEl ? selectEl.dataset.previousValue || selectEl.defaultValue || '' : '';
if (selectEl) selectEl.disabled = true;
const fd = new FormData();
fd.append('student_id', String(studentId));
fd.append('class_section_id', String(classSectionId));
fd.append('school_year', selectedYear);
const csrfNameEl = document.getElementById('csrfName');
const csrfValueEl= document.getElementById('csrfValue');
if (csrfNameEl && csrfValueEl) {
fd.append(csrfNameEl.value, csrfValueEl.value);
}
msgEl.textContent = 'Saving candidate assignment...';
fetch(updateCandidateUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
.then(r => r.json())
.then(res => {
if (res && res.csrfTokenName && res.csrfHash) {
if (csrfNameEl) csrfNameEl.value = res.csrfTokenName;
if (csrfValueEl) csrfValueEl.value = res.csrfHash;
}
if (!res || !res.ok) {
if (selectEl && previousValue) selectEl.value = previousValue;
msgEl.textContent = res && res.message ? res.message : 'Candidate assignment could not be saved.';
return;
}
msgEl.textContent = res.message || 'Candidate assignment saved.';
if (selectEl) selectEl.dataset.previousValue = String(classSectionId);
ensureIncludedClassVisible(classId);
loadTotals();
})
.catch(() => {
if (selectEl && previousValue) selectEl.value = previousValue;
msgEl.textContent = 'Candidate assignment could not be saved.';
})
.finally(() => {
if (selectEl) selectEl.disabled = false;
});
}
function loadTotals() {
@@ -355,7 +914,6 @@
.then(res => {
if (!res || !res.ok) { msgEl.textContent = res && res.message ? res.message : 'Failed to load totals.'; return; }
buildInitialTable(res.rows || []);
updateNeeds();
msgEl.textContent = '';
})
.catch(() => { msgEl.textContent = 'Failed to load totals.'; });
@@ -402,6 +960,9 @@
});
refreshBtn.addEventListener('click', function(){ loadTotals(); });
if (studentSearchInput) {
studentSearchInput.addEventListener('input', applyStudentSearch);
}
document.getElementById('generateAllBtn').addEventListener('click', async function(){
const sectionCount = parseInt(sectionCountInput.value || '0', 10);
const minStudents = parseInt(minInput.value || '0', 10);
@@ -421,8 +982,6 @@
}
msgEl.textContent = 'All distributions completed.';
});
sectionCountInput.addEventListener('input', function(){ updateNeeds(); });
loadTotals();
})();
</script>
+29 -17
View File
@@ -64,6 +64,7 @@ $allergyOptions = $allergyOptions ?? [
];
$gradeOptions = $gradeOptions ?? ['KG', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', 'Youth'];
$selectedYear = trim((string)($selectedYear ?? ''));
?>
<?= $this->extend('layout/management_layout') ?>
@@ -279,8 +280,8 @@ $gradeOptions = $gradeOptions ?? ['KG', '1', '2', '3', '4', '5', '6', '7', '8',
<!-- age (readonly) -->
<div class="col-md-3">
<label class="form-label">Age</label>
<input type="number" name="age" class="form-control js-age" value="<?= (int)($student['age'] ?? 0) ?>" min="0" step="1" readonly>
<div class="form-text">Auto-calculated from DOB.</div>
<input type="number" name="age" class="form-control js-age" value="<?= esc((string)($student['age'] ?? '')) ?>" min="0" step="1" readonly>
<div class="form-text">Auto-calculated from DOB as of Sep 1 of the school year.</div>
</div>
<!-- registration_grade -->
@@ -589,32 +590,43 @@ $gradeOptions = $gradeOptions ?? ['KG', '1', '2', '3', '4', '5', '6', '7', '8',
// Bind all multi-selects present on the page (all modals rendered server-side)
document.querySelectorAll('select.js-multi').forEach(bindSelect);
const fallbackSchoolYear = <?= json_encode($selectedYear, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
function ageAsOfSchoolYearStart(dobValue, schoolYearValue) {
if (!dobValue) return '';
const yearMatch = String(schoolYearValue || fallbackSchoolYear || '').match(/^(\d{4})/);
if (!yearMatch) return '';
const birthParts = dobValue.split('-').map(Number);
if (birthParts.length !== 3 || birthParts.some(Number.isNaN)) return '';
const dob = new Date(birthParts[0], birthParts[1] - 1, birthParts[2]);
const cutoff = new Date(Number(yearMatch[1]), 8, 1);
if (Number.isNaN(dob.getTime()) || dob > cutoff) return '';
let age = cutoff.getFullYear() - dob.getFullYear();
const monthDelta = cutoff.getMonth() - dob.getMonth();
if (monthDelta < 0 || (monthDelta === 0 && cutoff.getDate() < dob.getDate())) age--;
return age >= 0 ? String(age) : '';
}
// Modal lifecycle: wire age + (re)bind selects inside shown modal
document.querySelectorAll('.modal').forEach((modal) => {
modal.addEventListener('shown.bs.modal', () => {
// Age from DOB
const dob = modal.querySelector('.js-dob');
const age = modal.querySelector('.js-age');
const schoolYear = modal.querySelector('input[name="school_year"]');
if (dob && age) {
const update = () => {
if (!dob.value) {
age.value = '';
return;
}
const d = new Date(dob.value + 'T00:00:00');
if (isNaN(d.getTime())) {
age.value = '';
return;
}
const t = new Date();
let a = t.getFullYear() - d.getFullYear();
const m = t.getMonth() - d.getMonth();
if (m < 0 || (m === 0 && t.getDate() < d.getDate())) a--;
age.value = a >= 0 ? a : '';
age.value = ageAsOfSchoolYearStart(dob.value, schoolYear ? schoolYear.value : '');
};
update();
dob.addEventListener('change', update);
dob.addEventListener('input', update);
if (schoolYear) {
schoolYear.addEventListener('change', update);
schoolYear.addEventListener('input', update);
}
}
modal.querySelectorAll('select.js-multi').forEach(bindSelect);
@@ -38,6 +38,7 @@
<th>Registration Date</th>
<th>Parent/Guardian</th>
<th>Student Name</th>
<th>Age</th>
<th>New Student</th>
<th>Removed (Prior Years)</th>
<th>Current Class</th>
@@ -78,6 +79,11 @@
<?php endif; ?>
</td>
<!-- Age -->
<td class="text-center">
<?= isset($student['age']) && $student['age'] !== '' && $student['age'] !== null ? esc((string)(int)$student['age']) : '-' ?>
</td>
<!-- New Student -->
<td>
<?php if (($student['new_student'] ?? 'Unknown') === 'Yes'): ?>
@@ -109,6 +115,9 @@
case 'admission under review':
echo '<span class="badge bg-primary">admission under review</span>';
break;
case 'review & decision':
echo '<span class="badge bg-secondary">review & decision</span>';
break;
case 'payment pending':
echo '<span class="badge bg-warning text-dark">payment pending</span>';
break;
@@ -145,6 +154,7 @@
data-parent-id="<?= $pid ?>"
data-prev="<?= esc($student['enrollment_status'] ?? '') ?>">
<option value="admission under review" <?= ($student['enrollment_status'] ?? '') === 'admission under review' ? 'selected' : '' ?>>admission under review</option>
<option value="review & decision" <?= ($student['enrollment_status'] ?? '') === 'review & decision' ? 'selected' : '' ?>>review &amp; decision</option>
<option value="payment pending" <?= ($student['enrollment_status'] ?? '') === 'payment pending' ? 'selected' : '' ?>>payment pending</option>
<option value="enrolled" <?= ($student['enrollment_status'] ?? '') === 'enrolled' ? 'selected' : '' ?>>enrolled</option>
<option value="withdraw under review" <?= ($student['enrollment_status'] ?? '') === 'withdraw under review' ? 'selected' : '' ?>>withdraw under review</option>
@@ -155,9 +165,9 @@
</select>
</td>
<!-- Assign Class (only for 'admission under review') -->
<!-- Assign Class (only for review statuses) -->
<td>
<?php $isUnderReview = (strtolower($student['enrollment_status'] ?? '') === 'admission under review'); ?>
<?php $isUnderReview = in_array(strtolower($student['enrollment_status'] ?? ''), ['admission under review', 'review & decision'], true); ?>
<?php if ($isUnderReview): ?>
<select class="form-select form-select-sm assign-class-select" <?= empty($isCurrentYear) ? 'disabled' : '' ?>
data-student-id="<?= $sid ?>"
@@ -181,7 +191,7 @@
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="9">No students available.</td>
<td colspan="10">No students available.</td>
</tr>
<?php endif; ?>
</tbody>
@@ -379,8 +389,8 @@
// Disable sort/search on interactive columns (status select, assign select)
columnDefs: [
{ targets: [7, 8], orderable: false, searchable: false },
{ targets: [0, 1, 2, 3, 4, 5, 6], render: function(data, type) {
{ targets: [8, 9], orderable: false, searchable: false },
{ targets: [0, 1, 2, 3, 4, 5, 6, 7], render: function(data, type) {
if (type === 'filter' || type === 'sort' || type === 'type') {
return stripHtml(data);
}
@@ -460,6 +470,7 @@
const s = norm(status);
switch (s) {
case 'admission under review': return '<span class="badge bg-primary">admission under review</span>';
case 'review & decision': return '<span class="badge bg-secondary">review &amp; decision</span>';
case 'payment pending': return '<span class="badge bg-warning text-dark">payment pending</span>';
case 'enrolled': return '<span class="badge bg-success">enrolled</span>';
case 'withdraw under review': return '<span class="badge bg-warning text-dark">withdraw under review</span>';
@@ -559,7 +570,7 @@
btn.disabled = true;
if (prog) prog.classList.remove('d-none');
// Collect tasks: rows where status is 'admission under review' and a class is selected
// Collect tasks: rows where status is a review state and a class is selected
const assigns = Array.from(document.querySelectorAll('.assign-class-select'));
const tasks = [];
const parentSet = new Set();
@@ -570,7 +581,7 @@
const desired = norm(statusSelect?.getAttribute('data-desired') || statusSelect?.value || '');
const classId = sel.value;
// Only include rows explicitly set to move to payment pending with a class selected
if (prev === 'admission under review' && desired === 'payment pending' && classId) {
if (['admission under review', 'review & decision'].includes(prev) && desired === 'payment pending' && classId) {
const studentId = sel.getAttribute('data-student-id');
const parentId = sel.getAttribute('data-parent-id');
const className = sel.options[sel.selectedIndex]?.text || '';
@@ -579,7 +590,7 @@
}
});
// Collect standalone status updates (not AUR->payment pending)
// Collect standalone status updates (not review-state -> payment pending)
const statusTasks = [];
const shouldInvoice = new Set(['payment pending','enrolled','withdrawn','refund pending','withdraw under review']);
document.querySelectorAll('select.enrollment-status').forEach(se => {
@@ -587,7 +598,7 @@
const prev = norm(se.getAttribute('data-prev') || '');
const desired = norm(se.getAttribute('data-desired') || '');
if (!desired || desired === prev) return;
if (prev === 'admission under review' && desired === 'payment pending') return; // handled by tasks
if (['admission under review', 'review & decision'].includes(prev) && desired === 'payment pending') return; // handled by tasks
const parentId = se.getAttribute('data-parent-id') || tr?.getAttribute('data-parent-id') || '';
statusTasks.push({ tr, studentId: se.getAttribute('data-student-id'), desired, parentId });
if (parentId && shouldInvoice.has(desired)) parentSet.add(parentId);
@@ -672,8 +683,8 @@
const rowData = r.data();
rowData[STATUS_COL_INDEX] = badgeForStatusJs(s.desired);
rowData[STATUS_SELECT_COL_INDEX] = buildStatusSelectHtml(s.studentId, s.tr.getAttribute('data-parent-id') || '', s.desired);
if (norm(s.desired) === 'admission under review') {
// leave current assign select as-is
if (['admission under review', 'review & decision'].includes(norm(s.desired))) {
rowData[ASSIGN_COL_INDEX] = buildAssignSelectHtml(s.studentId, s.tr.getAttribute('data-parent-id') || '', <?= json_encode($classes ?? [], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>);
} else {
rowData[ASSIGN_COL_INDEX] = '<span class="text-muted">—</span>';
}
@@ -688,10 +699,12 @@
statusSelect.removeAttribute('data-desired');
statusSelect.classList.remove('pending-status-select');
}
if (norm(s.desired) !== 'admission under review') {
const cells = s.tr.querySelectorAll('td');
const assignCell = cells[ASSIGN_COL_INDEX];
if (assignCell) assignCell.innerHTML = '<span class="text-muted">—</span>';
const cells = s.tr.querySelectorAll('td');
const assignCell = cells[ASSIGN_COL_INDEX];
if (assignCell) {
assignCell.innerHTML = ['admission under review', 'review & decision'].includes(norm(s.desired))
? buildAssignSelectHtml(s.studentId, s.tr.getAttribute('data-parent-id') || '', <?= json_encode($classes ?? [], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>)
: '<span class="text-muted">—</span>';
}
}
showRowToast(s.tr, 'Status updated.');
@@ -749,7 +762,7 @@
function buildStatusSelectHtml(studentId, parentId, current) {
const opts = [
'admission under review','payment pending','enrolled','withdraw under review','refund pending','withdrawn','waitlist','denied'
'admission under review','review & decision','payment pending','enrolled','withdraw under review','refund pending','withdrawn','waitlist','denied'
];
let html = `<select name="enrollment_status[${studentId}]" class="form-control enrollment-status" ${IS_CURRENT_YEAR ? '' : 'disabled'} data-student-id="${studentId}" data-parent-id="${parentId}" data-prev="${esc(current)}">`;
for (const o of opts) {
@@ -793,7 +806,7 @@
rowData[CLASS_COL_INDEX] = esc(st.class_section || 'Class not Assigned');
rowData[STATUS_COL_INDEX] = badgeForStatusJs(st.enrollment_status || '');
rowData[STATUS_SELECT_COL_INDEX] = buildStatusSelectHtml(id, tr.getAttribute('data-parent-id') || '', st.enrollment_status || '');
if (norm(st.enrollment_status || '') === 'admission under review') {
if (['admission under review', 'review & decision'].includes(norm(st.enrollment_status || ''))) {
rowData[ASSIGN_COL_INDEX] = buildAssignSelectHtml(id, tr.getAttribute('data-parent-id') || '', data.classes || []);
} else {
rowData[ASSIGN_COL_INDEX] = '<span class="text-muted">—</span>';
@@ -72,6 +72,9 @@
case 'admission under review':
echo '<span class="badge bg-primary">admission under review</span>';
break;
case 'review & decision':
echo '<span class="badge bg-secondary">review &amp; decision</span>';
break;
case 'payment pending':
echo '<span class="badge bg-warning text-dark">payment pending</span>';
break;
+16 -4
View File
@@ -8,6 +8,7 @@ $semester = 'year';
$schoolYear = $schoolYear ?? '';
$schoolYears = $schoolYears ?? [];
$isEditable = (bool)($isEditable ?? true);
if (empty($schoolYears) && $schoolYear !== '') {
$schoolYears = [$schoolYear];
@@ -102,6 +103,12 @@ if (empty($schoolYears) && $schoolYear !== '') {
</div>
<?php endif; ?>
<?php if (!$isEditable): ?>
<div class="alert alert-warning" role="alert">
This school year is closed. Decisions are read-only.
</div>
<?php endif; ?>
<?php
$decisionOptions = [
'' => '— No decision yet —',
@@ -214,11 +221,13 @@ if (empty($schoolYears) && $schoolYear !== '') {
<textarea name="notes"
class="form-control form-control-sm decision-notes"
rows="3"
placeholder="Add comments or rationale…"><?= esc($currentNotes) ?></textarea>
placeholder="Add comments or rationale…"
<?= $isEditable ? '' : 'readonly' ?>><?= esc($currentNotes) ?></textarea>
<div class="d-flex gap-2 mt-2 align-items-center">
<select name="decision"
class="form-select form-select-sm decision-select flex-grow-1">
class="form-select form-select-sm decision-select flex-grow-1"
<?= $isEditable ? '' : 'disabled' ?>>
<?php foreach ($decisionOptions as $val => $label): ?>
<option value="<?= esc($val) ?>" <?= $currentDecision === $val ? 'selected' : '' ?>>
<?= esc($label) ?>
@@ -226,7 +235,9 @@ if (empty($schoolYears) && $schoolYear !== '') {
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary">
<button type="submit"
class="btn btn-sm btn-primary"
<?= $isEditable ? '' : 'disabled' ?>>
Save
</button>
</div>
@@ -243,7 +254,8 @@ if (empty($schoolYears) && $schoolYear !== '') {
class="btn btn-sm btn-outline-primary btn-send-email"
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
data-semester="year"
data-school-year="<?= esc((string)$schoolYear) ?>">
data-school-year="<?= esc((string)$schoolYear) ?>"
<?= $isEditable ? '' : 'disabled' ?>>
Send Email
</button>
<?php else: ?>
+1 -1
View File
@@ -663,7 +663,7 @@
<h1 class="mb-4">Our Curriculum and Grade Structure</h1>
<p>Our program spans nine structured grade levels, each building upon the previous year's knowledge to ensure a solid foundation in faith, character, and Islamic learning.</p>
<p>Upon completing the 9th grade, students transition into our three-year Youth Program, which emphasizes deeper community engagement, personal development and practical application of Islamic principles. Participation in the Youth Program requires students to be at least 15 years old, ensuring they are mature enough to benefit from its advanced content.</p>
<p>Starting this academic year, children must be at least 6 years old by 12-31-2025 to enroll in Grade 1. For younger learners, we are pleased to offer our newly established Kindergarten class, welcoming children who are at least 5 years old by 12-31-2025. This early education program is designed to introduce young minds to the basics of Islamic teachings in a warm, age-appropriate environment.</p>
<p>Starting this academic year, children must be at least 6 years old by 09-01-2025 to enroll in Grade 1. For younger learners, we are pleased to offer our newly established Kindergarten class, welcoming children who are at least 5 years old by 12-31-2025. This early education program is designed to introduce young minds to the basics of Islamic teachings in a warm, age-appropriate environment.</p>
<a class="btn btn-success py-3 px-5 mt-3" href="/register">Get Started Now<i class="fa fa-arrow-right ms-2"></i></a>
</div>
</div>
+57 -85
View File
@@ -14,6 +14,11 @@ $deadlineObj = (new DateTime($lastDayOfRegistration, new DateTimeZone($tz)))->se
$nowObj = new DateTime('now', new DateTimeZone($tz));
$deadlinePassed = $nowObj > $deadlineObj;
$deadlineISO = $deadlineObj->format('Y-m-d\TH:i:sP'); // for JS
$familyFinancialSummary = is_array($familyFinancialSummary ?? null) ? $familyFinancialSummary : [];
$money = static function ($amount) use ($familyFinancialSummary): string {
$currency = (string) ($familyFinancialSummary['currency'] ?? '$');
return $currency . number_format((float) $amount, 2);
};
?>
<!-- Registration Info -->
<div class="alert alert-info mb-3">
@@ -25,84 +30,46 @@ $deadlineISO = $deadlineObj->format('Y-m-d\TH:i:sP'); // for JS
</ul>
</div>
<?php if ($familyFinancialSummary !== []): ?>
<div class="border rounded p-3 mb-3 bg-light">
<div class="fw-semibold mb-2">Family Account Information</div>
<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">Registration fee:</span>
<strong><?= 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">Mandatory fees:</span>
<strong><?= 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">Total currently due:</span>
<strong><?= 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>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php $hasAcceptedSchoolPolicy = (bool) ($hasAcceptedSchoolPolicy ?? false); ?>
<?php
$fallMakeupExamLabel = !empty($fallMakeupExamOn) ? local_date($fallMakeupExamOn, 'm-d-Y') : null;
$decisionMessageForStudent = static function (array $student) use ($fallMakeupExamLabel): array {
$decisionRow = is_array($student['previous_year_decision'] ?? null) ? $student['previous_year_decision'] : [];
$decision = trim((string) ($decisionRow['decision'] ?? ''));
$source = strtolower(trim((string) ($decisionRow['source'] ?? '')));
$decisionKey = strtolower($decision);
$name = trim((string) ($student['firstname'] ?? 'Your child') . ' ' . (string) ($student['lastname'] ?? ''));
$name = $name !== '' ? $name : 'Your child';
$previousClass = trim((string) ($decisionRow['class_section_name'] ?? ''));
$previousClass = $previousClass !== '' ? $previousClass : 'the same class';
if ($decisionKey === 'pass' || ($decisionKey === '' && $source !== 'pending')) {
return ['message' => '', 'blocking' => false, 'level' => 'info'];
}
if ($decisionKey === 'make-up exam in fall') {
$dateText = $fallMakeupExamLabel !== null ? ' on ' . $fallMakeupExamLabel : '';
return [
'message' => $name . ' has a make-up exam decision. Enrollment can be completed only after the fall make-up exam' . $dateText . ' and after the result is finalized.',
'blocking' => true,
'level' => 'warning',
];
}
if ($decisionKey === 'deferred decision') {
return [
'message' => $name . ' has a deferred decision. Enrollment can be completed only after you visit the administration to discuss your child\'s situation.',
'blocking' => true,
'level' => 'warning',
];
}
if ($decisionKey === 'repeat class') {
return [
'message' => $name . ' has a repeat class decision and is accepted only in ' . $previousClass . '.',
'blocking' => false,
'level' => 'info',
];
}
if ($decisionKey === 'expel') {
return [
'message' => $name . ' has an expel decision. Enrollment cannot be completed online; please contact the administration.',
'blocking' => true,
'level' => 'danger',
];
}
if ($decisionKey === 'withdrawn') {
return [
'message' => $name . ' was marked withdrawn in the final decision. Please contact the administration before requesting enrollment.',
'blocking' => true,
'level' => 'warning',
];
}
if ($decisionKey === '' || $source === 'pending') {
return [
'message' => $name . ' does not have a final promotion decision yet. Enrollment can be completed only after the administration finalizes the decision.',
'blocking' => true,
'level' => 'warning',
];
}
return [
'message' => $name . ' has a "' . $decision . '" decision. Please contact the administration before requesting enrollment.',
'blocking' => true,
'level' => 'warning',
];
};
?>
<?php if (!empty($students)): ?>
<form action="<?= base_url('/parent/enroll_classes_handler') ?>" method="post">
<?= csrf_field() ?>
@@ -118,6 +85,8 @@ $decisionMessageForStudent = static function (array $student) use ($fallMakeupEx
<th>Age</th>
<th>Gender</th>
<th>Grade</th>
<th>Decision</th>
<th>Required Action</th>
<th>Enroll</th>
<th>Withdraw</th>
<th>Status</th>
@@ -125,7 +94,7 @@ $decisionMessageForStudent = static function (array $student) use ($fallMakeupEx
</thead>
<tbody>
<?php foreach ($students as $index => $student): ?>
<?php $decisionMessage = $decisionMessageForStudent($student); ?>
<?php $eligibilityMessage = is_array($student['enrollment_eligibility_message'] ?? null) ? $student['enrollment_eligibility_message'] : ['message' => '', 'blocking' => false, 'level' => 'info']; ?>
<tr>
<td><?= $index + 1 ?></td>
<td><?= esc($student['school_id'] ?? 'N/A') ?></td>
@@ -141,19 +110,21 @@ $decisionMessageForStudent = static function (array $student) use ($fallMakeupEx
: 'Not Assigned';
?>
</td>
<td><?= esc($student['transition_evaluation']['decision_label'] ?? 'Pending') ?></td>
<td><?= esc($student['required_action_label'] ?? 'Contact the school administration.') ?></td>
<!-- Enroll Checkbox -->
<td>
<?php if ($student['enrollment_status'] === 'not enrolled'): ?>
<?php $disableEnrollUI = $deadlinePassed || !$isEditable; ?>
<?php $disableEnrollUI = $deadlinePassed || !$isEditable || (bool) ($eligibilityMessage['blocking'] ?? false); ?>
<input type="checkbox"
name="enroll[]"
value="<?= esc($student['id']) ?>"
data-decision-message="<?= esc($decisionMessage['message']) ?>"
data-decision-blocking="<?= $decisionMessage['blocking'] ? '1' : '0' ?>"
data-decision-message="<?= esc($eligibilityMessage['message'] ?? '') ?>"
data-decision-blocking="<?= !empty($eligibilityMessage['blocking']) ? '1' : '0' ?>"
data-decision-message-target="enrollment-decision-message-<?= esc($student['id']) ?>"
<?= $disableEnrollUI ? 'disabled' : '' ?>>
<?php elseif (in_array($student['enrollment_status'], ['enrolled', 'admission under review', 'payment pending', 'withdraw under review'])): ?>
<?php elseif (in_array($student['enrollment_status'], ['enrolled', 'admission under review', 'review & decision', 'payment pending', 'withdraw under review'])): ?>
<input type="checkbox" checked disabled>
<?php else: ?>
<input type="checkbox" disabled>
@@ -180,6 +151,9 @@ $decisionMessageForStudent = static function (array $student) use ($fallMakeupEx
case 'admission under review':
echo '<span class="badge bg-primary">admission under review</span>';
break;
case 'review & decision':
echo '<span class="badge bg-secondary">review &amp; decision</span>';
break;
case 'payment pending':
echo '<span class="badge bg-warning text-dark">payment pending</span>';
break;
@@ -210,11 +184,11 @@ $decisionMessageForStudent = static function (array $student) use ($fallMakeupEx
?>
</td>
</tr>
<?php if ($decisionMessage['message'] !== ''): ?>
<tr id="enrollment-decision-message-<?= esc($student['id']) ?>" class="enrollment-decision-message-row d-none">
<td colspan="10">
<div class="alert alert-<?= esc($decisionMessage['level']) ?> mb-0">
<?= esc($decisionMessage['message']) ?>
<?php if (($eligibilityMessage['message'] ?? '') !== ''): ?>
<tr id="enrollment-decision-message-<?= esc($student['id']) ?>" class="enrollment-decision-message-row">
<td colspan="12">
<div class="alert alert-<?= esc($eligibilityMessage['level'] ?? 'info') ?> mb-0">
<?= esc($eligibilityMessage['message']) ?>
</div>
</td>
</tr>
@@ -363,8 +337,6 @@ $decisionMessageForStudent = static function (array $student) use ($fallMakeupEx
// 1) Block clicking "enroll" checkboxes after deadline
document.querySelectorAll("input[name='enroll[]']").forEach(cb => {
cb.addEventListener("click", function(e) {
hideAllDecisionMessages();
if (deadlinePassed) {
e.preventDefault();
e.stopImmediatePropagation();
+29 -4
View File
@@ -59,6 +59,31 @@ if (!function_exists('parseDbDateTime')) {
}
}
}
if (!function_exists('formatCalendarDateOnly')) {
function formatCalendarDateOnly($raw): ?string
{
if (!$raw) return null;
if ($raw instanceof DateTimeInterface) {
return $raw->format('m-d-Y');
}
$s = trim((string)$raw);
if ($s === '' || preg_match('/^0{4}-0{2}-0{2}/', $s)) return null;
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $s, $m)) {
return $m[2] . '-' . $m[3] . '-' . $m[1];
}
if (preg_match('/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})/', $s, $m)) {
return sprintf('%02d-%02d-%04d', (int)$m[1], (int)$m[2], (int)$m[3]);
}
$ts = strtotime($s);
return $ts === false ? null : date('m-d-Y', $ts);
}
}
?>
<div class="container my-5">
@@ -71,14 +96,14 @@ if (!function_exists('parseDbDateTime')) {
<!-- Display Payment Notice / Deadline -->
<?php
$displayTz = user_timezone();
$deadline = parseDbDateTime($dueDate, 'UTC', $displayTz); // format either DATE or DATETIME safely
$deadlineDisplay = formatCalendarDateOnly($dueDate);
?>
<div class="alert alert-info mb-3 d-inline-block" style="display: inline-block; width: auto; padding: 10px;">
<ul class="mb-0">
<li>
This website release version does not have an option to pay your invoice online.
All payments this school year will be made in person on first day of school
(<strong><?= esc($deadline ? $deadline->format('m-d-Y') : 'TBD') ?></strong>).
(<strong><?= esc($deadlineDisplay ?: 'TBD') ?></strong>).
</li>
<li>
Cash, checks and debit/credit cards are all accepted forms of payment. However, if you elect to pay in
@@ -109,7 +134,7 @@ $deadline = parseDbDateTime($dueDate, 'UTC', $displayTz); // format either DATE
<?php foreach ($invoices as $index => $invoice): ?>
<?php
$issueDt = parseDbDateTime($invoice['issue_date'] ?? null, 'UTC', $displayTz); // assume stored UTC
$dueDt = parseDbDateTime($invoice['due_date'] ?? null, 'UTC', $displayTz, '12:00:00'); // if date-only, set noon
$dueDateDisplay = formatCalendarDateOnly($invoice['due_date'] ?? null);
$lastPay = parseDbDateTime($invoice['last_payment_date'] ?? null, 'UTC', $displayTz);
?>
@@ -117,7 +142,7 @@ $deadline = parseDbDateTime($dueDate, 'UTC', $displayTz); // format either DATE
<td><?= (int)$index + 1 ?></td>
<td><?= esc($invoice['invoice_number']) ?></td>
<td><?= $issueDt ? esc($issueDt->format('m-d-Y h:i A')) : '—' ?></td>
<td><?= $dueDt ? esc($dueDt->format('m-d-Y')) : '—' ?></td>
<td><?= $dueDateDisplay ? esc($dueDateDisplay) : '—' ?></td>
<td><?= esc($invoice['description'] ?? '') ?: '—' ?></td>
<td>$<?= number_format((float)($invoice['balance'] ?? 0), 2) ?></td>
<td>$<?= number_format((float)($invoice['last_paid_amount'] ?? 0), 2) ?></td>
+2 -1
View File
@@ -352,7 +352,8 @@ $closeAtFmt12 = $closeAt->format('m-d-Y g:i A T'); // e.g., 10-01-2025 12:00 AM
<script>
window.appConfig = {
registrationAgeDeadline: "<?= esc($registrationAgeDeadline) ?>"
registrationAgeDeadline: "<?= esc($registrationAgeDeadline) ?>",
schoolYearAgeDeadline: "<?= esc($schoolYearAgeDeadline ?? $registrationAgeDeadline) ?>"
};
</script>
<!-- Only keep the script imports -->
+3
View File
@@ -84,6 +84,7 @@ $role = strtolower(session()->get('role') ?? 'guest');
<a class="dropdown-item" href="/rfid_coming_soon">Attendance Scans</a>
<a class="dropdown-item" href="/administrator/class_assignment">Classes List</a>
<a class="dropdown-item" href="/administrator/emergency_contact">Emergency Contact</a>
<a class="dropdown-item" href="/administrator/enrollment-admin">Enrollment Administration</a>
<a class="dropdown-item" href="/enroll_withdraw/enrollment_withdrawal">Enrollment-Withdrawal</a>
<!--a class="dropdown-item" href="/administrator/exam_management">Exams Management</a-->
<a class="dropdown-item" href="/flags/flags_management">Flags Management</a>
@@ -213,6 +214,7 @@ $role = strtolower(session()->get('role') ?? 'guest');
<!--a class="dropdown-item" href="/rfid_coming_soon">Attendance Scans</a-->
<a class="dropdown-item" href="/administrator/class_assignment">Classes List</a>
<a class="dropdown-item" href="/administrator/emergency_contact">Emergency Contact</a>
<a class="dropdown-item" href="/administrator/enrollment-admin">Enrollment Administration</a>
<a class="dropdown-item" href="/enroll_withdraw/enrollment_withdrawal">Enrollment-Withdrawal</a>
<!--a class="dropdown-item" href="/administrator/exam_management">Exams Management</a-->
<a class="dropdown-item" href="/flags/flags_management">Flags Management</a>
@@ -301,6 +303,7 @@ $role = strtolower(session()->get('role') ?? 'guest');
<!--a class="dropdown-item" href="/rfid_coming_soon">Attendance Scans</a-->
<a class="dropdown-item" href="/administrator/class_assignment">Classes List</a>
<a class="dropdown-item" href="/administrator/emergency_contact">Emergency Contact</a>
<a class="dropdown-item" href="/administrator/enrollment-admin">Enrollment Administration</a>
<a class="dropdown-item" href="/enroll_withdraw/enrollment_withdrawal">Enrollment-Withdrawal</a>
<!--a class="dropdown-item" href="/administrator/exam_management">Exams Management</a-->
<a class="dropdown-item" href="/flags/flags_management">Flags Management</a>
+2 -2
View File
@@ -32,7 +32,7 @@ We therefore expect your support and cooperation to achieve the goals and object
'subsections' => [
[
'title' => 'Registration',
'body' => 'The school offers a solid curriculum of Islamic Studies and Quran/Arabic through 9 progressive grades, after which the students will join a 3-year rotation youth program <u>with a minimum age rule of at least 15</u>. <strong><u>Starting this year, to be eligible to get into 1st grade, your child needs to be at least 6 years old by 12-31-2025. Younger students will have the possibility to be enrolled in our newly created Kindergarten class if they are at least 5 years old by 12-31-2025.</u></strong>
'body' => 'The school offers a solid curriculum of Islamic Studies and Quran/Arabic through 9 progressive grades, after which the students will join a 3-year rotation youth program <u>with a minimum age rule of at least 15</u>. <strong><u>Starting this year, to be eligible to get into 1st grade, your child needs to be at least 6 years old by 09-01-2025. Younger students will have the possibility to be enrolled in our newly created Kindergarten class if they are at least 5 years old by 12-31-2025.</u></strong>
Registration this year will open <strong><u>from 09-01-2025 to 10-01-2025</u></strong>. We highly encourage parents to enroll their children early so they will not miss out on early classes during the year. <strong><u>By 10-01-2025 at midnight</u></strong>, registration will have closed and parents will not be allowed to register their kids except if the said parents just recently moved to the area from a faraway town/city/state.
@@ -40,7 +40,7 @@ The school will make every effort to broadcast the opening of the registration c
Please make sure your contact information is correct and up to date especially the phone numbers and emails section because that would be our only way to get in contact with you regarding school matters. Also, please make sure to fill out the known allergies section accurately so that we ensure your kids are never presented with food they could be allergic to.
<strong><u>We only accept students with ages between 5 and 18</u></strong>. For younger students, they must be <strong><u>at least 6 years old by 12-31-2025</u></strong> to be admitted, there will be no exceptions. <strong><u>Students that are 5 years old by 12-31-2025 can enroll in our newly created Kindergarten class</u></strong>. Students outside of the range described above are considered too young or too old for the activities offered by the school and will not be eligible to enroll with us. Please do not register your kids if they do not meet the criteria described above.
<strong><u>We only accept students with ages between 5 and 18</u></strong>. For younger students, they must be <strong><u>at least 6 years old by 09-01-2025</u></strong> to be admitted above Kindergarten, there will be no exceptions. <strong><u>Students that are 5 years old by 12-31-2025 can enroll in our newly created Kindergarten class</u></strong>. Students outside of the range described above are considered too young or too old for the activities offered by the school and will not be eligible to enroll with us. Please do not register your kids if they do not meet the criteria described above.
After the registration period ends, parents will be invited to join WhatsApp grade groups where their kids are enrolled. These groups allow teachers, parents and administration to stay close and communicate efficiently about all school matters and events.'
],