recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
+53
View File
@@ -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>
+8
View File
@@ -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>
+271
View File
@@ -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 &amp; 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() ?>
+592
View File
@@ -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 &amp; 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
+581
View File
@@ -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() ?>
+81
View File
@@ -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&currency=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() ?>
+226
View File
@@ -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">&mdash;</th>
<th class="text-end">$<?= number_format($sumInstAmt, 2) ?></th>
<th></th>
<th class="text-end">$<?= number_format($sumBal, 2) ?></th>
<th></th>
<th></th>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
(function(){
var $ = window.jQuery;
var $tbl = $('#unpaidTable');
if (!$tbl.length) return;
// Try DataTables first
if ($ && $.fn && $.fn.DataTable) {
try {
$tbl.DataTable({
pageLength: 100,
order: [[9, 'desc']], // Outstanding Balance desc
columnDefs: [
{ targets: [11], orderable: false }, // Actions
{
targets: [3,4,5,7,9], // currency columns
render: function(data, type, row) {
if (type === 'sort' || type === 'type') {
var v = (data || '').toString().replace(/[^0-9.\-]/g, '');
var n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
return data;
}
}
]
});
return; // done
} catch(e) { /* fall through to simple sorter */ }
}
// Fallback: simple header click sorter (no dependencies)
try {
var table = $tbl.get(0);
var ths = table.tHead && table.tHead.rows[0] ? table.tHead.rows[0].cells : [];
var currencyIdx = {3:true,4:true,5:true,7:true,9:true};
var dateIdx = {10:true};
var noSortIdx = {11:true};
var dirState = {};
function getText(td){ return (td && (td.innerText||td.textContent)||'').trim(); }
function toNumber(s){ var n = parseFloat((s||'').replace(/[^0-9.\-]/g, '')); return isNaN(n)?0:n; }
function toDate(s){ var t = Date.parse(s); return isNaN(t)?0:t; }
function sortBy(col){
if (noSortIdx[col]) return;
var tbody = table.tBodies[0];
var rows = Array.prototype.slice.call(tbody.rows);
var asc = dirState[col] === 'desc'; // toggle
dirState[col] = asc ? 'asc' : 'desc';
rows.sort(function(a,b){
var av = getText(a.cells[col]);
var bv = getText(b.cells[col]);
var cmp;
if (currencyIdx[col]) { cmp = toNumber(av) - toNumber(bv); }
else if (dateIdx[col]) { cmp = toDate(av) - toDate(bv); }
else { cmp = av.localeCompare(bv, undefined, {numeric:true, sensitivity:'base'}); }
return asc ? cmp : -cmp;
});
rows.forEach(function(r){ tbody.appendChild(r); });
}
for (var i=0; i<ths.length; i++){
(function(idx){
ths[idx].style.cursor = noSortIdx[idx] ? 'default' : 'pointer';
ths[idx].addEventListener('click', function(){ sortBy(idx); });
})(i);
}
} catch(_) { /* ignore */ }
})();
</script>
<?= $this->endSection() ?>