Files
alrahma_sunday_school/app/Views/attendance/violations_notified.php
T
2026-02-10 22:11:06 -05:00

545 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
$page_title = $title ?? 'Notified Attendance Violations';
$school_year = $school_year ?? '';
$semester = $semester ?? '';
$show_actions = false; // notified page hides actions by design
?>
<div class="container-fluid px-0">
<h2 class="text-center mt-4 mb-3"><?= esc($page_title) ?></h2>
<div class="d-flex justify-content-center mb-2">
<div class="col-12 col-lg-10">
<?= $this->include('partials/academic_filter') ?>
</div>
</div>
<?php if ($msg = session()->getFlashdata('message')): ?>
<div class="alert alert-success mx-3"><?= esc($msg) ?></div>
<?php endif; ?>
<?php if ($err = session()->getFlashdata('error')): ?>
<div class="alert alert-danger mx-3"><?= esc($err) ?></div>
<?php endif; ?>
<div class="row g-0">
<div class="col-12">
<div class="card border-0 rounded-3 shadow-sm">
<div class="card-header d-flex flex-wrap gap-2 justify-content-between align-items-center">
<h5 class="mt-2 mb-0">Notified Attendance Violations</h5>
<div class="d-flex flex-wrap gap-2 align-items-center">
<span class="badge bg-primary">School Year: <?= esc($school_year) ?></span>
<?php if ($semester !== ''): ?>
<span class="badge bg-secondary">Semester: <?= esc($semester) ?></span>
<?php endif; ?>
<a href="<?= site_url('attendance/violations') ?>" class="btn btn-outline-dark btn-sm">
<i class="fas fa-hourglass-half"></i> View Pending
</a>
</div>
</div>
<div class="card-body">
<?php if (empty($students)): ?>
<div class="alert alert-info mb-0">No notified violations found.</div>
<?php else: ?>
<div class="table-responsive">
<table id="notifiedTable" class="table table-striped table-hover align-middle w-100">
<thead class="table-dark">
<tr>
<th>Student</th>
<th>Grade</th>
<th>Last Incident</th>
<th>Reason</th>
<th>Note</th>
<th>Parent</th>
<th>Notified On</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $st): ?>
<?php
// ID may be under 'student_id' (from tracking) or 'id' (from students)
$studId = (int)($st['student_id'] ?? $st['id'] ?? 0);
// Robust name resolution (name, student_name, or first/last)
$studName =
($st['name'] ?? null)
?: ($st['student_name'] ?? null)
?: trim(
($st['firstname'] ?? $st['first_name'] ?? '') . ' ' .
($st['lastname'] ?? $st['last_name'] ?? '')
);
if ($studName === '') {
$studName = $studId ? ('Student #'.$studId) : '—';
}
// Grade/Class name resolution
$className = $st['class_section_name']
?? $st['class_name']
?? $st['className']
?? $st['grade']
?? '';
// Dates and notified meta
$lastDate = $st['last_date'] ?? $st['date'] ?? null; // DATETIME
$notified = !empty($st['is_notified']);
$notifCnt = (int)($st['notif_counter'] ?? 0);
$reason = trim((string)($st['reason'] ?? ''));
// Term fields (fallback to page vars if absent)
$sy = (string)($st['school_year'] ?? $school_year);
$sem = (string)($st['semester'] ?? $semester);
// Row color (optional)
$viColor = (string)($st['color'] ?? '#0d6efd');
// Parent display info (if available)
$parentEmail = $st['parent_email'] ?? '';
$parentName = $st['parent_name'] ?? '';
$parentPhone = $st['parent_phone'] ?? '';
$parentLabel = $parentName ?: ($parentEmail ?: 'View');
// Note from attendance_tracking
$note = trim((string)($st['note'] ?? ''));
$noteShort = $note !== '' ? (mb_strlen($note) > 120 ? (mb_substr($note, 0, 120) . '…') : $note) : '';
$incidentYmd = $lastDate ? substr((string)$lastDate, 0, 10) : '';
$code = (string)($st['reason'] ?? $st['violation_code'] ?? '');
$notifiedAt = $st['updated_at'] ?? $st['date'] ?? null;
?>
<tr style="border-left:6px solid <?= esc($viColor) ?>;">
<td>
<a
href="<?= site_url('family') . '?student_id=' . (int)$studId ?>"
class="text-decoration-none"
data-family-student-id="<?= (int)$studId ?>"
title="Open Family Card">
<?= esc($studName) ?>
</a>
</td>
<td><?= esc($className) ?></td>
<td data-order="<?= $incidentYmd !== '' ? esc($incidentYmd) : '' ?>">
<?= $incidentYmd !== '' ? esc(date('m-d-Y', strtotime($incidentYmd))) : '—' ?>
</td>
<td><?= $reason !== '' ? esc($reason) : '—' ?></td>
<td style="max-width:280px;">
<?php if ($note !== ''): ?>
<div class="small text-wrap mb-1">
<?= nl2br(esc($noteShort)) ?>
</div>
<?php if (mb_strlen($note) > 120): ?>
<button
type="button"
class="btn btn-sm btn-outline-secondary me-1 mb-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="<?= esc($note) ?>"
>
View full
</button>
<?php endif; ?>
<button
type="button"
class="btn btn-sm btn-outline-primary mb-1 add-note-btn"
data-student-id="<?= (int)$studId ?>"
data-student-name="<?= esc($studName) ?>"
data-code="<?= esc($code) ?>"
data-incident-date="<?= esc($incidentYmd) ?>"
data-note="<?= esc($note) ?>"
data-return-url="<?= current_url() ?>"
>
Edit note
</button>
<?php else: ?>
<button
type="button"
class="btn btn-sm btn-outline-primary add-note-btn"
data-student-id="<?= (int)$studId ?>"
data-student-name="<?= esc($studName) ?>"
data-code="<?= esc($code) ?>"
data-incident-date="<?= esc($incidentYmd) ?>"
data-note=""
data-return-url="<?= current_url() ?>"
>
Add note
</button>
<?php endif; ?>
</td>
<!-- Parent column -->
<td>
<?php if ($parentName !== ''): ?>
<a
href="<?= site_url('family') . '?student_id=' . (int)$studId ?>"
class="text-decoration-none"
data-family-student-id="<?= (int)$studId ?>"
title="Open Family Card">
<?= esc($parentName) ?>
</a>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
<td>
<?= $notifiedAt ? esc(local_date($notifiedAt, 'm-d-Y')) : '—' ?>
</td>
<td class="text-nowrap">
<a href="<?= site_url('attendance/view/' . $studId) . '?code=' . urlencode($code) . ($lastDate ? '&incident_date=' . urlencode(substr((string)$lastDate,0,10)) : '') . '&include_notified=1' ?>" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i> Details
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Add/Edit Comment/Note Modal -->
<div class="modal fade" id="notifyCommentModal" tabindex="-1" aria-labelledby="notifyCommentModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="<?= site_url('attendance/save-note') ?>" class="modal-content">
<?= csrf_field() ?>
<div class="modal-header">
<h5 class="modal-title" id="notifyCommentModalLabel">Add Comment / Note</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2 small text-muted" id="noteMeta">
<div><strong>Student Name:</strong> <span id="noteStudentName">—</span></div>
<div><strong>Code:</strong> <span id="noteCode">—</span> &nbsp; <strong>Date:</strong> <span id="noteDate">—</span></div>
</div>
<div class="mb-3">
<label for="note" class="form-label">Comment / Note</label>
<textarea id="note" name="note" class="form-control" rows="5" required placeholder="Type any follow-up, call outcome, or special circumstances..."></textarea>
</div>
<!-- Hidden fields to carry context back -->
<input type="hidden" name="student_id" id="noteStudentId">
<input type="hidden" name="code" id="noteCodeInput">
<input type="hidden" name="incident_date" id="noteIncidentDate">
<input type="hidden" name="to" id="noteTo">
<input type="hidden" name="subject" id="noteSubject">
<input type="hidden" name="variant" id="noteVariant">
<input type="hidden" name="return_url" id="noteReturnUrl" value="<?= current_url() ?>">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Save Note
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
(function(){
// simple HTML escaper for dynamic strings
function esc(s) {
return String(s ?? '')
.replaceAll('&','&amp;').replaceAll('<','&lt;')
.replaceAll('>','&gt;').replaceAll('"','&quot;').replaceAll("'", "&#039;");
}
// Initialize Bootstrap tooltips (for "View full" note)
document.addEventListener('DOMContentLoaded', function () {
if (window.bootstrap) {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (el) { new bootstrap.Tooltip(el); });
}
});
function parentCard(p, label) {
if (!p) return '';
const name = esc(p.name || '');
const email = esc(p.email || '');
const phone = esc(p.phone || '');
return `
<div class="col-12 col-md-6">
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>${esc(label)} Parent</strong>
${email ? `<a class="btn btn-sm btn-outline-primary" href="mailto:${email}">
<i class="fas fa-envelope"></i> Email</a>` : ''}
</div>
<div class="card-body">
<div class="mb-1"><span class="text-muted">Name:</span> ${name || '—'}</div>
<div class="mb-1"><span class="text-muted">Email:</span> ${email || '—'}</div>
<div class="mb-1"><span class="text-muted">Phone:</span> ${phone || '—'}</div>
</div>
</div>
</div>`;
}
function renderParentsModal(data) {
const body = document.getElementById('parentModalBody');
if (!data || (!data.primary && !data.secondary)) {
body.innerHTML = `<div class="alert alert-warning mb-0">No parent information available for this student.</div>`;
return;
}
const primary = data.primary ? {
name: (data.primary.firstname ? `${data.primary.firstname} ${data.primary.lastname||''}`.trim() : (data.primary.name || '')),
email: data.primary.email || '',
phone: data.primary.cellphone || data.primary.phone || ''
} : null;
const secondary = data.secondary ? {
name: (data.secondary.firstname ? `${data.secondary.firstname} ${data.secondary.lastname||''}`.trim() : (data.secondary.name || '')),
email: data.secondary.email || '',
phone: data.secondary.cellphone || data.secondary.phone || ''
} : null;
body.innerHTML = `<div class="row">
${primary ? parentCard(primary, 'First') : ''}
${secondary ? parentCard(secondary, 'Second') : ''}
</div>`;
}
// Wire up modal open/fetch
document.addEventListener('click', function(e) {
const parentBtn = e.target.closest('.show-parents');
if (!parentBtn) return;
e.preventDefault();
const modalEl = document.getElementById('parentModal');
const bodyEl = document.getElementById('parentModalBody');
const sid = parentBtn.dataset.studentId || '';
if (!sid) {
if (bodyEl) bodyEl.innerHTML = '<div class="alert alert-warning mb-0">Missing student id for this row.</div>';
if (window.bootstrap) new bootstrap.Modal(modalEl).show();
return;
}
bodyEl.innerHTML = `
<div class="text-center text-muted my-4">
<div class="spinner-border" role="status" aria-hidden="true"></div>
<div class="mt-2">Loading parent info…</div>
</div>`;
if (window.bootstrap) new bootstrap.Modal(modalEl).show();
let url = parentBtn.dataset.fetchUrl;
if (!url || !url.includes('student_id=')) {
url = '<?= site_url('attendance/parents-info') ?>?student_id=' + encodeURIComponent(sid);
}
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(async (r) => {
let data = null;
try { data = await r.json(); } catch (e) {}
if (!r.ok) throw new Error((data && data.error) ? data.error : ('HTTP ' + r.status));
return data;
})
.then((data) => renderParentsModal(data))
.catch((err) => {
bodyEl.innerHTML = '<div class="alert alert-danger mb-0">Unable to load parent info: ' + esc(err?.message || err) + '</div>';
});
});
// Compute offset if a fixed/sticky top navbar exists to prevent overlap
function getFixedHeaderOffset() {
let total = 0;
const stack = [];
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top');
if (header) stack.push(header);
const mgmt = document.getElementById('navbarManagement');
if (mgmt && (mgmt.classList.contains('sticky-top') || mgmt.classList.contains('fixed-top'))) stack.push(mgmt);
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(el => { if (!stack.includes(el)) stack.push(el); });
stack.forEach(el => { const h = el.offsetHeight || el.getBoundingClientRect().height || 0; total += Math.max(0, Math.round(h)); });
return total;
}
// ===== DataTables init (with FixedHeader) =====
function initDataTable() {
if (!window.jQuery || !jQuery.fn || !jQuery.fn.DataTable) return false;
const $ = jQuery;
const table = $('#notifiedTable');
if (!table.length) return true;
// Column indexes after header changes:
// 0 Student, 1 Grade, 2 Last Incident, 3 Reason, 4 Note, 5 Notified On, 6 Parent, 7 Details
table.DataTable({
paging: true,
pageLength: 25,
lengthMenu: [[10,25,50,100,-1],[10,25,50,100,'All']],
ordering: true,
order: [[2, 'desc'], [0, 'asc']], // Last Incident desc then Student asc
searching: true,
info: true,
responsive: true,
autoWidth: false,
columnDefs: [
{ targets: [6, 7], orderable: false, searchable: false }, // Parent, Details
{ targets: [4], orderable: false } // Note not sortable
],
fixedHeader: {
header: true,
headerOffset: getFixedHeaderOffset()
},
dom: "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: {
search: "Quick search:",
lengthMenu: "Show _MENU_ rows",
info: "Showing _START__END_ of _TOTAL_",
infoEmpty: "No rows to show",
zeroRecords: "No matching records",
paginate: { previous: "", next: "" }
}
});
return true;
}
function ensureDataTablesThenInit() {
const head = document.head;
function loadScript(src, id) {
return new Promise((resolve, reject) => {
if (id && document.getElementById(id)) return resolve();
const s = document.createElement('script');
if (id) s.id = id;
s.src = src;
s.onload = resolve;
s.onerror = reject;
head.appendChild(s);
});
}
function loadCss(href, id) {
return new Promise((resolve) => {
if (id && document.getElementById(id)) return resolve();
const l = document.createElement('link');
if (id) l.id = id;
l.rel = 'stylesheet';
l.href = href;
l.onload = resolve;
head.appendChild(l);
});
}
const hasDT = !!(window.jQuery && jQuery.fn && jQuery.fn.DataTable);
const hasFH = !!(hasDT && jQuery.fn.dataTable && jQuery.fn.dataTable.FixedHeader);
if (hasDT && hasFH) {
initDataTable();
return;
}
const tasks = [];
if (!hasDT) {
if (!window.jQuery) tasks.push(loadScript('https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js', 'jq-core'));
tasks.push(loadScript('https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js', 'dt-core'));
tasks.push(loadScript('https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js', 'dt-bs5'));
tasks.push(loadCss('https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css', 'dt-bs5-css'));
}
if (!hasFH) {
tasks.push(loadScript('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader@3.4.0/js/dataTables.fixedHeader.min.js', 'dt-fixedheader'));
tasks.push(loadCss('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader-bs5@3.4.0/css/fixedHeader.bootstrap5.min.css', 'dt-fixedheader-css'));
}
Promise.all(tasks)
.then(() => setTimeout(initDataTable, 0))
.catch(() => console.warn('DataTables assets failed to load; table will render without enhancements.'));
}
document.addEventListener('DOMContentLoaded', ensureDataTablesThenInit);
})();
// ---------- Add/Edit Note Modal ----------
document.addEventListener('click', function(e) {
const btn = e.target.closest('.add-note-btn');
if (!btn) return;
const modalEl = document.getElementById('notifyCommentModal');
if (!modalEl) return;
// Populate fields
document.getElementById('noteStudentId').value = btn.dataset.studentId || '';
document.getElementById('noteStudentName').textContent = btn.dataset.studentName || '—';
document.getElementById('noteCode').textContent = btn.dataset.code || '—';
document.getElementById('noteCodeInput').value = btn.dataset.code || '';
document.getElementById('noteIncidentDate').value= btn.dataset.incidentDate || '';
document.getElementById('noteDate').textContent = btn.dataset.incidentDate || '—';
document.getElementById('noteReturnUrl').value = btn.dataset.returnUrl || '<?= current_url() ?>';
document.getElementById('note').value = btn.dataset.note || '';
// Optional: clear extra meta
document.getElementById('noteTo').value = '';
document.getElementById('noteSubject').value = '';
document.getElementById('noteVariant').value = '';
if (window.bootstrap) {
new bootstrap.Modal(modalEl).show();
} else {
modalEl.classList.add('show');
}
});
(function autoOpenCommentModal(){
<?php $cm = session('open_comment_modal'); ?>
<?php if ($cm): ?>
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'd-none add-note-btn';
btn.dataset.studentId = '<?= (int)($cm['student_id'] ?? 0) ?>';
btn.dataset.studentName = '<?= esc($cm['student_name'] ?? '', 'js') ?>';
btn.dataset.code = '<?= esc($cm['code'] ?? '', 'js') ?>';
btn.dataset.incidentDate = '<?= esc(substr((string)($cm['incident_date'] ?? ''), 0, 10), 'js') ?>';
btn.dataset.note = '';
btn.dataset.returnUrl = '<?= current_url() ?>';
document.body.appendChild(btn);
btn.click();
document.body.removeChild(btn);
<?php session()->remove('open_comment_modal'); ?>
<?php endif; ?>
})();
</script>
<!-- Shared Parent Modal -->
<div class="modal fade" id="parentModal" tabindex="-1" aria-labelledby="parentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="parentModalLabel">Parent Information</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="parentModalBody">
<div class="text-center text-muted my-4">
<div class="spinner-border" role="status" aria-hidden="true"></div>
<div class="mt-2">Loading parent info…</div>
</div>
</div>
</div>
<div class="modal-footer justify-content-between">
<div class="small text-muted">First &amp; Second contacts are shown if available.</div>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>