import { useState, useRef } from "react"; import { useParams, Link } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getAccounts, previewImport, importCsvToAccount, type CsvMapping } from "@/api/accounts"; import { getTransactions } from "@/api/transactions"; import { formatCurrency } from "@/utils/currency"; import { cn } from "@/utils/cn"; import { format } from "date-fns"; import { ArrowLeft, Upload, FileText, XCircle, Loader2, CheckCircle, ArrowUpCircle, ArrowDownCircle, ArrowLeftRight, ChevronLeft, ChevronRight, } from "lucide-react"; export default function AccountDetail() { const { accountId } = useParams<{ accountId: string }>(); const qc = useQueryClient(); const [showImport, setShowImport] = useState(false); const [page, setPage] = useState(1); const { data: accounts = [] } = useQuery({ queryKey: ["accounts"], queryFn: getAccounts }); const account = accounts.find(a => a.id === accountId); const { data: txnData, isLoading: txnLoading } = useQuery({ queryKey: ["transactions", { account_id: accountId, page, page_size: 25 }], queryFn: () => getTransactions({ account_id: accountId, page, page_size: 25 }), enabled: !!accountId, }); if (!account) { return (

Account not found

); } const isLiability = ["credit_card", "loan", "mortgage"].includes(account.type); const utilPct = account.credit_limit && account.credit_limit > 0 ? Math.min(100, (Math.abs(account.current_balance) / account.credit_limit) * 100) : null; return (
{/* Back + header */}

{account.name}

{account.institution && `${account.institution} · `} {account.type.replace(/_/g, " ")} · {account.currency}

{/* Account stats */}

Current Balance

{formatCurrency(account.current_balance, account.currency)}

{account.credit_limit != null && (

Credit Limit

{formatCurrency(account.credit_limit, account.currency)}

)} {account.interest_rate != null && (

Interest Rate

{Number(account.interest_rate).toFixed(2)}% p.a.

)} {txnData && (

Total Transactions

{txnData.total}

)}
{/* Credit utilisation */} {utilPct !== null && (
Credit Utilisation 80 ? "text-destructive" : utilPct > 50 ? "text-yellow-500" : "text-success")}> {utilPct.toFixed(0)}%
80 ? "bg-destructive" : utilPct > 50 ? "bg-yellow-500" : "bg-success")} style={{ width: `${utilPct}%` }} />

{formatCurrency(Math.abs(account.current_balance), account.currency)} used of {formatCurrency(account.credit_limit!, account.currency)}

)} {account.notes && (

Notes

{account.notes}

)} {/* Transactions */}

Transactions

{txnData && txnData.pages > 1 && (
Page {page} of {txnData.pages}
)}
{txnLoading ? (
{[1, 2, 3, 4, 5].map(i =>
)}
) : !txnData?.items.length ? (
No transactions yet.{" "}
) : (
{txnData.items.map(txn => (
{txn.type === "income" ? : txn.type === "transfer" ? : }

{txn.description}

{format(new Date(txn.date), "dd MMM yyyy")}

{Number(txn.amount) > 0 ? "+" : ""}{formatCurrency(txn.amount, txn.currency)}

))}
)}
{showImport && account && ( setShowImport(false)} onSuccess={() => { qc.invalidateQueries({ queryKey: ["transactions"] }); qc.invalidateQueries({ queryKey: ["accounts"] }); qc.invalidateQueries({ queryKey: ["net-worth"] }); }} /> )}
); } // ─── Import Modal ───────────────────────────────────────────────────────────── type ImportStep = "upload" | "preview" | "done"; function ImportModal({ accountId, accountName, onClose, onSuccess, }: { accountId: string; accountName: string; onClose: () => void; onSuccess: () => void; }) { const fileRef = useRef(null); const [step, setStep] = useState("upload"); const [file, setFile] = useState(null); const [preview, setPreview] = useState> | null>(null); const [mapping, setMapping] = useState(null); const [result, setResult] = useState<{ imported: number; skipped: number } | null>(null); const [detecting, setDetecting] = useState(false); const [detectError, setDetectError] = useState(null); const importMutation = useMutation({ mutationFn: () => importCsvToAccount(accountId, file!, mapping!), onSuccess: (data) => { setResult(data); setStep("done"); onSuccess(); }, }); async function handleFileSelect(f: File) { setFile(f); setDetecting(true); setDetectError(null); try { const p = await previewImport(accountId, f); setPreview(p); setMapping(p.mapping); setStep("preview"); } catch (e: any) { setDetectError(e?.response?.data?.detail ?? "Failed to read file"); } finally { setDetecting(false); } } function onDrop(e: React.DragEvent) { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f?.name.toLowerCase().endsWith(".csv")) handleFileSelect(f); } const isSplit = mapping ? (!!mapping.debit && !!mapping.credit) : false; return (
{/* Header */}

Import CSV

into {accountName}

{/* Step: upload */} {step === "upload" && ( <>
e.preventDefault()} onClick={() => fileRef.current?.click()} className="border-2 border-dashed border-border rounded-xl p-10 text-center cursor-pointer hover:border-primary/50 transition-colors" > {detecting ? (

Detecting format…

) : (

Drop your bank CSV here

Supports Monzo, Starling, Revolut, Barclays, Lloyds, NatWest, HSBC, Santander, Nationwide

)} { const f = e.target.files?.[0]; if (f) handleFileSelect(f); }} />
{detectError &&

{detectError}

} )} {/* Step: preview + mapping */} {step === "preview" && preview && mapping && ( <> {/* Detected format badge */}
{preview.detected_format ? ( <> Detected: {preview.detected_format} ) : ( <> Unknown format — please verify column mapping below )} {preview.total_rows} rows
{/* Column mapping */}

Column Mapping

setMapping(m => m ? { ...m, date: v } : m)} /> setMapping(m => m ? { ...m, description: v } : m)} /> {isSplit ? ( <> setMapping(m => m ? { ...m, debit: v || null } : m)} /> setMapping(m => m ? { ...m, credit: v || null } : m)} /> ) : ( setMapping(m => m ? { ...m, amount: v || null } : m)} /> )}
{/* Preview table */}

Preview (first {preview.preview.length} rows)

{preview.preview.map((row, i) => ( ))}
Date Description Amount
{row.date_raw} {row.description_raw} = 0 ? "text-success" : "text-destructive" )}> {row.amount_raw != null ? formatCurrency(row.amount_raw, "GBP") : "—"}
{importMutation.isError && (

{(importMutation.error as any)?.response?.data?.detail ?? "Import failed"}

)}
)} {/* Step: done */} {step === "done" && result && (

{result.imported} transaction{result.imported !== 1 ? "s" : ""} imported

{result.skipped > 0 && (

{result.skipped} duplicate{result.skipped !== 1 ? "s" : ""} skipped

)}
)}
); } function ColSelect({ label, value, headers, onChange }: { label: string; value: string; headers: string[]; onChange: (v: string) => void; }) { return (
); }