Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 16:51:53 +00:00
co-authored by renee-png
parent 2b513e6c05
commit 0e8cc4f2ae
+151 -2
View File
@@ -801,8 +801,15 @@ function ImportPage() {
}
}
// required check
const missing = cfg.required.find((k) => !transformed[k] && transformed[k] !== 0);
// required check (defer case_id for time/expense — async step may create one)
const deferCaseId =
(cfg.table === "time_entries" || cfg.table === "expenses") &&
!transformed.case_id &&
(transformed._caseext || transformed._casenum || transformed._casename);
const missing = cfg.required.find((k) => {
if (k === "case_id" && deferCaseId) return false;
return !transformed[k] && transformed[k] !== 0;
});
if (missing) { bumpSkip(`missing required: ${missing}`); continue; }
// attach created_by where not present
@@ -812,6 +819,148 @@ function ImportPage() {
mapped.push(transformed);
}
// Async pre-insert: for time/expense rows missing a case, auto-create an
// archived client + archived case from the row hints, then attach a
// placeholder voided invoice so the row imports as "already invoiced".
if (cfg.table === "time_entries" || cfg.table === "expenses") {
const placeholderInvoiceByClient = new Map<string, string>();
const ensureArchivedClient = async (name: string, extId?: string | null): Promise<string | null> => {
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 time/expense 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<string | null> => {
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 time/expense 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 ensurePreInvoicedInvoice = async (clientId: string, caseId: string): Promise<string | null> => {
const cached = placeholderInvoiceByClient.get(clientId);
if (cached) return cached;
const invNumber = `IMPORT-PREBILLED-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
const { data, error } = await supabase
.from("invoices")
.insert({
client_id: clientId,
case_id: caseId,
invoice_number: invNumber,
status: "void",
issue_date: new Date().toISOString().slice(0, 10),
subtotal: 0,
tax: 0,
total: 0,
notes: "Placeholder invoice for items imported as already invoiced.",
created_by: user.id,
} as any)
.select("id")
.single();
if (error || !data) {
errors.push(`Auto-create placeholder invoice: ${error?.message ?? "unknown"}`);
return null;
}
placeholderInvoiceByClient.set(clientId, data.id);
return data.id;
};
const resolved: Row[] = [];
const caseClientCache = new Map<string, string>(); // case_id -> client_id (for created cases)
for (const row of mapped) {
let caseId: string | null = row.case_id ?? null;
let clientId: string | null = null;
if (!caseId) {
// Build display strings from hints
const extId = row._caseext ? String(row._caseext) : null;
const caseNum = row._casenum ? String(row._casenum).trim() : null;
const caseName = row._casename ? String(row._casename).trim() : null;
const titleGuess = caseName || caseNum || extId || "Imported case";
const numberGuess = caseNum || extId || `IMPORTED-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
// Auto-create an "Imported" archived client to host the case
clientId = await ensureArchivedClient("Imported (no client)", "imported-no-client");
if (!clientId) { bumpSkip("auto-create client failed"); continue; }
caseId = await ensureArchivedCase(clientId, titleGuess, numberGuess, extId);
if (!caseId) { bumpSkip("auto-create case failed"); continue; }
caseClientCache.set(caseId, clientId);
}
// Clean hint columns before insert
delete row._caseext; delete row._casenum; delete row._casename;
row.case_id = caseId;
// Look up client_id for the case to attach a placeholder voided invoice
if (!clientId) {
clientId = caseClientCache.get(caseId) ?? null;
if (!clientId) {
const { data: caseRow } = await supabase
.from("cases")
.select("client_id")
.eq("id", caseId)
.maybeSingle();
clientId = (caseRow?.client_id as string | null) ?? null;
if (clientId) caseClientCache.set(caseId, clientId);
}
}
// Mark as already invoiced via placeholder voided invoice (per client)
if (clientId && !row.invoice_id) {
const invId = await ensurePreInvoicedInvoice(clientId, caseId);
if (invId) row.invoice_id = invId;
}
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;