From e56099780ea319363113b2307d6d70d64d217469 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:08:31 +0000 Subject: [PATCH 01/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 36 +++++++++++++++++++ ...8_bf9aa428-7c1f-410d-ab4a-9b2ce8c81254.sql | 24 +++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 supabase/migrations/20260419010828_bf9aa428-7c1f-410d-ab4a-9b2ce8c81254.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index cf66644..aa50265 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1771,6 +1771,42 @@ export type Database = { } Relationships: [] } + import_runs: { + Row: { + created_at: string + error_count: number + id: string + importer_key: string + inserted: number + ran_at: string + ran_by: string | null + skipped: number + total: number + } + Insert: { + created_at?: string + error_count?: number + id?: string + importer_key: string + inserted?: number + ran_at?: string + ran_by?: string | null + skipped?: number + total?: number + } + Update: { + created_at?: string + error_count?: number + id?: string + importer_key?: string + inserted?: number + ran_at?: string + ran_by?: string | null + skipped?: number + total?: number + } + Relationships: [] + } incoming_emails: { Row: { attachment_count: number diff --git a/supabase/migrations/20260419010828_bf9aa428-7c1f-410d-ab4a-9b2ce8c81254.sql b/supabase/migrations/20260419010828_bf9aa428-7c1f-410d-ab4a-9b2ce8c81254.sql new file mode 100644 index 0000000..91af866 --- /dev/null +++ b/supabase/migrations/20260419010828_bf9aa428-7c1f-410d-ab4a-9b2ce8c81254.sql @@ -0,0 +1,24 @@ +CREATE TABLE public.import_runs ( + id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + importer_key text NOT NULL, + total integer NOT NULL DEFAULT 0, + inserted integer NOT NULL DEFAULT 0, + skipped integer NOT NULL DEFAULT 0, + error_count integer NOT NULL DEFAULT 0, + ran_by uuid, + ran_at timestamptz NOT NULL DEFAULT now(), + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX idx_import_runs_key_ran_at ON public.import_runs (importer_key, ran_at DESC); + +ALTER TABLE public.import_runs ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "import_runs_select_auth" ON public.import_runs + FOR SELECT TO authenticated USING (true); + +CREATE POLICY "import_runs_insert_auth" ON public.import_runs + FOR INSERT TO authenticated WITH CHECK (auth.uid() IS NOT NULL); + +CREATE POLICY "import_runs_delete_admin" ON public.import_runs + FOR DELETE TO authenticated USING (public.is_admin(auth.uid())); \ No newline at end of file From a5303d04e688bd1290e1569a174f142d13c72229 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:08:53 +0000 Subject: [PATCH 02/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 65a7f57..ee0c2ba 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -280,14 +280,19 @@ const IMPORTERS: ImporterConfig[] = [ allday: "all_day", type: "event_type", eventtype: "event_type", caseid: "_caseext", matterid: "_caseext", casenumber: "_casenum", + casename: "_casename", casetitle: "_casename", matter: "_casename", mattername: "_casename", companyid: "_clientext", clientid: "_clientext", }, boolCols: ["all_day"], datetimeCols: ["start_at", "end_at"], transform: (r, ctx) => { - r.case_id = (r._caseext && ctx.caseByExt.get(String(r._caseext))) || (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || null; + r.case_id = + (r._caseext && ctx.caseByExt.get(String(r._caseext))) || + (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || + (r._casename && ctx.caseByTitle.get(String(r._casename).toLowerCase().trim())) || + null; r.client_id = (r._clientext && ctx.clientByExt.get(String(r._clientext))) || null; - delete r._caseext; delete r._casenum; delete r._clientext; + delete r._caseext; delete r._casenum; delete r._casename; delete r._clientext; if (!r.event_type) r.event_type = "general"; return r; }, From ae4c8f473dc9065b1516b4a95d72db98d2fd3415 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:09:06 +0000 Subject: [PATCH 03/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index ee0c2ba..cab23e5 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -308,6 +308,7 @@ const IMPORTERS: ImporterConfig[] = [ aliases: { id: "external_id", callid: "external_id", externalid: "external_id", caseid: "_caseext", matterid: "_caseext", casenumber: "_casenum", + casename: "_casename", casetitle: "_casename", matter: "_casename", mattername: "_casename", direction: "direction", subject: "subject", topic: "subject", notes: "notes", description: "notes", callername: "caller_name", from: "caller_name", @@ -320,8 +321,12 @@ const IMPORTERS: ImporterConfig[] = [ boolCols: ["billable"], datetimeCols: ["call_date"], transform: (r, ctx) => { - r.case_id = (r._caseext && ctx.caseByExt.get(String(r._caseext))) || (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || null; - delete r._caseext; delete r._casenum; + r.case_id = + (r._caseext && ctx.caseByExt.get(String(r._caseext))) || + (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || + (r._casename && ctx.caseByTitle.get(String(r._casename).toLowerCase().trim())) || + null; + delete r._caseext; delete r._casenum; delete r._casename; if (!r.case_id) return null; const d = String(r.direction ?? "").toLowerCase(); r.direction = d.includes("in") ? "inbound" : "outbound"; From 1ca61787f88b5405cd57efe85eeea156dca72ab4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:09:20 +0000 Subject: [PATCH 04/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index cab23e5..4c156c3 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -502,10 +502,15 @@ const IMPORTERS: ImporterConfig[] = [ id: "external_id", noteid: "external_id", externalid: "external_id", body: "body", content: "body", text: "body", notes: "body", caseid: "_caseext", matterid: "_caseext", casenumber: "_casenum", + casename: "_casename", casetitle: "_casename", matter: "_casename", mattername: "_casename", }, transform: (r, ctx) => { - r.case_id = (r._caseext && ctx.caseByExt.get(String(r._caseext))) || (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || null; - delete r._caseext; delete r._casenum; + r.case_id = + (r._caseext && ctx.caseByExt.get(String(r._caseext))) || + (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || + (r._casename && ctx.caseByTitle.get(String(r._casename).toLowerCase().trim())) || + null; + delete r._caseext; delete r._casenum; delete r._casename; if (!r.case_id || !r.body) return null; r.author_id = ctx.userId; r.entity_type = "case"; From fc5cab6a561df5b9058b9afe4f852d020de3233f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:09:31 +0000 Subject: [PATCH 05/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 4c156c3..b9d5296 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -531,10 +531,15 @@ const IMPORTERS: ImporterConfig[] = [ title: "title", subject: "title", body: "body", content: "body", notes: "body", description: "body", caseid: "_caseext", matterid: "_caseext", casenumber: "_casenum", + casename: "_casename", casetitle: "_casename", matter: "_casename", mattername: "_casename", }, transform: (r, ctx) => { - r.case_id = (r._caseext && ctx.caseByExt.get(String(r._caseext))) || (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || null; - delete r._caseext; delete r._casenum; + r.case_id = + (r._caseext && ctx.caseByExt.get(String(r._caseext))) || + (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || + (r._casename && ctx.caseByTitle.get(String(r._casename).toLowerCase().trim())) || + null; + delete r._caseext; delete r._casenum; delete r._casename; if (!r.case_id || !r.body) return null; return r; }, From b2633ce214bb7457462102847d72f77592bdfdd9 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:09:49 +0000 Subject: [PATCH 06/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index b9d5296..eaca28b 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -248,13 +248,16 @@ const IMPORTERS: ImporterConfig[] = [ duedate: "due_date", due: "due_date", priority: "priority", status: "status", caseid: "_caseext", matterid: "_caseext", casenumber: "_casenum", + casename: "_casename", casetitle: "_casename", matter: "_casename", mattername: "_casename", }, dateCols: ["due_date"], transform: (r, ctx) => { - const cid = r._caseext ? ctx.caseByExt.get(String(r._caseext)) : null; - const cidByNum = r._casenum ? ctx.caseByNumber.get(String(r._casenum)) : null; - r.case_id = cid ?? cidByNum ?? null; - delete r._caseext; delete r._casenum; + r.case_id = + (r._caseext && ctx.caseByExt.get(String(r._caseext))) || + (r._casenum && ctx.caseByNumber.get(String(r._casenum))) || + (r._casename && ctx.caseByTitle.get(String(r._casename).toLowerCase().trim())) || + null; + delete r._caseext; delete r._casenum; delete r._casename; const s = String(r.status ?? "").toLowerCase(); r.status = s.includes("complete") || s.includes("done") ? "complete" : "incomplete"; const p = String(r.priority ?? "").toLowerCase(); From f348121e6129dc6129467deb76bf16459ef4e8a6 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:10:15 +0000 Subject: [PATCH 07/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index eaca28b..2ad5187 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -716,6 +716,10 @@ function ImportPage() { // transform const transformed = cfg.transform ? cfg.transform(row, ctx) : row; if (!transformed) { + // Tables whose rows are linked to a case via _caseext / _casenum / _casename + const caseLinked = ["tasks", "events", "call_logs", "time_entries", "expenses", "comments", "status_updates"]; + const hasCaseHint = row._caseext != null || row._casenum != null || row._casename != null; + // Diagnose common transform-rejection reasons for clearer feedback if (cfg.table === "homeowners" && !row.client_id && !row._clientext && !row._clientname) { bumpSkip("no client match (missing companyid/company)"); @@ -725,20 +729,16 @@ function ImportPage() { bumpSkip("client not found"); } else if (cfg.table === "time_entries" && !row.work_date) { bumpSkip("missing or invalid work date"); - } else if (cfg.table === "time_entries" && !row.case_id && !row._caseext && !row._casenum && !row._casename) { - bumpSkip("missing case reference"); - } else if (cfg.table === "time_entries" && (row._caseext || row._casenum || row._casename) && !row.case_id) { - bumpSkip("case not found (import cases first or match case title/number)"); } else if (cfg.table === "time_entries" && (row.hours == null || row.hours <= 0) && row._time == null) { bumpSkip("missing or invalid hours"); } else if (cfg.table === "time_entries" && !row.description && !row._activity) { bumpSkip("missing description/activity"); - } else if (cfg.table === "expenses" && !row.case_id && !row._caseext && !row._casenum && !row._casename) { - bumpSkip("missing case reference"); - } else if (cfg.table === "expenses" && (row._caseext || row._casenum || row._casename) && !row.case_id) { - bumpSkip("case not found (import cases first or match case title/number)"); } else if (cfg.table === "expenses" && row.amount == null) { bumpSkip("missing or invalid amount"); + } else if (caseLinked.includes(cfg.table) && !row.case_id && !hasCaseHint) { + bumpSkip("missing case reference"); + } else if (caseLinked.includes(cfg.table) && hasCaseHint && !row.case_id) { + bumpSkip("case not found (import cases first or match case title/number)"); } else { bumpSkip("rejected by transform"); } From b2ffbba1bce6eefde47e98ac4aa9eeadc8875b19 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:10:39 +0000 Subject: [PATCH 08/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 2ad5187..ae0a30e 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -557,9 +557,18 @@ interface ImportResult { skipReasons?: Record; } +interface LastRun { + ran_at: string; + total: number; + inserted: number; + skipped: number; + error_count: number; +} + function ImportPage() { const { user } = useAuth(); const [results, setResults] = useState>({}); + const [lastRuns, setLastRuns] = useState>({}); const [clientList, setClientList] = useState<{ id: string; name: string }[]>([]); const [defaultClientId, setDefaultClientId] = useState(""); const [contactGroup, setContactGroup] = useState("client"); From 62310e462f569df691226008829f08473593188d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:10:58 +0000 Subject: [PATCH 09/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index ae0a30e..a9c64a5 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -597,6 +597,30 @@ function ImportPage() { .then(({ data }) => setClientList((data ?? []) as { id: string; name: string }[])); }, []); + // Load latest import per category + useEffect(() => { + void (async () => { + const { data, error } = await supabase + .from("import_runs" as any) + .select("importer_key, ran_at, total, inserted, skipped, error_count") + .order("ran_at", { ascending: false }); + if (error || !data) return; + const map: Record = {}; + for (const row of data as any[]) { + if (!map[row.importer_key]) { + map[row.importer_key] = { + ran_at: row.ran_at, + total: row.total, + inserted: row.inserted, + skipped: row.skipped, + error_count: row.error_count, + }; + } + } + setLastRuns(map); + })(); + }, []); + // Pre-load lookup caches from existing rows so subsequent imports can join. // Always reload (and paginate past Supabase's 1000-row default limit) so caches // pick up rows added since the page mounted (e.g., from a prior CSV import). From 1afa2d67c010a43e96545154628ba5af78f76963 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:11:11 +0000 Subject: [PATCH 10/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index a9c64a5..31b4de7 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -930,6 +930,33 @@ function ImportPage() { const summary = { total: rawRows.length, inserted, skipped, errors, skipReasons }; setResults((r) => ({ ...r, [cfg.key]: summary })); + + // Persist run history (best-effort) + const runRow = { + importer_key: cfg.key, + total: rawRows.length, + inserted, + skipped, + error_count: errors.length, + ran_by: user.id, + }; + void supabase.from("import_runs" as any).insert(runRow as any).then(({ error: runErr }) => { + if (runErr) { + console.warn("[import] failed to record run", runErr); + return; + } + setLastRuns((prev) => ({ + ...prev, + [cfg.key]: { + ran_at: new Date().toISOString(), + total: rawRows.length, + inserted, + skipped, + error_count: errors.length, + }, + })); + }); + if (errors.length === 0) { const extra = mirrored > 0 ? ` (+${mirrored} mirrored to Clients)` : ""; toast.success(`${cfg.label}: ${inserted} imported${extra}`); From e7f1c4c96974c74447fd6eb405740409aa560a12 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:11:24 +0000 Subject: [PATCH 11/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 31b4de7..9ad0ff9 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -989,6 +989,15 @@ function ImportPage() { {cfg.label}

{cfg.description}

+ {!r && lastRuns[cfg.key] && ( +

+ Last import:{" "} + {new Date(lastRuns[cfg.key].ran_at).toLocaleString()} —{" "} + {lastRuns[cfg.key].inserted} of {lastRuns[cfg.key].total} imported + {lastRuns[cfg.key].skipped > 0 ? `, ${lastRuns[cfg.key].skipped} skipped` : ""} + {lastRuns[cfg.key].error_count > 0 ? `, ${lastRuns[cfg.key].error_count} error(s)` : ""} +

+ )}
{cfg.key === "contacts" && (