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,154 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php $dateLabel = !empty($date) ? local_date($date, 'm-d-Y') : ''; ?>
<div class="container-xxl py-4">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h2 class="mb-0">Admin Attendance</h2>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary"
href="<?= site_url('admin/teacher-attendance/month?date='.urlencode($date).'&semester='.urlencode($semester).'&school_year='.urlencode($schoolYear)) ?>">
← Back to Monthly Attendance
</a>
</div>
</div>
<?php if (session()->getFlashdata('status')): ?>
<div class="alert alert-success"><?= session()->getFlashdata('status') ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
<?php endif; ?>
<!-- Filters -->
<form class="row gy-2 gx-3 align-items-end mb-3" method="get" action="<?= site_url('admin/admins-attendance') ?>">
<div class="col-sm-3">
<label class="form-label">School Year</label>
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="e.g. 2025-2026">
</div>
<div class="col-sm-2">
<label class="form-label">Semester</label>
<input type="text" name="semester" class="form-control" value="<?= esc($semester) ?>" placeholder="Fall">
</div>
<div class="col-sm-3">
<label class="form-label">Date</label>
<input type="date" name="date" class="form-control" value="<?= esc($date) ?>">
</div>
<div class="col-sm-4 d-flex gap-2">
<button type="submit" class="btn btn-secondary">Load</button>
<a class="btn btn-outline-secondary" href="<?= site_url('admin/admins-attendance') ?>">Reset</a>
</div>
</form>
<!-- Admins grid -->
<form method="post" action="<?= site_url('admin/admins-attendance/save') ?>" id="adminsAttForm">
<?= csrf_field() ?>
<input type="hidden" name="date" value="<?= esc($date) ?>">
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Admins on <?= esc($dateLabel) ?></strong>
<span class="badge bg-secondary ms-2"><?= is_array($adminsGrid) ? count($adminsGrid) : 0 ?></span>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-success" id="markAllPresentAdmins">Mark All Present</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearAllAdmins">Clear All</button>
</div>
</div>
<div class="card-body table-responsive">
<?php if (empty($adminsGrid)): ?>
<div class="alert alert-info mb-0">No Admin users found for this day/term.</div>
<?php else: ?>
<table id="adminsAttTable" class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th class="text-center" style="width:70px;">#</th>
<th>Name</th>
<th class="text-center" style="width:180px;">Role</th>
<th class="text-center" style="width:220px;">Status</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
<?php $i=1; foreach ($adminsGrid as $row): ?>
<?php
$uid = (int)($row['user_id'] ?? 0);
$name = trim(($row['firstname'] ?? '').' '.($row['lastname'] ?? '')) ?: ('User#'.$uid);
$role = (string)($row['role_name'] ?? 'Admin');
$stat = strtolower((string)($row['status'] ?? ''));
$reason = (string)($row['reason'] ?? '');
?>
<tr>
<td class="text-center"><?= $i++ ?></td>
<td><?= esc($name) ?></td>
<td class="text-center"><?= esc($role) ?></td>
<td class="text-center">
<input type="hidden" name="admins[<?= $uid ?>][user_id]" value="<?= $uid ?>">
<input type="hidden" name="admins[<?= $uid ?>][role_name]" value="<?= esc($role) ?>">
<select name="admins[<?= $uid ?>][status]" class="form-select form-select-sm admin-status-select">
<option value="">—</option>
<option value="present" <?= $stat === 'present' ? 'selected' : '' ?>>Present</option>
<option value="absent" <?= $stat === 'absent' ? 'selected' : '' ?>>Absent</option>
<option value="late" <?= $stat === 'late' ? 'selected' : '' ?>>Late</option>
</select>
</td>
<td>
<input type="text" name="admins[<?= $uid ?>][reason]" class="form-control form-control-sm" placeholder="Optional reason" value="<?= esc($reason) ?>">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="card-footer d-flex justify-content-end">
<button type="submit" class="btn btn-primary" <?= empty($adminsGrid) ? 'disabled' : '' ?>>Save Admins Attendance</button>
</div>
</div>
</form>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
const markAll = document.getElementById('markAllPresentAdmins');
const clearAll = document.getElementById('clearAllAdmins');
if (markAll) {
markAll.addEventListener('click', () => {
document.querySelectorAll('#adminsAttTable .admin-status-select')
.forEach(sel => { sel.value = 'present'; });
});
}
if (clearAll) {
clearAll.addEventListener('click', () => {
document.querySelectorAll('#adminsAttTable .admin-status-select')
.forEach(sel => { sel.value = ''; });
document.querySelectorAll('#adminsAttTable input[name^="admins"][name$="[reason]"]')
.forEach(inp => { inp.value = ''; });
});
}
});
</script>
<script>
$(function(){
if ($('#adminsAttTable').length) {
$('#adminsAttTable').DataTable({
paging: false,
searching: false,
info: false,
order: []
});
}
});
</script>
<?= $this->endSection() ?>
@@ -0,0 +1,139 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container">
<h2 class="text-center mt-4 mb-3"><?= $title ?></h1>
<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 (session()->getFlashdata('message')): ?>
<div class="alert alert-success">
<?= session()->getFlashdata('message') ?>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger">
<?= session()->getFlashdata('error') ?>
</div>
<?php endif; ?>
<div class="card mb-4">
<div class="card-header">
<h5>Record Attendance</h5>
</div>
<div class="card-body">
<form action="<?= base_url('attendance/record') ?>" method="post">
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="student_id">Student</label>
<select class="form-control" id="student_id" name="student_id" required>
<option value="">Select Student</option>
<?php foreach ($students as $student): ?>
<option value="<?= $student['id'] ?>"><?= $student['name'] ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="date">Date</label>
<input type="date" class="form-control" id="date" name="date" required value="<?= local_date(utc_now(), 'Y-m-d') ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="is_present">Status</label>
<select class="form-control" id="is_present" name="is_present" required>
<option value="1">Present</option>
<option value="0">Absent</option>
</select>
</div>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary">Record</button>
</div>
</div>
<div class="row mt-3" id="reasonRow" style="display: none;">
<div class="col-md-6">
<div class="form-group">
<label for="reason">Reason for Absence</label>
<textarea class="form-control" id="reason" name="reason" rows="2"></textarea>
</div>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_reported" name="is_reported" value="1">
<label class="form-check-label" for="is_reported">
Reported Absence
</label>
</div>
<div class="form-check ml-3">
<input class="form-check-input" type="checkbox" id="notified" name="notified" value="1">
<label class="form-check-label" for="notified">
Parent Notified
</label>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
<h5>Student List</h5>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Student Name</th>
<th>Total Absences</th>
<th>Last Absence Date</th>
<th>Last Absence Reason</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<tr>
<td><?= $student['name'] ?></td>
<td><?= $student['total_absences'] ?></td>
<td>
<?= $student['last_absence'] ? local_date($student['last_absence']['date'], 'm-d-Y') : 'N/A' ?>
</td>
<td>
<?= $student['last_absence']['reason'] ?? 'N/A' ?>
</td>
<td>
<a href="<?= base_url('attendance/view/' . $student['id']) ?>" class="btn btn-sm btn-info">
View Details
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
// Show reason field when absent is selected
document.getElementById('is_present').addEventListener('change', function() {
const reasonRow = document.getElementById('reasonRow');
if (this.value === '0') {
reasonRow.style.display = 'flex';
} else {
reasonRow.style.display = 'none';
}
});
</script>
<?= $this->endSection() ?>
+151
View File
@@ -0,0 +1,151 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
// Prepare initial editor content:
// Prefer $body_html if provided; else fall back to $body_text (escaped)
$initialEditorContent = (string)(
isset($body_html) && $body_html !== ''
? $body_html
: (isset($body_text) ? htmlspecialchars((string)$body_text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') : '')
);
?>
<div class="container my-4">
<h2 class="text-center mb-3">Compose Attendance Email</h2>
<?php if ($err = session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc($err) ?></div>
<?php endif; ?>
<?php if ($msg = session()->getFlashdata('message')): ?>
<div class="alert alert-success"><?= esc($msg) ?></div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong><?= esc($student_name ?? '') ?></strong>
<span class="badge bg-secondary ms-2">
<?= esc($code) ?><?= $variant ? ' • ' . esc($variant) : '' ?>
</span>
</div>
<div>
<span class="badge bg-primary">School Year: <?= esc($school_year ?? '') ?></span>
<?php if (!empty($semester)): ?>
<span class="badge bg-info text-dark ms-1">Semester: <?= esc($semester) ?></span>
<?php endif; ?>
</div>
</div>
<div class="card-body">
<form method="post" action="<?= site_url('attendance/send-email-manual') ?>">
<?= csrf_field() ?>
<input type="hidden" name="student_id" value="<?= esc($student_id) ?>">
<input type="hidden" name="code" value="<?= esc($code) ?>">
<input type="hidden" name="variant" value="<?= esc($variant) ?>">
<input type="hidden" name="incident_date" value="<?= esc($incident_date ?? local_date(utc_now(), 'Y-m-d')) ?>">
<!-- This hidden field is what your controller validates (body_html) -->
<input type="hidden" name="body_html" id="body_html" value="<?= htmlspecialchars($initialEditorContent, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
<div class="mb-3">
<label class="form-label">To</label>
<input type="email" name="to" class="form-control" value="<?= esc($parent_email) ?>" required>
<div class="form-text">Primary parent email (you can change it here).</div>
</div>
<?php
$secEmail = (string)($secondary_email ?? '');
$secName = (string)($secondary_name ?? '');
?>
<?php if ($secEmail !== ''): ?>
<div class="alert alert-light border d-flex align-items-start">
<div class="me-2"><i class="fas fa-copy"></i></div>
<div>
<div><strong>Second Parent (CC):</strong> <?= esc($secEmail) ?><?= $secName ? ' — ' . esc($secName) : '' ?></div>
</div>
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label">Subject</label>
<input type="text" name="subject" class="form-control" value="<?= esc($subject) ?>" required>
</div>
<!-- Rich Text Editor -->
<div class="mb-3">
<label class="form-label">Body (Rich Text)</label>
<textarea id="editor" class="form-control" rows="12"><?= $initialEditorContent ?></textarea>
<div class="form-text">
Use the toolbar to format your message. Links/images/tables are supported.
</div>
<noscript>
<div class="alert alert-warning mt-2">
JavaScript is disabled. The message will be sent as-is from the hidden <code>body_html</code> field.
</div>
</noscript>
</div>
<div class="d-flex gap-2">
<a href="<?= site_url('attendance/violations') ?>" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane me-1"></i> Send Email
</button>
</div>
</form>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<!-- TinyMCE self-hosted -->
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
<script>
(function () {
const form = document.querySelector('form[action$="attendance/send-email-manual"]');
const hiddenHtml = document.getElementById('body_html');
tinymce.init({
selector: '#editor',
// CRUCIAL for self-host:
base_url: '<?= base_url('assets/tinymce') ?>',
suffix: '.min',
// Optional: explicitly mark GPL mode (suppresses license nags in some builds)
license_key: 'gpl',
height: 420,
menubar: true,
branding: false,
promotion: false,
// ONLY use OSS plugins here. Remove any premium/cloud ones (e.g., powerpaste, a11ychecker, tinycomments, spellchecker, linkchecker, checklist, premium table plugins).
plugins: 'advlist autolink lists link image charmap preview anchor ' +
'searchreplace visualblocks code fullscreen insertdatetime media table ' +
'help wordcount emoticons codesample',
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | ' +
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | ' +
'link image media table | emoticons codesample | removeformat | preview code',
convert_urls: false,
paste_data_images: true,
image_caption: true,
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }',
setup(editor) {
editor.on('keyup change undo redo SetContent', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
if (form) {
form.addEventListener('submit', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
}
}
});
})();
</script>
<?= $this->endSection() ?>
+150
View File
@@ -0,0 +1,150 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?= csrf_meta() ?>
<?php
use App\Libraries\AttendancePolicy;
$roleRaw = session()->get('role');
$roleList = is_array($roleRaw) ? $roleRaw : [$roleRaw];
$canManage = AttendancePolicy::canManageEarlyDismissals($roleList);
$signatureByDate = $signatureByDate ?? [];
$query = $_SERVER['QUERY_STRING'] ?? '';
$returnUrl = current_url();
if ($query && strpos($returnUrl, '?') === false) {
$returnUrl .= '?' . $query;
}
?>
<div class="container-xxl py-4">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div>
<h2 class="mb-0">Early Dismissals</h2>
<small class="text-muted">Parent-submitted early dismissal requests</small>
</div>
<?php if ($canManage): ?>
<a href="<?= site_url('attendance/early-dismissals/new') ?>" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Early Dismissal
</a>
<?php endif; ?>
</div>
<form method="get" class="row gy-2 gx-3 align-items-end mb-4">
<div class="col-md-4">
<label class="form-label">School Year</label>
<input type="text" class="form-control"
name="school_year"
value="<?= esc($schoolYear ?? ($_GET['school_year'] ?? '')) ?>"
placeholder="e.g. 2025-2026">
</div>
<div class="col-md-3">
<label class="form-label">Semester</label>
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
<select name="semester" class="form-select">
<option value="">All Semesters</option>
<option value="Fall" <?= (strcasecmp($semVal, 'Fall') === 0 ? 'selected' : '') ?>>Fall</option>
<option value="Spring" <?= (strcasecmp($semVal, 'Spring') === 0 ? 'selected' : '') ?>>Spring</option>
</select>
</div>
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-secondary mt-4">Apply</button>
<a class="btn btn-outline-secondary mt-4" href="<?= site_url('attendance/early-dismissals') ?>">Reset</a>
</div>
</form>
<?php if ($msg = session()->getFlashdata('message')): ?>
<div class="alert alert-success"><?= $msg ?></div>
<?php endif; ?>
<?php if ($err = session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= $err ?></div>
<?php endif; ?>
<?php if (empty($groups)): ?>
<div class="alert alert-info">No early dismissals found for the selected school year<?= $semester ? ' and semester' : '' ?>.</div>
<?php else: ?>
<?php foreach ($groups as $date => $rows): ?>
<?php
$dateLabel = $date;
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $date)) {
$dateLabel = date('m-d-Y', strtotime($date));
}
?>
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<strong><?= esc($dateLabel) ?></strong>
<?php if ($date !== 'Unknown'): ?>
<?php $sig = $signatureByDate[$date] ?? null; ?>
<div class="d-flex flex-wrap align-items-center gap-2">
<?php if (!empty($sig['filename'])): ?>
<a class="btn btn-sm btn-outline-primary" href="<?= site_url('early-dismissal-signatures/' . $sig['filename']) ?>" target="_blank" rel="noopener">
View Signature
</a>
<?php if (!empty($sig['original_name'])): ?>
<span class="small text-muted"><?= esc($sig['original_name']) ?></span>
<?php endif; ?>
<?php else: ?>
<span class="small text-muted">No signature uploaded</span>
<?php endif; ?>
<?php if ($canManage): ?>
<form method="post"
action="<?= site_url('attendance/early-dismissals/signature') ?>"
enctype="multipart/form-data"
class="d-flex flex-wrap align-items-center gap-2">
<?= csrf_field() ?>
<input type="hidden" name="report_date" value="<?= esc($date) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<input type="hidden" name="semester" value="<?= esc($semester ?? '') ?>">
<input type="hidden" name="return_url" value="<?= esc($returnUrl) ?>">
<input type="file"
name="signature_file"
class="form-control form-control-sm"
accept=".jpg,.jpeg,.png,.webp,.gif,.pdf"
required>
<button type="submit" class="btn btn-sm btn-primary">
<?= !empty($sig['filename']) ? 'Replace' : 'Upload' ?>
</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="table-responsive">
<table class="table table-striped align-middle mb-0 no-mgmt-sticky" data-no-mgmt-sticky="1">
<thead>
<tr>
<th>Student</th>
<th>Class/Section</th>
<th>Dismissal Time</th>
<th>Reason</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= esc(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? '')) ?></td>
<td><?= esc($r['class_section_name'] ?? '—') ?></td>
<td><?= !empty($r['dismiss_time']) ? esc(substr($r['dismiss_time'], 0, 5)) : '—' ?></td>
<td style="max-width: 420px; white-space: normal;"><?= esc($r['reason'] ?? '') ?></td>
<td>
<span class="badge bg-<?= ($r['status'] === 'processed'
? 'success'
: ($r['status'] === 'seen' ? 'info' : 'secondary')) ?>">
<?= esc(ucfirst($r['status'] ?? 'new')) ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?= $this->endSection() ?>
@@ -0,0 +1,204 @@
<?= $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() ?>
+104
View File
@@ -0,0 +1,104 @@
<?php
/**
* Attendance Edit Modal
*
* @param array $student Student data
* @param int $classId Class ID
* @param int $sectionId Section ID
* @param string $sid Student ID
*/
?>
<!-- Modal -->
<div class="modal fade" id="editAttendanceModal-<?= $sid ?>" tabindex="-1"
aria-labelledby="editAttendanceModalLabel-<?= $sid ?>" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editAttendanceModalLabel-<?= $sid ?>">
Edit Attendance for <?= esc($student['firstname'] . ' ' . $student['lastname']) ?>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="<?= base_url('attendance/update') ?>" method="post">
<?= csrf_field() ?>
<input type="hidden" name="student_id" value="<?= $sid ?>">
<input type="hidden" name="class_id" value="<?= $classId ?>">
<input type="hidden" name="class_section_id" value="<?= $sectionId ?>">
<div class="mb-3">
<label for="status-<?= $sid ?>" class="form-label">Status</label>
<select name="status" class="form-select attendance-status" id="status-<?= $sid ?>" required>
<option value="present">Present</option>
<option value="late">Late</option>
<option value="absent">Absent</option>
</select>
</div>
<div class="mb-3">
<label for="date-<?= $sid ?>" class="form-label">Date</label>
<input type="date" class="form-control" id="date-<?= $sid ?>" name="date"
value="<?= local_date(utc_now(), 'Y-m-d') ?>" required>
</div>
<div class="mb-3">
<label for="reason-<?= $sid ?>" class="form-label">Reason</label>
<textarea name="reason" id="reason-<?= $sid ?>" class="form-control attendance-reason"
rows="3" placeholder="Enter reason for absence or lateness"></textarea>
</div>
<!-- Absence Reported? (shown only when Status = Absent) -->
<div class="mb-3 d-none" id="reported-group-<?= $sid ?>">
<label for="reported-<?= $sid ?>" class="form-label">Absence Reported?</label>
<select name="reported" id="reported-<?= $sid ?>" class="form-select">
<option value="0" selected>No</option>
<option value="1">Yes</option>
</select>
<div class="form-text">Select “Yes” if the parent/guardian reported this absence.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
(function () {
const statusEl = document.getElementById('status-<?= $sid ?>');
const reasonEl = document.getElementById('reason-<?= $sid ?>');
const reportedGp = document.getElementById('reported-group-<?= $sid ?>');
const reportedEl = document.getElementById('reported-<?= $sid ?>');
function syncFields() {
const v = statusEl.value;
// Reason: required for absent/late, disabled otherwise
if (v === 'absent' || v === 'late') {
reasonEl.removeAttribute('disabled');
reasonEl.setAttribute('required', 'required');
} else {
reasonEl.value = '';
reasonEl.removeAttribute('required');
reasonEl.setAttribute('disabled', 'disabled');
}
// Reported selector: visible only for absent; reset to "No" otherwise
if (v === 'absent') {
reportedGp.classList.remove('d-none');
} else {
reportedEl.value = '0';
reportedGp.classList.add('d-none');
}
}
statusEl.addEventListener('change', syncFields);
syncFields(); // init on modal load
})();
</script>
+85
View File
@@ -0,0 +1,85 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<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">Parent Attendance Reports</h2>
<small class="text-muted">Submitted by parents (absent/late/early dismissal)</small>
</div>
<form method="get" class="row gy-2 gx-3 align-items-end mb-4">
<div class="col-md-3">
<label class="form-label">School Year</label>
<input type="text" class="form-control" name="school_year" value="<?= esc($schoolYear ?? ($_GET['school_year'] ?? '')) ?>" placeholder="e.g. 2025-2026">
</div>
<div class="col-md-2">
<label class="form-label">Semester</label>
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
<select name="semester" class="form-select">
<option value="">—</option>
<option value="Fall" <?= (strcasecmp($semVal,'Fall')===0?'selected':'') ?>>Fall</option>
<option value="Spring" <?= (strcasecmp($semVal,'Spring')===0?'selected':'') ?>>Spring</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Start Date</label>
<input type="date" class="form-control" name="start" value="<?= esc($start ?? local_date(utc_now(), 'Y-m-d')) ?>">
</div>
<div class="col-md-2">
<label class="form-label">End Date</label>
<input type="date" class="form-control" name="end" value="<?= esc($end ?? '') ?>">
</div>
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-secondary mt-4">Apply</button>
<a class="btn btn-outline-secondary mt-4" href="<?= site_url('attendance/parent-reports') ?>">Reset</a>
</div>
</form>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-striped align-middle">
<thead>
<tr>
<th>Date</th>
<th>Student</th>
<th>Class/Section</th>
<th>Type</th>
<th>Time</th>
<th>Reason</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php if (!empty($rows)): ?>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= esc(!empty($r['report_date']) ? local_date($r['report_date'], 'm-d-Y') : '') ?></td>
<td><?= esc(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? '')) ?></td>
<td><?= esc($r['class_section_name'] ?? '—') ?></td>
<td class="text-capitalize"><?= esc(str_replace('_',' ', $r['type'])) ?></td>
<td>
<?php if ($r['type'] === 'late' && !empty($r['arrival_time'])): ?>
Arrival: <?= esc(substr($r['arrival_time'], 0, 5)) ?>
<?php elseif ($r['type'] === 'early_dismissal' && !empty($r['dismiss_time'])): ?>
Dismiss: <?= esc(substr($r['dismiss_time'], 0, 5)) ?>
<?php else: ?>
<?php endif; ?>
</td>
<td style="max-width: 360px; white-space: normal;"><?= esc($r['reason'] ?? '') ?></td>
<td><span class="badge bg-<?= ($r['status'] === 'processed' ? 'success' : ($r['status'] === 'seen' ? 'info' : 'secondary')) ?>
"><?= esc(ucfirst($r['status'] ?? 'new')) ?></span></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="7" class="text-center text-muted">No reports found for the selected range.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?= $this->endSection() ?>
@@ -0,0 +1,180 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php $dateLabel = !empty($date) ? local_date($date, 'm-d-Y') : ''; ?>
<div class="container-xxl py-4">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h2 class="mb-0">Teacher Attendance</h2>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary"
href="<?= site_url('admin/teacher-attendance/month?date='.urlencode($date).'&semester='.urlencode($semester).'&school_year='.urlencode($schoolYear)) ?>">
← Back to Monthly Attendance
</a>
</div>
</div>
<?php if (session()->getFlashdata('status')): ?>
<div class="alert alert-success"><?= session()->getFlashdata('status') ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
</div>
<form class="row gy-2 gx-3 align-items-end mb-3" method="get" action="<?= site_url('admin/teacher-attendance') ?>">
<div class="col-sm-3">
<label class="form-label">School Year</label>
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="e.g. 2025-2026">
</div>
<div class="col-sm-2">
<label class="form-label">Semester</label>
<input type="text" name="semester" class="form-control" value="<?= esc($semester) ?>" placeholder="Fall">
</div>
<div class="col-sm-3">
<label class="form-label">Date</label>
<input type="date" name="date" class="form-control" value="<?= esc($date) ?>">
</div>
<div class="col-sm-4">
<label class="form-label">Class Section</label>
<select name="class_section_id" class="form-select">
<option value="0">— Choose Section —</option>
<?php foreach ($sections as $id => $label): ?>
<option value="<?= (int)$id ?>" <?= (int)$id === (int)$class_section_id ? 'selected' : '' ?>>
<?= esc($label) ?> (ID: <?= (int)$id ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 d-flex gap-2">
<button type="submit" class="btn btn-secondary">Load</button>
<?php if ($class_section_id): ?>
<a class="btn btn-outline-secondary" href="<?= site_url('admin/teacher-attendance') ?>">Reset</a>
<?php endif; ?>
</div>
</form>
<?php if ($class_section_id): ?>
<?php if ($locked): ?>
<div class="alert alert-warning">
<strong>Note:</strong> Student/section attendance for this day appears to be <em>finalized</em>.
Admin edits to teacher attendance here are allowed and will not unlock the section.
</div>
<?php endif; ?>
<form method="post" action="<?= site_url('admin/teacher-attendance/save') ?>" id="adminTeacherAttForm">
<?= csrf_field() ?>
<input type="hidden" name="class_section_id" value="<?= (int)$class_section_id ?>">
<input type="hidden" name="date" value="<?= esc($date) ?>">
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Teachers on <?= esc($dateLabel) ?></strong>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-success" id="markAllPresent">Mark All Present</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearAll">Clear All</button>
</div>
</div>
<div class="card-body table-responsive">
<table id="teacherAttTable" class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th class="text-center" style="width:70px;">#</th>
<th>Name</th>
<th class="text-center" style="width:160px;">Role</th>
<th class="text-center" style="width:200px;">Status</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
<?php if (!empty($grid)): ?>
<?php $i = 1; foreach ($grid as $row): ?>
<?php
$tid = (int)$row['teacher_id'];
$name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')) ?: ('User#' . $tid);
$pos = (string)$row['position'];
$stat = strtolower((string)($row['status'] ?? ''));
$reason= (string)($row['reason'] ?? '');
?>
<tr>
<td class="text-center"><?= $i++ ?></td>
<td><?= esc($name) ?></td>
<td class="text-center"><?= esc($pos === 'main' ? 'Main Teacher' : 'Assistant') ?></td>
<td class="text-center">
<input type="hidden" name="teachers[<?= $tid ?>][teacher_id]" value="<?= $tid ?>">
<select name="teachers[<?= $tid ?>][status]" class="form-select form-select-sm status-select">
<option value="">—</option>
<option value="present" <?= $stat === 'present' ? 'selected' : '' ?>>Present</option>
<option value="absent" <?= $stat === 'absent' ? 'selected' : '' ?>>Absent</option>
<option value="late" <?= $stat === 'late' ? 'selected' : '' ?>>Late</option>
</select>
</td>
<td>
<input type="text"
name="teachers[<?= $tid ?>][reason]"
class="form-control form-control-sm"
placeholder="Optional reason"
value="<?= esc($reason) ?>">
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="5" class="text-center">No teachers found for this section/term.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="card-footer d-flex justify-content-end">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</div>
</form>
<?php endif; ?>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', function(){
const markAllBtn = document.getElementById('markAllPresent');
const clearBtn = document.getElementById('clearAll');
if (markAllBtn) {
markAllBtn.addEventListener('click', () => {
document.querySelectorAll('#teacherAttTable .status-select').forEach(sel => {
sel.value = 'present';
});
});
}
if (clearBtn) {
clearBtn.addEventListener('click', () => {
document.querySelectorAll('#teacherAttTable .status-select').forEach(sel => {
sel.value = '';
});
document.querySelectorAll('#teacherAttTable input[name$="[reason]"]').forEach(inp => {
inp.value = '';
});
});
}
});
</script>
<!-- DataTables (optional) -->
<script>
$(function(){
$('#teacherAttTable').DataTable({
paging: false, // usually small set; disable paging for editing
searching: false,
info: false,
order: []
});
});
</script>
<?= $this->endSection() ?>
@@ -0,0 +1,900 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?= csrf_meta() ?>
<?php
$queryParams = [
'semester' => $semester ?? '',
'school_year' => $schoolYear ?? '',
];
$queryString = http_build_query(array_filter($queryParams, static function ($value) {
return $value !== '';
}));
?>
<div
class="container-fluid py-4"
data-endpoint="<?= esc($endpoint ?? '') ?>"
data-save-endpoint="<?= esc($saveEndpoint ?? '') ?>"
data-csrf-name="<?= esc(csrf_token()) ?>"
data-csrf-value="<?= esc(csrf_hash()) ?>"
data-semester="<?= esc($semester ?? '') ?>"
data-school-year="<?= esc($schoolYear ?? '') ?>"
data-is-current="<?= isset($isCurrentYear) && $isCurrentYear ? '1' : '0' ?>"
>
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<h2 class="mb-0">Admin &amp; Teacher — Monthly Attendance</h2>
<div class="legend">
<span class="badge bg-success">P</span><span class="ms-1">Present</span>
<span class="badge bg-danger ms-3">A</span><span class="ms-1">Absent</span>
<span class="badge bg-warning text-dark ms-3">L</span><span class="ms-1">Late</span>
<span class="badge bg-info text-dark ms-3">NS</span><span class="ms-1">No School</span>
<span class="ms-3 cell-empty">—</span><span class="ms-1">No entry</span>
</div>
</div>
<?php if (!empty($missingYear)): ?>
<div class="alert alert-warning" role="alert">
Current school year is not configured. This page is read-only until configured in
<a href="<?= site_url('configuration/configuration_view') ?>" class="alert-link">Add/Edit Configuration</a> (key: <code>school_year</code>).
</div>
<?php endif; ?>
<form class="row gy-2 gx-3 align-items-end justify-content-center mb-4" method="get" action="<?= site_url('admin/teacher-attendance/month') ?>">
<div class="col-md-3">
<label class="form-label">School Year</label>
<select name="school_year" class="form-select">
<?php
$years = isset($schoolYears) && is_array($schoolYears) ? $schoolYears : [];
if (empty($years) && !empty($schoolYear)) $years = [$schoolYear];
foreach ($years as $y): $val = is_array($y) && isset($y['school_year']) ? $y['school_year'] : (string)$y; ?>
<option value="<?= esc($val) ?>" <?= ((string)($schoolYear ?? '') === (string)$val) ? 'selected' : '' ?>>
<?= esc($val) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Semester</label>
<select name="semester" class="form-select">
<?php $semVal = (string)($semester ?? ''); ?>
<?php foreach (['---','Fall','Spring'] as $sem): ?>
<option value="<?= esc($sem) ?>" <?= ($semVal === $sem ? 'selected' : '') ?>><?= esc($sem) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4 d-flex gap-2">
<button type="submit" class="btn btn-secondary">Load</button>
<a class="btn btn-outline-secondary" href="<?= site_url('admin/teacher-attendance/month') ?>">Reset</a>
<a class="btn btn-outline-primary"
href="<?= site_url('admin/teacher-attendance/month.csv' . ($queryString ? ('?' . $queryString) : '')) ?>">
Export CSV
</a>
</div>
</form>
<style>
.att-select.form-select.form-select-sm {
min-width: 54px;
padding-left: 6px;
padding-right: 18px;
line-height: 1.1;
}
.att-none {
background: transparent;
color: #6c757d;
}
.att-present {
background: #19875422;
}
.att-absent {
background: #dc354522;
}
.att-late {
background: #ffc10755;
}
.month-grid-wrap {
overflow-x: auto;
}
.month-grid thead th {
position: sticky;
top: 0;
z-index: 2;
background: #f8f9fa;
}
.month-grid .sticky-col {
position: sticky;
left: 0;
z-index: 3;
background: #fff;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.month-grid .sticky-col-2 {
position: sticky;
left: 180px;
z-index: 3;
background: #fff;
}
.month-grid .sticky-col-3 {
position: sticky;
left: 460px; /* 180 (col1) + 280 (col2) */
z-index: 3;
background: #fff;
}
.badge-cell {
display: inline-block;
min-width: 18px;
text-align: center;
border-radius: 4px;
padding: 2px 0;
font-weight: 600;
}
.cell-empty {
color: #999;
}
.legend .badge {
min-width: 28px;
}
.month-grid .badge-cell {
min-width: 32px;
padding: 4px 0;
border-radius: 6px;
font-weight: 700;
border: 1px solid transparent;
}
.month-grid .badge-cell.bg-success {
background-color: #157347 !important;
border-color: #0f5132;
color: #fff !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .25) inset, 0 0 0 3px rgba(21, 115, 71, .18);
}
.month-grid .badge-cell.bg-danger {
background-color: #bb2d3b !important;
border-color: #842029;
color: #fff !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .25) inset, 0 0 0 3px rgba(187, 45, 59, .18);
}
.month-grid .badge-cell.bg-warning {
background-color: #ffca2c !important;
border-color: #997404;
color: #111 !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .18) inset, 0 0 0 3px rgba(255, 202, 44, .25);
}
.month-grid .badge-cell.bg-info {
background-color: #0dcaf0 !important; /* Bootstrap info */
border-color: #055160; /* darker info border */
color: #111 !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .18) inset, 0 0 0 3px rgba(13, 202, 240, .25);
}
.month-grid .cell-empty {
color: #adb5bd;
}
.att-select.form-select.form-select-sm {
min-width: 54px;
padding-left: 6px;
padding-right: 18px;
line-height: 1.1;
border: 1px solid #adb5bd;
transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
/* No School column/day highlight */
.no-school-day {
background-color: #cff4fc !important; /* Bootstrap info-subtle */
}
.att-select.att-present {
background: #157347;
color: #fff;
border-color: #0f5132;
box-shadow: 0 0 0 2px rgba(21, 115, 71, .18);
}
.att-select.att-absent {
background: #bb2d3b;
color: #fff;
border-color: #842029;
box-shadow: 0 0 0 2px rgba(187, 45, 59, .18);
}
.att-select.att-late {
background: #ffca2c;
color: #111;
border-color: #997404;
box-shadow: 0 0 0 2px rgba(255, 202, 44, .25);
}
.att-select.att-none {
background: #fff;
color: #212529;
border-color: #adb5bd;
}
</style>
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Teachers &amp; Assistants — Sundays in <span id="teacherMonthLabel"><?= esc($rangeLabel ?? ('School Year ' . ($schoolYear ?? ''))) ?></span></strong>
<?php if (isset($isCurrentYear) && !$isCurrentYear): ?>
<span class="badge bg-secondary ms-2">Read-only (Past Year)</span>
<?php endif; ?>
</div>
<small class="text-muted">Use the dropdowns to set attendance inline</small>
</div>
<div class="card-body month-grid-wrap">
<div id="teachersGrid" class="text-center text-muted py-4">Loading...</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Admins — Sundays in <span id="adminMonthLabel"><?= esc($rangeLabel ?? ('School Year ' . ($schoolYear ?? ''))) ?></span></strong>
<span class="text-muted ms-2">(All roles except Parent/Guest/Teacher/Teacher Assistant)</span>
</div>
</div>
<div class="card-body month-grid-wrap">
<div id="adminsGrid" class="text-center text-muted py-4">Loading...</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
// Bootstrap 5 Toast utility
(function(){
function ensureToastContainer() {
var c = document.getElementById('toast-container');
if (!c) {
c = document.createElement('div');
c.id = 'toast-container';
c.className = 'toast-container position-fixed top-0 end-0 p-3';
c.style.zIndex = '1080';
document.body.appendChild(c);
}
return c;
}
window.showToast = function(message, ok) {
var c = ensureToastContainer();
var el = document.createElement('div');
el.className = 'toast align-items-center text-bg-' + (ok === false ? 'danger' : 'success') + ' border-0';
el.setAttribute('role','alert');
el.setAttribute('aria-live','assertive');
el.setAttribute('aria-atomic','true');
el.innerHTML = '<div class="d-flex">'
+ '<div class="toast-body">' + (message || (ok === false ? 'Action failed' : 'Saved')) + '</div>'
+ '<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>'
+ '</div>';
c.appendChild(el);
try {
var t = new bootstrap.Toast(el, { delay: 2500, autohide: true });
el.addEventListener('hidden.bs.toast', function(){ el.remove(); });
t.show();
} catch (_) {
setTimeout(function(){ el.remove(); }, 2500);
}
}
})();
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var container = document.querySelector('[data-endpoint]');
if (!container) {
return;
}
var endpoint = container.getAttribute('data-endpoint') || '';
var saveUrl = container.getAttribute('data-save-endpoint') || '';
var csrfName = container.getAttribute('data-csrf-name') || '';
var csrfHash = container.getAttribute('data-csrf-value') || '';
var semesterDefault = container.getAttribute('data-semester') || '';
var schoolYearDefault = container.getAttribute('data-school-year') || '';
var isCurrentYear = (container.getAttribute('data-is-current') || '1') === '1';
var teachersGrid = document.getElementById('teachersGrid');
var adminsGrid = document.getElementById('adminsGrid');
var teacherMonthLabel = document.getElementById('teacherMonthLabel');
var adminMonthLabel = document.getElementById('adminMonthLabel');
if (!endpoint) {
if (teachersGrid) {
teachersGrid.innerHTML = '<div class="alert alert-danger mb-0">Endpoint not configured.</div>';
}
if (adminsGrid) {
adminsGrid.innerHTML = '';
}
return;
}
fetch(endpoint, {
headers: { 'Accept': 'application/json' },
credentials: 'same-origin'
})
.then(function(response) {
if (!response.ok) {
throw new Error('Request failed with status ' + response.status);
}
return response.json();
})
.then(function(payload) {
renderTeachers(payload);
renderAdmins(payload);
initializeSelects();
})
.catch(function(error) {
console.error(error);
if (teachersGrid) {
teachersGrid.innerHTML = '<div class="alert alert-danger mb-0">Unable to load teacher attendance.</div>';
}
if (adminsGrid) {
adminsGrid.innerHTML = '<div class="alert alert-danger mb-0">Unable to load admin attendance.</div>';
}
if (window.showToast) showToast('Failed to load attendance data', false);
});
function clearElement(el) {
if (!el) {
return;
}
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
function renderTeachers(payload) {
if (!teachersGrid) {
return;
}
clearElement(teachersGrid);
var filters = payload && payload.filters ? payload.filters : {};
var sections = payload && payload.sections ? payload.sections : [];
var sundays = payload && payload.sundays ? payload.sundays : [];
var noSchoolDays = payload && payload.noSchoolDays ? payload.noSchoolDays : [];
var noSchoolSet = {};
for (var i = 0; i < noSchoolDays.length; i++) { noSchoolSet[noSchoolDays[i]] = true; }
var label = filters.range_label || filters.month_label || '';
if (teacherMonthLabel && label) {
teacherMonthLabel.textContent = label;
}
if (!sections.length) {
var info = document.createElement('div');
info.className = 'alert alert-info mb-0';
info.textContent = 'No sections with assigned teachers found for this term.';
teachersGrid.appendChild(info);
return;
}
var table = document.createElement('table');
table.className = 'table table-bordered table-striped month-grid align-middle';
table.setAttribute('data-no-mgmt-sticky', '1');
var thead = document.createElement('thead');
var headerRow = document.createElement('tr');
var thSection = document.createElement('th');
thSection.className = 'sticky-col';
thSection.style.minWidth = '180px';
thSection.appendChild(document.createTextNode('Class / Section'));
headerRow.appendChild(thSection);
var thTeacher = document.createElement('th');
thTeacher.className = 'sticky-col-2';
thTeacher.style.minWidth = '280px';
thTeacher.appendChild(document.createTextNode('Teacher'));
headerRow.appendChild(thTeacher);
var thRole = document.createElement('th');
thRole.className = 'sticky-col-3 text-center';
thRole.style.minWidth = '120px';
thRole.appendChild(document.createTextNode('Role'));
headerRow.appendChild(thRole);
for (var i = 0; i < sundays.length; i++) {
var day = sundays[i];
var thDay = document.createElement('th');
thDay.className = 'text-center';
thDay.style.minWidth = '56px';
if (noSchoolSet[day]) thDay.classList.add('no-school-day');
// Show date as mm-dd-yyyy
var yyyy = day.substring(0, 4);
var mm = day.substring(5, 7);
var dd = day.substring(8, 10);
thDay.title = day; // full YYYY-MM-DD tooltip
thDay.appendChild(document.createTextNode(mm + '-' + dd));
headerRow.appendChild(thDay);
}
var totals = ['P', 'A', 'L'];
for (var j = 0; j < totals.length; j++) {
var thTotal = document.createElement('th');
thTotal.className = 'text-center';
thTotal.style.minWidth = '70px';
thTotal.appendChild(document.createTextNode(totals[j]));
headerRow.appendChild(thTotal);
}
thead.appendChild(headerRow);
table.appendChild(thead);
var tbody = document.createElement('tbody');
var hasTeachers = false;
for (var s = 0; s < sections.length; s++) {
var section = sections[s];
var teacherList = section && section.teachers ? section.teachers : [];
if (!teacherList.length) {
continue;
}
hasTeachers = true;
var rowSpan = teacherList.length;
for (var t = 0; t < teacherList.length; t++) {
var teacher = teacherList[t];
var row = document.createElement('tr');
if (t === 0) {
var tdSection = document.createElement('td');
tdSection.className = 'sticky-col';
tdSection.style.minWidth = '180px';
tdSection.rowSpan = rowSpan;
tdSection.appendChild(document.createTextNode(section.label || ('Section ' + section.section_code)));
row.appendChild(tdSection);
}
var tdTeacher = document.createElement('td');
tdTeacher.className = 'sticky-col-2';
tdTeacher.appendChild(document.createTextNode(teacher.name || '—'));
row.appendChild(tdTeacher);
var tdRole = document.createElement('td');
tdRole.className = 'sticky-col-3 text-center';
var roleText = (teacher.position || '').toLowerCase() === 'main' ? 'Main' : 'Assistant';
tdRole.appendChild(document.createTextNode(roleText));
row.appendChild(tdRole);
for (var d = 0; d < sundays.length; d++) {
var dayKey = sundays[d];
var cellData = teacher.cells && teacher.cells[dayKey] ? teacher.cells[dayKey] : null;
var status = '';
if (cellData && cellData.status) {
var st = String(cellData.status).toLowerCase();
if (st === 'present' || st === 'absent' || st === 'late') {
status = st;
}
}
var tdCell = document.createElement('td');
tdCell.className = 'text-center';
if (noSchoolSet[dayKey]) {
tdCell.classList.add('no-school-day');
var ns = document.createElement('span');
ns.className = 'badge-cell bg-info text-dark';
ns.textContent = 'NS';
tdCell.appendChild(ns);
row.appendChild(tdCell);
continue;
}
var select = createAttendanceSelect(
'teacher',
section.section_code,
teacher.user_id || teacher.teacher_id || section.section_code,
dayKey,
filters,
roleText,
status
);
tdCell.appendChild(select);
row.appendChild(tdCell);
}
var totalsRow = teacher.totals || { p: 0, a: 0, l: 0 };
var totalValues = [totalsRow.p || 0, totalsRow.a || 0, totalsRow.l || 0];
for (var tv = 0; tv < totalValues.length; tv++) {
var tdTotal = document.createElement('td');
tdTotal.className = 'text-center';
var strong = document.createElement('strong');
strong.appendChild(document.createTextNode(String(totalValues[tv])));
tdTotal.appendChild(strong);
row.appendChild(tdTotal);
}
tbody.appendChild(row);
}
}
if (!hasTeachers) {
var emptyRow = document.createElement('tr');
var emptyCell = document.createElement('td');
emptyCell.colSpan = 3 + sundays.length + 3;
emptyCell.className = 'text-center';
emptyCell.appendChild(document.createTextNode('No teachers assigned.'));
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
}
table.appendChild(tbody);
teachersGrid.appendChild(table);
}
function renderAdmins(payload) {
if (!adminsGrid) {
return;
}
clearElement(adminsGrid);
var filters = payload && payload.filters ? payload.filters : {};
var admins = payload && payload.admins ? payload.admins : [];
var sundays = payload && payload.sundays ? payload.sundays : [];
var noSchoolDays = payload && payload.noSchoolDays ? payload.noSchoolDays : [];
var noSchoolSet = {};
for (var i = 0; i < noSchoolDays.length; i++) { noSchoolSet[noSchoolDays[i]] = true; }
var label = filters.range_label || filters.month_label || '';
if (adminMonthLabel && label) {
adminMonthLabel.textContent = label;
}
if (!admins.length) {
var info = document.createElement('div');
info.className = 'alert alert-info mb-0';
info.textContent = 'No admin users found for this term.';
adminsGrid.appendChild(info);
return;
}
var table = document.createElement('table');
table.className = 'table table-bordered table-striped month-grid align-middle';
table.setAttribute('data-no-mgmt-sticky', '1');
var thead = document.createElement('thead');
var headerRow = document.createElement('tr');
var thAdmin = document.createElement('th');
thAdmin.className = 'sticky-col';
thAdmin.style.minWidth = '180px';
thAdmin.appendChild(document.createTextNode('Admin'));
headerRow.appendChild(thAdmin);
var thRole = document.createElement('th');
thRole.className = 'sticky-col-2 text-center';
thRole.style.minWidth = '240px';
thRole.appendChild(document.createTextNode('Role'));
headerRow.appendChild(thRole);
for (var i = 0; i < sundays.length; i++) {
var thDay = document.createElement('th');
thDay.className = 'text-center';
thDay.style.minWidth = '56px';
if (noSchoolSet[sundays[i]]) thDay.classList.add('no-school-day');
// Show date as mm-dd-yyyy
var sday = sundays[i];
var sdd = sday.substring(8, 10);
var smm = sday.substring(5, 7);
var syyyy = sday.substring(0, 4);
thDay.title = sday; // full YYYY-MM-DD tooltip
thDay.appendChild(document.createTextNode(smm + '-' + sdd));
headerRow.appendChild(thDay);
}
var totals = ['P', 'A', 'L'];
for (var j = 0; j < totals.length; j++) {
var thTotal = document.createElement('th');
thTotal.className = 'text-center';
thTotal.style.minWidth = '70px';
thTotal.appendChild(document.createTextNode(totals[j]));
headerRow.appendChild(thTotal);
}
thead.appendChild(headerRow);
table.appendChild(thead);
var tbody = document.createElement('tbody');
for (var a = 0; a < admins.length; a++) {
var admin = admins[a];
var row = document.createElement('tr');
var tdName = document.createElement('td');
tdName.className = 'sticky-col';
tdName.appendChild(document.createTextNode(admin.name || '—'));
row.appendChild(tdName);
var tdRole = document.createElement('td');
tdRole.className = 'sticky-col-2 text-center';
tdRole.appendChild(document.createTextNode(admin.role || '—'));
row.appendChild(tdRole);
for (var d = 0; d < sundays.length; d++) {
var dayKey = sundays[d];
var cellData = admin.cells && admin.cells[dayKey] ? admin.cells[dayKey] : null;
var status = '';
if (cellData && cellData.status) {
var st = String(cellData.status).toLowerCase();
if (st === 'present' || st === 'absent' || st === 'late') {
status = st;
}
}
var tdCell = document.createElement('td');
tdCell.className = 'text-center';
if (noSchoolSet[dayKey]) {
tdCell.classList.add('no-school-day');
var ns = document.createElement('span');
ns.className = 'badge-cell bg-info text-dark';
ns.textContent = 'NS';
tdCell.appendChild(ns);
row.appendChild(tdCell);
continue;
}
var select = createAttendanceSelect(
'admin',
null,
admin.user_id,
dayKey,
filters,
admin.role || 'Admin',
status
);
tdCell.appendChild(select);
row.appendChild(tdCell);
}
var totalsRow = admin.totals || { p: 0, a: 0, l: 0 };
var totalValues = [totalsRow.p || 0, totalsRow.a || 0, totalsRow.l || 0];
for (var tv = 0; tv < totalValues.length; tv++) {
var tdTotal = document.createElement('td');
tdTotal.className = 'text-center';
var strong = document.createElement('strong');
strong.appendChild(document.createTextNode(String(totalValues[tv])));
tdTotal.appendChild(strong);
row.appendChild(tdTotal);
}
tbody.appendChild(row);
}
table.appendChild(tbody);
adminsGrid.appendChild(table);
}
function createAttendanceSelect(type, sectionCode, userId, date, filters, roleName, currentValue) {
var select = document.createElement('select');
select.className = 'form-select form-select-sm att-select';
select.setAttribute('data-type', type);
if (sectionCode !== null && sectionCode !== undefined) {
select.setAttribute('data-section-id', sectionCode);
}
select.setAttribute('data-user-id', userId);
select.setAttribute('data-date', date);
// Derive semester for saves: if filter is whole-year (---), infer by date
var semForSave = filters.semester || semesterDefault;
if (semForSave === '---') {
semForSave = inferSemesterByDate(date, (filters.school_year || schoolYearDefault));
}
select.setAttribute('data-semester', semForSave);
select.setAttribute('data-school-year', filters.school_year || schoolYearDefault);
if (roleName) {
select.setAttribute('data-role-name', roleName);
}
var statuses = ['', 'present', 'absent', 'late'];
var labels = ['—', 'P', 'A', 'L'];
for (var i = 0; i < statuses.length; i++) {
var option = document.createElement('option');
option.value = statuses[i];
option.appendChild(document.createTextNode(labels[i]));
if (statuses[i] === currentValue) {
option.selected = true;
}
select.appendChild(option);
}
if (!isCurrentYear) {
select.disabled = true;
select.title = 'Editing disabled for non-current year';
}
return select;
}
// Infer semester by date using fixed windows:
// Fall: Y1-09-21 to Y2-01-18
// Spring: Y2-01-25 to Y2-05-30
function inferSemesterByDate(dateStr, schoolYear) {
try {
if (!schoolYear || schoolYear.indexOf('-') < 0) return '';
var parts = schoolYear.split('-');
var y1 = parseInt(parts[0], 10);
var y2 = parseInt(parts[1], 10);
if (!y1 || !y2) return '';
var fallStart = new Date(y1 + '-09-21T00:00:00Z');
var fallEnd = new Date(y2 + '-01-18T00:00:00Z');
var spStart = new Date(y2 + '-01-25T00:00:00Z');
var spEnd = new Date(y2 + '-05-30T00:00:00Z');
var d = new Date(dateStr + 'T00:00:00Z');
if (d >= fallStart && d <= fallEnd) return 'Fall';
if (d >= spStart && d <= spEnd) return 'Spring';
// Fallback by month if out of bounds
var m = d.getUTCMonth() + 1; // 1..12
return (m >= 9 || m <= 1) ? 'Fall' : 'Spring';
} catch (_) {
return '';
}
}
function initializeSelects() {
var selects = document.querySelectorAll('.att-select');
for (var i = 0; i < selects.length; i++) {
applyAppearance(selects[i]);
}
}
function applyAppearance(select) {
if (!select) {
return;
}
select.classList.remove('att-none', 'att-present', 'att-absent', 'att-late');
var value = (select.value || '').toLowerCase();
if (value === 'present') {
select.classList.add('att-present');
} else if (value === 'absent') {
select.classList.add('att-absent');
} else if (value === 'late') {
select.classList.add('att-late');
} else {
select.classList.add('att-none');
}
}
function recomputeRowTotals(row) {
if (!row) {
return;
}
var totals = { p: 0, a: 0, l: 0 };
var selects = row.querySelectorAll('.att-select');
for (var i = 0; i < selects.length; i++) {
var value = selects[i].value;
if (value === 'present') {
totals.p++;
} else if (value === 'absent') {
totals.a++;
} else if (value === 'late') {
totals.l++;
}
}
var cells = row.querySelectorAll('td');
var len = cells.length;
if (len >= 3) {
cells[len - 3].innerHTML = '<strong>' + totals.p + '</strong>';
cells[len - 2].innerHTML = '<strong>' + totals.a + '</strong>';
cells[len - 1].innerHTML = '<strong>' + totals.l + '</strong>';
}
}
function toFormData(obj) {
var fd = new FormData();
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fd.append(key, obj[key]);
}
}
return fd;
}
document.addEventListener('change', function(event) {
var select = event.target;
if (!select || !select.classList || !select.classList.contains('att-select')) {
return;
}
if (!isCurrentYear) {
event.preventDefault();
if (window.showToast) showToast('Editing disabled for non-current year', false);
return;
}
var payload = {};
payload[csrfName] = csrfHash;
payload.date = select.getAttribute('data-date') || '';
payload.status = select.value || '';
payload.user_id = select.getAttribute('data-user-id') || '';
payload.semester = select.getAttribute('data-semester') || semesterDefault;
payload.school_year = select.getAttribute('data-school-year') || schoolYearDefault;
payload.role_name = select.getAttribute('data-role-name') || '';
if (!payload.date || !payload.user_id || !payload.semester || !payload.school_year) {
console.error('Missing required fields for save', payload);
return;
}
select.disabled = true;
fetch(saveUrl, {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
// Send CSRF in headers as well to satisfy strict CSRF setups
'X-CSRF-TOKEN': csrfHash,
'X-CSRF-Token': csrfHash
},
body: toFormData(payload),
credentials: 'same-origin'
})
.then(function(response) {
return response.json().catch(function() {
return {};
}).then(function(data) {
return { ok: response.ok, data: data };
});
})
.then(function(result) {
if (!result) {
throw new Error('Invalid response');
}
var data = result.data || {};
if (data.csrf_token && data.csrf_hash) {
csrfName = data.csrf_token;
csrfHash = data.csrf_hash;
} else {
// Fallback: try refresh from cookie if server rotated token
try {
var c = document.cookie.split(';').map(function(v){return v.trim();}).find(function(v){
return v.indexOf('<?= esc(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>=')===0;
});
if (c) {
csrfHash = decodeURIComponent(c.split('=')[1] || '');
}
} catch (_) {}
}
if (!result.ok || data.ok === false) {
console.error('Save failed', data);
if (window.showToast) showToast('Save failed', false);
return;
}
applyAppearance(select);
recomputeRowTotals(select.closest('tr'));
if (window.showToast) showToast('Saved');
})
.catch(function(error) {
console.error('Network/save error', error);
if (window.showToast) showToast('Network error while saving', false);
})
.finally(function() {
select.disabled = false;
});
});
});
</script>
<?= $this->endSection() ?>
+94
View File
@@ -0,0 +1,94 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container">
<h2 class="text-center mt-4 mb-3"><?= esc($title ?? 'Attendance History') ?></h2>
<div class="card mb-4">
<div class="card-body">
<a href="<?= base_url('attendance/violations') ?>" class="btn btn-secondary mb-3">Back to Attendance</a>
<p class="text-muted small mb-3">Showing absences and lates for this student within the violation window.</p>
<?php
$viCode = $violation_code ?? null;
$viDateRaw = $violation_date ?? '';
$viDateLbl = $viDateRaw !== '' ? date('m-d-Y', strtotime($viDateRaw)) : '—';
$viNotifRaw = $violation_notified ?? null;
$viNotif = (is_string($viNotifRaw) && in_array(strtolower(trim($viNotifRaw)), ['yes', '1'], true))
|| ((int)$viNotifRaw === 1);
?>
<h5 class="card-title mb-3">Attendance Violations</h5>
<table class="table table-striped attendance-history-table no-mgmt-sticky" data-no-mgmt-sticky="1">
<thead>
<tr>
<th>Date</th>
<th>Status</th>
<th>Reason</th>
<th>Notified</th>
<th>Reported</th>
</tr>
</thead>
<tbody>
<?php if (!empty($show_violation_summary) && ($viCode || $viDateRaw !== '')): ?>
<tr class="table-info">
<td><?= esc($viDateLbl) ?></td>
<td><?= esc($viCode ?: '—') ?></td>
<td>Violation summary</td>
<td><?= $viNotif ? 'Yes' : 'No' ?></td>
<td>—</td>
</tr>
<?php endif; ?>
<?php if (!empty($attendance) && is_array($attendance)): ?>
<?php foreach ($attendance as $record): ?>
<?php
$dateStr = (string)($record['date'] ?? '');
$ymd = $dateStr !== '' ? substr($dateStr, 0, 10) : '';
$dateLabel = $ymd !== '' ? date('m-d-Y', strtotime($ymd)) : '—';
$statusRaw = strtolower(trim((string)($record['status'] ?? ''))); // absent/late
$reason = trim((string)($record['reason'] ?? '')); // may come from tracking join
$notifRaw = $record['is_notified'] ?? null;
$isNotified = (is_string($notifRaw) && in_array(strtolower(trim($notifRaw)), ['yes', '1'], true))
|| ((int)$notifRaw === 1);
$reportedRaw = $record['is_reported'] ?? null;
$isReported = (is_string($reportedRaw) && in_array(strtolower(trim($reportedRaw)), ['yes', '1'], true))
|| ((int)$reportedRaw === 1);
$isAbs = $statusRaw === 'absent' || $statusRaw === 'abs' || $statusRaw === 'a' || str_starts_with($statusRaw, 'abs');
$isLate = $statusRaw === 'late' || $statusRaw === 'l' || str_starts_with($statusRaw, 'late');
$status = $statusRaw !== '' ? ucfirst($statusRaw) : 'Unknown';
if ($isAbs) {
$status = $isReported ? 'Reported Absence' : 'Unreported Absence';
} elseif ($isLate) {
$status = $isReported ? 'Reported Late' : 'Unreported Late';
}
?>
<tr>
<td><?= esc($dateLabel) ?></td>
<td><?= esc($status) ?></td>
<td><?= $reason !== '' ? esc($reason) : 'No reason provided' ?></td>
<td><?= $isNotified ? 'Yes' : 'No' ?></td>
<td><?= $isReported ? 'Yes' : 'No' ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="5" class="text-center text-muted">No attendance violations found for this student.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('styles') ?>
<style>
/* Ensure table header is not sticky on this view to avoid overlap */
.attendance-history-table thead th {
position: static !important;
top: auto !important;
background: #f8f9fa;
z-index: 1;
}
</style>
<?= $this->endSection() ?>
@@ -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() ?>
+579
View File
@@ -0,0 +1,579 @@
<?= $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() ?>