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 (
);
}
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 && (
)}
{/* 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 ? (
) : (
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)
| Date |
Description |
Amount |
{preview.preview.map((row, i) => (
| {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 (
);
}