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 occasionalService is temporarily unavailablesurfaced fromscan-receiptviasrc/lib/ocrUtils.ts. - Status semantics:
402from 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 provider4xxmessages such as402) 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-receiptnow attemptsopenai/gpt-4o,openai/gpt-4o-mini, thenanthropic/claude-3.5-sonnetfor 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
expenseEligibleEntitiesand current expense accounts: - invalid
entityIdvalues 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:
expensesUPDATE RLS now allows non-parent users to update only their own reimbursement rows indraft/pending/rejectedstates (plusvoidedtransition). Parent-org admins keep broad update access for approval and payment operations. - P0 — Pending feed consistency:
get_expenses_pageis now checked intosupabase/migrationsand 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 bothpendingandrejectedrows so “Needs Revision” items always stay visible in the Pending tab. - P1 — Constraint alignment:
supabase/migrationsnow includesvoidedinexpenses_status_checkso 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 (stashedFile). 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.json→captureUpload.*. Reimbursement-only manual entry and receipt-specific OCR note:reimbursements.capture.manualEntry,reimbursements.capture.ocrNote. Picker dialog:common→pdfPicker.*(with `defaultValue` fallbacks). Resume copy:reimbursements.resumePdf**. - Files:
ReimbursementsManager.tsx, sharedPdfPagePickerDialog.tsx,AttachmentPreview.tsx,ReceiptViewer.tsx,pdfClient.ts,ocrUtils.ts. See also08-CHECK-DEPOSIT-MANAGER.mdandDEVELOPER-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
ReceiptViewerwhenreceiptUrlis set; otherwise a short toast (labels.noReceiptToView). ⋮ (MoreVertical) opens aDropdownMenufor 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 usesstopPropagationso 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 statusdraft,pending, orrejected, the footer shows Edit reimbursement; it closes the preview and opens the existing edit dialog. Submit-step “complete” list still opens the viewer from unstagedReceiptDataonly —receiptViewerSourceExpenseis 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:
Actionsusedw-[80px]while Drafts rows render three controls; projectButtonsize="sm"still enforcesmin-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-0on cell). Row controls usesize="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.editExpenseandpending.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()insrc/lib/db/expenses.tswith.eq('status', 'draft')safety guard.handleSaveExpensenow uses this function for draft rows and refetchesdraft-expensesafter successful save.
P1 — Draft edit modal only changed ledger USD amount
- Previous modal did not expose
original_amount,original_currency, orexchange_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
titlebut not explicitaria-label. - Fix: Added
aria-labelon 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 + refetchsrc/lib/db/expenses.ts—updateDraftExpense()helper + shared update payload builder07-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-rowbreakpoint (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-0on the image column prevented any compression. Currencygrid grid-cols-2forced two inputs side-by-side in ~300px. - Fix: Changed
md:flex-row→lg:flex-row(1024px+), image columnmd:w-[40%]→lg:w-[35%], currency gridgrid-cols-2→grid-cols-1 sm:grid-cols-2. Allmd:responsive prefixes on the review step image/form updated tolg:.
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.subtitlekey ("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 usedvariant="ghost"+text-destructive. - Fix: Changed to
variant="ghost"withtext-destructiveon 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
useEffecthooks that auto-expand whenpendingByFund,approvedByFund, orfilteredConfirmedByFundhas exactly 1 group.
P3 — Duplicate Actions i18n keys
pending.actions,drafts.actions,approved.actionsall had the same translation.- Fix: Added shared
labels.actionskey 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.jsonlocale files —confirmed.subtitle,labels.actions 07-REIMBURSEMENTS-MANAGER.md— this changelogDEVELOPER-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
fetchExpensespattern. 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()writesstatus: 'voided'andcreateExpense()can writestatus: 'draft', but the TypeScriptExpenseinterface only had'pending' | 'approved' | 'paid' | 'rejected'. Added'reimbursed' | 'draft' | 'voided'to bothdb/expenses.tsandtypes/data.ts. Also syncedvendors.tsstatus 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
noApprovedDescwhich says "Approved expenses will appear here" — misleading when expenses exist. Added separateapproved.subtitlekey: "Expenses that have been approved and are awaiting payment."
P2 — 4 hardcoded `aria-label` strings (§21)
"View receipt","Edit expense"(×2),"Delete expense"→ wrapped witht()usinglabels.viewReceipt,labels.editExpense,labels.deleteExpensekeys (copy later updated to “Edit reimbursement”, Mar 25, 2026).labels.viewReceiptremains 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 subtitlesrc/lib/db/expenses.ts—fetchDraftExpensesexplicit columns + limit,Expense.statustypesrc/types/data.ts—Expense.statustype syncsrc/lib/db/vendors.ts— status cast sync- 7 ×
reimbursements.jsonlocale files — 4 new keys (labels.viewReceipt,labels.editExpense,labels.deleteExpense,approved.subtitle) 07-REIMBURSEMENTS-MANAGER.md— this changelogDEVELOPER-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-2withmax-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-rowlayout. Image column getsmd:w-[40%]withmd:min-h-[400px],md:sticky md:top-4, andmd:max-h-[600px]. Mobile retains compactmax-h-56. Image centered vertically viaflex 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 usingreceiptViewer.*keys incommonnamespace. 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 toreimbursements.json× 7 locales.
P2 — §11 query guard: expenses + drafts queries missing `isMappingInitialized()`
expensesanddraft-expensesReact Query hooks hadenabled: safeSelectedEntity !== 'all'but were missing theisMappingInitialized()guard. Theaccountsquery correctly had it.- Fix: Added
isMappingInitialized() &&to both queryenabledconditions.
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 sharedEmptyStatecomponent. - Fix: Replaced both with
<EmptyState icon={...} title={...} description={...} />.
Files Changed:
ReimbursementsManager.tsx— receipt layout, i18n, §11 guards, §43 empty statesReceiptViewer.tsx— 7 hardcoded labels →t()- 7 ×
reimbursements.jsonlocale files — ~6 new keys each - 7 ×
common.jsonlocale files — 7receiptViewer.*keys each 07-REIMBURSEMENTS-MANAGER.md— this changelogDEVELOPER-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-USlocale withformatMonthYear()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))infetchExpenses()andfetchDraftExpenses().
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, formatMonthYearsrc/lib/db/expenses.ts— §22 date fixes, dead status removal- 7 ×
reimbursements.jsonlocale 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_userroles from Fund Accounting (JEs, deposits, reconciliation), butReimbursementsManagerwas gating submission oncanWriteAccountinginstead ofcanSubmitExpenses. SincecanAccessAccounting()returnsfalsefor all fund_users, every fund_user lost the ability to submit receipts, save drafts, and edit pending expenses. - Root Cause:
canWriteAccountingdepends oncanAccessAccounting()which hasif (ctx.role === 'fund_user') return false. Reimbursement submission is a Tools hub action, not a Fund Accounting operation. - Fix: Added
canSubmitExpenses()topermissions.ts— allowsread_writeaccess level users (excluding portal roles) to submit. Replaced all 6canWriteAccountingreferences inReimbursementsManager.tsxwithcanSubmitExpenses. Wired intousePermissionshook. - 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 matchingExpensesManagerpattern. 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
SimpleExportDialogwith 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
EmptyStatecomponent on Confirmed tab. Three states: select nonprofit prompt, no confirmed expenses, no search results. - P2 — Cleanup: Removed unused
confirmedByFundmemo (replaced byfilteredConfirmedByFundwhich includes filtering logic). - Data Layer: Uses existing
fetchExpenses()withstatus: 'paid'. No new DB queries. Grouping viagroupByFund()helper. Export viaexportToCSV(),exportToExcel(),exportToPDF()fromsrc/lib/exportUtils.ts. - State Added:
confirmedEntityFilter(string, default 'all'),confirmedSearchQuery(string, default ''),exportDialogOpen(boolean). - Files Modified:
ReimbursementsManager.tsx, 6 ×reimbursements.jsonlocale 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'saccept="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/onDrophandlers with visual feedback (isDraggingstate, 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
ReceiptDataand skips OCR, going straight to review. Addresses "won't let me enter the amount on my own" complaint. - P0 #4 — `reader.onerror` handler:
handleFileCapturenow has areader.onerrorcallback that shows a toast and resetsisProcessing. Previously, ifFileReaderfailed (corrupted file, unsupported format on Safari), the user got zero feedback. - P0 #5 — Pre-OCR image compression:
processReceiptOCRnow compresses images viacompressImage(url, 1600, 0.85)before extracting base64 for the edge function. Matches theCheckDepositManagerpattern. Prevents payload size failures on large iPhone photos (4-8MB JPEG → 5-11MB base64). - P1 #1 — `placeholderData: keepPreviousData`: Added to
expensesanddraft-expensesReact 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)}withformatCurrency()fromuseCurrencyFormathook (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
expenseEligibleEntitiesis empty, a red message now explains why buttons are disabled. - Files Modified:
ReimbursementsManager.tsx,ReceiptViewer.tsx, 6 ×reimbursements.jsonlocale 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
useMemocombiningallExpensesanddraftExpensesfiltered byuser.id. No new DB queries. - UI:
Card/CardHeader/CardContent,tabular-nums,formatCurrency(),useCountUp(), progress bars (bg-primaryfill),Badge variant="secondary". - i18n: Added
monthlySummary.titleandmonthlySummary.lastNMonthskeys to all 6 locale files (en,de,es,fr,th,zh). - Files Modified:
ReimbursementsManager.tsx, 6 ×reimbursements.jsonlocale 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()→submittedByfield 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.submittedByin a new column (hidden onxl:breakpoint for responsive layout). - P0 — Notifications RLS fix: The INSERT policy on
notificationswas failing when a parent_org admin approved an expense for a child org user. Condition 2 only checked the inserter's ownorganization_usersmemberships, which didn't include child orgs. Broadened the policy to includeorganizations.parent_organization_idlookups 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 inExpensesManager.tsx(pending group total, pending amount cells, approved amount cells, paid amount cells) withformatCurrency(). - Files Modified:
ExpensesManager.tsx,ReimbursementsManager.tsx,07-REIMBURSEMENTS-MANAGER.md - DB Migration:
fix_notifications_rls_child_orgs(Supabase projectzlokhayitthdzitjysht)
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
categoryToAccountCodemapping 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),
createExpenseuses'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()inimageUtils.tsnow usescreateImageBitmap({ imageOrientation: 'from-image' })to auto-correct rotated mobile photos. Falls back toImage()for older browsers. Fixes Jacqueline's "picture flipped" report. - P0 #2 — Dead state removed: Removed unused
viewReceiptOpen/selectedReceiptForViewstate variables (replaced byreceiptViewerOpen/receiptViewerDatain Feb 10 refactor). - P0 #3 — Stale closure fix:
handleDeleteReceiptnow uses functionalsetReceipts(prev => ...)to avoid stale closure when deleting the last receipt. - P1 #4 — `font-mono` → `tabular-nums`: Replaced all 7
font-monoinstances on date/amount table cells withtabular-numsper styling guide. - P1 #5 — Amount color fix: Removed
text-primaryfrom 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-cellon Confirmed tab<TableCell>elements to match their<TableHead>counterparts. - P2 — Unused imports: Removed
Reactnamespace,Xicon,createJournalEntryFromTransaction,useQueryClient, and unusedqueryClientdeclaration. - P2 — Helper extraction: Replaced 5 repeated IIFE account lookups with
getCategoryDisplay()andgetCategoryName()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-autoto container andwhitespace-nowrapto 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 excludestatus='draft'when no specific status was passed. Added.neq('status', 'draft')to the query. - P0 #4 — View Receipt broken in Complete step & Drafts tab:
viewReceiptOpenstate was set but no Dialog was rendered. Rewired both click handlers to use the sharedreceiptViewerOpen/receiptViewerData+<ReceiptViewer>pattern. - P0 #5 — localStorage key mismatch:
handleSaveAsDraftclearedalignmint-reimbursement-batch-${entityId}(from formData) instead of theSTORAGE_KEY(fromsafeSelectedEntity). Replaced withclearSavedBatch()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 toa.code === draft.category. - P1 #4/#5 — Approved tab responsive columns: Category and Approved By cells were missing
hidden lg:table-cell/hidden xl:table-cellclasses, 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
expensestable: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_triggerto auto-set expiry dates - Edge Function:
check-expiring-draftsruns 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 draftssubmitDraft(draftId)- Submit single draft for approvalsubmitAllDrafts(entityId, userId)- Batch submit all draftsdeleteDraft(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 droppedusers.rolecolumn. - Root Cause: The
users.rolecolumn was dropped on Jan 19, 2026. Role is now stored inorganization_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.sqlto replace allusers.rolechecks withis_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
approvedExpensesfilter 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
expenseEligibleEntitiesfilter 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
useMemoto 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.json→captureUpload.*; manual entry + receipt-specific OCR line usereimbursements.capture.*. Multi-page PDFs openPdfPagePickerDialog** (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-fullwith 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
SimpleExportDialogfor 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
EmptyStatecomponent: 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
receiptUrlexists; ⋮ → 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 viacompressImage()(EXIF-aware viacreateImageBitmap), uploads to Supabase StoragereceiptsbucketgetReceiptImageUrl(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
4xxfailures and retries once for transient failures. - Category-to-account mapping: hardcoded
categoryToAccountCodemap (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 →canSubmitExpensesfor submission permission (NOTcanWriteAccounting— 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 droppedusers.rolecolumn)
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
expenseEligibleEntitieslist - Permission check via
canSubmitExpensesbefore 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-draftssends 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+originalCurrencyexchangeRaterefresh/manual override- date-based exchange rate refetch (unless overridden)
- computed USD ledger amount preview
- Draft save path uses
updateDraftExpense()withstatus='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 userentitiesLoading— 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 functionssrc/lib/imageUtils.ts— Image compression and Supabase Storage uploadsrc/lib/entityMapping.ts—getActualOrgId()for org UUID resolutionsrc/lib/exportUtils.ts—exportToCSV(),exportToExcel(),exportToPDF()for Confirmed tab exportsrc/components/shared/ReceiptViewer.tsx— Receipt image viewer dialogsrc/components/shared/ConfirmDeleteDialog.tsx— Delete confirmationsrc/components/shared/EmptyState.tsx— Standardized empty state componentsrc/components/ui/export-dialog.tsx—SimpleExportDialogfor format selectionsrc/components/shared/PageHeader.tsx/Breadcrumb.tsx— Page headersrc/hooks/useCountUp.ts— Animated number count-upsrc/hooks/useEntities.ts—useExpenseEligibleEntities()src/hooks/usePermissions.ts—canSubmitExpensessrc/hooks/useCurrencyFormat.ts—formatCurrency(display preference),formatCurrencyStatic(true USD on review step)src/hooks/usePreferencesSync.ts— loadsdisplay_currencyand display exchange rate on login
External Libraries
@tanstack/react-query— Data fetching and cachinglucide-react— Icons (Camera, Upload, Check, Trash2, Eye, etc.)sonner— Toast notificationsreact-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
- 06-EXPENSES-MANAGER.md — Admin-side expense approval/payment
- 04-GENERAL-LEDGER.md — Where approved expenses appear
- 03-CHART-OF-ACCOUNTS.md — Expense account codes
- 01-DATA-SCHEMA.md — historical database schema
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