564 lines
23 KiB
PHP
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() ?>
|