import { useRef, useState } from 'react'; import { toast } from 'sonner'; import { FileText, Upload, Loader2, CheckCircle2, X } from 'lucide-react'; import { api } from '@/api'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { formatCentsUSD } from '@/lib/money'; import { SectionCard, importErrorState } from './dataShared'; /** * 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 = {} }) { const fileRef = useRef(null); const [preview, setPreview] = useState(null); // { import_session_id, count, sample } const [busy, setBusy] = useState(null); // 'preview' | 'commit' | null async function handleFile(e) { const file = e.target.files?.[0]; if (!file) return; setBusy('preview'); setPreview(null); try { setPreview(await api.previewOfxTransactionImport(file)); } 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 }); const parts = [`${r.imported} 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(err.message || '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
  • )}
)}
); }