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

580 lines
26 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 ?? 'Pending Attendance Violations';
$school_year = $school_year ?? '';
$semester = $semester ?? '';
$show_actions = true; // pending page shows actions
?>
<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">Pending 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/notified') ?>" class="btn btn-outline-dark btn-sm">
<i class="fas fa-check-circle"></i> View Notified
</a>
</div>
</div>
<div class="card-body">
<div id="autoEmailAlert" class="alert d-none mb-3" role="alert"></div>
<!-- Legend -->
<div class="d-flex flex-wrap gap-3 mb-3 small">
<span><span class="badge rounded-pill" style="background-color:#e6f7ff;color:#000;">&nbsp;&nbsp;</span> 1 Absence — Auto Email</span>
<span><span class="badge rounded-pill" style="background-color:#fadb14;color:#000;">&nbsp;&nbsp;</span> 2 Absences (row/≤3 weeks) — Team Notify</span>
<span><span class="badge rounded-pill" style="background-color:#fa8c16;">&nbsp;&nbsp;</span> 3 Absences (row/≤4 weeks) — Team Notify</span>
<span><span class="badge rounded-pill" style="background-color:#ff4d4f;">&nbsp;&nbsp;</span> 4 Absences (row/≤5 weeks) — Expel Notify / 4 Lates — Team Notify</span>
<span><span class="badge rounded-pill" style="background-color:#bae7ff;color:#000;">&nbsp;&nbsp;</span> 2 Lates (row/≤3 weeks) — Auto Email</span>
<span><span class="badge rounded-pill" style="background-color:#002766;">&nbsp;&nbsp;</span> 3 Lates or 2L+1A (≤4 weeks) — Team Notify</span>
</div>
<?php if (empty($students)): ?>
<div class="alert alert-info mb-2">No attendance violations found.</div>
<?php if (!empty($debug ?? null)): ?>
<div class="alert alert-secondary small">
<strong>Debug</strong>
<div>School Year: <?= esc($debug['school_year_param'] ?? '') ?> | Semester: <?= esc($debug['semester_param'] ?? '') ?></div>
<div>Class students: <?= esc($debug['class_students'] ?? 0) ?> | Student IDs: <?= esc($debug['student_ids'] ?? 0) ?></div>
<div>Attendance rows (raw/filtered): <?= esc($debug['attendance_rows'] ?? 0) ?>/<?= esc($debug['filtered_rows'] ?? 0) ?></div>
<div>Violations computed: <?= esc($debug['violations'] ?? 0) ?></div>
<div>Active weeks: <?= esc($debug['active_weeks'] ?? 0) ?> | Students with attendance: <?= esc($debug['by_student'] ?? 0) ?></div>
<div>Absences last 5 weeks: <?= esc($debug['abs_last5'] ?? 0) ?> | Lates last 5 weeks: <?= esc($debug['late_last5'] ?? 0) ?></div>
</div>
<?php endif; ?>
<?php else: ?>
<div class="table-responsive">
<table id="violationsTable" class="table table-striped table-hover align-middle w-100">
<thead class="table-dark">
<tr>
<th>Student</th>
<th>Grade</th>
<th>Violation</th>
<th>Type</th>
<th class="text-center">Abs</th>
<th class="text-center">Late</th>
<th>Last Incident</th>
<th>Action</th>
<th>Notified?</th>
<th>#</th>
<th>Parent</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $st): ?>
<?php
// Robust ID + name (supports multiple shapes)
$studId = (int)($st['id'] ?? $st['student_id'] ?? 0);
$studName =
($st['name'] ?? null)
?? ($st['student_name'] ?? null)
?? trim(($st['firstname'] ?? $st['first_name'] ?? '') . ' ' . ($st['lastname'] ?? $st['last_name'] ?? ''));
if ($studName === '' || $studName === null) {
$studName = $studId ? ('Student #' . $studId) : '—';
}
$grade = $st['class_name'] ?? '';
// Violation data (always present in pending view)
$viTitle = (string)($st['violation'] ?? '');
$viCode = (string)($st['violation_code'] ?? '');
$viType = (string)($st['type'] ?? '');
$viColor = (string)($st['color'] ?? '#6c757d');
$action = (string)($st['action'] ?? '');
$absences = (array)($st['absences'] ?? []);
$lates = (array)($st['lates'] ?? []);
$cntAbs = count($absences);
$cntLate = count($lates);
$lastDate = $st['last_date'] ?? null;
$incidentYmd = $lastDate ? substr((string)$lastDate, 0, 10) : '';
$incidentParam = $incidentYmd !== ''
? '&incident_date=' . urlencode($incidentYmd)
: '';
// Notified meta (may be 0/empty on pending view)
$notified = !empty($st['is_notified']);
$notifCnt = (int)($st['notif_counter'] ?? 0);
// Parent display info (optional)
$parentEmail = $st['parent_email'] ?? '';
$parentName = $st['parent_name'] ?? '';
$parentLabel = $parentName ?: ($parentEmail ?: 'View');
// Badge contrast
$isLight = in_array(strtolower($viColor), ['#bae7ff', '#e6f7ff', '#fadb14'], true);
$badgeCls = 'badge rounded-pill ' . ($isLight ? 'text-dark' : 'text-light');
$badgeCss = 'background-color:' . esc($viColor) . ';';
$subjectType = ($viType === 'late') ? 'Late' : (($viType === 'absence') ? 'Absent' : 'Attendance');
?>
<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($grade) ?></td>
<td><span class="<?= esc($badgeCls) ?>" style="<?= $badgeCss ?>"><?= esc($viTitle) ?></span></td>
<td class="text-capitalize"><?= esc($viType ?: '—') ?></td>
<td class="text-center"><?= esc($cntAbs) ?></td>
<td class="text-center"><?= esc($cntLate) ?></td>
<td data-order="<?= $incidentYmd !== '' ? esc($incidentYmd) : '' ?>">
<?= $incidentYmd !== '' ? esc(date('m-d-Y', strtotime($incidentYmd))) : '—' ?>
</td>
<td>
<?php if ($action === 'auto_email'): ?>
<span class="badge bg-primary">Auto Email</span>
<?php elseif ($action === 'expel_notify'): ?>
<span class="badge bg-danger">Expel Notify</span>
<?php else: ?>
<span class="badge bg-warning text-dark">Team Notify</span>
<?php endif; ?>
</td>
<td>
<span class="badge <?= $notified ? 'bg-success' : 'bg-warning text-dark' ?>">
<?= $notified ? 'Yes' : 'No' ?>
</span>
</td>
<td><?= esc($notifCnt) ?></td>
<!-- Parent column -->
<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($parentLabel) ?>
</a>
<?php if (!empty($parentEmail)): ?>
<a class="ms-2" href="mailto:<?= esc($parentEmail) ?>" title="Email First parent">
<i class="fas fa-envelope"></i>
</a>
<?php endif; ?>
</td>
<td class="text-nowrap">
<a href="<?= site_url('attendance/view/' . $studId) . '?code=' . urlencode($viCode) . $incidentParam ?>" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i> Details
</a>
<?php if ($action === 'auto_email'): ?>
<a href="<?= site_url(
'attendance/compose?student_id=' . $studId .
'&code=' . urlencode($viCode) .
'&variant=default' .
$incidentParam
) ?>" class="btn btn-sm btn-primary ms-1">
<i class="fas fa-paper-plane"></i> Compose Auto Email
</a>
<?php else: ?>
<div class="btn-group ms-1">
<a href="<?= site_url(
'attendance/compose?student_id=' . $studId .
'&code=' . urlencode($viCode) .
'&variant=answered' .
$incidentParam
) ?>" class="btn btn-sm btn-success">
<i class="fas fa-phone-alt"></i> Compose (Answered)
</a>
<a href="<?= site_url(
'attendance/compose?student_id=' . $studId .
'&code=' . urlencode($viCode) .
'&variant=no_answer' .
$incidentParam
) ?>" class="btn btn-sm btn-warning">
<i class="fas fa-voicemail"></i> Compose (No Answer)
</a>
</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Hidden POST for notify (kept for batch CSRF or future single-row actions) -->
<form id="notifyForm" action="<?= site_url('attendance/record') ?>" method="post" class="d-none">
<?= csrf_field() ?>
<input type="hidden" name="student_id">
<input type="hidden" name="date">
<input type="hidden" name="subject_type">
<input type="hidden" name="parent_email">
<input type="hidden" name="parent_name">
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<input type="hidden" name="school_year" value="<?= esc($school_year) ?>">
<input type="hidden" name="return_url" value="<?= esc(current_url()) ?>">
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
(function() {
// Basic HTML escape util
function esc(s) {
return String(s ?? '')
.replaceAll('&', '&amp;').replaceAll('<', '&lt;')
.replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", "&#039;");
}
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>`;
}
// Parent modal open/fetch
document.addEventListener('click', function(e) {
const parentBtn = e.target.closest('.show-parents');
if (!parentBtn) return;
e.preventDefault();
const sid = parentBtn.dataset.studentId || '';
const modalEl = document.getElementById('parentModal');
const bodyEl = document.getElementById('parentModalBody');
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>';
});
});
// ===== Helpers for FixedHeader =====
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 = $('#violationsTable');
if (!table.length) return true;
// Column indexes:
// 0 Student, 1 Grade, 2 Violation, 3 Type, 4 Abs, 5 Late, 6 Last Incident,
// 7 Action, 8 Notified?, 9 #, 10 Parent, 11 Actions
table.DataTable({
paging: true,
pageLength: 25,
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, 'All']
],
ordering: true,
order: [
[6, 'desc'],
[0, 'asc']
], // Last Incident desc, then Student asc
searching: true,
info: true,
responsive: true,
autoWidth: false,
columnDefs: [{
targets: [4, 5, 9],
className: 'text-center'
},
{
targets: [7, 8, 9, 10, 11],
orderable: false
}, // actions / meta not meaningful to sort
{
targets: [7, 8, 9, 10, 11],
searchable: false
} // and not useful for search
],
// Keep the Bootstrap look & feel
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 students found",
paginate: {
previous: "",
next: ""
}
},
fixedHeader: {
header: true,
headerOffset: getFixedHeaderOffset()
}
});
return true;
}
// Ensure DT core + FixedHeader are present; then init
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) {
// Core + Bootstrap CSS
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);
})();
</script>
<!-- Parent Info 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>
<?php $cm = session('open_comment_modal'); ?>
<?php if ($cm): ?>
<!-- Add 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>
<?php
$cmDateFmt = substr((string)($cm['incident_date'] ?? ''), 0, 10);
$cmName = (string)($cm['student_name'] ?? '');
if ($cmName === '' && !empty($cm['student_id'])) {
$cmName = 'Student #' . (int) $cm['student_id'];
}
?>
<div class="modal-body">
<div class="mb-2 small text-muted">
<div><strong>Student Name:</strong> <?= esc($cmName) ?></div>
<div><strong>Code:</strong> <?= esc($cm['code']) ?> &nbsp; <strong>Date:</strong> <?= esc($cmDateFmt) ?></div>
<?php if (!empty($cm['to'])): ?>
<div><strong>To:</strong> <?= esc($cm['to']) ?></div>
<?php endif; ?>
<?php if (!empty($cm['subject'])): ?>
<div><strong>Subject:</strong> <?= esc($cm['subject']) ?></div>
<?php endif; ?>
</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" value="<?= esc($cm['student_id']) ?>">
<input type="hidden" name="code" value="<?= esc($cm['code']) ?>">
<input type="hidden" name="incident_date" value="<?= esc($cmDateFmt) ?>">
<input type="hidden" name="to" value="<?= esc($cm['to'] ?? '') ?>">
<input type="hidden" name="subject" value="<?= esc($cm['subject'] ?? '') ?>">
<input type="hidden" name="variant" value="<?= esc($cm['variant'] ?? '') ?>">
<input type="hidden" name="return_url" value="<?= current_url() ?>">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Save Note
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var modalEl = document.getElementById('notifyCommentModal');
if (modalEl && window.bootstrap) {
new bootstrap.Modal(modalEl).show();
}
});
</script>
<?php endif; ?>
<?= $this->endSection() ?>