add trophy winner name print
This commit is contained in:
@@ -14,6 +14,47 @@ $totalConfirmed = array_sum(array_column($classResults, 'confirmed'));
|
|||||||
$totalSurprise = array_sum(array_column($classResults, 'surprises'));
|
$totalSurprise = array_sum(array_column($classResults, 'surprises'));
|
||||||
$totalMissed = array_sum(array_column($classResults, 'missed'));
|
$totalMissed = array_sum(array_column($classResults, 'missed'));
|
||||||
$overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) : ($totalActual === 0 ? 100 : 0);
|
$overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) : ($totalActual === 0 ? 100 : 0);
|
||||||
|
|
||||||
|
// Winner gender breakdown.
|
||||||
|
// "Winner" means actual year-end trophy winner.
|
||||||
|
$totalWinnerBoys = 0;
|
||||||
|
$totalWinnerGirls = 0;
|
||||||
|
$totalWinnerOther = 0;
|
||||||
|
|
||||||
|
// Sticker names.
|
||||||
|
// 2 columns x 10 rows = 20 stickers per page.
|
||||||
|
// Stickers print NAME ONLY.
|
||||||
|
$winnerStickerNames = [];
|
||||||
|
|
||||||
|
foreach ($classResults as $cls) {
|
||||||
|
foreach (($cls['students'] ?? []) as $s) {
|
||||||
|
if (empty($s['actual'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gender = strtolower(trim((string)($s['gender'] ?? '')));
|
||||||
|
|
||||||
|
if (in_array($gender, ['male', 'm', 'boy', 'boys'], true)) {
|
||||||
|
$totalWinnerBoys++;
|
||||||
|
} elseif (in_array($gender, ['female', 'f', 'girl', 'girls'], true)) {
|
||||||
|
$totalWinnerGirls++;
|
||||||
|
} else {
|
||||||
|
$totalWinnerOther++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string)($s['name'] ?? ''));
|
||||||
|
|
||||||
|
if ($name !== '') {
|
||||||
|
$winnerStickerNames[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalWinners = $totalWinnerBoys + $totalWinnerGirls + $totalWinnerOther;
|
||||||
|
|
||||||
|
$winnerBoysPct = $totalWinners > 0 ? round(($totalWinnerBoys / $totalWinners) * 100, 1) : 0;
|
||||||
|
$winnerGirlsPct = $totalWinners > 0 ? round(($totalWinnerGirls / $totalWinners) * 100, 1) : 0;
|
||||||
|
$winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners) * 100, 1) : 0;
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -23,23 +64,141 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
.status-none { color:#adb5bd; }
|
.status-none { color:#adb5bd; }
|
||||||
|
|
||||||
.print-only { display: none !important; }
|
.print-only { display: none !important; }
|
||||||
|
.winner-sticker-print-area { display: none; }
|
||||||
|
|
||||||
|
@page {
|
||||||
|
size: Letter portrait;
|
||||||
|
margin: 0.35in;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.no-print { display: none !important; }
|
.no-print { display: none !important; }
|
||||||
.print-only { display: block !important; }
|
.print-only { display: block !important; }
|
||||||
.screen-only { display: none !important; }
|
.screen-only { display: none !important; }
|
||||||
|
|
||||||
body { font-size: 11px; }
|
body { font-size: 11px; }
|
||||||
.container-fluid { padding: 0 !important; }
|
.container-fluid { padding: 0 !important; }
|
||||||
h2, h3 { font-size: 13px; margin-bottom: .3rem; }
|
h2, h3 { font-size: 13px; margin-bottom: .3rem; }
|
||||||
.print-page-break { break-before: page; }
|
.print-page-break { break-before: page; }
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
||||||
table th, table td { border: 1px solid #bbb; padding: 3px 5px; }
|
table th, table td { border: 1px solid #bbb; padding: 3px 5px; }
|
||||||
table thead { background: #333 !important; color: #fff !important;
|
table thead {
|
||||||
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
background: #333 !important;
|
||||||
table thead th { position: static !important; top: auto !important; box-shadow: none !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;
|
||||||
|
}
|
||||||
|
|
||||||
.s-confirmed { color: #198754; font-weight: bold; }
|
.s-confirmed { color: #198754; font-weight: bold; }
|
||||||
.s-surprise { color: #0d6efd; font-weight: bold; }
|
.s-surprise { color: #0d6efd; font-weight: bold; }
|
||||||
.s-missed { color: #fd7e14; font-weight: bold; }
|
.s-missed { color: #fd7e14; font-weight: bold; }
|
||||||
|
|
||||||
|
body.print-stickers-mode {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode * {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode header,
|
||||||
|
body.print-stickers-mode nav,
|
||||||
|
body.print-stickers-mode aside,
|
||||||
|
body.print-stickers-mode footer,
|
||||||
|
body.print-stickers-mode .navbar,
|
||||||
|
body.print-stickers-mode .sidebar,
|
||||||
|
body.print-stickers-mode .topbar,
|
||||||
|
body.print-stickers-mode .app-header,
|
||||||
|
body.print-stickers-mode .main-header,
|
||||||
|
body.print-stickers-mode .layout-header,
|
||||||
|
body.print-stickers-mode .management-header,
|
||||||
|
body.print-stickers-mode .page-header,
|
||||||
|
body.print-stickers-mode .breadcrumb,
|
||||||
|
body.print-stickers-mode .brand,
|
||||||
|
body.print-stickers-mode .logo,
|
||||||
|
body.print-stickers-mode .header,
|
||||||
|
body.print-stickers-mode .no-print,
|
||||||
|
body.print-stickers-mode .screen-only,
|
||||||
|
body.print-stickers-mode .print-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .container-fluid > *:not(#winnerStickerPrintArea) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .container-fluid {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode #winnerStickerPrintArea {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
position: static !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode #winnerStickerPrintArea,
|
||||||
|
body.print-stickers-mode #winnerStickerPrintArea * {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-page {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(10, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 10.3in;
|
||||||
|
page-break-after: always;
|
||||||
|
break-after: page;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-page:last-child {
|
||||||
|
page-break-after: auto;
|
||||||
|
break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-cell {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-name {
|
||||||
|
display: block !important;
|
||||||
|
font-size: 20pt;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-empty {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -57,10 +216,16 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
with the <strong>year-end result</strong> based on the average of Fall & Spring scores.
|
with the <strong>year-end result</strong> based on the average of Fall & Spring scores.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<button onclick="printWithCharts()" class="btn btn-outline-secondary btn-sm">
|
<button onclick="printWithCharts()" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-printer-fill me-1"></i>Print
|
<i class="bi bi-printer-fill me-1"></i>Print
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button onclick="printWinnerStickers()" class="btn btn-warning btn-sm">
|
||||||
|
<i class="bi bi-tags-fill me-1"></i>Print Winner Stickers
|
||||||
|
</button>
|
||||||
|
|
||||||
<a href="<?= site_url('administrator/trophy?' . http_build_query(['school_year' => $selectedYear, 'percentile' => $selectedPercentile])) ?>"
|
<a href="<?= site_url('administrator/trophy?' . http_build_query(['school_year' => $selectedYear, 'percentile' => $selectedPercentile])) ?>"
|
||||||
class="btn btn-outline-secondary btn-sm">
|
class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-arrow-left me-1"></i>Back
|
<i class="bi bi-arrow-left me-1"></i>Back
|
||||||
@@ -75,18 +240,27 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<label class="form-label mb-1 small fw-semibold">School Year</label>
|
<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;">
|
<select name="school_year" class="form-select form-select-sm" style="min-width:130px;">
|
||||||
<?php foreach ($years as $yr): ?>
|
<?php foreach ($years as $yr): ?>
|
||||||
<option value="<?= esc($yr) ?>" <?= $yr === $selectedYear ? 'selected' : '' ?>><?= esc($yr) ?></option>
|
<option value="<?= esc($yr) ?>" <?= $yr === $selectedYear ? 'selected' : '' ?>>
|
||||||
|
<?= esc($yr) ?>
|
||||||
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label class="form-label mb-1 small fw-semibold">Percentile</label>
|
<label class="form-label mb-1 small fw-semibold">Percentile</label>
|
||||||
<div class="input-group input-group-sm" style="width:110px;">
|
<div class="input-group input-group-sm" style="width:110px;">
|
||||||
<input type="number" name="percentile" class="form-control"
|
<input type="number"
|
||||||
min="1" max="99" step="1" value="<?= (int)$selectedPercentile ?>">
|
name="percentile"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="99"
|
||||||
|
step="1"
|
||||||
|
value="<?= (int)$selectedPercentile ?>">
|
||||||
<span class="input-group-text">%</span>
|
<span class="input-group-text">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Apply</button>
|
<button type="submit" class="btn btn-primary btn-sm">Apply</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,19 +321,29 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<span class="fw-bold fs-6"><?= esc($cls['section_name']) ?></span>
|
<span class="fw-bold fs-6"><?= esc($cls['section_name']) ?></span>
|
||||||
<span class="badge bg-primary"><?= $cls['predicted_count'] ?> predicted</span>
|
<span class="badge bg-primary"><?= $cls['predicted_count'] ?> predicted</span>
|
||||||
<span class="badge bg-warning text-dark"><?= $cls['actual_count'] ?> actual</span>
|
<span class="badge bg-warning text-dark"><?= $cls['actual_count'] ?> actual</span>
|
||||||
|
|
||||||
<?php if ($cls['confirmed'] > 0): ?>
|
<?php if ($cls['confirmed'] > 0): ?>
|
||||||
<span class="badge bg-success"><?= $cls['confirmed'] ?> confirmed</span>
|
<span class="badge bg-success"><?= $cls['confirmed'] ?> confirmed</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($cls['surprises'] > 0): ?>
|
<?php if ($cls['surprises'] > 0): ?>
|
||||||
<span class="badge bg-info text-dark"><?= $cls['surprises'] ?> surprise<?= $cls['surprises'] > 1 ? 's' : '' ?></span>
|
<span class="badge bg-info text-dark"><?= $cls['surprises'] ?> surprise<?= $cls['surprises'] > 1 ? 's' : '' ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($cls['missed'] > 0): ?>
|
<?php if ($cls['missed'] > 0): ?>
|
||||||
<span class="badge" style="background:#fd7e14"><?= $cls['missed'] ?> missed</span>
|
<span class="badge" style="background:#fd7e14"><?= $cls['missed'] ?> missed</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-3 small align-items-center">
|
<div class="d-flex gap-3 small align-items-center">
|
||||||
<span class="text-muted">Fall ≥ <strong><?= $cls['fall_threshold'] !== null ? number_format((float)$cls['fall_threshold'], 1) : '—' ?></strong></span>
|
<span class="text-muted">
|
||||||
<span class="text-muted">Year ≥ <strong><?= $cls['year_threshold'] !== null ? number_format((float)$cls['year_threshold'], 1) : '—' ?></strong></span>
|
Fall ≥
|
||||||
|
<strong><?= $cls['fall_threshold'] !== null ? number_format((float)$cls['fall_threshold'], 1) : '—' ?></strong>
|
||||||
|
</span>
|
||||||
|
<span class="text-muted">
|
||||||
|
Year ≥
|
||||||
|
<strong><?= $cls['year_threshold'] !== null ? number_format((float)$cls['year_threshold'], 1) : '—' ?></strong>
|
||||||
|
</span>
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
Accuracy:
|
Accuracy:
|
||||||
<span class="<?= $cls['accuracy'] >= 80 ? 'text-success' : ($cls['accuracy'] >= 50 ? 'text-warning' : 'text-danger') ?>">
|
<span class="<?= $cls['accuracy'] >= 80 ? 'text-success' : ($cls['accuracy'] >= 50 ? 'text-warning' : 'text-danger') ?>">
|
||||||
@@ -168,6 +352,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-hover mb-0 align-middle" data-no-mgmt-sticky>
|
<table class="table table-sm table-hover mb-0 align-middle" data-no-mgmt-sticky>
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@@ -183,11 +368,15 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th class="text-center">Status</th>
|
<th class="text-center">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php $rank = 0; foreach ($cls['students'] as $s):
|
<?php $rank = 0; foreach ($cls['students'] as $s):
|
||||||
if ($s['status'] === 'none') continue;
|
if ($s['status'] === 'none') continue;
|
||||||
$rank++;
|
$rank++;
|
||||||
$isMale = strtolower($s['gender'] ?? '') === 'male';
|
|
||||||
|
$genderNorm = strtolower(trim((string)($s['gender'] ?? '')));
|
||||||
|
$isMale = in_array($genderNorm, ['male', 'm', 'boy'], true);
|
||||||
|
|
||||||
$rowBg = match ($s['status']) {
|
$rowBg = match ($s['status']) {
|
||||||
'confirmed' => 'table-success',
|
'confirmed' => 'table-success',
|
||||||
'surprise' => 'table-primary',
|
'surprise' => 'table-primary',
|
||||||
@@ -237,6 +426,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<div class="card-header bg-dark text-white fw-semibold py-2">
|
<div class="card-header bg-dark text-white fw-semibold py-2">
|
||||||
<i class="bi bi-bar-chart-fill me-2"></i>Prediction Accuracy Summary — <?= esc($selectedYear) ?>
|
<i class="bi bi-bar-chart-fill me-2"></i>Prediction Accuracy Summary — <?= esc($selectedYear) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-bordered mb-0 align-middle" data-no-mgmt-sticky>
|
<table class="table table-sm table-bordered mb-0 align-middle" data-no-mgmt-sticky>
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
@@ -253,6 +443,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th class="text-end pe-2">Year ≥</th>
|
<th class="text-end pe-2">Year ≥</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($classResults as $cls): ?>
|
<?php foreach ($classResults as $cls): ?>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -271,6 +462,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
<tfoot class="table-secondary fw-semibold">
|
<tfoot class="table-secondary fw-semibold">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-2">Total</td>
|
<td class="ps-2">Total</td>
|
||||||
@@ -292,7 +484,6 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
|
|
||||||
<!-- Charts -->
|
<!-- Charts -->
|
||||||
<div class="row g-4 mt-1 mb-4">
|
<div class="row g-4 mt-1 mb-4">
|
||||||
<!-- Grouped bar: predicted / actual / confirmed / surprises / missed per class -->
|
|
||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-lg-8">
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="border rounded p-3 h-100">
|
||||||
<div class="small fw-semibold text-muted mb-2">
|
<div class="small fw-semibold text-muted mb-2">
|
||||||
@@ -301,7 +492,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<canvas id="chart-counts" style="max-height:280px;"></canvas>
|
<canvas id="chart-counts" style="max-height:280px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Bar: accuracy % per class + doughnut overall breakdown -->
|
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
<div class="row g-3 h-100">
|
<div class="row g-3 h-100">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -312,6 +503,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<canvas id="chart-accuracy" style="max-height:130px;"></canvas>
|
<canvas id="chart-accuracy" style="max-height:130px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="border rounded p-3">
|
<div class="border rounded p-3">
|
||||||
<div class="small fw-semibold text-muted mb-2">
|
<div class="small fw-semibold text-muted mb-2">
|
||||||
@@ -324,6 +516,93 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Winner gender summary -->
|
||||||
|
<div class="card shadow-sm mt-2 mb-4">
|
||||||
|
<div class="card-header bg-dark text-white fw-semibold py-2">
|
||||||
|
<i class="bi bi-gender-ambiguous me-2"></i>Winner Gender Breakdown
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 align-items-stretch">
|
||||||
|
<div class="col-12 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Total Winners</div>
|
||||||
|
<div class="display-6 fw-bold"><?= (int)$totalWinners ?></div>
|
||||||
|
<div class="small text-muted">Actual year-end trophy winners</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Boys</div>
|
||||||
|
<div class="display-6 fw-bold text-primary"><?= (int)$totalWinnerBoys ?></div>
|
||||||
|
<div class="fw-semibold"><?= number_format($winnerBoysPct, 1) ?>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Girls</div>
|
||||||
|
<div class="display-6 fw-bold" style="color:#E47AB0;"><?= (int)$totalWinnerGirls ?></div>
|
||||||
|
<div class="fw-semibold"><?= number_format($winnerGirlsPct, 1) ?>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Unknown / Other</div>
|
||||||
|
<div class="display-6 fw-bold text-secondary"><?= (int)$totalWinnerOther ?></div>
|
||||||
|
<div class="fw-semibold"><?= number_format($winnerOtherPct, 1) ?>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="progress" style="height:26px;">
|
||||||
|
<?php if ($totalWinners > 0): ?>
|
||||||
|
<div class="progress-bar bg-primary"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: <?= $winnerBoysPct ?>%;"
|
||||||
|
aria-valuenow="<?= $winnerBoysPct ?>"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
Boys <?= number_format($winnerBoysPct, 1) ?>%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: <?= $winnerGirlsPct ?>%; background:#E47AB0;"
|
||||||
|
aria-valuenow="<?= $winnerGirlsPct ?>"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
Girls <?= number_format($winnerGirlsPct, 1) ?>%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($totalWinnerOther > 0): ?>
|
||||||
|
<div class="progress-bar bg-secondary"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: <?= $winnerOtherPct ?>%;"
|
||||||
|
aria-valuenow="<?= $winnerOtherPct ?>"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
Other <?= number_format($winnerOtherPct, 1) ?>%
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="progress-bar bg-secondary"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 100%;"
|
||||||
|
aria-valuenow="0"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
No winners
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /screen-only -->
|
</div><!-- /screen-only -->
|
||||||
|
|
||||||
<!-- ══ PRINT VIEW ════════════════════════════════════════════════════════ -->
|
<!-- ══ PRINT VIEW ════════════════════════════════════════════════════════ -->
|
||||||
@@ -340,7 +619,6 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- All students flat table -->
|
|
||||||
<h3 style="margin-bottom:4px;">Student Detail</h3>
|
<h3 style="margin-bottom:4px;">Student Detail</h3>
|
||||||
<table data-no-mgmt-sticky>
|
<table data-no-mgmt-sticky>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -357,6 +635,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th style="text-align:center;">Status</th>
|
<th style="text-align:center;">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php
|
<?php
|
||||||
$rank = 0;
|
$rank = 0;
|
||||||
@@ -364,7 +643,8 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
foreach ($cls['students'] as $s):
|
foreach ($cls['students'] as $s):
|
||||||
if (!in_array($s['status'], ['confirmed', 'surprise'], true)) continue;
|
if (!in_array($s['status'], ['confirmed', 'surprise'], true)) continue;
|
||||||
$rank++;
|
$rank++;
|
||||||
$isMale = strtolower($s['gender'] ?? '') === 'male';
|
$genderNorm = strtolower(trim((string)($s['gender'] ?? '')));
|
||||||
|
$isMale = in_array($genderNorm, ['male', 'm', 'boy'], true);
|
||||||
$statusLabel = match ($s['status']) {
|
$statusLabel = match ($s['status']) {
|
||||||
'confirmed' => '✓ Confirmed',
|
'confirmed' => '✓ Confirmed',
|
||||||
'surprise' => '↑ Surprise',
|
'surprise' => '↑ Surprise',
|
||||||
@@ -389,7 +669,6 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Accuracy summary (new page) -->
|
|
||||||
<div class="print-page-break"></div>
|
<div class="print-page-break"></div>
|
||||||
<h3 style="margin-bottom:4px;">Prediction Accuracy Summary</h3>
|
<h3 style="margin-bottom:4px;">Prediction Accuracy Summary</h3>
|
||||||
<table data-no-mgmt-sticky style="margin-bottom:14px;">
|
<table data-no-mgmt-sticky style="margin-bottom:14px;">
|
||||||
@@ -407,6 +686,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th style="text-align:right;">Year ≥</th>
|
<th style="text-align:right;">Year ≥</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($classResults as $cls): ?>
|
<?php foreach ($classResults as $cls): ?>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -423,6 +703,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight:bold;">Total</td>
|
<td style="font-weight:bold;">Total</td>
|
||||||
@@ -438,13 +719,14 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Charts as images (populated by JS before printing) -->
|
|
||||||
<div class="print-page-break"></div>
|
<div class="print-page-break"></div>
|
||||||
<h3 style="margin-bottom:6px;">Charts</h3>
|
<h3 style="margin-bottom:6px;">Charts</h3>
|
||||||
|
|
||||||
<div style="margin-bottom:14px;">
|
<div style="margin-bottom:14px;">
|
||||||
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Trophy Counts per Class</p>
|
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Trophy Counts per Class</p>
|
||||||
<img id="print-chart-counts" style="width:100%;max-height:220px;object-fit:contain;" src="" alt="">
|
<img id="print-chart-counts" style="width:100%;max-height:220px;object-fit:contain;" src="" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;gap:16px;margin-bottom:14px;">
|
<div style="display:flex;gap:16px;margin-bottom:14px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Prediction Accuracy per Class</p>
|
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Prediction Accuracy per Class</p>
|
||||||
@@ -456,8 +738,69 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:10px;border:1px solid #bbb;padding:8px;">
|
||||||
|
<p style="font-size:11px;font-weight:bold;margin:0 0 6px;">Winner Gender Breakdown</p>
|
||||||
|
|
||||||
|
<table data-no-mgmt-sticky>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Group</th>
|
||||||
|
<th style="text-align:center;">Winners</th>
|
||||||
|
<th style="text-align:center;">Percentage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Boys</td>
|
||||||
|
<td style="text-align:center;"><?= (int)$totalWinnerBoys ?></td>
|
||||||
|
<td style="text-align:center;"><?= number_format($winnerBoysPct, 1) ?>%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Girls</td>
|
||||||
|
<td style="text-align:center;"><?= (int)$totalWinnerGirls ?></td>
|
||||||
|
<td style="text-align:center;"><?= number_format($winnerGirlsPct, 1) ?>%</td>
|
||||||
|
</tr>
|
||||||
|
<?php if ($totalWinnerOther > 0): ?>
|
||||||
|
<tr>
|
||||||
|
<td>Unknown / Other</td>
|
||||||
|
<td style="text-align:center;"><?= (int)$totalWinnerOther ?></td>
|
||||||
|
<td style="text-align:center;"><?= number_format($winnerOtherPct, 1) ?>%</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:bold;">Total Winners</td>
|
||||||
|
<td style="text-align:center;font-weight:bold;"><?= (int)$totalWinners ?></td>
|
||||||
|
<td style="text-align:center;font-weight:bold;"><?= $totalWinners > 0 ? '100.0%' : '0.0%' ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /print-only -->
|
</div><!-- /print-only -->
|
||||||
|
|
||||||
|
<!-- Winner sticker print area: names only, no header, no score, no class -->
|
||||||
|
<div id="winnerStickerPrintArea" class="winner-sticker-print-area">
|
||||||
|
<?php if (!empty($winnerStickerNames)): ?>
|
||||||
|
<?php foreach (array_chunk($winnerStickerNames, 20) as $chunk): ?>
|
||||||
|
<div class="sticker-page">
|
||||||
|
<?php foreach ($chunk as $winnerName): ?>
|
||||||
|
<div class="sticker-cell">
|
||||||
|
<div class="sticker-name"><?= esc($winnerName) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php for ($i = 0, $remaining = 20 - count($chunk); $i < $remaining; $i++): ?>
|
||||||
|
<div class="sticker-cell sticker-empty"></div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -473,6 +816,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
$cSurprise = [];
|
$cSurprise = [];
|
||||||
$cMissed = [];
|
$cMissed = [];
|
||||||
$cAccuracy = [];
|
$cAccuracy = [];
|
||||||
|
|
||||||
foreach ($classResults as $cls) {
|
foreach ($classResults as $cls) {
|
||||||
$cLabels[] = $cls['section_name'];
|
$cLabels[] = $cls['section_name'];
|
||||||
$cPredicted[] = $cls['predicted_count'];
|
$cPredicted[] = $cls['predicted_count'];
|
||||||
@@ -483,6 +827,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
$cAccuracy[] = $cls['accuracy'];
|
$cAccuracy[] = $cls['accuracy'];
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
var labels = <?= json_encode($cLabels) ?>;
|
var labels = <?= json_encode($cLabels) ?>;
|
||||||
var predicted = <?= json_encode($cPredicted) ?>;
|
var predicted = <?= json_encode($cPredicted) ?>;
|
||||||
var actual = <?= json_encode($cActual) ?>;
|
var actual = <?= json_encode($cActual) ?>;
|
||||||
@@ -491,7 +836,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
var missed = <?= json_encode($cMissed) ?>;
|
var missed = <?= json_encode($cMissed) ?>;
|
||||||
var accuracy = <?= json_encode($cAccuracy) ?>;
|
var accuracy = <?= json_encode($cAccuracy) ?>;
|
||||||
|
|
||||||
/* ── Chart 1: grouped bar – counts per class ── */
|
if (document.getElementById('chart-counts')) {
|
||||||
new Chart(document.getElementById('chart-counts'), {
|
new Chart(document.getElementById('chart-counts'), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
@@ -514,8 +859,9 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Chart 2: accuracy % per class ── */
|
if (document.getElementById('chart-accuracy')) {
|
||||||
new Chart(document.getElementById('chart-accuracy'), {
|
new Chart(document.getElementById('chart-accuracy'), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
@@ -535,15 +881,21 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
plugins: { legend: { display: false } },
|
plugins: { legend: { display: false } },
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true, max: 100,
|
beginAtZero: true,
|
||||||
ticks: { callback: function(v) { return v + '%'; } },
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(v) {
|
||||||
|
return v + '%';
|
||||||
|
}
|
||||||
|
},
|
||||||
grid: { color: '#f0f0f0' }
|
grid: { color: '#f0f0f0' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Chart 3: doughnut overall outcome breakdown ── */
|
if (document.getElementById('chart-breakdown')) {
|
||||||
new Chart(document.getElementById('chart-breakdown'), {
|
new Chart(document.getElementById('chart-breakdown'), {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
@@ -562,7 +914,9 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: function(ctx) {
|
label: function(ctx) {
|
||||||
var total = ctx.dataset.data.reduce(function(a, b) { return a + b; }, 0);
|
var total = ctx.dataset.data.reduce(function(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}, 0);
|
||||||
var pct = total > 0 ? Math.round(ctx.parsed / total * 100) : 0;
|
var pct = total > 0 ? Math.round(ctx.parsed / total * 100) : 0;
|
||||||
return ctx.label + ': ' + ctx.parsed + ' (' + pct + '%)';
|
return ctx.label + ': ' + ctx.parsed + ' (' + pct + '%)';
|
||||||
}
|
}
|
||||||
@@ -571,6 +925,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function captureCharts() {
|
function captureCharts() {
|
||||||
@@ -579,19 +934,40 @@ function captureCharts() {
|
|||||||
'chart-accuracy': 'print-chart-accuracy',
|
'chart-accuracy': 'print-chart-accuracy',
|
||||||
'chart-breakdown': 'print-chart-breakdown',
|
'chart-breakdown': 'print-chart-breakdown',
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(map).forEach(function(canvasId) {
|
Object.keys(map).forEach(function(canvasId) {
|
||||||
var canvas = document.getElementById(canvasId);
|
var canvas = document.getElementById(canvasId);
|
||||||
var img = document.getElementById(map[canvasId]);
|
var img = document.getElementById(map[canvasId]);
|
||||||
if (canvas && img) img.src = canvas.toDataURL('image/png');
|
|
||||||
|
if (canvas && img) {
|
||||||
|
img.src = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function printWithCharts() {
|
function printWithCharts() {
|
||||||
|
document.body.classList.remove('print-stickers-mode');
|
||||||
captureCharts();
|
captureCharts();
|
||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('beforeprint', captureCharts);
|
function printWinnerStickers() {
|
||||||
|
document.body.classList.add('print-stickers-mode');
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
window.print();
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.classList.remove('print-stickers-mode');
|
||||||
|
}, 1000);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeprint', function () {
|
||||||
|
if (!document.body.classList.contains('print-stickers-mode')) {
|
||||||
|
captureCharts();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user