import { type DragEvent, useCallback, useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { ApiHttpError } from '../../api/http' import { createReimbursementBatch, downloadBatchCsv, downloadReimbursementsExport, fetchUnderProcessingWorkspace, postAdminCheckUpload, postBatchLock, postBatchUpdate, postMarkDonation, type UnderProcessingWorkspace, } from '../../api/reimbursements' import './underProcessing.css' const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) type DragPayload = | { source: 'table'; itemId: number; reimbursementId?: number | null } | { source: 'batch'; itemId: number; fromBatchId: number; fromAdminId: number; reimbursementId?: number | null } export function ReimbursementsUnderProcessingPage() { const [ws, setWs] = useState(null) const [err, setErr] = useState(null) const [busy, setBusy] = useState(false) const [receiptModalUrl, setReceiptModalUrl] = useState(null) const dragPayload = useRef(null) const load = useCallback(() => { setErr(null) fetchUnderProcessingWorkspace() .then(setWs) .catch((e: unknown) => setErr(e instanceof ApiHttpError ? e.message : 'Failed to load workspace.'), ) }, []) useEffect(() => { load() }, [load]) async function persistToBatch( expenseId: number, batchId: number, adminId: number | null, reimbursementId?: number | null, ) { const form = new FormData() form.set('expense_id', String(expenseId)) form.set('batch_id', String(batchId)) form.set('batch_number', String(batchId)) if (reimbursementId) form.set('reimbursement_id', String(reimbursementId)) if (adminId != null && adminId !== 0) form.set('admin_id', String(adminId)) else form.set('admin_id', '') await postBatchUpdate(form) load() } async function removeFromBatch(expenseId: number, reimbursementId?: number | null) { const form = new FormData() form.set('expense_id', String(expenseId)) form.set('batch_id', '0') form.set('batch_number', '0') form.set('admin_id', '') if (reimbursementId) form.set('reimbursement_id', String(reimbursementId)) await postBatchUpdate(form) load() } async function onMarkDonation(expenseId: number) { if (!confirm('Mark this expense as a donation? It will be removed from reimbursement batches.')) return const fd = new FormData() fd.set('expense_id', String(expenseId)) try { await postMarkDonation(fd) load() } catch (e: unknown) { alert(e instanceof ApiHttpError ? e.message : 'Failed.') } } async function onAddBatch() { setBusy(true) try { await createReimbursementBatch() load() } catch (e: unknown) { alert(e instanceof ApiHttpError ? e.message : 'Could not create batch.') } finally { setBusy(false) } } async function onLockBatch(batchId: number) { const fd = new FormData() fd.set('batch_id', String(batchId)) try { await postBatchLock(fd) load() } catch (e: unknown) { alert(e instanceof ApiHttpError ? e.message : 'Lock failed.') } } function onCheckUpload(batchId: number, adminId: number, file: File | null) { if (!file) return const fd = new FormData() fd.set('batch_id', String(batchId)) fd.set('admin_id', String(adminId)) fd.set('check_file', file) postAdminCheckUpload(fd) .then(() => load()) .catch((e: unknown) => alert(e instanceof ApiHttpError ? e.message : 'Upload failed.')) } const pendingItems = ws?.pendingItems ?? [] const batches = ws?.existingBatches ?? [] const admins = ws?.adminUsers ?? [] return (
{err ?
{err}
: null}
Reimbursements Under Processing
Drag items into a batch zone on the right.
Reimbursement Main
{ if (dragPayload.current?.source === 'batch') { e.preventDefault() e.currentTarget.classList.add('drag-over') } }} onDragLeave={(e) => { if (e.target === e.currentTarget) e.currentTarget.classList.remove('drag-over') }} onDrop={(e) => { e.preventDefault() e.currentTarget.classList.remove('drag-over') const p = dragPayload.current dragPayload.current = null if (!p || p.source !== 'batch') return void removeFromBatch(p.itemId, p.reimbursementId).catch((ex: unknown) => alert(ex instanceof ApiHttpError ? ex.message : 'Update failed.'), ) }} > {pendingItems.length === 0 ? ( ) : ( pendingItems.map((item) => { const expenseId = Number(item.expense_id ?? item.id) const amt = Number(item.expense_amount ?? item.amount ?? 0) const reimbId = item.reimbursement_id ? Number(item.reimbursement_id) : null const ru = item.receipt_url ? String(item.receipt_url) : '' return ( { ev.dataTransfer.effectAllowed = 'move' ev.dataTransfer.setData('text/plain', String(expenseId)) dragPayload.current = { source: 'table', itemId: expenseId, reimbursementId: reimbId, } ;(ev.currentTarget as HTMLElement).classList.add('dragging') }} onDragEnd={(ev) => { ;(ev.currentTarget as HTMLElement).classList.remove('dragging') }} > ) }) )}
Expense Amount Vendor Description Purchased By Expense Receipt Actions
All reimbursements are currently assigned to a batch.
{currency.format(amt)} {String(item.vendor || 'N/A')} {String(item.description || '-')} {String(item.purchased_by || 'Unknown')} {ru ? ( ) : ( N/A )}
Active Batches
Each admin slot accepts drops from the table.
{batches.length === 0 ? (
No batches yet. Click "Add New Batch".
) : (
{batches.map((batch) => { const batchId = Number(batch.batchNumber ?? batch.batch_id ?? batch.batchId ?? 0) const statusRaw = String(batch.status ?? '').toLowerCase().trim() const locked = statusRaw !== '' && statusRaw !== 'open' const slots = (Array.isArray(batch.slots) ? batch.slots : []) as Array< Record > const sequence = Number(batch.sequence ?? batch.yearly_batch_number ?? batchId) return (
{String(batch.label ?? `Batch #${sequence || batchId}`)}
ID {batchId}
{!locked ? ( ) : null}
{slots.length === 0 ? ( { e.preventDefault() const p = dragPayload.current dragPayload.current = null if (!p || p.source !== 'table') return void persistToBatch( p.itemId, batchId, 0, p.reimbursementId, ).catch((ex: unknown) => alert(ex instanceof ApiHttpError ? ex.message : 'Update failed.'), ) }} /> ) : ( slots.map((slot, idx) => { const adminId = Number(slot.admin_id ?? slot.adminId ?? 0) const label = String( slot.admin_name ?? slot.adminName ?? `Admin #${adminId}`, ) const itemIds = (Array.isArray(slot.items) ? slot.items : []) as number[] const details = (slot.items_detail as Array> | undefined) ?? [] return (
{label}
{ e.preventDefault() const p = dragPayload.current dragPayload.current = null if (!p) return const reimb = p.source === 'table' ? p.reimbursementId : p.reimbursementId if (p.source === 'table') { void persistToBatch( p.itemId, batchId, adminId, reimb, ).catch((ex: unknown) => alert( ex instanceof ApiHttpError ? ex.message : 'Update failed.', ), ) } else { void persistToBatch( p.itemId, batchId, adminId, reimb, ).catch((ex: unknown) => alert( ex instanceof ApiHttpError ? ex.message : 'Update failed.', ), ) } }} />
{itemIds.map((eid) => { const row = details.find((x) => Number(x.expense_id ?? x.id) === eid) ?? {} const ru = row.receipt_url ? String(row.receipt_url) : '' const rmb = row.reimbursement_id ? Number(row.reimbursement_id) : null return (
{ dragPayload.current = { source: 'batch', itemId: eid, fromBatchId: batchId, fromAdminId: adminId, reimbursementId: rmb, } }} onDragEnd={() => { /* clear only after drop */ }} >
{String(row.description ?? 'Item')}
{String(row.vendor ?? '')}
{currency.format( Number(row.amount ?? row.expense_amount ?? 0), )}
{ru ? ( ) : null}
) })}
{!locked ? (
onCheckUpload(batchId, adminId, e.target.files?.[0] ?? null) } />
) : null}
) }) )}
) })}
)}
Admins:{' '} {admins.length ? admins.map((a) => `${a.firstname ?? ''} ${a.lastname ?? ''}`.trim()).join(', ') : '—'}
{receiptModalUrl ? (
Expense Receipt