fix closing year
Tests / PHPUnit (push) Successful in 1m20s

This commit is contained in:
root
2026-08-10 01:11:16 -04:00
parent 1ef2800f12
commit c12bb59372
5 changed files with 416 additions and 126 deletions
+136 -105
View File
@@ -8,17 +8,8 @@
$finance = $preview['finance'] ?? [];
$promotion = $preview['promotion'] ?? ['summary' => [], 'rows' => []];
$promotionSummary = $promotion['summary'] ?? [];
$promotionTable = $promotionTable ?? [];
$promotionRows = $promotionTable['rows'] ?? ($promotion['rows'] ?? []);
$promotionSort = (string) ($promotionTable['sort'] ?? 'class');
$promotionOrder = (string) ($promotionTable['order'] ?? 'asc');
$promotionPage = (int) ($promotionTable['page'] ?? 1);
$promotionPerPage = (int) ($promotionTable['perPage'] ?? 25);
$promotionTotal = (int) ($promotionTable['total'] ?? count($promotionRows));
$promotionPageCount = (int) ($promotionTable['pageCount'] ?? 1);
$promotionFrom = (int) ($promotionTable['from'] ?? ($promotionTotal === 0 ? 0 : 1));
$promotionTo = (int) ($promotionTable['to'] ?? count($promotionRows));
$promotionPerPageOptions = $promotionTable['allowedPerPage'] ?? [10, 25, 50, 100];
$promotionRows = $promotion['rows'] ?? [];
$promotionTotal = count($promotionRows);
$blockers = $preview['blockers'] ?? [];
$warnings = $preview['warnings'] ?? [];
$carryForward = $preview['carry_forward'] ?? [];
@@ -27,27 +18,61 @@
$missingCarryForwardInvoices = (int) ($missingCarryForwardInvoices ?? 0);
$money = static fn ($value): string => '$' . number_format((float) $value, 2);
$score = static fn ($value): string => is_numeric($value) ? number_format((float) $value, 2) : '-';
$promotionUrl = static function (array $overrides = []) use ($source, $target, $promotionSort, $promotionOrder, $promotionPage, $promotionPerPage): string {
$query = array_merge([
'target_school_year_id' => $target['id'] ?? null,
'sort' => $promotionSort,
'order' => $promotionOrder,
'page' => $promotionPage,
'per_page' => $promotionPerPage,
], $overrides);
$query = array_filter($query, static fn ($value): bool => $value !== null && $value !== '');
$ageBySchoolYearSecondSeptember = static function (array $row) use ($source): string {
$dob = trim((string) ($row['dob'] ?? ''));
$schoolYear = (string) ($source['name'] ?? '');
if ($dob === '' || preg_match('/^\d{4}-(\d{4})$/', $schoolYear, $matches) !== 1) {
return '';
}
return site_url('administrator/school-years/' . (int) ($source['id'] ?? 0) . '/closing/preview?' . http_build_query($query));
try {
$birthDate = new DateTimeImmutable($dob);
$cutoff = new DateTimeImmutable($matches[1] . '-09-01');
} catch (Throwable) {
return '';
}
if ($birthDate > $cutoff) {
return '';
}
return (string) $birthDate->diff($cutoff)->y;
};
$sortLink = static function (string $key, string $label, string $class = '') use ($promotionSort, $promotionOrder, $promotionUrl): string {
$nextOrder = $promotionSort === $key && $promotionOrder === 'asc' ? 'desc' : 'asc';
$indicator = $promotionSort === $key ? ($promotionOrder === 'asc' ? ' (asc)' : ' (desc)') : '';
$kgPlacementFallback = static function (array $row) use ($target): string {
$classText = strtoupper(trim(
(string) ($row['class_name'] ?? '') . ' '
. (string) ($row['class_section_name'] ?? '')
));
if (preg_match('/(^|[^A-Z0-9])KG([^A-Z0-9]|$)/', $classText) !== 1 && ! str_contains($classText, 'KINDERGARTEN')) {
return '';
}
return '<a class="link-dark text-decoration-none ' . esc($class, 'attr') . '" href="' . esc($promotionUrl([
'sort' => $key,
'order' => $nextOrder,
'page' => 1,
]), 'attr') . '">' . esc($label . $indicator) . '</a>';
$dob = trim((string) ($row['dob'] ?? ''));
if ($dob === '') {
return '';
}
try {
$targetName = (string) ($target['name'] ?? '');
if (preg_match('/^(\d{4})-\d{4}$/', $targetName, $matches) === 1) {
$cutoff = new DateTimeImmutable($matches[1] . '-09-01');
} else {
$today = new DateTimeImmutable('today');
$cutoff = new DateTimeImmutable($today->format('Y') . '-09-01');
if ($today > $cutoff) {
$cutoff = $cutoff->modify('+1 year');
}
}
$birthDate = new DateTimeImmutable($dob);
} catch (Throwable) {
return '';
}
if ($birthDate > $cutoff) {
return '';
}
return $birthDate->diff($cutoff)->y >= 6 ? '1' : 'KG';
};
?>
@@ -188,7 +213,7 @@
</div>
</div>
<div class="border rounded bg-white p-3 mb-4">
<div class="border rounded bg-white p-3 mb-4" id="promotion-table">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h5 class="mb-1">Student Promotion Preview</h5>
@@ -232,52 +257,26 @@
</div>
</div>
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<div class="text-muted small">
Showing <?= esc((string) $promotionFrom) ?>-<?= esc((string) $promotionTo) ?> of <?= esc((string) $promotionTotal) ?> students
</div>
<form class="d-flex align-items-center gap-2" method="get" action="<?= site_url('administrator/school-years/' . (int) ($source['id'] ?? 0) . '/closing/preview') ?>">
<?php if ($target): ?>
<input type="hidden" name="target_school_year_id" value="<?= (int) $target['id'] ?>">
<?php endif; ?>
<input type="hidden" name="sort" value="<?= esc($promotionSort, 'attr') ?>">
<input type="hidden" name="order" value="<?= esc($promotionOrder, 'attr') ?>">
<input type="hidden" name="page" value="1">
<label class="form-label small text-muted mb-0" for="promotion_per_page">Rows</label>
<select class="form-select form-select-sm w-auto" id="promotion_per_page" name="per_page" onchange="this.form.submit()">
<?php foreach ($promotionPerPageOptions as $option): ?>
<option value="<?= (int) $option ?>" <?= (int) $option === $promotionPerPage ? 'selected' : '' ?>>
<?= esc((string) $option) ?>
</option>
<?php endforeach; ?>
</select>
</form>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
<table id="promotionPreviewTable" class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
<th><?= $sortLink('student', 'Student') ?></th>
<th><?= $sortLink('school_id', 'School ID') ?></th>
<th><?= $sortLink('class', 'Class') ?></th>
<th class="text-end"><?= $sortLink('year_score', 'Year Score') ?></th>
<th><?= $sortLink('decision', 'Decision') ?></th>
<th><?= $sortLink('source', 'Source') ?></th>
<th><?= $sortLink('queue', 'Queue') ?></th>
<th><?= $sortLink('target', 'Next Year Placement') ?></th>
<th><?= $sortLink('status', 'Status') ?></th>
<th>Student</th>
<th>School ID</th>
<th>Class</th>
<th class="text-end">Age Sep 1</th>
<th class="text-end">Year Score</th>
<th>Decision</th>
<th>Source</th>
<th>Next Year Placement</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php if ($promotionTotal === 0): ?>
<tr><td colspan="9" class="text-muted">No active class assignments found for this school year.</td></tr>
<?php endif; ?>
<?php foreach ($promotionRows as $row): ?>
<?php
$rowStatus = (string) ($row['status'] ?? 'missing');
$statusClass = $rowStatus === 'decided' ? 'success' : 'danger';
$queueStatus = (string) ($row['queue_status'] ?? '');
$sourceLabel = (string) ($row['source'] ?? '');
if ($sourceLabel === 'automatic_kg_age') {
$sourceLabel = 'Auto KG age';
@@ -286,54 +285,32 @@
if ($targetLabel === '') {
$targetLabel = trim((string) ($row['target_class'] ?? ''));
}
if ($targetLabel === '') {
$targetLabel = $kgPlacementFallback($row);
}
$ageSepOne = $ageBySchoolYearSecondSeptember($row);
?>
<tr>
<td><?= esc($row['student_name'] ?? ('Student #' . (int) ($row['student_id'] ?? 0))) ?></td>
<td><?= esc($row['school_id'] ?? '') ?></td>
<td><?= esc($row['class_section_name'] ?? '') ?></td>
<td class="text-end"><?= esc($score($row['year_score'] ?? null)) ?></td>
<td class="text-end" data-order="<?= esc($ageSepOne, 'attr') ?>"><?= esc($ageSepOne !== '' ? $ageSepOne : '-') ?></td>
<td class="text-end" data-order="<?= esc(is_numeric($row['year_score'] ?? null) ? (string) $row['year_score'] : '', 'attr') ?>"><?= esc($score($row['year_score'] ?? null)) ?></td>
<td><?= esc(($row['decision'] ?? '') !== '' ? $row['decision'] : '-') ?></td>
<td><?= esc($sourceLabel) ?></td>
<td><?= esc($queueStatus !== '' ? ucfirst($queueStatus) : '-') ?></td>
<td><?= esc($targetLabel !== '' ? $targetLabel : '-') ?></td>
<td><span class="badge bg-<?= esc($statusClass) ?>"><?= esc(ucfirst($rowStatus)) ?></span></td>
<td data-order="<?= esc($targetLabel, 'attr') ?>"><?= esc($targetLabel !== '' ? $targetLabel : '-') ?></td>
<td data-order="<?= esc($rowStatus, 'attr') ?>"><span class="badge bg-<?= esc($statusClass) ?>"><?= esc(ucfirst($rowStatus)) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($promotionPageCount > 1): ?>
<?php
$pageStart = max(1, $promotionPage - 2);
$pageEnd = min($promotionPageCount, $promotionPage + 2);
if ($pageEnd - $pageStart < 4) {
$pageStart = max(1, $pageEnd - 4);
$pageEnd = min($promotionPageCount, $pageStart + 4);
}
?>
<nav aria-label="Student promotion pages">
<ul class="pagination pagination-sm justify-content-end mb-0">
<li class="page-item <?= $promotionPage <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="<?= esc($promotionUrl(['page' => max(1, $promotionPage - 1)]), 'attr') ?>">Previous</a>
</li>
<?php for ($pageNumber = $pageStart; $pageNumber <= $pageEnd; $pageNumber++): ?>
<li class="page-item <?= $pageNumber === $promotionPage ? 'active' : '' ?>">
<a class="page-link" href="<?= esc($promotionUrl(['page' => $pageNumber]), 'attr') ?>"><?= esc((string) $pageNumber) ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $promotionPage >= $promotionPageCount ? 'disabled' : '' ?>">
<a class="page-link" href="<?= esc($promotionUrl(['page' => min($promotionPageCount, $promotionPage + 1)]), 'attr') ?>">Next</a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
<div class="border rounded bg-white p-3 mb-4">
<div class="border rounded bg-white p-3 mb-4" id="carry-forward-table">
<h5>Carry-Forward Families</h5>
<div class="table-responsive">
<table class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
<table id="carryForwardFamiliesTable" class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
<th>Family</th>
@@ -345,9 +322,6 @@
</tr>
</thead>
<tbody>
<?php if ($carryForward === []): ?>
<tr><td colspan="6" class="text-muted">No carry-forward balances found.</td></tr>
<?php endif; ?>
<?php foreach ($carryForward as $row): ?>
<tr>
<td><?= esc($row['family']) ?></td>
@@ -357,10 +331,10 @@
<div class="text-muted small"><?= esc($row['parent_email']) ?></div>
<?php endif; ?>
</td>
<td class="text-end"><?= esc($money($row['source_balance'])) ?></td>
<td class="text-end"><?= esc($money($row['credit_amount'])) ?></td>
<td class="text-end"><?= esc($money($row['adjustment_amount'])) ?></td>
<td class="text-end"><?= esc($money($row['carry_forward_amount'])) ?></td>
<td class="text-end" data-order="<?= esc((string) (float) ($row['source_balance'] ?? 0), 'attr') ?>"><?= esc($money($row['source_balance'])) ?></td>
<td class="text-end" data-order="<?= esc((string) (float) ($row['credit_amount'] ?? 0), 'attr') ?>"><?= esc($money($row['credit_amount'])) ?></td>
<td class="text-end" data-order="<?= esc((string) (float) ($row['adjustment_amount'] ?? 0), 'attr') ?>"><?= esc($money($row['adjustment_amount'])) ?></td>
<td class="text-end" data-order="<?= esc((string) (float) ($row['carry_forward_amount'] ?? 0), 'attr') ?>"><?= esc($money($row['carry_forward_amount'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
@@ -421,3 +395,60 @@
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return;
}
try {
Object.keys(window.localStorage || {}).forEach(function (key) {
if (key.indexOf('DataTables_promotionPreviewTable_') === 0 || key.indexOf('DataTables_carryForwardFamiliesTable_') === 0) {
window.localStorage.removeItem(key);
}
});
} catch (error) {
// Ignore storage restrictions; DataTables can still initialize without saved state.
}
var baseOptions = {
paging: true,
pageLength: 25,
lengthMenu: [[25, 50, 100, 200, -1], [25, 50, 100, 200, 'All']],
pagingType: 'full_numbers',
stateSave: false,
autoWidth: false,
dom: "<'row mb-2'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
"t" +
"<'row mt-2'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: {
lengthMenu: 'Show _MENU_ entries',
paginate: {
first: 'First',
previous: 'Previous',
next: 'Next',
last: 'Last'
}
}
};
window.jQuery('#promotionPreviewTable').DataTable(Object.assign({}, baseOptions, {
order: [[2, 'asc'], [0, 'asc']],
language: {
lengthMenu: 'Show _MENU_ entries',
emptyTable: 'No active class assignments found for this school year.'
}
}));
window.jQuery('#carryForwardFamiliesTable').DataTable(Object.assign({}, baseOptions, {
order: [[0, 'asc']],
language: {
lengthMenu: 'Show _MENU_ entries',
emptyTable: 'No carry-forward balances found.'
}
}));
});
</script>
<?= $this->endSection() ?>