205 lines
7.3 KiB
PHP
205 lines
7.3 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
<?= csrf_meta() ?>
|
|
|
|
<div class="container-xxl py-4">
|
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
|
<h2 class="mb-0">Add Early Dismissal</h2>
|
|
<a href="<?= site_url('attendance/early-dismissals') ?>" class="btn btn-outline-secondary">Back to List</a>
|
|
</div>
|
|
|
|
<?php if (session()->getFlashdata('error')): ?>
|
|
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
|
|
<?php endif; ?>
|
|
<?php if (session()->getFlashdata('message')): ?>
|
|
<div class="alert alert-success"><?= session()->getFlashdata('message') ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<form method="post" action="<?= site_url('attendance/early-dismissals') ?>">
|
|
<?= csrf_field() ?>
|
|
<div class="row gy-3">
|
|
<div class="col-md-5 position-relative">
|
|
<label class="form-label">Student</label>
|
|
<input type="text" id="studentSearch" class="form-control" autocomplete="off" placeholder="Type student name to search…" value="<?= esc(old('student_label', '')) ?>">
|
|
<input type="hidden" name="student_id" id="studentId" value="<?= esc(old('student_id', '')) ?>">
|
|
<div id="studentSuggestions" class="list-group position-absolute w-100" style="z-index:1000; max-height: 240px; overflow:auto;"></div>
|
|
<div class="form-text">Start typing first or last name, then pick from the list.</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<label class="form-label">Date (Sunday)</label>
|
|
<input type="date" name="date" class="form-control" value="<?= esc(old('date', $defaultDate ?? local_date(utc_now(), 'Y-m-d'))) ?>" required>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<label class="form-label">Dismissal Time</label>
|
|
<input type="time" name="dismiss_time" class="form-control" value="<?= esc(old('dismiss_time', '')) ?>" required>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Reason (optional)</label>
|
|
<textarea name="reason" class="form-control" rows="2" placeholder="e.g., Family appointment"><?= esc(old('reason', '')) ?></textarea>
|
|
</div>
|
|
|
|
<div class="col-12 d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary">Save Early Dismissal</button>
|
|
<a href="<?= site_url('attendance/early-dismissals') ?>" class="btn btn-outline-secondary">Cancel</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 small text-muted">
|
|
Note: Early dismissals do not change daily present/absent status; they are shown for staff awareness and pickup coordination.
|
|
</div>
|
|
</div>
|
|
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<?php
|
|
// Prepare a minimal dataset for client-side search
|
|
$jsStudents = array_map(static function($s) {
|
|
return [
|
|
'id' => (int)($s['id'] ?? 0),
|
|
'firstname'=> (string)($s['firstname'] ?? ''),
|
|
'lastname' => (string)($s['lastname'] ?? ''),
|
|
'section' => (string)($s['class_section_name'] ?? ''),
|
|
];
|
|
}, (array)($students ?? []));
|
|
?>
|
|
<script>
|
|
(function() {
|
|
const STUDENTS = <?= json_encode($jsStudents, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_AMP|JSON_HEX_QUOT) ?>;
|
|
|
|
const input = document.getElementById('studentSearch');
|
|
const hiddenId = document.getElementById('studentId');
|
|
const list = document.getElementById('studentSuggestions');
|
|
if (!input || !hiddenId || !list) return;
|
|
|
|
const norm = (s) => (s || '').toLowerCase().trim();
|
|
const buildKey = (o) => {
|
|
const first = norm(o.firstname);
|
|
const last = norm(o.lastname);
|
|
const sec = norm(o.section);
|
|
return `${first} ${last} ${last} ${first} ${sec}`;
|
|
};
|
|
|
|
// Build enriched search dataset
|
|
const DATA = STUDENTS.map(s => ({
|
|
...s,
|
|
key: buildKey(s),
|
|
label: `${s.lastname}, ${s.firstname}${s.section ? ' — ' + s.section : ''}`,
|
|
display: `${s.firstname} ${s.lastname}${s.section ? ' — ' + s.section : ''}`
|
|
}));
|
|
|
|
let activeIndex = -1;
|
|
|
|
function render(q) {
|
|
const query = norm(q);
|
|
list.innerHTML = '';
|
|
list.style.display = 'none';
|
|
activeIndex = -1;
|
|
|
|
if (query.length === 0) {
|
|
// If user clears, clear selected student
|
|
hiddenId.value = '';
|
|
return;
|
|
}
|
|
|
|
// Clear selection whenever user types
|
|
hiddenId.value = '';
|
|
|
|
const matches = DATA.filter(s => s.key.includes(query)).slice(0, 20);
|
|
if (matches.length === 0) return;
|
|
|
|
for (let i = 0; i < matches.length; i++) {
|
|
const m = matches[i];
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-group-item list-group-item-action';
|
|
btn.textContent = m.display;
|
|
btn.dataset.id = String(m.id);
|
|
btn.addEventListener('click', () => selectStudent(m));
|
|
list.appendChild(btn);
|
|
}
|
|
list.style.display = '';
|
|
}
|
|
|
|
function selectStudent(s) {
|
|
hiddenId.value = s.id;
|
|
input.value = s.display;
|
|
list.innerHTML = '';
|
|
list.style.display = 'none';
|
|
}
|
|
|
|
input.addEventListener('input', () => render(input.value));
|
|
|
|
// Keyboard navigation
|
|
input.addEventListener('keydown', (e) => {
|
|
const items = Array.from(list.querySelectorAll('.list-group-item'));
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (items.length === 0) return;
|
|
activeIndex = (activeIndex + 1) % items.length;
|
|
updateActive(items);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (items.length === 0) return;
|
|
activeIndex = (activeIndex - 1 + items.length) % items.length;
|
|
updateActive(items);
|
|
} else if (e.key === 'Enter') {
|
|
if (activeIndex >= 0 && items[activeIndex]) {
|
|
e.preventDefault();
|
|
const id = parseInt(items[activeIndex].dataset.id, 10);
|
|
const s = DATA.find(x => x.id === id);
|
|
if (s) selectStudent(s);
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
list.innerHTML = '';
|
|
list.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
function updateActive(items) {
|
|
items.forEach((el, idx) => {
|
|
if (idx === activeIndex) {
|
|
el.classList.add('active');
|
|
el.scrollIntoView({ block: 'nearest' });
|
|
} else {
|
|
el.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Hide suggestions on outside click
|
|
document.addEventListener('click', (e) => {
|
|
if (!list.contains(e.target) && e.target !== input) {
|
|
list.innerHTML = '';
|
|
list.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Client-side submit validation: ensure a student was selected
|
|
const form = input.closest('form');
|
|
if (form) {
|
|
form.addEventListener('submit', (e) => {
|
|
if (!hiddenId.value) {
|
|
e.preventDefault();
|
|
render(input.value);
|
|
input.classList.add('is-invalid');
|
|
if (!list.style.display || list.innerHTML === '') {
|
|
// No matches; show simple feedback
|
|
alert('Please select a student from the list.');
|
|
}
|
|
}
|
|
});
|
|
input.addEventListener('input', () => input.classList.remove('is-invalid'));
|
|
}
|
|
})();
|
|
</script>
|
|
<?= $this->endSection() ?>
|