apply the school year concept
Tests / PHPUnit (push) Failing after 1m19s

This commit is contained in:
root
2026-07-14 00:59:00 -04:00
parent 504c3bc9f9
commit feb1b29a32
73 changed files with 4288 additions and 620 deletions
+247 -14
View File
@@ -6,12 +6,48 @@
$target = $preview['target'] ?? null;
$overview = $preview['overview'] ?? [];
$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];
$blockers = $preview['blockers'] ?? [];
$warnings = $preview['warnings'] ?? [];
$carryForward = $preview['carry_forward'] ?? [];
$status = (string) ($source['status'] ?? '');
$batchStatus = (string) ($latestBatch['status'] ?? '');
$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 !== '');
return site_url('administrator/school-years/' . (int) ($source['id'] ?? 0) . '/closing/preview?' . http_build_query($query));
};
$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)') : '';
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>';
};
?>
<?php if (session()->getFlashdata('success')): ?>
@@ -23,21 +59,30 @@
<div class="container-fluid py-3">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h2 class="mb-1">Closing Preview: <?= esc($source['name'] ?? '') ?></h2>
<h2 class="mb-1">End-Year Checklist: <?= esc($source['name'] ?? '') ?></h2>
<div class="text-muted">
Status: <span class="fw-semibold"><?= esc(ucfirst($status)) ?></span>
<?php if ($target): ?>
<span class="mx-2">Target: <span class="fw-semibold"><?= esc($target['name'] ?? '') ?></span></span>
<span class="mx-2">Next year: <span class="fw-semibold"><?= esc($target['name'] ?? '') ?></span></span>
<?php endif; ?>
<span>Generated: <?= esc($preview['generated_at'] ?? '') ?></span>
</div>
</div>
<a class="btn btn-outline-secondary" href="<?= site_url('administrator/school-years') ?>">Back to Management</a>
<a class="btn btn-outline-secondary" href="<?= site_url('administrator/school-years') ?>">Back to School Years</a>
</div>
<div class="border rounded bg-white p-3 mb-4">
<h5 class="mb-2">How to End This Year</h5>
<ol class="mb-0">
<li>Clear all blocking issues below.</li>
<li>Start the end-year process to lock the checklist.</li>
<li>Confirm carry-forward balances, then mark this school year closed.</li>
</ol>
</div>
<form class="row g-2 align-items-end mb-4" method="get" action="<?= site_url('administrator/school-years/' . (int) ($source['id'] ?? 0) . '/closing/preview') ?>">
<div class="col-md-4">
<label class="form-label" for="target_school_year_id">Target school year</label>
<label class="form-label" for="target_school_year_id">Next school year</label>
<select class="form-select" id="target_school_year_id" name="target_school_year_id">
<option value="">Auto-select next draft year</option>
<?php foreach (($schoolYears ?? []) as $year): ?>
@@ -50,7 +95,7 @@
</select>
</div>
<div class="col-md-3">
<button class="btn btn-primary" type="submit">Refresh Preview</button>
<button class="btn btn-primary" type="submit">Refresh Checklist</button>
</div>
</form>
@@ -130,11 +175,11 @@
</div>
<div class="col-lg-7">
<div class="border rounded bg-white p-3 h-100">
<h5>Closing Progress</h5>
<h5>End-Year Progress</h5>
<ol class="mb-0">
<li>Preview generated</li>
<li>Checklist generated</li>
<li class="<?= $blockers === [] ? '' : 'text-muted' ?>">Validation <?= $blockers === [] ? 'passed' : 'has blockers' ?></li>
<li class="<?= $batchStatus !== '' ? '' : 'text-muted' ?>">Closing batch <?= $batchStatus !== '' ? esc($batchStatus) : 'not created' ?></li>
<li class="<?= $batchStatus !== '' ? '' : 'text-muted' ?>">End-year process <?= $batchStatus !== '' ? esc($batchStatus) : 'not started' ?></li>
<li class="<?= in_array($batchStatus, ['executed', 'completed'], true) ? '' : 'text-muted' ?>">Carry-forward completed</li>
<li class="<?= $status === 'closed' ? '' : 'text-muted' ?>">Year closed</li>
</ol>
@@ -142,13 +187,186 @@
</div>
</div>
<div class="border rounded bg-white p-3 mb-4">
<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>
<div class="text-muted small">Promotion decisions must be generated and resolved before this school year can end.</div>
</div>
<a class="btn btn-outline-primary btn-sm" href="<?= site_url('grading/decisions?' . http_build_query(['school_year' => $source['name'] ?? ''])) ?>">
Manage Promotion Decisions
</a>
</div>
<div class="row g-3 mb-3">
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Students</div>
<div class="fw-semibold"><?= esc((string) ($promotionSummary['total_students'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Decided</div>
<div class="fw-semibold"><?= esc((string) ($promotionSummary['with_decision'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Pass</div>
<div class="fw-semibold"><?= esc((string) ($promotionSummary['pass'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Other</div>
<div class="fw-semibold"><?= esc((string) ($promotionSummary['other_decision'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Queued</div>
<div class="fw-semibold"><?= esc((string) ($promotionSummary['queued'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Assigned</div>
<div class="fw-semibold"><?= esc((string) ($promotionSummary['assigned'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Applied</div>
<div class="fw-semibold"><?= esc((string) ($promotionSummary['applied'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Pending</div>
<div class="fw-semibold text-danger"><?= esc((string) ($promotionSummary['pending_decision'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Missing</div>
<div class="fw-semibold text-danger"><?= esc((string) ($promotionSummary['missing_decision'] ?? 0)) ?></div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="text-muted small">Missing Queue</div>
<div class="fw-semibold text-danger"><?= esc((string) ($promotionSummary['missing_queue'] ?? 0)) ?></div>
</div>
</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>
<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>
</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';
}
$targetLabel = trim((string) ($row['target_section'] ?? ''));
if ($targetLabel === '') {
$targetLabel = trim((string) ($row['target_class'] ?? ''));
}
?>
<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><?= 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>
</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">
<h5>Carry-Forward Families</h5>
<div class="table-responsive">
<table class="table table-sm align-middle">
<table class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
<th>Family</th>
<th>Parent</th>
<th class="text-end">Source Balance</th>
<th class="text-end">Credits</th>
<th class="text-end">Adjustments</th>
@@ -157,11 +375,17 @@
</thead>
<tbody>
<?php if ($carryForward === []): ?>
<tr><td colspan="5" class="text-muted">No carry-forward balances found.</td></tr>
<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>
<td>
<div><?= esc($row['parent'] ?? ('Parent #' . (int) ($row['family_id'] ?? 0))) ?></div>
<?php if (! empty($row['parent_email'])): ?>
<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>
@@ -178,28 +402,37 @@
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/start') ?>" method="post">
<?= csrf_field() ?>
<input type="hidden" name="target_school_year_id" value="<?= (int) $target['id'] ?>">
<button class="btn btn-warning" type="submit">Begin Closing</button>
<button class="btn btn-warning" type="submit">Close Year</button>
</form>
<?php elseif ($status === 'active'): ?>
<button class="btn btn-warning" type="button" disabled>Close Year</button>
<span class="align-self-center text-muted small">
<?php if (! $target): ?>
Create or select the next school year first.
<?php elseif ($blockers !== []): ?>
Resolve the blocking issues above first.
<?php endif; ?>
</span>
<?php endif; ?>
<?php if ($status === 'closing' && $batchStatus === 'started'): ?>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/execute') ?>" method="post">
<?= csrf_field() ?>
<button class="btn btn-primary" type="submit">Execute Carry-Forward</button>
<button class="btn btn-primary" type="submit">Confirm Carry-Forward</button>
</form>
<?php endif; ?>
<?php if ($status === 'closing' && $batchStatus === 'executed'): ?>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/complete') ?>" method="post">
<?= csrf_field() ?>
<button class="btn btn-success" type="submit">Complete Closing</button>
<button class="btn btn-success" type="submit">Close Year</button>
</form>
<?php endif; ?>
<?php if ($status === 'closing' && ! in_array($batchStatus, ['executed', 'completed'], true)): ?>
<form action="<?= site_url('administrator/school-years/' . (int) $source['id'] . '/closing/cancel') ?>" method="post">
<?= csrf_field() ?>
<button class="btn btn-outline-secondary" type="submit">Cancel Closing</button>
<button class="btn btn-outline-secondary" type="submit">Cancel End-Year Process</button>
</form>
<?php endif; ?>
</div>