@@ -6,7 +6,7 @@
|
||||
<div class="d-flex gap-2 mb-3 justify-content-end">
|
||||
<div>
|
||||
<a href="/discount/create" class="btn btn-primary">Create New Voucher</a>
|
||||
<a href="/discount/apply" class="btn btn-success">Apply Voucher to Parents</a>
|
||||
<a href="/discount/voucher-form" class="btn btn-success">Apply Voucher to Parents</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -89,20 +89,12 @@
|
||||
$disc = $discMatches ? reset($discMatches) : null;
|
||||
$discounted = (float)($disc['discount_amount'] ?? 0);
|
||||
$totalAmount = (float)($inv['total_amount'] ?? 0);
|
||||
// Recompute balance for display: total - discount - paid - refunded
|
||||
$balanceCalc = (float)$totalAmount - (float)$discounted - (float)$paid - (float)$refunded;
|
||||
if ($balanceCalc < 0) $balanceCalc = 0.0;
|
||||
$balance = $balanceCalc;
|
||||
if ($balanceCalc <= 0.00001) {
|
||||
$status = 'Paid';
|
||||
$statusClass = 'bg-success';
|
||||
} elseif ($paid > 0.00001) {
|
||||
$status = 'Partially Paid';
|
||||
$statusClass = 'bg-warning text-dark';
|
||||
} else {
|
||||
$status = 'Unpaid';
|
||||
$statusClass = 'bg-danger';
|
||||
}
|
||||
$balance = (float)($inv['balance'] ?? 0);
|
||||
$status = (string)($inv['status'] ?? 'unpaid');
|
||||
$statusClass = $status === 'paid'
|
||||
? 'bg-success'
|
||||
: ($status === 'partially_paid' ? 'bg-warning text-dark' : 'bg-danger');
|
||||
$statusLabel = str_replace('_', ' ', $status);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($inv['invoice_number']) ?></td>
|
||||
@@ -125,7 +117,7 @@
|
||||
<td data-order="<?= $balance ?>">$<?= number_format($balance, 2) ?></td>
|
||||
<td data-order="<?= $refunded ?>">$<?= number_format($refunded, 2) ?></td>
|
||||
<td data-order="<?= $discounted ?>">$<?= number_format($discounted, 2) ?></td>
|
||||
<td data-order="<?= esc($status) ?>"><span class="badge <?= esc($statusClass) ?>"><?= esc($status) ?></span></td>
|
||||
<td data-order="<?= esc($status) ?>"><span class="badge <?= esc($statusClass) ?>"><?= esc($statusLabel) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
@@ -375,17 +367,12 @@
|
||||
const refunded = refundsMap[inv.id] || 0;
|
||||
const discounted = discountsMap[inv.id] || 0;
|
||||
const total = Number(inv.total_amount || 0);
|
||||
let balance = total - paid - discounted - refunded;
|
||||
if (!Number.isFinite(balance) || balance < 0) balance = 0;
|
||||
let status = 'Unpaid';
|
||||
const balance = Number(inv.balance || 0);
|
||||
const status = String(inv.status || 'unpaid');
|
||||
let statusClass = 'bg-danger';
|
||||
if (balance <= 0.00001) {
|
||||
status = 'Paid';
|
||||
statusClass = 'bg-success';
|
||||
} else if (paid > 0.00001) {
|
||||
status = 'Partially Paid';
|
||||
statusClass = 'bg-warning text-dark';
|
||||
}
|
||||
if (status === 'paid') statusClass = 'bg-success';
|
||||
if (status === 'partially_paid') statusClass = 'bg-warning text-dark';
|
||||
const statusLabel = status.replace(/_/g, ' ');
|
||||
const tr = document.createElement('tr');
|
||||
const parentHtml = (inv.parent_id && Number(inv.parent_id) > 0) ?
|
||||
`<a href="<?= site_url('family') ?>?guardian_id=${encodeURIComponent(inv.parent_id)}" class="text-decoration-none" data-family-guardian-id="${inv.parent_id}">${inv.parent_name||''}</a>` :
|
||||
@@ -402,7 +389,7 @@
|
||||
<td data-order="${balance}">${fmt(balance)}</td>
|
||||
<td data-order="${refunded}">${fmt(refunded)}</td>
|
||||
<td data-order="${discounted}">${fmt(discounted)}</td>
|
||||
<td data-order="${status}"><span class="badge ${statusClass}">${status}</span></td>`;
|
||||
<td data-order="${status}"><span class="badge ${statusClass}">${statusLabel}</span></td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
|
||||
@@ -371,8 +371,7 @@
|
||||
<td>$<?= number_format((float)$invoice['paid_amount'], 2) ?></td>
|
||||
<td>$<?= number_format((float)($invoice['discount'] ?? 0), 2) ?></td>
|
||||
<td>$<?= number_format((float)($invoice['refund_paid'] ?? 0), 2) ?></td>
|
||||
<?php $tblBal = max(0, (float)($invoice['total_amount'] ?? 0) - (float)($invoice['paid_amount'] ?? 0) - (float)($invoice['discount'] ?? 0) - (float)($invoice['refund_paid'] ?? 0)); ?>
|
||||
<td>$<?= number_format($tblBal, 2) ?></td>
|
||||
<td>$<?= number_format((float)($invoice['balance'] ?? 0), 2) ?></td>
|
||||
<td><?= esc($invoice['status']) ?></td>
|
||||
<td><?= esc(!empty($invoice['due_date']) ? local_date($invoice['due_date'], 'm-d-Y') : '') ?></td>
|
||||
</tr>
|
||||
@@ -397,6 +396,7 @@
|
||||
<form method="post" action="<?= base_url('payment/manual_pay_update') ?>" id="addPaymentForm" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="search_term" value="<?= esc($searchTermUsedInSearch ?? '') ?>">
|
||||
<input type="hidden" name="idempotency_key" value="<?= esc(bin2hex(random_bytes(16))) ?>">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addPaymentModalLabel">Add Manual Payment</h5>
|
||||
@@ -419,17 +419,13 @@
|
||||
<option
|
||||
value="<?= esc($invoice['id']) ?>"
|
||||
data-balance="<?= esc($invoice['balance']) ?>"
|
||||
data-total="<?= esc($invoice['total_amount'] ?? $invoice['total'] ?? '') ?>"
|
||||
data-paid="<?= esc($invoice['paid_amount'] ?? 0) ?>"
|
||||
data-discount="<?= esc($invoice['discount'] ?? 0) ?>"
|
||||
data-refund="<?= esc($invoice['refund_paid'] ?? 0) ?>"
|
||||
data-display-total="<?= esc($invoice['display_total'] ?? $invoice['total_amount'] ?? '') ?>"
|
||||
data-balance-due-cents="<?= (int)($invoice['balance_due_cents'] ?? 0) ?>"
|
||||
data-customer-credit-cents="<?= (int)($invoice['customer_credit_cents'] ?? 0) ?>"
|
||||
data-next-installment="<?= (int)($invoice['next_installment'] ?? 1) ?>">
|
||||
<?php
|
||||
$uiTotal = (float)($invoice['total_amount'] ?? 0);
|
||||
$uiPaid = (float)($invoice['paid_amount'] ?? 0);
|
||||
$uiDisc = (float)($invoice['discount'] ?? 0);
|
||||
$uiRef = (float)($invoice['refund_paid'] ?? 0);
|
||||
$uiBal = max(0, $uiTotal - $uiPaid - $uiDisc - $uiRef);
|
||||
$uiBal = (float)($invoice['balance'] ?? 0);
|
||||
?>
|
||||
Invoice <?= esc($invoice['invoice_number']) ?> |
|
||||
Balance: $<?= number_format($uiBal, 2) ?> |
|
||||
@@ -615,20 +611,11 @@
|
||||
function getBalance() {
|
||||
const opt = currentOpt();
|
||||
if (!opt) return 0;
|
||||
// Compute from total - paid FIRST (more reliable if DB balance is stale)
|
||||
const totRaw = opt.getAttribute('data-total');
|
||||
const paidRaw = opt.getAttribute('data-paid');
|
||||
const discRaw = opt.getAttribute('data-discount');
|
||||
const refRaw = opt.getAttribute('data-refund');
|
||||
const tot = (totRaw !== null && totRaw !== '') ? parseFloat(totRaw) : NaN;
|
||||
const paid = (paidRaw !== null && paidRaw !== '') ? parseFloat(paidRaw) : NaN;
|
||||
const disc = (discRaw !== null && discRaw !== '') ? parseFloat(discRaw) : 0;
|
||||
const ref = (refRaw !== null && refRaw !== '') ? parseFloat(refRaw) : 0;
|
||||
if (isFinite(tot) && isFinite(paid)) {
|
||||
const computed = tot - paid - (isFinite(disc) ? disc : 0) - (isFinite(ref) ? ref : 0);
|
||||
if (isFinite(computed)) return Math.max(0, computed);
|
||||
const centsRaw = opt.getAttribute('data-balance-due-cents');
|
||||
const cents = (centsRaw !== null && centsRaw !== '') ? parseInt(centsRaw, 10) : NaN;
|
||||
if (isFinite(cents)) {
|
||||
return Math.max(0, cents / 100);
|
||||
}
|
||||
// Fallback to provided balance
|
||||
const raw = opt.getAttribute('data-balance');
|
||||
const bal = (raw !== null && raw !== '') ? parseFloat(raw) : NaN;
|
||||
return isFinite(bal) ? Math.max(0, bal) : 0;
|
||||
@@ -636,7 +623,7 @@
|
||||
|
||||
function getTotal() {
|
||||
const opt = currentOpt();
|
||||
const v = opt ? parseFloat(opt.getAttribute('data-total') || '0') : 0;
|
||||
const v = opt ? parseFloat(opt.getAttribute('data-display-total') || '0') : 0;
|
||||
return isFinite(v) ? v : 0;
|
||||
}
|
||||
|
||||
@@ -926,25 +913,7 @@
|
||||
}
|
||||
|
||||
const invoiceText = invOpt.text || ('Invoice #' + invOpt.value);
|
||||
// Derive balance: prefer total - paid - discount - refunds, else fallback to data-balance
|
||||
let balance = NaN;
|
||||
if (invOpt) {
|
||||
const rt = invOpt.getAttribute('data-total');
|
||||
const rp = invOpt.getAttribute('data-paid');
|
||||
const rd = invOpt.getAttribute('data-discount');
|
||||
const rr = invOpt.getAttribute('data-refund');
|
||||
const t = (rt !== null && rt !== '') ? parseFloat(rt) : NaN;
|
||||
const p = (rp !== null && rp !== '') ? parseFloat(rp) : NaN;
|
||||
const d = (rd !== null && rd !== '') ? parseFloat(rd) : 0;
|
||||
const r = (rr !== null && rr !== '') ? parseFloat(rr) : 0;
|
||||
if (isFinite(t) && isFinite(p)) {
|
||||
balance = t - p - (isFinite(d) ? d : 0) - (isFinite(r) ? r : 0);
|
||||
}
|
||||
if (!isFinite(balance)) {
|
||||
const rb = invOpt.getAttribute('data-balance');
|
||||
balance = (rb !== null && rb !== '') ? parseFloat(rb) : NaN;
|
||||
}
|
||||
}
|
||||
const balance = getBalance();
|
||||
const nextInst = invOpt.dataset?.nextInstallment || '';
|
||||
const instCount = $instCount() ? ($instCount().value || '') : '';
|
||||
const instAmt = $instAmt() ? ($instAmt().value || '') : '';
|
||||
@@ -1131,19 +1100,13 @@
|
||||
// Normalize initial state
|
||||
const inv = $invoice();
|
||||
if (inv && inv.options.length > 0) {
|
||||
// Prefer the first invoice whose (total - paid) > 0
|
||||
// Prefer the first invoice with a backend-provided amount due.
|
||||
let chosen = inv.selectedIndex;
|
||||
for (let i = 0; i < inv.options.length; i++) {
|
||||
const o = inv.options[i];
|
||||
const rt = o.getAttribute('data-total');
|
||||
const rp = o.getAttribute('data-paid');
|
||||
const rd = o.getAttribute('data-discount');
|
||||
const rr = o.getAttribute('data-refund');
|
||||
const t = (rt !== null && rt !== '') ? parseFloat(rt) : NaN;
|
||||
const p = (rp !== null && rp !== '') ? parseFloat(rp) : NaN;
|
||||
const d = (rd !== null && rd !== '') ? parseFloat(rd) : 0;
|
||||
const r = (rr !== null && rr !== '') ? parseFloat(rr) : 0;
|
||||
let b = (isFinite(t) && isFinite(p)) ? (t - p - (isFinite(d) ? d : 0) - (isFinite(r) ? r : 0)) : NaN;
|
||||
const centsRaw = o.getAttribute('data-balance-due-cents');
|
||||
const cents = (centsRaw !== null && centsRaw !== '') ? parseInt(centsRaw, 10) : NaN;
|
||||
let b = isFinite(cents) ? cents / 100 : NaN;
|
||||
if (!isFinite(b)) {
|
||||
const rb = o.getAttribute('data-balance');
|
||||
b = (rb !== null && rb !== '') ? parseFloat(rb) : NaN;
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
<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>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -119,6 +121,12 @@
|
||||
<?php endif; ?>
|
||||
</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">
|
||||
<button class="btn btn-success btn-sm"
|
||||
onclick="handleRecordRefundClick(<?= (int)$r['id'] ?>, '<?= esc($r['status']) ?>', <?= (float)$r['refund_amount'] ?>)">
|
||||
@@ -178,6 +186,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="refund_id" id="refundIdPayment">
|
||||
<input type="hidden" name="idempotency_key" id="refundPayoutIdempotencyKey">
|
||||
<div class="mb-3">
|
||||
<label for="paidAmount" class="form-label">Paid Amount</label>
|
||||
<input type="number" step="0.01" min="0.01" class="form-control" name="paid_amount" id="paidAmount" required>
|
||||
@@ -304,6 +313,11 @@ function showPaymentModal(refundId) {
|
||||
$('#paymentForm').removeClass('d-none');
|
||||
|
||||
$('#refundIdPayment').val(refundId);
|
||||
$('#refundPayoutIdempotencyKey').val(
|
||||
(window.crypto && crypto.randomUUID)
|
||||
? crypto.randomUUID()
|
||||
: ('refund-' + refundId + '-' + Date.now() + '-' + Math.random().toString(16).slice(2))
|
||||
);
|
||||
$('#paidAmount').val('');
|
||||
$('#paymentMethod').val('').trigger('change');
|
||||
$('#checkDetails').addClass('d-none');
|
||||
|
||||
Reference in New Issue
Block a user