recreate project
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h1>Add Reimbursement</h1>
|
||||
|
||||
<form method="post" action="<?= base_url('reimbursements/store') ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="expense_id" value="<?= esc($expense_id ?? '') ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="amount"
|
||||
class="form-control"
|
||||
required
|
||||
value="<?= old('amount', isset($prefill_amount) ? number_format((float)$prefill_amount, 2, '.', '') : '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Reimbursed To</label>
|
||||
<?php $selectedRecipient = (string) (old('reimbursed_to') ?? ''); ?>
|
||||
<select name="reimbursed_to" class="form-control" required>
|
||||
<option value="">-- Select Person --</option>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<?php
|
||||
$id = (string) ($user['id'] ?? '');
|
||||
$label = trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? ''));
|
||||
?>
|
||||
<option value="<?= esc($id) ?>" <?= $selectedRecipient !== '' && $selectedRecipient === $id ? 'selected' : '' ?>>
|
||||
<?= esc($label !== '' ? $label : 'Unknown') ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Reimbursement Method</label>
|
||||
<select name="reimbursement_method" id="reimbursement_method" class="form-control" required>
|
||||
<option value="">-- Select Method --</option>
|
||||
<option value="Cash">Cash</option>
|
||||
<option value="Check">Check</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-none" id="check_number_group">
|
||||
<label>Check Number</label>
|
||||
<input type="text" name="check_number" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-none" id="check_receipt_group">
|
||||
<label>Check Receipt Image</label>
|
||||
<input type="file" name="receipt" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<textarea name="description" class="form-control"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="<?= base_url('reimbursements') ?>" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const methodSelect = document.getElementById('reimbursement_method');
|
||||
const checkNumberGroup = document.getElementById('check_number_group');
|
||||
const checkReceiptGroup = document.getElementById('check_receipt_group');
|
||||
|
||||
methodSelect.addEventListener('change', function() {
|
||||
const isCheck = this.value === 'Check';
|
||||
checkNumberGroup.classList.toggle('d-none', !isCheck);
|
||||
checkReceiptGroup.classList.toggle('d-none', !isCheck);
|
||||
checkReceiptGroup.querySelector('input').required = isCheck;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2>Edit Reimbursement #<?= esc($reimb['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('reimbursements/update/'.$reimb['id']) ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Amount</label>
|
||||
<input type="number" step="0.01" name="amount" class="form-control"
|
||||
value="<?= old('amount', $reimb['amount']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Reimbursed To</label>
|
||||
<?php $currentRecipient = (string) (old('reimbursed_to', $reimb['reimbursed_to']) ?? ''); ?>
|
||||
<select name="reimbursed_to" class="form-control" required>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<?php
|
||||
$id = (string) ($u['id'] ?? '');
|
||||
$label = trim(($u['firstname'] ?? '') . ' ' . ($u['lastname'] ?? ''));
|
||||
?>
|
||||
<option value="<?= esc($id) ?>" <?= $currentRecipient === $id ? 'selected' : '' ?>>
|
||||
<?= esc($label !== '' ? $label : 'Unknown') ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Reimbursement Method</label>
|
||||
<select name="reimbursement_method" id="reimbursement_method" class="form-control" required>
|
||||
<option value="Cash" <?= $reimb['reimbursement_method']==='Cash'?'selected':'' ?>>Cash</option>
|
||||
<option value="Check" <?= $reimb['reimbursement_method']==='Check'?'selected':'' ?>>Check</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="check_number_group" style="<?= $reimb['reimbursement_method']==='Check'?'':'display:none' ?>">
|
||||
<label>Check Number</label>
|
||||
<input type="text" name="check_number" class="form-control" value="<?= old('check_number', $reimb['check_number']) ?>">
|
||||
</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($reimb['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>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<textarea name="description" class="form-control"><?= old('description', $reimb['description']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary">Save</button>
|
||||
<a class="btn btn-secondary" href="<?= site_url('reimbursements') ?>">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.getElementById('reimbursement_method').addEventListener('change', e => {
|
||||
document.getElementById('check_number_group').style.display = (e.target.value === 'Check') ? '' : 'none';
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,577 @@
|
||||
<?= $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() ?>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user