recreate project
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h1>Add Expense / Purchase</h1>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?= session()->getFlashdata('error') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="<?= base_url('expenses/store') ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-control" required>
|
||||
<option value="Expense">Expense</option>
|
||||
<option value="Purchase">Purchase</option>
|
||||
<option value="Reimbursement">Reimbursement</option>
|
||||
<option value="Donation">Donation (non-reimbursable)</option>
|
||||
</select>
|
||||
<div class="form-text text-muted">Use Donation when someone gifts items to the school so it is counted as an expense but skipped from reimbursement queues.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Amount</label>
|
||||
<input type="number" step="0.01" name="amount" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Vendor</label>
|
||||
<select id="retailor_select" class="form-control">
|
||||
<option value="">-- Select Venror --</option>
|
||||
<?php foreach (($retailors ?? []) as $r): ?>
|
||||
<option value="<?= esc($r) ?>" <?= (old('retailor') === $r) ? 'selected' : '' ?>><?= esc($r) ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php $oldRetailor = (string) old('retailor'); ?>
|
||||
<option value="_other" <?= ($oldRetailor && !in_array($oldRetailor, ($retailors ?? []), true)) ? 'selected' : '' ?>>Other</option>
|
||||
</select>
|
||||
<input type="text" id="retailor_input" name="retailor" class="form-control mt-2" placeholder="Enter retailor"
|
||||
value="<?= esc(old('retailor')) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Purchased By</label>
|
||||
<select name="purchased_by" class="form-control" required>
|
||||
<option value="">-- Select --</option>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<?php
|
||||
$userValue = $user['id'] . '|' . $user['firstname'] . ' ' . $user['lastname'];
|
||||
?>
|
||||
<option value="<?= esc($userValue) ?>">
|
||||
<?= esc($user['firstname'] . ' ' . $user['lastname']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Date of Purchase</label>
|
||||
<input type="date" name="date_of_purchase" class="form-control" value="<?= old('date_of_purchase') ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control"><?= old('description') ?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Receipt Image</label>
|
||||
<input type="file" name="receipt" class="form-control" required>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- school_year and semester are auto-populated in controller -->
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="<?= site_url('expenses/index') ?>" class="btn btn-secondary ms-2">Cancel</a>
|
||||
|
||||
</form>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function(){
|
||||
const select = document.getElementById('retailor_select');
|
||||
const input = document.getElementById('retailor_input');
|
||||
const list = <?php
|
||||
// Safe JSON of retailors for quick client-side check
|
||||
$list = json_encode(array_values($retailors ?? []));
|
||||
echo $list ?: '[]';
|
||||
?>;
|
||||
|
||||
function syncFromSelect() {
|
||||
const val = select.value;
|
||||
if (val === '_other') {
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
if (!input.value) input.focus();
|
||||
} else if (val) {
|
||||
input.value = val;
|
||||
input.setAttribute('readonly', 'readonly');
|
||||
input.classList.add('d-none');
|
||||
} else {
|
||||
// No selection: allow manual entry if desired
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
}
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const current = input.value || '';
|
||||
if (current && list.includes(current)) {
|
||||
// Match in list: select it and hide input
|
||||
for (const opt of select.options) { if (opt.value === current) { opt.selected = true; break; } }
|
||||
input.classList.add('d-none');
|
||||
input.setAttribute('readonly', 'readonly');
|
||||
} else if (current && !list.includes(current)) {
|
||||
// Not in list: choose Other and show input
|
||||
for (const opt of select.options) { if (opt.value === '_other') { opt.selected = true; break; } }
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
} else {
|
||||
// Empty: allow selection or manual entry
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
}
|
||||
|
||||
select.addEventListener('change', syncFromSelect);
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2>Edit Expense #<?= esc($expense['id']) ?></h2>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-danger"><?= implode('<br>', array_map('esc', $errors)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?= site_url('expenses/update/' . $expense['id']) ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Category</label>
|
||||
<select name="category" class="form-control" required>
|
||||
<?php foreach (['Expense', 'Purchase', 'Reimbursement', 'Donation'] as $opt): ?>
|
||||
<option value="<?= $opt ?>" <?= $expense['category'] === $opt ? 'selected' : '' ?>>
|
||||
<?= $opt === 'Donation' ? 'Donation (non-reimbursable)' : $opt ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text text-muted">Donation entries are tracked as expenses but will not move through reimbursement batches.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Amount</label>
|
||||
<input type="number" step="0.01" name="amount" class="form-control"
|
||||
value="<?= old('amount', $expense['amount']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Vendor</label>
|
||||
<?php $currentRetailor = old('retailor', $expense['retailor'] ?? ''); ?>
|
||||
<select id="retailor_select" class="form-control">
|
||||
<option value="">-- Select Vendor --</option>
|
||||
<?php foreach (($retailors ?? []) as $r): ?>
|
||||
<option value="<?= esc($r) ?>" <?= ($currentRetailor === $r) ? 'selected' : '' ?>><?= esc($r) ?></option>
|
||||
<?php endforeach; ?>
|
||||
<option value="_other" <?= ($currentRetailor && !in_array($currentRetailor, ($retailors ?? []), true)) ? 'selected' : '' ?>>Other</option>
|
||||
</select>
|
||||
<input type="text" id="retailor_input" name="retailor" class="form-control mt-2" placeholder="Enter retailor"
|
||||
value="<?= esc($currentRetailor) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purchased By</label>
|
||||
<select name="purchased_by" class="form-control" required>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<?php $val = $u['id'] . '|' . $u['firstname'] . ' ' . $u['lastname']; ?>
|
||||
<option value="<?= esc($val) ?>" <?= ($expense['purchased_by'] == $u['id'] ? 'selected' : '') ?>>
|
||||
<?= esc($u['firstname'] . ' ' . $u['lastname']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Date of Purchase</label>
|
||||
<input type="date" name="date_of_purchase" class="form-control"
|
||||
value="<?= old('date_of_purchase', $expense['date_of_purchase'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<textarea name="description" class="form-control"><?= old('description', $expense['description']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Receipt (optional to replace)</label>
|
||||
<?php if (!empty($receipt_url)): ?>
|
||||
<div class="mb-2"><a href="<?= esc($receipt_url) ?>" target="_blank">Current receipt</a></div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="receipt" class="form-control" accept=".jpg,.jpeg,.png,.webp,.gif,.pdf">
|
||||
<?php if (!empty($expense['receipt_path'])): ?>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" name="remove_receipt" value="1" id="rmr">
|
||||
<label class="form-check-label" for="rmr">Remove existing receipt</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary">Save</button>
|
||||
<a class="btn btn-secondary" href="<?= site_url('expenses/index') ?>">Cancel</a>
|
||||
</form>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function(){
|
||||
const select = document.getElementById('retailor_select');
|
||||
const input = document.getElementById('retailor_input');
|
||||
const list = <?php
|
||||
$list = json_encode(array_values($retailors ?? []));
|
||||
echo $list ?: '[]';
|
||||
?>;
|
||||
|
||||
function syncFromSelect() {
|
||||
const val = select.value;
|
||||
if (val === '_other') {
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
if (!input.value) input.focus();
|
||||
} else if (val) {
|
||||
input.value = val;
|
||||
input.setAttribute('readonly', 'readonly');
|
||||
input.classList.add('d-none');
|
||||
} else {
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
}
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const current = input.value || '';
|
||||
if (current && list.includes(current)) {
|
||||
for (const opt of select.options) { if (opt.value === current) { opt.selected = true; break; } }
|
||||
input.classList.add('d-none');
|
||||
input.setAttribute('readonly', 'readonly');
|
||||
} else if (current && !list.includes(current)) {
|
||||
for (const opt of select.options) { if (opt.value === '_other') { opt.selected = true; break; } }
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
} else {
|
||||
input.classList.remove('d-none');
|
||||
input.removeAttribute('readonly');
|
||||
}
|
||||
|
||||
select.addEventListener('change', syncFromSelect);
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,189 @@
|
||||
<?= $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() ?>
|
||||
Reference in New Issue
Block a user