import { useRef, useState } from 'react'; import { toast } from 'sonner'; import { FileText, Upload, Loader2, CheckCircle2, X } from 'lucide-react'; import { api } from '@/api'; import { cn, errMessage } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { formatCentsUSD, type Cents } from '@/lib/money'; import { SectionCard, importErrorState, type SectionCardProps } from './dataShared'; interface OfxTx { payee?: string | null; description?: string | null; posted_date?: string | null; amount: Cents; } interface OfxPreview { import_session_id?: string; count: number; sample?: OfxTx[]; } /** * Import bank transactions from an OFX / QFX file. Unlike CSV, the file is * structured, so there is no column-mapping step: upload → preview → import. * Duplicates are skipped by the server (same dedupe scope as CSV/SimpleFIN). */ export default function ImportOfxSection({ onHistoryRefresh, cardProps = {} }: { onHistoryRefresh?: () => void; cardProps?: Partial; }) { const fileRef = useRef(null); const [preview, setPreview] = useState(null); const [busy, setBusy] = useState<'preview' | 'commit' | null>(null); async function handleFile(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setBusy('preview'); setPreview(null); try { setPreview(await api.previewOfxTransactionImport(file) as OfxPreview); } catch (err) { toast.error(importErrorState(err, 'Could not read that OFX/QFX file.').message); } finally { setBusy(null); if (fileRef.current) fileRef.current.value = ''; } } async function handleImport() { if (!preview?.import_session_id) return; setBusy('commit'); try { const r = await api.commitOfxTransactionImport({ import_session_id: preview.import_session_id }) as { imported?: number; skipped?: number; failed?: number }; const parts = [`${r.imported ?? 0} imported`]; if (r.skipped) parts.push(`${r.skipped} already present`); if (r.failed) parts.push(`${r.failed} failed`); toast.success(`OFX import complete — ${parts.join(', ')}.`); setPreview(null); onHistoryRefresh?.(); } catch (err) { toast.error(errMessage(err, 'OFX import failed.')); } finally { setBusy(null); } } return (

Many banks export .ofx or{' '} .qfx files. Upload one to import its transactions — duplicates are skipped automatically.

{!preview ? (
) : (

{preview.count} transaction{preview.count === 1 ? '' : 's'} found

    {(preview.sample || []).map((tx, i) => (
  • {tx.payee || tx.description || 'Transaction'} {tx.posted_date} {formatCentsUSD(tx.amount, { signed: true })}
  • ))} {preview.count > (preview.sample || []).length && (
  • + {preview.count - (preview.sample || []).length} more
  • )}
)}
); }