recreate project
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/** @var array<int,array> $payments */
|
||||
/** @var int $parentId */
|
||||
/** @var string $parentName */
|
||||
?>
|
||||
<div data-payments-title="<?= esc(trim(($parentName !== '' ? ($parentName . ' ') : '') . '(#' . (int)$parentId . ')')) ?>">
|
||||
<div class="p-3 border-bottom">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<strong>Payments</strong>
|
||||
<small class="text-muted ms-2">Parent <?= esc($parentName) ?> (ID #<?= (int)$parentId ?>)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Invoice</th>
|
||||
<th class="text-end">Paid</th>
|
||||
<th class="text-end">Balance</th>
|
||||
<th>Method</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($payments)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">No payment</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($payments as $p): ?>
|
||||
<tr>
|
||||
<td><?= esc(!empty($p['payment_date']) ? local_date($p['payment_date'], 'm-d-Y') : '') ?></td>
|
||||
<td>
|
||||
<?php $inv = trim((string)($p['invoice_number'] ?? '')); ?>
|
||||
<?= $inv !== '' ? esc($inv) : ('#' . (int)($p['invoice_id'] ?? 0)) ?>
|
||||
</td>
|
||||
<td class="text-end">$<?= number_format((float)($p['paid_amount'] ?? 0), 2) ?></td>
|
||||
<td class="text-end">$<?= number_format((float)($p['balance'] ?? 0), 2) ?></td>
|
||||
<td><?= esc($p['payment_method'] ?? '') ?></td>
|
||||
<td><?= esc($p['status'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<form method="post" action="<?= base_url('/discount/apply') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="student_id" value="<?= esc($studentId) ?>">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="voucher_code" class="form-control" placeholder="Enter Voucher Code" required>
|
||||
<button class="btn btn-primary" type="submit">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,271 @@
|
||||
<!-- app/Views/payment/extra_charges.php -->
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success"><?= session()->get('status') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->get('error')): ?>
|
||||
<div class="alert alert-danger"><?= session()->get('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Centered School Year filter (match compact design) -->
|
||||
<div class="d-flex justify-content-center mb-3">
|
||||
<form id="chargesYearFilter" class="d-flex align-items-center gap-3" method="get" action="<?= site_url('admin/charges') ?>">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 gap-3 flex-wrap">
|
||||
<h3 class="mb-0">
|
||||
Add & Deduct Charges
|
||||
<span class="text-muted ms-2">(<?= esc($schoolYear) ?> - <?= esc($semester) ?>)</span>
|
||||
</h3>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap">
|
||||
<label for="schoolYear" class="form-label mb-0">School year</label>
|
||||
<select id="schoolYear"
|
||||
name="school_year"
|
||||
class="form-select form-select-sm rounded-pill px-3"
|
||||
style="min-width: 180px; width: auto;">
|
||||
<?php foreach (($schoolYears ?? []) as $sy): ?>
|
||||
<option value="<?= esc($sy) ?>" <?= (string)$schoolYear === (string)$sy ? 'selected' : '' ?>>
|
||||
<?= esc($sy) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-3 justify-content-end"></div>
|
||||
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#chargeModal">
|
||||
<i class="bi bi-plus-circle"></i> Add Charge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php if (!empty($status)): ?>
|
||||
<input type="hidden" name="status" value="<?= esc($status) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($parentId)): ?>
|
||||
<input type="hidden" name="parent_id" value="<?= (int)$parentId ?>">
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body table-responsive">
|
||||
<?php $pageTotal = 0.0; if (!empty($rows)) { foreach ($rows as $__r) { $pageTotal += (float)($__r['amount'] ?? 0); } } ?>
|
||||
<table id="chargesTable" class="table table-bordered table-striped align-middle w-100 no-mgmt-sticky" style="table-layout:auto;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Type</th>
|
||||
<th>Title</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th>Parent</th>
|
||||
<th>Status</th>
|
||||
<th>Invoice</th>
|
||||
<th>Due</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-4">No charges</td>
|
||||
</tr>
|
||||
<?php else: foreach ($rows as $r): ?>
|
||||
<tr>
|
||||
<td>#<?= (int)$r['id'] ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$ctype = strtolower((string)($r['charge_type'] ?? ''));
|
||||
$isAdd = ($ctype === 'add') || ((float)($r['amount'] ?? 0) > 0);
|
||||
$isDed = ($ctype === 'deduct') || ((float)($r['amount'] ?? 0) < 0);
|
||||
$badge = $isAdd ? 'bg-success' : ($isDed ? 'bg-danger' : 'bg-secondary');
|
||||
$label = $isAdd ? 'Add' : ($isDed ? 'Deduct' : ucfirst($ctype ?: ''));
|
||||
?>
|
||||
<span class="badge <?= $badge ?>"><?= esc($label) ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= esc($r['title']) ?></div>
|
||||
<?php if (!empty($r['description'])): ?>
|
||||
<div class="small text-muted"><?= esc($r['description']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">$<?= number_format((float)$r['amount'], 2) ?></td>
|
||||
<td><?= esc($r['parent_name'] ?: ('ID: ' . (int)$r['parent_id'])) ?></td>
|
||||
<td>
|
||||
<?php $cls = ['pending' => 'bg-info', 'applied' => 'bg-success', 'void' => 'bg-danger'][$r['status']] ?? 'bg-light'; ?>
|
||||
<span class="badge <?= $cls ?>"><?= ucfirst($r['status']) ?></span>
|
||||
</td>
|
||||
<td><?= !empty($r['invoice_number']) ? esc($r['invoice_number']) : ($r['invoice_id'] ? '#' . (int)$r['invoice_id'] : '—') ?></td>
|
||||
<td><?= !empty($r['due_date']) ? esc(local_date($r['due_date'], 'm-d-Y')) : '—' ?></td>
|
||||
</tr>
|
||||
<?php endforeach;
|
||||
endif; ?>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<td colspan="3" class="text-end fw-semibold">Total Amount</td>
|
||||
<td class="text-end fw-semibold">$<?= number_format($pageTotal, 2) ?></td>
|
||||
<td colspan="4"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<?php if (isset($pager)): ?>
|
||||
<div class="mt-3"><?= $pager->links() ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Removed local sticky CSS; DataTables FixedHeader handles header pinning -->
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div class="modal fade" id="chargeModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<form class="modal-content" method="post" action="<?= site_url('admin/charges/store') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Adjust Charge</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Parent *</label>
|
||||
<select id="parentSelect" class="form-select" name="parent_id" required>
|
||||
<option value="">— Select Parent —</option>
|
||||
<?php foreach (($parents ?? []) as $p):
|
||||
$pid = (int)$p['id'];
|
||||
$label = trim(($p['lastname'] ?? '') . ', ' . ($p['firstname'] ?? ''));
|
||||
if ($label === '' || $label === ',') $label = 'Unnamed';
|
||||
?>
|
||||
<option value="<?= $pid ?>"><?= esc($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Invoice Number</label>
|
||||
<input type="text" class="form-control" id="invoiceNumberDisplay" value="" readonly>
|
||||
<input type="hidden" name="invoice_id" id="invoiceIdHidden">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" class="form-control" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Description*</label>
|
||||
<input type="text" class="form-control" name="description" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Amount *</label>
|
||||
<input type="number" step="0.01" class="form-control" name="amount" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Type *</label>
|
||||
<select class="form-select" name="charge_type" required>
|
||||
<option value="add">Add Charge</option>
|
||||
<option value="deduct">Deduct Charge</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Due Date</label>
|
||||
<input type="date" class="form-control" name="due_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary"><i class="bi bi-save"></i> Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
$(function() {
|
||||
// Auto-apply School Year when selection changes (no Filter button)
|
||||
$('#chargesYearFilter select[name="school_year"]').on('change', function() {
|
||||
this.form.submit();
|
||||
});
|
||||
|
||||
const $parent = $('#parentSelect');
|
||||
const $invId = $('#invoiceIdHidden');
|
||||
const $invNum = $('#invoiceNumberDisplay');
|
||||
|
||||
function pickDefaultInvoice(items) {
|
||||
if (!items || !items.length) return null;
|
||||
const openStatuses = new Set(['unpaid', 'partial', 'open', 'pending']);
|
||||
const open = items.filter(r => (parseFloat(r.balance ?? 0) > 0) || openStatuses.has(String(r.status || '').toLowerCase()));
|
||||
const pool = open.length ? open : items.slice();
|
||||
pool.sort((a, b) => {
|
||||
const da = Date.parse(a.issue_date || '') || 0;
|
||||
const db = Date.parse(b.issue_date || '') || 0;
|
||||
if (db !== da) return db - da;
|
||||
return (b.id || 0) - (a.id || 0);
|
||||
});
|
||||
return pool[0] || null;
|
||||
}
|
||||
|
||||
function loadInvoicesForParent(pid) {
|
||||
$invId.val('');
|
||||
$invNum.val('');
|
||||
if (!pid) return;
|
||||
|
||||
$.getJSON('<?= site_url('admin/charges/invoices') ?>', {
|
||||
parent_id: pid,
|
||||
school_year: '<?= esc($schoolYear) ?>'
|
||||
}).done(function(data) {
|
||||
const items = (data && Array.isArray(data.results)) ? data.results : [];
|
||||
const chosen = pickDefaultInvoice(items);
|
||||
if (chosen) {
|
||||
$invId.val(String(chosen.id));
|
||||
$invNum.val(chosen.invoice_number);
|
||||
} else {
|
||||
$invNum.val('No invoice found for selected parent');
|
||||
}
|
||||
}).fail(function(xhr) {
|
||||
console.error('Invoice fetch failed', xhr.status, xhr.responseText);
|
||||
$invNum.val('Could not load invoices (' + xhr.status + ')');
|
||||
});
|
||||
}
|
||||
|
||||
$parent.on('change', function() {
|
||||
loadInvoicesForParent($(this).val());
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Attach DataTables FixedHeader for sticky header reliability
|
||||
(function(){
|
||||
function getFixedHeaderOffset(){
|
||||
var total = 0;
|
||||
try {
|
||||
var sels = ['header.navbar.sticky-top','header.navbar.fixed-top','#navbarManagement.navbar.sticky-top','#navbarManagement.navbar.fixed-top'];
|
||||
var seen = [];
|
||||
sels.forEach(function(s){ document.querySelectorAll(s).forEach(function(el){ if (seen.indexOf(el) === -1) seen.push(el); }); });
|
||||
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(function(el){ if (seen.indexOf(el) === -1) seen.push(el); });
|
||||
seen.forEach(function(el){ var h = el.offsetHeight || (el.getBoundingClientRect && el.getBoundingClientRect().height) || 0; total += Math.max(0, Math.round(h)); });
|
||||
} catch(_) {}
|
||||
return total;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
if (window.jQuery && jQuery.fn && jQuery.fn.DataTable) {
|
||||
try {
|
||||
jQuery('#chargesTable').DataTable({ paging:false, searching:false, info:false, order: [], fixedHeader: { header: true, headerOffset: getFixedHeaderOffset() } });
|
||||
} catch(_) {}
|
||||
}
|
||||
|
||||
// No polyfill needed when DataTables FixedHeader is active
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,592 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<h2 class="text-center mt-4 mb-3">Detailed Financial Report</h2>
|
||||
|
||||
<style>
|
||||
/* Keep table headers visible while scrolling the page */
|
||||
#invoicesTable thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
/* above table body cells */
|
||||
background-color: #f8f9fa;
|
||||
/* Bootstrap light header bg */
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Top toolbar: put all filters on one line -->
|
||||
<form id="financialFilterForm" method="get" class="d-flex align-items-end gap-3 flex-nowrap justify-content-center mb-3 w-100 overflow-auto" style="white-space: nowrap;">
|
||||
<div class="d-flex align-items-center gap-2 flex-nowrap">
|
||||
<label class="form-label mb-0 me-2">School year</label>
|
||||
<select name="school_year" class="form-select form-select-sm rounded-pill px-3" style="min-width: 180px;">
|
||||
<?php foreach (($schoolYears ?? []) as $sy): ?>
|
||||
<option value="<?= esc($sy) ?>" <?= (string)($selectedYear ?? '') === (string)$sy ? 'selected' : '' ?>><?= esc($sy) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex align-items-end gap-2 flex-nowrap">
|
||||
<div class="d-flex flex-column">
|
||||
<label class="form-label">Date From:</label>
|
||||
<input type="date" name="date_from" class="form-control" value="<?= esc($_GET['date_from'] ?? ($dateFrom ?? '')) ?>">
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<label class="form-label">Date To:</label>
|
||||
<input type="date" name="date_to" class="form-control" value="<?= esc($_GET['date_to'] ?? ($dateTo ?? '')) ?>">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary ms-2">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Loading & error placeholders -->
|
||||
<div id="reportLoading" class="text-center my-4 d-none">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<div class="mt-2">Loading report…</div>
|
||||
<div class="text-muted small" id="reportLastRef"></div>
|
||||
</div>
|
||||
<div id="reportError" class="alert alert-danger d-none">
|
||||
Failed to load report. Please try again.
|
||||
<a href="#" id="reportRetry" class="alert-link ms-2">Retry</a>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-3 justify-content-end flex-nowrap w-100 overflow-auto" style="white-space: nowrap;">
|
||||
<a id="downloadCsvLink" href="<?= base_url('payment/download_csv') ?>" class="btn btn-success">Download CSV</a>
|
||||
<button class="btn btn-primary" onclick="window.print()">Print Report</button>
|
||||
<a id="summaryReportLink" href="<?= base_url('financial-report/financialReportSummary') ?>" class="btn btn-info">Display Summary Report</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="invoicesTable" class="table table-bordered table-striped align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Parent</th>
|
||||
<th>School Year</th>
|
||||
<th>Total</th>
|
||||
<th>Paid</th>
|
||||
<th>Cash</th>
|
||||
<th>Credit</th>
|
||||
<th>Check</th>
|
||||
<th>Balance</th>
|
||||
<th>Refund</th>
|
||||
<th>Discount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($invoices ?? [])): ?>
|
||||
<?php
|
||||
$breakdowns = $paymentBreakdown ?? [];
|
||||
foreach ($invoices as $inv):
|
||||
$payMatches = array_filter($payments ?? [], fn($p) => isset($p['invoice_id']) && (int)$p['invoice_id'] === (int)$inv['id']);
|
||||
$pay = $payMatches ? reset($payMatches) : null;
|
||||
$paid = (float)($pay['paid_amount'] ?? 0);
|
||||
$bd = $breakdowns[(int)$inv['id']] ?? [];
|
||||
$cash = (float)($bd['cash'] ?? 0);
|
||||
$cred = (float)($bd['credit'] ?? 0);
|
||||
$chk = (float)($bd['check'] ?? 0);
|
||||
$refMatches = array_filter($refunds ?? [], fn($r) => isset($r['invoice_id']) && (int)$r['invoice_id'] === (int)$inv['id']);
|
||||
$ref = $refMatches ? reset($refMatches) : null;
|
||||
$refunded = (float)($ref['total_refunded'] ?? 0);
|
||||
$discMatches = array_filter($discounts ?? [], fn($d) => isset($d['invoice_id']) && (int)$d['invoice_id'] === (int)$inv['id']);
|
||||
$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;
|
||||
$status = ($balanceCalc === 0.0) ? 'Paid' : 'Unpaid';
|
||||
$statusClass = ($status === 'Paid') ? 'bg-success' : 'bg-danger';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($inv['invoice_number']) ?></td>
|
||||
<td>
|
||||
<?php $pid = (int)($inv['parent_id'] ?? 0); ?>
|
||||
<?php if ($pid): ?>
|
||||
<a href="<?= site_url('family') ?>?guardian_id=<?= (int)$pid ?>" class="text-decoration-none" data-family-guardian-id="<?= $pid ?>">
|
||||
<?= esc($inv['parent_name']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?= esc($inv['parent_name']) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc($inv['school_year']) ?></td>
|
||||
<td data-order="<?= $totalAmount ?>">$<?= number_format($totalAmount, 2) ?></td>
|
||||
<td data-order="<?= $paid ?>">$<?= number_format($paid, 2) ?></td>
|
||||
<td data-order="<?= $cash ?>">$<?= number_format($cash, 2) ?></td>
|
||||
<td data-order="<?= $cred ?>">$<?= number_format($cred, 2) ?></td>
|
||||
<td data-order="<?= $chk ?>">$<?= number_format($chk, 2) ?></td>
|
||||
<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>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5">Payments by Method (Filtered)</h3>
|
||||
<div class="table-responsive">
|
||||
<table id="summaryTable" class="table table-bordered align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cash</th>
|
||||
<th>Credit</th>
|
||||
<th>Check</th>
|
||||
<th>Grand Total (All Payments)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$pt = $paymentTotals ?? [];
|
||||
$totalCash = (float)($pt['total_cash'] ?? 0);
|
||||
$totalCredit = (float)($pt['total_credit'] ?? 0);
|
||||
$totalCheck = (float)($pt['total_check'] ?? 0);
|
||||
$grandTotal = (float)($pt['total_all'] ?? ($totalCash + $totalCredit + $totalCheck));
|
||||
?>
|
||||
<tr>
|
||||
<td class="sum-cash" data-order="<?= $totalCash ?>">$<?= number_format($totalCash, 2) ?></td>
|
||||
<td class="sum-credit" data-order="<?= $totalCredit ?>">$<?= number_format($totalCredit, 2) ?></td>
|
||||
<td class="sum-check" data-order="<?= $totalCheck ?>">$<?= number_format($totalCheck, 2) ?></td>
|
||||
<td class="sum-all fw-bold" data-order="<?= $grandTotal ?>">$<?= number_format($grandTotal, 2) ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5">Expenses Summary</h3>
|
||||
<div class="table-responsive">
|
||||
<table id="expensesTable" class="table table-bordered align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Total Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($expenses ?? [])): foreach ($expenses as $exp): $expAmt = (float)($exp['total_amount'] ?? 0); ?>
|
||||
<tr>
|
||||
<td><?= esc($exp['category']) ?></td>
|
||||
<td data-order="<?= $expAmt ?>">$<?= number_format($expAmt, 2) ?></td>
|
||||
</tr>
|
||||
<?php endforeach;
|
||||
endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5">Reimbursements Summary</h3>
|
||||
<div class="table-responsive">
|
||||
<table id="reimbTable" class="table table-bordered align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Total Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($reimbursements ?? [])): foreach ($reimbursements as $r): $rAmt = (float)($r['total_amount'] ?? 0); ?>
|
||||
<tr>
|
||||
<td><?= esc($r['status']) ?></td>
|
||||
<td data-order="<?= $rAmt ?>">$<?= number_format($rAmt, 2) ?></td>
|
||||
</tr>
|
||||
<?php endforeach;
|
||||
endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Fetch data from API and hydrate tables
|
||||
function fmt(n) {
|
||||
return '$' + Number(n || 0).toFixed(2);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.valueOf())) {
|
||||
return '';
|
||||
}
|
||||
const pad = (num) => String(num).padStart(2, '0');
|
||||
const datePart = `${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${date.getFullYear()}`;
|
||||
const timePart = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
return `${datePart} ${timePart}`;
|
||||
}
|
||||
|
||||
function qs(sel) {
|
||||
return document.querySelector(sel);
|
||||
}
|
||||
|
||||
function qsa(sel) {
|
||||
return Array.from(document.querySelectorAll(sel));
|
||||
}
|
||||
|
||||
// --- DataTables FixedHeader support (lazy-load assets if missing) ---
|
||||
function loadScript(src, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (id && document.getElementById(id)) return resolve();
|
||||
const s = document.createElement('script');
|
||||
if (id) s.id = id;
|
||||
s.src = src;
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
function loadCss(href, id) {
|
||||
return new Promise((resolve) => {
|
||||
if (id && document.getElementById(id)) return resolve();
|
||||
const l = document.createElement('link');
|
||||
if (id) l.id = id;
|
||||
l.rel = 'stylesheet';
|
||||
l.href = href;
|
||||
l.onload = resolve;
|
||||
document.head.appendChild(l);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDtFixedHeaderAssets() {
|
||||
const tasks = [];
|
||||
const needJQ = !window.jQuery;
|
||||
if (needJQ) tasks.push(loadScript('https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js', 'jq-core'));
|
||||
|
||||
// Core DataTables + Bootstrap 5 integration
|
||||
if (!window.jQuery || !jQuery.fn || !jQuery.fn.DataTable) {
|
||||
tasks.push(loadScript('https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js', 'dt-core'));
|
||||
tasks.push(loadScript('https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js', 'dt-bs5'));
|
||||
// base CSS if not already present (id optional)
|
||||
tasks.push(loadCss('https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css', 'dt-bs5-css'));
|
||||
}
|
||||
|
||||
// FixedHeader extension (JS + Bootstrap 5 CSS)
|
||||
// Only load if plugin missing
|
||||
const needFH = !(window.jQuery && jQuery.fn && jQuery.fn.dataTable && jQuery.fn.dataTable.FixedHeader);
|
||||
if (needFH) {
|
||||
tasks.push(loadScript('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader@3.4.0/js/dataTables.fixedHeader.min.js', 'dt-fixedheader'));
|
||||
tasks.push(loadCss('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader-bs5@3.4.0/css/fixedHeader.bootstrap5.min.css', 'dt-fixedheader-css'));
|
||||
}
|
||||
|
||||
return Promise.all(tasks).catch(() => {});
|
||||
}
|
||||
|
||||
// Compute offset if a fixed/sticky top navbar exists to prevent overlap
|
||||
function getFixedHeaderOffset() {
|
||||
let total = 0;
|
||||
const stack = [];
|
||||
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top');
|
||||
if (header) stack.push(header);
|
||||
const mgmt = document.getElementById('navbarManagement');
|
||||
if (mgmt && (mgmt.classList.contains('sticky-top') || mgmt.classList.contains('fixed-top'))) stack.push(mgmt);
|
||||
// Include any other sticky/fixed navbars at top (avoid duplicates)
|
||||
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(el => { if (!stack.includes(el)) stack.push(el); });
|
||||
stack.forEach(el => { const h = el.offsetHeight || el.getBoundingClientRect().height || 0; total += Math.max(0, Math.round(h)); });
|
||||
return total;
|
||||
}
|
||||
|
||||
function buildParams() {
|
||||
const p = new URLSearchParams();
|
||||
const f = document.getElementById('financialFilterForm');
|
||||
if (f) {
|
||||
new FormData(f).forEach((v, k) => p.set(k, v));
|
||||
}
|
||||
return p.toString();
|
||||
}
|
||||
|
||||
function updateActionLinks() {
|
||||
const params = buildParams();
|
||||
const q = params ? ('?' + params) : '';
|
||||
const csvBase = '<?= base_url('payment/download_csv') ?>';
|
||||
const sumBase = '<?= base_url('financial-report/financialReportSummary') ?>';
|
||||
const dl = document.getElementById('downloadCsvLink');
|
||||
const sr = document.getElementById('summaryReportLink');
|
||||
if (dl) dl.href = csvBase + q;
|
||||
if (sr) sr.href = sumBase + q;
|
||||
}
|
||||
|
||||
let __reqSeq = 0;
|
||||
|
||||
function hydrate() {
|
||||
const mySeq = ++__reqSeq;
|
||||
const baseUrl = '<?= site_url('payment/financial_report') ?>';
|
||||
const params = buildParams();
|
||||
const url = baseUrl + (params ? ('?' + params + '&format=json') : '?format=json');
|
||||
// Show loading
|
||||
const $loading = document.getElementById('reportLoading');
|
||||
const $error = document.getElementById('reportError');
|
||||
$error.classList.add('d-none');
|
||||
$loading.classList.remove('d-none');
|
||||
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (mySeq !== __reqSeq) return; // stale response; ignore
|
||||
if (!data || data.ok !== true) throw new Error('bad response');
|
||||
// Destroy DataTables BEFORE changing DOM to avoid conflicts
|
||||
if (window.jQuery && jQuery.fn && jQuery.fn.DataTable) {
|
||||
['#invoicesTable', '#summaryTable', '#expensesTable', '#reimbTable'].forEach(sel => {
|
||||
if (jQuery.fn.DataTable.isDataTable(sel)) {
|
||||
jQuery(sel).DataTable().clear().destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tbody = qs('#invoicesTable tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
const paymentsMap = {};
|
||||
(data.payments || []).forEach(p => {
|
||||
paymentsMap[p.invoice_id] = Number(p.paid_amount || 0);
|
||||
});
|
||||
const refundsMap = {};
|
||||
(data.refunds || []).forEach(r => {
|
||||
refundsMap[r.invoice_id] = Number(r.total_refunded || 0);
|
||||
});
|
||||
const discountsMap = {};
|
||||
(data.discounts || []).forEach(d => {
|
||||
discountsMap[d.invoice_id] = Number(d.discount_amount || 0);
|
||||
});
|
||||
const breakdown = data.paymentBreakdown || {};
|
||||
|
||||
(data.invoices || []).forEach(inv => {
|
||||
const paid = paymentsMap[inv.id] || 0;
|
||||
const bd = breakdown[inv.id] || {};
|
||||
const cash = Number(bd.cash || 0),
|
||||
credit = Number(bd.credit || 0),
|
||||
check = Number(bd.check || 0);
|
||||
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;
|
||||
const status = balance === 0 ? 'Paid' : 'Unpaid';
|
||||
const statusClass = balance === 0 ? 'bg-success' : 'bg-danger';
|
||||
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>` :
|
||||
(inv.parent_name || '');
|
||||
tr.innerHTML = `
|
||||
<td>${inv.invoice_number||''}</td>
|
||||
<td>${parentHtml}</td>
|
||||
<td>${inv.school_year||''}</td>
|
||||
<td data-order="${total}">${fmt(total)}</td>
|
||||
<td data-order="${paid}">${fmt(paid)}</td>
|
||||
<td data-order="${cash}">${fmt(cash)}</td>
|
||||
<td data-order="${credit}">${fmt(credit)}</td>
|
||||
<td data-order="${check}">${fmt(check)}</td>
|
||||
<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>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Summary totals: rebuild row to be safe after DT destroy
|
||||
const pt = data.paymentTotals || {};
|
||||
const tCash = Number(pt.total_cash || 0);
|
||||
const tCred = Number(pt.total_credit || 0);
|
||||
const tChk = Number(pt.total_check || 0);
|
||||
const tAll = Number(pt.total_all || (tCash + tCred + tChk));
|
||||
const sBody = qs('#summaryTable tbody');
|
||||
if (sBody) {
|
||||
sBody.innerHTML = `<tr>
|
||||
<td class="sum-cash" data-order="${tCash}">${fmt(tCash)}</td>
|
||||
<td class="sum-credit" data-order="${tCred}">${fmt(tCred)}</td>
|
||||
<td class="sum-check" data-order="${tChk}">${fmt(tChk)}</td>
|
||||
<td class="sum-all fw-bold" data-order="${tAll}">${fmt(tAll)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// Expenses
|
||||
const expTbody = qs('#expensesTable tbody');
|
||||
expTbody.innerHTML = '';
|
||||
(data.expenses || []).forEach(exp => {
|
||||
const amt = Number(exp.total_amount || 0);
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${exp.category||''}</td><td data-order="${amt}">${fmt(amt)}</td>`;
|
||||
expTbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Reimbursements
|
||||
const rTbody = qs('#reimbTable tbody');
|
||||
rTbody.innerHTML = '';
|
||||
(data.reimbursements || []).forEach(r => {
|
||||
const amt = Number(r.total_amount || 0);
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${r.status||''}</td><td data-order="${amt}">${fmt(amt)}</td>`;
|
||||
rTbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Re-init with options (FixedHeader enabled)
|
||||
initDT('#invoicesTable', {
|
||||
order: [
|
||||
[2, 'asc'],
|
||||
[0, 'asc']
|
||||
],
|
||||
columnDefs: [{
|
||||
targets: 2,
|
||||
visible: false
|
||||
} // hide School Year column
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: getFixedHeaderOffset()
|
||||
}
|
||||
});
|
||||
initDT('#summaryTable', {
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
ordering: true
|
||||
});
|
||||
initDT('#expensesTable', {
|
||||
order: [
|
||||
[1, 'desc']
|
||||
]
|
||||
});
|
||||
initDT('#reimbTable', {
|
||||
order: [
|
||||
[1, 'desc']
|
||||
]
|
||||
});
|
||||
|
||||
// Update last refreshed timestamp
|
||||
const lastRef = document.getElementById('reportLastRef');
|
||||
if (lastRef) lastRef.textContent = 'Last refreshed: ' + formatDateTime(new Date());
|
||||
|
||||
$loading.classList.add('d-none');
|
||||
$error.classList.add('d-none');
|
||||
// Keep action links synced with current filters
|
||||
updateActionLinks();
|
||||
})
|
||||
.catch(() => {
|
||||
if (mySeq !== __reqSeq) return; // stale error; ignore
|
||||
document.getElementById('reportLoading').classList.add('d-none');
|
||||
document.getElementById('reportError').classList.remove('d-none');
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to (re)initialize safely
|
||||
function initDT(selector, options) {
|
||||
if (!window.jQuery || !jQuery.fn || !jQuery.fn.DataTable) return;
|
||||
|
||||
if (jQuery.fn.DataTable.isDataTable(selector)) {
|
||||
jQuery(selector).DataTable().clear().destroy();
|
||||
}
|
||||
return jQuery(selector).DataTable(Object.assign({
|
||||
// Defaults that prevent “stuck at 10/page 1”
|
||||
stateSave: false, // don't restore old 10/page
|
||||
paging: true,
|
||||
ordering: true,
|
||||
info: true,
|
||||
searching: true,
|
||||
pageLength: 100,
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, 'All']
|
||||
],
|
||||
pagingType: 'full_numbers',
|
||||
dom: 'lfrtip'
|
||||
}, options || {}));
|
||||
}
|
||||
|
||||
function initializeTablesAndHandlers() {
|
||||
// Be noisy if DT errors happen
|
||||
if (window.jQuery && jQuery.fn && jQuery.fn.dataTable) {
|
||||
jQuery.fn.dataTable.ext.errMode = 'console';
|
||||
}
|
||||
|
||||
// Main invoices table with FixedHeader
|
||||
initDT('#invoicesTable', {
|
||||
order: [
|
||||
[2, 'asc'],
|
||||
[0, 'asc']
|
||||
], // School Year (hidden), then Invoice #
|
||||
columnDefs: [{
|
||||
targets: 2,
|
||||
visible: false
|
||||
} // hide School Year column
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: getFixedHeaderOffset()
|
||||
}
|
||||
});
|
||||
|
||||
// Payments summary (1 row; keep ordering for convenience)
|
||||
initDT('#summaryTable', {
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
ordering: true
|
||||
});
|
||||
|
||||
// Expenses & Reimbursements
|
||||
initDT('#expensesTable', {
|
||||
order: [
|
||||
[1, 'desc']
|
||||
]
|
||||
});
|
||||
initDT('#reimbTable', {
|
||||
order: [
|
||||
[1, 'desc']
|
||||
]
|
||||
});
|
||||
|
||||
// Initial hydrate and on form submit
|
||||
hydrate();
|
||||
updateActionLinks();
|
||||
const filterForm = document.getElementById('financialFilterForm');
|
||||
if (filterForm) {
|
||||
filterForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
hydrate();
|
||||
updateActionLinks();
|
||||
});
|
||||
filterForm.addEventListener('change', function() {
|
||||
updateActionLinks();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto hydrate on year change
|
||||
const yearSel = document.querySelector('#financialFilterForm select[name="school_year"]');
|
||||
if (yearSel) yearSel.addEventListener('change', function() {
|
||||
hydrate();
|
||||
updateActionLinks();
|
||||
});
|
||||
|
||||
// Retry handler on error banner
|
||||
const retry = document.getElementById('reportRetry');
|
||||
if (retry) retry.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
hydrate();
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure DT core + FixedHeader then init
|
||||
ensureDtFixedHeaderAssets().then(initializeTablesAndHandlers, initializeTablesAndHandlers);
|
||||
|
||||
// OPTIONAL: one-time clear of any old saved state from previous versions
|
||||
// Uncomment once, reload, then comment it back.
|
||||
// if (window.localStorage) {
|
||||
// Object.keys(localStorage)
|
||||
// .filter(k => k.startsWith('DataTables_'))
|
||||
// .forEach(k => localStorage.removeItem(k));
|
||||
// }
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,250 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<h2 class="text-center mt-4 mb-3">Financial Report Summary</h2>
|
||||
<!-- Toolbar: controls above the table -->
|
||||
<div class="d-flex align-items-center gap-2 my-3 flex-nowrap w-100 overflow-auto" style="white-space: nowrap;">
|
||||
<!-- School year filter -->
|
||||
<form id="summaryYearFilter" class="d-flex align-items-center gap-2 flex-nowrap me-auto" method="get" action="<?= site_url('financial-report/financialReportSummary') ?>" style="white-space: nowrap;">
|
||||
<label class="form-label mb-0 me-2">School year</label>
|
||||
<select name="school_year" class="form-select form-select-sm rounded-pill px-3" style="min-width: 180px;">
|
||||
<?php foreach (($schoolYears ?? []) as $sy): ?>
|
||||
<option value="<?= esc($sy) ?>" <?= (string)$schoolYear === (string)$sy ? 'selected' : '' ?>><?= esc($sy) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex align-items-center gap-2 flex-nowrap flex-shrink-0" style="white-space: nowrap;">
|
||||
<a href="<?= base_url('reports/downloadFinancialReport') ?>" class="btn btn-primary">Download PDF</a>
|
||||
<a href="<?= base_url('/payment/financial_report') ?>" class="btn btn-secondary">Back To Detailed Report</a>
|
||||
</div>
|
||||
</div>
|
||||
<p id="summaryPeriod"></p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table w-100" id="reportTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th class="text-right">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="summaryBody">
|
||||
<tr><td>Total Charges</td><td class="text-right" id="sumCharges">$0.00</td></tr>
|
||||
<tr><td>Total Extra Charges</td><td class="text-right" id="sumExtraCharges">$0.00</td></tr>
|
||||
<tr><td>Total Discounts</td><td class="text-right" id="sumDiscounts">$0.00</td></tr>
|
||||
<tr><td>Total Refunds</td><td class="text-right" id="sumRefunds">$0.00</td></tr>
|
||||
<tr><td>Total Expenses</td><td class="text-right" id="sumExpenses">$0.00</td></tr>
|
||||
<tr><td>Total Reimbursements</td><td class="text-right" id="sumReimb">$0.00</td></tr>
|
||||
<tr><td>Donation to School (Masjid & Donation)</td><td class="text-right" id="sumDonationToSchool">$0.00</td></tr>
|
||||
<tr class="table-success"><td>Net Amount (Earned Income)</td><td class="text-right font-weight-bold" id="sumNet">$0.00</td></tr>
|
||||
<tr class="table-info"><td>Amount Collected (Paid)</td><td class="text-right font-weight-bold" id="sumCollected">$0.00</td></tr>
|
||||
<tr class="table-warning"><td>Amount Unpaid (Outstanding)</td><td class="text-right font-weight-bold" id="sumUnpaid">$0.00</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<h3>Summary Graph</h3>
|
||||
<canvas id="summaryChart" style="max-height:300px;"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="h-100 p-3 border rounded">
|
||||
<h3 class="h5">Collected vs Outstanding</h3>
|
||||
<canvas id="collectedChart" style="max-height:320px; width:100%;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="h-100 p-3 border rounded">
|
||||
<h3 class="h5">Expense Breakdown</h3>
|
||||
<canvas id="expenseChart" style="max-height:320px; width:100%;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
|
||||
<script>
|
||||
function fmt(n){ return '$' + Number(n||0).toFixed(2); }
|
||||
// Auto-submit school year on change and hydrate via API
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sel = document.querySelector('#summaryYearFilter select[name="school_year"]');
|
||||
if (sel) sel.addEventListener('change', function(){ loadSummary(); });
|
||||
loadSummary();
|
||||
});
|
||||
|
||||
let __sumSeq = 0;
|
||||
function loadSummary(){
|
||||
const mySeq = ++__sumSeq;
|
||||
const sel = document.querySelector('#summaryYearFilter select[name="school_year"]');
|
||||
const params = new URLSearchParams();
|
||||
if (sel && sel.value) params.append('school_year', sel.value);
|
||||
const baseUrl = '<?= site_url('financial-report/financialReportSummary') ?>';
|
||||
const url = baseUrl + (params.toString() ? ('?' + params.toString() + '&format=json') : '?format=json');
|
||||
fetch(url, { headers: { 'Accept': 'application/json' }})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (mySeq !== __sumSeq) return; // stale response
|
||||
if (!d || d.ok !== true) return;
|
||||
document.getElementById('summaryPeriod').textContent = 'Report for School Year: ' + (d.schoolYear||'');
|
||||
document.getElementById('sumCharges').textContent = fmt(d.totalCharges);
|
||||
if (document.getElementById('sumExtraCharges')) {
|
||||
document.getElementById('sumExtraCharges').textContent = fmt(d.totalExtraCharges || 0);
|
||||
}
|
||||
document.getElementById('sumDiscounts').textContent = fmt(d.totalDiscounts);
|
||||
document.getElementById('sumRefunds').textContent = fmt(d.totalRefunds);
|
||||
document.getElementById('sumExpenses').textContent = fmt(d.totalExpenses);
|
||||
document.getElementById('sumReimb').textContent = fmt(d.totalReimbursements);
|
||||
if (document.getElementById('sumDonationToSchool')) {
|
||||
document.getElementById('sumDonationToSchool').textContent = fmt(d.donationToSchool);
|
||||
}
|
||||
document.getElementById('sumNet').textContent = fmt(d.netAmount);
|
||||
document.getElementById('sumCollected').textContent = fmt(d.amountCollected);
|
||||
document.getElementById('sumUnpaid').textContent = fmt(d.totalUnpaid);
|
||||
|
||||
// Refresh charts with new data
|
||||
renderCharts(d);
|
||||
})
|
||||
.catch(()=>{});
|
||||
}
|
||||
|
||||
function renderCharts(d){
|
||||
if (!window.Chart) return;
|
||||
// Destroy existing if present
|
||||
if (window._summaryChart) { window._summaryChart.destroy(); }
|
||||
if (window._collectedChart) { window._collectedChart.destroy(); }
|
||||
if (window._expenseChart) { window._expenseChart.destroy(); }
|
||||
const summaryCtx = document.getElementById('summaryChart').getContext('2d');
|
||||
window._summaryChart = new Chart(summaryCtx, {
|
||||
type: 'bar',
|
||||
data: { labels: ['Charges','Paid','Unpaid','Discounts','Refunds','Expenses','Reimbursements','Net'],
|
||||
datasets: [{ label:'Amount (USD)', data:[
|
||||
d.totalCharges||0, d.totalPaid||0, d.totalUnpaid||0, d.totalDiscounts||0, d.totalRefunds||0, d.totalExpenses||0, d.totalReimbursements||0, d.netAmount||0
|
||||
], backgroundColor: ['#007bff','#28a745','#ffc107','#17a2b8','#ffc107','#dc3545','#6f42c1','#20c997']}] },
|
||||
options: { responsive:true, scales:{ y:{ beginAtZero:true }}}
|
||||
});
|
||||
|
||||
const collectedCtx = document.getElementById('collectedChart').getContext('2d');
|
||||
window._collectedChart = new Chart(collectedCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Amount Collected (Paid)', 'Amount Outstanding (Unpaid)'],
|
||||
datasets: [{
|
||||
data: [ d.amountCollected || d.totalPaid || 0, d.totalUnpaid || 0 ],
|
||||
backgroundColor: ['#28a745', '#ffc107'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: { responsive:true, plugins:{ legend:{ position:'bottom' } } }
|
||||
});
|
||||
|
||||
const expenseCtx = document.getElementById('expenseChart').getContext('2d');
|
||||
window._expenseChart = new Chart(expenseCtx, {
|
||||
type: 'pie',
|
||||
data: { labels: ['Expenses','Reimbursements'], datasets: [{ data:[ d.totalExpenses||0, d.totalReimbursements||0 ], backgroundColor: ['#dc3545','#6f42c1']}]},
|
||||
options: { responsive:true }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.jQuery && jQuery.fn && jQuery.fn.DataTable) {
|
||||
jQuery('#reportTable').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
ordering: false
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Bar chart summary
|
||||
const summaryCtx = document.getElementById('summaryChart').getContext('2d');
|
||||
window._summaryChart = new Chart(summaryCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Charges', 'Paid', 'Unpaid', 'Discounts', 'Refunds', 'Expenses', 'Reimbursements', 'Net'],
|
||||
datasets: [{
|
||||
label: 'Amount (USD)',
|
||||
data: [
|
||||
<?= (float)$totalCharges ?>,
|
||||
<?= (float)$totalPaid ?>,
|
||||
<?= (float)$totalUnpaid ?>,
|
||||
<?= (float)$totalDiscounts ?>,
|
||||
<?= (float)$totalRefunds ?>,
|
||||
<?= (float)$totalExpenses ?>,
|
||||
<?= (float)$totalReimbursements ?>,
|
||||
<?= (float)$netAmount ?>
|
||||
],
|
||||
backgroundColor: [
|
||||
'#007bff', '#28a745', '#ffc107', '#17a2b8', '#ffc107', '#dc3545', '#6f42c1', '#20c997'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Collected vs Outstanding pie
|
||||
const collectedCtx = document.getElementById('collectedChart').getContext('2d');
|
||||
window._collectedChart = new Chart(collectedCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Amount Collected (Paid)', 'Amount Outstanding (Unpaid)'],
|
||||
datasets: [{
|
||||
data: [
|
||||
<?= (float)$amountCollected ?>,
|
||||
<?= (float)$totalUnpaid ?>
|
||||
],
|
||||
backgroundColor: ['#28a745', '#ffc107']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { position: 'bottom' } }
|
||||
}
|
||||
});
|
||||
|
||||
// Pie chart expenses
|
||||
const expenseCtx = document.getElementById('expenseChart').getContext('2d');
|
||||
window._expenseChart = new Chart(expenseCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Expenses', 'Reimbursements'],
|
||||
datasets: [{
|
||||
data: [
|
||||
<?= (float)$totalExpenses ?>,
|
||||
<?= (float)$totalReimbursements ?>
|
||||
],
|
||||
backgroundColor: ['#dc3545', '#6f42c1']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,581 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2 class="text-center mt-4 mb-3">Manual Payment</h2>
|
||||
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success"><?= session()->getFlashdata('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Search form -->
|
||||
<form method="get" action="<?= base_url('payment/manual_pay') ?>" class="mb-3">
|
||||
<div class="input-group">
|
||||
<input type="text" name="search_term" class="form-control" placeholder="Enter Parent Email or Phone" required value="<?= esc($searchTermUsedInSearch ?? '') ?>">
|
||||
<button class="btn btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($parent): ?>
|
||||
<!-- Parent + Student info -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Parent Information</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Name:</strong> <?= esc($parent['firstname']) ?> <?= esc($parent['lastname']) ?></p>
|
||||
<p><strong>Phone:</strong> <?= esc($parent['cellphone']) ?></p>
|
||||
<p><strong>Email:</strong> <?= esc($parent['email']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Students</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($students)): ?>
|
||||
<ul class="list-group">
|
||||
<?php foreach ($students as $student): ?>
|
||||
<li class="list-group-item"><?= esc($student['firstname']) ?> <?= esc($student['lastname']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p>No students found.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payments table -->
|
||||
<h5>Payments</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Paid Date</th>
|
||||
<th>Paid Amount</th>
|
||||
<th>Balance</th>
|
||||
<th>Payment Method</th>
|
||||
<th>Check Number</th>
|
||||
<th>Installments</th>
|
||||
<th>Status</th>
|
||||
<th>Check/Card File</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($payments as $payment): ?>
|
||||
<tr>
|
||||
<td><?= $payment['payment_date']->format('m-d-Y h:i A') ?></td>
|
||||
<td>$<?= number_format((float)$payment['paid_amount'], 2) ?></td>
|
||||
<td>$<?= number_format((float)$payment['balance'], 2) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$method = strtolower((string)$payment['payment_method']);
|
||||
if ($method === 'cash') {
|
||||
echo '<span class="badge" style="background-color:#28a745;color:#fff;">Cash</span>';
|
||||
} elseif ($method === 'check') {
|
||||
echo '<span class="badge" style="background-color:#1ba2b9;color:#fff;">Check</span>';
|
||||
} elseif ($method === 'card' || $method === 'debit/credit card') {
|
||||
echo '<span class="badge" style="background-color:#007bff;color:#fff;">Card</span>';
|
||||
} else {
|
||||
echo '<span class="badge bg-secondary">' . esc($payment['payment_method']) . '</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?= !empty($payment['check_number']) ? esc($payment['check_number']) : '-' ?></td>
|
||||
<td><?= esc($payment['number_of_installments']) ?></td>
|
||||
<td><?= esc($payment['status']) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($payment['check_file'])): ?>
|
||||
<?php $file = rawurlencode((string)$payment['check_file']); ?>
|
||||
<!-- Trigger Modal -->
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#checkModal<?= (int)$payment['id'] ?>">View</a> /
|
||||
<a href="<?= base_url('payment/serveCheckFile/' . $file . '/download') ?>">Download</a>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="checkModal<?= (int)$payment['id'] ?>" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Check Preview</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<iframe src="<?= base_url('payment/serveCheckFile/' . $file . '/inline') ?>"
|
||||
width="100%" height="600px" style="border:none;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#editPaymentModal<?= (int)$payment['id'] ?>">Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Edit Payment Modal -->
|
||||
<div class="modal fade" id="editPaymentModal<?= (int)$payment['id'] ?>" tabindex="-1" aria-labelledby="editPaymentModalLabel<?= (int)$payment['id'] ?>" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="<?= base_url('payment/manual_pay_edit') ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="payment_id" value="<?= (int)$payment['id'] ?>">
|
||||
<input type="hidden" name="search_term" value="<?= esc($searchTermUsedInSearch ?? '') ?>">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editPaymentModalLabel<?= (int)$payment['id'] ?>">Edit Payment ID <?= (int)$payment['id'] ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label>Paid Amount</label>
|
||||
<input type="number" step="0.01" name="paid_amount" class="form-control" value="<?= esc($payment['paid_amount']) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Payment Method</label>
|
||||
<select name="payment_method" class="form-select edit-payment-method"
|
||||
data-payment-id="<?= (int)$payment['id'] ?>"
|
||||
onchange="toggleEditFields(<?= (int)$payment['id'] ?>)" required>
|
||||
<option value="Cash" <?= ($payment['payment_method'] === 'Cash') ? 'selected' : '' ?>>Cash</option>
|
||||
<option value="Check" <?= ($payment['payment_method'] === 'Check') ? 'selected' : '' ?>>Check</option>
|
||||
<option value="Card" <?= ($payment['payment_method'] === 'Card') ? 'selected' : '' ?>>Debit/Credit Card</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 check-number-edit" id="check-number-edit-<?= (int)$payment['id'] ?>" style="display:none;">
|
||||
<label>Check Number</label>
|
||||
<input type="text" name="check_number" class="form-control"
|
||||
value="<?= !empty($payment['check_number']) ? esc($payment['check_number']) : '' ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="payment-file-edit-<?= (int)$payment['id'] ?>" style="display:none;">
|
||||
<label>Replace Payment File (optional)</label>
|
||||
<input type="file" name="payment_file" class="form-control" accept=".jpg,.jpeg,.png,.pdf">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-success">Save Changes</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if (isset($pager)): ?>
|
||||
<div class="d-flex justify-content-center">
|
||||
<?= $pager->links() ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Invoices -->
|
||||
<?php if (!empty($invoices)): ?>
|
||||
<h5>Invoices</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Total</th>
|
||||
<th>Paid</th>
|
||||
<th>Balance</th>
|
||||
<th>Status</th>
|
||||
<th>Due Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($invoices as $invoice): ?>
|
||||
<?php
|
||||
$dueDisp = '-';
|
||||
if (!empty($invoice['due_date'])) {
|
||||
$dueDisp = local_date($invoice['due_date'], 'm-d-Y');
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($invoice['invoice_number']) ?></td>
|
||||
<td>$<?= number_format((float)$invoice['total_amount'], 2) ?></td>
|
||||
<td>$<?= number_format((float)$invoice['paid_amount'], 2) ?></td>
|
||||
<td>$<?= number_format((float)$invoice['balance'], 2) ?></td>
|
||||
<td><?= esc($invoice['status']) ?></td>
|
||||
<td><?= esc($dueDisp) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPaymentModal">Add Payment</button>
|
||||
</div>
|
||||
|
||||
<?php elseif (isset($parent)): ?>
|
||||
<p class="text-danger">Parent found but no payment records.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Add Payment Modal -->
|
||||
<div class="modal fade" id="addPaymentModal" tabindex="-1" aria-labelledby="addPaymentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<!-- IMPORTANT: post to existing POST route payment/manual_pay -->
|
||||
<form method="post" action="<?= base_url('payment/manual_pay') ?>" id="addPaymentForm" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="search_term" value="<?= esc($searchTermUsedInSearch ?? '') ?>">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addPaymentModalLabel">Add Manual Payment</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Invoice select with per-invoice data for JS -->
|
||||
<div class="mb-3">
|
||||
<label for="invoice_id" class="form-label">Select Invoice</label>
|
||||
<select
|
||||
name="invoice_id"
|
||||
id="invoice_id"
|
||||
class="form-select"
|
||||
required
|
||||
onchange="onInvoiceChange(this)"
|
||||
data-end-date="<?= esc($installmentEndYmd) ?>">
|
||||
<?php if (!empty($invoices)): ?>
|
||||
<?php foreach ($invoices as $invoice): ?>
|
||||
<option
|
||||
value="<?= (int)$invoice['id'] ?>"
|
||||
data-balance="<?= esc($invoice['balance']) ?>">
|
||||
Invoice <?= esc($invoice['invoice_number']) ?> |
|
||||
Balance: $<?= number_format((float)$invoice['balance'], 2) ?> |
|
||||
Paid: $<?= number_format((float)$invoice['paid_amount'], 2) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<option value="">No invoices available</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Method -->
|
||||
<div class="mb-3">
|
||||
<label for="payment_method" class="form-label">Payment Method</label>
|
||||
<select name="payment_method" id="payment_method" class="form-select" required onchange="togglePaymentFieldsAdd()">
|
||||
<option value="">Select Method</option>
|
||||
<option value="cash">Cash</option>
|
||||
<option value="check">Check</option>
|
||||
<option value="card">Debit/Credit Card</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Check number (only for checks) -->
|
||||
<div class="mb-3" id="check-number-field" style="display:none;">
|
||||
<label for="check_number" class="form-label">Check Number</label>
|
||||
<input type="text" name="check_number" id="check_number" class="form-control">
|
||||
</div>
|
||||
|
||||
<!-- Receipt upload (check/card) -->
|
||||
<div class="mb-3" id="payment-upload" style="display:none;">
|
||||
<label for="payment_file" class="form-label">Upload File</label>
|
||||
<input type="file" name="payment_file" id="payment_file" class="form-control" accept=".jpg,.jpeg,.png,.pdf">
|
||||
</div>
|
||||
|
||||
<!-- Payment type -->
|
||||
<div class="mb-3">
|
||||
<label for="payment_type" class="form-label">Payment Type</label>
|
||||
<select name="payment_type" id="payment_type" class="form-select" required onchange="toggleInstallmentFields()">
|
||||
<option value="full">Full Payment</option>
|
||||
<option value="installment">Installment Payment</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Installment section -->
|
||||
<div id="installment-section" style="display:none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="installment_count" class="form-label">Number of Installments</label>
|
||||
<select name="installment_count" id="installment_count" class="form-select" onchange="recalcInstallments()"></select>
|
||||
<div class="form-text">Maximum installments available: <span id="max-installments">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="installment_amount" class="form-label">Amount per Installment</label>
|
||||
<input type="number" step="0.01" name="installment_amount" id="installment_amount" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
Choose the number of installments (up to the maximum available). The first payment is due today.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount -->
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">Amount</label>
|
||||
<input type="number" step="0.01" name="amount" id="amount" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-success">Submit Payment</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Payment Modal -->
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
// =========================
|
||||
// Helpers
|
||||
// =========================
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function parseYMD(ymd) {
|
||||
if (typeof ymd !== 'string') return null;
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd.trim());
|
||||
if (!m) return null;
|
||||
const y = +m[1],
|
||||
mo = +m[2],
|
||||
d = +m[3];
|
||||
const dt = new Date(y, mo - 1, d);
|
||||
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
||||
return dt;
|
||||
}
|
||||
|
||||
function todayYMD() {
|
||||
const t = new Date();
|
||||
const y = t.getFullYear();
|
||||
const m = String(t.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(t.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
// Months between TODAY and end date. Always >= 1.
|
||||
function calculateInstallmentsFromToday(endDateStr) {
|
||||
const start = parseYMD(todayYMD());
|
||||
const end = parseYMD(endDateStr);
|
||||
if (!start || !end) return 1;
|
||||
if (end.getTime() < start.getTime()) return 1;
|
||||
|
||||
let months = (end.getFullYear() - start.getFullYear()) * 12 +
|
||||
(end.getMonth() - start.getMonth());
|
||||
if (end.getDate() > start.getDate()) months += 1;
|
||||
|
||||
return Math.max(1, months);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// State
|
||||
// =========================
|
||||
let currentInvoiceBalance = 0;
|
||||
let maxInstallmentNumber = 1;
|
||||
|
||||
// =========================
|
||||
// Edit modal helpers
|
||||
// =========================
|
||||
function toggleEditFields(paymentId) {
|
||||
const select = document.querySelector(`select[data-payment-id='${paymentId}']`);
|
||||
const checkField = document.getElementById(`check-number-edit-${paymentId}`);
|
||||
const fileField = document.getElementById(`payment-file-edit-${paymentId}`);
|
||||
const input = checkField ? checkField.querySelector('input') : null;
|
||||
if (!select || !checkField || !fileField || !input) return;
|
||||
|
||||
const v = (select.value || '').toLowerCase();
|
||||
if (v === 'check') {
|
||||
checkField.style.display = 'block';
|
||||
fileField.style.display = 'block';
|
||||
input.required = true;
|
||||
} else if (v === 'card') {
|
||||
checkField.style.display = 'none';
|
||||
fileField.style.display = 'block';
|
||||
input.required = false;
|
||||
} else {
|
||||
checkField.style.display = 'none';
|
||||
fileField.style.display = 'none';
|
||||
input.required = false;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Add Payment modal logic
|
||||
// =========================
|
||||
function onInvoiceChange(select) {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
currentInvoiceBalance = parseFloat(opt?.dataset?.balance || '0') || 0;
|
||||
|
||||
// READ FROM THE SELECT (it has data-end-date)
|
||||
const endDate = select?.dataset?.endDate || select.getAttribute('data-end-date') || '';
|
||||
|
||||
const computed = calculateInstallmentsFromToday(endDate);
|
||||
maxInstallmentNumber = computed;
|
||||
|
||||
const maxSpan = el('max-installments');
|
||||
if (maxSpan) maxSpan.textContent = String(maxInstallmentNumber);
|
||||
|
||||
populateInstallmentOptions();
|
||||
toggleInstallmentFields();
|
||||
recalcInstallments();
|
||||
updateAmountField();
|
||||
}
|
||||
|
||||
function populateInstallmentOptions() {
|
||||
const countSelect = el('installment_count');
|
||||
if (!countSelect) return;
|
||||
|
||||
countSelect.innerHTML = '';
|
||||
for (let i = 1; i <= maxInstallmentNumber; i++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.textContent = i + (i === 1 ? ' installment' : ' installments');
|
||||
countSelect.appendChild(option);
|
||||
}
|
||||
countSelect.value = String(maxInstallmentNumber);
|
||||
}
|
||||
|
||||
function togglePaymentFieldsAdd() {
|
||||
const method = (el('payment_method').value || '').toLowerCase();
|
||||
const checkField = el('check-number-field');
|
||||
const fileField = el('payment-upload');
|
||||
const checkInput = el('check_number');
|
||||
const ptype = el('payment_type');
|
||||
const section = el('installment-section');
|
||||
|
||||
if (method === 'check') {
|
||||
if (checkField) checkField.style.display = 'block';
|
||||
if (fileField) fileField.style.display = 'block';
|
||||
if (checkInput) checkInput.required = true;
|
||||
if (ptype) ptype.disabled = false;
|
||||
} else if (method === 'card') {
|
||||
if (checkField) checkField.style.display = 'none';
|
||||
if (fileField) fileField.style.display = 'block';
|
||||
if (checkInput) checkInput.required = false;
|
||||
if (ptype) {
|
||||
ptype.value = 'full';
|
||||
ptype.disabled = true;
|
||||
}
|
||||
if (section) section.style.display = 'none';
|
||||
} else {
|
||||
if (checkField) checkField.style.display = 'none';
|
||||
if (fileField) fileField.style.display = 'none';
|
||||
if (checkInput) checkInput.required = false;
|
||||
if (ptype) ptype.disabled = false;
|
||||
toggleInstallmentFields();
|
||||
}
|
||||
updateAmountField();
|
||||
}
|
||||
|
||||
function toggleInstallmentFields() {
|
||||
const method = (el('payment_method').value || '').toLowerCase();
|
||||
const type = el('payment_type') ? el('payment_type').value : 'full';
|
||||
const section = el('installment-section');
|
||||
|
||||
if (method === 'card') {
|
||||
if (section) section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (section) section.style.display = (type === 'installment') ? 'block' : 'none';
|
||||
|
||||
if (type === 'installment') {
|
||||
populateInstallmentOptions();
|
||||
}
|
||||
}
|
||||
|
||||
function recalcInstallments() {
|
||||
const method = (el('payment_method').value || '').toLowerCase();
|
||||
const type = el('payment_type') ? el('payment_type').value : 'full';
|
||||
const countSelect = el('installment_count');
|
||||
const amtEl = el('installment_amount');
|
||||
|
||||
if (!countSelect || !amtEl) return;
|
||||
|
||||
if (type !== 'installment' || method === 'card') {
|
||||
amtEl.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = parseInt(countSelect.value || '1', 10);
|
||||
const raw = (currentInvoiceBalance > 0 ? (currentInvoiceBalance / selected) : 0);
|
||||
const rounded = (Math.round(raw * 100) / 100).toFixed(2);
|
||||
amtEl.value = rounded;
|
||||
|
||||
updateAmountField();
|
||||
}
|
||||
|
||||
function updateAmountField() {
|
||||
const method = (el('payment_method').value || '').toLowerCase();
|
||||
const type = el('payment_type') ? el('payment_type').value : 'full';
|
||||
const amountInput = el('amount');
|
||||
|
||||
if (!amountInput) return;
|
||||
|
||||
if (method === 'card' || type === 'full') {
|
||||
amountInput.value = (currentInvoiceBalance || 0).toFixed(2);
|
||||
} else {
|
||||
const installmentAmount = el('installment_amount') ? (el('installment_amount').value || '0') : '0';
|
||||
amountInput.value = installmentAmount;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Init
|
||||
// =========================
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize edit modals
|
||||
document.querySelectorAll('.edit-payment-method').forEach(select => {
|
||||
const id = select.getAttribute('data-payment-id');
|
||||
toggleEditFields(id);
|
||||
select.addEventListener('change', () => toggleEditFields(id));
|
||||
});
|
||||
|
||||
// Wire Add Payment modal
|
||||
const invoiceSelect = el('invoice_id');
|
||||
if (invoiceSelect && invoiceSelect.options.length > 0) {
|
||||
invoiceSelect.addEventListener('change', function() {
|
||||
onInvoiceChange(this);
|
||||
});
|
||||
onInvoiceChange(invoiceSelect); // initial compute
|
||||
}
|
||||
|
||||
const paymentTypeSel = el('payment_type');
|
||||
if (paymentTypeSel) {
|
||||
paymentTypeSel.addEventListener('change', function() {
|
||||
toggleInstallmentFields();
|
||||
recalcInstallments();
|
||||
updateAmountField();
|
||||
});
|
||||
}
|
||||
|
||||
const paymentMethodSel = el('payment_method');
|
||||
if (paymentMethodSel) {
|
||||
paymentMethodSel.addEventListener('change', function() {
|
||||
togglePaymentFieldsAdd();
|
||||
recalcInstallments();
|
||||
updateAmountField();
|
||||
});
|
||||
}
|
||||
|
||||
const installmentCountSel = el('installment_count');
|
||||
if (installmentCountSel) {
|
||||
installmentCountSel.addEventListener('change', function() {
|
||||
recalcInstallments();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/** @var array $logs */
|
||||
/** @var array $parentsById */
|
||||
/** @var array $filters */
|
||||
|
||||
helper('html');
|
||||
?>
|
||||
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
.card { box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
|
||||
.table thead th { background: #f1f3f5; }
|
||||
.badge-type { text-transform: capitalize; }
|
||||
.page-header { display:flex; align-items:center; gap:12px; }
|
||||
.page-header h1 { font-size: 1.5rem; margin: 0; }
|
||||
.page-header .hint { color:#6c757d; font-size: .95rem; }
|
||||
}</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container-fluid py-3">
|
||||
<div class="page-header mb-3">
|
||||
<h1>Payment Notification Management</h1>
|
||||
<div class="hint">Review monthly reminder emails sent to parents.</div>
|
||||
</div>
|
||||
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">From</label>
|
||||
<input id="from" type="text" name="from" value="<?= esc($filters['from'] ?? '') ?>" class="form-control" placeholder="YYYY-MM-DD HH:MM:SS">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">To</label>
|
||||
<input id="to" type="text" name="to" value="<?= esc($filters['to'] ?? '') ?>" class="form-control" placeholder="YYYY-MM-DD HH:MM:SS">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="">All</option>
|
||||
<option value="no_payment" <?= ($filters['type'] ?? '')==='no_payment' ? 'selected' : '' ?>>No Payment</option>
|
||||
<option value="installment" <?= ($filters['type'] ?? '')==='installment' ? 'selected' : '' ?>>Installment</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex gap-2">
|
||||
<button class="btn btn-primary" type="submit">Filter</button>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="setQuickRange(30)">Last 30 days</button>
|
||||
<a class="btn btn-outline-dark" href="<?= site_url('payment/notification_management') ?>">Reset</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sent At</th>
|
||||
<th>Parent</th>
|
||||
<th>Type</th>
|
||||
<th>To</th>
|
||||
<th>CC</th>
|
||||
<th>Balance</th>
|
||||
<th>Status</th>
|
||||
<th>Subject</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($logs)): ?>
|
||||
<tr><td colspan="8" class="text-center text-muted py-4">No notifications found.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($logs as $r): ?>
|
||||
<tr>
|
||||
<?php
|
||||
$sentAtRaw = $r['sent_at'] ?? $r['created_at'] ?? '';
|
||||
$sentAtDisp = $sentAtRaw ? local_datetime($sentAtRaw, 'm-d-Y H:i') : '';
|
||||
?>
|
||||
<td><?= esc($sentAtDisp) ?></td>
|
||||
<td><?= esc($parentsById[(int)$r['parent_id']] ?? ('#'.$r['parent_id'])) ?></td>
|
||||
<td><span class="badge bg-info text-dark badge-type"><?= esc($r['type']) ?></span></td>
|
||||
<td><?= esc($r['to_email'] ?? '') ?></td>
|
||||
<td><?= esc($r['cc_email'] ?? '') ?></td>
|
||||
<td><?= isset($r['balance_snapshot']) ? ('$'.number_format((float)$r['balance_snapshot'],2)) : '' ?></td>
|
||||
<td>
|
||||
<?php if (($r['status'] ?? '') === 'sent'): ?>
|
||||
<span class="badge bg-success">Sent</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-danger" title="<?= esc($r['error_message'] ?? '') ?>">Failed</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc($r['subject'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
function setQuickRange(days) {
|
||||
const to = new Date();
|
||||
const from = new Date();
|
||||
from.setDate(to.getDate() - days);
|
||||
const fromEl = document.getElementById('from');
|
||||
const toEl = document.getElementById('to');
|
||||
if (fromEl) fromEl.value = from.toISOString().slice(0,10) + ' 00:00:00';
|
||||
if (toEl) toEl.value = to.toISOString().slice(0,10) + ' 23:59:59';
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,81 @@
|
||||
<!-- This snippet supports rendering separate standalone buttons
|
||||
in different parts of your page with unique payment methods.
|
||||
Test EMAIL: sb-cbmre43313068@business.example.com
|
||||
Test PASSWORD: 9JC?].qM
|
||||
-->
|
||||
|
||||
<?= $this->extend('layout/register_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container d-flex justify-content-center align-items-start" style="min-height: 100vh; padding-top: 80px;">
|
||||
<div class="registration-form text-center p-4 rounded-4 shadow"
|
||||
style="background-color: white; max-width: 600px; width: 300%;">
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="mb-4">
|
||||
<a href="<?= base_url('/parent_dashboard') ?>">
|
||||
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="Alrahma Logo"
|
||||
style="width: 180px; height: 120px;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Page Title -->
|
||||
<h3 class="text-success" style="font-family: Arial, sans-serif;">Payment</h3>
|
||||
<br>
|
||||
|
||||
<!-- PayPal Button -->
|
||||
<div class="d-flex justify-content-center">
|
||||
<div id="paypal-button-container"></div>
|
||||
</div>
|
||||
<p class="text-success">
|
||||
<br><strong>Your payment information, including credit card and bank account details, is securely stored only by PayPal and Venmo. </strong>
|
||||
</p>
|
||||
<p class="text-danger">
|
||||
We do not collect or save any financial data on our website.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script src="https://www.paypal.com/sdk/js?client-id=AYsiOekZUvrjgx9C5c554ZeSQ4W6yd0ZX-OHE93_D0fWoa4YXrMmroEeLiAjjdCKkELH8EVZR_yGMLPS¤cy=USD&components=buttons,funding-eligibility&enable-funding=venmo&disable-funding=card,paylater">
|
||||
</script>
|
||||
<script>
|
||||
const parentName = <?= json_encode($parentName) ?>;
|
||||
const totalAmount = <?= json_encode(number_format($totalAmount, 2, '.', '')) ?>;
|
||||
const schoolId = <?= json_encode($schoolId) ?>;
|
||||
|
||||
paypal.Buttons({
|
||||
style: {
|
||||
layout: 'vertical',
|
||||
color: 'blue',
|
||||
shape: 'rect',
|
||||
label: 'paypal'
|
||||
},
|
||||
createOrder: function(data, actions) {
|
||||
return actions.order.create({
|
||||
purchase_units: [{
|
||||
amount: {
|
||||
value: totalAmount
|
||||
},
|
||||
custom_id: `${schoolId}`,
|
||||
description: `Payment by ${parentName}`
|
||||
}]
|
||||
});
|
||||
},
|
||||
|
||||
onApprove: function(data, actions) {
|
||||
return actions.order.capture().then(function(details) {
|
||||
alert('Transaction completed by ' + details.payer.name.given_name + ' for <?= esc($parentName) ?>');
|
||||
});
|
||||
},
|
||||
onError: function(err) {
|
||||
console.error('An error occurred during the transaction', err);
|
||||
}
|
||||
}).render('#paypal-button-container');
|
||||
</script>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,226 @@
|
||||
<?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,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; }
|
||||
.email-cell { max-width: 280px; overflow: hidden; text-overflow: ellipsis; }
|
||||
@media (max-width: 576px){ .email-cell { max-width: 180px; } }
|
||||
/* 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>
|
||||
<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>Email</th>
|
||||
<th>Type</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="12" 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="email-cell"><a href="mailto:<?= esc($r['email']) ?>"><?= esc($r['email']) ?></a></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 $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['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($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() ?>
|
||||
Reference in New Issue
Block a user