diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index ca77f3e..9b013e3 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -1206,24 +1206,56 @@ function ImportPage() { return data.id; }; + // Pre-load existing invoices by number so we can defer client resolution + // when the invoice already exists with a real client_id (don't clobber + // it with a placeholder). + const invNumbers = mapped.map((r) => String(r.invoice_number ?? "")).filter(Boolean); + const existingInvClient = new Map(); // invoice_number -> client_id + for (let k = 0; k < invNumbers.length; k += 500) { + const slice = invNumbers.slice(k, k + 500); + const { data } = await supabase + .from("invoices") + .select("invoice_number, client_id") + .in("invoice_number", slice); + for (const row of (data ?? []) as { invoice_number: string; client_id: string }[]) { + if (row.client_id) existingInvClient.set(row.invoice_number, row.client_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; } + // If this invoice already exists with a real client, inherit it + // instead of falling back to a placeholder — prevents wiping good + // client links during a re-import of the invoices CSV. + const invNum = row.invoice_number ? String(row.invoice_number) : ""; + const existingClient = invNum ? existingInvClient.get(invNum) : null; + if (existingClient) { + clientId = existingClient; + } else if (cName || cExt) { + // Only auto-create a placeholder when the CSV actually had a + // client hint we couldn't resolve. Pure-blank rows are left + // unset so the merge step skips client_id entirely. + const nameGuess = cName || cExt!; + clientId = await ensureArchivedClient(nameGuess, cExt); + if (!clientId) { bumpSkip("auto-create client failed"); continue; } + } else { + // No client info at all — leave client_id unset. The merge step + // will skip it (preserving any existing value); inserts will + // route through the catch-all placeholder below. + } } - row.client_id = clientId; + if (clientId) 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)) { + if (!caseId && clientId && (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); @@ -1340,6 +1372,30 @@ function ImportPage() { } } + // Inserts must have a non-null client_id; fall back to placeholder + // ONLY for genuinely new invoices that never matched anything. + const insertsNeedingClient = toInsertInv.filter((r) => !r.client_id); + if (insertsNeedingClient.length > 0) { + const { data: ph } = await supabase + .from("clients") + .upsert( + { + name: "Imported (no client)", + external_id: "imported-no-client", + client_type: "hoa", + archived_at: new Date().toISOString(), + notes: "Auto-created from invoice import (no matching client).", + created_by: user.id, + } as any, + { onConflict: "external_id" }, + ) + .select("id") + .single(); + const placeholderClient = (ph?.id as string | undefined) ?? null; + if (placeholderClient) { + for (const r of insertsNeedingClient) r.client_id = placeholderClient; + } + } 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("*");