Fix semester context, attendance rosters, and billing workflows
- load global semester helpers consistently and use date-based semester defaults - fix grading and daily attendance duplicate student/section rows - keep attendance violations scoped to the current semester by default - update invoice, refund, discount, payment, and financial aid flows - add configuration cleanup migrations for duplicate calendar/semester keys - refresh parent registration/report-card and print request handling - update related models, services, views, cron notes, and test coverage
This commit is contained in:
+79
-80
@@ -34,112 +34,104 @@
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="d-flex justify-content-end mb-2 gap-2 flex-wrap">
|
||||
<form method="post" action="/refunds/recalculateOverpayments" class="d-flex gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-outline-primary btn-sm" type="submit" title="Current school year">
|
||||
Recalculate Overpayments (This Year)
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/refunds/recalculateOverpayments" class="d-flex gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<input type="text" name="invoice_number" class="form-control form-control-sm" placeholder="Invoice # (e.g., INV-...)" style="min-width: 260px;">
|
||||
<button class="btn btn-outline-secondary btn-sm" type="submit" title="Recalc a specific invoice across any year">
|
||||
Recalc Specific Invoice
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="refundsTable" class="table table-bordered table-striped align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>School ID</th>
|
||||
<th>Parent</th>
|
||||
<th>Request</th>
|
||||
<th>Term</th>
|
||||
<th>Invoice ID</th>
|
||||
<th class="text-end">Refund Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Invoice #</th>
|
||||
<th>Requested</th>
|
||||
<th>Approved</th>
|
||||
<th>Approved By</th>
|
||||
<th>Refunded</th>
|
||||
<th>Method</th>
|
||||
<th>Check #</th>
|
||||
<th>Check File</th>
|
||||
<th class="text-end">Paid Amount</th>
|
||||
<th class="text-end">Source Available</th>
|
||||
<th class="text-end">Parent Available</th>
|
||||
<th>Status</th>
|
||||
<th>Refund Details</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($refunds as $r): ?>
|
||||
<?php
|
||||
// Friendly badges
|
||||
$req = strtolower((string)($r['request'] ?? ''));
|
||||
$reqBadgeClass = [
|
||||
'tuition' => 'primary',
|
||||
'overpayment' => 'success',
|
||||
'extra' => 'info',
|
||||
'duplicate' => 'warning',
|
||||
][$req] ?? 'secondary';
|
||||
|
||||
// Date formatting (keep raw if null)
|
||||
$fmt = function($dt) {
|
||||
if (empty($dt) || $dt === '0000-00-00 00:00:00') return '-';
|
||||
// show date only; assumes DB is UTC
|
||||
return local_date($dt, 'm-d-Y');
|
||||
};
|
||||
$statusRaw = (string)($r['status'] ?? '');
|
||||
$statusKey = strtolower(str_replace(' ', '_', trim($statusRaw)));
|
||||
if ($statusKey === 'requested') {
|
||||
$statusKey = 'pending';
|
||||
} elseif ($statusKey === 'partially_paid') {
|
||||
$statusKey = 'partial';
|
||||
}
|
||||
$statusLabel = [
|
||||
'pending' => 'Pending',
|
||||
'approved' => 'Approved',
|
||||
'rejected' => 'Rejected',
|
||||
'partial' => 'Partial',
|
||||
'paid' => 'Paid',
|
||||
][$statusKey] ?? ($statusRaw !== '' ? $statusRaw : 'Pending');
|
||||
$statusClass = [
|
||||
'pending' => 'warning text-dark',
|
||||
'approved' => 'primary',
|
||||
'rejected' => 'danger',
|
||||
'partial' => 'info text-dark',
|
||||
'paid' => 'success',
|
||||
][$statusKey] ?? 'secondary';
|
||||
$refundAmount = (float)($r['refund_amount'] ?? 0);
|
||||
$paidAmount = (float)($r['refund_paid_amount'] ?? 0);
|
||||
$remainingAmount = max(0, $refundAmount - $paidAmount);
|
||||
$hasSource = !empty($r['source_type']) && !empty($r['source_id']);
|
||||
$canApprove = $statusKey === 'pending' && $refundAmount > 0 && $hasSource;
|
||||
$canReject = $statusKey === 'pending';
|
||||
$canRecord = in_array($statusKey, ['approved', 'partial'], true) && $remainingAmount > 0;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($r['school_id']) ?></td>
|
||||
<td><?= esc(($r['firstname'] ?? '').' '.($r['lastname'] ?? '')) ?></td>
|
||||
<td><?= esc($r['invoice_number'] ?? '-') ?></td>
|
||||
<td>
|
||||
<?php if ($req): ?>
|
||||
<span class="badge bg-<?= $reqBadgeClass ?> text-uppercase"><?= esc($req) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<div class="fw-semibold">$<?= esc(number_format($refundAmount, 2)) ?></div>
|
||||
<div class="text-muted small"><?= esc($fmt($r['requested_at'] ?? null)) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><span class="badge bg-<?= esc($statusClass) ?>"><?= esc($statusLabel) ?></span></div>
|
||||
<?php if (!empty($r['approved_at']) && $r['approved_at'] !== '0000-00-00 00:00:00'): ?>
|
||||
<div class="text-muted small mt-1">
|
||||
<?= esc($fmt($r['approved_at'])) ?>
|
||||
<?php if (!empty($r['approved_by_name']) && $r['approved_by_name'] !== '-'): ?>
|
||||
by <?= esc($r['approved_by_name']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc(($r['school_year'] ?? '-'). ' / ' . ($r['semester'] ?? '-')) ?></td>
|
||||
<td><?= esc($r['invoice_id'] ?? '-') ?></td>
|
||||
<td class="text-end">$<?= esc(number_format((float)$r['refund_amount'], 2)) ?></td>
|
||||
<td><?= esc($r['status']) ?></td>
|
||||
<td><?= esc($fmt($r['requested_at'] ?? null)) ?></td>
|
||||
<td><?= esc($fmt($r['approved_at'] ?? null)) ?></td>
|
||||
<td><?= esc($r['approved_by_name'] ?? '-') ?></td>
|
||||
<td><?= esc($fmt($r['refunded_at'] ?? null)) ?></td>
|
||||
<td><?= esc($r['refund_method'] ?? '-') ?></td>
|
||||
<td><?= esc($r['check_nbr'] ?? '-') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($r['check_file'])): ?>
|
||||
<a href="<?= base_url('refunds/file/' . (int) $r['id'] . '/inline') ?>" target="_blank">View</a>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
<div><span class="text-muted small">Check #:</span> <?= esc($r['check_nbr'] ?? '-') ?></div>
|
||||
<div>
|
||||
<span class="text-muted small">Check File:</span>
|
||||
<?php if (!empty($r['check_file'])): ?>
|
||||
<a href="<?= base_url('refunds/file/' . (int) $r['id'] . '/inline') ?>" target="_blank">View</a>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div><span class="text-muted small">Paid:</span> $<?= esc(number_format($paidAmount, 2)) ?></div>
|
||||
</td>
|
||||
<td class="text-end">$<?= esc(number_format((float)$r['refund_paid_amount'], 2)) ?></td>
|
||||
<td class="text-end">
|
||||
<?= $r['available_refundable_credit'] === null ? '-' : '$' . esc(number_format((float)$r['available_refundable_credit'], 2)) ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?= isset($r['parent_available_refundable_credit']) ? '$' . esc(number_format((float)$r['parent_available_refundable_credit'], 2)) : '-' ?>
|
||||
</td>
|
||||
<td class="d-flex gap-2">
|
||||
<td>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-success btn-sm"
|
||||
onclick="handleRecordRefundClick(<?= (int)$r['id'] ?>, '<?= esc($r['status']) ?>', <?= (float)$r['refund_amount'] ?>)">
|
||||
<?= $canRecord ? '' : 'disabled' ?>
|
||||
onclick="handleRecordRefundClick(<?= (int)$r['id'] ?>, '<?= esc($statusLabel) ?>', <?= $remainingAmount ?>)">
|
||||
Record
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm"
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Approved', <?= (float)$r['refund_amount'] ?>)">
|
||||
<?= $canApprove ? '' : 'disabled' ?>
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Approved', <?= $refundAmount ?>)">
|
||||
Approve
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm"
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Rejected', <?= (float)$r['refund_amount'] ?>)">
|
||||
<?= $canReject ? '' : 'disabled' ?>
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Rejected', <?= max($refundAmount, 0.01) ?>)">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -231,7 +223,7 @@ $(function () {
|
||||
// DataTable
|
||||
$('#refundsTable').DataTable({
|
||||
pageLength: 25,
|
||||
order: [[7, 'desc']], // order by Requested desc
|
||||
order: [[2, 'desc']], // order by Requested desc
|
||||
scrollX: true,
|
||||
autoWidth: false,
|
||||
});
|
||||
@@ -277,21 +269,27 @@ $(function () {
|
||||
});
|
||||
|
||||
// ---- Actions ----
|
||||
function normalizeRefundStatus(status) {
|
||||
return String(status || '').trim().toLowerCase().replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
function handleRecordRefundClick(refundId, status, amount) {
|
||||
if (!['Approved','Partial'].includes(status)) {
|
||||
alert('⚠ Refund must be Approved (or Partial) before recording a payout.');
|
||||
const normalized = normalizeRefundStatus(status);
|
||||
if (!['approved','partial','partially_paid'].includes(normalized)) {
|
||||
alert('Refund must be approved or partial before recording a payout.');
|
||||
return;
|
||||
}
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
alert('⚠ Refund amount not set.');
|
||||
alert('Refund amount is not set.');
|
||||
return;
|
||||
}
|
||||
showPaymentModal(refundId);
|
||||
showPaymentModal(refundId, amount);
|
||||
}
|
||||
|
||||
function handleStatusClick(refundId, status, amount) {
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
alert('⚠ Refund amount not set.');
|
||||
const normalized = normalizeRefundStatus(status);
|
||||
if (normalized !== 'rejected' && (!amount || parseFloat(amount) <= 0)) {
|
||||
alert('Refund amount is not set.');
|
||||
return;
|
||||
}
|
||||
showStatusModal(refundId, status);
|
||||
@@ -308,7 +306,7 @@ function showStatusModal(refundId, status) {
|
||||
$('#statusModal').modal('show');
|
||||
}
|
||||
|
||||
function showPaymentModal(refundId) {
|
||||
function showPaymentModal(refundId, amount) {
|
||||
$('#statusForm').addClass('d-none');
|
||||
$('#paymentForm').removeClass('d-none');
|
||||
|
||||
@@ -318,7 +316,8 @@ function showPaymentModal(refundId) {
|
||||
? crypto.randomUUID()
|
||||
: ('refund-' + refundId + '-' + Date.now() + '-' + Math.random().toString(16).slice(2))
|
||||
);
|
||||
$('#paidAmount').val('');
|
||||
$('#paidAmount').val(Number(amount || 0).toFixed(2));
|
||||
$('#paidAmount').attr('max', Number(amount || 0).toFixed(2));
|
||||
$('#paymentMethod').val('').trigger('change');
|
||||
$('#checkDetails').addClass('d-none');
|
||||
$('#checkNumber').val('');
|
||||
|
||||
Reference in New Issue
Block a user