From 3e51ed14a79f93fc6d8dd301995feb5709fde146 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:01:08 +0000 Subject: [PATCH 1/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 96 ++++++++-------------------------- 1 file changed, 21 insertions(+), 75 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 00d1060..1949ed0 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -92,97 +92,43 @@ const toDateTime = (v: any): string | null => { }; const IMPORTERS: ImporterConfig[] = [ - // Companies → clients + // Unified Contacts importer (all groups). When group=client, also mirrors into clients table. { - key: "companies", - label: "Companies → Clients", - description: "HOAs and corporate clients.", - table: "clients", - conflict: "external_id", - required: ["name"], - aliases: { - id: "external_id", companyid: "external_id", externalid: "external_id", - name: "name", companyname: "name", company: "name", - type: "client_type", clienttype: "client_type", - addressline1: "address_line1", address1: "address_line1", address: "address_line1", - addressline2: "address_line2", address2: "address_line2", - city: "city", state: "state", - zip: "postal_code", zipcode: "postal_code", postalcode: "postal_code", - phone: "primary_contact_phone", primaryphone: "primary_contact_phone", - email: "primary_contact_email", primaryemail: "primary_contact_email", - contactname: "primary_contact_name", primarycontact: "primary_contact_name", - management: "management_company", managementcompany: "management_company", - units: "num_units", numunits: "num_units", - notes: "notes", - archived: "_archived", isarchived: "_archived", status: "_archived", - }, - numeric: ["num_units", "annual_interest_rate"], - transform: (r) => { - const t = String(r.client_type ?? "").toLowerCase(); - r.client_type = t.includes("condo") ? "condo" : t.includes("hoa") ? "hoa" : "hoa"; - return r; - }, - postInsert: (rows, ctx) => { - for (const r of rows) { - if (r.external_id && r.id) ctx.clientByExt.set(r.external_id, r.id); - if (r.name && r.id) ctx.clientByName.set(r.name.toLowerCase(), r.id); - } - }, - }, - // People → contacts - { - key: "people", - label: "People → Contacts", - description: "Individual contacts (board members, vendors, etc.).", + key: "contacts", + label: "Contacts → Contacts", + description: "Upload one CSV per contact group. Rows tagged 'Client' also get mirrored into the Clients table.", table: "contacts", conflict: "external_id", required: ["name"], aliases: { - id: "external_id", personid: "external_id", externalid: "external_id", - name: "name", fullname: "name", + id: "external_id", contactid: "external_id", personid: "external_id", companyid: "external_id", externalid: "external_id", + name: "name", fullname: "name", companyname: "name", firstname: "_first", lastname: "_last", - email: "email", phone: "phone", - company: "company", title: "title", - addressline1: "address_line1", address1: "address_line1", address: "address_line1", - addressline2: "address_line2", address2: "address_line2", - city: "city", state: "state", + email: "email", emailaddress: "email", + phone: "phone", phonenumber: "phone", mobile: "phone", + company: "company", firm: "company", organization: "company", + title: "title", role: "title", + addressline1: "address_line1", address1: "address_line1", address: "address_line1", street: "address_line1", + addressline2: "address_line2", address2: "address_line2", suite: "address_line2", apt: "address_line2", + city: "city", state: "state", province: "state", zip: "postal_code", zipcode: "postal_code", postalcode: "postal_code", type: "contact_type", contacttype: "contact_type", - notes: "notes", + notes: "notes", comments: "notes", archived: "_archived", isarchived: "_archived", }, - transform: (r) => { + transform: (r, ctx) => { if (!r.name && (r._first || r._last)) { r.name = [r._first, r._last].filter(Boolean).join(" ").trim(); } delete r._first; delete r._last; - if (!r.contact_type) r.contact_type = "other"; + // Force the group selected by the user (overrides any per-row contact_type column) + r.contact_type = ctx.defaultContactType || r.contact_type || "other"; return r; }, - }, - // Lawyers → contacts (type=attorney) - { - key: "lawyers", - label: "Lawyers → Contacts", - description: "Opposing counsel & external attorneys (added as contacts).", - table: "contacts", - conflict: "external_id", - required: ["name"], - aliases: { - id: "external_id", lawyerid: "external_id", externalid: "external_id", - name: "name", fullname: "name", - firstname: "_first", lastname: "_last", - email: "email", phone: "phone", - firm: "company", company: "company", - addressline1: "address_line1", address: "address_line1", - city: "city", state: "state", zip: "postal_code", postalcode: "postal_code", - notes: "notes", - }, - transform: (r) => { - if (!r.name && (r._first || r._last)) r.name = [r._first, r._last].filter(Boolean).join(" ").trim(); - delete r._first; delete r._last; - r.contact_type = "attorney"; - return r; + postInsert: (rows, ctx) => { + for (const r of rows) { + if (r.external_id && r.id) ctx.contactByExt.set(r.external_id, r.id); + } }, }, // Locations → homeowners (units) From 593eac9e7db054803fa7e0ead8d0c75fbf05f05e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:01:20 +0000 Subject: [PATCH 2/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 1949ed0..32045c1 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -54,6 +54,8 @@ interface ImportCtx { caseByNumber: Map; // Optional fallback client id (e.g. for address-only locations CSV) defaultClientId?: string | null; + // Selected contact group (for unified contacts importer) + defaultContactType?: string | null; } const norm = (s: string) => From a8799189d36f10ed67ee533ce80efed0a914fcc4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:01:35 +0000 Subject: [PATCH 3/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 32045c1..f6a48fa 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -10,6 +10,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Upload, CheckCircle2, AlertCircle, Loader2, FileSpreadsheet } from "lucide-react"; import { toast } from "sonner"; +import { CONTACT_TYPES } from "@/components/contacts/contact-form-dialog"; export const Route = createFileRoute("/settings/import")({ component: ImportPage, From a5b9f2aa4dcc1cc8f16410ef64e8506155efae2f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:01:44 +0000 Subject: [PATCH 4/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index f6a48fa..3ff288d 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -467,6 +467,7 @@ function ImportPage() { const [results, setResults] = useState>({}); const [clientList, setClientList] = useState<{ id: string; name: string }[]>([]); const [defaultClientId, setDefaultClientId] = useState(""); + const [contactGroup, setContactGroup] = useState("client"); const ctx = useMemo(() => ({ userId: user?.id ?? "", clientByExt: new Map(), @@ -477,6 +478,7 @@ function ImportPage() { clientByName: new Map(), caseByNumber: new Map(), defaultClientId: null, + defaultContactType: null, }), [user?.id]); useEffect(() => { From 4b02eaea9ce4ff9ba33413cdb87c8fd6409723be Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:01:53 +0000 Subject: [PATCH 5/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 3ff288d..3045073 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -540,6 +540,7 @@ function ImportPage() { if (!user?.id) return; setResults((r) => ({ ...r, [cfg.key]: "running" })); ctx.defaultClientId = cfg.key === "locations" ? (defaultClientId || null) : null; + ctx.defaultContactType = cfg.key === "contacts" ? (contactGroup || "other") : null; await ensureLookupsLoaded(); const parsed = await new Promise>>((resolve, reject) => { From 3babc82cba7151cf41bccb8127772619a8a64b54 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:02:09 +0000 Subject: [PATCH 6/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 43 ++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 3045073..16b271c 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -685,10 +685,49 @@ function ImportPage() { if (cfg.postInsert) cfg.postInsert(inserted_rows, ctx); + // Mirror imported "client" contacts into the clients table so they appear on the Clients page. + let mirrored = 0; + if (cfg.key === "contacts" && contactGroup === "client" && inserted_rows.length > 0) { + const clientRows = inserted_rows + .filter((c) => c.external_id || c.name) + .map((c) => ({ + external_id: c.external_id ?? null, + name: c.name || c.company || "Unnamed client", + client_type: "hoa" as const, + primary_contact_email: c.email ?? null, + primary_contact_phone: c.phone ?? null, + primary_contact_name: c.name ?? null, + address_line1: c.address_line1 ?? null, + address_line2: c.address_line2 ?? null, + city: c.city ?? null, + state: c.state ?? null, + postal_code: c.postal_code ?? null, + notes: c.notes ?? null, + created_by: user.id, + })); + const withExt = clientRows.filter((r) => r.external_id); + const withoutExt = clientRows.filter((r) => !r.external_id); + if (withExt.length > 0) { + const { data, error } = await supabase + .from("clients") + .upsert(withExt, { onConflict: "external_id", ignoreDuplicates: false }) + .select("id"); + if (error) errors.push(`Client mirror (upsert): ${error.message}`); + else mirrored += data?.length ?? 0; + } + if (withoutExt.length > 0) { + const { data, error } = await supabase.from("clients").insert(withoutExt).select("id"); + if (error) errors.push(`Client mirror (insert): ${error.message}`); + else mirrored += data?.length ?? 0; + } + } + const summary = { total: rawRows.length, inserted, skipped, errors, skipReasons }; setResults((r) => ({ ...r, [cfg.key]: summary })); - if (errors.length === 0) toast.success(`${cfg.label}: ${inserted} imported`); - else toast.error(`${cfg.label}: ${errors.length} batch error(s)`); + if (errors.length === 0) { + const extra = mirrored > 0 ? ` (+${mirrored} mirrored to Clients)` : ""; + toast.success(`${cfg.label}: ${inserted} imported${extra}`); + } else toast.error(`${cfg.label}: ${errors.length} batch error(s)`); }; return ( From b5c98a29dbdca2ed57adb075ce78e5bb2b271a22 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:02:20 +0000 Subject: [PATCH 7/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 16b271c..1e110f2 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -736,8 +736,8 @@ function ImportPage() { Headers are auto-mapped (snake_case + common aliases). Existing rows with a matching{" "} - external_id are updated. Import in order: - companies → people/lawyers → locations → cases → tasks/events/calls/time/expenses/invoices/notes/status. + external_id are updated. Pick a contact group per + upload — rows tagged Client are also mirrored into the Clients page. From e6716dd24d461f59561412c930cfb200a7764267 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:02:35 +0000 Subject: [PATCH 8/8] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 1e110f2..f756e1a 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -758,6 +758,18 @@ function ImportPage() {

{cfg.description}

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