Files
alrahma_sunday_school/app/Views/attendance/teacher_attendance_month.php
T
2026-05-16 13:44:12 -04:00

901 lines
30 KiB
PHP
Executable File

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?= csrf_meta() ?>
<?php
$queryParams = [
'semester' => $semester ?? '',
'school_year' => $schoolYear ?? '',
];
$queryString = http_build_query(array_filter($queryParams, static function ($value) {
return $value !== '';
}));
?>
<div
class="container-fluid py-4"
data-endpoint="<?= esc($endpoint ?? '') ?>"
data-save-endpoint="<?= esc($saveEndpoint ?? '') ?>"
data-csrf-name="<?= esc(csrf_token()) ?>"
data-csrf-value="<?= esc(csrf_hash()) ?>"
data-semester="<?= esc($semester ?? '') ?>"
data-school-year="<?= esc($schoolYear ?? '') ?>"
data-is-current="<?= isset($isCurrentYear) && $isCurrentYear ? '1' : '0' ?>"
>
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<h2 class="mb-0">Admin &amp; Teacher — Monthly Attendance</h2>
<div class="legend">
<span class="badge bg-success">P</span><span class="ms-1">Present</span>
<span class="badge bg-danger ms-3">A</span><span class="ms-1">Absent</span>
<span class="badge bg-warning text-dark ms-3">L</span><span class="ms-1">Late</span>
<span class="badge bg-info text-dark ms-3">NS</span><span class="ms-1">No School</span>
<span class="ms-3 cell-empty">—</span><span class="ms-1">No entry</span>
</div>
</div>
<?php if (!empty($missingYear)): ?>
<div class="alert alert-warning" role="alert">
Current school year is not configured. This page is read-only until configured in
<a href="<?= site_url('configuration/configuration_view') ?>" class="alert-link">Add/Edit Configuration</a> (key: <code>school_year</code>).
</div>
<?php endif; ?>
<form class="row gy-2 gx-3 align-items-end justify-content-center mb-4" method="get" action="<?= site_url('admin/teacher-attendance/month') ?>">
<div class="col-md-3">
<label class="form-label">School Year</label>
<select name="school_year" class="form-select">
<?php
$years = isset($schoolYears) && is_array($schoolYears) ? $schoolYears : [];
if (empty($years) && !empty($schoolYear)) $years = [$schoolYear];
foreach ($years as $y): $val = is_array($y) && isset($y['school_year']) ? $y['school_year'] : (string)$y; ?>
<option value="<?= esc($val) ?>" <?= ((string)($schoolYear ?? '') === (string)$val) ? 'selected' : '' ?>>
<?= esc($val) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Semester</label>
<select name="semester" class="form-select">
<?php $semVal = (string)($semester ?? ''); ?>
<?php foreach (['---','Fall','Spring'] as $sem): ?>
<option value="<?= esc($sem) ?>" <?= ($semVal === $sem ? 'selected' : '') ?>><?= esc($sem) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4 d-flex gap-2">
<button type="submit" class="btn btn-secondary">Load</button>
<a class="btn btn-outline-secondary" href="<?= site_url('admin/teacher-attendance/month') ?>">Reset</a>
<a class="btn btn-outline-primary"
href="<?= site_url('admin/teacher-attendance/month.csv' . ($queryString ? ('?' . $queryString) : '')) ?>">
Export CSV
</a>
</div>
</form>
<style>
.att-select.form-select.form-select-sm {
min-width: 54px;
padding-left: 6px;
padding-right: 18px;
line-height: 1.1;
}
.att-none {
background: transparent;
color: #6c757d;
}
.att-present {
background: #19875422;
}
.att-absent {
background: #dc354522;
}
.att-late {
background: #ffc10755;
}
.month-grid-wrap {
overflow-x: auto;
}
.month-grid thead th {
position: sticky;
top: 0;
z-index: 2;
background: #f8f9fa;
}
.month-grid .sticky-col {
position: sticky;
left: 0;
z-index: 3;
background: #fff;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.month-grid .sticky-col-2 {
position: sticky;
left: 180px;
z-index: 3;
background: #fff;
}
.month-grid .sticky-col-3 {
position: sticky;
left: 460px; /* 180 (col1) + 280 (col2) */
z-index: 3;
background: #fff;
}
.badge-cell {
display: inline-block;
min-width: 18px;
text-align: center;
border-radius: 4px;
padding: 2px 0;
font-weight: 600;
}
.cell-empty {
color: #999;
}
.legend .badge {
min-width: 28px;
}
.month-grid .badge-cell {
min-width: 32px;
padding: 4px 0;
border-radius: 6px;
font-weight: 700;
border: 1px solid transparent;
}
.month-grid .badge-cell.bg-success {
background-color: #157347 !important;
border-color: #0f5132;
color: #fff !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .25) inset, 0 0 0 3px rgba(21, 115, 71, .18);
}
.month-grid .badge-cell.bg-danger {
background-color: #bb2d3b !important;
border-color: #842029;
color: #fff !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .25) inset, 0 0 0 3px rgba(187, 45, 59, .18);
}
.month-grid .badge-cell.bg-warning {
background-color: #ffca2c !important;
border-color: #997404;
color: #111 !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .18) inset, 0 0 0 3px rgba(255, 202, 44, .25);
}
.month-grid .badge-cell.bg-info {
background-color: #0dcaf0 !important; /* Bootstrap info */
border-color: #055160; /* darker info border */
color: #111 !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .18) inset, 0 0 0 3px rgba(13, 202, 240, .25);
}
.month-grid .cell-empty {
color: #adb5bd;
}
.att-select.form-select.form-select-sm {
min-width: 54px;
padding-left: 6px;
padding-right: 18px;
line-height: 1.1;
border: 1px solid #adb5bd;
transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
/* No School column/day highlight */
.no-school-day {
background-color: #cff4fc !important; /* Bootstrap info-subtle */
}
.att-select.att-present {
background: #157347;
color: #fff;
border-color: #0f5132;
box-shadow: 0 0 0 2px rgba(21, 115, 71, .18);
}
.att-select.att-absent {
background: #bb2d3b;
color: #fff;
border-color: #842029;
box-shadow: 0 0 0 2px rgba(187, 45, 59, .18);
}
.att-select.att-late {
background: #ffca2c;
color: #111;
border-color: #997404;
box-shadow: 0 0 0 2px rgba(255, 202, 44, .25);
}
.att-select.att-none {
background: #fff;
color: #212529;
border-color: #adb5bd;
}
</style>
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Teachers &amp; Assistants — Sundays in <span id="teacherMonthLabel"><?= esc($rangeLabel ?? ('School Year ' . ($schoolYear ?? ''))) ?></span></strong>
<?php if (isset($isCurrentYear) && !$isCurrentYear): ?>
<span class="badge bg-secondary ms-2">Read-only (Past Year)</span>
<?php endif; ?>
</div>
<small class="text-muted">Use the dropdowns to set attendance inline</small>
</div>
<div class="card-body month-grid-wrap">
<div id="teachersGrid" class="text-center text-muted py-4">Loading...</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Admins — Sundays in <span id="adminMonthLabel"><?= esc($rangeLabel ?? ('School Year ' . ($schoolYear ?? ''))) ?></span></strong>
<span class="text-muted ms-2">(All roles except Parent/Guest/Teacher/Teacher Assistant)</span>
</div>
</div>
<div class="card-body month-grid-wrap">
<div id="adminsGrid" class="text-center text-muted py-4">Loading...</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
// Bootstrap 5 Toast utility
(function(){
function ensureToastContainer() {
var c = document.getElementById('toast-container');
if (!c) {
c = document.createElement('div');
c.id = 'toast-container';
c.className = 'toast-container position-fixed top-0 end-0 p-3';
c.style.zIndex = '1080';
document.body.appendChild(c);
}
return c;
}
window.showToast = function(message, ok) {
var c = ensureToastContainer();
var el = document.createElement('div');
el.className = 'toast align-items-center text-bg-' + (ok === false ? 'danger' : 'success') + ' border-0';
el.setAttribute('role','alert');
el.setAttribute('aria-live','assertive');
el.setAttribute('aria-atomic','true');
el.innerHTML = '<div class="d-flex">'
+ '<div class="toast-body">' + (message || (ok === false ? 'Action failed' : 'Saved')) + '</div>'
+ '<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>'
+ '</div>';
c.appendChild(el);
try {
var t = new bootstrap.Toast(el, { delay: 2500, autohide: true });
el.addEventListener('hidden.bs.toast', function(){ el.remove(); });
t.show();
} catch (_) {
setTimeout(function(){ el.remove(); }, 2500);
}
}
})();
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var container = document.querySelector('[data-endpoint]');
if (!container) {
return;
}
var endpoint = container.getAttribute('data-endpoint') || '';
var saveUrl = container.getAttribute('data-save-endpoint') || '';
var csrfName = container.getAttribute('data-csrf-name') || '';
var csrfHash = container.getAttribute('data-csrf-value') || '';
var semesterDefault = container.getAttribute('data-semester') || '';
var schoolYearDefault = container.getAttribute('data-school-year') || '';
var isCurrentYear = (container.getAttribute('data-is-current') || '1') === '1';
var teachersGrid = document.getElementById('teachersGrid');
var adminsGrid = document.getElementById('adminsGrid');
var teacherMonthLabel = document.getElementById('teacherMonthLabel');
var adminMonthLabel = document.getElementById('adminMonthLabel');
if (!endpoint) {
if (teachersGrid) {
teachersGrid.innerHTML = '<div class="alert alert-danger mb-0">Endpoint not configured.</div>';
}
if (adminsGrid) {
adminsGrid.innerHTML = '';
}
return;
}
fetch(endpoint, {
headers: { 'Accept': 'application/json' },
credentials: 'same-origin'
})
.then(function(response) {
if (!response.ok) {
throw new Error('Request failed with status ' + response.status);
}
return response.json();
})
.then(function(payload) {
renderTeachers(payload);
renderAdmins(payload);
initializeSelects();
})
.catch(function(error) {
console.error(error);
if (teachersGrid) {
teachersGrid.innerHTML = '<div class="alert alert-danger mb-0">Unable to load teacher attendance.</div>';
}
if (adminsGrid) {
adminsGrid.innerHTML = '<div class="alert alert-danger mb-0">Unable to load admin attendance.</div>';
}
if (window.showToast) showToast('Failed to load attendance data', false);
});
function clearElement(el) {
if (!el) {
return;
}
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
function renderTeachers(payload) {
if (!teachersGrid) {
return;
}
clearElement(teachersGrid);
var filters = payload && payload.filters ? payload.filters : {};
var sections = payload && payload.sections ? payload.sections : [];
var sundays = payload && payload.sundays ? payload.sundays : [];
var noSchoolDays = payload && payload.noSchoolDays ? payload.noSchoolDays : [];
var noSchoolSet = {};
for (var i = 0; i < noSchoolDays.length; i++) { noSchoolSet[noSchoolDays[i]] = true; }
var label = filters.range_label || filters.month_label || '';
if (teacherMonthLabel && label) {
teacherMonthLabel.textContent = label;
}
if (!sections.length) {
var info = document.createElement('div');
info.className = 'alert alert-info mb-0';
info.textContent = 'No sections with assigned teachers found for this term.';
teachersGrid.appendChild(info);
return;
}
var table = document.createElement('table');
table.className = 'table table-bordered table-striped month-grid align-middle';
table.setAttribute('data-no-mgmt-sticky', '1');
var thead = document.createElement('thead');
var headerRow = document.createElement('tr');
var thSection = document.createElement('th');
thSection.className = 'sticky-col';
thSection.style.minWidth = '180px';
thSection.appendChild(document.createTextNode('Class / Section'));
headerRow.appendChild(thSection);
var thTeacher = document.createElement('th');
thTeacher.className = 'sticky-col-2';
thTeacher.style.minWidth = '280px';
thTeacher.appendChild(document.createTextNode('Teacher'));
headerRow.appendChild(thTeacher);
var thRole = document.createElement('th');
thRole.className = 'sticky-col-3 text-center';
thRole.style.minWidth = '120px';
thRole.appendChild(document.createTextNode('Role'));
headerRow.appendChild(thRole);
for (var i = 0; i < sundays.length; i++) {
var day = sundays[i];
var thDay = document.createElement('th');
thDay.className = 'text-center';
thDay.style.minWidth = '56px';
if (noSchoolSet[day]) thDay.classList.add('no-school-day');
// Show date as mm-dd-yyyy
var yyyy = day.substring(0, 4);
var mm = day.substring(5, 7);
var dd = day.substring(8, 10);
thDay.title = day; // full YYYY-MM-DD tooltip
thDay.appendChild(document.createTextNode(mm + '-' + dd));
headerRow.appendChild(thDay);
}
var totals = ['P', 'A', 'L'];
for (var j = 0; j < totals.length; j++) {
var thTotal = document.createElement('th');
thTotal.className = 'text-center';
thTotal.style.minWidth = '70px';
thTotal.appendChild(document.createTextNode(totals[j]));
headerRow.appendChild(thTotal);
}
thead.appendChild(headerRow);
table.appendChild(thead);
var tbody = document.createElement('tbody');
var hasTeachers = false;
for (var s = 0; s < sections.length; s++) {
var section = sections[s];
var teacherList = section && section.teachers ? section.teachers : [];
if (!teacherList.length) {
continue;
}
hasTeachers = true;
var rowSpan = teacherList.length;
for (var t = 0; t < teacherList.length; t++) {
var teacher = teacherList[t];
var row = document.createElement('tr');
if (t === 0) {
var tdSection = document.createElement('td');
tdSection.className = 'sticky-col';
tdSection.style.minWidth = '180px';
tdSection.rowSpan = rowSpan;
tdSection.appendChild(document.createTextNode(section.label || ('Section ' + section.section_code)));
row.appendChild(tdSection);
}
var tdTeacher = document.createElement('td');
tdTeacher.className = 'sticky-col-2';
tdTeacher.appendChild(document.createTextNode(teacher.name || '—'));
row.appendChild(tdTeacher);
var tdRole = document.createElement('td');
tdRole.className = 'sticky-col-3 text-center';
var roleText = (teacher.position || '').toLowerCase() === 'main' ? 'Main' : 'Assistant';
tdRole.appendChild(document.createTextNode(roleText));
row.appendChild(tdRole);
for (var d = 0; d < sundays.length; d++) {
var dayKey = sundays[d];
var cellData = teacher.cells && teacher.cells[dayKey] ? teacher.cells[dayKey] : null;
var status = '';
if (cellData && cellData.status) {
var st = String(cellData.status).toLowerCase();
if (st === 'present' || st === 'absent' || st === 'late') {
status = st;
}
}
var tdCell = document.createElement('td');
tdCell.className = 'text-center';
if (noSchoolSet[dayKey]) {
tdCell.classList.add('no-school-day');
var ns = document.createElement('span');
ns.className = 'badge-cell bg-info text-dark';
ns.textContent = 'NS';
tdCell.appendChild(ns);
row.appendChild(tdCell);
continue;
}
var select = createAttendanceSelect(
'teacher',
section.section_code,
teacher.user_id || teacher.teacher_id || section.section_code,
dayKey,
filters,
roleText,
status
);
tdCell.appendChild(select);
row.appendChild(tdCell);
}
var totalsRow = teacher.totals || { p: 0, a: 0, l: 0 };
var totalValues = [totalsRow.p || 0, totalsRow.a || 0, totalsRow.l || 0];
for (var tv = 0; tv < totalValues.length; tv++) {
var tdTotal = document.createElement('td');
tdTotal.className = 'text-center';
var strong = document.createElement('strong');
strong.appendChild(document.createTextNode(String(totalValues[tv])));
tdTotal.appendChild(strong);
row.appendChild(tdTotal);
}
tbody.appendChild(row);
}
}
if (!hasTeachers) {
var emptyRow = document.createElement('tr');
var emptyCell = document.createElement('td');
emptyCell.colSpan = 3 + sundays.length + 3;
emptyCell.className = 'text-center';
emptyCell.appendChild(document.createTextNode('No teachers assigned.'));
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
}
table.appendChild(tbody);
teachersGrid.appendChild(table);
}
function renderAdmins(payload) {
if (!adminsGrid) {
return;
}
clearElement(adminsGrid);
var filters = payload && payload.filters ? payload.filters : {};
var admins = payload && payload.admins ? payload.admins : [];
var sundays = payload && payload.sundays ? payload.sundays : [];
var noSchoolDays = payload && payload.noSchoolDays ? payload.noSchoolDays : [];
var noSchoolSet = {};
for (var i = 0; i < noSchoolDays.length; i++) { noSchoolSet[noSchoolDays[i]] = true; }
var label = filters.range_label || filters.month_label || '';
if (adminMonthLabel && label) {
adminMonthLabel.textContent = label;
}
if (!admins.length) {
var info = document.createElement('div');
info.className = 'alert alert-info mb-0';
info.textContent = 'No admin users found for this term.';
adminsGrid.appendChild(info);
return;
}
var table = document.createElement('table');
table.className = 'table table-bordered table-striped month-grid align-middle';
table.setAttribute('data-no-mgmt-sticky', '1');
var thead = document.createElement('thead');
var headerRow = document.createElement('tr');
var thAdmin = document.createElement('th');
thAdmin.className = 'sticky-col';
thAdmin.style.minWidth = '180px';
thAdmin.appendChild(document.createTextNode('Admin'));
headerRow.appendChild(thAdmin);
var thRole = document.createElement('th');
thRole.className = 'sticky-col-2 text-center';
thRole.style.minWidth = '240px';
thRole.appendChild(document.createTextNode('Role'));
headerRow.appendChild(thRole);
for (var i = 0; i < sundays.length; i++) {
var thDay = document.createElement('th');
thDay.className = 'text-center';
thDay.style.minWidth = '56px';
if (noSchoolSet[sundays[i]]) thDay.classList.add('no-school-day');
// Show date as mm-dd-yyyy
var sday = sundays[i];
var sdd = sday.substring(8, 10);
var smm = sday.substring(5, 7);
var syyyy = sday.substring(0, 4);
thDay.title = sday; // full YYYY-MM-DD tooltip
thDay.appendChild(document.createTextNode(smm + '-' + sdd));
headerRow.appendChild(thDay);
}
var totals = ['P', 'A', 'L'];
for (var j = 0; j < totals.length; j++) {
var thTotal = document.createElement('th');
thTotal.className = 'text-center';
thTotal.style.minWidth = '70px';
thTotal.appendChild(document.createTextNode(totals[j]));
headerRow.appendChild(thTotal);
}
thead.appendChild(headerRow);
table.appendChild(thead);
var tbody = document.createElement('tbody');
for (var a = 0; a < admins.length; a++) {
var admin = admins[a];
var row = document.createElement('tr');
var tdName = document.createElement('td');
tdName.className = 'sticky-col';
tdName.appendChild(document.createTextNode(admin.name || '—'));
row.appendChild(tdName);
var tdRole = document.createElement('td');
tdRole.className = 'sticky-col-2 text-center';
tdRole.appendChild(document.createTextNode(admin.role || '—'));
row.appendChild(tdRole);
for (var d = 0; d < sundays.length; d++) {
var dayKey = sundays[d];
var cellData = admin.cells && admin.cells[dayKey] ? admin.cells[dayKey] : null;
var status = '';
if (cellData && cellData.status) {
var st = String(cellData.status).toLowerCase();
if (st === 'present' || st === 'absent' || st === 'late') {
status = st;
}
}
var tdCell = document.createElement('td');
tdCell.className = 'text-center';
if (noSchoolSet[dayKey]) {
tdCell.classList.add('no-school-day');
var ns = document.createElement('span');
ns.className = 'badge-cell bg-info text-dark';
ns.textContent = 'NS';
tdCell.appendChild(ns);
row.appendChild(tdCell);
continue;
}
var select = createAttendanceSelect(
'admin',
null,
admin.user_id,
dayKey,
filters,
admin.role || 'Admin',
status
);
tdCell.appendChild(select);
row.appendChild(tdCell);
}
var totalsRow = admin.totals || { p: 0, a: 0, l: 0 };
var totalValues = [totalsRow.p || 0, totalsRow.a || 0, totalsRow.l || 0];
for (var tv = 0; tv < totalValues.length; tv++) {
var tdTotal = document.createElement('td');
tdTotal.className = 'text-center';
var strong = document.createElement('strong');
strong.appendChild(document.createTextNode(String(totalValues[tv])));
tdTotal.appendChild(strong);
row.appendChild(tdTotal);
}
tbody.appendChild(row);
}
table.appendChild(tbody);
adminsGrid.appendChild(table);
}
function createAttendanceSelect(type, sectionCode, userId, date, filters, roleName, currentValue) {
var select = document.createElement('select');
select.className = 'form-select form-select-sm att-select';
select.setAttribute('data-type', type);
if (sectionCode !== null && sectionCode !== undefined) {
select.setAttribute('data-section-id', sectionCode);
}
select.setAttribute('data-user-id', userId);
select.setAttribute('data-date', date);
// Derive semester for saves: if filter is whole-year (---), infer by date
var semForSave = filters.semester || semesterDefault;
if (semForSave === '---') {
semForSave = inferSemesterByDate(date, (filters.school_year || schoolYearDefault));
}
select.setAttribute('data-semester', semForSave);
select.setAttribute('data-school-year', filters.school_year || schoolYearDefault);
if (roleName) {
select.setAttribute('data-role-name', roleName);
}
var statuses = ['', 'present', 'absent', 'late'];
var labels = ['—', 'P', 'A', 'L'];
for (var i = 0; i < statuses.length; i++) {
var option = document.createElement('option');
option.value = statuses[i];
option.appendChild(document.createTextNode(labels[i]));
if (statuses[i] === currentValue) {
option.selected = true;
}
select.appendChild(option);
}
if (!isCurrentYear) {
select.disabled = true;
select.title = 'Editing disabled for non-current year';
}
return select;
}
// Infer semester by date using fixed windows:
// Fall: Y1-09-21 to Y2-01-18
// Spring: Y2-01-25 to Y2-05-30
function inferSemesterByDate(dateStr, schoolYear) {
try {
if (!schoolYear || schoolYear.indexOf('-') < 0) return '';
var parts = schoolYear.split('-');
var y1 = parseInt(parts[0], 10);
var y2 = parseInt(parts[1], 10);
if (!y1 || !y2) return '';
var fallStart = new Date(y1 + '-09-21T00:00:00Z');
var fallEnd = new Date(y2 + '-01-18T00:00:00Z');
var spStart = new Date(y2 + '-01-25T00:00:00Z');
var spEnd = new Date(y2 + '-05-30T00:00:00Z');
var d = new Date(dateStr + 'T00:00:00Z');
if (d >= fallStart && d <= fallEnd) return 'Fall';
if (d >= spStart && d <= spEnd) return 'Spring';
// Fallback by month if out of bounds
var m = d.getUTCMonth() + 1; // 1..12
return (m >= 9 || m <= 1) ? 'Fall' : 'Spring';
} catch (_) {
return '';
}
}
function initializeSelects() {
var selects = document.querySelectorAll('.att-select');
for (var i = 0; i < selects.length; i++) {
applyAppearance(selects[i]);
}
}
function applyAppearance(select) {
if (!select) {
return;
}
select.classList.remove('att-none', 'att-present', 'att-absent', 'att-late');
var value = (select.value || '').toLowerCase();
if (value === 'present') {
select.classList.add('att-present');
} else if (value === 'absent') {
select.classList.add('att-absent');
} else if (value === 'late') {
select.classList.add('att-late');
} else {
select.classList.add('att-none');
}
}
function recomputeRowTotals(row) {
if (!row) {
return;
}
var totals = { p: 0, a: 0, l: 0 };
var selects = row.querySelectorAll('.att-select');
for (var i = 0; i < selects.length; i++) {
var value = selects[i].value;
if (value === 'present') {
totals.p++;
} else if (value === 'absent') {
totals.a++;
} else if (value === 'late') {
totals.l++;
}
}
var cells = row.querySelectorAll('td');
var len = cells.length;
if (len >= 3) {
cells[len - 3].innerHTML = '<strong>' + totals.p + '</strong>';
cells[len - 2].innerHTML = '<strong>' + totals.a + '</strong>';
cells[len - 1].innerHTML = '<strong>' + totals.l + '</strong>';
}
}
function toFormData(obj) {
var fd = new FormData();
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fd.append(key, obj[key]);
}
}
return fd;
}
document.addEventListener('change', function(event) {
var select = event.target;
if (!select || !select.classList || !select.classList.contains('att-select')) {
return;
}
if (!isCurrentYear) {
event.preventDefault();
if (window.showToast) showToast('Editing disabled for non-current year', false);
return;
}
var payload = {};
payload[csrfName] = csrfHash;
payload.date = select.getAttribute('data-date') || '';
payload.status = select.value || '';
payload.user_id = select.getAttribute('data-user-id') || '';
payload.semester = select.getAttribute('data-semester') || semesterDefault;
payload.school_year = select.getAttribute('data-school-year') || schoolYearDefault;
payload.role_name = select.getAttribute('data-role-name') || '';
if (!payload.date || !payload.user_id || !payload.semester || !payload.school_year) {
console.error('Missing required fields for save', payload);
return;
}
select.disabled = true;
fetch(saveUrl, {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
// Send CSRF in headers as well to satisfy strict CSRF setups
'X-CSRF-TOKEN': csrfHash,
'X-CSRF-Token': csrfHash
},
body: toFormData(payload),
credentials: 'same-origin'
})
.then(function(response) {
return response.json().catch(function() {
return {};
}).then(function(data) {
return { ok: response.ok, data: data };
});
})
.then(function(result) {
if (!result) {
throw new Error('Invalid response');
}
var data = result.data || {};
if (data.csrf_token && data.csrf_hash) {
csrfName = data.csrf_token;
csrfHash = data.csrf_hash;
} else {
// Fallback: try refresh from cookie if server rotated token
try {
var c = document.cookie.split(';').map(function(v){return v.trim();}).find(function(v){
return v.indexOf('<?= esc(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>=')===0;
});
if (c) {
csrfHash = decodeURIComponent(c.split('=')[1] || '');
}
} catch (_) {}
}
if (!result.ok || data.ok === false) {
console.error('Save failed', data);
if (window.showToast) showToast('Save failed', false);
return;
}
applyAppearance(select);
recomputeRowTotals(select.closest('tr'));
if (window.showToast) showToast('Saved');
})
.catch(function(error) {
console.error('Network/save error', error);
if (window.showToast) showToast('Network error while saving', false);
})
.finally(function() {
select.disabled = false;
});
});
});
</script>
<?= $this->endSection() ?>