Files
alrahma_sunday_school/app/Views/reimbursements/index.php
T
2026-02-10 22:11:06 -05:00

578 lines
28 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid mt-4">
<h2 class="text-center mt-4 mb-3">Reimbursed Expenses</h2>
<form method="get" action="<?= base_url('reimbursements') ?>" class="row g-3 mb-4">
<!-- Semester -->
<div class="col-md-2">
<label class="form-label">Semester</label>
<select name="semester" class="form-select">
<option value="">All</option>
<option value="Fall" <?= request()->getGet('semester') === 'Fall' ? 'selected' : '' ?>>Fall</option>
<option value="Spring" <?= request()->getGet('semester') === 'Spring' ? 'selected' : '' ?>>Spring</option>
</select>
</div>
<!-- Status (from reimbursements.status) -->
<div class="col-md-2">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">All</option>
<?php foreach (['Pending', 'Approved', 'Rejected', 'Paid'] as $opt): ?>
<option value="<?= esc($opt) ?>" <?= request()->getGet('status') === $opt ? 'selected' : '' ?>>
<?= esc($opt) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- School Year -->
<div class="col-md-2">
<label class="form-label">School Year</label>
<select name="school_year" class="form-select">
<option value="">All</option>
<?php foreach ($schoolYears as $sy): ?>
<option value="<?= esc($sy) ?>" <?= request()->getGet('school_year') == $sy ? 'selected' : '' ?>>
<?= esc($sy) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- User -->
<div class="col-md-3">
<label class="form-label">Reimbursed To</label>
<select name="user_id" class="form-select">
<option value="">All</option>
<?php foreach ($users as $u): ?>
<option value="<?= esc($u['id']) ?>" <?= request()->getGet('user_id') == $u['id'] ? 'selected' : '' ?>>
<?= esc(trim(($u['firstname'] ?? '') . ' ' . ($u['lastname'] ?? ''))) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 d-flex gap-2 justify-content-end align-items-end mb-3">
<button class="btn btn-primary" type="submit">Filter</button>
<a href="<?= base_url('reimbursements/export?' . http_build_query($_GET ?? [])) ?>" class="btn btn-success">
Export CSV
</a>
<a href="<?= base_url('reimbursements/under-processing') ?>" class="btn btn-warning">
Reimbursement Under Processing
</a>
</div>
</form>
<?php if (!empty($batchSummaries ?? []) || !empty($donationBatch ?? null)): ?>
<div class="mb-4 batch-summary-container">
<div class="d-flex align-items-center gap-2 mb-2">
<h5 class="mb-0">Submitted Batches</h5>
</div>
<div class="row g-3 batch-stack">
<?php foreach ($batchSummaries as $batch): ?>
<div class="col-12">
<div class="card shadow-sm h-100 batch-card" data-batch-id="<?= (int)($batch['batch_id'] ?? 0) ?>">
<button class="card-body text-start w-100 border-0 bg-white p-3 batch-toggle" type="button">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="card-title mb-1"><?= esc($batch['title'] ?? ('Batch #' . ($batch['batch_id'] ?? ''))) ?></h6>
<div class="text-muted small">
Items: <?= (int)($batch['items'] ?? 0) ?> • Amount: $<?= number_format((float)($batch['amount'] ?? 0), 2) ?>
<?php if (!empty($batch['closed_at'])): ?>
• Closed: <?= esc($batch['closed_at']) ?>
<?php endif; ?>
</div>
</div>
<span class="badge bg-light text-dark">Expand</span>
</div>
</button>
<div class="collapse batch-details px-3 pb-3">
<?php $details = $batchDetails[$batch['batch_id']] ?? ['items'=>[], 'checks'=>[]]; ?>
<?php if (!empty($details['items'])): ?>
<?php
$itemsByPurchaser = [];
foreach ($details['items'] as $item) {
$purchaser = trim((string) ($item['purchased_by'] ?? ''));
if ($purchaser === '') {
$purchaser = 'Unknown';
}
if (!isset($itemsByPurchaser[$purchaser])) {
$itemsByPurchaser[$purchaser] = [
'total' => 0.0,
'items' => [],
];
}
$amount = (float) ($item['amount'] ?? 0);
$itemsByPurchaser[$purchaser]['total'] += $amount;
$itemsByPurchaser[$purchaser]['items'][] = $item;
}
if (!empty($itemsByPurchaser)) {
uksort($itemsByPurchaser, static function ($a, $b) {
return strcasecmp($a, $b);
});
}
?>
<div class="mt-2">
<div class="fw-semibold small text-muted mb-2">Items</div>
<?php foreach ($itemsByPurchaser as $purchaser => $group): ?>
<div class="purchaser-group mb-3">
<div class="d-flex justify-content-between align-items-center purchaser-group-header">
<div class="fw-semibold"><?= esc($purchaser) ?></div>
<div class="text-muted small">Total: $<?= number_format((float) ($group['total'] ?? 0), 2) ?></div>
</div>
<ul class="list-group list-group-flush">
<?php foreach ($group['items'] as $item): ?>
<li class="list-group-item px-0 d-flex justify-content-between">
<div>
<div class="fw-semibold"><?= esc($item['description'] ?: 'No description') ?></div>
<div class="text-muted small">
<?= esc($item['vendor'] ?: 'Vendor N/A') ?>
<?php if (!empty($item['receipt_url'])): ?>
• <a href="<?= esc($item['receipt_url']) ?>" target="_blank" rel="noopener">Receipt</a>
<?php endif; ?>
</div>
</div>
<div class="fw-semibold">$<?= number_format((float)($item['amount'] ?? 0), 2) ?></div>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($details['checks'])): ?>
<div class="mt-3">
<div class="fw-semibold small text-muted mb-2">Uploaded Checks</div>
<ul class="list-group list-group-flush">
<?php foreach ($details['checks'] as $file): ?>
<li class="list-group-item px-0 d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold"><?= esc($file['admin'] ?? 'Admin') ?></div>
<div class="text-muted small"><?= esc($file['original'] ?? $file['filename'] ?? '') ?></div>
</div>
<?php if (!empty($file['url'])): ?>
<a class="btn btn-sm btn-outline-primary" href="<?= esc($file['url']) ?>" target="_blank" rel="noopener">View</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
<div class="card-footer bg-light d-flex justify-content-between align-items-center flex-wrap gap-2">
<span class="text-muted small">ID: <?= (int)($batch['batch_id'] ?? 0) ?></span>
<div class="d-flex gap-2">
<button type="button"
class="btn btn-sm btn-outline-primary send-batch-email"
data-batch-id="<?= (int)($batch['batch_id'] ?? 0) ?>">
Send To
</button>
<a class="btn btn-sm btn-outline-success" href="<?= base_url('reimbursements/batch/export?batch_id=' . ($batch['batch_id'] ?? 0)) ?>" target="_blank" rel="noopener">
Download CSV
</a>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (!empty($donationBatch)): ?>
<div class="col-12">
<div class="card shadow-sm h-100 batch-card donation-batch">
<button class="card-body text-start w-100 border-0 bg-white p-3 batch-toggle" type="button">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="card-title mb-1"><?= esc($donationBatch['title'] ?? 'Donations') ?></h6>
<div class="text-muted small">
Items: <?= (int)($donationBatch['items'] ?? 0) ?> • Amount: $<?= number_format((float)($donationBatch['amount'] ?? 0), 2) ?>
</div>
</div>
<span class="badge bg-light text-dark">Expand</span>
</div>
</button>
<div class="collapse batch-details px-3 pb-3">
<?php $details = $donationDetails ?? ['items'=>[], 'checks'=>[]]; ?>
<?php if (!empty($details['items'])): ?>
<?php
$itemsByPurchaser = [];
foreach ($details['items'] as $item) {
$purchaser = trim((string) ($item['purchased_by'] ?? ''));
if ($purchaser === '') {
$purchaser = 'Unknown';
}
if (!isset($itemsByPurchaser[$purchaser])) {
$itemsByPurchaser[$purchaser] = [
'total' => 0.0,
'items' => [],
];
}
$amount = (float) ($item['amount'] ?? 0);
$itemsByPurchaser[$purchaser]['total'] += $amount;
$itemsByPurchaser[$purchaser]['items'][] = $item;
}
if (!empty($itemsByPurchaser)) {
uksort($itemsByPurchaser, static function ($a, $b) {
return strcasecmp($a, $b);
});
}
?>
<div class="mt-2">
<div class="fw-semibold small text-muted mb-2">Items</div>
<?php foreach ($itemsByPurchaser as $purchaser => $group): ?>
<div class="purchaser-group mb-3">
<div class="d-flex justify-content-between align-items-center purchaser-group-header">
<div class="fw-semibold"><?= esc($purchaser) ?></div>
<div class="text-muted small">Total: $<?= number_format((float) ($group['total'] ?? 0), 2) ?></div>
</div>
<ul class="list-group list-group-flush">
<?php foreach ($group['items'] as $item): ?>
<li class="list-group-item px-0 d-flex justify-content-between">
<div>
<div class="fw-semibold"><?= esc($item['description'] ?: 'No description') ?></div>
<div class="text-muted small">
<?= esc($item['vendor'] ?: 'Vendor N/A') ?>
<?php if (!empty($item['receipt_url'])): ?>
• <a href="<?= esc($item['receipt_url']) ?>" target="_blank" rel="noopener">Receipt</a>
<?php endif; ?>
</div>
</div>
<div class="fw-semibold">$<?= number_format((float)($item['amount'] ?? 0), 2) ?></div>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="card-footer bg-light d-flex justify-content-between align-items-center">
<span class="text-muted small">Category: Donation</span>
<span class="text-muted small">Not reimbursable</span>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<div class="alert alert-secondary mb-0">
Individual reimbursement table is hidden. Use the submitted batch cards above or export CSV for details.
</div>
</div>
<div class="modal fade" id="batchEmailModal" tabindex="-1" aria-labelledby="batchEmailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<form id="batchEmailForm" class="needs-validation" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="batch_id" id="batchEmailBatchId">
<div class="modal-header">
<h5 class="modal-title" id="batchEmailModalLabel">Email Batch</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="batchEmailRecipient" class="form-label">Recipient Email</label>
<input type="email" name="recipient_email" id="batchEmailRecipient" class="form-control" placeholder="recipient@example.com" required>
</div>
<div class="mb-3">
<label for="batchEmailMessage" class="form-label">Message (optional)</label>
<textarea name="message" id="batchEmailMessage" class="form-control" rows="4" placeholder="Add a quick note..."></textarea>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-semibold">Attachments</span>
<small class="text-muted">Uncheck to omit from the email.</small>
</div>
<div class="list-group list-group-flush" id="batchEmailAttachmentList"></div>
<p class="text-muted small mt-2 mb-0">Receipts and check scans are packaged into zipped archives.</p>
</div>
<div id="batchEmailStatus" class="alert d-none" role="alert"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Send Email</button>
</div>
</form>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('styles') ?>
<style>
/* Match under-processing view to prevent header crowding/overlap */
.wrap-headers thead th { white-space: normal !important; vertical-align: middle; }
.wrap-headers td { vertical-align: middle; }
.batch-card .batch-details { display: none; }
.batch-card.open .batch-details { display: block; }
.batch-card .batch-toggle { width: 100%; text-align: left; }
.batch-summary-container { max-width: none; }
.batch-stack { width: 100%; }
.purchaser-group {
background: #f8f9fa;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 0.75rem;
}
.purchaser-group-header {
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.4rem;
margin-bottom: 0.5rem;
}
.purchaser-group .list-group-item {
background: transparent;
}
</style>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
window.reimbursementBatchAttachments = <?= json_encode($batchAttachments ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.batch-card').forEach((card) => {
const toggle = card.querySelector('.batch-toggle');
if (!toggle) return;
toggle.addEventListener('click', () => {
card.classList.toggle('open');
});
});
const modalElement = document.getElementById('batchEmailModal');
if (!modalElement || typeof bootstrap === 'undefined') {
return;
}
const modal = new bootstrap.Modal(modalElement);
const form = document.getElementById('batchEmailForm');
const attachmentList = document.getElementById('batchEmailAttachmentList');
const statusAlert = document.getElementById('batchEmailStatus');
const batchIdInput = document.getElementById('batchEmailBatchId');
const recipientInput = document.getElementById('batchEmailRecipient');
const messageInput = document.getElementById('batchEmailMessage');
const modalLabel = document.getElementById('batchEmailModalLabel');
const sendUrl = '<?= base_url('reimbursements/batch/send') ?>';
const csrfTokenName = '<?= csrf_token() ?>';
const csrfCookieName = '<?= config('Security')->cookieName ?: (config('Security')->csrfCookieName ?? 'csrf_cookie_name') ?>';
const attachmentsData = window.reimbursementBatchAttachments || {};
const sendButtons = document.querySelectorAll('.send-batch-email');
sendButtons.forEach((button) => {
button.addEventListener('click', () => {
const batchId = button.dataset.batchId;
batchIdInput.value = batchId ?? '';
const cardTitle = button.closest('.batch-card')?.querySelector('.card-title')?.textContent?.trim();
modalLabel.textContent = cardTitle ? `Email ${cardTitle}` : 'Email Batch';
recipientInput.value = '';
messageInput.value = '';
if (statusAlert) {
statusAlert.className = 'alert d-none';
statusAlert.textContent = '';
}
populateAttachments(batchId);
modal.show();
});
});
if (form) {
form.addEventListener('submit', async (event) => {
event.preventDefault();
const submitButton = form.querySelector('button[type="submit"]');
if (!submitButton) {
return;
}
let shouldReload = false;
submitButton.disabled = true;
const originalLabel = submitButton.textContent;
submitButton.textContent = 'Sending...';
if (statusAlert) {
statusAlert.className = 'alert alert-info';
statusAlert.textContent = 'Sending email...';
}
const selectedReceipts = gatherSelectedIds('receipt');
const selectedChecks = gatherSelectedIds('check');
const formData = new FormData(form);
syncCsrfToken(formData);
formData.set('receipts', JSON.stringify(selectedReceipts));
formData.set('checks', JSON.stringify(selectedChecks));
try {
const response = await fetch(sendUrl, {
method: 'POST',
body: formData,
});
const data = await response.json().catch(() => ({}));
if (response.ok && data.success) {
if (statusAlert) {
statusAlert.className = 'alert alert-success';
statusAlert.textContent = data.message || 'Email sent successfully.';
}
modal.hide();
form.reset();
if (attachmentList) {
attachmentList.innerHTML = '';
}
shouldReload = true;
} else {
throw new Error(data.error || 'Unable to send the email.');
}
} catch (error) {
if (statusAlert) {
statusAlert.className = 'alert alert-danger';
statusAlert.textContent = error.message || 'An unexpected error occurred.';
}
} finally {
submitButton.disabled = false;
submitButton.textContent = originalLabel;
if (shouldReload) {
setTimeout(() => window.location.reload(), 200);
}
}
});
}
function populateAttachments(batchId) {
if (!attachmentList) {
return;
}
attachmentList.innerHTML = '';
const batchEntries = attachmentsData[batchId] ?? { receipts: [], checks: [] };
let rendered = false;
if (batchEntries.receipts?.length) {
const header = document.createElement('div');
header.className = 'list-group-item px-0 border-0 pb-0 fw-semibold';
header.textContent = 'Receipts';
attachmentList.appendChild(header);
batchEntries.receipts.forEach((item) => {
const vendor = item.vendor || 'Vendor N/A';
const amount = parseFloat(item.amount || 0);
const date = item.date_of_purchase ? item.date_of_purchase.split(' ')[0] : '';
const descriptionParts = [vendor];
if (!Number.isNaN(amount) && amount > 0) {
descriptionParts.push(`$${amount.toFixed(2)}`);
}
if (date) {
descriptionParts.push(date);
}
if (item.description) {
descriptionParts.push(item.description);
}
const detail = descriptionParts.join(' • ');
attachmentList.appendChild(
createAttachmentRow(
'receipt',
item.expense_id,
vendor,
detail,
item.receipt_url,
'View receipt'
)
);
rendered = true;
});
}
if (batchEntries.checks?.length) {
const header = document.createElement('div');
header.className = 'list-group-item px-0 border-0 pb-0 fw-semibold mt-3';
header.textContent = 'Check Scans';
attachmentList.appendChild(header);
batchEntries.checks.forEach((file) => {
const meta = file.original_filename || file.filename || 'Check';
attachmentList.appendChild(
createAttachmentRow(
'check',
file.file_id,
file.admin || 'Check Scan',
meta,
file.url,
'View file'
)
);
rendered = true;
});
}
if (!rendered) {
const empty = document.createElement('p');
empty.className = 'text-muted small mb-0';
empty.textContent = 'No attachments are available for this batch.';
attachmentList.appendChild(empty);
}
}
function syncCsrfToken(formData) {
if (!csrfTokenName || !csrfCookieName) {
return;
}
const cookieValue = getCookieValue(csrfCookieName);
if (!cookieValue) {
return;
}
formData.set(csrfTokenName, cookieValue);
const hiddenInput = form?.querySelector(`input[name="${csrfTokenName}"]`);
if (hiddenInput) {
hiddenInput.value = cookieValue;
}
}
function getCookieValue(name) {
const escaped = name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : '';
}
function createAttachmentRow(type, id, title, meta, url, linkLabel) {
const wrapper = document.createElement('div');
wrapper.className = 'list-group-item border-0 px-0 py-2 d-flex align-items-start gap-3';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'form-check-input mt-1';
checkbox.dataset.type = type;
checkbox.value = id ?? '';
checkbox.checked = true;
wrapper.appendChild(checkbox);
const body = document.createElement('div');
body.className = 'flex-grow-1';
const titleEl = document.createElement('div');
titleEl.className = 'fw-semibold';
titleEl.textContent = title;
body.appendChild(titleEl);
if (meta) {
const metaEl = document.createElement('div');
metaEl.className = 'text-muted small';
metaEl.textContent = meta;
if (url) {
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener';
link.className = 'ms-1';
link.textContent = linkLabel;
metaEl.appendChild(link);
}
body.appendChild(metaEl);
}
wrapper.appendChild(body);
return wrapper;
}
function gatherSelectedIds(type) {
if (!attachmentList) {
return [];
}
return Array.from(attachmentList.querySelectorAll(`input[data-type="${type}"]:checked`))
.map((input) => Number(input.value) || 0)
.filter((value) => value > 0);
}
});
</script>
<?= $this->endSection() ?>