recreate project
This commit is contained in:
@@ -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() ?>
|
||||
@@ -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() ?>
|
||||
@@ -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() ?>
|
||||
@@ -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>
|
||||
@@ -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 & 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 & 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() ?>
|
||||
@@ -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> <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('&','&').replaceAll('<','<')
|
||||
.replaceAll('>','>').replaceAll('"','"').replaceAll("'", "'");
|
||||
}
|
||||
|
||||
// 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 & 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() ?>
|
||||
@@ -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;"> </span> 1 Absence — Auto Email</span>
|
||||
<span><span class="badge rounded-pill" style="background-color:#fadb14;color:#000;"> </span> 2 Absences (row/≤3 weeks) — Team Notify</span>
|
||||
<span><span class="badge rounded-pill" style="background-color:#fa8c16;"> </span> 3 Absences (row/≤4 weeks) — Team Notify</span>
|
||||
<span><span class="badge rounded-pill" style="background-color:#ff4d4f;"> </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;"> </span> 2 Lates (row/≤3 weeks) — Auto Email</span>
|
||||
<span><span class="badge rounded-pill" style="background-color:#002766;"> </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('&', '&').replaceAll('<', '<')
|
||||
.replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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 & 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']) ?> <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() ?>
|
||||
Reference in New Issue
Block a user