remove school year from pages
Tests / PHPUnit (push) Successful in 1m15s

This commit is contained in:
root
2026-07-17 01:07:31 -04:00
parent 0d0a771dd1
commit 4db8334a3c
12 changed files with 70 additions and 77 deletions
+10 -10
View File
@@ -1015,23 +1015,23 @@ $routes->group('communications', static function ($routes) {
// app/Config/Routes.php // app/Config/Routes.php
$routes->group('api', static function ($routes) { $routes->group('api', static function ($routes) {
// Families & Guardians API // Families & Guardians API
$routes->get('students/(:num)/families', 'FamilyController::familiesByStudent/$1'); $routes->get('students/(:num)/families', 'View\FamilyController::familiesByStudent/$1');
$routes->get('families/(:num)/guardians', 'FamilyController::guardiansByFamily/$1'); $routes->get('families/(:num)/guardians', 'View\FamilyController::guardiansByFamily/$1');
}); });
$routes->group('families', static function ($routes) { $routes->group('families', static function ($routes) {
// Allow GET for convenience in browser; keep POST for API/automation // Allow GET for convenience in browser; keep POST for API/automation
$routes->match(['get', 'post'], 'bootstrap', 'FamilyController::bootstrap'); // protect with auth in production $routes->match(['get', 'post'], 'bootstrap', 'View\FamilyController::bootstrap'); // protect with auth in production
$routes->post('attach-second-by-user', 'FamilyController::attachSecondByUser'); $routes->post('attach-second-by-user', 'View\FamilyController::attachSecondByUser');
$routes->post('attach-second-by-email', 'FamilyController::attachSecondByEmail'); $routes->post('attach-second-by-email', 'View\FamilyController::attachSecondByEmail');
$routes->post('set-primary-home', 'FamilyController::setPrimaryHome'); $routes->post('set-primary-home', 'View\FamilyController::setPrimaryHome');
$routes->post('set-guardian-flags', 'FamilyController::setGuardianFlags'); $routes->post('set-guardian-flags', 'View\FamilyController::setGuardianFlags');
$routes->post('unlink-guardian', 'FamilyController::unlinkGuardian'); $routes->post('unlink-guardian', 'View\FamilyController::unlinkGuardian');
$routes->post('unlink-student', 'FamilyController::unlinkStudent'); $routes->post('unlink-student', 'View\FamilyController::unlinkStudent');
}); });
// Allow GET for manual triggering from browser // Allow GET for manual triggering from browser
$routes->match(['get', 'post'], 'families/import-legacy', 'FamilyController::importSecondParentsFromLegacy'); $routes->match(['get', 'post'], 'families/import-legacy', 'View\FamilyController::importSecondParentsFromLegacy');
// Admin page (protect with your auth/permission) // Admin page (protect with your auth/permission)
$routes->group('family', static function ($routes) { $routes->group('family', static function ($routes) {
@@ -6,7 +6,6 @@
<div class="wrapper"> <div class="wrapper">
<div class="content mb-3"></div> <div class="content mb-3"></div>
<h2 class="text-center mt-4 mb-3">Attendance Management</h2> <h2 class="text-center mt-4 mb-3">Attendance Management</h2>
<?= $this->include('partials/academic_filter') ?>
<?= $this->section('styles') ?> <?= $this->section('styles') ?>
<style> <style>
+1 -10
View File
@@ -42,16 +42,7 @@ $warningText = static function (array $warnings): string {
<div class="card shadow-sm mb-4"> <div class="card shadow-sm mb-4">
<div class="card-body"> <div class="card-body">
<form method="get" action="<?= site_url('administrator/tuition-forecast') ?>" class="row g-3 align-items-end"> <form method="get" action="<?= site_url('administrator/tuition-forecast') ?>" class="row g-3 align-items-end">
<div class="col-md-3"> <input type="hidden" name="school_year" value="<?= esc($filters['school_year'] ?? '') ?>">
<label for="school_year" class="form-label">School Year</label>
<select id="school_year" name="school_year" class="form-select">
<?php foreach (($schoolYears ?? []) as $schoolYear): ?>
<option value="<?= esc($schoolYear) ?>" <?= ($filters['school_year'] ?? '') === $schoolYear ? 'selected' : '' ?>>
<?= esc($schoolYear) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"> <div class="col-md-2">
<label for="semester" class="form-label">Semester Filter</label> <label for="semester" class="form-label">Semester Filter</label>
<select id="semester" name="semester" class="form-select"> <select id="semester" name="semester" class="form-select">
+1 -9
View File
@@ -29,14 +29,6 @@
</div> </div>
<form method="get" class="row gy-2 gx-3 align-items-end mb-4"> <form method="get" class="row gy-2 gx-3 align-items-end mb-4">
<div class="col-md-4">
<label class="form-label">School Year</label>
<input type="text" class="form-control"
name="school_year"
value="<?= esc($schoolYear ?? ($_GET['school_year'] ?? '')) ?>"
placeholder="e.g. 2025-2026">
</div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Semester</label> <label class="form-label">Semester</label>
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?> <?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
@@ -48,7 +40,7 @@
</div> </div>
<div class="col-md-3 d-flex gap-2"> <div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-secondary mt-4">Apply</button> <button type="submit" class="btn btn-secondary mt-4">Filter</button>
<a class="btn btn-outline-secondary mt-4" href="<?= site_url('attendance/early-dismissals') ?>">Reset</a> <a class="btn btn-outline-secondary mt-4" href="<?= site_url('attendance/early-dismissals') ?>">Reset</a>
</div> </div>
</form> </form>
@@ -10,11 +10,6 @@ $show_actions = true; // pending page shows actions
<div class="container-fluid px-0"> <div class="container-fluid px-0">
<h2 class="text-center mt-4 mb-3"><?= esc($page_title) ?></h2> <h2 class="text-center mt-4 mb-3"><?= esc($page_title) ?></h2>
<div class="d-flex justify-content-center mb-2">
<div class="col-12 col-lg-10">
<?= $this->include('partials/academic_filter') ?>
</div>
</div>
<?php if ($msg = session()->getFlashdata('message')): ?> <?php if ($msg = session()->getFlashdata('message')): ?>
<div class="alert alert-success mx-3"><?= esc($msg) ?></div> <div class="alert alert-success mx-3"><?= esc($msg) ?></div>
+1 -4
View File
@@ -25,6 +25,7 @@
<div class="card-body pt-2"> <div class="card-body pt-2">
<form method="get" action="<?= base_url('grading/placement') ?>" class="row g-3 align-items-end"> <form method="get" action="<?= base_url('grading/placement') ?>" class="row g-3 align-items-end">
<input type="hidden" name="open" value="1"> <input type="hidden" name="open" value="1">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<div class="col-12 col-md-6 col-lg-5"> <div class="col-12 col-md-6 col-lg-5">
<label class="form-label">Choose placement test</label> <label class="form-label">Choose placement test</label>
<select name="placement_test" class="form-select" required> <select name="placement_test" class="form-select" required>
@@ -34,10 +35,6 @@
<option value="arabic" <?= $placementTest === 'arabic' ? 'selected' : '' ?>>Arabic</option> <option value="arabic" <?= $placementTest === 'arabic' ? 'selected' : '' ?>>Arabic</option>
</select> </select>
</div> </div>
<div class="col-12 col-md-4 col-lg-3">
<label class="form-label">School Year</label>
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="YYYY-YYYY" required>
</div>
<div class="col-12 col-md-2 col-lg-2 d-flex gap-2"> <div class="col-12 col-md-2 col-lg-2 d-flex gap-2">
<button type="submit" class="btn btn-primary w-100">Set Placement Scores</button> <button type="submit" class="btn btn-primary w-100">Set Placement Scores</button>
<a href="<?= base_url('grading/placement') ?>" class="btn btn-outline-secondary">Cancel</a> <a href="<?= base_url('grading/placement') ?>" class="btn btn-outline-secondary">Cancel</a>
+6 -3
View File
@@ -94,7 +94,10 @@
</div> </div>
<script> <script>
$(function(){ document.addEventListener('DOMContentLoaded', function(){
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) return;
const $ = window.jQuery;
const table = $('#movementsTable').DataTable({ const table = $('#movementsTable').DataTable({
pageLength: 100, pageLength: 100,
order: [[1, 'desc']], // 0=checkbox, 1=ID order: [[1, 'desc']], // 0=checkbox, 1=ID
@@ -199,9 +202,9 @@
}); });
</script> </script>
<script> <script>
// jQuery-free fallback: enables bulk delete buttons and single delete // Fallback when DataTables/jQuery are not present: enables bulk delete buttons and single delete.
(function(){ (function(){
if (window.jQuery) return; // jQuery path already binds everything if (window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable) return;
document.addEventListener('DOMContentLoaded', function(){ document.addEventListener('DOMContentLoaded', function(){
function qsa(sel, root){ return Array.prototype.slice.call((root||document).querySelectorAll(sel)); } function qsa(sel, root){ return Array.prototype.slice.call((root||document).querySelectorAll(sel)); }
+1 -15
View File
@@ -18,14 +18,7 @@
<!-- Top toolbar: put all filters on one line --> <!-- Top toolbar: put all filters on one line -->
<form id="financialFilterForm" method="get" class="d-flex align-items-end gap-3 flex-nowrap justify-content-center mb-3 w-100 overflow-auto" style="white-space: nowrap;"> <form id="financialFilterForm" method="get" class="d-flex align-items-end gap-3 flex-nowrap justify-content-center mb-3 w-100 overflow-auto" style="white-space: nowrap;">
<div class="d-flex align-items-center gap-2 flex-nowrap"> <input type="hidden" name="school_year" value="<?= esc($selectedYear ?? '') ?>">
<label class="form-label mb-0 me-2">School year</label>
<select name="school_year" class="form-select form-select-sm rounded-pill px-3" style="min-width: 180px;">
<?php foreach (($schoolYears ?? []) as $sy): ?>
<option value="<?= esc($sy) ?>" <?= (string)($selectedYear ?? '') === (string)$sy ? 'selected' : '' ?>><?= esc($sy) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="d-flex align-items-end gap-2 flex-nowrap"> <div class="d-flex align-items-end gap-2 flex-nowrap">
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<label class="form-label">Date From:</label> <label class="form-label">Date From:</label>
@@ -580,13 +573,6 @@
}); });
} }
// Auto hydrate on year change
const yearSel = document.querySelector('#financialFilterForm select[name="school_year"]');
if (yearSel) yearSel.addEventListener('change', function() {
hydrate();
updateActionLinks();
});
// Retry handler on error banner // Retry handler on error banner
const retry = document.getElementById('reportRetry'); const retry = document.getElementById('reportRetry');
if (retry) retry.addEventListener('click', function(ev) { if (retry) retry.addEventListener('click', function(ev) {
+1 -8
View File
@@ -8,10 +8,7 @@
</div> </div>
<div class="row g-2 align-items-center justify-content-center mb-3"> <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> <input type="hidden" id="schoolYearSelect" value="<?= esc($schoolYear ?? '') ?>">
<div class="col-auto">
<select id="schoolYearSelect" class="form-select form-select-sm" style="min-width: 200px;"></select>
</div>
<div class="col-auto"><label for="semesterSelect" class="col-form-label">Semester</label></div> <div class="col-auto"><label for="semesterSelect" class="col-form-label">Semester</label></div>
<div class="col-auto"> <div class="col-auto">
<select id="semesterSelect" class="form-select form-select-sm" style="min-width: 160px;"></select> <select id="semesterSelect" class="form-select form-select-sm" style="min-width: 160px;"></select>
@@ -136,9 +133,6 @@ document.addEventListener('DOMContentLoaded', function(){
const data = await res.json(); const data = await res.json();
if (!res.ok || data?.ok === false) throw new Error('Failed to load data'); if (!res.ok || data?.ok === false) throw new Error('Failed to load data');
// Years
$year.innerHTML = '';
(data.schoolYears || []).forEach(y => opt($year, y, y));
if (data.selectedYear) $year.value = data.selectedYear; if (data.selectedYear) $year.value = data.selectedYear;
// Semesters // Semesters
@@ -193,7 +187,6 @@ document.addEventListener('DOMContentLoaded', function(){
} }
} }
$year.addEventListener('change', function(){ loadMeta(this.value, $sem.value).catch(console.error); });
$sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); }); $sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); });
$student.addEventListener('change', function(){ updateAckStatus(); }); $student.addEventListener('change', function(){ updateAckStatus(); });
@@ -5,7 +5,6 @@
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
<h2 class="mb-4">Generate Student Stickers</h2> <h2 class="mb-4">Generate Student Stickers</h2>
<?= $this->include('partials/academic_filter') ?>
<div class="d-flex gap-2 mb-3"> <div class="d-flex gap-2 mb-3">
<!-- Auto-count & Print All --> <!-- Auto-count & Print All -->
-7
View File
@@ -52,13 +52,6 @@ $chartLabels = array_map($formatDateKey, $dateOrder);
</div> </div>
<form method="get" class="row gy-2 gx-3 align-items-end mb-4"> <form method="get" class="row gy-2 gx-3 align-items-end mb-4">
<div class="col-md-4">
<label class="form-label">School Year</label>
<input type="text" class="form-control"
name="school_year"
value="<?= esc($school_year ?? ($_GET['school_year'] ?? '')) ?>"
placeholder="e.g. 2025-2026">
</div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Semester</label> <label class="form-label">Semester</label>
<?php $semVal = strtolower(trim((string)($semester ?? ($_GET['semester'] ?? '')))); ?> <?php $semVal = strtolower(trim((string)($semester ?? ($_GET['semester'] ?? '')))); ?>
+49 -4
View File
@@ -77,6 +77,8 @@
let currentPage = 1; let currentPage = 1;
let perPage = parseInt(container.dataset.defaultPerPage, 10) || 25; let perPage = parseInt(container.dataset.defaultPerPage, 10) || 25;
let pageCount = 1; let pageCount = 1;
let dataTable = null;
let requestSequence = 0;
const setPerPageSelect = () => { const setPerPageSelect = () => {
const options = Array.from(perPageSelect.options); const options = Array.from(perPageSelect.options);
@@ -112,8 +114,25 @@
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) { if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return; return;
} }
if (window.jQuery.fn.DataTable.isDataTable(tableSelector)) { const table = document.querySelector(tableSelector);
window.jQuery(tableSelector).DataTable().clear().destroy(); if (!table || !table.isConnected || !table.parentNode) {
dataTable = null;
return;
}
try {
if (dataTable) {
dataTable.destroy();
dataTable = null;
return;
}
if (window.jQuery.fn.DataTable.isDataTable(table)) {
window.jQuery(table).DataTable().destroy();
}
} catch (error) {
dataTable = null;
console.warn('Unable to destroy login activity DataTable cleanly.', error);
} }
}; };
@@ -121,7 +140,25 @@
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) { if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return; return;
} }
window.jQuery(tableSelector).DataTable({ const table = document.querySelector(tableSelector);
if (!table || !table.isConnected || !table.parentNode) {
return;
}
const expectedColumns = table.tHead?.rows?.[0]?.cells?.length || 0;
const bodyRows = Array.from(table.tBodies?.[0]?.rows || []);
const hasOnlyRegularRows = expectedColumns > 0
&& bodyRows.length > 0
&& bodyRows.every((row) => row.cells.length === expectedColumns
&& Array.from(row.cells).every((cell) => cell.colSpan === 1 && cell.rowSpan === 1));
if (!hasOnlyRegularRows) {
return;
}
if (window.jQuery.fn.DataTable.isDataTable(table)) {
dataTable = window.jQuery(table).DataTable();
return;
}
dataTable = window.jQuery(table).DataTable({
paging: false, paging: false,
searching: false, searching: false,
info: false, info: false,
@@ -159,7 +196,6 @@
cell.textContent = 'No login activities found.'; cell.textContent = 'No login activities found.';
row.appendChild(cell); row.appendChild(cell);
tableBody.appendChild(row); tableBody.appendChild(row);
initDataTable();
return; return;
} }
@@ -207,6 +243,7 @@
const fetchActivities = () => { const fetchActivities = () => {
clearError(); clearError();
const url = buildUrl(); const url = buildUrl();
const sequence = ++requestSequence;
return fetch(url, { return fetch(url, {
headers: { headers: {
@@ -221,6 +258,10 @@
return response.json(); return response.json();
}) })
.then((payload) => { .then((payload) => {
if (sequence !== requestSequence) {
return;
}
const activities = Array.isArray(payload?.activities) ? payload.activities : []; const activities = Array.isArray(payload?.activities) ? payload.activities : [];
const pagination = payload?.pagination ?? {}; const pagination = payload?.pagination ?? {};
@@ -231,6 +272,10 @@
updatePaginationControls(); updatePaginationControls();
}) })
.catch((error) => { .catch((error) => {
if (sequence !== requestSequence) {
return;
}
showError(error.message || 'Unable to load login activity.'); showError(error.message || 'Unable to load login activity.');
destroyDataTable(); destroyDataTable();
tableBody.innerHTML = ''; tableBody.innerHTML = '';