This commit is contained in:
+10
-10
@@ -1015,23 +1015,23 @@ $routes->group('communications', static function ($routes) {
|
||||
// app/Config/Routes.php
|
||||
$routes->group('api', static function ($routes) {
|
||||
// Families & Guardians API
|
||||
$routes->get('students/(:num)/families', 'FamilyController::familiesByStudent/$1');
|
||||
$routes->get('families/(:num)/guardians', 'FamilyController::guardiansByFamily/$1');
|
||||
$routes->get('students/(:num)/families', 'View\FamilyController::familiesByStudent/$1');
|
||||
$routes->get('families/(:num)/guardians', 'View\FamilyController::guardiansByFamily/$1');
|
||||
});
|
||||
|
||||
$routes->group('families', static function ($routes) {
|
||||
// Allow GET for convenience in browser; keep POST for API/automation
|
||||
$routes->match(['get', 'post'], 'bootstrap', 'FamilyController::bootstrap'); // protect with auth in production
|
||||
$routes->post('attach-second-by-user', 'FamilyController::attachSecondByUser');
|
||||
$routes->post('attach-second-by-email', 'FamilyController::attachSecondByEmail');
|
||||
$routes->match(['get', 'post'], 'bootstrap', 'View\FamilyController::bootstrap'); // protect with auth in production
|
||||
$routes->post('attach-second-by-user', 'View\FamilyController::attachSecondByUser');
|
||||
$routes->post('attach-second-by-email', 'View\FamilyController::attachSecondByEmail');
|
||||
|
||||
$routes->post('set-primary-home', 'FamilyController::setPrimaryHome');
|
||||
$routes->post('set-guardian-flags', 'FamilyController::setGuardianFlags');
|
||||
$routes->post('unlink-guardian', 'FamilyController::unlinkGuardian');
|
||||
$routes->post('unlink-student', 'FamilyController::unlinkStudent');
|
||||
$routes->post('set-primary-home', 'View\FamilyController::setPrimaryHome');
|
||||
$routes->post('set-guardian-flags', 'View\FamilyController::setGuardianFlags');
|
||||
$routes->post('unlink-guardian', 'View\FamilyController::unlinkGuardian');
|
||||
$routes->post('unlink-student', 'View\FamilyController::unlinkStudent');
|
||||
});
|
||||
// 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)
|
||||
$routes->group('family', static function ($routes) {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<div class="wrapper">
|
||||
<div class="content mb-3"></div>
|
||||
<h2 class="text-center mt-4 mb-3">Attendance Management</h2>
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
|
||||
@@ -42,16 +42,7 @@ $warningText = static function (array $warnings): string {
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" action="<?= site_url('administrator/tuition-forecast') ?>" class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<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>
|
||||
<input type="hidden" name="school_year" value="<?= esc($filters['school_year'] ?? '') ?>">
|
||||
<div class="col-md-2">
|
||||
<label for="semester" class="form-label">Semester Filter</label>
|
||||
<select id="semester" name="semester" class="form-select">
|
||||
|
||||
@@ -29,14 +29,6 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label class="form-label">Semester</label>
|
||||
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
|
||||
@@ -48,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,11 +10,6 @@ $show_actions = true; // pending page shows actions
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<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')): ?>
|
||||
<div class="alert alert-success mx-3"><?= esc($msg) ?></div>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="card-body pt-2">
|
||||
<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="school_year" value="<?= esc($schoolYear) ?>">
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<label class="form-label">Choose placement test</label>
|
||||
<select name="placement_test" class="form-select" required>
|
||||
@@ -34,10 +35,6 @@
|
||||
<option value="arabic" <?= $placementTest === 'arabic' ? 'selected' : '' ?>>Arabic</option>
|
||||
</select>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) return;
|
||||
|
||||
const $ = window.jQuery;
|
||||
const table = $('#movementsTable').DataTable({
|
||||
pageLength: 100,
|
||||
order: [[1, 'desc']], // 0=checkbox, 1=ID
|
||||
@@ -199,9 +202,9 @@
|
||||
});
|
||||
</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(){
|
||||
if (window.jQuery) return; // jQuery path already binds everything
|
||||
if (window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable) return;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
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 -->
|
||||
<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">
|
||||
<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>
|
||||
<input type="hidden" name="school_year" value="<?= esc($selectedYear ?? '') ?>">
|
||||
<div class="d-flex align-items-end gap-2 flex-nowrap">
|
||||
<div class="d-flex flex-column">
|
||||
<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
|
||||
const retry = document.getElementById('reportRetry');
|
||||
if (retry) retry.addEventListener('click', function(ev) {
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
</div>
|
||||
|
||||
<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: 200px;"></select>
|
||||
</div>
|
||||
<input type="hidden" id="schoolYearSelect" value="<?= esc($schoolYear ?? '') ?>">
|
||||
<div class="col-auto"><label for="semesterSelect" class="col-form-label">Semester</label></div>
|
||||
<div class="col-auto">
|
||||
<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();
|
||||
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;
|
||||
|
||||
// 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); });
|
||||
$student.addEventListener('change', function(){ updateAckStatus(); });
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<h2 class="mb-4">Generate Student Stickers</h2>
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<!-- Auto-count & Print All -->
|
||||
|
||||
@@ -52,13 +52,6 @@ $chartLabels = array_map($formatDateKey, $dateOrder);
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label class="form-label">Semester</label>
|
||||
<?php $semVal = strtolower(trim((string)($semester ?? ($_GET['semester'] ?? '')))); ?>
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
let currentPage = 1;
|
||||
let perPage = parseInt(container.dataset.defaultPerPage, 10) || 25;
|
||||
let pageCount = 1;
|
||||
let dataTable = null;
|
||||
let requestSequence = 0;
|
||||
|
||||
const setPerPageSelect = () => {
|
||||
const options = Array.from(perPageSelect.options);
|
||||
@@ -112,8 +114,25 @@
|
||||
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
|
||||
return;
|
||||
}
|
||||
if (window.jQuery.fn.DataTable.isDataTable(tableSelector)) {
|
||||
window.jQuery(tableSelector).DataTable().clear().destroy();
|
||||
const table = document.querySelector(tableSelector);
|
||||
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) {
|
||||
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,
|
||||
searching: false,
|
||||
info: false,
|
||||
@@ -159,7 +196,6 @@
|
||||
cell.textContent = 'No login activities found.';
|
||||
row.appendChild(cell);
|
||||
tableBody.appendChild(row);
|
||||
initDataTable();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,6 +243,7 @@
|
||||
const fetchActivities = () => {
|
||||
clearError();
|
||||
const url = buildUrl();
|
||||
const sequence = ++requestSequence;
|
||||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
@@ -221,6 +258,10 @@
|
||||
return response.json();
|
||||
})
|
||||
.then((payload) => {
|
||||
if (sequence !== requestSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activities = Array.isArray(payload?.activities) ? payload.activities : [];
|
||||
const pagination = payload?.pagination ?? {};
|
||||
|
||||
@@ -231,6 +272,10 @@
|
||||
updatePaginationControls();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (sequence !== requestSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
showError(error.message || 'Unable to load login activity.');
|
||||
destroyDataTable();
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
Reference in New Issue
Block a user