Skip to main content

Reimbursements Manager

Reimbursements Manager

Component File: src/features/tools/components/ReimbursementsManager.tsx Route / navigation: Path /tools, Zustand toolsTool = reimbursements. See 00-TOOLS-HUB.md. Access Level: Parent Org and Fund Users (own reimbursements, position-based) Last Updated: April 24, 2026

> Data Layer: This component uses Supabase client functions in src/lib/db/expenses.ts (e.g., fetchExpensesPage(), createExpense(), updateExpense(), fetchDraftExpenses(), submitDraft(), submitAllDrafts(), deleteDraft()). Re-exported from src/lib/db.ts. There is no REST API server.

Recent Fixes

OCR provider status + fallback notes (Apr 24, 2026)

  • Observed production behavior: Repeated receipt OCR failures with OCR service rejected request (402) and occasional Service is temporarily unavailable surfaced from scan-receipt via src/lib/ocrUtils.ts.
  • Status semantics:
  • 402 from OCR provider = upstream billing/quota/credits rejection (non-retriable until account state is corrected).
  • Service is temporarily unavailable / 5xx = transient upstream outage.
  • Current client behavior: scanDocumentWithRetry() now skips retry for likely non-retriable OCR failures (notably provider 4xx messages such as 402) and retries once for transient failures.
  • User-facing fallback: If OCR fails, reimbursement capture falls back to manual entry (amount = 0, editable in review) and users can still complete/submit or save drafts.
  • Model chain (current): scan-receipt now attempts openai/gpt-4o, openai/gpt-4o-mini, then anthropic/claude-3.5-sonnet for better cross-model resilience.

Draft batch restore now auto-sanitizes stale references (Apr 14, 2026)

  • Problem: Older localStorage batches could restore with stale/non-eligible nonprofit IDs (for example after entity context changes) or stale account mappings, forcing manual cleanup before submit.
  • Fix: Restore pass now validates each receipt against current expenseEligibleEntities and current expense accounts:
  • invalid entityId values are auto-corrected to a safe eligible fallback,
  • stale account mappings are re-resolved by current category code fallback logic,
  • form-level nonprofit state is rehydrated from the sanitized batch.
  • UX: Users keep restored receipts (no forced clear). If corrections were applied, a review toast prompts them to verify before submit.
  • Scope: Submit flow hydration only (alignmint-reimbursement-batch-* restore path).

Reimbursement ownership + pending/revision visibility hardening (Apr 13, 2026)

  • P0 — DB update scope tightened: expenses UPDATE RLS now allows non-parent users to update only their own reimbursement rows in draft/pending/rejected states (plus voided transition). Parent-org admins keep broad update access for approval and payment operations.
  • P0 — Pending feed consistency: get_expenses_page is now checked into supabase/migrations and enforces non-parent submitter scoping directly in the RPC. This aligns runtime with the “own reimbursements” contract and removes hidden dependency on out-of-repo SQL.
  • P1 — Revision flow parity: RPC p_status='pending' explicitly includes both pending and rejected rows so “Needs Revision” items always stay visible in the Pending tab.
  • P1 — Constraint alignment: supabase/migrations now includes voided in expenses_status_check so local/schema replay matches current reimbursements delete/void behavior.

General Ledger drill-down now shows reimbursement receipts (Apr 13, 2026)

  • When approved reimbursements are paid through Expenses batch pay, the resulting payment JE now stores reference_id = expense-payment-group:{paymentGroupId}.
  • General Ledger transaction detail uses that reference to pull the linked paid expenses and open their attached receipt files from the drawer.
  • Result: bookkeepers can audit a paid reimbursement transaction to source receipts without switching back to Expenses/Reimbursements.

PDF receipt import — same UX as check deposits (Mar 27, 2026)

  • Behavior: Upload or drag-and-drop PDF opens the shared `PdfPagePickerDialog`. Each selected page becomes one `ReceiptData` row (same mapping rule as check deposits: N pages → N line items). Rasterization (PDF.js in src/lib/pdfClient.ts), OCR with bounded concurrency, then existing submit path (`uploadReceiptImage()` → JPEG). Resume button appears if the user closed the picker with Done — pick more later (stashed File). Picker footer: Cancel, Continue, and Done — pick more later only (redundant Save draft / Add another file removed); thumbnail grid scrolls inside a flex `min-h-0` + `ScrollArea` region when there are many pages.
  • Preview: Review step and submit batch list use `AttachmentPreview`; `ReceiptViewer` also uses `AttachmentPreview` so PDF attachments display correctly when stored as .pdf (iframe) or as rasterized JPEG.
  • i18n — capture step: Camera/upload tiles, format line, drag/drop, drop overlay, and shared OCR line use **common.jsoncaptureUpload.*. Reimbursement-only manual entry and receipt-specific OCR note: reimbursements.capture.manualEntry, reimbursements.capture.ocrNote. Picker dialog: commonpdfPicker.* (with `defaultValue` fallbacks). Resume copy: reimbursements.resumePdf**.
  • Files: ReimbursementsManager.tsx, shared PdfPagePickerDialog.tsx, AttachmentPreview.tsx, ReceiptViewer.tsx, pdfClient.ts, ocrUtils.ts. See also 08-CHECK-DEPOSIT-MANAGER.md and DEVELOPER-PLAYBOOK.md §44.

Row ⋮ menu: no duplicate “View receipt” (Mar 27, 2026)

  • Why: Row click already opens ReceiptViewer; a View receipt item in was redundant.
  • Drafts / Pending: Removed View receipt from DropdownMenu (and the trailing separator that followed it). now starts with edit / submit / delete (drafts) or resubmit (when rejected) / edit / delete (pending).
  • Approved / Confirmed: Those tabs previously had with only View receipt — removed the Actions column (header + trigger cell) entirely. Row click / Enter / Space still opens the receipt preview when applicable.
  • Unchanged: Submit flow Complete step still uses a per-receipt Eye control (labels.viewReceipt) beside delete — that list is not row-click–only.
  • Files: ReimbursementsManager.tsx, this doc, DEVELOPER-PLAYBOOK.md §20 changelog.

Row click → receipt preview, ⋮ row menu, edit in ReceiptViewer footer (Mar 25, 2026)

Interaction model

  • Drafts / Pending: Removed per-column receipt eye and inline icon clusters. Click the row (or Enter / Space when the row is focused) opens ReceiptViewer when receiptUrl is set; otherwise a short toast (labels.noReceiptToView). (MoreVertical) opens a DropdownMenu for secondary actions only — not “View receipt” (removed Mar 27, 2026 as duplicate of row click): drafts → edit / submit / delete; pending → resubmit when rejected, edit, delete. The menu trigger cell uses stopPropagation so opening the menu does not fire the row click.
  • Approved / Confirmed: Read-only — no / Actions column (Mar 27, 2026); row still opens receipt preview when a receipt exists.
  • `ReceiptViewer`: Accepts optional onEdit + editActionLabel. When the viewer was opened from an expense with status draft, pending, or rejected, the footer shows Edit reimbursement; it closes the preview and opens the existing edit dialog. Submit-step “complete” list still opens the viewer from unstaged ReceiptData only — receiptViewerSourceExpense is cleared there so the footer edit button does not appear.
  • i18n: labels.noReceiptToView, labels.clickRowViewReceipt, labels.rowActionsMenu (all 7 locales).

Files changed: ReimbursementsManager.tsx, src/components/shared/ReceiptViewer.tsx, 7 × reimbursements.json, this doc.

Drafts / Pending receipt vs actions column layout + “Edit reimbursement” copy (Mar 25, 2026)

P0 — View-receipt (eye) and row actions overlapped

  • Root cause: Actions used w-[80px] while Drafts rows render three controls; project Button size="sm" still enforces min-h-[44px] with horizontal padding, so the flex row overflowed into the Receipt column.
  • Fix: Drafts table: <colgroup> with fixed widths for date, category, amount, expires, receipt (4.5rem), and actions (11.25rem); middle <col /> takes remaining width for vendor (truncate + max-w-0 on cell). Row controls use size="icon" + flex-nowrap + shrink-0. Pending tab: wider receipt/actions headers (4.5rem / 11.25rem), min-w-[880px] on the table, same icon-button pattern. *(Mar 25 also adjusted Approved/Confirmed receipt affordances; Mar 27 removed / Actions on those tabs — receipt via row click only.)*

P1 — Edit copy

  • Fix: labels.editExpense and pending.editExpense (dialog title) now read “Edit reimbursement” in English and equivalent strings in all 6 non-EN locales.

Files changed: ReimbursementsManager.tsx, 7 × src/i18n/locales/*/reimbursements.json, this doc, DEVELOPER-PLAYBOOK.md (changelog line).

Drafts Fully Editable (Mar 25, 2026)

P0 — Draft rows could not be edited individually

  • Drafts table only had submit/delete actions; users could not revise an individual draft before sending for approval.
  • Fix: Added per-row pencil action in Drafts → Actions. Draft rows now open the shared edit dialog used for pending expenses.

P0 — Draft edit save path lacked draft safety + stale UI

  • Save path only called updateExpense() and refreshed paginated non-draft queries.
  • Fix: Added updateDraftExpense() in src/lib/db/expenses.ts with .eq('status', 'draft') safety guard. handleSaveExpense now uses this function for draft rows and refetches draft-expenses after successful save.

P1 — Draft edit modal only changed ledger USD amount

  • Previous modal did not expose original_amount, original_currency, or exchange_rate, causing potential mismatch for foreign-currency drafts.
  • Fix: Expanded edit modal to mirror review-step currency controls: original amount, currency selector, exchange-rate refresh/override, USD ledger preview, display-currency helper text, and date-triggered rate refetch (when not overridden).

P2 — Draft action buttons were missing aria labels

  • Submit/delete used title but not explicit aria-label.
  • Fix: Added aria-label on draft edit/submit/delete action buttons for consistency with Pending tab accessibility patterns.

Files Changed:

  • ReimbursementsManager.tsx — draft pencil action, currency-aware edit modal, conditional draft save path + refetch
  • src/lib/db/expenses.tsupdateDraftExpense() helper + shared update payload builder
  • 07-REIMBURSEMENTS-MANAGER.md — this changelog

Layout & UX Audit — Mike Labrum Bug Report (Mar 16, 2026)

P0 — Review step layout clipped on narrow laptops (1345×697)

  • Mike Labrum (Deeper Walk) reported "formatting is a bit wonky down the side" — form fields were truncated/clipped on the right when the receipt image and form rendered side-by-side.
  • Root Cause: md:flex-row breakpoint (768px) triggered side-by-side layout too early. With sidebar + card padding, the form column was squeezed below minimum width. md:w-[40%] md:flex-shrink-0 on the image column prevented any compression. Currency grid grid-cols-2 forced two inputs side-by-side in ~300px.
  • Fix: Changed md:flex-rowlg:flex-row (1024px+), image column md:w-[40%]lg:w-[35%], currency grid grid-cols-2grid-cols-1 sm:grid-cols-2. All md: responsive prefixes on the review step image/form updated to lg:.

P1 — Confirmed tab subtitle reused empty-state key

  • Card subtitle (always visible) used noConfirmedDesc ("Confirmed payments will appear here") — misleading when expenses exist.
  • Fix: Added confirmed.subtitle key ("Paid and completed reimbursements grouped by ministry.") to all 7 locale files.

P1 — Approved tab edit button broke audit trail

  • Users could edit already-approved expenses (amount, vendor, description) without re-triggering approval. Documentation (line 297) says Approved/Confirmed tabs are "read-only."
  • Fix: Removed edit button and Actions column header from the Approved tab.

P2 — Mobile receipt image too small

  • max-h-56 (224px) was restrictive for mobile receipt scanning (primary use case).
  • Fix: Changed to max-h-72 (288px).

P2 — Pending tab delete button too aggressive

  • Used variant="destructive" (red background) in every table row. Other tabs used variant="ghost" + text-destructive.
  • Fix: Changed to variant="ghost" with text-destructive on the icon to match Drafts tab pattern.

P2 — Fund group accordions defaulted to collapsed for single-fund users

  • Users with expenses in only one fund had to click to expand every time.
  • Fix: Added 3 useEffect hooks that auto-expand when pendingByFund, approvedByFund, or filteredConfirmedByFund has exactly 1 group.

P3 — Duplicate Actions i18n keys

  • pending.actions, drafts.actions, approved.actions all had the same translation.
  • Fix: Added shared labels.actions key to all 7 locale files.

P3 — Hardcoded `'5890'` fallback account code

  • If an org didn't have account 5890, expense got an unresolvable category.
  • Fix: Falls back to expenseAccounts[0]?.code || '5890' (dynamic first available, then static fallback).

Files Changed:

  • ReimbursementsManager.tsx — layout breakpoints, approved tab edit removal, delete button styling, auto-expand, dynamic fallback account
  • 7 × reimbursements.json locale files — confirmed.subtitle, labels.actions
  • 07-REIMBURSEMENTS-MANAGER.md — this changelog
  • DEVELOPER-PLAYBOOK.md — §20 bug-fix entry

Playbook Compliance Audit (Mar 15, 2026)

**P0 — fetchDraftExpenses used select('*') (§17.5 rule 1)**

  • Replaced with explicit columns matching fetchExpenses pattern. Also added missing .limit(5000) (§17.5 rule 2).

P0 — Breadcrumb labels used `t()` (§16 Page Layout Standard)

  • Breadcrumb labels MUST be hardcoded strings per §16. Changed from t('reimbursements.breadcrumb.tools') / t('reimbursements.breadcrumb.reimbursements') to plain 'Tools' / 'Reimbursements'.

P1 — `Expense.status` type union incomplete

  • deleteExpense() writes status: 'voided' and createExpense() can write status: 'draft', but the TypeScript Expense interface only had 'pending' | 'approved' | 'paid' | 'rejected'. Added 'reimbursed' | 'draft' | 'voided' to both db/expenses.ts and types/data.ts. Also synced vendors.ts status cast.

P1 — Drafts tab + Complete-step inline empty states (§43)

  • Replaced both with shared <EmptyState> component. Pending and Approved tabs were already fixed in the Mar 13 audit.

P1 — Approved tab subtitle reused empty-state i18n key

  • Card subtitle (always visible) used noApprovedDesc which says "Approved expenses will appear here" — misleading when expenses exist. Added separate approved.subtitle key: "Expenses that have been approved and are awaiting payment."

P2 — 4 hardcoded `aria-label` strings (§21)

  • "View receipt", "Edit expense" (×2), "Delete expense" → wrapped with t() using labels.viewReceipt, labels.editExpense, labels.deleteExpense keys (copy later updated to “Edit reimbursement”, Mar 25, 2026). labels.viewReceipt remains used for the Complete-step receipt Eye button; table menus no longer include View receipt (Mar 27, 2026).

P2 — 3 `SkeletonTable` header arrays hardcoded English (§21)

  • Drafts, Pending, and Approved tab skeletons used raw English strings. Changed to t() calls matching the Confirmed tab pattern.

Intentional exceptions noted:

  • No DesktopOnlyWarning — component is intentionally mobile-accessible (mileage tracker integration).
  • Component at 2,660 lines exceeds §17.4 threshold (800 lines) — not splitting per Steve's direction.

Files Changed:

  • ReimbursementsManager.tsx — breadcrumbs, empty states, aria-labels, skeleton headers, approved subtitle
  • src/lib/db/expenses.tsfetchDraftExpenses explicit columns + limit, Expense.status type
  • src/types/data.tsExpense.status type sync
  • src/lib/db/vendors.ts — status cast sync
  • 7 × reimbursements.json locale files — 4 new keys (labels.viewReceipt, labels.editExpense, labels.deleteExpense, approved.subtitle)
  • 07-REIMBURSEMENTS-MANAGER.md — this changelog
  • DEVELOPER-PLAYBOOK.md — §20 bug-fix entry

Receipt Image Layout + Playbook Compliance Audit (Mar 13, 2026)

P1 — Receipt image too small on desktop

  • Review step used grid md:grid-cols-2 with max-h-48 md:max-h-[500px] — receipt image rendered ~120×160px visually on desktop. User requested bigger, centered-left.
  • Fix: Changed to flex flex-col md:flex-row layout. Image column gets md:w-[40%] with md:min-h-[400px], md:sticky md:top-4, and md:max-h-[600px]. Mobile retains compact max-h-56. Image centered vertically via flex items-center justify-center.

P2 — ReceiptViewer.tsx: 7 hardcoded English labels (§21)

  • Dialog title, "No receipt image available", and 5 field labels (Vendor, Amount, Date, Category, Description) were all hardcoded English.
  • Fix: Wrapped all 7 with t() calls using receiptViewer.* keys in common namespace. Added keys to all 7 locale files.

P2 — ReimbursementsManager.tsx: 9 hardcoded English strings (§21)

  • OCR failure toast, notification title/message, submission success toast, draft save toast, "Uncategorized" fallback, "Currency" label, "Exchange Rate" label, "Refresh" button, and "Resubmit for approval" tooltip.
  • Fix: Wrapped all 9 with t() calls. Added keys to reimbursements.json × 7 locales.

P2 — §11 query guard: expenses + drafts queries missing `isMappingInitialized()`

  • expenses and draft-expenses React Query hooks had enabled: safeSelectedEntity !== 'all' but were missing the isMappingInitialized() guard. The accounts query correctly had it.
  • Fix: Added isMappingInitialized() && to both query enabled conditions.

P3 — §43 compliance: Pending + Approved empty states

  • Pending and Approved tabs used inline <div className="py-12 text-center"> empty states. Confirmed tab already used the shared EmptyState component.
  • Fix: Replaced both with <EmptyState icon={...} title={...} description={...} />.

Files Changed:

  • ReimbursementsManager.tsx — receipt layout, i18n, §11 guards, §43 empty states
  • ReceiptViewer.tsx — 7 hardcoded labels → t()
  • 7 × reimbursements.json locale files — ~6 new keys each
  • 7 × common.json locale files — 7 receiptViewer.* keys each
  • 07-REIMBURSEMENTS-MANAGER.md — this changelog
  • DEVELOPER-PLAYBOOK.md — §20 bug-fix entry

Confirmed Tab Columns + i18n Compliance (Mar 13, 2026)

P0 — Confirmed Tab: Missing Submitted By and Paid By columns

  • Staff users could not see who submitted or who paid an expense on the Confirmed tab. Data was already fetched by fetchExpenses() but never rendered.
  • Fix: Added Submitted By and Paid By columns to the Confirmed tab table (responsive hidden xl:table-cell).
  • Fix: Added Submitted By and Paid By columns to the Confirmed tab CSV/XLSX/PDF export.

P1 — §21 i18n compliance: Hardcoded English strings

  • Wrapped ~10 hardcoded English strings with t() calls: localStorage restore toast, batch summary, draft submit success/failure toasts, notification title/message, pluralization badges, and Unknown Fund fallback.
  • Added 10 new i18n keys to all 7 locale files.

P1 — §22 compliance: `toLocaleDateString('en-US')` in monthly summary

  • Replaced hardcoded en-US locale with formatMonthYear() hook for locale-aware month display.

P1 — §22 compliance: `.split('T')[0]` in `expenses.ts`

  • Fixed 6 occurrences of .split('T')[0] date parsing → toLocalDateString(new Date(timestamp)) in fetchExpenses() and fetchDraftExpenses().

RULE: Every expense table view (Pending, Approved, Confirmed) MUST show Submitted By. Confirmed views MUST also show Paid By. Exports MUST include both for audit trail.

Files Changed:

  • ReimbursementsManager.tsx — Confirmed tab columns, export, i18n wrapping, formatMonthYear
  • src/lib/db/expenses.ts — §22 date fixes, dead status removal
  • 7 × reimbursements.json locale files — 10 new keys each
  • 07-REIMBURSEMENTS-MANAGER.md — this changelog

Fund User Permission Fix — Tami Michaels Bug Report (Mar 13, 2026)

  • P0 — Fund users blocked from submitting reimbursements: The Mar 12 accounting lockout correctly blocked fund_user roles from Fund Accounting (JEs, deposits, reconciliation), but ReimbursementsManager was gating submission on canWriteAccounting instead of canSubmitExpenses. Since canAccessAccounting() returns false for all fund_users, every fund_user lost the ability to submit receipts, save drafts, and edit pending expenses.
  • Root Cause: canWriteAccounting depends on canAccessAccounting() which has if (ctx.role === 'fund_user') return false. Reimbursement submission is a Tools hub action, not a Fund Accounting operation.
  • Fix: Added canSubmitExpenses() to permissions.ts — allows read_write access level users (excluding portal roles) to submit. Replaced all 6 canWriteAccounting references in ReimbursementsManager.tsx with canSubmitExpenses. Wired into usePermissions hook.
  • Impact: All 51 enterprise fund_users under IFM regained reimbursement submission access. No change to accounting lockout — fund_users still cannot access JEs, deposits, reconciliation, chart of accounts.
  • Files Modified: permissions.ts, usePermissions.ts, ReimbursementsManager.tsx, 07-REIMBURSEMENTS-MANAGER.md, DEVELOPER-PLAYBOOK.md

Confirmed Tab Refactor — Entity Grouping + Export (Mar 9, 2026)

  • P0 #1 — Entity/ministry grouping: Rewrote Confirmed tab to group reimbursements by entity (ministry) instead of payment batch (payment_group_id). Uses expandable accordion layout matching ExpensesManager pattern. Each group shows entity icon, name, item count badge, and total amount.
  • P0 #2 — Entity filter: Added entity filter dropdown on Confirmed tab. Shows "All Ministries" by default. When multiple entities have confirmed expenses, dropdown allows filtering to a single ministry. Filter state: confirmedEntityFilter.
  • P0 #3 — Search functionality: Added search input to filter confirmed expenses by vendor, description, or amount. Search state: confirmedSearchQuery. Shows filtered item count and total when active.
  • P0 #4 — Export button: Added export button in Confirmed tab header. Opens SimpleExportDialog with CSV/XLSX/PDF format options. Export filename: confirmed-reimbursements-{date}.{ext}. Headers: Date, Vendor, Description, Amount, Category, Ministry, Paid Date. Data source: filteredConfirmedExpenses (respects entity filter and search).
  • P1 #1 — i18n compliance (§21): Wrapped ~25 hardcoded English strings with t() calls. Added new keys to all 6 locale files (en, es, fr, de, zh, th): confirmed.title, confirmed.export, confirmed.allMinistries, confirmed.searchPlaceholder, confirmed.items, confirmed.noConfirmed, confirmed.noConfirmedDesc, confirmed.selectNonprofitDesc, confirmed.noResults, confirmed.noResultsDesc, confirmed.date, confirmed.vendor, confirmed.description, confirmed.amount, confirmed.category, confirmed.paidDate, confirmed.receipt.
  • P1 #2 — EmptyState component (§43): Replaced inline empty states with EmptyState component on Confirmed tab. Three states: select nonprofit prompt, no confirmed expenses, no search results.
  • P2 — Cleanup: Removed unused confirmedByFund memo (replaced by filteredConfirmedByFund which includes filtering logic).
  • Data Layer: Uses existing fetchExpenses() with status: 'paid'. No new DB queries. Grouping via groupByFund() helper. Export via exportToCSV(), exportToExcel(), exportToPDF() from src/lib/exportUtils.ts.
  • State Added: confirmedEntityFilter (string, default 'all'), confirmedSearchQuery (string, default ''), exportDialogOpen (boolean).
  • Files Modified: ReimbursementsManager.tsx, 6 × reimbursements.json locale files, 07-REIMBURSEMENTS-MANAGER.md
  • Build: Clean — 0 TypeScript errors

Receipt Upload & Capture Fixes — Jacqueline's Bug Report (Feb 20, 2026)

  • P0 #1 — PDF support: File upload input now accepts image/*,.pdf,application/pdf. Safari's accept="image/*" was graying out PDF receipts in the Downloads folder — Jacqueline's exact complaint ("can see downloads with receipt but it can't be captured").
  • P0 #2 — Drag-and-drop: Capture card now has full onDragOver/onDragEnter/onDragLeave/onDrop handlers with visual feedback (isDragging state, ring highlight, bounce animation). Previously the dashed-border styling implied drag-and-drop but no handlers existed.
  • P0 #3 — Manual entry: New "Enter manually without receipt" button on capture step. Creates a blank ReceiptData and skips OCR, going straight to review. Addresses "won't let me enter the amount on my own" complaint.
  • P0 #4 — `reader.onerror` handler: handleFileCapture now has a reader.onerror callback that shows a toast and resets isProcessing. Previously, if FileReader failed (corrupted file, unsupported format on Safari), the user got zero feedback.
  • P0 #5 — Pre-OCR image compression: processReceiptOCR now compresses images via compressImage(url, 1600, 0.85) before extracting base64 for the edge function. Matches the CheckDepositManager pattern. Prevents payload size failures on large iPhone photos (4-8MB JPEG → 5-11MB base64).
  • P1 #1 — `placeholderData: keepPreviousData`: Added to expenses and draft-expenses React Query hooks to prevent flash on entity switch (Playbook §8 compliance).
  • P1 #2 — File input reset: Added e.target.value = '' after file selection so the same file can be re-selected.
  • P2 #1 — ReceiptViewer `formatCurrency()`: Replaced hardcoded $${amount.toFixed(2)} with formatCurrency() from useCurrencyFormat hook (Playbook §19 compliance).
  • P2 #2 — i18n sweep: Replaced ~15 hardcoded English strings with t() calls. Added new keys to all 6 locale files: tabs.drafts, capture.manualEntry, capture.dragHint, capture.dropHere, reviewStep.manualEntryLabel, reviewStep.noReceiptImage, drafts.* (7 keys), pending.awaitingApproval, pending.needsRevision, pending.revisionDesc, selectNonprofit, errors.fileReadFailed, errors.noEntities.
  • P2 #3 — Disabled-state helper text: When expenseEligibleEntities is empty, a red message now explains why buttons are disabled.
  • Files Modified: ReimbursementsManager.tsx, ReceiptViewer.tsx, 6 × reimbursements.json locale files, DEVELOPER-PLAYBOOK.md

Monthly Reimbursement Summary Card (Feb 17, 2026)

  • New Feature: Added a "My Monthly Reimbursements" summary card to the Submit tab. Shows the logged-in user's expenses aggregated by month (last 6 months), with progress bars, per-month totals/counts, and an animated grand total.
  • Data Source: Client-side useMemo combining allExpenses and draftExpenses filtered by user.id. No new DB queries.
  • UI: Card/CardHeader/CardContent, tabular-nums, formatCurrency(), useCountUp(), progress bars (bg-primary fill), Badge variant="secondary".
  • i18n: Added monthlySummary.title and monthlySummary.lastNMonths keys to all 6 locale files (en, de, es, fr, th, zh).
  • Files Modified: ReimbursementsManager.tsx, 6 × reimbursements.json locale files

Reimbursements Audit — Submitted By & RLS Fix (Feb 15, 2026)

  • P0 — "Submitted By" column added to ExpensesManager: All three tabs (Pending, Approved, Paid) now display who submitted each expense. The data was already fetched via fetchExpenses()submittedBy field but was never rendered. Christy reported not knowing who submitted expenses for Senior Life Champions when multiple staff have access.
  • P0 — "Submitted By" column added to ReimbursementsManager: All three expense tabs (Pending, Approved, Confirmed) now display expense.submittedBy in a new column (hidden on xl: breakpoint for responsive layout).
  • P0 — Notifications RLS fix: The INSERT policy on notifications was failing when a parent_org admin approved an expense for a child org user. Condition 2 only checked the inserter's own organization_users memberships, which didn't include child orgs. Broadened the policy to include organizations.parent_organization_id lookups so parent org admins can insert notifications for child org contexts. Error: 42501 — new row violates row-level security policy for table "notifications".
  • P2 — Notification message improved: Submission notifications now include the submitter's email (e.g., "sue@example.com submitted 2 expense(s) totaling $150.00 for Senior Life Champions") instead of just the entity name.
  • P2 — `formatCurrency()` standardization: Replaced remaining $${amount.toFixed(2)} instances in ExpensesManager.tsx (pending group total, pending amount cells, approved amount cells, paid amount cells) with formatCurrency().
  • Files Modified: ExpensesManager.tsx, ReimbursementsManager.tsx, 07-REIMBURSEMENTS-MANAGER.md
  • DB Migration: fix_notifications_rls_child_orgs (Supabase project zlokhayitthdzitjysht)

Expense Account Removed from Submission Flow (Feb 14, 2026)

  • Change: Removed the expense account selector from the receipt review step. The person submitting a reimbursement no longer picks the GL expense account — that responsibility belongs to the bookkeeper in ExpensesManager.tsx.
  • OCR still runs: The categoryToAccountCode mapping still auto-maps OCR categories to account codes silently, so the bookkeeper gets a pre-filled suggestion when reviewing in Expenses.
  • Fallback: If no account is mapped (e.g., OCR fails), createExpense uses '5890' (Other/Uncategorized) as the default category.
  • Validation: Vendor, amount, date, and nonprofit are still required. Expense account is no longer required.
  • Files Modified: ReimbursementsManager.tsx, documentation/pages/accounting/07-REIMBURSEMENTS-MANAGER.md

Reimbursements Audit (Feb 11, 2026)

  • P0 #1 — EXIF orientation fix: compressImage() in imageUtils.ts now uses createImageBitmap({ imageOrientation: 'from-image' }) to auto-correct rotated mobile photos. Falls back to Image() for older browsers. Fixes Jacqueline's "picture flipped" report.
  • P0 #2 — Dead state removed: Removed unused viewReceiptOpen/selectedReceiptForView state variables (replaced by receiptViewerOpen/receiptViewerData in Feb 10 refactor).
  • P0 #3 — Stale closure fix: handleDeleteReceipt now uses functional setReceipts(prev => ...) to avoid stale closure when deleting the last receipt.
  • P1 #4 — `font-mono` → `tabular-nums`: Replaced all 7 font-mono instances on date/amount table cells with tabular-nums per styling guide.
  • P1 #5 — Amount color fix: Removed text-primary from table amount cells (Approved + Confirmed tabs). Amounts now use neutral styling instead of theme-dependent primary color.
  • P1 #6 — Confirmed tab responsive fix: Added missing hidden md:table-cell, hidden xl:table-cell, hidden lg:table-cell on Confirmed tab <TableCell> elements to match their <TableHead> counterparts.
  • P2 — Unused imports: Removed React namespace, X icon, createJournalEntryFromTransaction, useQueryClient, and unused queryClient declaration.
  • P2 — Helper extraction: Replaced 5 repeated IIFE account lookups with getCategoryDisplay() and getCategoryName() helpers.
  • P2 — Currency formatting: Standardized all currency display to use formatCurrency() (Intl.NumberFormat with commas). Replaced 6 instances of $${amount.toFixed(2)}.
  • P3 — Documentation overhaul: Rewrote Overview, UI Features, Data Layer, Data Schema, Auth, Business Logic, State Management, Dependencies, Error Handling, and Loading States sections to match actual implementation. Removed stale REST API endpoints, Rails references, and completed migration notes.
  • Files Modified: ReimbursementsManager.tsx, src/lib/imageUtils.ts, documentation/pages/accounting/07-REIMBURSEMENTS-MANAGER.md

Reimbursements Audit Fixes (Feb 10, 2026)

  • P0 #1 — "Not seeing saved items": After saving drafts, user was left on the Submit tab. Now auto-navigates to the Drafts tab (setActiveTab('drafts')) so saved items are immediately visible.
  • P0 #2 — "Words stacked on top of each other": Tab navigation pill bar had no overflow handling. Added overflow-x-auto to container and whitespace-nowrap to all 5 tab buttons so they scroll horizontally instead of compressing/overlapping on narrower viewports.
  • P0 #3 — Drafts leaking into general queries: fetchExpenses() did not exclude status='draft' when no specific status was passed. Added .neq('status', 'draft') to the query.
  • P0 #4 — View Receipt broken in Complete step & Drafts tab: viewReceiptOpen state was set but no Dialog was rendered. Rewired both click handlers to use the shared receiptViewerOpen/receiptViewerData + <ReceiptViewer> pattern.
  • P0 #5 — localStorage key mismatch: handleSaveAsDraft cleared alignmint-reimbursement-batch-${entityId} (from formData) instead of the STORAGE_KEY (from safeSelectedEntity). Replaced with clearSavedBatch() which uses the correct key.
  • P1 #2 — Draft category showing raw code: Drafts table showed raw account code (e.g., "5300") instead of name. Added account lookup: expenseAccounts.find(a => a.code === draft.category)?.name.
  • P1 #3 — Draft receipt viewer wrong lookup: Account was looked up by a.name === draft.category (always failed). Changed to a.code === draft.category.
  • P1 #4/#5 — Approved tab responsive columns: Category and Approved By cells were missing hidden lg:table-cell / hidden xl:table-cell classes, causing layout inconsistency with Pending tab.
  • P2 — 14 missing CSS utility classes: Added to src/index.css: bg-accent/50, bg-destructive/5, bg-destructive/10, bg-primary/5, bg-muted/30, bg-muted/50, hover:bg-primary/10, hover:bg-muted/50, hover:bg-muted/70, hover:bg-destructive/10, hover:text-destructive/80, hover:text-primary/80, border-destructive/20, border-primary/30.
  • P2 — useCountUp: Added animated count-up to draft total amount and receipt batch total per styling guide.
  • Files Modified: ReimbursementsManager.tsx, src/lib/db/expenses.ts, src/index.css

Draft Reimbursements System (Jan 27, 2026)

  • Feature: Users can now save receipts as drafts instead of immediately submitting for approval
  • Behavior:
  • Drafts expire after 90 days and are automatically deleted
  • Users receive notifications at 60, 30, and 7 days before expiration
  • New "Drafts" tab (second tab) shows all saved drafts with expiry countdown
  • "Save as Draft" button added to the review step
  • "Submit All Drafts" button allows batch submission of all drafts
  • Individual drafts can be submitted or deleted from the Drafts tab
  • Database Changes:
  • Added columns to expenses table: draft_created_at, draft_expires_at, draft_reminder_30_sent, draft_reminder_60_sent, draft_reminder_7_sent
  • Added status = 'draft' to expense status options
  • Created trigger set_draft_expiry_trigger to auto-set expiry dates
  • Edge Function: check-expiring-drafts runs daily at 9 AM UTC to:
  • Send expiration reminder notifications
  • Delete expired drafts (90+ days old)
  • Migration: supabase/migrations/20260127_draft_reimbursements.sql
  • New db.ts Functions:
  • fetchDraftExpenses(entityId, userId) - Get user's drafts
  • submitDraft(draftId) - Submit single draft for approval
  • submitAllDrafts(entityId, userId) - Batch submit all drafts
  • deleteDraft(draftId) - Delete a draft
  • Notification Types Added: draft_expiring, draft_expired

RLS Policy Fix for Parent Org Admins (Jan 26, 2026)

  • Problem: Parent org admins (e.g., steven@getalignmint.org) could not submit reimbursements for child organizations. The INSERT and SELECT RLS policies referenced the dropped users.role column.
  • Root Cause: The users.role column was dropped on Jan 19, 2026. Role is now stored in organization_users.role. The expenses RLS policies still had:
  EXISTS (SELECT 1 FROM users WHERE id = auth.uid() AND role = 'parent_org')

This check failed silently because the column doesn't exist.

  • Solution: Created migration 20260126_fix_expenses_rls_parent_org.sql to replace all users.role checks with is_parent_org_admin() function.
  • Migration: Run in Supabase SQL Editor:
  -- See: supabase/migrations/20260126_fix_expenses_rls_parent_org.sql
  • Affected Policies:
  • Users can view expenses for their organizations (SELECT)
  • Users can insert expenses for their organizations (INSERT)
  • Users can delete their own pending expenses (DELETE)

Approved Tab Added (Jan 12, 2026)

  • Problem: Staff could not see when their expenses had been approved but not yet paid - expenses jumped from "Pending" directly to "Confirmed" (paid)
  • Solution: Added a new "Approved" tab between Pending and Confirmed
  • Behavior:
  • Shows expenses with status: 'approved' that are awaiting payment
  • Blue styling to distinguish from pending (yellow) and confirmed (green)
  • Displays approval date and receipt access
  • Technical: Added approvedExpenses filter and new tab UI section

Parent Org Filtering (Dec 31, 2025)

  • Problem: Users could select the parent parent_org organization (e.g., "InFocus Admin") when submitting reimbursements, causing RLS policy violations since expenses should be submitted at the fund level
  • Solution: Added expenseEligibleEntities filter to exclude parent_org type organizations from the nonprofit dropdown
  • Behavior:
  • In multi-org setups, the parent parent_org org is hidden from the dropdown
  • In single-org setups (standalone nonprofit), the org is shown regardless of type
  • The 'all' pseudo-entity is always excluded
  • Technical: Uses useMemo to filter entities by type, with special handling for single-org scenarios

Batch Persistence - localStorage (Dec 22, 2025)

  • Problem: Receipts scanned in a batch were lost if user navigated away before submitting
  • Solution: Added localStorage persistence for receipt batches
  • Behavior:
  • Batch is automatically saved to localStorage as receipts are scanned
  • On return to page, saved batch is restored with notification
  • Batches expire after 24 hours
  • "Clear" button allows users to discard saved batch
  • Batch is cleared from localStorage after successful submission
  • Technical: Uses localStorage with entity-specific keys (alignmint-reimbursement-batch-{entityId})
  • Note: Receipt images can be large; if localStorage quota is exceeded, persistence fails gracefully

Overview

The Reimbursements Manager is the staff-facing expense submission tool. It allows fund users and volunteers to scan receipts, submit expense reimbursement requests, save drafts, and track approval status. Admin-side approval/rejection/payment is handled in ExpensesManager.tsx.

Features mobile-friendly receipt capture via camera or file upload, OCR-powered auto-fill, multi-currency support with live exchange rates, and batch submission with localStorage persistence.

Parallel to Regular / Check deposit batching: The Submit tab’s capture → review → Complete flow is the same stage a batch, review, then commit pattern as [Regular Deposit](./10-REGULAR-DEPOSIT-MANAGER.md) (Add to batch → batch review step) and [Check Deposit](./08-CHECK-DEPOSIT-MANAGER.md) (Complete step). The Drafts tab here stores server-backed draft expenses; the Complete step holds an in-memory / localStorage staged batch (alignmint-reimbursement-batch-*) until submit or save-as-draft — analogous to the deposit managers’ draft batches (alignmint-deposit-batch-*, alignmint-check-batch-*).

UI Features

Tab Navigation (5 tabs, pill bar with horizontal scroll)

1. Submit — Multi-step receipt capture flow (capture → review → complete) 2. Drafts — Saved drafts with expiry countdown; row click opens receipt preview; for edit / submit / delete 3. Pending — Submitted expenses awaiting admin approval (grouped by fund); row click for receipt; for edit / delete / resubmit when rejected 4. Approved — Expenses approved but not yet paid (grouped by fund); row click for receipt (read-only, no ) 5. Confirmed — Paid/completed expenses (grouped by fund); row click for receipt (read-only, no )

Receipt Capture Flow (Submit tab)

  • Step 1 — Capture: Camera capture (mobile), file upload (desktop, accepts images + PDF), drag-and-drop, or manual entry (no receipt). Primary upload/drag/OCR hint strings use **common.jsoncaptureUpload.*; manual entry + receipt-specific OCR line use reimbursements.capture.*. Multi-page PDFs open PdfPagePickerDialog** (see Recent Fixes).
  • Step 2 — Review: OCR auto-fills vendor, amount, date, currency (or blank fields for manual entry). User can edit all fields except expense account (assigned by bookkeeper in ExpensesManager). Multi-currency support with exchange rate display. Manual entries show a placeholder instead of receipt image.
  • Step 3 — Complete: Review all receipts in batch, select nonprofit, submit or save as draft.
  • Batch counter shows receipt count and animated total (useCountUp)
  • Batches persist to localStorage (24-hour expiry, entity-specific key) and restore through an auto-sanitization pass for stale nonprofit/account references

Pending/Approved Tabs

  • Expenses grouped by fund with expandable accordion layout
  • Each group shows fund name, entity icon, expense count badge, and total amount
  • Tables use table-fixed w-full with responsive column hiding
  • Pending tab includes revision warning banner for rejected expenses
  • Receipt viewer via shared <ReceiptViewer> component

Confirmed Tab

  • Grouping: Expenses grouped by entity/ministry (not payment batch) with expandable accordion layout
  • Filter Bar: Entity filter dropdown (when multiple entities have confirmed expenses) + search input (vendor/description/amount)
  • Export: Export button in header opens SimpleExportDialog for CSV/XLSX/PDF export of filtered results
  • Summary Stats: Shows filtered item count and total when filters are active
  • Table Columns: Date, Vendor, Submitted By, Description, Amount, Category, Paid By, Paid Date (responsive column hiding on smaller screens; receipt via row click, not a dedicated column)
  • Empty States: Three states via EmptyState component: select nonprofit prompt, no confirmed expenses, no search results

Actions

  • Submit tab: Scan receipt, confirm details, add more, submit batch, save as draft; on the Complete step, each staged receipt row supports Eye (preview) and delete
  • Drafts tab: Row click (or focused row Enter / Space) opens receipt preview when receiptUrl exists; → edit draft, submit draft, delete draft
  • Pending tab: Same row interaction for receipt; → edit, delete, resubmit when rejected
  • Approved/Confirmed tabs: Read-only — row click opens receipt preview when available; no / Actions column

Data Layer

All data access goes through Supabase client functions in src/lib/db/expenses.ts, re-exported from src/lib/db.ts. There is no REST API server.

Key Functions

| Function | Description |
|----------|-------------|
| `fetchExpensesPage(entityId, { page, pageSize, status, search })` | RPC-backed paginated expense fetch with summary stats |
| `fetchDraftExpenses(entityId, userId, options?)` | Fetch user's draft expenses |
| `createExpense(expense)` | Create expense with receipt upload and multi-currency fields |
| `updateExpense(id, updates)` | Update non-draft expense fields (pending edits from UI) |
| `updateDraftExpense(id, updates)` | Update draft expense fields only (`status='draft'` guard) |
| `deleteExpense(id)` | Void a pending/rejected expense (soft-delete lifecycle) |
| `resubmitExpense(id)` | Resubmit a rejected expense for approval |
| `submitDraft(draftId)` | Submit single draft → pending |
| `submitAllDrafts(entityId, userId)` | Batch submit all drafts |
| `deleteDraft(draftId)` | Delete a draft expense |
| `fetchSupportedCurrencies()` | Get active currencies from `supported_currencies` table |

Image Handling

  • uploadReceiptImage(input, orgId, type) — Compresses via compressImage() (EXIF-aware via createImageBitmap), uploads to Supabase Storage receipts bucket
  • getReceiptImageUrl(filePath) — Returns signed URL (1-hour expiry)
  • compressImage(input, maxWidth, quality) — JPEG compression with iOS Safari canvas size guard

OCR

  • Edge function: scan-receipt — Accepts base64 image, returns vendor, amount, date, category, currency
  • Provider responses to know:
  • OCR service rejected request (402) = provider rejected due to billing/quota/credits.
  • Service is temporarily unavailable (or similar 5xx) = transient provider issue.
  • Retry helper now skips likely non-retriable provider 4xx failures and retries once for transient failures.
  • Category-to-account mapping: hardcoded categoryToAccountCode map (e.g., 'travel' → '5600'). Runs silently — not shown to submitter. Provides a pre-filled suggestion for the bookkeeper in ExpensesManager.

Data Schema

Expense Record (from expenses table)

| Field | Type | Description |
|-------|------|-------------|
| `id` | uuid | Primary key |
| `organization_id` | uuid | Fund/nonprofit that owns the expense |
| `submitted_by` | uuid | User who submitted |
| `vendor` | string | Vendor/merchant name |
| `amount` | decimal | USD amount (converted if foreign currency) |
| `date` | date | Expense date |
| `category` | string | Account code (e.g., '5300') |
| `description` | text | Expense description |
| `receipt_url` | string | Storage path for receipt image |
| `status` | string | `'draft'`, `'pending'`, `'approved'`, `'rejected'`, `'paid'` |
| `batch_id` | string | Groups receipts submitted together |
| `original_amount` | decimal | Amount in original currency |
| `original_currency` | string | ISO 4217 code (USD, MXN, EUR, etc.) |
| `exchange_rate` | decimal | Rate: original → USD |
| `exchange_rate_date` | date | Date rate was fetched |
| `exchange_rate_override` | boolean | Whether user manually set the rate |
| `revision_notes` | text | Admin notes when requesting revision |
| `approved_by` | uuid | Admin who approved |
| `approved_date` | datetime | When approved |
| `paid_by` | uuid | Admin who marked as paid |
| `paid_at` | datetime | When paid |
| `draft_created_at` | datetime | When draft was saved |
| `draft_expires_at` | datetime | 90 days after draft creation |

Authentication & Authorization

Access Control

  • Uses usePermissions() hook → canSubmitExpenses for submission permission (NOT canWriteAccounting — reimbursements are a Tools hub action, not Fund Accounting)
  • Uses useExpenseEligibleEntities() to exclude parent_org from nonprofit dropdown
  • Parent org admins (user.role === 'parent_org') can view all entities
  • Fund users can only submit/view expenses for their assigned entities

RLS Policies (Supabase)

  • SELECT: Users can view expenses for organizations they belong to; parent org admins see all
  • INSERT: Users can create expenses for their organizations; parent org admins can create for any child org
  • DELETE: Users can delete their own pending/draft expenses
  • Uses is_parent_org_admin() DB function (not the dropped users.role column)

Business Logic

Frontend Validations

  • Vendor, amount, and nonprofit are required (expense account is assigned by bookkeeper, not submitter)
  • Amount must be > 0
  • Exchange rate must be > 0 (for non-USD currencies)
  • Entity must be in user's expenseEligibleEntities list
  • Permission check via canSubmitExpenses before submission

Multi-Currency Flow

1. OCR detects currency from receipt 2. If non-USD, exchange rate auto-fetched from get-exchange-rate edge function 3. User can manually override the rate 4. If rate fetch fails, user is prompted with a fallback confirmation dialog 5. USD amount = originalAmount × exchangeRate (rounded to 2 decimals) 6. Rate date: Fetches use the receipt expense date when it is set as YYYY-MM-DD; otherwise today (submission date). Changing the receipt date (without a manual rate override) refetches the rate for that date. 7. Display vs ledger: The review step shows ledger USD with formatCurrencyStatic so it never doubles as “display currency” (Settings). If the user’s display currency is not USD, a separate line shows the approximate amount in display currency via formatCurrency.

Global display currency (Settings)

Users may set Display currency under Settings → Preferences. That preference drives useCurrencyFormat().formatCurrency() across the app (USD ledger × USD→display rate). Reimbursement review explicitly labels USD recorded in books and must use static USD formatting there so submitters are not confused (e.g. seeing a peso-scale number next to “USD”).

Draft System

  • Drafts expire after 90 days
  • Edge function check-expiring-drafts sends reminders at 60, 30, and 7 days
  • Expired drafts are auto-deleted
  • Drafts tab shows days-until-expiry badge (destructive if ≤7 days)
  • Draft rows now include a per-row edit action (pencil) in Actions
  • Draft edits use the same modal shell as pending edits, with full currency-aware controls:
  • originalAmount + originalCurrency
  • exchangeRate refresh/manual override
  • date-based exchange rate refetch (unless overridden)
  • computed USD ledger amount preview
  • Draft save path uses updateDraftExpense() with status='draft' guard and refreshes both draft and non-draft queries
  • Approved and Confirmed remain read-only for audit trail integrity

Notifications

  • On submission: createNotificationForParentOrgAdmins() sends notification with expense count and total
  • Notification types: expense_submitted, draft_expiring, draft_expired

State Management

Local State

| Variable | Type | Description |
|----------|------|-------------|
| `activeTab` | `ActiveTab` | Current tab: `'reimbursements'`, `'drafts'`, `'pending'`, `'approved'`, `'confirmed'` |
| `step` | `Step` | Capture flow step: `'capture'`, `'review'`, `'complete'` |
| `receipts` | `ReceiptData[]` | Current batch of scanned receipts |
| `currentReceipt` | `ReceiptData \| null` | Receipt being edited in review step |
| `isProcessing` | `boolean` | OCR/submission in progress |
| `receiptViewerOpen` | `boolean` | Shared ReceiptViewer dialog state |
| `receiptViewerData` | `object \| null` | Data for ReceiptViewer (imageUrl, vendor, amount, etc.) |
| `expandedFundGroups` | `Set<string>` | Which fund accordion groups are expanded |
| `editExpenseOpen` | `boolean` | Edit expense dialog state |
| `editingExpense` | `Expense \| null` | Expense being edited |
| `formData` | `{ entityId }` | Selected nonprofit for submission |
| `rateFallbackDialogOpen` | `boolean` | Exchange rate fallback confirmation |
| `confirmedEntityFilter` | `string` | Entity filter for Confirmed tab (default: `'all'`) |
| `confirmedSearchQuery` | `string` | Search query for Confirmed tab filtering |
| `exportDialogOpen` | `boolean` | SimpleExportDialog state for Confirmed tab export |

Global State (Zustand via useAppStore)

  • selectedEntity — Current entity selection (global dropdown)
  • entities — All entities for the user
  • entitiesLoading — Loading state for entities

React Query Keys

  • ['accounts', safeSelectedEntity] — Expense accounts for the selected entity
  • ['expensesPage', safeSelectedEntity, 'pending', pendingPage, PAGE_SIZE] — Pending/revision reimbursements
  • ['expensesPage', safeSelectedEntity, 'approved', approvedPage, PAGE_SIZE] — Approved reimbursements
  • ['expensesPage', safeSelectedEntity, 'paid', confirmedPage, PAGE_SIZE, confirmedSearchQuery] — Confirmed reimbursements
  • ['draft-expenses', safeSelectedEntity, userId] — User's draft expenses
  • ['supported-currencies'] — Active currencies from DB

Dependencies

Internal

  • src/lib/db/expenses.ts — All expense CRUD functions
  • src/lib/imageUtils.ts — Image compression and Supabase Storage upload
  • src/lib/entityMapping.tsgetActualOrgId() for org UUID resolution
  • src/lib/exportUtils.tsexportToCSV(), exportToExcel(), exportToPDF() for Confirmed tab export
  • src/components/shared/ReceiptViewer.tsx — Receipt image viewer dialog
  • src/components/shared/ConfirmDeleteDialog.tsx — Delete confirmation
  • src/components/shared/EmptyState.tsx — Standardized empty state component
  • src/components/ui/export-dialog.tsxSimpleExportDialog for format selection
  • src/components/shared/PageHeader.tsx / Breadcrumb.tsx — Page header
  • src/hooks/useCountUp.ts — Animated number count-up
  • src/hooks/useEntities.tsuseExpenseEligibleEntities()
  • src/hooks/usePermissions.tscanSubmitExpenses
  • src/hooks/useCurrencyFormat.tsformatCurrency (display preference), formatCurrencyStatic (true USD on review step)
  • src/hooks/usePreferencesSync.ts — loads display_currency and display exchange rate on login

External Libraries

  • @tanstack/react-query — Data fetching and caching
  • lucide-react — Icons (Camera, Upload, Check, Trash2, Eye, etc.)
  • sonner — Toast notifications
  • react-i18next — Internationalization

Error Handling

| Scenario | Behavior |
|----------|----------|
| OCR timeout (30s) | Toast error, fields left blank for manual entry |
| OCR service error | Toast with specific message, fallback to manual entry |
| OCR provider 402 | Upstream billing/quota rejection; retry is unlikely to succeed until provider account state is fixed |
| Receipt upload failure | `createExpense` falls back to base64 storage |
| Exchange rate fetch failure | Toast warning, rate defaults to 1.0, fallback dialog on submit |
| localStorage quota exceeded | Warning logged, batch not persisted (graceful degradation) |
| Permission denied | Toast "You do not have permission" |
| Entity not accessible | Toast "You do not have access to this nonprofit" |
| Expense update/delete failure | Toast with error message |

Loading States

| Context | Implementation |
|---------|---------------|
| Initial expense load | `<SkeletonTable>` with appropriate headers |
| Draft list loading | `<SkeletonTable>` |
| OCR processing | `isProcessing` state disables UI, shows processing indicator |
| Submission | `isProcessing` disables submit button |
| Entity selector | Disabled when `expenseEligibleEntities` is empty |

Related Documentation


Synced from IFMmvp-Frontend documentation: pages/accounting/07-REIMBURSEMENTS-MANAGER.md

Ready to Get Started?

See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.

Questions? Email us at steven@getalignmint.org

Ready to get started?Start Plus Trial