Simplified invoice import logic
X-Lovable-Edit-ID: edt-9a448b52-1f99-4a20-bf52-278d34c082d7 Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -342,7 +342,7 @@ const IMPORTERS: ImporterConfig[] = [
|
||||
{
|
||||
key: "time_entries",
|
||||
label: "Time entries → Time",
|
||||
description: "Expected columns: Date, Case, Time, Rate, Flat rate, Total, Description, User, Case Name, Invoice, Nonbillable. Missing clients/cases are auto-created as archived; rows are marked already-invoiced.",
|
||||
description: "Expected columns: Date, Case, Time, Rate, Flat rate, Total, Description, User, Case Name, Invoice, Nonbillable. Missing clients/cases are auto-created as archived. Rows with an Invoice number are linked to (or create) an invoice with that number; rows without an Invoice number stay unbilled.",
|
||||
table: "time_entries",
|
||||
conflict: "external_id",
|
||||
required: ["case_id", "description", "hours"],
|
||||
@@ -425,8 +425,9 @@ const IMPORTERS: ImporterConfig[] = [
|
||||
r.user_id = uid || ctx.userId;
|
||||
delete r._username;
|
||||
|
||||
// Invoice number is informational — we don't link it (would require invoice lookup).
|
||||
delete r._invoice;
|
||||
// Invoice number — keep as hint; async pre-insert step will look up or
|
||||
// create an invoice with that number and link this row to it.
|
||||
// (do NOT delete _invoice here)
|
||||
|
||||
if (r.hours == null || r.hours <= 0) return null;
|
||||
if (!r.description) return null;
|
||||
@@ -437,7 +438,7 @@ const IMPORTERS: ImporterConfig[] = [
|
||||
{
|
||||
key: "expenses",
|
||||
label: "Expenses → Expenses",
|
||||
description: "Case-related expenses. Missing clients/cases are auto-created as archived; rows are marked already-invoiced.",
|
||||
description: "Case-related expenses. Missing clients/cases are auto-created as archived. Rows with an Invoice number are linked to (or create) an invoice with that number; rows without an Invoice number stay unbilled.",
|
||||
table: "expenses",
|
||||
conflict: "external_id",
|
||||
required: ["case_id", "description", "amount"],
|
||||
@@ -449,6 +450,7 @@ const IMPORTERS: ImporterConfig[] = [
|
||||
amount: "amount", cost: "amount",
|
||||
billable: "billable",
|
||||
date: "expense_date", expensedate: "expense_date",
|
||||
invoice: "_invoice", invoicenumber: "_invoice",
|
||||
},
|
||||
numeric: ["amount"],
|
||||
boolCols: ["billable"],
|
||||
@@ -823,7 +825,7 @@ function ImportPage() {
|
||||
// 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>();
|
||||
// Cache of (clientId,invoiceNumber) -> invoiceId is defined below
|
||||
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);
|
||||
@@ -882,31 +884,49 @@ function ImportPage() {
|
||||
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);
|
||||
// Look up an existing invoice by number for this client, or create a
|
||||
// new "sent" invoice with that exact number to bucket imported items.
|
||||
const invoiceByClientAndNumber = new Map<string, string>(); // `${clientId}::${number}` -> invoiceId
|
||||
const ensureNamedInvoice = async (
|
||||
clientId: string,
|
||||
caseId: string,
|
||||
invoiceNumber: string,
|
||||
): Promise<string | null> => {
|
||||
const cacheKey = `${clientId}::${invoiceNumber}`;
|
||||
const cached = invoiceByClientAndNumber.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
const invNumber = `IMPORT-PREBILLED-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
// First try existing invoice with this number for this client
|
||||
const { data: existing } = await supabase
|
||||
.from("invoices")
|
||||
.select("id")
|
||||
.eq("client_id", clientId)
|
||||
.eq("invoice_number", invoiceNumber)
|
||||
.maybeSingle();
|
||||
if (existing?.id) {
|
||||
invoiceByClientAndNumber.set(cacheKey, existing.id);
|
||||
return existing.id;
|
||||
}
|
||||
const { data, error } = await supabase
|
||||
.from("invoices")
|
||||
.insert({
|
||||
client_id: clientId,
|
||||
case_id: caseId,
|
||||
invoice_number: invNumber,
|
||||
status: "void",
|
||||
invoice_number: invoiceNumber,
|
||||
status: "sent",
|
||||
issue_date: new Date().toISOString().slice(0, 10),
|
||||
subtotal: 0,
|
||||
tax: 0,
|
||||
total: 0,
|
||||
notes: "Placeholder invoice for items imported as already invoiced.",
|
||||
notes: "Auto-created from import to hold items linked to this invoice number.",
|
||||
created_by: user.id,
|
||||
} as any)
|
||||
.select("id")
|
||||
.single();
|
||||
if (error || !data) {
|
||||
errors.push(`Auto-create placeholder invoice: ${error?.message ?? "unknown"}`);
|
||||
errors.push(`Auto-create invoice "${invoiceNumber}": ${error?.message ?? "unknown"}`);
|
||||
return null;
|
||||
}
|
||||
placeholderInvoiceByClient.set(clientId, data.id);
|
||||
invoiceByClientAndNumber.set(cacheKey, data.id);
|
||||
return data.id;
|
||||
};
|
||||
|
||||
@@ -949,9 +969,13 @@ function ImportPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as already invoiced via placeholder voided invoice (per client)
|
||||
if (clientId && !row.invoice_id) {
|
||||
const invId = await ensurePreInvoicedInvoice(clientId, caseId);
|
||||
// Link to a real invoice ONLY when the source row provided an
|
||||
// invoice number. Otherwise leave invoice_id null so the user can
|
||||
// bill these items normally later.
|
||||
const invNumber = row._invoice ? String(row._invoice).trim() : "";
|
||||
delete row._invoice;
|
||||
if (invNumber && clientId && !row.invoice_id) {
|
||||
const invId = await ensureNamedInvoice(clientId, caseId, invNumber);
|
||||
if (invId) row.invoice_id = invId;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user