Files
2026-02-10 22:11:06 -05:00

272 lines
13 KiB
PHP

<!-- 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() ?>