recreate project
This commit is contained in:
@@ -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() ?>
|
||||
Reference in New Issue
Block a user