recreate project
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
<?= $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">
|
||||
<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') ?>">
|
||||
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
|
||||
|
||||
<div id="clientCheckAlert" class="alert d-none" role="alert"></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) submitBtn.disabled = false;
|
||||
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) submitBtn.disabled = false;
|
||||
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) submitBtn.disabled = (selectedIds().length === 0);
|
||||
} catch (e) {
|
||||
setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.');
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<?= $this->endSection() ?>
|
||||
Reference in New Issue
Block a user