Files
2026-05-16 13:44:12 -04:00

230 lines
10 KiB
PHP
Executable File

<?php
/** @var string $school_year */
/** @var array<string> $schoolYears */
/** @var array<int,array{parent_id:int,parent_name:string,email:string,total_invoice:float,total_balance:float,total_discount:float,total_paid:float,payment_count:int,remaining_installments:int,installment_amount:float,type:string,has_installment?:int,next_installment?:string}> $rows */
?>
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('styles') ?>
<style>
.page-header { display:flex; align-items:center; gap:12px; }
.page-header h1 { font-size: 1.5rem; margin: 0; }
.hint { color:#6c757d; font-size:.95rem; }
.badge-type { text-transform: capitalize; }
.table thead th { background: var(--mgmt-thead-bg, #f1f3f5); }
.actions { white-space: nowrap; }
.actions .btn { --bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; }
/* Disable sticky header for this table to avoid overlap */
table.no-mgmt-sticky thead th { position: static !important; }
</style>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="container-fluid py-3">
<div class="page-header mb-3">
<h1>Parents With Outstanding Balance</h1>
<div class="hint">Balance is summed across invoices for the selected school year.</div>
</div>
<?= $this->include('partials/academic_filter') ?>
<?php $flashStatus = session()->getFlashdata('status'); $flashError = session()->getFlashdata('error'); ?>
<?php if (!empty($flashStatus)): ?>
<div class="alert alert-success"><?= esc($flashStatus) ?></div>
<?php endif; ?>
<?php if (!empty($flashError)): ?>
<div class="alert alert-danger"><?= esc($flashError) ?></div>
<?php endif; ?>
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-end mb-2 gap-2">
<form method="post" action="<?= site_url('api/payments/notifications/send') ?>" onsubmit="return confirm('Send reminders to all listed parents?');">
<?= csrf_field() ?>
<input type="hidden" name="school_year" value="<?= esc($school_year) ?>">
<input type="hidden" name="return_to" value="<?= esc(site_url('payment/unpaid-parents') . '?school_year=' . urlencode($school_year)) ?>">
<button class="btn btn-primary btn-sm" type="submit">Send Reminders (All)</button>
</form>
</div>
<?php $sumEventFees = 0.0; ?>
<div class="table-responsive">
<table id="unpaidTable" class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
<th>Parent</th>
<th class="text-center">Nbr of Installements</th>
<th>Type</th>
<th class="text-end">Event Fees</th>
<th class="text-end">Invoice Amount</th>
<th class="text-end">Applied Discount</th>
<th class="text-end">Paid Amount</th>
<th class="text-center">Remaining Inst.</th>
<th class="text-end">Inst Amount</th>
<th>Installment</th>
<th class="text-end">Outstanding Balance</th>
<th>Next Installment</th>
<th class="no-sort">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="13" class="text-center text-muted py-4">No parents with outstanding balance.</td></tr>
<?php else: ?>
<?php
$sumInvoice = 0.0;
$sumPaid = 0.0;
$sumDisc = 0.0;
$sumInstAmt = 0.0;
$sumBal = 0.0;
?>
<?php foreach ($rows as $r): ?>
<tr>
<td>
<a href="#" data-family-guardian-id="<?= (int)$r['parent_id'] ?>" title="Open family card">
<?= esc($r['parent_name']) ?>
</a>
<small class="text-muted">#<?= (int)$r['parent_id'] ?></small>
</td>
<td class="text-center"><?= (int)($r['payment_count'] ?? 0) ?></td>
<td>
<?php if (($r['type'] ?? '') === 'no_payment'): ?>
<span class="badge bg-danger badge-type">no payment</span>
<?php else: ?>
<span class="badge bg-success badge-type">installment</span>
<?php endif; ?>
</td>
<?php $sumInvoice += (float)($r['total_invoice'] ?? 0); ?>
<?php $sumEventFees += (float)($r['event_fees'] ?? 0); ?>
<?php $sumPaid += (float)($r['total_paid'] ?? 0); ?>
<?php $sumDisc += (float)($r['total_discount'] ?? 0); ?>
<?php $sumInstAmt += (float)($r['installment_amount'] ?? 0); ?>
<?php $sumBal += (float)($r['total_balance'] ?? 0); ?>
<td class="text-end">$<?= number_format((float)($r['event_fees'] ?? 0), 2) ?></td>
<td class="text-end">$<?= number_format((float)($r['total_invoice'] ?? 0), 2) ?></td>
<td class="text-end text-success">-$<?= number_format((float)($r['total_discount'] ?? 0), 2) ?></td>
<td class="text-end">$<?= number_format((float)($r['total_paid'] ?? 0), 2) ?></td>
<td class="text-center"><?= (int)($r['remaining_installments'] ?? 0) ?></td>
<td class="text-end">$<?= number_format((float)($r['installment_amount'] ?? 0), 2) ?></td>
<td>
<?php if (!empty($r['has_installment'])): ?>
<span class="badge bg-success">Yes</span>
<?php else: ?>
<span class="badge bg-danger">No</span>
<?php endif; ?>
</td>
<td class="text-end">$<?= number_format((float)$r['total_balance'], 2) ?></td>
<td><?= esc($r['next_installment'] ?? '') ?></td>
<td class="actions">
<a href="#" class="btn btn-outline-secondary btn-sm" data-payments-parent-id="<?= (int)$r['parent_id'] ?>">Payments</a>
<form method="post" action="<?= site_url('api/payments/notifications/send') ?>" class="d-inline">
<?= csrf_field() ?>
<input type="hidden" name="parent_id" value="<?= (int)$r['parent_id'] ?>">
<input type="hidden" name="school_year" value="<?= esc($school_year) ?>">
<input type="hidden" name="return_to" value="<?= esc(site_url('payment/unpaid-parents') . '?school_year=' . urlencode($school_year)) ?>">
<button type="submit" class="btn btn-outline-primary btn-sm" title="Send reminder email to this parent">Send Reminder</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<?php if (!empty($rows)): ?>
<tfoot>
<tr>
<th colspan="3" class="text-end">Totals:</th>
<th class="text-end">$<?= number_format($sumEventFees, 2) ?></th>
<th class="text-end">$<?= number_format($sumInvoice, 2) ?></th>
<th class="text-end text-success">-$<?= number_format($sumDisc, 2) ?></th>
<th class="text-end">$<?= number_format($sumPaid, 2) ?></th>
<th class="text-center">&mdash;</th>
<th class="text-end">$<?= number_format($sumInstAmt, 2) ?></th>
<th></th>
<th class="text-end">$<?= number_format($sumBal, 2) ?></th>
<th></th>
<th></th>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
(function(){
var $ = window.jQuery;
var $tbl = $('#unpaidTable');
if (!$tbl.length) return;
// Try DataTables first
if ($ && $.fn && $.fn.DataTable) {
try {
$tbl.DataTable({
pageLength: 100,
order: [[9, 'desc']], // Outstanding Balance desc
columnDefs: [
{ targets: [11], orderable: false }, // Actions
{
targets: [3,4,5,7,9], // currency columns
render: function(data, type, row) {
if (type === 'sort' || type === 'type') {
var v = (data || '').toString().replace(/[^0-9.\-]/g, '');
var n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
return data;
}
}
]
});
return; // done
} catch(e) { /* fall through to simple sorter */ }
}
// Fallback: simple header click sorter (no dependencies)
try {
var table = $tbl.get(0);
var ths = table.tHead && table.tHead.rows[0] ? table.tHead.rows[0].cells : [];
var currencyIdx = {3:true,4:true,5:true,7:true,9:true};
var dateIdx = {10:true};
var noSortIdx = {11:true};
var dirState = {};
function getText(td){ return (td && (td.innerText||td.textContent)||'').trim(); }
function toNumber(s){ var n = parseFloat((s||'').replace(/[^0-9.\-]/g, '')); return isNaN(n)?0:n; }
function toDate(s){ var t = Date.parse(s); return isNaN(t)?0:t; }
function sortBy(col){
if (noSortIdx[col]) return;
var tbody = table.tBodies[0];
var rows = Array.prototype.slice.call(tbody.rows);
var asc = dirState[col] === 'desc'; // toggle
dirState[col] = asc ? 'asc' : 'desc';
rows.sort(function(a,b){
var av = getText(a.cells[col]);
var bv = getText(b.cells[col]);
var cmp;
if (currencyIdx[col]) { cmp = toNumber(av) - toNumber(bv); }
else if (dateIdx[col]) { cmp = toDate(av) - toDate(bv); }
else { cmp = av.localeCompare(bv, undefined, {numeric:true, sensitivity:'base'}); }
return asc ? cmp : -cmp;
});
rows.forEach(function(r){ tbody.appendChild(r); });
}
for (var i=0; i<ths.length; i++){
(function(idx){
ths[idx].style.cursor = noSortIdx[idx] ? 'default' : 'pointer';
ths[idx].addEventListener('click', function(){ sortBy(idx); });
})(i);
}
} catch(_) { /* ignore */ }
})();
</script>
<?= $this->endSection() ?>