Filtered active cases in importer
X-Lovable-Edit-ID: edt-25579b08-b963-494e-a173-efa68d3076a8 Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -58,6 +58,8 @@ interface ImportCtx {
|
||||
caseByTitle: Map<string, string>;
|
||||
userByName: Map<string, string>;
|
||||
userByEmail: Map<string, string>;
|
||||
// Set of case ids considered "active" (not closed, not archived)
|
||||
activeCaseIds: Set<string>;
|
||||
// Optional fallback client id (e.g. for address-only locations CSV)
|
||||
defaultClientId?: string | null;
|
||||
// Selected contact group (for unified contacts importer)
|
||||
@@ -414,7 +416,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 with an Invoice number are linked to (or create) an invoice with that number; rows without an Invoice number stay unbilled.",
|
||||
description: "Expected columns: Date, Case, Time, Rate, Flat rate, Total, Description, User, Case Name, Invoice, Nonbillable. Only rows matching an ACTIVE (non-archived, non-closed) case by case name/number are imported. Rows referencing missing, archived, or closed cases are skipped silently with a count.",
|
||||
table: "time_entries",
|
||||
conflict: "external_id",
|
||||
required: ["case_id", "description", "hours"],
|
||||
@@ -444,8 +446,11 @@ const IMPORTERS: ImporterConfig[] = [
|
||||
(r._casenum && ctx.caseByNumber.get(String(r._casenum))) ||
|
||||
(r._casename && ctx.caseByTitle.get(String(r._casename).toLowerCase().trim())) ||
|
||||
null;
|
||||
// NOTE: do NOT delete _caseext/_casenum/_casename here — async step needs them.
|
||||
// NOTE: do NOT bail on missing case — async step will create one.
|
||||
// Only accept active (non-archived, non-closed) cases.
|
||||
if (r.case_id && !ctx.activeCaseIds.has(r.case_id)) {
|
||||
r.case_id = null;
|
||||
}
|
||||
// NOTE: do NOT delete _caseext/_casenum/_casename here — pre-insert step needs them for skip reporting.
|
||||
|
||||
// Resolve hours from "Time" (e.g. "1.5", "1:30", "01:30:00") if needed
|
||||
if (r.hours == null && r._time != null) {
|
||||
@@ -510,7 +515,7 @@ const IMPORTERS: ImporterConfig[] = [
|
||||
{
|
||||
key: "expenses",
|
||||
label: "Expenses → Expenses",
|
||||
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.",
|
||||
description: "Case-related expenses. Only rows matching an ACTIVE (non-archived, non-closed) case are imported. Rows referencing missing, archived, or closed cases are skipped silently with a count.",
|
||||
table: "expenses",
|
||||
conflict: "external_id",
|
||||
required: ["case_id", "description", "amount"],
|
||||
@@ -533,7 +538,11 @@ const IMPORTERS: ImporterConfig[] = [
|
||||
(r._casenum && ctx.caseByNumber.get(String(r._casenum))) ||
|
||||
(r._casename && ctx.caseByTitle.get(String(r._casename).toLowerCase().trim())) ||
|
||||
null;
|
||||
// Keep hints; async pre-insert step may create an archived case.
|
||||
// Only accept active (non-archived, non-closed) cases.
|
||||
if (r.case_id && !ctx.activeCaseIds.has(r.case_id)) {
|
||||
r.case_id = null;
|
||||
}
|
||||
// Keep hints; pre-insert step decides whether to skip.
|
||||
if (r.amount == null) return null;
|
||||
r.user_id = ctx.userId;
|
||||
return r;
|
||||
@@ -664,6 +673,7 @@ function ImportPage() {
|
||||
caseByTitle: new Map(),
|
||||
userByName: new Map(),
|
||||
userByEmail: new Map(),
|
||||
activeCaseIds: new Set(),
|
||||
defaultClientId: null,
|
||||
defaultContactType: null,
|
||||
}), [user?.id]);
|
||||
@@ -737,14 +747,19 @@ function ImportPage() {
|
||||
ctx.caseByExt.clear();
|
||||
ctx.caseByNumber.clear();
|
||||
ctx.caseByTitle.clear();
|
||||
const cases = await fetchAllPaginated<{ id: string; case_number: string | null; title: string | null; external_id: string | null }>(
|
||||
ctx.activeCaseIds.clear();
|
||||
const cases = await fetchAllPaginated<{ id: string; case_number: string | null; title: string | null; external_id: string | null; status: string | null; archived_at: string | null }>(
|
||||
"cases",
|
||||
"id, case_number, title, external_id",
|
||||
"id, case_number, title, external_id, status, archived_at",
|
||||
);
|
||||
const CLOSED_STATUSES = new Set(["closed", "closed_won", "closed_lost"]);
|
||||
for (const r of cases) {
|
||||
if (r.external_id) ctx.caseByExt.set(r.external_id, r.id);
|
||||
if (r.case_number) ctx.caseByNumber.set(r.case_number, r.id);
|
||||
if (r.title) ctx.caseByTitle.set(r.title.toLowerCase().trim(), r.id);
|
||||
if (!r.archived_at && !CLOSED_STATUSES.has(String(r.status ?? "").toLowerCase())) {
|
||||
ctx.activeCaseIds.add(r.id);
|
||||
}
|
||||
}
|
||||
ctx.userByName.clear();
|
||||
ctx.userByEmail.clear();
|
||||
@@ -1009,6 +1024,13 @@ function ImportPage() {
|
||||
let clientId: string | null = null;
|
||||
|
||||
if (!caseId) {
|
||||
// For time entries / expenses, never auto-create cases.
|
||||
// Skip silently and report a count by reason.
|
||||
if (cfg.key === "time_entries" || cfg.key === "expenses") {
|
||||
const hint = row._casename || row._casenum || row._caseext;
|
||||
bumpSkip(hint ? "no matching active case" : "missing case reference");
|
||||
continue;
|
||||
}
|
||||
// Build display strings from hints
|
||||
const extId = row._caseext ? String(row._caseext) : null;
|
||||
const caseNum = row._casenum ? String(row._casenum).trim() : null;
|
||||
|
||||
Reference in New Issue
Block a user