Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
e80fe61178
commit
6287b2bc3f
@@ -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<Record<string, string>>(file, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: async (parsed) => {
|
||||
try {
|
||||
const headers = parsed.meta.fields ?? [];
|
||||
const map: Record<string, string> = {};
|
||||
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<string, string>();
|
||||
const caseByTitle = new Map<string, string>();
|
||||
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<string, string> = {};
|
||||
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 (
|
||||
<Button asChild size="sm" variant="outline" disabled={busy} title="Import trust ledger from CSV (columns: Date, Type, Amount, Note, optional Case; or Debit/Credit)">
|
||||
<label className="cursor-pointer">
|
||||
{busy ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <Upload className="h-3.5 w-3.5 mr-1" />}
|
||||
Import CSV
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
e.target.value = "";
|
||||
if (f) onPick(f);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user