710 lines
32 KiB
PHP
710 lines
32 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<!-- Meta Information -->
|
|
<!-- <meta name="csrf-token" content="<?= csrf_hash() ?>"> -->
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<!-- Prevent back/forward cache issues on management pages -->
|
|
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="0">
|
|
<title>Al Rahma Sunday School</title>
|
|
|
|
<!-- Force same-tab by default -->
|
|
<base target="_self">
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon" href="<?= base_url('assets/images/favicon.ico') ?>" type="image/x-icon">
|
|
|
|
<!-- Google Web Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
<!-- Icon Fonts -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.7.2/font/bootstrap-icons.min.css">
|
|
|
|
<!-- Bootstrap 5 CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
<!-- External Libraries CSS -->
|
|
<link rel="stylesheet" href="<?= base_url('lib/owlcarousel/assets/owl.carousel.min.css') ?>">
|
|
<!-- FullCalendar CSS (needed for calendar styling) -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css">
|
|
|
|
<!-- DataTables CSS -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
|
|
<!-- DataTables CSS (core + Bootstrap 5 integration) -->
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.10/css/jquery.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.10/css/dataTables.bootstrap5.min.css">
|
|
<!-- DataTables FixedHeader (Bootstrap 5 theme) -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-fixedheader-bs5@3.4.0/css/fixedHeader.bootstrap5.min.css">
|
|
|
|
<!-- Note: Avoid loading a second Bootstrap build to prevent conflicts with CDN 5.3.3 -->
|
|
<?php
|
|
$styleCfg = config('Style');
|
|
$sess = session();
|
|
// Default parent/teacher accents to green unless a style is explicitly chosen
|
|
$role = strtolower((string)($sess->get('role') ?? ''));
|
|
$roleDefaultStyle = in_array($role, ['parent', 'teacher', 'teacher_assistant'], true)
|
|
? 'green'
|
|
: ($styleCfg->defaultStyle ?? 'blue');
|
|
$styleKey = $sess->get('style_color') ?? $roleDefaultStyle;
|
|
// Default parent/teacher menu to a greenish palette (emerald)
|
|
$roleDefaultMenu = in_array($role, ['parent', 'teacher', 'teacher_assistant'], true)
|
|
? 'emerald'
|
|
: ($styleCfg->defaultMenu ?? 'white');
|
|
$menuKey = $sess->get('menu_color') ?? $roleDefaultMenu;
|
|
$style = $styleCfg->stylePalettes[$styleKey] ?? ($styleCfg->stylePalettes[$styleCfg->defaultStyle] ?? [
|
|
'primary' => '#4da3ff',
|
|
'primary_hover' => '#1d7eff',
|
|
'thead_bg' => '#f0f7ff',
|
|
'thead_border' => 'rgba(29, 126, 255, 0.25)',
|
|
]);
|
|
if ($menuKey === 'custom') {
|
|
$menu = [
|
|
'bg' => (string)($sess->get('menu_custom_bg') ?? '#0f172a'),
|
|
'text' => (string)($sess->get('menu_custom_text') ?? '#e2e8f0'),
|
|
'mode' => (string)($sess->get('menu_custom_mode') ?? 'dark'),
|
|
];
|
|
} else {
|
|
$menu = $styleCfg->menuPalettes[$menuKey] ?? ($styleCfg->menuPalettes[$styleCfg->defaultMenu] ?? [
|
|
'bg' => '#ffffff',
|
|
'text' => '#334155',
|
|
'mode' => 'light',
|
|
]);
|
|
}
|
|
$resolveMenuMode = function (string $mode, string $bg): string {
|
|
$mode = strtolower(trim($mode));
|
|
if ($mode === 'light' || $mode === 'dark') return $mode;
|
|
$hex = ltrim(trim($bg), '#');
|
|
if (strlen($hex) === 3) {
|
|
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
|
|
}
|
|
if (strlen($hex) !== 6) return 'dark';
|
|
$r = hexdec(substr($hex, 0, 2));
|
|
$g = hexdec(substr($hex, 2, 2));
|
|
$b = hexdec(substr($hex, 4, 2));
|
|
$lum = (0.2126 * $r + 0.7152 * $g + 0.0722 * $b) / 255.0;
|
|
return ($lum < 0.5) ? 'dark' : 'light';
|
|
};
|
|
$mgmtMenuMode = $resolveMenuMode((string)($menu['mode'] ?? 'light'), (string)($menu['bg'] ?? '#0f172a'));
|
|
?>
|
|
<!-- Management layout theme overrides: configurable accents and menu colors -->
|
|
<style>
|
|
/* Prevent horizontal scrollbars from stray overflows */
|
|
html, body { overflow-x: hidden; }
|
|
:root {
|
|
/* Brand + palette for management area */
|
|
--mgmt-primary: <?= esc($style['primary']) ?>; /* accent color */
|
|
--mgmt-primary-hover: <?= esc($style['primary_hover']) ?>; /* deeper hover */
|
|
--mgmt-text: #1f2937; /* slate-800 */
|
|
--mgmt-muted: #64748b; /* slate-500 */
|
|
--mgmt-surface: #ffffff; /* cards */
|
|
--mgmt-bg: #f5f7fb; /* app background */
|
|
/* Menu palette */
|
|
--mgmt-menu-bg: <?= esc($menu['bg']) ?>;
|
|
--mgmt-menu-text: <?= esc($menu['text']) ?>;
|
|
/* Bootstrap variable nudges */
|
|
--bs-body-color: var(--mgmt-text);
|
|
--bs-link-color: var(--mgmt-primary);
|
|
--bs-link-hover-color: var(--mgmt-primary-hover);
|
|
--bs-primary: var(--mgmt-primary);
|
|
/* Sticky header offset (computed in JS) */
|
|
--mgmt-sticky-top: 0px;
|
|
}
|
|
body {
|
|
color: var(--bs-body-color);
|
|
background-color: var(--mgmt-bg);
|
|
}
|
|
a { color: var(--bs-link-color); }
|
|
a:hover { color: var(--bs-link-hover-color); }
|
|
|
|
/* Header + management navbar (menu color aware) */
|
|
header.navbar {
|
|
background-color: var(--mgmt-menu-bg) !important;
|
|
color: var(--mgmt-menu-text) !important;
|
|
border-bottom: 1px solid rgba(2, 6, 23, 0.06);
|
|
}
|
|
/* Ensure header sits above management navbar so its dropdown can overlay */
|
|
header.navbar.sticky-top,
|
|
header.navbar.fixed-top { z-index: 1045 !important; }
|
|
/* Ensure header dropdown overlays everything else */
|
|
header.navbar .dropdown-menu { z-index: 2050 !important; }
|
|
header.navbar .navbar-brand,
|
|
header.navbar .navbar-brand strong {
|
|
color: var(--mgmt-menu-text) !important;
|
|
}
|
|
#navbarManagement {
|
|
background-color: var(--mgmt-menu-bg) !important;
|
|
border-bottom: 1px solid rgba(2, 6, 23, 0.06);
|
|
box-shadow: 0 1px 0 rgba(2,6,23,0.02);
|
|
}
|
|
#navbarManagement .nav-link { color: var(--mgmt-menu-text) !important; }
|
|
#navbarManagement .nav-link:hover,
|
|
#navbarManagement .dropdown-item:hover { color: var(--mgmt-primary) !important; }
|
|
#navbarManagement .dropdown-menu {
|
|
border-radius: .5rem;
|
|
box-shadow: 0 8px 24px rgba(2, 6, 23, 0.08);
|
|
}
|
|
/* Accent chip that surrounds navbar titles */
|
|
.nav-label-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .35rem;
|
|
padding: .15rem .5rem;
|
|
border-radius: 9999px;
|
|
border: 1px solid var(--mgmt-primary);
|
|
background-color: var(--mgmt-primary);
|
|
color: #fff !important; /* ensure contrast over accent */
|
|
line-height: 1.1;
|
|
font-weight: 500;
|
|
}
|
|
/* Hover state: deepen the accent */
|
|
#navbarManagement .nav-link:hover .nav-label-chip,
|
|
#navbarManagement .dropdown-item:hover .nav-label-chip {
|
|
background-color: var(--mgmt-primary-hover);
|
|
border-color: var(--mgmt-primary-hover);
|
|
}
|
|
/* Improve toggler contrast on dark menu */
|
|
#navbarManagement[data-mgmt-menu-mode="dark"] .navbar-toggler { border-color: var(--mgmt-menu-text); }
|
|
#navbarManagement[data-mgmt-menu-mode="dark"] .navbar-toggler-icon { filter: invert(1) brightness(2); }
|
|
.logout-btn { border-color: var(--mgmt-primary) !important; color: var(--mgmt-primary) !important; }
|
|
.logout-btn:hover { background-color: var(--mgmt-primary) !important; color: #fff !important; }
|
|
|
|
/* Sidebar (hover to reveal) */
|
|
#navbarManagement.mgmt-sidebar {
|
|
position: fixed;
|
|
left: 0;
|
|
top: var(--mgmt-sticky-top, 0px);
|
|
width: 260px;
|
|
height: calc(100vh - var(--mgmt-sticky-top, 0px));
|
|
background-color: var(--mgmt-menu-bg);
|
|
color: var(--mgmt-menu-text);
|
|
border-right: 1px solid rgba(2, 6, 23, 0.06);
|
|
box-shadow: 0 2px 12px rgba(2,6,23,0.08);
|
|
transform: translateX(-100%);
|
|
transition: transform 160ms ease-in-out;
|
|
z-index: 1044; /* below header dropdowns (1045) and modals (1055) */
|
|
overflow: visible;
|
|
}
|
|
#navbarManagement.mgmt-sidebar .mgmt-sidebar-content { height: 100%; overflow-y: auto; overflow-x: visible; padding: 8px; }
|
|
#navbarManagement.mgmt-sidebar .nav-link { padding: .35rem .5rem; }
|
|
#navbarManagement.mgmt-sidebar .nav { position: relative; overflow: visible; }
|
|
#navbarManagement.mgmt-sidebar .nav-item { overflow: visible; }
|
|
#navbarManagement.mgmt-sidebar .nav-item.dropend { position: relative; }
|
|
#navbarManagement.mgmt-sidebar .nav-item.dropend > .dropdown-menu {
|
|
position: absolute;
|
|
top: 0;
|
|
left: calc(100% + 6px);
|
|
margin: 0;
|
|
z-index: 2000; /* above content and sidebar */
|
|
display: none;
|
|
}
|
|
#navbarManagement.mgmt-sidebar .nav-item.dropend > .dropdown-menu.show { display: block; }
|
|
/* Show when hovered or when explicitly opened via class */
|
|
html.mgmt-sidebar-open #navbarManagement.mgmt-sidebar,
|
|
#navbarManagement.mgmt-sidebar:hover { transform: translateX(0); }
|
|
|
|
/* Thin hotzone to trigger sidebar on mouseover */
|
|
#mgmtSidebarHotzone {
|
|
position: fixed;
|
|
left: 0;
|
|
top: var(--mgmt-sticky-top, 0px);
|
|
width: 16px;
|
|
height: calc(100vh - var(--mgmt-sticky-top, 0px));
|
|
z-index: 1019;
|
|
background: transparent;
|
|
}
|
|
|
|
/* Sidebar dropdown opens to the right */
|
|
#navbarManagement.mgmt-sidebar .dropend .dropdown-toggle::after { display: none; }
|
|
#navbarManagement.mgmt-sidebar .sidebar-dropdown-menu { min-width: 220px; }
|
|
#navbarManagement.mgmt-sidebar .dropdown-menu { z-index: 3001 !important; }
|
|
/* Body-ported dropdown: always above everything */
|
|
/*.portal-dropdown { position: fixed !important; z-index: 12000 !important; display: block !important; inset: auto !important; transform: none !important; pointer-events: auto !important; }*/
|
|
|
|
/* Content area spacing */
|
|
.wrapper { background: transparent; }
|
|
.content { padding: 24px; }
|
|
|
|
/* Table header palette from accent */
|
|
:root { --mgmt-thead-bg: <?= esc($style['thead_bg']) ?>; --mgmt-thead-border: <?= esc($style['thead_border']) ?>; }
|
|
/* Sticky table headers across management pages (opt-in via .mgmt-sticky or not opted-out)
|
|
Exclude FullCalendar's internal tables (fc-scrollgrid) to avoid layout issues */
|
|
.table.mgmt-sticky > thead > tr > th,
|
|
.content table:not(.no-mgmt-sticky):not([data-no-mgmt-sticky]):not(.fc-scrollgrid) > thead > tr > th {
|
|
position: sticky !important;
|
|
top: var(--mgmt-sticky-top, 0px) !important;
|
|
z-index: 3 !important;
|
|
background-color: var(--mgmt-thead-bg) !important;
|
|
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
|
|
}
|
|
/* Explicitly reset any sticky styles on FullCalendar tables */
|
|
.content .fc table thead th { position: static !important; top: auto !important; z-index: auto !important; box-shadow: none !important; }
|
|
/* Month-grid uses container-local stickiness */
|
|
.month-grid-wrap .month-grid thead th,
|
|
.month-grid thead th {
|
|
top: 0 !important;
|
|
}
|
|
/* Explicit opt-in for container-scoped sticky on problem tables */
|
|
.table.mgmt-sticky-inset > thead > tr > th { top: 0 !important; }
|
|
/* General header cells */
|
|
.content table thead th,
|
|
.content table thead td,
|
|
.content table thead.table-light th,
|
|
.content table thead.table-light td,
|
|
table.dataTable thead th,
|
|
table.dataTable thead td,
|
|
table.fixedHeader-floating thead th,
|
|
div.dataTables_wrapper .dataTables_scrollHead table thead th {
|
|
background-color: var(--mgmt-thead-bg) !important;
|
|
color: var(--mgmt-text);
|
|
border-bottom: 1px solid var(--mgmt-thead-border);
|
|
}
|
|
.table-responsive { width: 100%; position: relative; }
|
|
|
|
/* Card polish */
|
|
.card {
|
|
border: 1px solid rgba(2, 6, 23, 0.06);
|
|
border-radius: .75rem;
|
|
box-shadow: 0 1px 2px rgba(2,6,23,0.04);
|
|
}
|
|
.card-header { background-color: #f0f7ff; }
|
|
.form-text { color: var(--mgmt-muted); }
|
|
</style>
|
|
<?= $this->renderSection('styles') ?>
|
|
</head>
|
|
<body>
|
|
<!-- Header & Navbar -->
|
|
<?php include(__DIR__ . '/../partials/header_back.php'); ?>
|
|
<?php include(__DIR__ . '/../partials/navbar_dynamic.php'); ?>
|
|
|
|
<!-- Main Wrapper -->
|
|
<div class="wrapper">
|
|
<!-- Main Content -->
|
|
<div class="content">
|
|
<?= $this->renderSection('content') ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<?php include(__DIR__ . '/../partials/footer_back.php'); ?>
|
|
|
|
<!-- Family Card Modal -->
|
|
<div class="modal fade" id="familyCardModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Family</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div id="familyCardContent"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Payments Modal -->
|
|
<div class="modal fade" id="paymentsModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Payments</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div id="paymentsModalContent"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts -->
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js"></script>
|
|
<script src="<?= base_url('lib/wow/wow.min.js') ?>"></script>
|
|
<script src="<?= base_url('lib/easing/easing.min.js') ?>"></script>
|
|
<script src="<?= base_url('lib/waypoints/waypoints.min.js') ?>"></script>
|
|
<script src="<?= base_url('lib/owlcarousel/owl.carousel.min.js') ?>"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<!-- DataTables core + Bootstrap 5 integration (same version as CSS above) -->
|
|
<script src="https://cdn.datatables.net/1.13.10/js/jquery.dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.10/js/dataTables.bootstrap5.min.js"></script>
|
|
<!-- DataTables FixedHeader extension -->
|
|
<script src="https://cdn.jsdelivr.net/npm/datatables.net-fixedheader@3.4.0/js/dataTables.fixedHeader.min.js"></script>
|
|
|
|
<!-- Guard: ensure internal links open in same tab; allow opt-in external links with .external-link -->
|
|
<script>
|
|
(function() {
|
|
const origin = location.origin;
|
|
document.addEventListener('click', function(e) {
|
|
const a = e.target.closest('a[href]');
|
|
if (!a) return;
|
|
|
|
// Skip links explicitly marked to open in new tab
|
|
if (a.classList.contains('external-link')) return;
|
|
|
|
try {
|
|
const u = new URL(a.href, origin);
|
|
// For same-origin links, force same tab
|
|
if (u.origin === origin) {
|
|
// Remove target if any; <base target="_self"> already sets default
|
|
a.removeAttribute('target');
|
|
}
|
|
} catch (_) { /* ignore invalid URLs */ }
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<!-- Management tables: normalize table styling + sticky headers across pages -->
|
|
<script>
|
|
(function () {
|
|
function getFixedHeaderOffset() {
|
|
let total = 0;
|
|
const stack = [];
|
|
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top');
|
|
if (header) stack.push(header);
|
|
const mgmt = document.getElementById('navbarManagement');
|
|
if (mgmt && (mgmt.classList.contains('sticky-top') || mgmt.classList.contains('fixed-top'))) stack.push(mgmt);
|
|
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(function (el) { if (!stack.includes(el)) stack.push(el); });
|
|
stack.forEach(function (el) { const h = el.offsetHeight || (el.getBoundingClientRect && el.getBoundingClientRect().height) || 0; total += Math.max(0, Math.round(h)); });
|
|
return total;
|
|
}
|
|
function updateStickyOffsetVar() {
|
|
try { document.documentElement.style.setProperty('--mgmt-sticky-top', String(getFixedHeaderOffset()) + 'px'); } catch (_) {}
|
|
}
|
|
function wrapResponsive(table) {
|
|
if (!table || table.closest('.table-responsive')) return;
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'table-responsive w-100';
|
|
const parent = table.parentNode;
|
|
if (!parent) return;
|
|
parent.insertBefore(wrapper, table);
|
|
wrapper.appendChild(table);
|
|
}
|
|
|
|
function normalizeHeader(table) {
|
|
const thead = table.querySelector('thead');
|
|
if (!thead) return;
|
|
// Remove old dark header style; prefer light header like enrollment page
|
|
thead.classList.remove('table-dark', 'bg-dark', 'thead-dark', 'text-white');
|
|
if (!thead.classList.contains('table-light')) thead.classList.add('table-light');
|
|
}
|
|
|
|
function isDataTable(el) {
|
|
try { return !!(window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable && window.jQuery.fn.DataTable.isDataTable(el)); } catch (_) { return false; }
|
|
}
|
|
function hasFixedHeader(el) {
|
|
if (!isDataTable(el)) return false;
|
|
try {
|
|
const dt = window.jQuery(el).DataTable();
|
|
if (!dt) return false;
|
|
const isInitOpt = !!(dt.settings && dt.settings()[0] && dt.settings()[0].oInit && dt.settings()[0].oInit.fixedHeader);
|
|
const hasApiObj = !!dt.fixedHeader; // present when extension attached
|
|
return isInitOpt || hasApiObj;
|
|
} catch (_) { return false; }
|
|
}
|
|
function tryAttachFixedHeader(el) {
|
|
try {
|
|
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.dataTable || !window.jQuery.fn.dataTable.FixedHeader) return;
|
|
if (!isDataTable(el)) return;
|
|
// Respect per-table opt-out flags
|
|
if (el && (el.classList?.contains('no-dt-fixedheader') || el.hasAttribute('data-no-dt-fixedheader'))) return;
|
|
const dt = window.jQuery(el).DataTable();
|
|
if (dt) {
|
|
try {
|
|
if (!dt.fixedHeader) {
|
|
new window.jQuery.fn.dataTable.FixedHeader(dt, { header: true, headerOffset: getFixedHeaderOffset() });
|
|
}
|
|
// Mark table to disable CSS sticky
|
|
if (el && el.classList) el.classList.add('no-mgmt-sticky');
|
|
} catch (_) {}
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
function applyMgmtTableDefaults(root) {
|
|
const scope = root || document;
|
|
const tables = scope.querySelectorAll('.content table');
|
|
tables.forEach(function (t) {
|
|
// Allow explicit opt-out per table
|
|
if (t.classList.contains('no-mgmt-style') || t.hasAttribute('data-no-mgmt-style')) return;
|
|
// Skip tables that belong to FullCalendar to avoid breaking its layout
|
|
try { if (t.closest('.fc')) return; } catch(_) {}
|
|
// Admin dashboard search tables: no sticky header to avoid overlap
|
|
try {
|
|
if (t.closest('.admin-dashboard')) {
|
|
t.classList.add('no-mgmt-sticky');
|
|
t.setAttribute('data-no-mgmt-sticky', '');
|
|
}
|
|
} catch (_) {}
|
|
|
|
// Apply consistent Bootstrap table look (match enrollment/withdrawal)
|
|
t.classList.add('table');
|
|
t.classList.add('table-bordered');
|
|
t.classList.add('table-striped');
|
|
t.classList.add('align-middle');
|
|
t.classList.add('w-100');
|
|
// Add top margin if none of mt- classes exist
|
|
if (![1,2,3,4,5].some(n => t.classList.contains('mt-' + n))) t.classList.add('mt-4');
|
|
|
|
// Ensure responsive wrapper
|
|
wrapResponsive(t);
|
|
// Normalize thead style
|
|
normalizeHeader(t);
|
|
|
|
// Make header sticky by default (opt-out available)
|
|
if (!(t.classList.contains('no-mgmt-sticky') || t.hasAttribute('data-no-mgmt-sticky'))) {
|
|
t.classList.add('mgmt-sticky');
|
|
}
|
|
|
|
// Avoid double headers when DataTables FixedHeader is active; attach FH if DT is present
|
|
function reconcileSticky() {
|
|
try {
|
|
tryAttachFixedHeader(t);
|
|
if (hasFixedHeader(t)) {
|
|
t.classList.remove('mgmt-sticky');
|
|
t.classList.add('no-mgmt-sticky');
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
reconcileSticky();
|
|
setTimeout(reconcileSticky, 600);
|
|
setTimeout(reconcileSticky, 1400);
|
|
setTimeout(reconcileSticky, 2600);
|
|
});
|
|
}
|
|
|
|
// When any DataTable initializes, attach FixedHeader (unless opted out) and disable CSS sticky
|
|
if (window.jQuery && window.jQuery.fn && window.jQuery.fn.dataTable) {
|
|
window.jQuery(document).on('init.dt', function (e, settings) {
|
|
try {
|
|
const api = new window.jQuery.fn.dataTable.Api(settings);
|
|
const tableNode = settings.nTable;
|
|
// Skip attaching FixedHeader for tables explicitly opting out
|
|
if (tableNode && (tableNode.classList?.contains('no-dt-fixedheader') || tableNode.hasAttribute('data-no-dt-fixedheader'))) {
|
|
return;
|
|
}
|
|
new window.jQuery.fn.dataTable.FixedHeader(api, { header: true, headerOffset: getFixedHeaderOffset() });
|
|
if (tableNode && tableNode.classList) tableNode.classList.add('no-mgmt-sticky');
|
|
} catch (_) {}
|
|
});
|
|
}
|
|
|
|
// Adjust FixedHeader instances on resize to keep offset correct
|
|
function adjustFixedHeaders() {
|
|
try {
|
|
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.dataTable) return;
|
|
window.jQuery('.dataTable, table.display').each(function(){
|
|
try {
|
|
const dt = window.jQuery(this).DataTable();
|
|
if (dt && dt.fixedHeader && typeof dt.fixedHeader.adjust === 'function') dt.fixedHeader.adjust();
|
|
} catch (_) {}
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
|
|
function setupObserver() {
|
|
const root = document.querySelector('.content');
|
|
if (!root || !window.MutationObserver) return;
|
|
const obs = new MutationObserver((mutations) => {
|
|
for (const m of mutations) {
|
|
m.addedNodes && m.addedNodes.forEach((node) => {
|
|
if (node.nodeType !== 1) return; // element only
|
|
if (node.matches && node.matches('table')) {
|
|
applyMgmtTableDefaults(node.parentNode || node);
|
|
} else if (node.querySelectorAll) {
|
|
const tbls = node.querySelectorAll('table');
|
|
if (tbls.length) applyMgmtTableDefaults(node);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
obs.observe(root, { childList: true, subtree: true });
|
|
}
|
|
|
|
function init() {
|
|
updateStickyOffsetVar();
|
|
applyMgmtTableDefaults();
|
|
setupObserver();
|
|
try {
|
|
var nav = document.getElementById('navbarManagement');
|
|
if (nav) nav.setAttribute('data-mgmt-menu-mode', '<?= esc($mgmtMenuMode) ?>');
|
|
} catch (_) {}
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
// Keep sticky offset in sync on resize and after layout shifts
|
|
window.addEventListener('resize', function(){ updateStickyOffsetVar(); adjustFixedHeaders(); });
|
|
setTimeout(updateStickyOffsetVar, 500);
|
|
setTimeout(updateStickyOffsetVar, 1500);
|
|
setTimeout(adjustFixedHeaders, 800);
|
|
setTimeout(adjustFixedHeaders, 1800);
|
|
})();
|
|
</script>
|
|
|
|
<!-- Family Card loader (global) -->
|
|
<script>
|
|
(function(){
|
|
const modalEl = document.getElementById('familyCardModal');
|
|
let modal;
|
|
function ensureModal(){
|
|
if (!modalEl) return null;
|
|
if (!modal) modal = new bootstrap.Modal(modalEl);
|
|
return modal;
|
|
}
|
|
async function loadFamilyCard(params){
|
|
try {
|
|
const usp = new URLSearchParams(params);
|
|
const url = '<?= site_url('family/card') ?>' + '?' + usp.toString();
|
|
const resp = await fetch(url, { headers: { 'Accept': 'text/html' } });
|
|
if (!resp.ok) throw new Error('Failed to load family card');
|
|
const html = await resp.text();
|
|
const target = document.getElementById('familyCardContent');
|
|
if (target) target.innerHTML = html;
|
|
// Try to set a better modal title from the payload
|
|
try {
|
|
const root = target ? target.querySelector('[data-family-title]') : null;
|
|
const title = root ? root.getAttribute('data-family-title') : '';
|
|
const headerTitle = document.querySelector('#familyCardModal .modal-title');
|
|
if (headerTitle) headerTitle.textContent = title || 'Family';
|
|
} catch (_) {}
|
|
|
|
const m = ensureModal();
|
|
if (m) m.show();
|
|
} catch (e) {
|
|
// Fallback: redirect to /family filtered page
|
|
try {
|
|
const gid = params && params.guardian_id;
|
|
const sid = params && params.student_id;
|
|
if (gid) window.location.href = '<?= site_url('family') ?>' + '?guardian_id=' + encodeURIComponent(gid);
|
|
else if (sid) window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(sid);
|
|
} catch(_) {}
|
|
}
|
|
}
|
|
// Expose for programmatic usage
|
|
window.openFamilyCard = loadFamilyCard;
|
|
|
|
// Bubble-phase handler (general)
|
|
document.addEventListener('click', function(e){
|
|
const link = e.target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id]');
|
|
if (!link) return;
|
|
e.preventDefault();
|
|
const sid = link.getAttribute('data-family-student-id');
|
|
const gid = link.getAttribute('data-family-guardian-id');
|
|
const fid = link.getAttribute('data-family-id');
|
|
const params = {};
|
|
if (fid) params.family_id = fid;
|
|
else if (sid) params.student_id = sid;
|
|
else if (gid) params.guardian_id = gid;
|
|
if (Object.keys(params).length) loadFamilyCard(params);
|
|
});
|
|
|
|
// Capture-phase safety net so components that stop propagation (e.g., DataTables) don't block us
|
|
document.addEventListener('click', function(e){
|
|
const link = e.target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id]');
|
|
if (!link) return;
|
|
// If already prevented by other handler, still proceed
|
|
e.preventDefault();
|
|
const sid = link.getAttribute('data-family-student-id');
|
|
const gid = link.getAttribute('data-family-guardian-id');
|
|
const fid = link.getAttribute('data-family-id');
|
|
const params = {};
|
|
if (fid) params.family_id = fid;
|
|
else if (sid) params.student_id = sid;
|
|
else if (gid) params.guardian_id = gid;
|
|
if (Object.keys(params).length) loadFamilyCard(params);
|
|
}, true);
|
|
})();
|
|
</script>
|
|
|
|
<!-- Payments Modal loader (global) -->
|
|
<script>
|
|
(function(){
|
|
const modalEl = document.getElementById('paymentsModal');
|
|
let modal;
|
|
function ensureModal(){
|
|
if (!modalEl) return null;
|
|
if (!modal) modal = new bootstrap.Modal(modalEl);
|
|
return modal;
|
|
}
|
|
async function loadPayments(params){
|
|
try {
|
|
const pid = params && params.parent_id;
|
|
if (!pid) return;
|
|
const url = '<?= site_url('payments/getByParent') ?>/' + encodeURIComponent(pid) + '?modal=1';
|
|
const resp = await fetch(url, { headers: { 'Accept': 'text/html' } });
|
|
if (!resp.ok) throw new Error('Failed to load payments');
|
|
const html = await resp.text();
|
|
const target = document.getElementById('paymentsModalContent');
|
|
if (target) target.innerHTML = html;
|
|
// Try to set a better modal title from the payload
|
|
try {
|
|
const root = target ? target.querySelector('[data-payments-title]') : null;
|
|
const title = root ? root.getAttribute('data-payments-title') : '';
|
|
const headerTitle = document.querySelector('#paymentsModal .modal-title');
|
|
if (headerTitle) headerTitle.textContent = title || 'Payments';
|
|
} catch (_) {}
|
|
const m = ensureModal();
|
|
if (m) m.show();
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
window.openPaymentsModal = loadPayments;
|
|
|
|
// Delegate click (bubble)
|
|
document.addEventListener('click', function(e){
|
|
const link = e.target.closest('[data-payments-parent-id]');
|
|
if (!link) return;
|
|
e.preventDefault();
|
|
const pid = link.getAttribute('data-payments-parent-id');
|
|
if (pid) loadPayments({ parent_id: pid });
|
|
});
|
|
// Capture-phase safety net
|
|
document.addEventListener('click', function(e){
|
|
const link = e.target.closest('[data-payments-parent-id]');
|
|
if (!link) return;
|
|
e.preventDefault();
|
|
const pid = link.getAttribute('data-payments-parent-id');
|
|
if (pid) loadPayments({ parent_id: pid });
|
|
}, true);
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
// Global CSRF sync for all forms on management pages.
|
|
(function(){
|
|
const TOKEN_NAME = '<?= csrf_token() ?>';
|
|
const COOKIE_NAME = <?= json_encode(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>;
|
|
function getCookie(name){
|
|
try {
|
|
return document.cookie.split(';').map(v=>v.trim()).find(v=>v.startsWith(name+'='))?.split('=')[1] || '';
|
|
} catch(_) { return ''; }
|
|
}
|
|
function syncFormToken(form){
|
|
try {
|
|
const fresh = decodeURIComponent(getCookie(COOKIE_NAME) || '');
|
|
if (!fresh) return;
|
|
const hidden = form.querySelector('input[name="' + TOKEN_NAME + '"]');
|
|
if (hidden) hidden.value = fresh;
|
|
} catch(_) { /* no-op */ }
|
|
}
|
|
// Capture-phase to run before other submit handlers
|
|
document.addEventListener('submit', function(e){
|
|
const form = e.target && e.target.closest ? e.target.closest('form') : null;
|
|
if (!form) return;
|
|
syncFormToken(form);
|
|
}, true);
|
|
})();
|
|
</script>
|
|
<?= $this->renderSection('scripts') ?>
|
|
</body>
|
|
</html>
|