481 lines
23 KiB
PHP
481 lines
23 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
|
|
<?php
|
|
$classResults = $classResults ?? [];
|
|
$selectedYear = $selectedYear ?? '';
|
|
$selectedPercentile = $selectedPercentile ?? 75;
|
|
$years = $years ?? [];
|
|
|
|
$totalWinners = array_sum(array_map(fn($c) => count($c['winners']), $classResults));
|
|
$totalStudents = array_sum(array_column($classResults, 'student_count'));
|
|
$totalBoys = array_sum(array_column($classResults, 'boys'));
|
|
$totalGirls = array_sum(array_column($classResults, 'girls'));
|
|
$totalTrophyBoys = array_sum(array_column($classResults, 'trophy_boys'));
|
|
$totalTrophyGirls = array_sum(array_column($classResults, 'trophy_girls'));
|
|
|
|
$pctWinners = $totalStudents > 0 ? round($totalWinners / $totalStudents * 100) : 0;
|
|
$pctTrophyBoys = $totalBoys > 0 ? round($totalTrophyBoys / $totalBoys * 100) : 0;
|
|
$pctTrophyGirls = $totalGirls > 0 ? round($totalTrophyGirls / $totalGirls * 100) : 0;
|
|
$pctBoys = $totalStudents > 0 ? round($totalBoys / $totalStudents * 100) : 0;
|
|
$pctGirls = $totalStudents > 0 ? round($totalGirls / $totalStudents * 100) : 0;
|
|
?>
|
|
|
|
<style>
|
|
/* ── Screen: hide print-only blocks ── */
|
|
.print-only { display: none !important; }
|
|
|
|
/* ── Print ── */
|
|
@media print {
|
|
.no-print { display: none !important; }
|
|
.print-only { display: block !important; }
|
|
.screen-only { display: none !important; }
|
|
|
|
body { font-size: 11px; }
|
|
.container-fluid { padding: 0 !important; }
|
|
h2, h3 { font-size: 14px; margin-bottom: .4rem; }
|
|
.print-page-break { break-before: page; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
|
table th,
|
|
table td { border: 1px solid #bbb; padding: 3px 6px; }
|
|
table thead { background: #333 !important; color: #fff !important;
|
|
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
table thead th { position: static !important; top: auto !important; box-shadow: none !important; }
|
|
table tfoot { background: #eee !important;
|
|
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
.badge-m { color: #4A90E2; font-weight: bold; }
|
|
.badge-f { color: #E47AB0; font-weight: bold; }
|
|
}
|
|
</style>
|
|
|
|
<div class="container-fluid">
|
|
|
|
<!-- ══ SCREEN HEADER ══════════════════════════════════════════════════════ -->
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-2 no-print">
|
|
<div>
|
|
<h2 class="mb-1"><i class="bi bi-trophy-fill text-warning me-2"></i>Trophy Winners</h2>
|
|
<p class="text-muted mb-0">
|
|
<?= esc($selectedYear) ?> — <?= (int)$selectedPercentile ?>th percentile —
|
|
<strong><?= $totalWinners ?></strong> winners across <strong><?= count($classResults) ?></strong> classes
|
|
</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button onclick="window.print()" class="btn btn-primary btn-sm">
|
|
<i class="bi bi-printer-fill me-1"></i>Print
|
|
</button>
|
|
<a href="<?= site_url('administrator/trophy?' . http_build_query(['school_year' => $selectedYear, 'percentile' => $selectedPercentile])) ?>"
|
|
class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-arrow-left me-1"></i>Back
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter (screen only) -->
|
|
<form method="get" action="<?= site_url('administrator/trophy/winners') ?>"
|
|
class="row g-2 align-items-end mb-4 no-print">
|
|
<div class="col-auto">
|
|
<label class="form-label mb-1 small fw-semibold">School Year</label>
|
|
<select name="school_year" class="form-select form-select-sm" style="min-width:130px;">
|
|
<?php foreach ($years as $yr): ?>
|
|
<option value="<?= esc($yr) ?>" <?= $yr === $selectedYear ? 'selected' : '' ?>><?= esc($yr) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<label class="form-label mb-1 small fw-semibold">Percentile</label>
|
|
<div class="input-group input-group-sm" style="width:110px;">
|
|
<input type="number" name="percentile" class="form-control"
|
|
min="1" max="99" step="1" value="<?= (int)$selectedPercentile ?>">
|
|
<span class="input-group-text">%</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="submit" class="btn btn-primary btn-sm">Apply</button>
|
|
</div>
|
|
</form>
|
|
|
|
<?php if (empty($classResults)): ?>
|
|
<div class="alert alert-info no-print">No projected trophy winners found.</div>
|
|
<?php else: ?>
|
|
|
|
<!-- ══ PRINT VIEW ════════════════════════════════════════════════════════ -->
|
|
<div class="print-only">
|
|
|
|
<!-- Print title -->
|
|
<div style="text-align:center;margin-bottom:12px;border-bottom:2px solid #333;padding-bottom:8px;">
|
|
<h2 style="margin:0;">Trophy Winners — <?= esc($selectedYear) ?></h2>
|
|
<p style="margin:4px 0 0;font-size:10px;color:#555;">
|
|
<?= (int)$selectedPercentile ?>th percentile —
|
|
<?= $totalWinners ?> winners / <?= $totalStudents ?> students —
|
|
<?= count($classResults) ?> classes
|
|
</p>
|
|
</div>
|
|
|
|
<!-- TABLE 1: All winners -->
|
|
<h3 style="margin-bottom:4px;">Winners</h3>
|
|
<table data-no-mgmt-sticky>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Class</th>
|
|
<th>Name</th>
|
|
<th style="text-align:center;">Gender</th>
|
|
<th style="text-align:right;">Fall Score</th>
|
|
<th style="text-align:right;">Spring Score</th>
|
|
<th style="text-align:right;">Year Score</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$globalRank = 0;
|
|
foreach ($classResults as $cls):
|
|
foreach ($cls['winners'] as $student):
|
|
$globalRank++;
|
|
$isMale = strtolower($student['gender'] ?? '') === 'male';
|
|
?>
|
|
<tr>
|
|
<td style="text-align:center;"><?= $globalRank ?></td>
|
|
<td><?= esc($cls['section_name']) ?></td>
|
|
<td><strong><?= esc($student['name']) ?></strong></td>
|
|
<td style="text-align:center;">
|
|
<span class="<?= $isMale ? 'badge-m' : 'badge-f' ?>">
|
|
<?= $isMale ? 'M' : 'F' ?>
|
|
</span>
|
|
</td>
|
|
<td style="text-align:right;font-weight:bold;">
|
|
<?= $student['fall_score'] !== null ? number_format($student['fall_score'], 1) : '—' ?>
|
|
</td>
|
|
<td style="text-align:right;">
|
|
<?= $student['spring_score'] !== null ? number_format($student['spring_score'], 1) : '—' ?>
|
|
</td>
|
|
<td style="text-align:right;font-weight:bold;color:#155724;">
|
|
<?= $student['year_score'] !== null ? number_format($student['year_score'], 1) : '—' ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; endforeach; ?>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="2" style="font-weight:bold;">Total</td>
|
|
<td colspan="2" style="font-weight:bold;"><?= $totalWinners ?> winners</td>
|
|
<td colspan="3" style="text-align:right;font-weight:bold;"><?= $pctWinners ?>% of students</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<!-- TABLE 2: Year summary (new page) -->
|
|
<div class="print-page-break"></div>
|
|
|
|
<h3 style="margin-bottom:4px;">Year Summary — <?= esc($selectedYear) ?></h3>
|
|
<table style="margin-bottom:16px;" data-no-mgmt-sticky>
|
|
<thead>
|
|
<tr>
|
|
<th>Class</th>
|
|
<th style="text-align:center;">Students</th>
|
|
<th style="text-align:center;">Boys</th>
|
|
<th style="text-align:center;">Girls</th>
|
|
<th style="text-align:center;">Winners</th>
|
|
<th style="text-align:center;">Trophy Boys</th>
|
|
<th style="text-align:center;">Trophy Girls</th>
|
|
<th style="text-align:center;">Rate</th>
|
|
<th style="text-align:right;">Fall Threshold</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($classResults as $cls): ?>
|
|
<tr>
|
|
<td><?= esc($cls['section_name']) ?></td>
|
|
<td style="text-align:center;"><?= $cls['student_count'] ?></td>
|
|
<td style="text-align:center;"><?= $cls['boys'] ?> (<?= $cls['pct_boys'] ?>%)</td>
|
|
<td style="text-align:center;"><?= $cls['girls'] ?> (<?= $cls['pct_girls'] ?>%)</td>
|
|
<td style="text-align:center;font-weight:bold;"><?= count($cls['winners']) ?></td>
|
|
<td style="text-align:center;">
|
|
<span class="badge-m"><?= $cls['trophy_boys'] ?></span>
|
|
<?= $cls['boys'] > 0 ? '(' . $cls['pct_trophy_boys'] . '%)' : '' ?>
|
|
</td>
|
|
<td style="text-align:center;">
|
|
<span class="badge-f"><?= $cls['trophy_girls'] ?></span>
|
|
<?= $cls['girls'] > 0 ? '(' . $cls['pct_trophy_girls'] . '%)' : '' ?>
|
|
</td>
|
|
<td style="text-align:center;"><?= $cls['pct_trophy_total'] ?>%</td>
|
|
<td style="text-align:right;"><?= $cls['threshold'] !== null ? number_format((float)$cls['threshold'], 1) : '—' ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td style="font-weight:bold;">Total</td>
|
|
<td style="text-align:center;font-weight:bold;"><?= $totalStudents ?></td>
|
|
<td style="text-align:center;font-weight:bold;"><?= $totalBoys ?> (<?= $pctBoys ?>%)</td>
|
|
<td style="text-align:center;font-weight:bold;"><?= $totalGirls ?> (<?= $pctGirls ?>%)</td>
|
|
<td style="text-align:center;font-weight:bold;"><?= $totalWinners ?></td>
|
|
<td style="text-align:center;font-weight:bold;">
|
|
<span class="badge-m"><?= $totalTrophyBoys ?></span> (<?= $pctTrophyBoys ?>%)
|
|
</td>
|
|
<td style="text-align:center;font-weight:bold;">
|
|
<span class="badge-f"><?= $totalTrophyGirls ?></span> (<?= $pctTrophyGirls ?>%)
|
|
</td>
|
|
<td style="text-align:center;font-weight:bold;"><?= $pctWinners ?>%</td>
|
|
<td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
</div><!-- /print-only -->
|
|
|
|
<!-- ══ SCREEN VIEW ═══════════════════════════════════════════════════════ -->
|
|
<div class="screen-only">
|
|
|
|
<!-- Per-class cards -->
|
|
<?php foreach ($classResults as $cls):
|
|
$nW = count($cls['winners']);
|
|
?>
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header d-flex flex-wrap align-items-center justify-content-between gap-2 py-2"
|
|
style="background:#fff8e1;">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="fw-bold fs-6"><?= esc($cls['section_name']) ?></span>
|
|
<span class="badge bg-warning text-dark">
|
|
<i class="bi bi-trophy-fill"></i> <?= $nW ?> winner<?= $nW !== 1 ? 's' : '' ?>
|
|
</span>
|
|
<?php if ($cls['threshold'] !== null): ?>
|
|
<span class="text-muted small">Fall ≥ <?= number_format((float)$cls['threshold'], 1) ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="d-flex gap-3 small">
|
|
<span>
|
|
<i class="bi bi-gender-male" style="color:#4A90E2"></i>
|
|
<strong style="color:#4A90E2"><?= $cls['trophy_boys'] ?></strong>/<?= $cls['boys'] ?> boys
|
|
<?php if ($cls['boys'] > 0): ?><span class="text-muted">(<?= $cls['pct_trophy_boys'] ?>%)</span><?php endif; ?>
|
|
</span>
|
|
<span>
|
|
<i class="bi bi-gender-female" style="color:#E47AB0"></i>
|
|
<strong style="color:#E47AB0"><?= $cls['trophy_girls'] ?></strong>/<?= $cls['girls'] ?> girls
|
|
<?php if ($cls['girls'] > 0): ?><span class="text-muted">(<?= $cls['pct_trophy_girls'] ?>%)</span><?php endif; ?>
|
|
</span>
|
|
<span class="text-muted"><?= $nW ?>/<?= $cls['student_count'] ?> (<?= $cls['pct_trophy_total'] ?>%)</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<table class="table table-sm table-hover mb-0 align-middle" data-no-mgmt-sticky>
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th class="ps-3" style="width:2.2rem;">#</th>
|
|
<th>Name</th>
|
|
<th class="text-center" style="width:3rem;">Gender</th>
|
|
<th class="text-end">Fall Score</th>
|
|
<th class="text-end">Spring Score</th>
|
|
<th class="text-end pe-3">Year Score</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($cls['winners'] as $i => $student):
|
|
$isMale = strtolower($student['gender'] ?? '') === 'male';
|
|
?>
|
|
<tr>
|
|
<td class="ps-3 text-muted small"><?= $i + 1 ?></td>
|
|
<td class="fw-semibold small"><?= esc($student['name']) ?></td>
|
|
<td class="text-center">
|
|
<span class="badge" style="background:<?= $isMale ? '#4A90E2' : '#E47AB0' ?>;font-size:.65rem;">
|
|
<?= $isMale ? 'M' : 'F' ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end small fw-semibold">
|
|
<?= $student['fall_score'] !== null ? number_format($student['fall_score'], 1) : '<span class="text-muted">—</span>' ?>
|
|
</td>
|
|
<td class="text-end small">
|
|
<?= $student['spring_score'] !== null
|
|
? '<span class="fw-semibold">' . number_format($student['spring_score'], 1) . '</span>'
|
|
: '<span class="text-muted">—</span>' ?>
|
|
</td>
|
|
<td class="text-end pe-3 small fw-semibold" style="color:#155724;">
|
|
<?= $student['year_score'] !== null ? number_format($student['year_score'], 1) : '<span class="text-muted">—</span>' ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
|
|
<!-- Overall year stats -->
|
|
<div class="card shadow-sm mt-2">
|
|
<div class="card-header bg-dark text-white fw-semibold py-2">
|
|
<i class="bi bi-bar-chart-fill me-2"></i>Overall Year Summary — <?= esc($selectedYear) ?>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3 mb-3">
|
|
<?php foreach ([
|
|
[$totalWinners, 'Total Winners', 'warning', 'text-dark', $pctWinners . '% of students', null],
|
|
[$totalStudents, 'Total Students', 'secondary', 'text-white', count($classResults) . ' classes', null],
|
|
[$totalBoys, 'Total Boys', null, null, $pctBoys . '%', '#4A90E2'],
|
|
[$totalGirls, 'Total Girls', null, null, $pctGirls . '%', '#E47AB0'],
|
|
[$totalTrophyBoys, 'Trophy Boys', null, null, $pctTrophyBoys . '% of boys', '#4A90E2'],
|
|
[$totalTrophyGirls, 'Trophy Girls', null, null, $pctTrophyGirls . '% of girls', '#E47AB0'],
|
|
] as [$val, $label, $bg, $tc, $sub, $color]): ?>
|
|
<div class="col-6 col-sm-4 col-lg-2">
|
|
<div class="border rounded text-center p-3 h-100">
|
|
<div class="fs-3 fw-bold" <?= isset($color) ? 'style="color:' . $color . '"' : ($bg ? 'class="text-' . $bg . '"' : '') ?>>
|
|
<?= $val ?>
|
|
</div>
|
|
<div class="small text-muted"><?= $label ?></div>
|
|
<div class="badge mt-1 <?= $bg ? 'bg-' . $bg . ' ' . $tc : 'text-white' ?>"
|
|
<?= isset($color) && !$bg ? 'style="background:' . $color . '"' : '' ?>>
|
|
<?= $sub ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<div class="row g-4 mt-1">
|
|
<!-- Stacked bar: trophies per class (boys + girls) -->
|
|
<div class="col-12 col-lg-7">
|
|
<div class="border rounded p-3 h-100">
|
|
<div class="small fw-semibold text-muted mb-2">
|
|
<i class="bi bi-trophy-fill text-warning me-1"></i>Trophy Winners per Class
|
|
</div>
|
|
<canvas id="chart-per-class" style="max-height:260px;"></canvas>
|
|
</div>
|
|
</div>
|
|
<!-- Doughnut: trophy gender split + rate per class -->
|
|
<div class="col-12 col-lg-5">
|
|
<div class="row g-3 h-100">
|
|
<div class="col-12">
|
|
<div class="border rounded p-3">
|
|
<div class="small fw-semibold text-muted mb-2">
|
|
<i class="bi bi-gender-ambiguous me-1"></i>Winners by Gender
|
|
</div>
|
|
<canvas id="chart-gender" style="max-height:160px;"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="border rounded p-3">
|
|
<div class="small fw-semibold text-muted mb-2">
|
|
<i class="bi bi-bar-chart-fill me-1"></i>Trophy Rate per Class (%)
|
|
</div>
|
|
<canvas id="chart-rate" style="max-height:130px;"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /screen-only -->
|
|
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
<?php
|
|
$chartLabels = [];
|
|
$chartBoys = [];
|
|
$chartGirls = [];
|
|
$chartRates = [];
|
|
foreach ($classResults as $cls) {
|
|
$chartLabels[] = $cls['section_name'];
|
|
$chartBoys[] = $cls['trophy_boys'];
|
|
$chartGirls[] = $cls['trophy_girls'];
|
|
$chartRates[] = $cls['pct_trophy_total'];
|
|
}
|
|
?>
|
|
var labels = <?= json_encode($chartLabels) ?>;
|
|
var boys = <?= json_encode($chartBoys) ?>;
|
|
var girls = <?= json_encode($chartGirls) ?>;
|
|
var rates = <?= json_encode($chartRates) ?>;
|
|
|
|
var BOY_COLOR = '#4A90E2';
|
|
var GIRL_COLOR = '#E47AB0';
|
|
var RATE_COLOR = '#f0a500';
|
|
|
|
/* ── Chart 1: stacked horizontal bar per class ── */
|
|
new Chart(document.getElementById('chart-per-class'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{ label: 'Boys', data: boys, backgroundColor: BOY_COLOR, stack: 'winners' },
|
|
{ label: 'Girls', data: girls, backgroundColor: GIRL_COLOR, stack: 'winners' }
|
|
]
|
|
},
|
|
options: {
|
|
indexAxis: 'y',
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: { legend: { position: 'bottom' } },
|
|
scales: {
|
|
x: { stacked: true, ticks: { stepSize: 1 }, grid: { color: '#f0f0f0' } },
|
|
y: { stacked: true }
|
|
}
|
|
}
|
|
});
|
|
|
|
/* ── Chart 2: doughnut gender split ── */
|
|
new Chart(document.getElementById('chart-gender'), {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Trophy Boys', 'Trophy Girls'],
|
|
datasets: [{
|
|
data: [<?= $totalTrophyBoys ?>, <?= $totalTrophyGirls ?>],
|
|
backgroundColor: [BOY_COLOR, GIRL_COLOR],
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: { position: 'bottom' },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(ctx) {
|
|
var total = ctx.dataset.data.reduce(function(a,b){ return a+b; }, 0);
|
|
var pct = total > 0 ? Math.round(ctx.parsed / total * 100) : 0;
|
|
return ctx.label + ': ' + ctx.parsed + ' (' + pct + '%)';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/* ── Chart 3: bar chart rate per class ── */
|
|
new Chart(document.getElementById('chart-rate'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Trophy Rate %',
|
|
data: rates,
|
|
backgroundColor: rates.map(function(r) {
|
|
return r > 30 ? '#dc3545' : r > 20 ? RATE_COLOR : '#28a745';
|
|
}),
|
|
borderRadius: 3
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true, max: 100,
|
|
ticks: { callback: function(v) { return v + '%'; } },
|
|
grid: { color: '#f0f0f0' }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->endSection() ?>
|