901 lines
30 KiB
PHP
Executable File
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 & 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 & 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() ?>
|