578 lines
28 KiB
PHP
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() ?>
|