190 lines
8.7 KiB
PHP
190 lines
8.7 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
||
<?= $this->section('content') ?>
|
||
|
||
<div class="container-fluid mt-4">
|
||
<h2 class="text-center mt-4 mb-3">Expense / Purchase</h2>
|
||
<?= $this->include('partials/academic_filter') ?>
|
||
<div class="d-flex gap-2 mb-3 justify-content-end">
|
||
<a href="<?= base_url('expenses/create') ?>" class="btn btn-success mb-3">Add New</a>
|
||
</div>
|
||
<?php if (session()->getFlashdata('success')): ?>
|
||
<div class="alert alert-success">
|
||
<?= session()->getFlashdata('success') ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php $totalAmount = 0.0; ?>
|
||
<div class="table-responsive">
|
||
<table id="expensesTable" class="table table-bordered table-striped align-middle w-100 no-mgmt-sticky" style="table-layout:auto;">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Category</th>
|
||
<th>Amount</th>
|
||
<th>Vendor</th>
|
||
<th>Purchased By</th>
|
||
<th>Date of Purchase</th>
|
||
<th>Description</th>
|
||
<th>Status</th>
|
||
<th>Approved By</th>
|
||
<th>Receipt</th>
|
||
<th>Added On</th>
|
||
<th>Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($expenses as $e): ?>
|
||
<?php $totalAmount += (float)($e['amount'] ?? 0); ?>
|
||
<tr>
|
||
<td>
|
||
<?= esc($e['category']) ?>
|
||
<?php if (strcasecmp($e['category'], 'Donation') === 0): ?>
|
||
<span class="badge bg-info text-dark ms-1">Non-reimbursable</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>$<?= number_format($e['amount'], 2) ?></td>
|
||
<td>
|
||
<?php
|
||
$retail = $e['retailor'] ?? ($e['retailer'] ?? null);
|
||
echo esc($retail ?: 'N/A');
|
||
?>
|
||
</td>
|
||
<td>
|
||
<?= $e['purchaser_firstname'] || $e['purchaser_lastname']
|
||
? esc($e['purchaser_firstname'] . ' ' . $e['purchaser_lastname'])
|
||
: 'N/A' ?>
|
||
</td>
|
||
<td>
|
||
<?php
|
||
$purchaseDate = $e['date_of_purchase']
|
||
?? ($e['purchase_date']
|
||
?? ($e['purchased_at']
|
||
?? ($e['date_purchased']
|
||
?? null)));
|
||
if (!empty($purchaseDate)) {
|
||
$purchaseDisp = local_date($purchaseDate, 'm-d-Y');
|
||
} elseif (!empty($e['created_at'])) {
|
||
$purchaseDisp = local_date($e['created_at'], 'm-d-Y');
|
||
} else {
|
||
$purchaseDisp = '-';
|
||
}
|
||
echo esc($purchaseDisp);
|
||
?>
|
||
</td>
|
||
<td><?= esc($e['description']) ?></td>
|
||
<td>
|
||
<span class="badge bg-<?= $e['status'] === 'approved' ? 'success' : ($e['status'] === 'denied' ? 'danger' : 'secondary') ?>">
|
||
<?= ucfirst($e['status']) ?>
|
||
</span>
|
||
<?php if (strcasecmp($e['category'], 'Donation') === 0): ?>
|
||
<span class="badge bg-info text-dark ms-1">Donation</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?= esc($e['approver_firstname'] . ' ' . $e['approver_lastname'] ?? '-') ?></td>
|
||
<!--td><--?= esc($e['status_reason'] ?? '-') ?></td-->
|
||
<td>
|
||
<?php if ($e['receipt_path']): ?>
|
||
<!--a href="<--?= base_url('writable/' . $e['receipt_path']) ?>" target="_blank">View</a-->
|
||
<a href="<?= site_url('receipts/' . $e['receipt_path']) ?>" target="_blank">View receipt</a>
|
||
<?php else: ?>
|
||
N/A
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?= esc(!empty($e['created_at']) ? local_datetime($e['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||
<td class="d-flex gap-2">
|
||
<a class="btn btn-outline-secondary btn-sm" href="<?= site_url('expenses/edit/' . $e['id']) ?>">Edit</a>
|
||
<?php if (strcasecmp($e['category'], 'Donation') === 0): ?>
|
||
<span class="text-muted small align-self-center">Donation – no reimbursement actions</span>
|
||
<?php elseif ($e['status'] === 'pending'): ?>
|
||
<button class="btn btn-success btn-sm" onclick="handleExpenseAction(<?= $e['id'] ?>, 'approved')">Approve</button>
|
||
<button class="btn btn-danger btn-sm" onclick="handleExpenseAction(<?= $e['id'] ?>, 'denied')">Deny</button>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
<tfoot class="table-light">
|
||
<tr>
|
||
<th>Total</th>
|
||
<th>$<?= number_format($totalAmount, 2) ?></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Removed local sticky CSS; DataTables FixedHeader handles header pinning -->
|
||
|
||
</div>
|
||
<?= $this->endSection() ?>
|
||
|
||
<?= $this->section('scripts') ?>
|
||
<script>
|
||
(function(){
|
||
const CSRF_COOKIE_NAME = <?= json_encode(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>;
|
||
function getCookie(name){
|
||
return document.cookie.split(';').map(v=>v.trim()).find(v=>v.startsWith(name+'='))?.split('=')[1] || '';
|
||
}
|
||
|
||
window.handleExpenseAction = function(id, status) {
|
||
if (!confirm(`Are you sure you want to ${status} this expense?`)) return;
|
||
|
||
const csrf = decodeURIComponent(getCookie(CSRF_COOKIE_NAME) || '');
|
||
fetch("<?= base_url('expenses/updateStatus') ?>", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': csrf || '<?= csrf_hash() ?>'
|
||
},
|
||
// No 'reason' sent
|
||
body: JSON.stringify({
|
||
id,
|
||
status
|
||
})
|
||
})
|
||
.then(res => res.json())
|
||
.then(response => {
|
||
if (response.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(response.error || 'Action failed.');
|
||
}
|
||
})
|
||
.catch(() => alert('Server error.'));
|
||
}
|
||
})();
|
||
</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('#expensesTable').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() ?>
|