import { type FormEvent, useEffect, useMemo, useState } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { ApiHttpError } from '../../api/http' import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions' import { downloadBatchCsv, downloadReimbursementsExport, fetchReimbursementsIndex, postBatchEmail, type BatchDetailPayload, type BatchSummaryRow, type ReimbursementsIndexResponse, } from '../../api/reimbursements' function money(n: unknown): string { const x = typeof n === 'number' ? n : parseFloat(String(n ?? 0)) return Number.isFinite(x) ? x.toFixed(2) : '0.00' } type AttachmentEntry = { receipts?: Array> checks?: Array> } function groupItemsByPurchaser(items: Array>) { const map: Record> }> = {} for (const item of items) { let p = String(item.purchased_by ?? '').trim() if (!p) p = 'Unknown' if (!map[p]) map[p] = { total: 0, items: [] } const amt = parseFloat(String(item.amount ?? 0)) || 0 map[p].total += amt map[p].items.push(item) } return Object.keys(map) .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) .map((k) => ({ purchaser: k, ...map[k] })) } export function ReimbursementsIndexPage() { const [searchParams, setSearchParams] = useSearchParams() const semester = searchParams.get('semester') ?? '' const status = searchParams.get('status') ?? '' const schoolYear = searchParams.get('school_year') ?? '' const userId = searchParams.get('user_id') ?? '' const [data, setData] = useState(null) const [error, setError] = useState(null) const [openBatches, setOpenBatches] = useState>({}) const [emailOpen, setEmailOpen] = useState(false) const [emailBatchId, setEmailBatchId] = useState(null) const [emailTitle, setEmailTitle] = useState('') const [recipientEmail, setRecipientEmail] = useState('') const [emailMessage, setEmailMessage] = useState('') const [receiptIds, setReceiptIds] = useState([]) const [checkIds, setCheckIds] = useState([]) const [emailStatus, setEmailStatus] = useState(null) function load() { setError(null) fetchReimbursementsIndex({ semester: semester || undefined, status: status || undefined, school_year: schoolYear || undefined, user_id: userId || undefined, }) .then(setData) .catch((e: unknown) => setError(e instanceof ApiHttpError ? e.message : 'Failed to load reimbursements.'), ) } useEffect(() => { load() }, [semester, status, schoolYear, userId]) const batchAttachments = data?.batchAttachments ?? {} function applyFilter(e: FormEvent) { e.preventDefault() const fd = new FormData(e.currentTarget) const next = new URLSearchParams() const s = String(fd.get('semester') ?? '') const st = String(fd.get('status') ?? '') const sy = String(fd.get('school_year') ?? '') const uid = String(fd.get('user_id') ?? '') if (s) next.set('semester', s) if (st) next.set('status', st) if (sy) next.set('school_year', sy) if (uid) next.set('user_id', uid) setSearchParams(next) } const schoolYears = data?.schoolYears ?? [] const { options } = useSchoolYearOptions({ legacyYears: schoolYears, preferredYear: schoolYear, }) const users = data?.users ?? [] const batchSummaries = (data?.batchSummaries ?? []) as BatchSummaryRow[] const batchDetails = data?.batchDetails ?? {} const donationBatch = data?.donationBatch ?? null const donationDetails = (data?.donationDetails ?? { items: [], checks: [] }) as BatchDetailPayload const attachmentMeta = useMemo(() => batchAttachments, [batchAttachments]) function openEmailModal(batchId: number, title: string) { setEmailBatchId(batchId) setEmailTitle(title ? `Email ${title}` : 'Email Batch') setRecipientEmail('') setEmailMessage('') setEmailStatus(null) const entry = attachmentMeta[String(batchId)] as AttachmentEntry | undefined const receipts = entry?.receipts ?? [] const checks = entry?.checks ?? [] setReceiptIds( receipts .map((r) => Number((r as { expense_id?: unknown }).expense_id)) .filter((n) => n > 0), ) setCheckIds( checks.map((c) => Number((c as { file_id?: unknown }).file_id)).filter((n) => n > 0), ) setEmailOpen(true) } async function submitEmail(e: FormEvent) { e.preventDefault() if (emailBatchId == null) return setEmailStatus('Sending…') const fd = new FormData() fd.set('batch_id', String(emailBatchId)) fd.set('recipient_email', recipientEmail) fd.set('message', emailMessage) fd.set('receipts', JSON.stringify(receiptIds)) fd.set('checks', JSON.stringify(checkIds)) try { await postBatchEmail(fd) setEmailStatus('Sent.') setEmailOpen(false) load() } catch (err: unknown) { setEmailStatus(err instanceof ApiHttpError ? err.message : 'Send failed.') } } function toggleReceipt(expenseId: number, checked: boolean) { setReceiptIds((prev) => { const s = new Set(prev) if (checked) s.add(expenseId) else s.delete(expenseId) return [...s] }) } function toggleCheck(fileId: number, checked: boolean) { setCheckIds((prev) => { const s = new Set(prev) if (checked) s.add(fileId) else s.delete(fileId) return [...s] }) } return (

Reimbursed Expenses

{error ?
{error}
: null}
Reimbursement Under Processing
{!batchSummaries.length && !donationBatch ? (

No batch data for the current filters.

) : (
Submitted Batches
{batchSummaries.map((batch) => { const bid = Number(batch.batch_id ?? 0) const title = String(batch.title ?? `Batch #${bid}`) const details = (batchDetails[String(bid)] ?? { items: [], checks: [], }) as BatchDetailPayload const items = details.items ?? [] const checks = details.checks ?? [] const isOpen = openBatches[String(bid)] ?? false const groups = groupItemsByPurchaser(items) return (
{isOpen ? (
{groups.length > 0 ? (
Items
{groups.map((g) => (
{g.purchaser}
Total: ${money(g.total)}
    {g.items.map((item, idx) => (
  • {String(item.description || 'No description')}
    {String(item.vendor || 'Vendor N/A')} {item.receipt_url ? ( <> {' '} •{' '} Receipt ) : null}
    ${money(item.amount)}
  • ))}
))}
) : null} {checks.length > 0 ? (
Uploaded Checks
    {checks.map((file, idx) => (
  • {String(file.admin ?? 'Admin')}
    {String(file.original ?? file.filename ?? '')}
    {file.url ? ( View ) : null}
  • ))}
) : null}
) : null}
ID: {bid}
) })} {donationBatch ? (
{openBatches['donation'] ? (
{groupItemsByPurchaser(donationDetails.items ?? []).map((g) => (
{g.purchaser}
Total: ${money(g.total)}
    {g.items.map((item, idx) => (
  • {String(item.description || 'No description')}
    {String(item.vendor || 'Vendor N/A')}
    ${money(item.amount)}
  • ))}
))}
) : null}
Category: Donation Not reimbursable
) : null}
)}
Individual reimbursement table is hidden. Use the submitted batch cards above or export CSV for details.
{emailOpen ? (
{emailTitle}
setRecipientEmail(e.target.value)} placeholder="recipient@example.com" />