diff --git a/src/components/trust/trust-account-panel.tsx b/src/components/trust/trust-account-panel.tsx index 01b8f55..80f073c 100644 --- a/src/components/trust/trust-account-panel.tsx +++ b/src/components/trust/trust-account-panel.tsx @@ -673,3 +673,153 @@ export function PushPaymentToTrustButton({ ); } + +const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ""); + +function ImportTrustCsvButton({ + clientId, + defaultCaseId, + cases, + userId, + onImported, +}: { + clientId: string; + defaultCaseId: string | null; + cases: CaseOption[]; + userId: string | null; + onImported: () => void; +}) { + const [busy, setBusy] = useState(false); + + const onPick = (file: File) => { + setBusy(true); + Papa.parse>(file, { + header: true, + skipEmptyLines: true, + complete: async (parsed) => { + try { + const headers = parsed.meta.fields ?? []; + const map: Record = {}; + for (const h of headers) { + const n = norm(h); + if (["date", "entrydate", "txdate"].includes(n)) map[h] = "date"; + else if (["type", "entrytype", "txtype", "transactiontype"].includes(n)) map[h] = "type"; + else if (["amount", "total"].includes(n)) map[h] = "amount"; + else if (["deposit", "credit"].includes(n)) map[h] = "deposit"; + else if (["withdrawal", "debit", "payment"].includes(n)) map[h] = "withdrawal"; + else if (["note", "notes", "memo", "description"].includes(n)) map[h] = "note"; + else if (["case", "casenumber", "casename", "matter"].includes(n)) map[h] = "case"; + } + // Build case lookup + const caseByNum = new Map(); + const caseByTitle = new Map(); + for (const c of cases) { + if (c.case_number) caseByNum.set(c.case_number.toLowerCase().trim(), c.id); + if (c.title) caseByTitle.set(c.title.toLowerCase().trim(), c.id); + } + + const rows: Array<{ + client_id: string; + case_id: string | null; + entry_date: string; + entry_type: string; + amount: number; + note: string | null; + created_by: string | null; + }> = []; + let skipped = 0; + + for (const raw of parsed.data) { + const row: Record = {}; + for (const [h, k] of Object.entries(map)) { + const v = (raw as any)[h]; + if (v != null && v !== "") row[k] = String(v).trim(); + } + const dep = Number(row.deposit) || 0; + const wdr = Number(row.withdrawal) || 0; + let amount = Number(row.amount) || 0; + let type: "deposit" | "withdrawal" | null = null; + const tRaw = (row.type ?? "").toLowerCase(); + if (["deposit", "credit", "in", "+"].includes(tRaw)) type = "deposit"; + else if (["withdrawal", "withdraw", "debit", "out", "-", "payment"].includes(tRaw)) type = "withdrawal"; + if (!type && dep > 0) { type = "deposit"; amount = dep; } + else if (!type && wdr > 0) { type = "withdrawal"; amount = wdr; } + if (!type && amount < 0) { type = "withdrawal"; amount = Math.abs(amount); } + if (!type && amount > 0) type = "deposit"; + if (!type || !amount || amount <= 0) { skipped++; continue; } + + // Date + let date = row.date || new Date().toISOString().slice(0, 10); + // Try Date parsing if not YYYY-MM-DD + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + const d = new Date(date); + if (!isNaN(d.getTime())) date = d.toISOString().slice(0, 10); + } + + // Optional case + let case_id: string | null = defaultCaseId; + if (row.case) { + const k = row.case.toLowerCase().trim(); + case_id = caseByNum.get(k) ?? caseByTitle.get(k) ?? case_id; + } + + rows.push({ + client_id: clientId, + case_id, + entry_date: date, + entry_type: type, + amount: Math.abs(amount), + note: row.note || null, + created_by: userId, + }); + } + + if (rows.length === 0) { + toast.error("No valid rows found in CSV"); + return; + } + + // Insert in chunks + const CHUNK = 200; + let inserted = 0; + for (let i = 0; i < rows.length; i += CHUNK) { + const chunk = rows.slice(i, i + CHUNK); + const { error } = await supabase.from("trust_ledger_entries").insert(chunk as any); + if (error) { + toast.error(`Import failed: ${error.message}`); + return; + } + inserted += chunk.length; + } + toast.success(`Imported ${inserted} entr${inserted === 1 ? "y" : "ies"}${skipped ? `, skipped ${skipped}` : ""}`); + onImported(); + } finally { + setBusy(false); + } + }, + error: (err) => { + toast.error(`CSV parse error: ${err.message}`); + setBusy(false); + }, + }); + }; + + return ( + + ); +}