<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>

<div class="container-fluid mt-4 reimbursement-batch-page">
    <div class="row reimb-layout">
        <div class="reimb-col reimb-col-list">
            <div class="card shadow-sm h-100">
                <div class="card-header d-flex flex-wrap gap-2 align-items-center">
                    <div>
                        <h5 class="mb-0">Reimbursements Under Processing</h5>
                        <small class="text-muted">Drag items into batches to keep work moving.</small>
                    </div>
                    <div class="ms-auto d-flex flex-wrap gap-2">
                        <button type="button" id="addBatchBtn" class="btn btn-primary btn-sm">
                            <span class="me-1">➕</span>Add New Batch
                        </button>
                        <a href="<?= base_url('reimbursements') ?>" class="btn btn-sm btn-outline-secondary">Reimbursement Main</a>
                        <a href="<?= base_url('reimbursements/export?type=under_processing') ?>" class="btn btn-sm btn-outline-success">Export CSV</a>
                    </div>
                </div>
                <div class="card-body">
                    <p class="text-muted small mb-3">
                        Drag rows into a batch to assign them to admins. Drag cards back into the table to remove them from a batch.
                    </p>
                    <div class="reimb-table-scroll">
                        <div class="table-responsive">
                            <table class="table table-bordered align-middle w-100 wrap-headers mb-0 no-mgmt-sticky">
                                <thead class="table-light">
                                    <tr>
                                        <th>Expense Amount</th>
                                        <th>Vendor</th>
                                        <th>Description</th>
                                        <th>Purchased By</th>
                                        <th>Expense Receipt</th>
                                        <th>Actions</th>
                                    </tr>
                                </thead>
                                <tbody id="pending-list" class="align-middle reimb-draggable-table">
                                    <?php if (empty($pendingItems)): ?>
                                        <tr class="pending-empty">
                                            <td colspan="6" class="text-center text-muted py-4">
                                                All reimbursements are currently assigned to a batch.
                                            </td>
                                        </tr>
                                    <?php else: ?>
                                        <?php foreach ($pendingItems as $item): ?>
                                            <tr
                                                data-item-id="<?= (int) $item['expense_id'] ?>"
                                                data-reimb-id="<?= $item['reimbursement_id'] ? (int) $item['reimbursement_id'] : '' ?>"
                                                draggable="true"
                                            >
                                                <td class="fw-semibold">$<?= number_format((float) $item['expense_amount'], 2) ?></td>
                                                <td><?= esc($item['vendor'] ?: 'N/A') ?></td>
                                                <td><?= esc($item['description'] ?: '-') ?></td>
                                                <td><?= esc($item['purchased_by'] ?: 'Unknown') ?></td>
                                                <td>
                                                    <?php if (!empty($item['receipt_url'])): ?>
                                                        <a href="<?= esc($item['receipt_url']) ?>" target="_blank" rel="noopener">View Receipt</a>
                                                    <?php else: ?>
                                                        <span class="text-muted">N/A</span>
                                                    <?php endif; ?>
                                                </td>
                                                <td>
                                                    <button
                                                        type="button"
                                                        class="btn btn-sm btn-outline-success mark-donation-btn"
                                                        data-item-id="<?= (int) $item['expense_id'] ?>"
                                                        draggable="false"
                                                    >
                                                        Mark as Donation
                                                    </button>
                                                </td>
                                            </tr>
                                        <?php endforeach; ?>
                                    <?php endif; ?>
                                </tbody>
                            </table>
                        </div>
                        <div class="text-muted small mt-2">Tip: Hold and drag a row to assign it to a batch. Drag it back into the table to unassign.</div>
                    </div>
                </div>
            </div>
        </div>

        <div class="reimb-col reimb-col-batches">
            <div class="card shadow-sm h-100">
                <div class="card-header d-flex flex-wrap align-items-center gap-2">
                    <div>
                        <h5 class="mb-0">Active Batches</h5>
                        <small class="text-muted">Pick admins for the batch, then drag reimbursements under each admin.</small>
                    </div>
                </div>
                <div class="card-body">
                    <div class="reimb-batches-scroll">
                        <div id="batch-empty-state" class="alert alert-info py-2 px-3 <?= !empty($existingBatches) ? 'd-none' : '' ?>">
                            No batches yet. Click "Add New Batch" to start organizing reimbursements.
                        </div>
                        <div id="batch-board" class="reimb-batches-grid"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<div class="modal fade" id="receiptModal" tabindex="-1" aria-labelledby="receiptModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-scrollable">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="receiptModalLabel">Expense Receipt</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div class="receipt-viewer-wrapper">
                    <iframe id="receiptViewer" class="receipt-viewer" src="" title="Receipt preview" allowfullscreen></iframe>
                </div>
            </div>
        </div>
    </div>
</div>

<?= $this->endSection() ?>

<?= $this->section('styles') ?>
<style>
.reimbursement-batch-page .wrap-headers thead th {
    white-space: normal;
}
.reimb-layout {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1.5rem;
    align-items: start;
}
.reimb-col {
    min-width: 0;
}
.reimb-col-list,
.reimb-col-batches {
    width: 100%;
}
.reimb-table-scroll,
.reimb-batches-scroll {
    max-height: none;
}
@media (min-width: 992px) {
    .reimb-layout {
        grid-template-columns: 1fr 2fr;
    }
    .reimb-table-scroll,
    .reimb-batches-scroll {
        max-height: calc(100vh - 240px);
        overflow-y: auto;
        padding-right: 0.25rem;
    }
}
@media (min-width: 992px) {
    .reimb-layout {
        grid-template-columns: 1fr 2fr;
    }
}
.reimb-draggable-table tr {
    cursor: grab;
}
.reimb-draggable-table tr.dragging {
    opacity: 0.5;
}
.reimb-draggable-table.drag-over {
    outline: 2px dashed #0d6efd;
    outline-offset: -4px;
}
.reimb-batches-grid {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}
.batch-card .card-header {
    background-color: #f8f9fa;
}
.batch-card .batch-dropzones {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}
.batch-card .batch-dropzone {
    border: 1px dashed #dee2e6;
    border-radius: 0.65rem;
    padding: 0.75rem;
    min-height: 140px;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}
.batch-card .batch-dropzone .dropzone-title {
    gap: 1rem;
}
.batch-card .batch-dropzone .dropzone-title .admin-subtotal {
    font-size: 0.75rem;
    color: #0d6efd;
    font-weight: 600;
}
.batch-card .batch-dropzone.drag-over {
    border-color: #0d6efd;
    background-color: #eef5ff;
}
.batch-card .dropzone-title {
    font-size: 0.85rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.batch-card .dropzone-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.batch-item {
    background-color: #fff;
    border: 1px solid #dee2e6;
    border-radius: 0.5rem;
    padding: 0.5rem 0.75rem;
    cursor: grab;
}
.batch-item.dragging {
    opacity: 0.6;
    cursor: grabbing;
}
.batch-item .item-meta {
    font-size: 0.8rem;
    color: #6c757d;
}
.batch-card .card-footer {
    background-color: #f8f9fa;
}
.batch-card.batch-locked {
    border-color: #d1d1d1;
}
.batch-card.batch-locked .batch-dropzone {
    opacity: 0.75;
}
.batch-card.batch-locked .batch-admin-picker {
    display: none;
}
.batch-card .lock-batch-btn {
    border-width: 1px;
}
.batch-card.batch-locked .lock-batch-btn {
    pointer-events: none;
}
.batch-card .admin-check-wrapper {
    border-top: 1px solid #dee2e6;
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    font-size: 0.85rem;
}
.batch-card .admin-check-link {
    text-decoration: none;
    color: #0d6efd;
}
.batch-card.batch-locked .admin-check-wrapper {
    pointer-events: none;
    opacity: 0.6;
}
.batch-card .admin-check-wrapper input,
.batch-card .admin-check-wrapper .admin-check-link {
    pointer-events: auto;
}
.batch-card .toggle-card-btn {
    color: #0d6efd;
    border-color: #d5def0;
    background-color: #f6f9ff;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}
.batch-card .toggle-card-btn .toggle-icon {
    transition: transform 0.15s ease;
    display: inline-block;
}
.batch-card.batch-collapsed .toggle-card-btn .toggle-icon {
    transform: rotate(-90deg);
}
.batch-card.batch-collapsed .card-body,
.batch-card.batch-collapsed .card-footer {
    display: none;
}
.reimbursement-batch-page .receipt-link {
    text-decoration: underline;
}
.receipt-viewer-wrapper {
    border-radius: 0.5rem;
    overflow: hidden;
    background-color: #fff;
}
.receipt-viewer {
    width: 100%;
    min-height: 420px;
    border: none;
}
</style>
<?= $this->endSection() ?>

<?= $this->section('scripts') ?>
<script>
(() => {
    const cfg = {
        items: <?= json_encode(array_values($itemsPayload), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>,
        batches: <?= json_encode($existingBatches, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>,
        admins: <?= json_encode(array_values($adminUsers), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>,
        createBatchUrl: <?= json_encode(base_url('reimbursements/batch/create')) ?>,
        lockBatchUrl: <?= json_encode(base_url('reimbursements/batch/lock')) ?>,
        checkUploadUrl: <?= json_encode(base_url('reimbursements/batch/admin-file/upload')) ?>,
        updateUrl: <?= json_encode(base_url('reimbursements/batch/update')) ?>,
        batchExportUrl: <?= json_encode(base_url('reimbursements/batch/export')) ?>,
        markDonationUrl: <?= json_encode(base_url('reimbursements/mark-donation')) ?>,
        csrfToken: <?= json_encode(csrf_token()) ?>,
        csrfHash: <?= json_encode(csrf_hash()) ?>,
    };

    const tableBody = document.getElementById('pending-list');
    const batchBoard = document.getElementById('batch-board');
    if (!tableBody || !batchBoard) {
        return;
    }

    const addBatchBtn = document.getElementById('addBatchBtn');
    const pageContainer = document.querySelector('.reimbursement-batch-page');
    const emptyState = document.getElementById('batch-empty-state');
    const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
    const receiptModalEl = document.getElementById('receiptModal');
    const receiptViewer = receiptModalEl?.querySelector('#receiptViewer');
    const bootstrap = window.bootstrap;
    const receiptModalInstance = receiptModalEl && bootstrap && typeof bootstrap.Modal === 'function'
        ? new bootstrap.Modal(receiptModalEl)
        : null;
    if (receiptModalEl && receiptModalInstance) {
        receiptModalEl.addEventListener('hidden.bs.modal', () => {
            if (receiptViewer) {
                receiptViewer.src = '';
            }
        });
    }

    const itemMap = new Map();
    (cfg.items || []).forEach((raw) => {
        if (!raw || !raw.id) {
            return;
        }
        const rawBatchId = Number(raw.batch_id ?? raw.batchId ?? raw.batchNumber ?? raw.batch_number) || 0;
        itemMap.set(Number(raw.id), {
            id: Number(raw.id),
            expense_amount: Number(raw.expense_amount) || 0,
            vendor: raw.vendor || 'N/A',
            description: raw.description || '',
            purchased_by: raw.purchased_by || 'Unknown',
            receipt_url: raw.receipt_url || '',
            batch_number: rawBatchId,
            batchId: rawBatchId,
            reimbursement_id: raw.reimbursement_id ? Number(raw.reimbursement_id) : null,
        });
    });

    const admins = (cfg.admins || []).map((admin) => ({
        id: Number(admin.id),
        name: `${(admin.firstname || '').trim()} ${(admin.lastname || '').trim()}`.trim() || 'Admin',
    }));
    let tempBatchCounter = -1;

    let csrfValue = cfg.csrfHash || '';
    const csrfCookieName = <?= json_encode(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>;
    let csrfRequestQueue = Promise.resolve();
    let dragPayload = null;

    function getCookie(name) {
        const parts = document.cookie.split(';');
        for (let i = 0; i < parts.length; i += 1) {
            const part = parts[i].trim();
            if (part.startsWith(`${name}=`)) {
                return decodeURIComponent(part.slice(name.length + 1));
            }
        }
        return '';
    }

    function syncCsrfFromCookie() {
        if (!csrfCookieName) {
            return csrfValue;
        }
        const cookieValue = getCookie(csrfCookieName);
        if (cookieValue) {
            csrfValue = cookieValue;
        }
        return csrfValue;
    }

    function updateCsrfToken(response, data = null) {
        const headerValue = response?.headers?.get?.('X-CSRF-HASH') || '';
        if (headerValue) {
            csrfValue = headerValue;
            return;
        }
        const bodyValue = data && typeof data === 'object' ? (data.csrf_hash || data.csrfHash || '') : '';
        if (bodyValue) {
            csrfValue = bodyValue;
            return;
        }
        syncCsrfFromCookie();
    }

    function withSerializedCsrfRequest(task) {
        const run = csrfRequestQueue
            .catch(() => undefined)
            .then(() => task());
        csrfRequestQueue = run.then(() => undefined, () => undefined);
        return run;
    }

    async function postJson(url, form, fallbackError) {
        return withSerializedCsrfRequest(async () => {
            syncCsrfFromCookie();
            if (cfg.csrfToken) {
                form.set(cfg.csrfToken, csrfValue);
            }

            const headers = { 'X-Requested-With': 'XMLHttpRequest' };
            if (csrfValue) {
                headers['X-CSRF-TOKEN'] = csrfValue;
            }

            const response = await fetch(url, {
                method: 'POST',
                headers,
                credentials: 'same-origin',
                body: form,
            });

            const text = await response.text();
            let data = null;
            try {
                data = text ? JSON.parse(text) : {};
            } catch (_) {
                data = {
                    success: false,
                    error: text || fallbackError || 'Unexpected response',
                };
            }

            updateCsrfToken(response, data);

            if (!response.ok || !data.success) {
                throw new Error(data.error || fallbackError || 'Request failed.');
            }

            return data;
        });
    }

    function openReceiptModal(url) {
        if (!url) {
            return;
        }
        if (receiptModalInstance && receiptViewer) {
            receiptViewer.src = url;
            receiptModalInstance.show();
            return;
        }
        window.open(url, '_blank', 'noopener');
    }

    function handleReceiptLinkClick(event) {
        const trigger = event.target.closest('[data-receipt-url]');
        if (!trigger) {
            return;
        }
        const url = trigger.dataset.receiptUrl;
        if (!url) {
            return;
        }
        event.preventDefault();
        openReceiptModal(url);
    }

    function handleDonationClick(event) {
        const trigger = event.target.closest('.mark-donation-btn');
        if (!trigger) {
            return;
        }
        event.preventDefault();
        event.stopPropagation();
        const itemId = Number(trigger.dataset.itemId || 0);
        if (!itemId) {
            return;
        }
        markDonation(itemId);
    }

    function toggleBatchEmptyState() {
        if (!emptyState) {
            return;
        }
        const hasBatch = !!batchBoard.querySelector('.batch-card');
        emptyState.classList.toggle('d-none', hasBatch);
    }

    function refreshPendingEmptyState() {
        const hasRow = !!tableBody.querySelector('tr[data-item-id]');
        const emptyRow = tableBody.querySelector('.pending-empty');
        if (!hasRow && !emptyRow) {
            const tr = document.createElement('tr');
            tr.className = 'pending-empty';
            const td = document.createElement('td');
            td.colSpan = 6;
            td.className = 'text-center text-muted py-4';
            td.textContent = 'All reimbursements are currently assigned to a batch.';
            tr.appendChild(td);
            tableBody.appendChild(tr);
        } else if (hasRow && emptyRow) {
            emptyRow.remove();
        }
    }

    function removeItemFromUI(itemId) {
        const row = tableBody.querySelector(`tr[data-item-id="${itemId}"]`);
        if (row) {
            row.remove();
        }
        batchBoard.querySelectorAll(`.batch-item[data-item-id="${itemId}"]`).forEach((card) => {
            const batchId = Number(card.closest('.batch-card')?.dataset.batchId || 0);
            card.remove();
            if (batchId) {
                updateBatchTotals(batchId);
            }
        });
        itemMap.delete(itemId);
        refreshPendingEmptyState();
    }

    async function markDonation(itemId) {
        if (!cfg.markDonationUrl) {
            alert('Donation updates are not available right now.');
            return;
        }
        const entry = itemMap.get(itemId);
        if (!entry) {
            return;
        }
        const confirmMsg = 'Mark this expense as a donation? It will be removed from reimbursement batches.';
        if (!confirm(confirmMsg)) {
            return;
        }
        const form = new FormData();
        form.append('expense_id', itemId);
        try {
            await postJson(cfg.markDonationUrl, form, 'Unable to mark donation right now.');
            removeItemFromUI(itemId);
        } catch (err) {
            console.error(err);
            alert(err.message || 'Unable to mark donation right now.');
        }
    }

    function attachRowHandlers(row) {
        if (!row || row.dataset.bound === '1') {
            return;
        }
        row.dataset.bound = '1';
        row.addEventListener('dragstart', (event) => {
            const itemId = Number(row.dataset.itemId);
            if (!itemMap.has(itemId)) {
                return;
            }
            const dt = event.dataTransfer;
            if (dt) {
                dt.effectAllowed = 'move';
                dt.setData('text/plain', String(itemId));
            }
            row.classList.add('dragging');
            dragPayload = { source: 'table', itemId };
        });
        row.addEventListener('dragend', () => {
            row.classList.remove('dragging');
            dragPayload = null;
        });
    }

    function buildTableRow(item) {
        const tr = document.createElement('tr');
        tr.dataset.itemId = item.id;
        if (item.reimbursement_id) {
            tr.dataset.reimbId = item.reimbursement_id;
        } else {
            delete tr.dataset.reimbId;
        }
        tr.draggable = true;
        tr.innerHTML = `
            <td class="fw-semibold">${currency.format(item.expense_amount)}</td>
            <td>${item.vendor || 'N/A'}</td>
            <td>${item.description || '-'}</td>
            <td>${item.purchased_by || 'Unknown'}</td>
            <td>${item.receipt_url ? `<a href="${item.receipt_url}" class="receipt-link" data-receipt-url="${item.receipt_url}" target="_blank" rel="noopener">View Receipt</a>` : '<span class="text-muted">N/A</span>'}</td>
            <td>
                <button type="button" class="btn btn-sm btn-outline-success mark-donation-btn" data-item-id="${item.id}" draggable="false">
                    Mark as Donation
                </button>
            </td>
        `;
        attachRowHandlers(tr);
        return tr;
    }

    function createBatchItemElement(item, adminId) {
        const div = document.createElement('div');
        div.className = 'batch-item';
        div.draggable = true;
        div.dataset.itemId = item.id;
        div.dataset.adminId = adminId ?? 0;
        if (item.reimbursement_id) {
            div.dataset.reimbId = item.reimbursement_id;
        } else {
            delete div.dataset.reimbId;
        }
        const receiptLinkHtml = item.receipt_url
            ? `<a href="${item.receipt_url}" class="receipt-link small text-decoration-none" data-receipt-url="${item.receipt_url}" target="_blank" rel="noopener">View Receipt</a>`
            : '<span class="text-muted small">No receipt</span>';
        div.innerHTML = `
            <div class="d-flex justify-content-between align-items-start">
                <div>
                    <div class="fw-semibold">${item.description || 'No description'}</div>
                    <div class="item-meta">${item.vendor || 'N/A'} • ${item.purchased_by || 'Unknown'}</div>
                </div>
                <div class="text-end">
                    <strong>${currency.format(item.expense_amount)}</strong>
                    <div class="mt-1">${receiptLinkHtml}</div>
                </div>
            </div>
        `;
        div.addEventListener('dragstart', (event) => {
            const batchId = Number(div.closest('.batch-card')?.dataset.batchId || 0);
            const adminSlot = Number(div.dataset.adminId || 0);
            const dt = event.dataTransfer;
            if (dt) {
                dt.effectAllowed = 'move';
                dt.setData('text/plain', String(item.id));
            }
            div.classList.add('dragging');
            dragPayload = { source: 'batch', itemId: item.id, fromBatchId: batchId, fromAdminId: adminSlot };
        });
        div.addEventListener('dragend', () => {
            div.classList.remove('dragging');
            dragPayload = null;
        });
        return div;
    }

    function updateBatchTotals(batchId) {
        const card = batchBoard.querySelector(`.batch-card[data-batch-id="${batchId}"]`);
        if (!card) {
            return;
        }
        let total = 0;
        let count = 0;
        card.querySelectorAll('.batch-dropzone').forEach((zone) => {
            let zoneTotal = 0;
            zone.querySelectorAll('.batch-item').forEach((el) => {
                const item = itemMap.get(Number(el.dataset.itemId));
                if (!item) {
                    return;
                }
                count += 1;
                const amount = Number(item.expense_amount || 0);
                total += amount;
                zoneTotal += amount;
            });
            const summary = zone.querySelector('.admin-subtotal');
            if (summary) {
                summary.textContent = `Subtotal: ${currency.format(zoneTotal)}`;
            }
        });
        const itemsTarget = card.querySelector('.total-items');
        const amountTarget = card.querySelector('.total-amount');
        const headerItems = card.querySelector('.header-total-items');
        const headerAmount = card.querySelector('.header-total-amount');
        if (itemsTarget) {
            itemsTarget.textContent = count;
        }
        if (amountTarget) {
            amountTarget.textContent = currency.format(total);
        }
        if (headerItems) {
            headerItems.textContent = count;
        }
        if (headerAmount) {
            headerAmount.textContent = currency.format(total);
        }
        const exportBtn = card.querySelector('.batch-export-btn');
        if (exportBtn) {
            exportBtn.disabled = count === 0 || !cfg.batchExportUrl;
        }
        card.dataset.itemCount = String(count);
        updateLockButtonState(card);
    }

    function setCardCollapsed(card, collapsed) {
        if (!card) {
            return;
        }
        const shouldCollapse = collapsed === undefined
            ? !card.classList.contains('batch-collapsed')
            : !!collapsed;
        card.classList.toggle('batch-collapsed', shouldCollapse);
        const toggleBtn = card.querySelector('.toggle-card-btn');
        if (toggleBtn) {
            toggleBtn.setAttribute('aria-expanded', shouldCollapse ? 'false' : 'true');
            const label = toggleBtn.querySelector('.toggle-label');
            if (label) {
                label.textContent = shouldCollapse ? 'Expand' : 'Collapse';
            }
        }
    }

    async function ensureBatchPersisted(card) {
        if (!card) {
            throw new Error('Batch not found.');
        }
        let batchId = Number(card.dataset.batchId || 0);
        if (batchId > 0) {
            return batchId;
        }
        if (!cfg.createBatchUrl) {
            throw new Error('Batch creation is not available right now.');
        }
        const lockBtn = card.querySelector('.lock-batch-btn');
        const exportBtn = card.querySelector('.batch-export-btn');
        if (lockBtn) {
            lockBtn.disabled = true;
        }
        if (exportBtn) {
            exportBtn.disabled = true;
        }
        const data = await createRemoteBatch();
        batchId = Number(data.batch_id) || 0;
        if (batchId <= 0) {
            throw new Error('Invalid batch id returned by server.');
        }
        card.dataset.batchId = batchId;
        card.dataset.batchNumber = batchId;
        card.dataset.unsaved = '0';
        const sequence = Number(data.sequence ?? data.yearly_number ?? 0);
        card.dataset.yearlyBatchNumber = sequence;
        const titleEl = card.querySelector('.batch-title');
        const header = card.querySelector('.header-summary');
        if (titleEl) {
            const labelText = card.dataset.customLabel?.trim?.() || '';
            const label = labelText !== '' ? labelText : `Batch #${sequence || batchId}`;
            titleEl.textContent = label;
        }
        if (header) {
            const draftTag = header.querySelector('.text-warning');
            if (draftTag) {
                draftTag.remove();
            }
        }
        if (exportBtn) {
            exportBtn.disabled = false;
        }
        card.querySelectorAll('.admin-check-wrapper').forEach((wrapper) => {
            const zone = wrapper.previousElementSibling?.classList?.contains('batch-dropzone')
                ? wrapper.previousElementSibling
                : null;
            const hasFile = zone?.dataset?.hasCheckFile === '1';
            const status = wrapper.querySelector('.admin-check-status');
            const input = wrapper.querySelector('.admin-check-input');
            if (status && !hasFile) {
                status.textContent = 'No file uploaded';
            }
            if (input && card.dataset.locked !== '1') {
                input.disabled = false;
            }
        });
        const allInputs = card.querySelectorAll('.admin-check-input');
        allInputs.forEach((input) => {
            if (card.dataset.locked !== '1') {
                input.disabled = false;
            }
        });
        updateLockButtonState(card);
        return batchId;
    }

    function persistBatchChange(itemId, batchId, adminId, revertAction) {
        const entry = itemMap.get(itemId);
        if (!entry) { return; }
        const form = new FormData();
        form.append('expense_id', itemId);
        if (entry.reimbursement_id) {
            form.append('reimbursement_id', entry.reimbursement_id);
        }
        form.append('batch_id', batchId);
        form.append('batch_number', batchId);
        if (adminId || adminId === 0) {
            form.append('admin_id', adminId);
        } else {
            form.append('admin_id', '');
        }
        postJson(cfg.updateUrl, form, 'Unable to update batch assignment.').then((data) => {
            entry.batch_number = batchId;
            entry.batchId = batchId;
            entry.admin_id = (adminId || adminId === 0) ? adminId : null;
            if (data.reimbursement_id) {
                entry.reimbursement_id = Number(data.reimbursement_id);
            }
            const cardNode = batchBoard.querySelector(`.batch-item[data-item-id="${itemId}"]`);
            if (cardNode) {
                if (entry.reimbursement_id) {
                    cardNode.dataset.reimbId = entry.reimbursement_id;
                } else {
                    delete cardNode.dataset.reimbId;
                }
                cardNode.dataset.adminId = entry.admin_id ?? 0;
            }
            const rowNode = tableBody.querySelector(`tr[data-item-id="${itemId}"]`);
            if (rowNode) {
                if (entry.reimbursement_id) {
                    rowNode.dataset.reimbId = entry.reimbursement_id;
                } else {
                    delete rowNode.dataset.reimbId;
                }
            }
        }).catch((err) => {
            console.error(err);
            if (typeof revertAction === 'function') {
                revertAction();
            }
            alert(err.message || 'Unable to update batch assignment. Please try again.');
        });
    }

    async function createRemoteBatch() {
        if (!cfg.createBatchUrl) {
            throw new Error('Batch creation endpoint is not configured.');
        }
        const form = new FormData();
        return postJson(cfg.createBatchUrl, form, 'Unable to create a new batch right now.');
    }

    async function uploadAdminCheckFile(batchId, adminId, input, statusEl, linkEl) {
        if (!cfg.checkUploadUrl) {
            alert('Check uploads are not available right now.');
            return;
        }
        const file = input?.files?.[0];
        if (!file) {
            statusEl.textContent = 'No file selected';
            return;
        }

        const form = new FormData();
        form.append('batch_id', batchId);
        form.append('admin_id', adminId ?? 0);
        form.append('check_file', file);

        input.disabled = true;
        const previous = statusEl.textContent;
        statusEl.textContent = 'Uploading...';

        try {
            const data = await postJson(cfg.checkUploadUrl, form, 'Unable to upload check file right now.');
            const label = data.original_filename || 'View check';
            statusEl.textContent = `Uploaded: ${data.original_filename || 'file'}`;
            if (linkEl && data.url) {
                linkEl.href = data.url;
                linkEl.textContent = label;
                linkEl.classList.remove('d-none');
            }
            const parentZone = input.closest('.batch-dropzone');
            if (parentZone) {
                parentZone.dataset.hasCheckFile = '1';
                updateLockButtonState(parentZone.closest('.batch-card'));
            }
        } catch (err) {
            console.error(err);
            alert(err.message || 'Unable to upload the check file. Please try again.');
            statusEl.textContent = previous || 'Upload failed';
        } finally {
            input.disabled = false;
            input.value = '';
        }
    }

    function renderCheckUploader(zone, batchId, adminId, existing = null) {
        if (!zone) {
            return;
        }
        let wrapper = zone.querySelector('.admin-check-wrapper');
        if (!wrapper) {
            wrapper = document.createElement('div');
            wrapper.className = 'admin-check-wrapper';
            wrapper.innerHTML = `
                <div class="d-flex flex-wrap align-items-center gap-2">
                    <label class="form-label form-label-sm mb-0">Check File</label>
                    <input type="file" class="form-control form-control-sm admin-check-input" accept=".pdf,image/*">
                    <span class="text-muted small admin-check-status"></span>
                </div>
                <a class="admin-check-link small d-none" target="_blank" rel="noopener"></a>
            `;
            zone.appendChild(wrapper);
        }
        const input = wrapper.querySelector('.admin-check-input');
        const status = wrapper.querySelector('.admin-check-status');
        const link = wrapper.querySelector('.admin-check-link');
        if (!input || !status || !link) {
            return;
        }
        const card = zone.closest('.batch-card');
        const cardBatchId = Number(card?.dataset.batchId || 0);
        input.disabled = card?.dataset.locked === '1' || cardBatchId <= 0;
        if (existing?.url) {
            zone.dataset.hasCheckFile = '1';
            status.textContent = existing.original_filename ? `Uploaded: ${existing.original_filename}` : 'Uploaded';
            link.href = existing.url;
            link.textContent = existing.original_filename || 'View check';
            link.classList.remove('d-none');
        } else {
            zone.dataset.hasCheckFile = '0';
            status.textContent = cardBatchId <= 0 ? 'Save batch before uploading' : 'No file uploaded';
            link.classList.add('d-none');
        }
        input.onchange = () => {
            const latestBatchId = Number(card?.dataset.batchId || batchId || 0);
            uploadAdminCheckFile(latestBatchId, adminId ?? 0, input, status, link);
        };
        updateLockButtonState(card);
    }

    function markBatchCardLocked(card) {
        if (!card || card.dataset.locked === '1') {
            return;
        }
        card.dataset.locked = '1';
        card.classList.add('batch-locked');
        card.querySelectorAll('.batch-item').forEach((item) => {
            item.draggable = false;
        });
        card.querySelectorAll('.batch-dropzone').forEach((zone) => {
            zone.classList.add('locked');
        });
        const picker = card.querySelector('.batch-admin-picker');
        if (picker) {
            picker.classList.add('d-none');
        }
        card.querySelectorAll('.admin-check-input').forEach((input) => {
            input.disabled = true;
        });
        const button = card.querySelector('.lock-batch-btn');
        if (button) {
            button.textContent = 'Locked';
            button.disabled = true;
        }
        card.dataset.readyToLock = '0';
    }

    function updateLockButtonState(card) {
        if (!card) {
            return;
        }
        const button = card.querySelector('.lock-batch-btn');
        if (!button) {
            return;
        }
        const cardBatchId = Number(card.dataset.batchId || 0);
        if (cardBatchId <= 0) {
            button.disabled = true;
            card.dataset.readyToLock = '0';
            return;
        }
        const zones = Array.from(card.querySelectorAll('.batch-dropzone'));
        const zonesWithItems = zones.filter((zone) => zone.querySelector('.batch-item'));
        const hasItems = zonesWithItems.length > 0;
        const ready = hasItems && zonesWithItems.every((zone) => zone.dataset.hasCheckFile === '1');
        button.disabled = card.dataset.locked === '1' || !ready;
        card.dataset.readyToLock = ready && card.dataset.locked !== '1' ? '1' : '0';
    }

    async function handleBatchLock(card, batchId, button) {
        if (!card || card.dataset.locked === '1') {
            return;
        }
        if (card.dataset.readyToLock !== '1') {
            alert('Upload check files for every admin before submitting the batch.');
            return;
        }
        if (!cfg.lockBatchUrl) {
            alert('Batch locking is not available right now.');
            return;
        }
        const form = new FormData();
        form.append('batch_id', batchId);
        if (button) {
            button.disabled = true;
            button.textContent = 'Locking...';
        }
        try {
            await postJson(cfg.lockBatchUrl, form, 'Unable to lock batch at the moment.');
            markBatchCardLocked(card);
        } catch (err) {
            console.error(err);
            alert(err.message || 'Unable to lock the batch. Please try again.');
            if (button) {
                button.disabled = false;
                button.textContent = 'Submit Batch';
            }
        }
    }

    function moveRowToBatch(itemId, batchId, adminId, zoneList) {
        const row = tableBody.querySelector(`tr[data-item-id="${itemId}"]`);
        const item = itemMap.get(itemId);
        if (!item || !zoneList) {
            return null;
        }

        if (row) {
            row.remove();
        }

        const card = createBatchItemElement(item, adminId);
        zoneList.appendChild(card);
        refreshPendingEmptyState();
        updateBatchTotals(batchId);

        return () => {
            card.remove();
            const restored = buildTableRow(item);
            tableBody.appendChild(restored);
            refreshPendingEmptyState();
            updateBatchTotals(batchId);
        };
    }

    function moveBatchItemToZone(itemId, batchId, adminId, zoneList) {
        const card = batchBoard.querySelector(`.batch-item[data-item-id="${itemId}"]`);
        if (!card || !zoneList) {
            return null;
        }
        const previousZone = card.parentElement;
        const previousBatch = Number(card.closest('.batch-card')?.dataset.batchId || 0);
        const previousAdmin = Number(card.dataset.adminId || 0);

        zoneList.appendChild(card);
        card.dataset.adminId = adminId ?? '';
        updateBatchTotals(batchId);
        if (previousBatch && previousBatch !== batchId) {
            updateBatchTotals(previousBatch);
        }

        return () => {
            previousZone.appendChild(card);
            card.dataset.adminId = previousAdmin;
            updateBatchTotals(batchId);
            if (previousBatch && previousBatch !== batchId) {
                updateBatchTotals(previousBatch);
            }
        };
    }

    function moveItemBackToTable(itemId) {
        const card = batchBoard.querySelector(`.batch-item[data-item-id="${itemId}"]`);
        if (!card) {
            return null;
        }
        const batchId = Number(card.closest('.batch-card')?.dataset.batchId || 0);
        const previousZone = card.parentElement;
        const item = itemMap.get(itemId);

        card.remove();
        const newRow = buildTableRow(item);
        tableBody.appendChild(newRow);
        refreshPendingEmptyState();
        updateBatchTotals(batchId);

        return () => {
            newRow.remove();
            previousZone.appendChild(card);
            updateBatchTotals(batchId);
        };
    }

    function attachDropHandlers(zone) {
        zone.addEventListener('dragover', (event) => {
            const parentCard = zone.closest('.batch-card');
            if (parentCard?.dataset.locked === '1') {
                return;
            }
            if (!dragPayload) {
                return;
            }
            event.preventDefault();
            const dt = event.dataTransfer;
            if (dt) {
                dt.dropEffect = 'move';
            }
            zone.classList.add('drag-over');
        });
        zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
        zone.addEventListener('drop', async (event) => {
            event.preventDefault();
            zone.classList.remove('drag-over');
            const parentCard = zone.closest('.batch-card');
            if (parentCard?.dataset.locked === '1') {
                return;
            }
            if (!dragPayload || !dragPayload.itemId) {
                return;
            }
            const list = zone.querySelector('.dropzone-list');
            let batchId = Number(zone.closest('.batch-card')?.dataset.batchId || 0);
            const adminIdRaw = zone.dataset.adminId;
            const adminId = (adminIdRaw === undefined || adminIdRaw === '') ? null : Number(adminIdRaw);
            let revertAction = null;

            if (batchId <= 0) {
                try {
                    batchId = await ensureBatchPersisted(parentCard);
                } catch (err) {
                    alert(err.message || 'Unable to create batch.');
                    return;
                }
            }

            if (dragPayload.source === 'table') {
                revertAction = moveRowToBatch(dragPayload.itemId, batchId, adminId, list);
                persistBatchChange(dragPayload.itemId, batchId, adminId, revertAction);
            } else if (dragPayload.source === 'batch') {
                revertAction = moveBatchItemToZone(dragPayload.itemId, batchId, adminId, list);
                if (dragPayload.fromBatchId !== batchId || dragPayload.fromAdminId !== adminId) {
                    persistBatchChange(dragPayload.itemId, batchId, adminId, revertAction);
                }
            }
            dragPayload = null;
        });
    }

    function addAdminZone(card, adminId, label, isDefault = false, checkFile = null) {
        const existing = card.querySelector(`.batch-dropzone[data-admin-id="${adminId}"]`);
        if (existing) {
            return existing;
        }
        const zone = document.createElement('div');
        zone.className = 'batch-dropzone';
        zone.dataset.adminId = adminId;
        zone.dataset.hasCheckFile = checkFile ? '1' : '0';
        zone.innerHTML = `
            <div class="dropzone-title d-flex justify-content-between align-items-center">
                <span>${label}</span>
                <span class="admin-subtotal">Subtotal: ${currency.format(0)}</span>
            </div>
            <div class="dropzone-list"></div>
            <div class="admin-check-wrapper">
                <div class="d-flex flex-wrap align-items-center gap-2">
                    <label class="form-label form-label-sm mb-0">Check File</label>
                    <input type="file" class="form-control form-control-sm admin-check-input" accept=".pdf,image/*">
                    <span class="text-muted small admin-check-status"></span>
                </div>
                <a class="admin-check-link small d-none" target="_blank" rel="noopener"></a>
            </div>
        `;
        card.querySelector('.batch-dropzones')?.appendChild(zone);
        const placeholder = card.querySelector('.no-admin-placeholder');
        if (placeholder) {
            placeholder.remove();
        }
        attachDropHandlers(zone);
        renderCheckUploader(zone, Number(card.dataset.batchId || 0), adminId ?? 0, checkFile);
        updateBatchTotals(Number(card.dataset.batchId || 0));
        if (!isDefault && adminId) {
            zone.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
        }
        return zone;
    }

    function createBatchCard(batchId, initialItems = [], options = {}) {
        const card = document.createElement('div');
        card.className = 'card batch-card';
        card.dataset.batchId = batchId;
        card.dataset.batchNumber = batchId;
        const sequence = Number(options.sequence ?? 0);
        const visibleSequence = sequence > 0 ? sequence : batchId;
        card.dataset.yearlyBatchNumber = sequence;
        const labelText = (options.label || '').trim();
        const unsaved = !!options.unsaved;
        const startCollapsed = options.startCollapsed === true;
        const sequenceLabel = unsaved
            ? 'Draft Batch'
            : `Batch #${visibleSequence}`;
        const isPlainBatchLabel = /^Batch #\d+$/i.test(labelText);
        const title = unsaved
            ? (labelText || sequenceLabel)
            : (!labelText || isPlainBatchLabel
                ? sequenceLabel
                : `${labelText} (${sequenceLabel})`);
        if (unsaved) {
            card.dataset.unsaved = '1';
        }
        card.dataset.customLabel = labelText;
        const status = (options.status || 'open').trim().toLowerCase();
        const isLocked = status && status !== 'open';
        card.innerHTML = `
            <div class="card-header d-flex flex-wrap gap-2 align-items-center">
                <div class="d-flex flex-column flex-grow-1 gap-1">
                    <div class="d-flex align-items-center gap-2">
                        <button type="button" class="btn btn-sm btn-outline-primary toggle-card-btn" aria-expanded="true">
                            <span class="toggle-icon">v</span>
                            <span class="toggle-label">Collapse</span>
                        </button>
                        <strong class="mb-0 batch-title">${title}</strong>
                    </div>
                    <div class="text-muted small d-flex flex-wrap gap-3 header-summary">
                        <span>Items: <strong class="header-total-items">0</strong></span>
                        <span>Amount: <strong class="header-total-amount">${currency.format(0)}</strong></span>
                        ${unsaved ? '<span class="text-warning">Draft (not saved)</span>' : ''}
                    </div>
                </div>
                <div class="ms-auto d-flex gap-2 align-items-center">
                    <button type="button" class="btn btn-sm btn-outline-danger lock-batch-btn">Submit Batch</button>
                    <button type="button" class="btn btn-sm btn-outline-success batch-export-btn">Download CSV</button>
                    <div class="d-flex gap-2 align-items-center batch-admin-picker">
                        <div class="input-group input-group-sm">
                            <select class="form-select form-select-sm admin-select">
                                <option value="">Select an admin</option>
                            </select>
                            <button type="button" class="btn btn-outline-primary add-admin-btn">Add</button>
                        </div>
                    </div>
                </div>
            </div>
            <div class="card-body">
                <div class="batch-dropzones"></div>
            </div>
            <div class="card-footer d-flex justify-content-end gap-4 small">
                <span>Total Items: <strong class="total-items">0</strong></span>
                <span>Total Amount: <strong class="total-amount">${currency.format(0)}</strong></span>
            </div>
        `;

        const select = card.querySelector('.admin-select');
        const addBtn = card.querySelector('.add-admin-btn');
        const lockBtn = card.querySelector('.lock-batch-btn');
        const toggleBtn = card.querySelector('.toggle-card-btn');
        if (toggleBtn) {
            toggleBtn.addEventListener('click', () => {
                const nextCollapsed = !card.classList.contains('batch-collapsed');
                setCardCollapsed(card, nextCollapsed);
            });
        }
        if (lockBtn) {
            lockBtn.addEventListener('click', () => handleBatchLock(card, batchId, lockBtn));
            if (!cfg.lockBatchUrl) {
                lockBtn.disabled = true;
            }
            if (batchId <= 0) {
                lockBtn.disabled = true;
            }
        }
        const exportBtn = card.querySelector('.batch-export-btn');
        if (exportBtn) {
            if (cfg.batchExportUrl) {
                exportBtn.addEventListener('click', (event) => {
                    event.preventDefault();
                    const targetId = Number(card.dataset.batchId || 0);
                    if (targetId <= 0) {
                        return;
                    }
                    window.open(`${cfg.batchExportUrl}?batch_id=${targetId}`, '_blank');
                });
            } else {
                exportBtn.disabled = true;
            }
            if (batchId <= 0) {
                exportBtn.disabled = true;
            }
        }
        if (select && addBtn) {
            if (admins.length === 0) {
                select.innerHTML = '<option value="">No admin roles found</option>';
                select.disabled = true;
                addBtn.disabled = true;
            } else {
                admins.forEach((admin) => {
                    const option = document.createElement('option');
                    option.value = admin.id;
                    option.textContent = admin.name || `Admin #${admin.id}`;
                    select.appendChild(option);
                });
                addBtn.addEventListener('click', () => {
                    const adminId = Number(select.value);
                    if (!adminId) {
                        return;
                    }
                    const admin = admins.find((a) => a.id === adminId);
                    addAdminZone(card, adminId, admin?.name || `Admin #${adminId}`);
                    select.value = '';
                });
            }
        }

        const dropzoneWrapper = card.querySelector('.batch-dropzones');
        const placeholder = document.createElement('div');
        placeholder.className = 'no-admin-placeholder text-muted small';
        placeholder.textContent = 'Add at least one admin to start assigning reimbursements to this batch.';
        dropzoneWrapper.appendChild(placeholder);

        const slots = Array.isArray(options.slots) ? options.slots : [];

        const renderSlotItems = (zone, itemIds, adminId) => {
            const list = zone.querySelector('.dropzone-list');
            (Array.isArray(itemIds) ? itemIds : []).forEach((itemId) => {
                const item = itemMap.get(Number(itemId));
                if (!item) {
                    return;
                }
                const cardEl = createBatchItemElement(item, adminId);
                list.appendChild(cardEl);
                item.batch_number = batchId;
                item.batchId = batchId;
            });
        };

        if (slots.length > 0) {
            slots.forEach((slot, index) => {
                const adminId = Number(slot.admin_id ?? slot.adminId ?? 0);
                const label = (slot.admin_name || slot.adminName || '').trim() || (adminId > 0 ? `Admin #${adminId}` : 'Unassigned');
                const zone = addAdminZone(card, adminId, label, index === 0, slot.check_file || null);
                renderSlotItems(zone, slot.items, adminId);
            });
            placeholder.remove();
        } else {
            (initialItems || []).forEach((itemId) => {
                const item = itemMap.get(Number(itemId));
                if (!item) {
                    return;
                }
                const defaultAdmin = admins[0] ?? { id: 0, name: 'Unassigned' };
                const zone = addAdminZone(card, defaultAdmin.id, defaultAdmin.name, true);
                const list = zone.querySelector('.dropzone-list');
                const cardEl = createBatchItemElement(item, defaultAdmin.id);
                list.appendChild(cardEl);
                item.batch_number = batchId;
                item.batchId = batchId;
                placeholder.remove();
            });
        }
        updateBatchTotals(batchId);
        if (isLocked) {
            markBatchCardLocked(card);
        } else {
            card.dataset.locked = '0';
            updateLockButtonState(card);
        }
        if (startCollapsed) {
            setCardCollapsed(card, true);
        }
        return card;
    }

    tableBody.querySelectorAll('tr[data-item-id]').forEach((row) => attachRowHandlers(row));

    tableBody.addEventListener('dragover', (event) => {
        if (!dragPayload || dragPayload.source !== 'batch') {
            return;
        }
        event.preventDefault();
        if (event.dataTransfer) {
            event.dataTransfer.dropEffect = 'move';
        }
        tableBody.classList.add('drag-over');
    });
    tableBody.addEventListener('dragleave', (event) => {
        if (event.target === tableBody) {
            tableBody.classList.remove('drag-over');
        }
    });
    tableBody.addEventListener('drop', (event) => {
        if (!dragPayload || dragPayload.source !== 'batch') {
            return;
        }
        event.preventDefault();
        tableBody.classList.remove('drag-over');
        const revertAction = moveItemBackToTable(dragPayload.itemId);
        persistBatchChange(dragPayload.itemId, 0, null, revertAction);
        dragPayload = null;
    });

    if (pageContainer) {
        pageContainer.addEventListener('click', handleReceiptLinkClick);
        pageContainer.addEventListener('click', handleDonationClick);
    }

    (cfg.batches || []).forEach((batch) => {
        if (!batch) {
            return;
        }
        const batchId = Number(batch.batchNumber ?? batch.batchId ?? 0);
        if (batchId <= 0) {
            return;
        }
        const sequence = Number(batch.sequence ?? batch.yearly_batch_number ?? 0);
        const card = createBatchCard(batchId, [], {
            label: batch.label,
            slots: batch.slots,
            status: batch.status,
            sequence,
            startCollapsed: true,
        });
        batchBoard.appendChild(card);
        updateBatchTotals(batchId);
        toggleBatchEmptyState();
    });

    if (addBatchBtn) {
        addBatchBtn.addEventListener('click', async () => {
            addBatchBtn.disabled = true;
            const card = createBatchCard(0, [], {
                label: 'New Batch (draft)',
                sequence: 0,
                unsaved: true,
                startCollapsed: false,
            });
            batchBoard.prepend(card);
            toggleBatchEmptyState();
            card.scrollIntoView({ behavior: 'smooth', block: 'start' });
            try {
                await ensureBatchPersisted(card);
            } catch (err) {
                console.error(err);
                alert(err?.message || 'Unable to create a new batch right now.');
                card.remove();
                toggleBatchEmptyState();
            } finally {
                addBatchBtn.disabled = false;
            }
        });
    }
})();
</script>
<?= $this->endSection() ?>
