fix financial issues

This commit is contained in:
root
2026-06-01 02:08:27 -04:00
parent 090cb88573
commit 6444b61416
56 changed files with 4196 additions and 1824 deletions
@@ -1,84 +0,0 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="wrapper">
<div class="content"></div>
<h2 class="text-center mt-4 mb-3">PayPal Transactions</h2>
<?= $this->include('partials/academic_filter') ?>
<!-- Export Button -->
<div class="d-flex gap-2 mb-3 justify-content-end">
<a href="<?= base_url('admin/paypal-transactions/export') . ($keyword ? '?q=' . urlencode($keyword) : '') ?>"
class="btn btn-success">
Export CSV
</a>
</div>
<!-- Search Form -->
<!--
<form method="get" action="<?= base_url('admin/paypal-transactions') ?>" class="mb-3">
<?= csrf_field(); ?>
<div class="input-group">
<input type="text" name="q" value="<?= esc($keyword) ?>" class="form-control"
placeholder="Search by transaction ID, email, order ID, or event type">
<button type="submit" class="btn btn-primary">Search</button>
<?php if ($keyword): ?>
<a href="<?= base_url('admin/paypal-transactions') ?>" class="btn btn-secondary">Clear</a>
<?php endif; ?>
</div>
</form>
-->
<!-- Transactions Table -->
<table id="myTable" class="display table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Transaction ID</th>
<th>Order ID</th>
<th>Parent School ID</th>
<th>Email</th>
<th>Amount</th>
<th>Net Amount</th>
<th>Currency</th>
<th>Status</th>
<th>Event Type</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach ($transactions as $t): ?>
<tr>
<td><?= esc($t['id']) ?></td>
<td><?= esc($t['transaction_id']) ?></td>
<td><?= esc($t['order_id']) ?></td>
<td><?= esc($t['parent_school_id']) ?></td>
<td><?= esc($t['payer_email']) ?></td>
<td>$<?= esc(number_format($t['amount'], 2)) ?></td>
<td>$<?= esc(number_format($t['net_amount'], 2)) ?></td>
<td><?= esc($t['currency']) ?></td>
<td><?= esc($t['status']) ?></td>
<td><?= esc($t['event_type']) ?></td>
<td><?= esc(local_datetime($t['created_at'], 'm-d-Y H:i')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- Pagination -->
<div class="d-flex justify-content-center">
<?= $pager->links() ?>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
$(document).ready(function() {
$('#myTable').DataTable({
"order": [
[0, "desc"]
],
"pageLength": 10
});
});
</script>
<?= $this->endSection() ?>
+149 -46
View File
@@ -15,23 +15,65 @@ $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);
$pct = static function (int $count, int $total): string {
return $total > 0 ? number_format(($count / $total) * 100, 1) . '%' : '0.0%';
};
$genderKey = static function (?string $gender): string {
$value = strtolower(trim((string)$gender));
return match (true) {
in_array($value, ['male', 'm', 'boy', 'boys'], true) => 'boys',
in_array($value, ['female', 'f', 'girl', 'girls'], true) => 'girls',
default => 'other',
};
};
$genderStats = static function (array $items) use ($genderKey): array {
$stats = ['boys' => 0, 'girls' => 0, 'other' => 0];
foreach ($items as $item) {
$stats[$genderKey($item['gender'] ?? '')]++;
}
return $stats;
};
// Winner gender breakdown.
// "Winner" means actual year-end trophy winner.
$totalWinnerBoys = 0;
$totalWinnerGirls = 0;
$totalWinnerOther = 0;
$allStudentsFlat = [];
$passStudents = [];
$trophyStudents = [];
// Sticker names.
// 2 columns x 10 rows = 20 stickers per page.
// Stickers print NAME ONLY.
$winnerStickerNames = [];
// 2 columns x 7 rows = 14 stickers per page.
// Stickers print name and class section.
$stickerColumns = 2;
$stickerRows = 7;
$stickerWidthInches = 4.0;
$stickerHeightInches = 1.33;
$stickerPrintablePageWidthInches = 8.0;
$stickerPrintablePageHeightInches = 10.5;
$stickersPerPage = $stickerColumns * $stickerRows;
$winnerStickers = [];
foreach ($classResults as $cls) {
foreach (($cls['students'] ?? []) as $s) {
$allStudentsFlat[] = $s;
if (isset($s['year_score']) && is_numeric($s['year_score']) && (float)$s['year_score'] >= 60) {
$passStudents[] = $s;
}
if (empty($s['actual'])) {
continue;
}
$trophyStudents[] = $s;
$gender = strtolower(trim((string)($s['gender'] ?? '')));
if (in_array($gender, ['male', 'm', 'boy', 'boys'], true)) {
@@ -43,9 +85,13 @@ foreach ($classResults as $cls) {
}
$name = trim((string)($s['name'] ?? ''));
$sectionName = trim((string)($cls['section_name'] ?? ''));
if ($name !== '') {
$winnerStickerNames[] = $name;
$winnerStickers[] = [
'name' => $name,
'section' => $sectionName,
];
}
}
}
@@ -55,6 +101,10 @@ $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;
$allGenderStats = $genderStats($allStudentsFlat);
$passGenderStats = $genderStats($passStudents);
$trophyGenderStats = $genderStats($trophyStudents);
$totalPass = count($passStudents);
?>
<style>
@@ -68,7 +118,7 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
@page {
size: Letter portrait;
margin: 0.35in;
margin: 0.25in;
}
@media print {
@@ -162,11 +212,18 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
body.print-stickers-mode .sticker-page {
display: grid !important;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(10, 1fr);
grid-template-columns: repeat(<?= $stickerColumns ?>, <?= rtrim(rtrim(number_format($stickerWidthInches, 2, '.', ''), '0'), '.') ?>in);
grid-template-rows: repeat(<?= $stickerRows ?>, <?= rtrim(rtrim(number_format($stickerHeightInches, 2, '.', ''), '0'), '.') ?>in);
gap: 0;
width: 100%;
height: 10.3in;
width: <?= rtrim(rtrim(number_format($stickerPrintablePageWidthInches, 2, '.', ''), '0'), '.') ?>in;
height: <?= rtrim(rtrim(number_format($stickerPrintablePageHeightInches, 2, '.', ''), '0'), '.') ?>in;
margin: 0 auto;
padding: 0;
box-sizing: border-box;
align-content: start;
justify-content: center;
page-break-inside: avoid;
break-inside: avoid-page;
page-break-after: always;
break-after: page;
}
@@ -186,16 +243,36 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
overflow: hidden;
min-height: 0;
box-sizing: border-box;
width: <?= rtrim(rtrim(number_format($stickerWidthInches, 2, '.', ''), '0'), '.') ?>in;
height: <?= rtrim(rtrim(number_format($stickerHeightInches, 2, '.', ''), '0'), '.') ?>in;
}
body.print-stickers-mode .sticker-name {
display: block !important;
font-size: 20pt;
font-size: 18pt;
font-weight: 700;
line-height: 1.1;
color: #000 !important;
}
body.print-stickers-mode .sticker-content {
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
transform: translateY(0.22in);
}
body.print-stickers-mode .sticker-section {
display: block !important;
margin-top: 0.08in;
font-size: 10pt;
font-weight: 600;
line-height: 1.1;
color: #000 !important;
}
body.print-stickers-mode .sticker-empty {
visibility: hidden !important;
}
@@ -219,7 +296,7 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
<div class="d-flex gap-2 flex-wrap">
<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 Stats
</button>
<button onclick="printWinnerStickers()" class="btn btn-warning btn-sm">
@@ -548,13 +625,6 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
</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">
@@ -577,17 +647,6 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
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"
@@ -619,6 +678,50 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
</p>
</div>
<h3 style="margin-bottom:6px;">Print Stats</h3>
<table data-no-mgmt-sticky style="margin-bottom:12px;">
<thead>
<tr>
<th>Metric</th>
<th style="text-align:center;">Count</th>
<th style="text-align:center;">Percent</th>
<th style="text-align:center;">Boys</th>
<th style="text-align:center;">Boys %</th>
<th style="text-align:center;">Girls</th>
<th style="text-align:center;">Girls %</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total Students</td>
<td style="text-align:center;"><?= $totalStudents ?></td>
<td style="text-align:center;">100.0%</td>
<td style="text-align:center;"><?= $allGenderStats['boys'] ?></td>
<td style="text-align:center;"><?= $pct($allGenderStats['boys'], $totalStudents) ?></td>
<td style="text-align:center;"><?= $allGenderStats['girls'] ?></td>
<td style="text-align:center;"><?= $pct($allGenderStats['girls'], $totalStudents) ?></td>
</tr>
<tr>
<td>Pass</td>
<td style="text-align:center;"><?= $totalPass ?></td>
<td style="text-align:center;"><?= $pct($totalPass, $totalStudents) ?></td>
<td style="text-align:center;"><?= $passGenderStats['boys'] ?></td>
<td style="text-align:center;"><?= $pct($passGenderStats['boys'], $totalPass) ?></td>
<td style="text-align:center;"><?= $passGenderStats['girls'] ?></td>
<td style="text-align:center;"><?= $pct($passGenderStats['girls'], $totalPass) ?></td>
</tr>
<tr>
<td>Trophies</td>
<td style="text-align:center;"><?= $totalWinners ?></td>
<td style="text-align:center;"><?= $pct($totalWinners, $totalStudents) ?></td>
<td style="text-align:center;"><?= $trophyGenderStats['boys'] ?></td>
<td style="text-align:center;"><?= $pct($trophyGenderStats['boys'], $totalWinners) ?></td>
<td style="text-align:center;"><?= $trophyGenderStats['girls'] ?></td>
<td style="text-align:center;"><?= $pct($trophyGenderStats['girls'], $totalWinners) ?></td>
</tr>
</tbody>
</table>
<h3 style="margin-bottom:4px;">Student Detail</h3>
<table data-no-mgmt-sticky>
<thead>
@@ -638,11 +741,15 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
<tbody>
<?php
$rank = 0;
foreach ($classResults as $cls):
foreach ($cls['students'] as $s):
if (!in_array($s['status'], ['confirmed', 'surprise'], true)) continue;
$rank++;
$classPrintRows = array_values(array_filter(
$cls['students'],
static fn (array $student): bool => in_array($student['status'], ['confirmed', 'surprise'], true)
));
$classPrintRows = array_reverse($classPrintRows);
foreach ($classPrintRows as $idx => $s):
$rank = count($classPrintRows) - $idx;
$genderNorm = strtolower(trim((string)($s['gender'] ?? '')));
$isMale = in_array($genderNorm, ['male', 'm', 'boy'], true);
$statusLabel = match ($s['status']) {
@@ -761,13 +868,6 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
<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>
@@ -784,16 +884,19 @@ $winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners)
<!-- 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): ?>
<?php if (!empty($winnerStickers)): ?>
<?php foreach (array_chunk($winnerStickers, $stickersPerPage) as $chunk): ?>
<div class="sticker-page">
<?php foreach ($chunk as $winnerName): ?>
<?php foreach ($chunk as $winnerSticker): ?>
<div class="sticker-cell">
<div class="sticker-name"><?= esc($winnerName) ?></div>
<div class="sticker-content">
<div class="sticker-name"><?= esc($winnerSticker['name']) ?></div>
<div class="sticker-section"><?= esc($winnerSticker['section']) ?></div>
</div>
</div>
<?php endforeach; ?>
<?php for ($i = 0, $remaining = 20 - count($chunk); $i < $remaining; $i++): ?>
<?php for ($i = 0, $remaining = $stickersPerPage - count($chunk); $i < $remaining; $i++): ?>
<div class="sticker-cell sticker-empty"></div>
<?php endfor; ?>
</div>
@@ -971,4 +1074,4 @@ window.addEventListener('beforeprint', function () {
</script>
<?= $this->endSection() ?>
<?= $this->endSection() ?>
<?= $this->endSection() ?>
@@ -0,0 +1,244 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
$filters = $filters ?? [];
$result = $result ?? null;
$summary = $result['summary'] ?? [];
$mode = $filters['calculator_mode'] ?? 'compare';
$fmtMoney = static fn($value): string => '$' . number_format((float) $value, 2);
$warningText = static function (array $warnings): string {
$warnings = array_values(array_filter(array_map('trim', $warnings), 'strlen'));
return $warnings === [] ? 'None' : implode(' | ', $warnings);
};
?>
<div class="container-fluid mt-4">
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
<div>
<h2 class="mt-4 mb-1">Tuition Collection Forecast</h2>
<p class="text-muted mb-0">Uses actual enrollment families from the selected school year to compare old and new projected tuition income.</p>
</div>
<?php if ($result): ?>
<?php
$exportQuery = http_build_query([
'school_year' => $result['school_year'] ?? '',
'semester' => $result['semester'] ?? '',
'calculator_mode' => $result['calculator_mode'] ?? 'compare',
'include_withdrawn_mode' => $filters['include_withdrawn_mode'] ?? 'refund_deadline',
'include_payment_pending' => !empty($filters['include_payment_pending']) ? '1' : '0',
'include_event_only' => !empty($filters['include_event_only']) ? '1' : '0',
'include_paid_invoices' => !empty($filters['include_paid_invoices']) ? '1' : '0',
'unit_price' => $filters['unit_price'] ?? '',
]);
?>
<a href="<?= site_url('administrator/tuition-forecast/export?' . $exportQuery) ?>" class="btn btn-success">
Export CSV
</a>
<?php endif; ?>
</div>
<div class="card shadow-sm mb-4">
<div class="card-body">
<form method="get" action="<?= site_url('administrator/tuition-forecast') ?>" class="row g-3 align-items-end">
<div class="col-md-3">
<label for="school_year" class="form-label">School Year</label>
<select id="school_year" name="school_year" class="form-select">
<?php foreach (($schoolYears ?? []) as $schoolYear): ?>
<option value="<?= esc($schoolYear) ?>" <?= ($filters['school_year'] ?? '') === $schoolYear ? 'selected' : '' ?>>
<?= esc($schoolYear) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label for="semester" class="form-label">Semester Filter</label>
<select id="semester" name="semester" class="form-select">
<option value="" <?= ($filters['semester'] ?? '') === '' ? 'selected' : '' ?>>All Year</option>
<?php foreach (($semesters ?? []) as $semester): ?>
<option value="<?= esc($semester) ?>" <?= ($filters['semester'] ?? '') === $semester ? 'selected' : '' ?>>
<?= esc($semester) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label for="calculator_mode" class="form-label">Calculator Mode</label>
<select id="calculator_mode" name="calculator_mode" class="form-select">
<option value="compare" <?= $mode === 'compare' ? 'selected' : '' ?>>Compare Both</option>
<option value="old" <?= $mode === 'old' ? 'selected' : '' ?>>Old Only</option>
<option value="new" <?= $mode === 'new' ? 'selected' : '' ?>>New Only</option>
</select>
</div>
<div class="col-md-2">
<label for="include_withdrawn_mode" class="form-label">Withdrawn Students</label>
<select id="include_withdrawn_mode" name="include_withdrawn_mode" class="form-select">
<option value="refund_deadline" <?= ($filters['include_withdrawn_mode'] ?? 'refund_deadline') === 'refund_deadline' ? 'selected' : '' ?>>Use Refund Deadline Rule</option>
<option value="include" <?= ($filters['include_withdrawn_mode'] ?? '') === 'include' ? 'selected' : '' ?>>Always Include</option>
<option value="exclude" <?= ($filters['include_withdrawn_mode'] ?? '') === 'exclude' ? 'selected' : '' ?>>Always Exclude</option>
</select>
</div>
<div class="col-md-3">
<label for="unit_price" class="form-label">Unit Price</label>
<input
type="number"
min="0"
step="0.01"
id="unit_price"
name="unit_price"
class="form-control"
value="<?= esc((string) ($filters['unit_price'] ?? ($summary['unit_price'] ?? ''))) ?>"
placeholder="New tuition unit price">
</div>
<div class="col-md-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="include_payment_pending" name="include_payment_pending" value="1" <?= !empty($filters['include_payment_pending']) ? 'checked' : '' ?>>
<label class="form-check-label" for="include_payment_pending">Include payment-pending students</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="include_event_only" name="include_event_only" value="1" <?= !empty($filters['include_event_only']) ? 'checked' : '' ?>>
<label class="form-check-label" for="include_event_only">Show event-only students in warnings</label>
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Run Forecast</button>
</div>
</form>
</div>
</div>
<?php if ($result): ?>
<div class="row g-3 mb-4">
<div class="col-md-2">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">Total Parents</div>
<div class="fs-4 fw-semibold"><?= esc((string) ($summary['family_count'] ?? 0)) ?></div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">Total Students</div>
<div class="fs-4 fw-semibold"><?= esc((string) ($summary['student_count'] ?? 0)) ?></div>
<div class="small text-muted">Billable: <?= esc((string) ($summary['billable_student_count'] ?? 0)) ?></div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">Old Projected Income</div>
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['old_projected_income'] ?? ($summary['old_projected_tuition'] ?? 0))) ?></div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">New Projected Income</div>
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['new_projected_income'] ?? ($summary['new_projected_tuition'] ?? 0))) ?></div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">Unit Price</div>
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['unit_price'] ?? 0)) ?></div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small">Difference</div>
<div class="fs-5 fw-semibold <?= ((float) ($summary['difference'] ?? 0)) >= 0 ? 'text-success' : 'text-danger' ?>">
<?= esc($fmtMoney($summary['difference'] ?? 0)) ?>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Parent / Family</th>
<th>Student Count</th>
<th>Billable Student Count</th>
<th>Old Tuition Total</th>
<th>New Tuition Total</th>
<th>Difference</th>
<th>Warnings</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php if (!empty($result['families'])): ?>
<?php foreach ($result['families'] as $family): ?>
<tr>
<td><?= esc($family['parent_name'] ?? '') ?></td>
<td><?= esc((string) ($family['student_count'] ?? 0)) ?></td>
<td><?= esc((string) ($family['billable_student_count'] ?? 0)) ?></td>
<td><?= esc($fmtMoney($family['old_total'] ?? 0)) ?></td>
<td><?= esc($fmtMoney($family['new_total'] ?? 0)) ?></td>
<td class="<?= ((float) ($family['difference'] ?? 0)) >= 0 ? 'text-success' : 'text-danger' ?>">
<?= esc($fmtMoney($family['difference'] ?? 0)) ?>
</td>
<td class="small"><?= esc($warningText($family['warnings'] ?? [])) ?></td>
<td>
<details>
<summary>Students</summary>
<div class="table-responsive mt-2">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Student</th>
<th>Grade</th>
<th>Billable</th>
<th>Excluded Reason</th>
<th>Old Rule</th>
<th>Old Amount</th>
<th>New Rule</th>
<th>New Amount</th>
</tr>
</thead>
<tbody>
<?php foreach (($family['student_details'] ?? []) as $detail): ?>
<tr>
<td><?= esc($detail['student_name'] ?? '') ?></td>
<td><?= esc($detail['grade_level'] ?? '') ?></td>
<td><?= !empty($detail['billable']) ? 'Yes' : 'No' ?></td>
<td><?= esc((string) ($detail['excluded_reason'] ?? '')) ?></td>
<td><?= esc((string) ($detail['old_rule'] ?? '')) ?></td>
<td><?= esc($fmtMoney($detail['old_amount'] ?? 0)) ?></td>
<td><?= esc((string) ($detail['new_rule'] ?? '')) ?></td>
<td><?= esc($fmtMoney($detail['new_amount'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</details>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="text-center text-muted">No forecastable families matched the selected filters.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?= $this->endSection() ?>