Files
alrahma_sunday_school/app/Views/parent/report_attendance.php
T
2026-02-26 23:27:50 -05:00

564 lines
23 KiB
PHP

<?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?>
<div class="container my-5" style="max-width: 880px;">
<h3 class="text-center text-success mb-4" style="font-family: Arial, sans-serif;">
Report Absence/Late/Early Dismissal
</h3>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('message')): ?>
<div class="alert alert-success">
<?= session()->getFlashdata('message') ?>
</div>
<?php endif; ?>
<?php
// Build preview/edit table for upcoming reports
$myReports = is_array($myReports ?? null) ? $myReports : [];
$tzName = 'UTC';
try { $tzName = (string)(config('School')->attendance['timezone'] ?? user_timezone()); } catch (\Throwable $e) { $tzName = user_timezone(); }
$nowTz = new DateTime('now', new DateTimeZone($tzName));
?>
<?php if (!empty($myReports)): ?>
<div class="card shadow-sm mb-4">
<div class="card-header bg-light"><strong>Upcoming Submissions (Preview & Edit)</strong></div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Student</th>
<th>Date</th>
<th>Type</th>
<th>Time</th>
<th>Reason</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($myReports as $r): ?>
<?php
$reportDate = (string)($r['report_date'] ?? '');
$reportDateLabel = $reportDate !== '' ? local_date($reportDate, 'm-d-Y') : '';
$type = (string)($r['type'] ?? '');
$isLate = ($type === 'late');
$isED = ($type === 'early_dismissal');
$timeVal = $isLate ? ($r['arrival_time'] ?? '') : ($isED ? ($r['dismiss_time'] ?? '') : '');
$status = (string)($r['status'] ?? '');
$cutoff = null; $editable = false;
try { $cutoff = new DateTime($reportDate . ' 09:00:00', new DateTimeZone($tzName)); } catch (\Throwable $e) { $cutoff = null; }
if ($cutoff) {
$editable = ($status === 'new') && ($nowTz < $cutoff);
}
?>
<tr>
<td><?= esc(trim(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? ''))) ?></td>
<td><?= esc($reportDateLabel) ?></td>
<td><?= esc(ucwords(str_replace('_', ' ', $type))) ?></td>
<td><?= esc($timeVal ?: '—') ?></td>
<td><?= esc($r['reason'] ?? '') ?></td>
<td><?= esc($status ?: 'new') ?></td>
<td>
<?php if ($editable): ?>
<button type="button" class="btn btn-sm btn-outline-primary" data-edit-row="<?= (int)$r['id'] ?>">Edit</button>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
</tr>
<?php if ($editable): ?>
<tr class="d-none" data-edit-form-row="<?= (int)$r['id'] ?>">
<td colspan="7">
<form method="post" action="<?= site_url('parent/report-attendance/update') ?>" class="row g-2 align-items-end">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<div class="col-md-3">
<label class="form-label">Type</label>
<input type="text" class="form-control" value="<?= esc(ucwords(str_replace('_', ' ', $type))) ?>" disabled>
</div>
<?php if ($isLate): ?>
<div class="col-md-3">
<label class="form-label">Arrival Time</label>
<input type="time" class="form-control" name="arrival_time" value="<?= esc(substr((string)$timeVal,0,5)) ?>">
</div>
<?php elseif ($isED): ?>
<div class="col-md-3">
<label class="form-label">Dismissal Time</label>
<input type="time" class="form-control" name="dismiss_time" value="<?= esc(substr((string)$timeVal,0,5)) ?>">
</div>
<?php else: ?>
<div class="col-md-3">
<label class="form-label">Time</label>
<input type="text" class="form-control" value="—" disabled>
</div>
<?php endif; ?>
<div class="col-md-5">
<label class="form-label">Reason</label>
<input type="text" class="form-control" name="reason" value="<?= esc((string)($r['reason'] ?? '')) ?>" placeholder="Optional">
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-success btn-sm">Save</button>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-secondary btn-sm" data-cancel-edit="<?= (int)$r['id'] ?>">Cancel</button>
</div>
</form>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="text-muted small">Editing allowed until 9:00 AM on the report date.</div>
</div>
</div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-body">
<?php
$cutoffDate = '';
$cutoffTime = '';
$cutoffThreshold = (string) ($cutoffThreshold ?? '09:00');
$cutoffTzAbbr = '';
try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = new DateTimeZone($tzName ?: 'UTC');
$nowTz = new DateTime('now', $tz);
$cutoffDate = $nowTz->format('Y-m-d');
$cutoffTime = $nowTz->format('H:i');
$cutoffTzAbbr = $nowTz->format('T');
} catch (\Throwable $e) {
$cutoffDate = '';
$cutoffTime = '';
$cutoffTzAbbr = '';
}
?>
<form id="reportForm"
method="post"
action="<?= site_url('parent/report-attendance') ?>"
data-csrf-name="<?= esc(csrf_token()) ?>"
data-csrf-value="<?= esc(csrf_hash()) ?>"
data-check-url="<?= site_url('api/parent/report-attendance/check') ?>"
data-cutoff-date="<?= esc($cutoffDate) ?>"
data-cutoff-time="<?= esc($cutoffTime) ?>"
data-cutoff-threshold="<?= esc($cutoffThreshold) ?>"
data-cutoff-blocked="0">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
<div id="clientCheckAlert" class="alert d-none" role="alert"></div>
<div id="cutoffAlert" class="alert alert-warning d-none" role="alert">
Reporting closes at <?= esc($cutoffThreshold) ?><?= $cutoffTzAbbr !== '' ? ' ' . esc($cutoffTzAbbr) : '' ?> on the report date.
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Select Child(ren)</label>
<div class="row g-2">
<?php if (!empty($students)): ?>
<?php foreach ($students as $s): ?>
<div class="col-md-6">
<div class="form-check border rounded p-2" data-student-box="<?= (int)$s['id'] ?>">
<?php
$oldStudents = (array) old('student_ids');
$isChecked = in_array((string)$s['id'], array_map('strval', $oldStudents ?? []), true);
?>
<input class="form-check-input" type="checkbox" name="student_ids[]" id="stu<?= (int)$s['id'] ?>" value="<?= (int)$s['id'] ?>" <?= $isChecked ? 'checked' : '' ?>>
<label class="form-check-label" for="stu<?= (int)$s['id'] ?>">
<?= esc($s['firstname'] . ' ' . $s['lastname']) ?>
</label>
<div class="small mt-1 d-none" data-role="conflict-msg"></div>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="col-12">
<div class="alert alert-warning mb-0">No students found under your account.</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Sunday Date(s)</label>
<select name="dates[]" class="form-select" required multiple size="6" style="max-height: 220px; overflow-y: auto;">
<?php
$list = $sundays ?? [];
$oldDatesRaw = old('dates');
$selectedDates = [];
if (is_array($oldDatesRaw)) {
$selectedDates = array_map('strval', $oldDatesRaw);
} elseif ($oldDatesRaw !== null) {
$selectedDates = [(string) $oldDatesRaw];
} elseif (old('date')) {
$selectedDates = [(string) old('date')];
} elseif (!empty($defaultDate)) {
$selectedDates = [(string) $defaultDate];
}
$selectedDates = array_unique(array_filter($selectedDates));
foreach ($list as $opt):
$v = $opt['value'] ?? '';
$label = $opt['label'] ?? $v;
$isSel = in_array((string)$v, $selectedDates, true);
?>
<option value="<?= esc($v) ?>" <?= $isSel ? 'selected' : '' ?>><?= esc($label) ?></option>
<?php endforeach; ?>
</select>
<div class="form-text">Select one or more Sundays (hold Ctrl/Cmd or Shift to pick multiple). Scroll for future dates through early June.</div>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Type</label>
<select name="type" id="reportType" class="form-select" required>
<?php $oldType = (string) (old('type') ?: 'absent'); ?>
<option value="absent" <?= $oldType === 'absent' ? 'selected' : '' ?>>Absent</option>
<option value="late" <?= $oldType === 'late' ? 'selected' : '' ?>>Late</option>
<option value="early_dismissal" <?= $oldType === 'early_dismissal' ? 'selected' : '' ?>>Early Dismissal</option>
</select>
</div>
<div class="col-md-3" id="arrivalTimeWrap" style="display:none;">
<label class="form-label fw-semibold">Expected Arrival Time</label>
<input type="time" name="arrival_time" class="form-control" placeholder="HH:MM" value="<?= esc(old('arrival_time') ?: '') ?>">
</div>
<div class="col-md-3" id="dismissTimeWrap" style="display:none;">
<label class="form-label fw-semibold">Dismissal Time</label>
<input type="time" name="dismiss_time" class="form-control" placeholder="HH:MM" value="<?= esc(old('dismiss_time') ?: '') ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" id="reasonLabel">Reason</label>
<textarea name="reason" id="reasonInput" rows="3" class="form-control" placeholder="Brief reason to help teachers plan."><?= esc(old('reason') ?: '') ?></textarea>
<div class="form-text" id="reasonHelp">Required for Absence/Late so we can support your child.</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">Submit</button>
<?php $prev = previous_url() ?: site_url('/parent/attendance'); ?>
<a href="<?= esc($prev) ?>" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const typeSel = document.getElementById('reportType');
const arrivalWrap = document.getElementById('arrivalTimeWrap');
const dismissWrap = document.getElementById('dismissTimeWrap');
const reasonField = document.getElementById('reasonInput');
const reasonHelp = document.getElementById('reasonHelp');
const reasonLabel = document.getElementById('reasonLabel');
function updateFields() {
const v = (typeSel.value || '').toLowerCase();
arrivalWrap.style.display = (v === 'late') ? '' : 'none';
dismissWrap.style.display = (v === 'early_dismissal') ? '' : 'none';
const reasonRequired = (v === 'absent' || v === 'late');
if (reasonField) {
reasonField.required = reasonRequired;
}
if (reasonLabel) {
reasonLabel.innerHTML = reasonRequired
? 'Reason <span class="text-danger">*</span>'
: 'Reason (optional)';
}
if (reasonHelp) {
reasonHelp.textContent = reasonRequired
? 'Required for absence/late so teachers can plan.'
: 'Optional for early dismissal.';
}
// Nudge visibility into view so parents notice the time field
if (v === 'late') {
try { arrivalWrap.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch(e) {}
} else if (v === 'early_dismissal') {
try { dismissWrap.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch(e) {}
}
}
typeSel.addEventListener('change', updateFields);
updateFields();
});
</script>
<script>
// Inline edit toggling for the preview table
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-edit-row]').forEach(function(btn){
btn.addEventListener('click', function(){
const id = this.getAttribute('data-edit-row');
const row = document.querySelector('[data-edit-form-row="' + id + '"]');
if (!row) return;
row.classList.toggle('d-none');
try { row.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch(_) {}
});
});
document.querySelectorAll('[data-cancel-edit]').forEach(function(btn){
btn.addEventListener('click', function(){
const id = this.getAttribute('data-cancel-edit');
const row = document.querySelector('[data-edit-form-row="' + id + '"]');
if (!row) return;
row.classList.add('d-none');
});
});
});
</script>
<script>
// Conflict check with CSRF refresh
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('reportForm');
if (!form) return;
let checkUrl = form.getAttribute('data-check-url') || '';
let csrfName = form.getAttribute('data-csrf-name') || '';
let csrfVal = form.getAttribute('data-csrf-value') || '';
const typeSel = document.getElementById('reportType');
const dateSel = form.querySelector('select[name="dates[]"]');
const submitBtn = form.querySelector('button[type="submit"]');
const alertBox = document.getElementById('clientCheckAlert');
function selectedIds() {
const ids = [];
form.querySelectorAll('input[name="student_ids[]"]:checked').forEach(cb => {
const v = parseInt(cb.value, 10);
if (v > 0) ids.push(v);
});
return ids;
}
function setAlert(kind, html) {
if (!alertBox) return;
if (!html) {
alertBox.className = 'alert d-none';
alertBox.innerHTML = '';
return;
}
let cls = 'alert-info';
if (kind === 'error') cls = 'alert-danger';
else if (kind === 'warn') cls = 'alert-warning';
else if (kind === 'ok') cls = 'alert-success';
alertBox.className = 'alert ' + cls;
alertBox.innerHTML = html;
}
function setStudentMsg(studentId, msg, kind) {
const box = form.querySelector('[data-student-box="' + studentId + '"] [data-role="conflict-msg"]');
if (!box) return;
if (!msg) {
box.classList.add('d-none');
box.textContent = '';
return;
}
box.classList.remove('d-none');
box.classList.remove('text-danger', 'text-info');
box.classList.add(kind === 'info' ? 'text-info' : 'text-danger');
box.textContent = msg;
}
function selectedDates() {
if (!dateSel) return [];
return Array.from(dateSel.selectedOptions || [])
.map(opt => opt.value)
.filter(v => v);
}
async function runCheck() {
setAlert('', '');
const ids = selectedIds();
const dates = selectedDates();
if (!checkUrl || !dateSel || !typeSel) return;
if (!ids.length || !dates.length) {
if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
return;
}
form.querySelectorAll('[data-student-box] input[type="checkbox"]').forEach(cb => {
cb.disabled = false;
});
form.querySelectorAll('[data-role="conflict-msg"]').forEach(el => {
el.classList.add('d-none');
el.textContent = '';
});
const body = new URLSearchParams();
body.append(csrfName, csrfVal);
dates.forEach(function(d) {
body.append('dates[]', d);
});
body.append('type', typeSel.value || '');
ids.forEach(id => body.append('student_ids[]', String(id)));
try {
const res = await fetch(checkUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
[csrfName]: csrfVal
},
body: body.toString()
});
const data = await res.json();
// ✅ Refresh CSRF token if returned
if (data?.csrf) {
csrfName = data.csrf.name;
csrfVal = data.csrf.hash;
form.setAttribute('data-csrf-name', csrfName);
form.setAttribute('data-csrf-value', csrfVal);
const hidden = form.querySelector('input[name="' + csrfName + '"]');
if (hidden) hidden.value = csrfVal;
}
if (!data || !data.ok) {
if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
return;
}
const byId = {};
(data.students || []).forEach(s => {
byId[s.student_id] = s;
});
const type = (typeSel.value || '').toLowerCase();
const conflicts = [];
const infoOnly = [];
ids.forEach(id => {
const s = byId[id];
const dateTypes = (s && s.dates) ? s.dates : {};
const fullname = (s ? ((s.firstname || '') + ' ' + (s.lastname || '')) : ('Student #' + id)).trim();
const conflictDates = [];
const infoDates = [];
dates.forEach(function(dateVal) {
const entry = dateTypes && dateTypes[dateVal];
const typesForDate = Array.isArray(entry) ? entry : [];
if (!typesForDate.length) return;
if (type === 'early_dismissal') {
if (typesForDate.includes('absent') || typesForDate.includes('late')) {
conflictDates.push(dateVal);
} else if (typesForDate.includes('early_dismissal')) {
infoDates.push(dateVal);
}
} else {
conflictDates.push(dateVal);
}
});
if (conflictDates.length) {
conflicts.push(fullname + ' (' + conflictDates.join(', ') + ')');
setStudentMsg(id, 'Already submitted for: ' + conflictDates.join(', '), 'error');
const cb = form.querySelector('[data-student-box="' + id + '"] input[type="checkbox"]');
if (cb) {
cb.checked = false;
cb.disabled = true;
}
} else if (infoDates.length) {
infoOnly.push(fullname + ' (' + infoDates.join(', ') + ')');
setStudentMsg(id, 'Early dismissal already submitted for: ' + infoDates.join(', ') + ' — time will be updated.', 'info');
}
});
if (conflicts.length && ids.length === conflicts.length) {
setAlert('warn', 'All selected students have existing submissions for the chosen date(s).');
} else if (conflicts.length) {
setAlert('warn', 'Some selections disabled due to existing submissions on selected dates.');
} else if (infoOnly.length) {
setAlert('info', 'Note: some students already have early dismissal on these dates—time will be updated.');
} else {
setAlert('', '');
}
if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked || (selectedIds().length === 0);
}
} catch (e) {
setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.');
if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
}
}
form.addEventListener('change', function(e) {
const tgt = e.target;
if (!tgt) return;
if (tgt.matches('select[name="dates[]"], #reportType, input[name="student_ids[]"]')) {
runCheck();
}
});
runCheck();
});
</script>
<script>
// Cutoff enforcement helper (9:00 AM on the selected Sunday).
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('reportForm');
if (!form) return;
const dateSel = form.querySelector('select[name="dates[]"]');
const submitBtn = form.querySelector('button[type="submit"]');
const cutoffAlert = document.getElementById('cutoffAlert');
const cutoffDate = form.getAttribute('data-cutoff-date') || '';
const cutoffTime = form.getAttribute('data-cutoff-time') || '';
const cutoffThreshold = form.getAttribute('data-cutoff-threshold') || '09:00';
function parseTimeToMinutes(val) {
const parts = (val || '').split(':');
if (parts.length < 2) return null;
const hh = parseInt(parts[0], 10);
const mm = parseInt(parts[1], 10);
if (Number.isNaN(hh) || Number.isNaN(mm)) return null;
return (hh * 60) + mm;
}
function updateCutoffState() {
if (!dateSel || !cutoffDate || !cutoffTime) return;
const dates = Array.from(dateSel.selectedOptions || [])
.map(opt => opt.value)
.filter(v => v);
const nowMinutes = parseTimeToMinutes(cutoffTime);
const cutoffMinutes = parseTimeToMinutes(cutoffThreshold);
const isTodaySelected = dates.includes(cutoffDate);
const blocked = isTodaySelected && nowMinutes !== null && cutoffMinutes !== null && nowMinutes >= cutoffMinutes;
form.setAttribute('data-cutoff-blocked', blocked ? '1' : '0');
if (cutoffAlert) {
cutoffAlert.classList.toggle('d-none', !blocked);
}
if (submitBtn && blocked) {
submitBtn.disabled = true;
}
}
form.addEventListener('change', function(e) {
const tgt = e.target;
if (!tgt) return;
if (tgt.matches('select[name="dates[]"], input[name="student_ids[]"], #reportType')) {
updateCutoffState();
}
});
updateCutoffState();
});
</script>
<?= $this->endSection() ?>