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
+133
View File
@@ -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() ?>
+134
View File
@@ -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() ?>
+189
View File
@@ -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() ?>