This commit is contained in:
+10
-10
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)); }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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'] ?? '')))); ?>
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user