230 lines
10 KiB
PHP
230 lines
10 KiB
PHP
<?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">—</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() ?>
|