From 4b67b5123bb5754876ccf050a68c5c2124fdd2fb 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 21:29:28 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 5f8b29d..6ec3acb 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -1085,6 +1085,101 @@ function ImportPage() { mapped.push(...resolved); } + // Async pre-insert for invoices: if the row references an unknown client, + // auto-create an archived client from the row hints. If it also references + // an unknown case, auto-create an archived case under that client so the + // case is recorded (but clearly archived) rather than silently dropped. + if (cfg.table === "invoices") { + const ensureArchivedClient = async (name: string, extId?: string | null): Promise => { + const key = name.toLowerCase().trim(); + const existing = (extId && ctx.clientByExt.get(String(extId))) || ctx.clientByName.get(key); + if (existing) return existing; + const { data, error } = await supabase + .from("clients") + .insert({ + name, + external_id: extId || null, + client_type: "hoa", + archived_at: new Date().toISOString(), + notes: "Auto-created from invoice import (no matching client).", + created_by: user.id, + } as any) + .select("id") + .single(); + if (error || !data) { + errors.push(`Auto-create client "${name}": ${error?.message ?? "unknown"}`); + return null; + } + ctx.clientByName.set(key, data.id); + if (extId) ctx.clientByExt.set(String(extId), data.id); + return data.id; + }; + const ensureArchivedCase = async ( + clientId: string, + title: string, + caseNumber: string, + extId?: string | null, + ): Promise => { + const existing = + (extId && ctx.caseByExt.get(String(extId))) || + ctx.caseByNumber.get(caseNumber) || + ctx.caseByTitle.get(title.toLowerCase().trim()); + if (existing) return existing; + const { data, error } = await supabase + .from("cases") + .insert({ + client_id: clientId, + case_number: caseNumber, + title, + external_id: extId || null, + status: "closed", + archived_at: new Date().toISOString(), + description: "Auto-created from invoice import (no matching case).", + created_by: user.id, + } as any) + .select("id") + .single(); + if (error || !data) { + errors.push(`Auto-create case "${title}": ${error?.message ?? "unknown"}`); + return null; + } + ctx.caseByNumber.set(caseNumber, data.id); + ctx.caseByTitle.set(title.toLowerCase().trim(), data.id); + if (extId) ctx.caseByExt.set(String(extId), data.id); + return data.id; + }; + + const resolved: Row[] = []; + for (const row of mapped) { + let clientId: string | null = (row.client_id as string | null) ?? null; + if (!clientId) { + const cExt = row._clientext ? String(row._clientext).trim() : null; + const cName = row._clientname ? String(row._clientname).trim() : null; + const nameGuess = cName || cExt || "Imported (no client)"; + clientId = await ensureArchivedClient(nameGuess, cExt); + if (!clientId) { bumpSkip("auto-create client failed"); continue; } + } + row.client_id = clientId; + + // If the invoice mentions a case but we don't have it, create an + // archived placeholder case so the reference is preserved. + let caseId: string | null = (row.case_id as string | null) ?? null; + const caseExt = row._caseext ? String(row._caseext).trim() : null; + const caseNum = row._casenum ? String(row._casenum).trim() : null; + if (!caseId && (caseExt || caseNum)) { + const titleGuess = caseNum || caseExt || "Imported case"; + const numberGuess = caseNum || caseExt || `IMPORTED-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + caseId = await ensureArchivedCase(clientId, titleGuess, numberGuess, caseExt); + } + if (caseId) row.case_id = caseId; + + // Clean hint columns + delete row._clientext; delete row._clientname; delete row._caseext; delete row._casenum; + resolved.push(row); + } + mapped.length = 0; + mapped.push(...resolved); + } if (mapped.length === 0) { setResults((r) => ({ ...r, [cfg.key]: { total: rawRows.length, inserted: 0, skipped, errors: ["No valid rows after mapping. Check headers/required fields."], skipReasons } })); return;