Skipped inactive invoices

X-Lovable-Edit-ID: edt-b99d3a94-2b52-43d3-90b0-ba7b5ec90f15
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 23:13:58 +00:00
co-authored by renee-png
+32 -25
View File
@@ -569,7 +569,7 @@ const IMPORTERS: ImporterConfig[] = [
{
key: "invoices",
label: "Invoices → Invoices",
description: "Invoice headers. Matched against existing invoices by invoice number (preferred) or external ID — when a match exists, only header fields (status, dates, notes) are updated and totals are left intact (totals = sum of attached time/expense line items). Non-matching invoices are inserted as new. Import time entries and expenses FIRST so their invoices are auto-created as drafts; this CSV then promotes them to sent/paid/etc.",
description: "Invoice headers. Matched against existing invoices by invoice number (preferred) or external ID — when a match exists, only header fields (status, dates, notes) are updated and totals are left intact (totals = sum of attached time/expense line items). Non-matching invoices are inserted as new. Rows that don't link to an ACTIVE case or client are SKIPPED (no placeholder client is created). Import time entries and expenses FIRST so their invoices are auto-created as drafts; this CSV then promotes them to sent/paid/etc.",
table: "invoices",
conflict: "external_id",
// Only invoice_number is strictly required at the row level. client_id is
@@ -598,6 +598,13 @@ const IMPORTERS: ImporterConfig[] = [
transform: (r, ctx) => {
r.client_id = (r._clientext && ctx.clientByExt.get(String(r._clientext))) || (r._clientname && ctx.clientByName.get(String(r._clientname).toLowerCase())) || null;
r.case_id = (r._caseext && ctx.caseByExt.get(String(r._caseext))) || (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || null;
// Only accept ACTIVE (non-archived, non-closed) cases. Drop inactive case refs.
if (r.case_id && !ctx.activeCaseIds.has(r.case_id)) {
r.case_id = null;
}
// Skip rows that don't link to either an active case or a real client.
// No placeholder client is created — these invoices are dropped.
if (!r.client_id && !r.case_id) return null;
// Auto-generate invoice number if missing (uses external_id or timestamp)
if (!r.invoice_number || !String(r.invoice_number).trim()) {
r.invoice_number = r.external_id
@@ -1372,32 +1379,32 @@ 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;
// Skip new invoices without a real client. If only case_id is present,
// derive client_id from the case. Rows that still have no client are
// dropped (no placeholder is created).
const needClient = toInsertInv.filter((r) => !r.client_id && r.case_id);
if (needClient.length > 0) {
const caseIds = Array.from(new Set(needClient.map((r) => String(r.case_id))));
const { data: caseRows } = await supabase
.from("cases")
.select("id, client_id")
.in("id", caseIds);
const clientByCase = new Map<string, string>();
for (const c of (caseRows ?? []) as { id: string; client_id: string | null }[]) {
if (c.client_id) clientByCase.set(c.id, c.client_id);
}
for (const r of needClient) {
const cid = clientByCase.get(String(r.case_id));
if (cid) r.client_id = cid;
}
}
for (let k = 0; k < toInsertInv.length; k += chunkSize) {
const slice = toInsertInv.slice(k, k + chunkSize);
const droppedNoClient = toInsertInv.filter((r) => !r.client_id).length;
if (droppedNoClient > 0) {
bumpSkip("no active case/client");
}
const insertable = toInsertInv.filter((r) => !!r.client_id);
for (let k = 0; k < insertable.length; k += chunkSize) {
const slice = insertable.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) {