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
-16
View File
@@ -61,22 +61,6 @@ $decisionBadge = [
<i class="bi bi-award me-2"></i>Generate Certificates
</h2>
<!-- School year filter -->
<div class="d-flex justify-content-end mb-3">
<form method="get" action="<?= site_url('administrator/certificates') ?>" class="d-flex gap-2 align-items-center">
<label class="form-label mb-0 me-1 text-muted small">School Year</label>
<input type="text"
name="school_year"
class="form-control form-control-sm"
style="width:130px;"
value="<?= esc($schoolYear) ?>"
placeholder="e.g. 2024-2025">
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-arrow-repeat me-1"></i>Reload
</button>
</form>
</div>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= esc(session()->getFlashdata('error')) ?>
+18 -1
View File
@@ -30,8 +30,15 @@
$lowProgressSectionIds = $lowProgressSectionIds ?? [];
$lowProgressQuery = implode(',', $lowProgressSectionIds);
$lowProgressUrl = base_url('administrator/teacher-submissions');
$lowProgressParams = [];
if (!empty($filters['school_year'])) {
$lowProgressParams['school_year'] = $filters['school_year'];
}
if ($lowProgressQuery !== '') {
$lowProgressUrl .= '?low_progress_sections=' . rawurlencode($lowProgressQuery);
$lowProgressParams['low_progress_sections'] = $lowProgressQuery;
}
if (!empty($lowProgressParams)) {
$lowProgressUrl .= '?' . http_build_query($lowProgressParams);
}
?>
<a
@@ -48,6 +55,16 @@
<form class="card shadow-sm mb-3" method="get" action="<?= base_url('admin/progress') ?>">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">School year</label>
<select name="school_year" class="form-select">
<?php foreach (($schoolYears ?? []) as $year): ?>
<option value="<?= esc($year) ?>" <?= (string)($filters['school_year'] ?? $schoolYear ?? '') === (string)$year ? 'selected' : '' ?>>
<?= esc($year) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">From</label>
<input type="date" name="from" class="form-control" value="<?= esc($filters['from'] ?? '') ?>">
@@ -3,7 +3,12 @@
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="mb-0">Competition Winners</h3>
<div>
<h3 class="mb-0">Competition Winners</h3>
<?php if (!empty($schoolYear)): ?>
<div class="text-muted small">School Year: <?= esc($schoolYear) ?></div>
<?php endif; ?>
</div>
<a class="btn btn-primary" href="/admin/competition-winners/create">+ New Competition</a>
</div>
@@ -19,6 +24,7 @@
<tr>
<th>ID</th>
<th>Title</th>
<th>School Year</th>
<th>Class Section</th>
<th>Winners Rule</th>
<th>Published</th>
@@ -36,6 +42,7 @@
<tr>
<td><?= esc($c['id']) ?></td>
<td><?= esc($c['title']) ?></td>
<td><?= esc($c['school_year'] ?? '') ?></td>
<td><?= esc($sectionName) ?></td>
<td>Tiered per class</td>
<td><?= $c['is_published'] ? 'Yes' : 'No' ?></td>
+3 -2
View File
@@ -2,6 +2,7 @@
$activeStudents = $active_students ?? [];
$removedStudents = $removed_students ?? [];
$schoolYear = $school_year ?? '';
$activeSchoolYear = $active_school_year ?? '';
?>
<?= $this->extend('layout/management_layout') ?>
@@ -12,8 +13,8 @@ $schoolYear = $school_year ?? '';
<div class="content">
<h2 class="text-center mt-4 mb-2">Student Removal</h2>
<p class="text-center text-muted mb-4">
Removed students keep their data but do not appear in classes or attendance.
<?= $schoolYear !== '' ? 'Current year: ' . esc($schoolYear) : '' ?>
Active school year: <?= esc($schoolYear !== '' ? $schoolYear : $activeSchoolYear) ?>.
Active students have a class assignment in the active school year. Removed students are inactive or have no active-year assignment.
</p>
<?= $this->include('partials/flash_messages') ?>
+137 -153
View File
@@ -1,171 +1,156 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="wrapper">
<div class="content"></div>
<h2 class="text-center mt-4 mb-3">Class Preparation</h2>
<h2 class="text-center mt-4 mb-3">Class Preparation</h2>
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
<!-- School Year Selector + Calculate Button -->
<form method="get" action="<?= site_url('class-prep') ?>" class="mb-4 d-flex align-items-center gap-2 flex-wrap">
<label for="school_year" class="form-label mb-0">School Year:</label>
<select name="school_year" id="school_year" class="form-select form-select-sm w-auto">
<?php for ($y = date('Y'); $y >= 2020; $y--): ?>
<?php $sy = $y . '-' . ($y + 1); ?>
<option value="<?= $sy ?>" <?= $schoolYear === $sy ? 'selected' : '' ?>><?= $sy ?></option>
<?php endfor; ?>
</select>
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
<label for="semester" class="form-label mb-0">Semester:</label>
<select name="semester" id="semester" class="form-select form-select-sm w-auto">
<option value="">—</option>
<option value="Fall" <?= (strcasecmp($semVal,'Fall')===0?'selected':'') ?>>Fall</option>
<option value="Spring" <?= (strcasecmp($semVal,'Spring')===0?'selected':'') ?>>Spring</option>
</select>
<button class="btn btn-sm btn-primary">Calculate Items</button>
</form>
<!-- Recalculate using the current school year and semester -->
<form method="get" action="<?= site_url('class-prep') ?>" class="mb-4">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<?php if ($semVal !== ''): ?>
<input type="hidden" name="semester" value="<?= esc($semVal) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-sm btn-primary">Calculate Items</button>
</form>
<!-- Class Cards -->
<div class="row g-3">
<?php foreach ($prepResults as $prep): ?>
<?php $adjMap = $prep['adjustments'] ?? []; ?>
<div class="col-12 col-lg-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header d-flex justify-content-between align-items-center bg-light border-0">
<div>
<div class="fw-semibold mb-1">
<?= esc($prep['class_section']) ?>
</div>
<?php if (!empty($prep['needs_print'])): ?>
<span class="badge bg-danger">Updated since last print</span>
<?php else: ?>
<span class="badge bg-success">Up-to-date</span>
<?php endif; ?>
<?php if (!empty($prep['last_printed_at'])): ?>
<small class="text-muted ms-2">Last printed: <?= esc($prep['last_printed_at']) ?></small>
<?php endif; ?>
</div>
<div class="small text-muted text-end">
Students:<br>
<span class="badge bg-secondary"><?= (int)$prep['student_count'] ?></span>
</div>
<!-- Class Cards -->
<div class="row g-3">
<?php foreach ($prepResults as $prep): ?>
<?php $adjMap = $prep['adjustments'] ?? []; ?>
<div class="col-12 col-lg-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header d-flex justify-content-between align-items-center bg-light border-0">
<div>
<div class="fw-semibold mb-1">
<?= esc($prep['class_section']) ?>
</div>
<?php if (!empty($prep['needs_print'])): ?>
<span class="badge bg-danger">Updated since last print</span>
<?php else: ?>
<span class="badge bg-success">Up-to-date</span>
<?php endif; ?>
<?php if (!empty($prep['last_printed_at'])): ?>
<small class="text-muted ms-2">Last printed: <?= esc($prep['last_printed_at']) ?></small>
<?php endif; ?>
</div>
<div class="small text-muted text-end">
Students:<br>
<span class="badge bg-secondary"><?= (int)$prep['student_count'] ?></span>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<!-- Prep Items -->
<div class="col-12 col-md-7">
<div class="fw-semibold mb-2">Prep Items</div>
<ul class="list-group list-group-flush small border rounded overflow-hidden">
<?php foreach (($prep['prep_items'] ?? []) as $item => $qty): ?>
<li class="list-group-item d-flex justify-content-between align-items-center <?= (int)$qty === 0 ? 'text-muted-50' : '' ?>">
<span><?= esc($item) ?></span>
<span class="badge <?= (int)$qty > 0 ? 'bg-primary' : 'bg-secondary' ?> rounded-pill">
<?= (int)$qty ?>
</span>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="card-body">
<div class="row g-3">
<!-- Prep Items -->
<div class="col-12 col-md-7">
<div class="fw-semibold mb-2">Prep Items</div>
<ul class="list-group list-group-flush small border rounded overflow-hidden">
<?php foreach (($prep['prep_items'] ?? []) as $item => $qty): ?>
<li class="list-group-item d-flex justify-content-between align-items-center <?= (int)$qty === 0 ? 'text-muted-50' : '' ?>">
<span><?= esc($item) ?></span>
<span class="badge <?= (int)$qty > 0 ? 'bg-primary' : 'bg-secondary' ?> rounded-pill">
<?= (int)$qty ?>
</span>
</li>
<?php endforeach; ?>
</ul>
</div>
<!-- Adjustments -->
<div class="col-12 col-md-5">
<div class="fw-semibold mb-2">Adjustments (Tables)</div>
<form method="post" action="<?= site_url('class-prep/save-adjustment') ?>" class="d-grid gap-2">
<?= csrf_field() ?>
<input type="hidden" name="class_section_id" value="<?= esc($prep['class_section_id']) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<!-- Adjustments -->
<div class="col-12 col-md-5">
<div class="fw-semibold mb-2">Adjustments (Tables)</div>
<form method="post" action="<?= site_url('class-prep/save-adjustment') ?>" class="d-grid gap-2">
<?= csrf_field() ?>
<input type="hidden" name="class_section_id" value="<?= esc($prep['class_section_id']) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<?php foreach ($adjMap as $item => $value): ?>
<div>
<label class="form-label mb-1 small text-muted d-block"><?= esc($item) ?></label>
<div class="input-group input-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="adjustQty(this, -1)" aria-label="Decrease <?= esc($item) ?>"></button>
<input type="number"
class="form-control text-center"
name="adjustments[<?= esc($item) ?>]"
value="<?= (int)$value ?>"
readonly>
<button type="button" class="btn btn-outline-secondary" onclick="adjustQty(this, 1)" aria-label="Increase <?= esc($item) ?>">+</button>
</div>
</div>
<?php endforeach; ?>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-sm btn-primary">Save</button>
<?php foreach ($adjMap as $item => $value): ?>
<div>
<label class="form-label mb-1 small text-muted d-block"><?= esc($item) ?></label>
<div class="input-group input-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="adjustQty(this, -1)" aria-label="Decrease <?= esc($item) ?>"></button>
<input type="number"
class="form-control text-center"
name="adjustments[<?= esc($item) ?>]"
value="<?= (int)$value ?>"
readonly>
<button type="button" class="btn btn-outline-secondary" onclick="adjustQty(this, 1)" aria-label="Increase <?= esc($item) ?>">+</button>
</div>
</form>
</div>
<?php endforeach; ?>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-sm btn-primary">Save</button>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-0 d-flex justify-content-end">
<?php
$printUrl = site_url('class-prep/print/' . urlencode($prep['class_section_id']) . '/' . $schoolYear);
if ($semVal !== '') {
$printUrl .= '?semester=' . urlencode($semVal);
}
?>
<a href="<?= $printUrl ?>"
class="btn btn-lg btn-print-class <?= !empty($prep['needs_print']) ? 'btn-danger' : 'btn-success' ?>" target="_blank" rel="noopener noreferrer"
onclick="window.open(this.href, '_blank'); return false;">
Print
</a>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Totals Section -->
<div class="mt-5">
<h5 class="mb-3">📦 Totals for All Used Items</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered align-middle no-mgmt-sticky">
<thead class="table-light">
<tr>
<th>Item</th>
<th class="text-end">Total Needed</th>
<th class="text-end">In Inventory</th>
<th class="text-end">Short By</th>
</tr>
</thead>
<tbody>
<?php foreach ($totalNeeded as $item => $needed): ?>
<?php
$have = (int)($available[$item] ?? 0);
$short = max(0, (int)$needed - $have);
$rowCls = $short > 0 ? 'table-warning' : '';
?>
<tr class="<?= $rowCls ?>">
<td><?= esc($item) ?></td>
<td class="text-end"><?= (int)$needed ?></td>
<td class="text-end"><?= $have ?></td>
<td class="text-end <?= $short > 0 ? 'text-danger fw-bold' : 'text-success' ?>">
<?= $short ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Shortage Warning -->
<?php if (!empty($shortages)): ?>
<div class="alert alert-danger mt-3">
<strong>Shortages Detected</strong>
<ul class="mb-0">
<?php foreach ($shortages as $item => $short): ?>
<li><?= esc($item) ?>: short by <?= (int)$short ?> pcs</li>
<?php endforeach; ?>
</ul>
<div class="card-footer bg-transparent border-0 d-flex justify-content-end">
<?php
$printUrl = site_url('class-prep/print/' . urlencode($prep['class_section_id']) . '/' . $schoolYear);
if ($semVal !== '') {
$printUrl .= '?semester=' . urlencode($semVal);
}
?>
<a href="<?= $printUrl ?>"
class="btn btn-lg btn-print-class <?= !empty($prep['needs_print']) ? 'btn-danger' : 'btn-success' ?>" target="_blank" rel="noopener noreferrer"
onclick="window.open(this.href, '_blank'); return false;">
Print
</a>
</div>
<?php else: ?>
<div class="alert alert-success mt-3">✅ All items are available in stock.</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Totals Section -->
<div class="mt-5">
<h5 class="mb-3">📦 Totals for All Used Items</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered align-middle no-mgmt-sticky">
<thead class="table-light">
<tr>
<th>Item</th>
<th class="text-end">Total Needed</th>
<th class="text-end">In Inventory</th>
<th class="text-end">Short By</th>
</tr>
</thead>
<tbody>
<?php foreach ($totalNeeded as $item => $needed): ?>
<?php
$have = (int)($available[$item] ?? 0);
$short = max(0, (int)$needed - $have);
$rowCls = $short > 0 ? 'table-warning' : '';
?>
<tr class="<?= $rowCls ?>">
<td><?= esc($item) ?></td>
<td class="text-end"><?= (int)$needed ?></td>
<td class="text-end"><?= $have ?></td>
<td class="text-end <?= $short > 0 ? 'text-danger fw-bold' : 'text-success' ?>">
<?= $short ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Shortage Warning -->
<?php if (!empty($shortages)): ?>
<div class="alert alert-danger mt-3">
<strong>Shortages Detected</strong>
<ul class="mb-0">
<?php foreach ($shortages as $item => $short): ?>
<li><?= esc($item) ?>: short by <?= (int)$short ?> pcs</li>
<?php endforeach; ?>
</ul>
</div>
<?php else: ?>
<div class="alert alert-success mt-3">✅ All items are available in stock.</div>
<?php endif; ?>
</div>
<?= $this->endSection() ?>
@@ -204,10 +189,9 @@
function adjustQty(btn, delta) {
const input = btn.parentElement.querySelector('input[type="number"]');
const curr = parseInt(input.value, 10) || 0;
input.value = curr + delta; // clamp if you want: input.value = Math.max(0, curr + delta);
input.value = curr + delta;
}
// Force print links to open in a new tab even if other handlers intercept
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.btn-print-class').forEach(function(el) {
el.addEventListener('click', function(ev) {
+8
View File
@@ -0,0 +1,8 @@
<?= $this->extend('layout/main') ?>
<?= $this->section('content') ?>
<div class="alert alert-danger" role="alert">
<h1 class="h5 mb-2">School-year context unavailable</h1>
<p class="mb-0"><?= esc($message ?? 'The selected school year is not available.') ?></p>
</div>
<?= $this->endSection() ?>
@@ -6,16 +6,6 @@
<div class="content"></div>
<h2 class="text-center mt-4 mb-3">Invoice Management</h2>
<!-- Controls -->
<div class="row g-2 align-items-center justify-content-center mb-3">
<div class="col-auto">
<label for="schoolYearSelect" class="col-form-label">School year</label>
</div>
<div class="col-auto">
<select id="schoolYearSelect" class="form-select form-select-sm" style="min-width: 180px;"></select>
</div>
</div>
<!-- Table for displaying invoices (client-hydrated) -->
<table id="invoicesTable" class="table table-striped table-hover table-bordered w-100">
<thead class="thead-dark">
@@ -155,16 +145,20 @@
const $tbl = $('#invoicesTable');
let dt = null;
const $year = document.getElementById('schoolYearSelect');
const selectedSchoolYear = () => $year ? $year.value : '';
const syncSchoolYearSelect = (resp) => {
if (!$year) return;
const years = Array.isArray(resp.schoolYears) ? resp.schoolYears : [];
const selected = resp.schoolYear || (years[0] || '');
$year.innerHTML = years.map(y => `<option value="${esc(y)}" ${y===selected?'selected':''}>${esc(y)}</option>`).join('');
};
// Global fallback handler for inline onclick
window.__genInvoice = async function(btn){
try {
btn.disabled = true;
await generateInvoice(btn.getAttribute('data-parent-id'));
const resp = await loadInvoices();
// Populate year select
const years = Array.isArray(resp.schoolYears) ? resp.schoolYears : [];
const selected = resp.schoolYear || (years[0] || '');
$year.innerHTML = years.map(y => `<option value="${esc(y)}" ${y===selected?'selected':''}>${esc(y)}</option>`).join('');
const resp = await loadInvoices(selectedSchoolYear());
syncSchoolYearSelect(resp);
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
@@ -194,10 +188,7 @@
}
try {
const resp = await loadInvoices();
// Populate year select on first load
const years = Array.isArray(resp.schoolYears) ? resp.schoolYears : [];
const selected = resp.schoolYear || (years[0] || '');
$year.innerHTML = years.map(y => `<option value="${esc(y)}" ${y===selected?'selected':''}>${esc(y)}</option>`).join('');
syncSchoolYearSelect(resp);
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
@@ -240,7 +231,7 @@
try {
await generateInvoice(btn.getAttribute('data-parent-id'));
// Refresh table minimally: reload data
const resp = await loadInvoices($year.value);
const resp = await loadInvoices(selectedSchoolYear());
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
@@ -273,7 +264,7 @@
try {
this.disabled = true;
await generateInvoice(this.getAttribute('data-parent-id'));
const resp = await loadInvoices($year.value);
const resp = await loadInvoices(selectedSchoolYear());
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
@@ -312,34 +303,36 @@
});
// Year change
$year.addEventListener('change', async () => {
try {
const resp = await loadInvoices($year.value);
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
esc(r.parent_name || ''),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
fmtMoney(r.refund_amount),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
});
if ($.fn.DataTable.isDataTable($tbl)) {
const dti = $tbl.DataTable();
dti.clear();
dti.rows.add(data).draw(false);
if ($year) {
$year.addEventListener('change', async () => {
try {
const resp = await loadInvoices($year.value);
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
esc(r.parent_name || ''),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
fmtMoney(r.refund_amount),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
});
if ($.fn.DataTable.isDataTable($tbl)) {
const dti = $tbl.DataTable();
dti.clear();
dti.rows.add(data).draw(false);
}
showToast('Loaded ' + $year.value);
} catch (e) {
console.error(e);
showToast('Failed to load ' + $year.value, false);
}
showToast('Loaded ' + $year.value);
} catch (e) {
console.error(e);
showToast('Failed to load ' + $year.value, false);
}
});
});
}
});
</script>
<?= $this->endSection() ?>
+1
View File
@@ -23,6 +23,7 @@
<body>
<!-- Navbar -->
<?php include(__DIR__ . '/../partials/navbar.php'); ?>
<?= view('partials/school_year_selector') ?>
<div class="container my-4">
<?= $this->renderSection('content') ?>
+6 -1
View File
@@ -165,6 +165,7 @@ html, body { overflow-x: hidden; }
<body data-app-menu-mode="<?= esc($appMenuMode) ?>">
<?php include(__DIR__ . '/../partials/navbar.php'); ?>
<?= view('partials/school_year_selector') ?>
<?php
$uri = service('uri');
@@ -188,7 +189,11 @@ html, body { overflow-x: hidden; }
if ($isTeacher) {
$cfg = new \App\Models\ConfigurationModel();
$tcModel = new \App\Models\TeacherClassModel();
$sy = (string)($cfg->getConfig('school_year') ?? '');
try {
$sy = service('schoolYearContext')->resolve(service('request'))->yearName();
} catch (\Throwable $e) {
$sy = (string)($cfg->getConfig('school_year') ?? '');
}
$sem = (string)($cfg->getConfig('semester') ?? '');
$uid = (int)(session()->get('user_id') ?? 0);
if ($uid) {
+14
View File
@@ -287,6 +287,20 @@
<!-- Header & Navbar -->
<?php include(__DIR__ . '/../partials/header_back.php'); ?>
<?php include(__DIR__ . '/../partials/navbar_dynamic.php'); ?>
<?php
$managementSchoolYearSelectorData = [];
if (!isset($schoolYearContext, $schoolYearOptions) && session()->get('user_id')) {
try {
$managementSchoolYearSelectorData = service('schoolYearViewData')->forCurrentRequest(service('request'));
$managementSchoolYearSelectorData['schoolYearSelectorEnabled'] = true;
} catch (\Throwable $e) {
log_message('warning', 'Unable to load management school-year selector: {message}', [
'message' => $e->getMessage(),
]);
}
}
?>
<?= view('partials/school_year_selector', $managementSchoolYearSelectorData) ?>
<!-- Main Wrapper -->
<div class="wrapper">
+19 -15
View File
@@ -20,6 +20,8 @@ if ($currentYear === '' || $currentSem === '') {
} catch (\Throwable $e) { /* ignore */ }
}
$showSchoolYearControl = $showSchoolYearControl ?? true;
$selectedYear = (string)($schoolYear ?? ($_GET['school_year'] ?? ''));
if ($selectedYear === '') { $selectedYear = $currentYear; }
@@ -32,23 +34,25 @@ $classSectionVal = (string)($classSectionId ?? ($_GET['class_section_id'] ?? '')
<?php if ($classSectionVal !== ''): ?>
<input type="hidden" name="class_section_id" value="<?= esc($classSectionVal) ?>">
<?php endif; ?>
<div class="col-auto"><label class="form-label mb-0">School year</label></div>
<div class="col-auto">
<?php $years = isset($schoolYears) && is_array($schoolYears) ? $schoolYears : []; ?>
<select name="school_year" class="form-select form-select-sm" style="min-width: 180px;">
<?php if (!empty($years)): ?>
<?php foreach ($years as $y): $val = is_array($y) && isset($y['school_year']) ? (string)$y['school_year'] : (string)$y; ?>
<option value="<?= esc($val) ?>" <?= ($selectedYear === $val ? 'selected' : '') ?>><?= esc($val) ?></option>
<?php endforeach; ?>
<?php else: ?>
<?php if ($selectedYear !== ''): ?>
<option value="<?= esc($selectedYear) ?>" selected><?= esc($selectedYear) ?></option>
<?php if ($showSchoolYearControl): ?>
<div class="col-auto"><label class="form-label mb-0">School year</label></div>
<div class="col-auto">
<?php $years = isset($schoolYears) && is_array($schoolYears) ? $schoolYears : []; ?>
<select name="school_year" class="form-select form-select-sm" style="min-width: 180px;">
<?php if (!empty($years)): ?>
<?php foreach ($years as $y): $val = is_array($y) && isset($y['school_year']) ? (string)$y['school_year'] : (string)$y; ?>
<option value="<?= esc($val) ?>" <?= ($selectedYear === $val ? 'selected' : '') ?>><?= esc($val) ?></option>
<?php endforeach; ?>
<?php else: ?>
<option value="">—</option>
<?php if ($selectedYear !== ''): ?>
<option value="<?= esc($selectedYear) ?>" selected><?= esc($selectedYear) ?></option>
<?php else: ?>
<option value="">-</option>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
</select>
</div>
</select>
</div>
<?php endif; ?>
<div class="col-auto"><label class="form-label mb-0">Semester</label></div>
<div class="col-auto">
+10 -2
View File
@@ -60,7 +60,11 @@ switch ($role) {
}
} else {
$configModel = new \App\Models\ConfigurationModel();
$schoolYear = session()->get('school_year') ?? $configModel->getConfig('school_year');
try {
$schoolYear = service('schoolYearContext')->resolve(service('request'))->yearName();
} catch (\Throwable $e) {
$schoolYear = session()->get('school_year') ?? $configModel->getConfig('school_year');
}
$classSectionId = (int)($class_section_id ?? session()->get('class_section_id') ?? 0);
if ($classSectionId > 0 && !empty($schoolYear)) {
$scoreCardStudents = $studentModel->getByClassAndYear($classSectionId, $schoolYear);
@@ -267,7 +271,11 @@ switch ($role) {
if (!isset($activeEventCount) && ($role ?? '') === 'parent') {
$configModel = new \App\Models\ConfigurationModel();
$eventModel = new \App\Models\EventModel();
$year = $sess->get('school_year') ?? $configModel->getConfig('school_year');
try {
$year = service('schoolYearContext')->resolve(service('request'))->yearName();
} catch (\Throwable $e) {
$year = $sess->get('school_year') ?? $configModel->getConfig('school_year');
}
$semester = $sess->get('semester') ?? $configModel->getConfig('semester');
$activeEventCount = count($eventModel->getActiveEvents($year, $semester) ?? []);
}
@@ -0,0 +1,54 @@
<?php if (!empty($schoolYearSelectorEnabled) && isset($schoolYearContext, $schoolYearOptions)): ?>
<?php
$query = service('request')->getUri()->getQuery();
$returnTo = current_url() . ($query !== '' ? '?' . $query : '');
?>
<div class="school-year-context-bar border-bottom bg-light py-2">
<div class="container-fluid px-3">
<form method="post" action="<?= site_url('school-year/select') ?>" class="d-flex align-items-center gap-2 flex-wrap">
<?= csrf_field() ?>
<input type="hidden" name="return_to" value="<?= esc($returnTo) ?>">
<label for="global-school-year" class="form-label mb-0 fw-semibold">
School year
</label>
<select
id="global-school-year"
name="school_year_id"
class="form-select form-select-sm w-auto"
onchange="this.form.submit()"
aria-label="Selected school year"
>
<?php foreach ($schoolYearOptions as $year): ?>
<option
value="<?= (int) $year['id'] ?>"
<?= (int) $year['id'] === $schoolYearContext->id() ? 'selected' : '' ?>
>
<?= esc($year['name']) ?> - <?= esc(ucfirst((string) $year['status'])) ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($schoolYearContext->isReadonly()): ?>
<span class="btn btn-warning btn-sm disabled" aria-disabled="true">
<i class="bi bi-lock-fill"></i> READ-ONLY
</span>
<?php else: ?>
<span class="badge text-bg-success">ACTIVE</span>
<?php endif; ?>
<span class="text-muted small"><?= esc($schoolYearContext->yearName()) ?></span>
</form>
<?php if ($schoolYearContext->isExplicitSelection()): ?>
<form method="post" action="<?= site_url('school-year/reset') ?>" class="d-inline-block mt-2 mt-sm-0 ms-sm-2">
<?= csrf_field() ?>
<input type="hidden" name="return_to" value="<?= esc($returnTo) ?>">
<button type="submit" class="btn btn-outline-secondary btn-sm">
Active year
</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
+383 -11
View File
@@ -1,3 +1,35 @@
Yesterday 11:12 PM
write sql querry to update school_year = 2025-2026
UPDATE your_table_name
SET school_year = '2025-2026'
WHERE your_condition;
To update every row:
UPDATE your_table_name
SET school_year = '2025-2026';
Use the second version only if you genuinely intend to rewrite the entire table, because databases are remarkably obedient when humans forget WHERE.
Today 12:49 AM
Screenshot 2026-07-14 at 12.49.07AM.png
Pasted code(6).php
File
remove the image content from the view
Removed the School year / Semester filter from the view by deleting:
<?= $this->include('partials/academic_filter') ?>
Pasted_code_6_without_academic_filter.php
Library
/
Pasted_code_6_without_academic_filter.php
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
@@ -28,8 +60,6 @@ $formatRole = function (?string $role): string {
<h2 class="text-dark mb-3" style="font-family: Arial, sans-serif;">Generate Staff Badges</h2>
</div>
<?= $this->include('partials/academic_filter') ?>
<div class="d-flex flex-column flex-md-row justify-content-md-end align-items-md-center gap-2">
<!-- Submit button associates to form below -->
<button type="submit" class="btn btn-success mt-3" form="badgeForm">Generate Badges</button>
@@ -38,10 +68,7 @@ $formatRole = function (?string $role): string {
<!-- Staff selection -->
<form method="post" action="<?= base_url('badge') ?>" target="_blank" id="badgeForm">
<?= csrf_field() ?>
<input type="hidden" name="school_year" value="<?= esc($selectedYear ?? '') ?>">
<!-- hidden container to preserve selections and their data (guaranteed inside form) -->
<div id="selectedHiddenInputs" style="display:none"></div>
<table id="staffTable" class="table table-bordered table-striped align-middle w-100">
<thead class="table-dark">
<tr>
@@ -278,11 +305,7 @@ $formatRole = function (?string $role): string {
async function refreshCsrf() {
try {
const params = new URLSearchParams();
if (selectedYear) params.append('school_year', selectedYear);
const resp = await fetch('<?= base_url('api/printables/badges/status') ?>' + (params.toString() ? ('?' + params.toString()) : ''), {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!resp.ok) return;
const json = await resp.json();
if (json && json.csrf_token && json.csrf_hash && csrfInput && json.csrf_token === csrfName) {
@@ -351,3 +374,352 @@ $formatRole = function (?string $role): string {
});
</script>
<?= $this->endSection() ?>
Library
/
Pasted_code_6_without_academic_filter.php
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
// Role formatter for display + posting
$formatRole = function (?string $role): string {
$role = (string)$role;
$role = str_replace(['-', '_'], ' ', $role);
$role = preg_replace('/\s+/', ' ', trim($role));
if ($role === '') return '';
$out = [];
foreach (explode(' ', $role) as $w) {
if ($w === '') continue;
if (preg_match('/^[A-Za-z]{1,3}$/', $w)) {
$out[] = strtoupper($w); // TA, PTA, HR, KG...
} else {
$out[] = ucfirst(strtolower($w)); // Teacher, Assistant, Admin...
}
}
return implode(' ', $out);
};
?>
<div class="container-fluid">
<div class="wrapper">
<div class="content">
<div class="text-center mx-auto mb-5 wow" data-wow-delay="0.1s" style="max-width: 600px;">
<br>
<h2 class="text-dark mb-3" style="font-family: Arial, sans-serif;">Generate Staff Badges</h2>
</div>
<div class="d-flex flex-column flex-md-row justify-content-md-end align-items-md-center gap-2">
<!-- Submit button associates to form below -->
<button type="submit" class="btn btn-success mt-3" form="badgeForm">Generate Badges</button>
<br>
</div>
<!-- Staff selection -->
<form method="post" action="<?= base_url('badge') ?>" target="_blank" id="badgeForm">
<?= csrf_field() ?>
<table id="staffTable" class="table table-bordered table-striped align-middle w-100">
<thead class="table-dark">
<tr>
<th style="width: 70px;">#</th>
<th scope="col">Firstname</th>
<th scope="col">Lastname</th>
<th scope="col">Role</th>
<th scope="col">Teacher Class Section</th>
<th scope="col" style="width: 140px;">Badge Prints</th>
<th scope="col" style="width: 150px;">
<div class="form-check m-0">
<input class="form-check-input" type="checkbox" id="select-all">
<label class="form-check-label" for="select-all">Select All (page)</label>
</div>
</th>
</tr>
</thead>
<tbody>
<?php if (!empty($users)): ?>
<?php $order = 1; ?>
<?php foreach ($users as $user): ?>
<?php
// Pick a representative role (first of CSV or role_name/active_role)
$roleRaw = $user['active_role'] ?? $user['role_name'] ?? ($user['roles'] ?? '');
if (strpos((string)$roleRaw, ',') !== false) {
$parts = array_filter(array_map('trim', explode(',', (string)$roleRaw)));
$roleRaw = $parts[0] ?? '';
}
//$roleLabel = $roleRaw !== '' ? $formatRole($roleRaw) : '-';
$roleLabel = $user['role_name'] ?? ($user['roles'] ?? '-'); // both are pre-formatted by $formatRole
// Normalize user id key for checkbox
$uid = $user['user_id'] ?? $user['id'] ?? ($user['users.id'] ?? null);
// Class section name (optional)
$className = !empty($user['class_section_name']) ? (string)$user['class_section_name'] : '';
?>
<tr
data-user-id="<?= esc($uid ?? '') ?>"
data-role-label="<?= esc($roleLabel) ?>"
data-class-name="<?= esc($className) ?>">
<td style="text-align: center;"><?= esc($order++) ?></td>
<td><?= esc($user['firstname'] ?? '') ?></td>
<td><?= esc($user['lastname'] ?? '') ?></td>
<td><?= esc($roleLabel) ?></td>
<td><?= $className !== '' ? esc($className) : '-' ?></td>
<td><span class="badge bg-secondary prints-badge" data-user-id="<?= esc($uid ?? '') ?>">—</span></td>
<td>
<?php if ($uid !== null): ?>
<div class="form-check m-0">
<input type="checkbox" name="user_ids[]" value="<?= esc($uid) ?>" class="form-check-input user-checkbox" id="chk-<?= esc($uid) ?>">
<label class="form-check-label" for="chk-<?= esc($uid) ?>">Select</label>
</div>
<?php else: ?>
<span class="text-muted">N/A</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="7" class="text-center text-muted">No staff found for the selected year.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</form>
<br>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Track selected user IDs across paging/filtering/sorting
const selected = new Set();
const formEl = document.getElementById('badgeForm');
let clearTimerId = null; // pending auto-clear timer, if any
let hiddenContainer = document.getElementById('selectedHiddenInputs');
// Ensure hidden container exists inside the form (some earlier layouts had it outside)
if (!hiddenContainer || !formEl.contains(hiddenContainer)) {
hiddenContainer = document.createElement('div');
hiddenContainer.id = 'selectedHiddenInputs';
hiddenContainer.style.display = 'none';
formEl.appendChild(hiddenContainer);
}
const selectAll = document.getElementById('select-all');
const csrfName = <?= json_encode(csrf_token()) ?>;
const csrfInput = document.querySelector(`input[name='${csrfName}']`);
// Build a meta map (userId -> { role, className }) from the DOM BEFORE DataTables paginates
const rowMeta = {};
document.querySelectorAll('#staffTable tbody tr').forEach(tr => {
const id = tr.getAttribute('data-user-id');
if (!id) return;
rowMeta[id] = {
role: tr.getAttribute('data-role-label') || '',
className: tr.getAttribute('data-class-name') || ''
};
});
const table = $('#staffTable').DataTable({
stateSave: true,
pageLength: 100,
lengthMenu: [10, 25, 50, 100],
order: [
[1, 'asc'],
[2, 'asc']
], // Firstname, Lastname
columnDefs: [{
targets: 0,
searchable: false
}, // row #
{
targets: 6,
orderable: false,
searchable: false
} // checkbox column
]
});
// Helper: keep hidden inputs in sync so selections submit even if not on current page
function syncHiddenInputs() {
hiddenContainer.innerHTML = '';
selected.forEach(function(id) {
const meta = rowMeta[id] || {
role: '',
className: ''
};
// user_ids[]
const i1 = document.createElement('input');
i1.type = 'hidden';
i1.name = 'user_ids[]';
i1.value = id;
hiddenContainer.appendChild(i1);
// roles[ID]
const i2 = document.createElement('input');
i2.type = 'hidden';
i2.name = `roles[${id}]`;
i2.value = meta.role;
hiddenContainer.appendChild(i2);
// classes[ID]
const i3 = document.createElement('input');
i3.type = 'hidden';
i3.name = `classes[${id}]`;
i3.value = meta.className;
hiddenContainer.appendChild(i3);
});
}
// Helper: refresh checkboxes on current page based on Set
function syncPageCheckboxes() {
const pageNodes = table.rows({
page: 'current'
}).nodes().to$();
let allChecked = true;
let anyChecked = false;
pageNodes.each(function() {
const row = this;
const userId = row.getAttribute('data-user-id');
const cb = row.querySelector('.user-checkbox');
if (!cb || !userId) return;
cb.checked = selected.has(userId);
if (!cb.checked) allChecked = false;
if (cb.checked) anyChecked = true;
});
// Update header select-all for current page
selectAll.checked = allChecked && pageNodes.length > 0;
selectAll.indeterminate = !allChecked && anyChecked;
}
// Helper: clear all selections and reset the current page UI
function clearSelectionsNow() {
selected.clear();
// Uncheck visible checkboxes on current page
table.rows({ page: 'current' }).every(function () {
const row = this.node();
const cb = row.querySelector('.user-checkbox');
if (cb) cb.checked = false;
});
// Reset select-all and hidden inputs
selectAll.checked = false;
selectAll.indeterminate = false;
syncHiddenInputs();
syncPageCheckboxes();
}
// On draw (paging/sort/search), just refresh checkbox UI states
table.on('draw', function() {
syncPageCheckboxes();
});
// Delegate checkbox change handling once at the table level (works across redraws)
document.getElementById('staffTable').addEventListener('change', function (e) {
const target = e.target;
if (!target || !target.classList.contains('user-checkbox')) return;
const tr = target.closest('tr[data-user-id]');
const userId = tr ? tr.getAttribute('data-user-id') : null;
if (!userId) return;
if (target.checked) selected.add(userId); else selected.delete(userId);
syncHiddenInputs();
syncPageCheckboxes();
// If user interacts, cancel any pending auto-clear to avoid wiping new selections
if (clearTimerId !== null) { clearTimeout(clearTimerId); clearTimerId = null; }
});
// Initial sync on first load
table.draw(false);
// Header "Select All (page)" toggler
selectAll.addEventListener('change', function() {
const check = this.checked;
table.rows({
page: 'current'
}).every(function() {
const row = this.node();
const userId = row.getAttribute('data-user-id');
const cb = row.querySelector('.user-checkbox');
if (!cb || !userId) return;
cb.checked = check;
if (check) selected.add(userId);
else selected.delete(userId);
});
syncHiddenInputs();
syncPageCheckboxes();
if (clearTimerId !== null) { clearTimeout(clearTimerId); clearTimerId = null; }
});
// Before submit, ensure CSRF is fresh and hidden inputs include all selections
const form = document.getElementById('badgeForm');
async function refreshCsrf() {
try {
const params = new URLSearchParams();
if (!resp.ok) return;
const json = await resp.json();
if (json && json.csrf_token && json.csrf_hash && csrfInput && json.csrf_token === csrfName) {
csrfInput.value = json.csrf_hash;
}
} catch (e) {
// No-op: if refresh fails we'll still attempt submit; server may accept existing token
}
}
form.addEventListener('submit', async function(e) {
e.preventDefault();
if (selected.size === 0) {
alert('Please select at least one staff member to generate badges.');
return;
}
syncHiddenInputs();
// CSRF is excluded for this endpoint now, but keeping refresh is safe
try { await refreshCsrf(); } catch (_) {}
// Native submit so browser handles PDF in a new tab
form.submit();
// Auto-clear selections 5 seconds after generating badges
clearTimerId = setTimeout(function() { clearSelectionsNow(); clearTimerId = null; }, 5000);
});
// --- Fetch and render print status ---
const selectedYear = <?= json_encode($selectedYear ?? '') ?>;
function fetchPrintStatus() {
const ids = Object.keys(rowMeta);
if (ids.length === 0) return;
const params = new URLSearchParams();
ids.forEach(id => params.append('user_ids[]', id));
if (selectedYear) params.append('school_year', selectedYear);
fetch('<?= base_url('api/printables/badges/status') ?>?' + params.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json'
}
})
.then(r => r.ok ? r.json() : Promise.reject())
.then(json => {
if (!json || !json.ok || !json.data) return;
const data = json.data;
Object.keys(rowMeta).forEach(id => {
const info = data[id];
const badge = document.querySelector(`.prints-badge[data-user-id="${id}"]`);
const tr = document.querySelector(`#staffTable tbody tr[data-user-id="${id}"]`);
if (!badge || !tr) return;
const count = info ? (info.count || 0) : 0;
badge.textContent = count > 0 ? `Printed ${count}` : '—';
badge.className = 'badge prints-badge ' + (count > 0 ? 'bg-success' : 'bg-secondary');
tr.classList.toggle('table-success', count > 0);
});
// Refresh CSRF for subsequent submissions (so no page reload is needed)
if (json.csrf_token && json.csrf_hash && csrfInput && json.csrf_token === csrfName) {
csrfInput.value = json.csrf_hash;
}
})
.catch(() => {});
}
fetchPrintStatus();
window.addEventListener('focus', fetchPrintStatus);
});
</script>
<?= $this->endSection() ?>
+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>
+141 -57
View File
@@ -10,6 +10,12 @@
'archived' => 'bg-dark',
];
$formatDate = static fn ($value): string => $value ? esc((string) $value) : 'Not set';
$money = static fn ($value): string => '$' . number_format((float) $value, 2);
$activeId = (int) ($activeYear['id'] ?? 0);
$nextDraftId = (int) ($nextDraftYear['id'] ?? 0);
$closingPreviewUrl = $activeId > 0
? site_url('administrator/school-years/' . $activeId . '/closing/preview' . ($nextDraftId > 0 ? '?' . http_build_query(['target_school_year_id' => $nextDraftId]) : ''))
: '';
?>
<?php if (session()->getFlashdata('success')): ?>
@@ -19,46 +25,53 @@
<?php endif; ?>
<div class="container-fluid py-3">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div class="mb-3">
<div>
<h2 class="mb-1">School Year Management</h2>
<div class="text-muted">
Active year:
<?php if (! empty($activeYear)): ?>
<span class="badge bg-success"><?= esc($activeYear['name']) ?></span>
<?php else: ?>
<span class="badge bg-danger">None</span>
<?php endif; ?>
</div>
<h2 class="mb-1">School Years</h2>
<div class="text-muted">End the current school year after its checklist is clear, then start the next year.</div>
</div>
<button class="btn btn-success" type="button" data-bs-toggle="collapse" data-bs-target="#schoolYearCreateForm">
Add School Year
</button>
</div>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="border rounded bg-white p-3 h-100">
<div class="text-muted small">Active Year</div>
<div class="fs-5 fw-semibold"><?= esc($activeYear['name'] ?? 'None') ?></div>
<div class="border rounded bg-white p-3 mb-4">
<div class="d-flex flex-wrap justify-content-between gap-3">
<div>
<h5 class="mb-2">End Current School Year</h5>
<div class="d-flex flex-wrap gap-2 mb-2">
<span class="badge <?= $activeYear ? 'bg-success' : 'bg-secondary' ?>">Current: <?= esc($activeYear['name'] ?? 'None') ?></span>
<span class="badge <?= $nextDraftYear ? 'bg-info text-dark' : 'bg-secondary' ?>">Next: <?= esc($nextDraftYear['name'] ?? 'Not created') ?></span>
<?php if ($closingYear): ?>
<span class="badge bg-warning text-dark">Ending: <?= esc($closingYear['name']) ?></span>
<?php endif; ?>
</div>
<?php if ($closingYear): ?>
<div class="text-muted">Finish the end-year checklist for <?= esc($closingYear['name']) ?> before starting another year.</div>
<?php elseif ($activeYear && $nextDraftYear): ?>
<div class="text-muted">Review promotions and balances for <?= esc($activeYear['name']) ?>. When all blockers are resolved, end it and start <?= esc($nextDraftYear['name']) ?>.</div>
<?php elseif ($activeYear): ?>
<div class="text-muted">Create the next school year first. It will stay as a draft until the current year is closed.</div>
<?php elseif ($nextDraftYear): ?>
<div class="text-muted">No year is active. Start <?= esc($nextDraftYear['name']) ?> when you are ready.</div>
<?php else: ?>
<div class="text-muted">Create a school year draft to begin.</div>
<?php endif; ?>
<ol class="small text-muted ps-3 mt-2 mb-0">
<li>Create the next year as a draft.</li>
<li>Review the end-year checklist for the current year.</li>
<li>End the current year, then start the next year.</li>
</ol>
</div>
</div>
<div class="col-md-3">
<div class="border rounded bg-white p-3 h-100">
<div class="text-muted small">Next Draft</div>
<div class="fs-5 fw-semibold"><?= esc($nextDraftYear['name'] ?? 'None') ?></div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded bg-white p-3 h-100">
<div class="text-muted small">Currently Closing</div>
<div class="fs-5 fw-semibold"><?= esc($closingYear['name'] ?? 'None') ?></div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded bg-white p-3 h-100">
<div class="text-muted small">Archived Years</div>
<div class="fs-5 fw-semibold"><?= esc((string) ($archivedCount ?? 0)) ?></div>
<div class="d-flex align-items-start">
<?php if ($closingYear): ?>
<a class="btn btn-primary" href="<?= site_url('administrator/school-years/' . (int) $closingYear['id'] . '/closing/preview') ?>">Finish End-Year Checklist</a>
<?php elseif ($activeYear && $nextDraftYear): ?>
<a class="btn btn-primary" href="<?= esc($closingPreviewUrl, 'attr') ?>">Close Year</a>
<?php elseif ($activeYear): ?>
<button class="btn btn-success" type="button" data-bs-toggle="collapse" data-bs-target="#schoolYearCreateForm">Create Next Year</button>
<?php elseif ($nextDraftYear): ?>
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#activateSchoolYearModal<?= $nextDraftId ?>">Start <?= esc($nextDraftYear['name']) ?></button>
<?php else: ?>
<button class="btn btn-success" type="button" data-bs-toggle="collapse" data-bs-target="#schoolYearCreateForm">Create School Year</button>
<?php endif; ?>
</div>
</div>
</div>
@@ -71,6 +84,15 @@
<label class="form-label" for="new_school_year_name">School Year</label>
<input class="form-control" id="new_school_year_name" name="name" placeholder="2026-2027" required pattern="\d{4}-\d{4}">
</div>
<div class="col-md-2">
<label class="form-label" for="new_previous_school_year_id">Previous Year</label>
<select class="form-select" id="new_previous_school_year_id" name="previous_school_year_id">
<option value="">None</option>
<?php foreach (($schoolYears ?? []) as $existingYear): ?>
<option value="<?= (int) ($existingYear['id'] ?? 0) ?>"><?= esc($existingYear['name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="new_school_year_starts_on">Starts On</label>
<input class="form-control" id="new_school_year_starts_on" name="starts_on" type="date">
@@ -106,10 +128,8 @@
<th>School Year</th>
<th>Dates</th>
<th>Status</th>
<th>Students</th>
<th>Families</th>
<th>Financial Balance</th>
<th>Last Transition</th>
<th>Verify</th>
<th>Last Activity</th>
<th>Updated</th>
<th>Actions</th>
</tr>
@@ -121,6 +141,8 @@
$status = (string) ($year['status'] ?? 'draft');
$readonly = in_array($status, ['closed', 'archived'], true);
$transition = $latestTransitions[$id] ?? null;
$verify = $yearVerification[$id] ?? [];
$previousYearName = $schoolYearNamesById[(int) ($year['previous_school_year_id'] ?? 0)] ?? null;
?>
<tr>
<td class="fw-semibold"><?= esc($year['name'] ?? '') ?></td>
@@ -133,9 +155,13 @@
<?= esc(ucfirst($status)) ?>
</span>
</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">Preview</td>
<td class="small">
<div>Previous: <span class="fw-semibold"><?= esc($previousYearName ?? 'Not linked') ?></span></div>
<div>Students: <span class="fw-semibold"><?= esc((string) ($verify['students'] ?? 0)) ?></span></div>
<div>Promoted: <span class="fw-semibold"><?= esc((string) ($verify['promoted'] ?? 0)) ?></span> / Decisions: <?= esc((string) ($verify['decisions'] ?? 0)) ?></div>
<div>Queued: <span class="fw-semibold"><?= esc((string) ($verify['queued'] ?? 0)) ?></span></div>
<div>Carry-forward: <span class="fw-semibold"><?= esc($money($verify['carry_forward_balance'] ?? 0)) ?></span></div>
</td>
<td>
<?php if ($transition): ?>
<div><?= esc($transition['action'] ?? '') ?></div>
@@ -159,11 +185,13 @@
</li>
<?php endif; ?>
<?php if ($status === 'draft'): ?>
<li>
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#activateSchoolYearModal<?= $id ?>">
Activate
</button>
</li>
<?php if (empty($activeYear) && empty($closingYear)): ?>
<li>
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#activateSchoolYearModal<?= $id ?>">
Start this year
</button>
</li>
<?php endif; ?>
<li><hr class="dropdown-divider"></li>
<li>
<form action="<?= site_url('administrator/school-years/' . $id . '/delete-draft') ?>" method="post" onsubmit="return confirm('Delete this unused draft school year?');">
@@ -173,21 +201,21 @@
</li>
<?php elseif ($status === 'active'): ?>
<li>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">Preview closing</a>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview' . ($nextDraftId > 0 ? '?' . http_build_query(['target_school_year_id' => $nextDraftId]) : '')) ?>">Close year</a>
</li>
<?php elseif ($status === 'closing'): ?>
<li>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">View closing</a>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">Finish end-year checklist</a>
</li>
<li>
<form action="<?= site_url('administrator/school-years/' . $id . '/closing/cancel') ?>" method="post" onsubmit="return confirm('Cancel closing for this school year?');">
<form action="<?= site_url('administrator/school-years/' . $id . '/closing/cancel') ?>" method="post" onsubmit="return confirm('Cancel the end-year process for this school year?');">
<?= csrf_field() ?>
<button class="dropdown-item" type="submit">Cancel closing</button>
<button class="dropdown-item" type="submit">Cancel end-year process</button>
</form>
</li>
<?php elseif ($status === 'closed'): ?>
<li>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">View closing report</a>
<a class="dropdown-item" href="<?= site_url('administrator/school-years/' . $id . '/closing/preview') ?>">View end-year report</a>
</li>
<li>
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#reopenSchoolYearModal<?= $id ?>">
@@ -220,6 +248,8 @@
$id = (int) ($year['id'] ?? 0);
$status = (string) ($year['status'] ?? 'draft');
$readonly = in_array($status, ['closed', 'archived', 'closing'], true);
$verify = $yearVerification[$id] ?? [];
$previousYearName = $schoolYearNamesById[(int) ($year['previous_school_year_id'] ?? 0)] ?? null;
?>
<?php if (! $readonly): ?>
<div class="modal fade" id="editSchoolYearModal<?= $id ?>" tabindex="-1" aria-hidden="true">
@@ -231,6 +261,47 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="border rounded bg-light p-3 mb-3">
<h6 class="mb-2">Current Verification Data</h6>
<div class="row g-2 small">
<div class="col-md-4">
<div class="text-muted">Previous year</div>
<div class="fw-semibold"><?= esc($previousYearName ?? 'Not linked') ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">Students in year</div>
<div class="fw-semibold"><?= esc((string) ($verify['students'] ?? 0)) ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">Promotion decisions</div>
<div class="fw-semibold"><?= esc((string) ($verify['decisions'] ?? 0)) ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">Promoted students</div>
<div class="fw-semibold"><?= esc((string) ($verify['promoted'] ?? 0)) ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">Queued for next year</div>
<div class="fw-semibold"><?= esc((string) ($verify['queued'] ?? 0)) ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">Carry-forward balance</div>
<div class="fw-semibold"><?= esc($money($verify['carry_forward_balance'] ?? 0)) ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">Positive balances</div>
<div class="fw-semibold"><?= esc($money($verify['positive_balance'] ?? 0)) ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">Credits</div>
<div class="fw-semibold"><?= esc($money($verify['credit_balance'] ?? 0)) ?></div>
</div>
<div class="col-md-4">
<div class="text-muted">End-year status</div>
<div class="fw-semibold"><?= esc(($verify['closing_status'] ?? '') !== '' ? ucfirst((string) $verify['closing_status']) : 'Not started') ?></div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label" for="school_year_name_<?= $id ?>">School Year</label>
@@ -245,6 +316,19 @@
<input class="form-control" id="school_year_ends_on_<?= $id ?>" name="ends_on" type="date" value="<?= esc($year['ends_on'] ?? '') ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label" for="previous_school_year_id_<?= $id ?>">Previous Year</label>
<select class="form-select" id="previous_school_year_id_<?= $id ?>" name="previous_school_year_id">
<option value="">None</option>
<?php foreach (($schoolYears ?? []) as $existingYear): ?>
<?php if ((int) ($existingYear['id'] ?? 0) !== $id): ?>
<option value="<?= (int) ($existingYear['id'] ?? 0) ?>" <?= (int) ($year['previous_school_year_id'] ?? 0) === (int) ($existingYear['id'] ?? 0) ? 'selected' : '' ?>>
<?= esc($existingYear['name'] ?? '') ?>
</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="registration_starts_on_<?= $id ?>">Registration Starts</label>
@@ -273,24 +357,24 @@
<form class="modal-content" action="<?= site_url('administrator/school-years/' . $id . '/activate') ?>" method="post">
<?= csrf_field() ?>
<div class="modal-header">
<h5 class="modal-title">Activate <?= esc($year['name'] ?? '') ?></h5>
<h5 class="modal-title">Start <?= esc($year['name'] ?? '') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-2">The selected school year will become active.</p>
<p class="mb-2">This school year will become the active year.</p>
<?php if (! empty($activeYear)): ?>
<p class="mb-3">Current active year <strong><?= esc($activeYear['name']) ?></strong> will move to <strong>closing</strong>.</p>
<p class="mb-3">Current active year <strong><?= esc($activeYear['name']) ?></strong> will move to <strong>end-year review</strong>.</p>
<?php endif; ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="1" name="confirm_activation" id="confirm_activation_<?= $id ?>" required>
<label class="form-check-label" for="confirm_activation_<?= $id ?>">
I understand activation does not complete closing for the previous year.
I understand this starts the selected year.
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Activate</button>
<button type="submit" class="btn btn-success">Start Year</button>
</div>
</form>
</div>
@@ -101,9 +101,9 @@ function orderNum($v)
?>
<div class="mb-4 text-center">
<p class="text-muted small mb-1">Active Students</p>
<p class="text-muted small mb-1">Enrolled Students</p>
<div class="display-4 fw-semibold"><?= (int)$totalStudents ?></div>
<p class="text-muted small mb-0">Reflects only currently active students, so removals update the count instantly.</p>
<p class="text-muted small mb-0">Reflects accepted enrollments for the selected school year.</p>
</div>
<form method="get" class="row g-3 mb-4 align-items-end">
@@ -132,8 +132,8 @@ function orderNum($v)
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<strong>Active Student Snapshot</strong>
<span class="badge bg-primary"><?= (int)$totalStudents ?> active</span>
<strong>Enrolled Student Snapshot</strong>
<span class="badge bg-primary"><?= (int)$totalStudents ?> enrolled</span>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-3">
+5 -1
View File
@@ -66,7 +66,11 @@
if ($userId) {
$configModel = new \App\Models\ConfigurationModel();
$teacherClassModel = new \App\Models\TeacherClassModel();
$sy = $configModel->getConfig('school_year');
try {
$sy = service('schoolYearContext')->resolve(service('request'))->yearName();
} catch (\Throwable $e) {
$sy = $configModel->getConfig('school_year');
}
$sem = $configModel->getConfig('semester');
$classOptions = $teacherClassModel->getClassAssignmentsByUserId((int)$userId, (string)$sy, (string)$sem);
}
+6 -1
View File
@@ -3,7 +3,12 @@
<div class="container mt-4">
<h3>Competition Winners</h3>
<p class="text-muted">Published competition winner announcements.</p>
<p class="text-muted">
Published competition winner announcements
<?php if (!empty($schoolYear)): ?>
for <?= esc($schoolYear) ?>
<?php endif; ?>.
</p>
<div class="list-group">
<?php foreach ($competitions as $c): ?>
+3
View File
@@ -7,6 +7,9 @@
<h3><?= esc($competition['title']) ?></h3>
<div class="text-muted mb-3">
Published: <?= esc($competition['published_at'] ?? '') ?>
<?php if (!empty($schoolYear)): ?>
<span class="ms-2">School Year: <?= esc($schoolYear) ?></span>
<?php endif; ?>
</div>
<?php if (empty($winnersByClass)): ?>