recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
@@ -0,0 +1,544 @@
<?= $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() ?>