recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
@@ -0,0 +1,471 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?= csrf_meta() ?>
<?php
// Exclude unwanted grade buckets globally (e.g., legacy Grade 10/11) unless the bucket is the Arabic program.
$hiddenGrades = [10, 11];
$hasArabic = static function (array $sections): bool {
foreach ($sections as $section) {
$name = strtolower((string)($section['class_section_name'] ?? ''));
$code = strtolower((string)($section['class_section_id'] ?? ''));
if (strpos($name, 'arabic') !== false || strpos($code, 'arabic') !== false) {
return true;
}
}
return false;
};
foreach ($hiddenGrades as $hid) {
if (isset($grades[$hid]) && $hasArabic($grades[$hid])) {
continue; // keep Arabic sections even if the class_id matches a hidden grade bucket
}
unset($grades[$hid]);
}
// Prune out sections with no students (extra safety so empty Arabic-2/3 never render)
$filteredGrades = [];
foreach ($grades as $cid => $sections) {
$kept = [];
foreach ($sections as $section) {
$secKey = (string)($section['class_section_id'] ?? ($section['id'] ?? ''));
if ($secKey !== '' && !empty($studentsBySection[$secKey])) {
$kept[] = $section;
}
}
if (!empty($kept)) {
$filteredGrades[$cid] = $kept;
}
}
$grades = $filteredGrades;
?>
<div class="container-fluid">
<div class="wrapper">
<div class="content mb-3"></div>
<h2 class="text-center mt-4 mb-3">Attendance Analysis</h2>
<?= $this->include('partials/academic_filter') ?>
<?= $this->section('styles') ?>
<style>
.attn-analysis-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
.attn-analysis-card {
background: #fff;
border: 1px solid rgba(0,0,0,0.08);
border-radius: .5rem;
padding: 1rem;
}
.attn-analysis-card h6 { margin-bottom: .75rem; }
.attn-analysis-wide { grid-column: span 12; }
.attn-analysis-half { grid-column: span 6; }
.attn-analysis-table {
width: 100%;
font-size: .9rem;
}
.attn-analysis-table th,
.attn-analysis-table td { padding: .35rem .5rem; }
.attn-analysis-scroll {
max-height: none;
overflow: visible;
}
@media (max-width: 1200px) {
.attn-analysis-half { grid-column: span 12; }
}
</style>
<?= $this->endSection() ?>
<?php
$req = request();
$startDateRaw = trim((string)($req->getGet('start_date') ?? ''));
$endDateRaw = trim((string)($req->getGet('end_date') ?? ''));
$isValidDate = static function (string $d): bool {
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $d)) return false;
[$y, $m, $day] = array_map('intval', explode('-', $d));
return checkdate($m, $day, $y);
};
$filterStart = ($startDateRaw !== '' && $isValidDate($startDateRaw)) ? $startDateRaw : '';
$filterEnd = ($endDateRaw !== '' && $isValidDate($endDateRaw)) ? $endDateRaw : '';
// ---- Build labels and a stable order: KG -> Grade 1..12 -> Youth ----
$labelsByClassId = [];
$gradeNumberById = [];
$kgIds = [];
$arabicIds = [];
$youthIds = [];
foreach ($grades as $classId => $sections) {
$cid = (int)$classId;
$sampleName = '';
$isArabicProgram = false;
foreach ($sections as $s) {
$sampleName = (string)($s['class_section_name'] ?? '');
$lowName = strtolower($sampleName);
$lowCode = strtolower((string)($s['class_section_id'] ?? ''));
if (strpos($lowName, 'arabic') !== false || strpos($lowCode, 'arabic') !== false) {
$isArabicProgram = true;
}
if ($sampleName !== '') break;
}
if ($isArabicProgram || $cid === 14) {
$labelsByClassId[$cid] = 'Arabic';
$arabicIds[] = $cid;
} elseif (preg_match('/\bKG\b/i', $sampleName)) {
$labelsByClassId[$cid] = 'KG';
$kgIds[] = $cid;
} elseif (preg_match('/\bYouth\b/i', $sampleName)) {
$labelsByClassId[$cid] = 'Youth';
$youthIds[] = $cid;
} elseif (preg_match('/Grade\s*(\d+)/i', $sampleName, $m)) {
$labelsByClassId[$cid] = 'Grade ' . (int)$m[1];
$gradeNumberById[$cid] = (int)$m[1];
} else {
if ($cid === 0) {
$labelsByClassId[$cid] = 'KG';
$kgIds[] = $cid;
} elseif ($cid === 13) {
$labelsByClassId[$cid] = 'Youth';
$youthIds[] = $cid;
} else {
$labelsByClassId[$cid] = 'Grade ' . $cid;
$gradeNumberById[$cid] = $cid;
}
}
}
$sortedClassIds = [];
if ($kgIds) {
sort($kgIds, SORT_NUMERIC);
$sortedClassIds = array_merge($sortedClassIds, $kgIds);
}
if ($gradeNumberById) {
asort($gradeNumberById, SORT_NUMERIC);
$sortedClassIds = array_merge($sortedClassIds, array_keys($gradeNumberById));
}
if ($youthIds) {
sort($youthIds, SORT_NUMERIC);
$sortedClassIds = array_merge($sortedClassIds, $youthIds);
}
if ($arabicIds) {
sort($arabicIds, SORT_NUMERIC);
$sortedClassIds = array_merge($sortedClassIds, $arabicIds);
}
$presentIds = array_map('intval', array_keys($grades));
$missingIds = array_values(array_diff($presentIds, $sortedClassIds));
if (!empty($missingIds)) {
$sortedClassIds = array_merge($sortedClassIds, $missingIds);
}
$labelFor = function (int $id) use ($labelsByClassId): string {
return $labelsByClassId[$id] ?? ('Class ' . $id);
};
// ---- Attendance analysis ----
$analysisOverall = ['present' => 0, 'late' => 0, 'absent' => 0];
$analysisByGrade = [];
$analysisBySection = [];
$analysisTotal = 0;
foreach ($grades as $classId => $sections) {
$gradeLabel = $labelFor((int)$classId);
if (!isset($analysisByGrade[$gradeLabel])) {
$analysisByGrade[$gradeLabel] = ['present' => 0, 'late' => 0, 'absent' => 0];
}
foreach ($sections as $section) {
$sectionKey = (string)($section['class_section_id'] ?? ($section['id'] ?? ''));
if ($sectionKey === '' || empty($studentsBySection[$sectionKey])) continue;
$secNameRaw = trim((string)($section['class_section_name'] ?? ''));
$secLabel = $secNameRaw !== '' ? $secNameRaw : ('Section ' . $sectionKey);
if (!isset($analysisBySection[$secLabel])) {
$analysisBySection[$secLabel] = ['present' => 0, 'late' => 0, 'absent' => 0, 'total_students' => 0];
}
$sectionTotalStudents = count($studentsBySection[$sectionKey]);
if ($sectionTotalStudents > ($analysisBySection[$secLabel]['total_students'] ?? 0)) {
$analysisBySection[$secLabel]['total_students'] = $sectionTotalStudents;
}
foreach ($studentsBySection[$sectionKey] as $stu) {
$sid = (int)($stu['id'] ?? 0);
if ($sid <= 0) continue;
$entries = $attendanceData[$sectionKey][$sid] ?? [];
if (!is_array($entries)) continue;
foreach ($entries as $e) {
$d = substr((string)($e['date'] ?? ''), 0, 10);
if ($d === '') continue;
if ($filterStart !== '' && $d < $filterStart) continue;
if ($filterEnd !== '' && $d > $filterEnd) continue;
$st = strtolower(trim((string)($e['status'] ?? '')));
if (!in_array($st, ['present', 'late', 'absent'], true)) continue;
$analysisOverall[$st]++;
$analysisByGrade[$gradeLabel][$st]++;
$analysisBySection[$secLabel][$st]++;
$analysisTotal++;
}
}
}
}
$overallPercent = [];
foreach (['present', 'late', 'absent'] as $st) {
$overallPercent[$st] = $analysisTotal > 0 ? round(($analysisOverall[$st] * 100) / $analysisTotal, 1) : 0;
}
$analysisGradeLabels = [];
$analysisGradeAbsent = [];
$analysisGradeLate = [];
$analysisGradeTotals = [];
foreach ($sortedClassIds as $cid) {
$lbl = $labelFor((int)$cid);
$g = $analysisByGrade[$lbl] ?? ['present' => 0, 'late' => 0, 'absent' => 0];
$analysisGradeLabels[] = $lbl;
$analysisGradeAbsent[] = (int)($g['absent'] ?? 0);
$analysisGradeLate[] = (int)($g['late'] ?? 0);
$analysisGradeTotals[] = (int)($g['present'] ?? 0) + (int)($g['late'] ?? 0) + (int)($g['absent'] ?? 0);
}
$analysisSectionLabels = array_keys($analysisBySection);
$analysisSectionAbsent = [];
$analysisSectionLate = [];
$analysisSectionTotals = [];
foreach ($analysisSectionLabels as $lbl) {
$s = $analysisBySection[$lbl] ?? ['present' => 0, 'late' => 0, 'absent' => 0];
$analysisSectionAbsent[] = (int)($s['absent'] ?? 0);
$analysisSectionLate[] = (int)($s['late'] ?? 0);
$analysisSectionTotals[] = (int)($s['total_students'] ?? 0);
}
$totalDaysForPercent = 0;
if ($filterStart === '' && $filterEnd === '' && !empty($totalPassedDays)) {
$totalDaysForPercent = (int)$totalPassedDays;
} elseif (!empty($dateList) && is_array($dateList)) {
foreach ($dateList as $d) {
$d = (string)$d;
if ($d === '') continue;
if ($filterStart !== '' && $d < $filterStart) continue;
if ($filterEnd !== '' && $d > $filterEnd) continue;
if (!empty($noSchoolDays[$d])) continue;
$totalDaysForPercent++;
}
}
$backUrl = site_url('administrator/daily_attendance');
$queryParts = [];
if (!empty($semester)) $queryParts[] = 'semester=' . urlencode((string)$semester);
if (!empty($schoolYear)) $queryParts[] = 'school_year=' . urlencode((string)$schoolYear);
if ($queryParts) $backUrl .= '?' . implode('&', $queryParts);
?>
<div class="row justify-content-center mb-3">
<div class="col-auto">
<a class="btn btn-outline-secondary" href="<?= esc($backUrl) ?>">
Back to Daily Attendance
</a>
</div>
</div>
<form class="row g-2 justify-content-center mb-4" method="get" action="<?= esc(current_url()) ?>">
<?php if (!empty($semester)): ?>
<input type="hidden" name="semester" value="<?= esc((string)$semester) ?>">
<?php endif; ?>
<?php if (!empty($schoolYear)): ?>
<input type="hidden" name="school_year" value="<?= esc((string)$schoolYear) ?>">
<?php endif; ?>
<div class="col-12 col-md-3">
<label class="form-label">Start date</label>
<input type="date" class="form-control" name="start_date" value="<?= esc($filterStart) ?>">
</div>
<div class="col-12 col-md-3">
<label class="form-label">End date</label>
<input type="date" class="form-control" name="end_date" value="<?= esc($filterEnd) ?>">
</div>
<div class="col-12 col-md-auto d-flex align-items-end gap-2">
<button type="submit" class="btn btn-primary">Apply</button>
<a class="btn btn-outline-secondary" href="<?= esc(current_url()) ?>">Clear</a>
</div>
</form>
<div class="attn-analysis-grid">
<div class="attn-analysis-card attn-analysis-half">
<h6>Overall Distribution</h6>
<canvas id="attnPieChart" height="50" aria-label="Overall attendance pie chart"></canvas>
<table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
<th title="Attendance status for the selected date range">Status</th>
<th class="text-end" title="Number of attendance entries with this status">Count</th>
<th class="text-end" title="Percent of total attendance entries">Percent</th>
</tr>
</thead>
<tbody>
<tr>
<td>Present</td>
<td class="text-end"><?= (int)$analysisOverall['present'] ?></td>
<td class="text-end"><?= number_format($overallPercent['present'], 1) ?>%</td>
</tr>
<tr>
<td>Late</td>
<td class="text-end"><?= (int)$analysisOverall['late'] ?></td>
<td class="text-end"><?= number_format($overallPercent['late'], 1) ?>%</td>
</tr>
<tr>
<td>Absent</td>
<td class="text-end"><?= (int)$analysisOverall['absent'] ?></td>
<td class="text-end"><?= number_format($overallPercent['absent'], 1) ?>%</td>
</tr>
</tbody>
</table>
</div>
<div class="attn-analysis-card attn-analysis-half">
<h6>By Section (Absent / Late)</h6>
<canvas id="attnSectionChart" height="200" aria-label="Attendance by section bar chart"></canvas>
<div class="attn-analysis-scroll mt-3">
<table id="sectionAnalysisTable" class="attn-analysis-table no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
<th title="Class section name">Section</th>
<th class="text-end" title="Total semester days in range (excluding no-school days)">Semester Days</th>
<th class="text-end" title="Total students in the class section">Total Students</th>
<th class="text-end" title="Number of absent entries in range">Absent</th>
<th class="text-end" title="Absent entries as a percent of total possible attendance entries (total students × total days in range)">Absent%</th>
<th class="text-end" title="Number of late entries in range">Late</th>
<th class="text-end" title="Late entries as a percent of total possible attendance entries (total students × total days in range)">Late%</th>
</tr>
</thead>
<tbody>
<?php foreach ($analysisSectionLabels as $i => $lbl): ?>
<?php
$tot = (int)($analysisSectionTotals[$i] ?? 0);
$abs = (int)($analysisSectionAbsent[$i] ?? 0);
$late = (int)($analysisSectionLate[$i] ?? 0);
$denom = $totalDaysForPercent > 0 ? ($totalDaysForPercent * $tot) : 0;
$absPct = $denom > 0 ? round(($abs * 100) / $denom, 1) : 0;
$latePct = $denom > 0 ? round(($late * 100) / $denom, 1) : 0;
?>
<tr>
<td><?= esc($lbl) ?></td>
<td class="text-end"><?= (int)$totalDaysForPercent ?></td>
<td class="text-end"><?= $tot ?></td>
<td class="text-end"><?= $abs ?></td>
<td class="text-end"><?= number_format($absPct, 1) ?>%</td>
<td class="text-end"><?= $late ?></td>
<td class="text-end"><?= number_format($latePct, 1) ?>%</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ATTN_ANALYSIS = <?= json_encode([
'overall' => [
'labels' => ['Present', 'Late', 'Absent'],
'counts' => [
(int)($analysisOverall['present'] ?? 0),
(int)($analysisOverall['late'] ?? 0),
(int)($analysisOverall['absent'] ?? 0),
],
],
'byGrade' => [
'labels' => $analysisGradeLabels,
'absent' => $analysisGradeAbsent,
'late' => $analysisGradeLate,
],
'bySection' => [
'labels' => $analysisSectionLabels,
'absent' => $analysisSectionAbsent,
'late' => $analysisSectionLate,
],
]) ?>;
function percentOfTotal(value, total) {
if (!total) return '0%';
return (Math.round((value * 1000) / total) / 10).toFixed(1) + '%';
}
function buildAttendanceCharts() {
if (!window.Chart) return;
const pieEl = document.getElementById('attnPieChart');
const sectionEl = document.getElementById('attnSectionChart');
if (!pieEl || !sectionEl) return;
const overall = ATTN_ANALYSIS.overall || { labels: [], counts: [] };
const total = (overall.counts || []).reduce((a, b) => a + (parseInt(b, 10) || 0), 0);
new Chart(pieEl.getContext('2d'), {
type: 'pie',
data: {
labels: overall.labels,
datasets: [{
data: overall.counts,
backgroundColor: ['#198754', '#f0ad4e', '#dc3545'],
borderWidth: 1,
}]
},
options: {
plugins: {
tooltip: {
callbacks: {
label: function(ctx) {
const val = parseInt(ctx.parsed, 10) || 0;
return ctx.label + ': ' + val + ' (' + percentOfTotal(val, total) + ')';
}
}
},
legend: { position: 'bottom' }
}
}
});
new Chart(sectionEl.getContext('2d'), {
type: 'bar',
data: {
labels: ATTN_ANALYSIS.bySection?.labels || [],
datasets: [
{
label: 'Absent',
data: ATTN_ANALYSIS.bySection?.absent || [],
backgroundColor: '#dc3545',
},
{
label: 'Late',
data: ATTN_ANALYSIS.bySection?.late || [],
backgroundColor: '#f0ad4e',
}
]
},
options: {
plugins: { legend: { position: 'bottom' } },
scales: {
y: { beginAtZero: true, ticks: { precision: 0 } }
}
}
});
}
document.addEventListener('DOMContentLoaded', buildAttendanceCharts);
document.addEventListener('DOMContentLoaded', function () {
if (window.jQuery && $.fn.DataTable) {
$('#sectionAnalysisTable').DataTable({
paging: false,
searching: false,
info: false,
order: [[0, 'asc']]
});
}
});
</script>
<?= $this->endSection() ?>