599 lines
31 KiB
PHP
599 lines
31 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
||
<?= $this->section('content') ?>
|
||
|
||
<?php
|
||
$classResults = $classResults ?? [];
|
||
$selectedYear = $selectedYear ?? '';
|
||
$selectedPercentile = $selectedPercentile ?? 75;
|
||
$years = $years ?? [];
|
||
|
||
$totalStudents = array_sum(array_column($classResults, 'student_count'));
|
||
$totalPredicted = array_sum(array_column($classResults, 'predicted_count'));
|
||
$totalActual = array_sum(array_column($classResults, 'actual_count'));
|
||
$totalConfirmed = array_sum(array_column($classResults, 'confirmed'));
|
||
$totalSurprise = array_sum(array_column($classResults, 'surprises'));
|
||
$totalMissed = array_sum(array_column($classResults, 'missed'));
|
||
$overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) : ($totalActual === 0 ? 100 : 0);
|
||
?>
|
||
|
||
<style>
|
||
.status-confirmed { color:#198754; font-weight:600; }
|
||
.status-surprise { color:#0d6efd; font-weight:600; }
|
||
.status-missed { color:#fd7e14; font-weight:600; }
|
||
.status-none { color:#adb5bd; }
|
||
|
||
.print-only { display: none !important; }
|
||
|
||
@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: 13px; margin-bottom: .3rem; }
|
||
.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 5px; }
|
||
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; }
|
||
.s-confirmed { color: #198754; font-weight: bold; }
|
||
.s-surprise { color: #0d6efd; font-weight: bold; }
|
||
.s-missed { color: #fd7e14; font-weight: bold; }
|
||
}
|
||
</style>
|
||
|
||
<div class="container-fluid">
|
||
|
||
<!-- 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-1"></i>
|
||
Final vs Predicted — <?= esc($selectedYear) ?>
|
||
</h2>
|
||
<p class="text-muted mb-0">
|
||
Compares the <strong>Fall-score prediction</strong> (<?= (int)$selectedPercentile ?>th percentile)
|
||
with the <strong>year-end result</strong> based on the average of Fall & Spring scores.
|
||
</p>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<button onclick="printWithCharts()" class="btn btn-outline-secondary 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 -->
|
||
<form method="get" action="<?= site_url('administrator/trophy/final') ?>"
|
||
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 data found for the selected school year.</div>
|
||
<?php else: ?>
|
||
|
||
<!-- ══ SCREEN VIEW ═══════════════════════════════════════════════════════ -->
|
||
<div class="screen-only">
|
||
|
||
<!-- Legend -->
|
||
<div class="d-flex flex-wrap gap-3 mb-3 small">
|
||
<span><span class="badge bg-success me-1">✓ Confirmed</span> Predicted AND got a year-end trophy</span>
|
||
<span><span class="badge bg-primary me-1">↑ Surprise</span> NOT predicted, but earned a trophy</span>
|
||
<span><span class="badge bg-warning text-dark me-1">↓ Missed</span> Predicted, but fell short year-end</span>
|
||
<span><span class="badge bg-light text-muted border me-1">— None</span> No trophy either way</span>
|
||
</div>
|
||
|
||
<!-- Global stat tiles -->
|
||
<div class="row g-3 mb-4">
|
||
<?php foreach ([
|
||
[$totalPredicted, 'Predicted (Fall)', 'primary', null, $totalStudents > 0 ? round($totalPredicted / $totalStudents * 100) . '% of students' : '—'],
|
||
[$totalActual, 'Actual (Year)', 'warning', 'dark', $totalStudents > 0 ? round($totalActual / $totalStudents * 100) . '% of students' : '—'],
|
||
[$totalConfirmed, 'Confirmed', 'success', null, $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) . '% of predicted' : '—'],
|
||
[$totalSurprise, 'Surprises', 'info', 'dark', 'Not in prediction'],
|
||
[$totalMissed, 'Missed', 'orange', null, 'Were predicted'],
|
||
[$overallAccuracy,'Accuracy', 'dark', null, 'prediction rate'],
|
||
] as [$val, $label, $color, $textClass, $sub]):
|
||
$isOrange = $color === 'orange';
|
||
$bgClass = $isOrange ? '' : 'bg-' . $color;
|
||
$txClass = $textClass ? 'text-' . $textClass : 'text-white';
|
||
?>
|
||
<div class="col-6 col-sm-4 col-lg-2">
|
||
<div class="card text-center shadow-sm h-100 <?= $isOrange ? 'border-warning' : '' ?>">
|
||
<div class="card-body py-3">
|
||
<div class="fs-3 fw-bold <?= $isOrange ? 'text-warning' : 'text-' . $color ?>">
|
||
<?= $val ?><?= $label === 'Accuracy' ? '%' : '' ?>
|
||
</div>
|
||
<div class="text-muted small"><?= $label ?></div>
|
||
<span class="badge mt-1 <?= $bgClass . ' ' . $txClass ?>"
|
||
<?= $isOrange ? 'style="background:#fd7e14"' : '' ?>>
|
||
<?= $sub ?>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<!-- Per-class breakdown -->
|
||
<?php foreach ($classResults as $cls): ?>
|
||
<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:#f8f9fa;">
|
||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||
<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-warning text-dark"><?= $cls['actual_count'] ?> actual</span>
|
||
<?php if ($cls['confirmed'] > 0): ?>
|
||
<span class="badge bg-success"><?= $cls['confirmed'] ?> confirmed</span>
|
||
<?php endif; ?>
|
||
<?php if ($cls['surprises'] > 0): ?>
|
||
<span class="badge bg-info text-dark"><?= $cls['surprises'] ?> surprise<?= $cls['surprises'] > 1 ? 's' : '' ?></span>
|
||
<?php endif; ?>
|
||
<?php if ($cls['missed'] > 0): ?>
|
||
<span class="badge" style="background:#fd7e14"><?= $cls['missed'] ?> missed</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<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">Year ≥ <strong><?= $cls['year_threshold'] !== null ? number_format((float)$cls['year_threshold'], 1) : '—' ?></strong></span>
|
||
<span class="fw-semibold">
|
||
Accuracy:
|
||
<span class="<?= $cls['accuracy'] >= 80 ? 'text-success' : ($cls['accuracy'] >= 50 ? 'text-warning' : 'text-danger') ?>">
|
||
<?= $cls['accuracy'] ?>%
|
||
</span>
|
||
</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:2rem;">#</th>
|
||
<th>Name</th>
|
||
<th class="text-center" style="width:3rem;">Gender</th>
|
||
<th class="text-end">Fall</th>
|
||
<th class="text-end">Spring</th>
|
||
<th class="text-end">Year Avg</th>
|
||
<th class="text-center">Predicted</th>
|
||
<th class="text-center">Actual</th>
|
||
<th class="text-center">Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php $rank = 0; foreach ($cls['students'] as $s):
|
||
if ($s['status'] === 'none') continue;
|
||
$rank++;
|
||
$isMale = strtolower($s['gender'] ?? '') === 'male';
|
||
$rowBg = match ($s['status']) {
|
||
'confirmed' => 'table-success',
|
||
'surprise' => 'table-primary',
|
||
'missed' => 'table-warning',
|
||
default => '',
|
||
};
|
||
?>
|
||
<tr class="<?= $rowBg ?>">
|
||
<td class="ps-3 text-muted small"><?= $rank ?></td>
|
||
<td class="fw-semibold small"><?= esc($s['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"><?= $s['fall_score'] !== null ? number_format($s['fall_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
||
<td class="text-end small"><?= $s['spring_score'] !== null ? number_format($s['spring_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
||
<td class="text-end small fw-semibold"><?= $s['year_score'] !== null ? number_format($s['year_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
||
<td class="text-center small">
|
||
<?= $s['predicted']
|
||
? '<span class="badge bg-primary">Yes</span>'
|
||
: '<span class="badge bg-light text-muted border">No</span>' ?>
|
||
</td>
|
||
<td class="text-center small">
|
||
<?= $s['actual']
|
||
? '<span class="badge bg-warning text-dark">Yes</span>'
|
||
: '<span class="badge bg-light text-muted border">No</span>' ?>
|
||
</td>
|
||
<td class="text-center small">
|
||
<?php match ($s['status']) {
|
||
'confirmed' => print '<span class="badge bg-success">✓ Confirmed</span>',
|
||
'surprise' => print '<span class="badge bg-info text-dark">↑ Surprise</span>',
|
||
'missed' => print '<span class="badge" style="background:#fd7e14;color:#fff;">↓ Missed</span>',
|
||
default => print '<span class="text-muted small">—</span>',
|
||
}; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<!-- Overall accuracy summary table -->
|
||
<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-bar-chart-fill me-2"></i>Prediction Accuracy Summary — <?= esc($selectedYear) ?>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<table class="table table-sm table-bordered mb-0 align-middle" data-no-mgmt-sticky>
|
||
<thead class="table-dark">
|
||
<tr>
|
||
<th class="ps-2">Class</th>
|
||
<th class="text-center">Students</th>
|
||
<th class="text-center text-primary-emphasis">Predicted</th>
|
||
<th class="text-center text-warning-emphasis">Actual</th>
|
||
<th class="text-center text-success-emphasis">✓ Confirmed</th>
|
||
<th class="text-center" style="color:#6ea8fe">↑ Surprises</th>
|
||
<th class="text-center" style="color:#ffda6a">↓ Missed</th>
|
||
<th class="text-center">Accuracy</th>
|
||
<th class="text-end pe-2">Fall ≥</th>
|
||
<th class="text-end pe-2">Year ≥</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($classResults as $cls): ?>
|
||
<tr>
|
||
<td class="ps-2 fw-semibold small"><?= esc($cls['section_name']) ?></td>
|
||
<td class="text-center small"><?= $cls['student_count'] ?></td>
|
||
<td class="text-center small"><span class="badge bg-primary"><?= $cls['predicted_count'] ?></span></td>
|
||
<td class="text-center small"><span class="badge bg-warning text-dark"><?= $cls['actual_count'] ?></span></td>
|
||
<td class="text-center small"><span class="badge bg-success"><?= $cls['confirmed'] ?></span></td>
|
||
<td class="text-center small"><span class="badge bg-info text-dark"><?= $cls['surprises'] ?></span></td>
|
||
<td class="text-center small"><span class="badge" style="background:#fd7e14"><?= $cls['missed'] ?></span></td>
|
||
<td class="text-center small fw-semibold <?= $cls['accuracy'] >= 80 ? 'text-success' : ($cls['accuracy'] >= 50 ? 'text-warning' : 'text-danger') ?>">
|
||
<?= $cls['accuracy'] ?>%
|
||
</td>
|
||
<td class="text-end pe-2 small text-muted"><?= $cls['fall_threshold'] !== null ? number_format((float)$cls['fall_threshold'], 1) : '—' ?></td>
|
||
<td class="text-end pe-2 small text-muted"><?= $cls['year_threshold'] !== null ? number_format((float)$cls['year_threshold'], 1) : '—' ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
<tfoot class="table-secondary fw-semibold">
|
||
<tr>
|
||
<td class="ps-2">Total</td>
|
||
<td class="text-center"><?= $totalStudents ?></td>
|
||
<td class="text-center"><span class="badge bg-primary"><?= $totalPredicted ?></span></td>
|
||
<td class="text-center"><span class="badge bg-warning text-dark"><?= $totalActual ?></span></td>
|
||
<td class="text-center"><span class="badge bg-success"><?= $totalConfirmed ?></span></td>
|
||
<td class="text-center"><span class="badge bg-info text-dark"><?= $totalSurprise ?></span></td>
|
||
<td class="text-center"><span class="badge" style="background:#fd7e14"><?= $totalMissed ?></span></td>
|
||
<td class="text-center fw-semibold <?= $overallAccuracy >= 80 ? 'text-success' : ($overallAccuracy >= 50 ? 'text-warning' : 'text-danger') ?>">
|
||
<?= $overallAccuracy ?>%
|
||
</td>
|
||
<td colspan="2"></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Charts -->
|
||
<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="border rounded p-3 h-100">
|
||
<div class="small fw-semibold text-muted mb-2">
|
||
<i class="bi bi-bar-chart-fill me-1"></i>Trophy Counts per Class
|
||
</div>
|
||
<canvas id="chart-counts" style="max-height:280px;"></canvas>
|
||
</div>
|
||
</div>
|
||
<!-- Bar: accuracy % per class + doughnut overall breakdown -->
|
||
<div class="col-12 col-lg-4">
|
||
<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-bullseye me-1"></i>Prediction Accuracy per Class
|
||
</div>
|
||
<canvas id="chart-accuracy" style="max-height:130px;"></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-pie-chart-fill me-1"></i>Overall Outcome Breakdown
|
||
</div>
|
||
<canvas id="chart-breakdown" style="max-height:130px;"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /screen-only -->
|
||
|
||
<!-- ══ PRINT VIEW ════════════════════════════════════════════════════════ -->
|
||
<div class="print-only">
|
||
|
||
<div style="text-align:center;margin-bottom:10px;border-bottom:2px solid #333;padding-bottom:6px;">
|
||
<h2 style="margin:0;">Final vs Predicted — <?= esc($selectedYear) ?></h2>
|
||
<p style="margin:3px 0 0;font-size:10px;color:#555;">
|
||
<?= (int)$selectedPercentile ?>th percentile —
|
||
Predicted: <?= $totalPredicted ?> —
|
||
Actual: <?= $totalActual ?> —
|
||
Confirmed: <?= $totalConfirmed ?> —
|
||
Accuracy: <?= $overallAccuracy ?>%
|
||
</p>
|
||
</div>
|
||
|
||
<!-- All students flat table -->
|
||
<h3 style="margin-bottom:4px;">Student Detail</h3>
|
||
<table data-no-mgmt-sticky>
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Class</th>
|
||
<th>Name</th>
|
||
<th style="text-align:center;">G</th>
|
||
<th style="text-align:right;">Fall</th>
|
||
<th style="text-align:right;">Spring</th>
|
||
<th style="text-align:right;">Year Avg</th>
|
||
<th style="text-align:center;">Predicted</th>
|
||
<th style="text-align:center;">Actual</th>
|
||
<th style="text-align:center;">Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php
|
||
$rank = 0;
|
||
foreach ($classResults as $cls):
|
||
foreach ($cls['students'] as $s):
|
||
if (!in_array($s['status'], ['confirmed', 'surprise'], true)) continue;
|
||
$rank++;
|
||
$isMale = strtolower($s['gender'] ?? '') === 'male';
|
||
$statusLabel = match ($s['status']) {
|
||
'confirmed' => '✓ Confirmed',
|
||
'surprise' => '↑ Surprise',
|
||
'missed' => '↓ Missed',
|
||
default => '—',
|
||
};
|
||
$statusClass = 's-' . $s['status'];
|
||
?>
|
||
<tr>
|
||
<td style="text-align:center;"><?= $rank ?></td>
|
||
<td><?= esc($cls['section_name']) ?></td>
|
||
<td><strong><?= esc($s['name']) ?></strong></td>
|
||
<td style="text-align:center;"><?= $isMale ? 'M' : 'F' ?></td>
|
||
<td style="text-align:right;"><?= $s['fall_score'] !== null ? number_format($s['fall_score'], 1) : '—' ?></td>
|
||
<td style="text-align:right;"><?= $s['spring_score'] !== null ? number_format($s['spring_score'], 1) : '—' ?></td>
|
||
<td style="text-align:right;font-weight:bold;"><?= $s['year_score'] !== null ? number_format($s['year_score'], 1) : '—' ?></td>
|
||
<td style="text-align:center;"><?= $s['predicted'] ? 'Yes' : 'No' ?></td>
|
||
<td style="text-align:center;"><?= $s['actual'] ? 'Yes' : 'No' ?></td>
|
||
<td style="text-align:center;" class="<?= $statusClass ?>"><?= $statusLabel ?></td>
|
||
</tr>
|
||
<?php endforeach; endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- Accuracy summary (new page) -->
|
||
<div class="print-page-break"></div>
|
||
<h3 style="margin-bottom:4px;">Prediction Accuracy Summary</h3>
|
||
<table data-no-mgmt-sticky style="margin-bottom:14px;">
|
||
<thead>
|
||
<tr>
|
||
<th>Class</th>
|
||
<th style="text-align:center;">Students</th>
|
||
<th style="text-align:center;">Predicted</th>
|
||
<th style="text-align:center;">Actual</th>
|
||
<th style="text-align:center;">✓ Confirmed</th>
|
||
<th style="text-align:center;">↑ Surprises</th>
|
||
<th style="text-align:center;">↓ Missed</th>
|
||
<th style="text-align:center;">Accuracy</th>
|
||
<th style="text-align:right;">Fall ≥</th>
|
||
<th style="text-align:right;">Year ≥</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['predicted_count'] ?></td>
|
||
<td style="text-align:center;"><?= $cls['actual_count'] ?></td>
|
||
<td style="text-align:center;" class="s-confirmed"><?= $cls['confirmed'] ?></td>
|
||
<td style="text-align:center;" class="s-surprise"><?= $cls['surprises'] ?></td>
|
||
<td style="text-align:center;" class="s-missed"><?= $cls['missed'] ?></td>
|
||
<td style="text-align:center;font-weight:bold;"><?= $cls['accuracy'] ?>%</td>
|
||
<td style="text-align:right;"><?= $cls['fall_threshold'] !== null ? number_format((float)$cls['fall_threshold'], 1) : '—' ?></td>
|
||
<td style="text-align:right;"><?= $cls['year_threshold'] !== null ? number_format((float)$cls['year_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;"><?= $totalPredicted ?></td>
|
||
<td style="text-align:center;font-weight:bold;"><?= $totalActual ?></td>
|
||
<td style="text-align:center;font-weight:bold;" class="s-confirmed"><?= $totalConfirmed ?></td>
|
||
<td style="text-align:center;font-weight:bold;" class="s-surprise"><?= $totalSurprise ?></td>
|
||
<td style="text-align:center;font-weight:bold;" class="s-missed"><?= $totalMissed ?></td>
|
||
<td style="text-align:center;font-weight:bold;"><?= $overallAccuracy ?>%</td>
|
||
<td colspan="2"></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
|
||
<!-- Charts as images (populated by JS before printing) -->
|
||
<div class="print-page-break"></div>
|
||
<h3 style="margin-bottom:6px;">Charts</h3>
|
||
<div style="margin-bottom:14px;">
|
||
<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="">
|
||
</div>
|
||
<div style="display:flex;gap:16px;margin-bottom:14px;">
|
||
<div style="flex:1;">
|
||
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Prediction Accuracy per Class</p>
|
||
<img id="print-chart-accuracy" style="width:100%;max-height:160px;object-fit:contain;" src="" alt="">
|
||
</div>
|
||
<div style="flex:1;">
|
||
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Overall Outcome Breakdown</p>
|
||
<img id="print-chart-breakdown" style="width:100%;max-height:160px;object-fit:contain;" src="" alt="">
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /print-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
|
||
$cLabels = [];
|
||
$cPredicted = [];
|
||
$cActual = [];
|
||
$cConfirmed = [];
|
||
$cSurprise = [];
|
||
$cMissed = [];
|
||
$cAccuracy = [];
|
||
foreach ($classResults as $cls) {
|
||
$cLabels[] = $cls['section_name'];
|
||
$cPredicted[] = $cls['predicted_count'];
|
||
$cActual[] = $cls['actual_count'];
|
||
$cConfirmed[] = $cls['confirmed'];
|
||
$cSurprise[] = $cls['surprises'];
|
||
$cMissed[] = $cls['missed'];
|
||
$cAccuracy[] = $cls['accuracy'];
|
||
}
|
||
?>
|
||
var labels = <?= json_encode($cLabels) ?>;
|
||
var predicted = <?= json_encode($cPredicted) ?>;
|
||
var actual = <?= json_encode($cActual) ?>;
|
||
var confirmed = <?= json_encode($cConfirmed) ?>;
|
||
var surprises = <?= json_encode($cSurprise) ?>;
|
||
var missed = <?= json_encode($cMissed) ?>;
|
||
var accuracy = <?= json_encode($cAccuracy) ?>;
|
||
|
||
/* ── Chart 1: grouped bar – counts per class ── */
|
||
new Chart(document.getElementById('chart-counts'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [
|
||
{ label: 'Predicted', data: predicted, backgroundColor: '#4A90E2', borderRadius: 3 },
|
||
{ label: 'Actual', data: actual, backgroundColor: '#f0a500', borderRadius: 3 },
|
||
{ label: 'Confirmed', data: confirmed, backgroundColor: '#28a745', borderRadius: 3 },
|
||
{ label: 'Surprises', data: surprises, backgroundColor: '#17a2b8', borderRadius: 3 },
|
||
{ label: 'Missed', data: missed, backgroundColor: '#fd7e14', borderRadius: 3 },
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: true,
|
||
plugins: { legend: { position: 'bottom' } },
|
||
scales: {
|
||
x: { grid: { color: '#f0f0f0' } },
|
||
y: { beginAtZero: true, ticks: { stepSize: 1 }, grid: { color: '#f0f0f0' } }
|
||
}
|
||
}
|
||
});
|
||
|
||
/* ── Chart 2: accuracy % per class ── */
|
||
new Chart(document.getElementById('chart-accuracy'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: 'Accuracy %',
|
||
data: accuracy,
|
||
backgroundColor: accuracy.map(function(a) {
|
||
return a >= 80 ? '#28a745' : a >= 50 ? '#f0a500' : '#dc3545';
|
||
}),
|
||
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' }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
/* ── Chart 3: doughnut overall outcome breakdown ── */
|
||
new Chart(document.getElementById('chart-breakdown'), {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: ['Confirmed', 'Surprises', 'Missed'],
|
||
datasets: [{
|
||
data: [<?= $totalConfirmed ?>, <?= $totalSurprise ?>, <?= $totalMissed ?>],
|
||
backgroundColor: ['#28a745', '#17a2b8', '#fd7e14'],
|
||
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 + '%)';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
})();
|
||
|
||
function captureCharts() {
|
||
var map = {
|
||
'chart-counts': 'print-chart-counts',
|
||
'chart-accuracy': 'print-chart-accuracy',
|
||
'chart-breakdown': 'print-chart-breakdown',
|
||
};
|
||
Object.keys(map).forEach(function(canvasId) {
|
||
var canvas = document.getElementById(canvasId);
|
||
var img = document.getElementById(map[canvasId]);
|
||
if (canvas && img) img.src = canvas.toDataURL('image/png');
|
||
});
|
||
}
|
||
|
||
function printWithCharts() {
|
||
captureCharts();
|
||
window.print();
|
||
}
|
||
|
||
window.addEventListener('beforeprint', captureCharts);
|
||
</script>
|
||
<?= $this->endSection() ?>
|
||
|
||
<?= $this->endSection() ?>
|