From 0462ffb3ec182974f0f8f196d6b148d94e302b51 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:03:38 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 29c79f5..addf9c7 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -1271,6 +1271,89 @@ function ImportPage() { const chunkSize = 200; let inserted = 0; const inserted_rows: Row[] = []; + + // INVOICES: special merge — match existing invoices by invoice_number + // (preferred) or external_id and UPDATE only header fields. Subtotal, + // total, tax, and amount_paid are left alone so totals stay = sum of + // line items already attached from the time/expense imports. + if (cfg.table === "invoices" && mapped.length > 0) { + const numbers = mapped.map((r) => String(r.invoice_number ?? "")).filter(Boolean); + const externalIds = mapped.map((r) => String(r.external_id ?? "")).filter(Boolean); + const byNumber = new Map(); + const byExt = new Map(); + for (let k = 0; k < numbers.length; k += 500) { + const slice = numbers.slice(k, k + 500); + const { data } = await supabase + .from("invoices") + .select("id, invoice_number") + .in("invoice_number", slice); + for (const row of (data ?? []) as { id: string; invoice_number: string }[]) { + byNumber.set(row.invoice_number, row.id); + } + } + for (let k = 0; k < externalIds.length; k += 500) { + const slice = externalIds.slice(k, k + 500); + const { data } = await supabase + .from("invoices") + .select("id, external_id") + .in("external_id", slice); + for (const row of (data ?? []) as { id: string; external_id: string }[]) { + byExt.set(row.external_id, row.id); + } + } + + const HEADER_FIELDS = [ + "status", "issue_date", "due_date", "notes", + "client_id", "case_id", "external_id", "invoice_number", + ] as const; + const toUpdateInv: Array<{ id: string; payload: Row }> = []; + const toInsertInv: Row[] = []; + for (const r of mapped) { + const num = r.invoice_number ? String(r.invoice_number) : ""; + const ext = r.external_id ? String(r.external_id) : ""; + const matchId = (num && byNumber.get(num)) || (ext && byExt.get(ext)) || null; + if (matchId) { + const payload: Row = {}; + for (const f of HEADER_FIELDS) { + if (r[f] != null && r[f] !== "") payload[f] = r[f]; + } + toUpdateInv.push({ id: matchId, payload }); + } else { + toInsertInv.push(r); + } + } + + for (let k = 0; k < toUpdateInv.length; k += 50) { + const slice = toUpdateInv.slice(k, k + 50); + const results = await Promise.all( + slice.map(({ id, payload }) => + supabase.from("invoices").update(payload as any).eq("id", id).select("*").single(), + ), + ); + for (const result of results) { + if (result.error) { + errors.push(`Invoice merge update: ${result.error.message}`); + } else if (result.data) { + inserted += 1; + inserted_rows.push(result.data as Row); + } + } + } + + for (let k = 0; k < toInsertInv.length; k += chunkSize) { + const slice = toInsertInv.slice(k, k + chunkSize); + const { data, error } = await supabase.from("invoices").insert(slice as any).select("*"); + if (error) errors.push(`Invoice merge insert: ${error.message}`); + else if (data) { + inserted += data.length; + inserted_rows.push(...(data as Row[])); + } + } + + // Skip the generic chunk loop below for invoices. + mapped.length = 0; + } + for (let i = 0; i < mapped.length; i += chunkSize) { const chunk = mapped.slice(i, i + chunkSize); // Filter out rows missing the conflict column for upsert; insert plain