fix enrollment logic, add financial aid, fix class distribution
Tests / PHPUnit (push) Failing after 1m6s

This commit is contained in:
root
2026-08-15 15:07:16 -04:00
parent c12bb59372
commit 4603d9ced2
95 changed files with 6892 additions and 1295 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid py-3">
<h2 class="mb-3">Financial Aid Requests</h2>
<?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; ?>
<form method="get" class="row g-2 mb-3">
<div class="col-md-3">
<input class="form-control" name="school_year" placeholder="School year" value="<?= esc($schoolYear ?? '') ?>">
</div>
<div class="col-md-2">
<button class="btn btn-primary" type="submit">Filter</button>
</div>
</form>
<div class="table-responsive">
<table class="table table-striped no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
<th>ID</th>
<th>Parent</th>
<th>Year</th>
<th>Status</th>
<th>Requested</th>
<th>Submitted</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach (($requests ?? []) as $row): ?>
<?php $parent = $parents[(int) ($row['parent_id'] ?? 0)] ?? []; ?>
<tr>
<td><?= (int) ($row['id'] ?? 0) ?></td>
<td><?= esc(trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? '')) ?: ('#' . (int) ($row['parent_id'] ?? 0))) ?></td>
<td><?= esc($row['school_year'] ?? '') ?></td>
<td><?= esc($row['status'] ?? '') ?></td>
<td><?= $row['requested_amount'] !== null && $row['requested_amount'] !== '' ? '$' . number_format((float) $row['requested_amount'], 2) : '—' ?></td>
<td><?= esc($row['created_at'] ?? '') ?></td>
<td><a class="btn btn-sm btn-outline-primary" href="<?= site_url('administrator/financial-aid/' . (int) $row['id']) ?>">Review</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($requests)): ?>
<tr><td colspan="7" class="text-muted text-center">No financial aid requests found.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?= $this->endSection() ?>
@@ -0,0 +1,52 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container py-3">
<a href="<?= site_url('administrator/financial-aid') ?>" class="small">&larr; Back to queue</a>
<h2 class="mt-2">Review Financial Aid Request #<?= (int) ($requestRow['id'] ?? 0) ?></h2>
<?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">
<div><strong>Parent:</strong> <?= esc(trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''))) ?> <?= esc($parent['email'] ?? '') ?></div>
<div><strong>School year:</strong> <?= esc($requestRow['school_year'] ?? '') ?></div>
<div><strong>Status:</strong> <?= esc($requestRow['status'] ?? '') ?></div>
<div><strong>Household size:</strong> <?= esc((string) ($requestRow['household_size'] ?? 'Not provided')) ?></div>
<div><strong>Requested amount:</strong> <?= $requestRow['requested_amount'] !== null && $requestRow['requested_amount'] !== '' ? '$' . number_format((float) $requestRow['requested_amount'], 2) : 'Not specified' ?></div>
<div class="mt-2"><strong>Need statement</strong></div>
<p><?= nl2br(esc($requestRow['need_statement'] ?? '')) ?></p>
<div><strong>Students</strong></div>
<ul>
<?php foreach (($students ?? []) as $student): ?>
<li><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php if (in_array((string) ($requestRow['status'] ?? ''), ['submitted', 'under_review'], true)): ?>
<div class="row g-3">
<div class="col-md-6">
<form method="post" action="<?= site_url('administrator/financial-aid/' . (int) $requestRow['id'] . '/approve') ?>" class="border rounded p-3">
<?= csrf_field() ?>
<h5>Approve</h5>
<label class="form-label" for="admin_amount">Amount to apply</label>
<input class="form-control mb-2" type="number" min="0.01" step="0.01" name="admin_amount" id="admin_amount" required value="<?= esc(old('admin_amount', $requestRow['requested_amount'] ?? '')) ?>">
<label class="form-label" for="admin_note">Note</label>
<textarea class="form-control mb-2" name="admin_note" id="admin_note" rows="3"><?= esc(old('admin_note')) ?></textarea>
<button class="btn btn-success" type="submit">Approve and apply to invoice</button>
</form>
</div>
<div class="col-md-6">
<form method="post" action="<?= site_url('administrator/financial-aid/' . (int) $requestRow['id'] . '/deny') ?>" class="border rounded p-3">
<?= csrf_field() ?>
<h5>Deny</h5>
<label class="form-label" for="deny_note">Reason</label>
<textarea class="form-control mb-2" name="admin_note" id="deny_note" rows="3" required><?= esc(old('admin_note')) ?></textarea>
<button class="btn btn-danger" type="submit">Deny request</button>
</form>
</div>
</div>
<?php endif; ?>
</div>
<?= $this->endSection() ?>
@@ -152,17 +152,17 @@
<div class="card-body">
<div class="row g-3 align-items-end mb-2">
<div class="col-md-2">
<label for="sectionCount" class="form-label">Number of Sections</label>
<label for="sectionCount" class="form-label">Max Number of Sections</label>
<input type="number" min="1" id="sectionCount" class="form-control" placeholder="e.g. 2" />
</div>
<div class="col-md-2">
<label for="minStudents" class="form-label">Minimum Students</label>
<label for="minStudents" class="form-label">Min Students Per Section</label>
<input type="number" min="1" id="minStudents" class="form-control" placeholder="e.g. 20" />
</div>
<div class="col-md-2">
<!--div class="col-md-2">
<label for="maxStudents" class="form-label">Maximum Students</label>
<input type="number" min="1" id="maxStudents" class="form-control" placeholder="Optional" />
</div>
</div-->
<div class="col-md-3 d-flex gap-2">
<button type="button" id="refreshTotalsBtn" class="btn btn-outline-secondary">Refresh Totals</button>
<button type="button" id="generateAllBtn" class="btn btn-primary">Generate All</button>
@@ -217,6 +217,13 @@
</tr>
</thead>
<tbody id="tblBody"></tbody>
<tfoot class="table-light">
<tr>
<th>Total</th>
<th class="text-end" id="allStudentsTotal">0</th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
@@ -247,6 +254,7 @@
}, $classes ?? [])), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
const tblBody = document.getElementById('tblBody');
const allStudentsTotal = document.getElementById('allStudentsTotal');
const sectionCountInput = document.getElementById('sectionCount');
const minInput = document.getElementById('minStudents');
const maxInput = document.getElementById('maxStudents');
@@ -352,6 +360,20 @@
});
});
if (!resolvedClassId && selectedClassId > 0) {
const firstSection = (sectionsByClassId[String(selectedClassId)] || [])[0] || null;
if (firstSection) {
select.value = String(firstSection.id);
resolvedClassId = selectedClassId;
} else {
const baseClass = baseClasses.find(c => c.class_id === selectedClassId) || null;
if (baseClass) {
select.value = String(baseClass.class_section_id);
resolvedClassId = selectedClassId;
}
}
}
const selectedOption = select.options[select.selectedIndex] || null;
if (!resolvedClassId && selectedOption) {
resolvedClassId = parseInt(selectedOption.dataset.classId || '0', 10);
@@ -380,8 +402,10 @@
function buildInitialTable(rows) {
tblBody.innerHTML = '';
rowIndexByClassId = {};
let allTotal = 0;
rows.forEach(function(r){
allTotal += parseInt(r.total || '0', 10) || 0;
const tr = document.createElement('tr');
tr.dataset.classId = r.class_id;
tr.dataset.classSectionId = r.class_section_id;
@@ -417,13 +441,16 @@
renderSectionsForRow(r.class_section_id, r.sections, r.students || []);
}
});
if (allStudentsTotal) {
allStudentsTotal.textContent = String(allTotal);
}
applyStudentSearch();
}
function runDistribution(baseSectionId, baseName) {
const sectionCount = parseInt(sectionCountInput.value || '0', 10);
const minStudents = parseInt(minInput.value || '0', 10);
const maxStudents = parseInt(maxInput.value || '0', 10);
const maxStudents = parseInt((maxInput && maxInput.value) || '0', 10);
if (!sectionCount || sectionCount <= 0 || !minStudents || minStudents <= 0) {
msgEl.textContent = 'Enter number of sections and minimum students first.';
return;
@@ -611,6 +638,7 @@
student_name: assignment.student_name || 'Student',
age_at_reference: assignment.age_at_reference ?? null,
gender: assignment.gender || '',
previous_final_score: assignment.previous_final_score ?? null,
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),
@@ -623,6 +651,27 @@
return out;
}
function assignmentStats(assignments) {
const stats = { male: 0, female: 0, scoreTotal: 0, scoreCount: 0 };
(Array.isArray(assignments) ? assignments : []).forEach(function(assignment){
const gender = String(assignment.gender || '').trim().toLowerCase();
if (gender === 'female' || gender === 'f') {
stats.female++;
} else if (gender === 'male' || gender === 'm') {
stats.male++;
}
const score = Number(assignment.previous_final_score);
if (Number.isFinite(score)) {
stats.scoreTotal += score;
stats.scoreCount++;
}
});
stats.averageScore = stats.scoreCount ? stats.scoreTotal / stats.scoreCount : null;
return stats;
}
function renderClassRoster(tr, assignments) {
const wrap = document.createElement('div');
wrap.className = 'distribution-class-roster';
@@ -641,6 +690,23 @@
title.appendChild(count);
wrap.appendChild(title);
const stats = assignmentStats(assignments);
const meta = document.createElement('div');
meta.className = 'distribution-meta';
if (stats.averageScore !== null) {
const avg = document.createElement('span');
avg.className = 'badge text-bg-light';
avg.textContent = 'Avg ' + stats.averageScore.toFixed(2);
meta.appendChild(avg);
}
if (stats.male || stats.female) {
const gender = document.createElement('span');
gender.className = 'badge text-bg-light';
gender.textContent = 'M ' + stats.male + ' / F ' + stats.female;
meta.appendChild(gender);
}
if (meta.children.length) wrap.appendChild(meta);
if (!assignments.length) {
const empty = document.createElement('div');
empty.className = 'distribution-empty';
+3 -24
View File
@@ -30,7 +30,6 @@ $warningText = static function (array $warnings): string {
'include_event_only' => !empty($filters['include_event_only']) ? '1' : '0',
'include_paid_invoices' => !empty($filters['include_paid_invoices']) ? '1' : '0',
'unit_price' => $filters['unit_price'] ?? '',
'youth_unit_price' => $filters['youth_unit_price'] ?? '',
]);
?>
<a href="<?= site_url('administrator/tuition-forecast/export?' . $exportQuery) ?>" class="btn btn-success">
@@ -71,7 +70,7 @@ $warningText = static function (array $warnings): string {
</select>
</div>
<div class="col-md-3">
<label for="unit_price" class="form-label">Grades Unit Price</label>
<label for="unit_price" class="form-label">Base Tuition</label>
<input
type="number"
min="0"
@@ -80,19 +79,7 @@ $warningText = static function (array $warnings): string {
name="unit_price"
class="form-control"
value="<?= esc((string) ($filters['unit_price'] ?? ($summary['unit_price'] ?? ''))) ?>"
placeholder="New tuition unit price for grades">
</div>
<div class="col-md-3">
<label for="youth_unit_price" class="form-label">Youth Unit Price</label>
<input
type="number"
min="0"
step="0.01"
id="youth_unit_price"
name="youth_unit_price"
class="form-control"
value="<?= esc((string) ($filters['youth_unit_price'] ?? ($summary['youth_unit_price'] ?? ''))) ?>"
placeholder="New tuition unit price for youth">
placeholder="First student tuition">
</div>
<div class="col-md-2">
<div class="form-check">
@@ -149,19 +136,11 @@ $warningText = static function (array $warnings): string {
<div class="col-xl col-lg-3 col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">Grades Unit Price</div>
<div class="text-muted small">Base Tuition</div>
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['unit_price'] ?? 0)) ?></div>
</div>
</div>
</div>
<div class="col-xl col-lg-3 col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">Youth Unit Price</div>
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['youth_unit_price'] ?? 0)) ?></div>
</div>
</div>
</div>
<div class="col-xl col-lg-3 col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">