From a36aca80d09349fff4020273acd75276715d53e4 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 07:20:53 +0000 Subject: [PATCH 1/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index f756e1a..83a8503 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -53,6 +53,9 @@ interface ImportCtx { // Name → uuid lookups (for files that reference by name only) clientByName: Map; caseByNumber: Map; + caseByTitle: Map; + userByName: Map; + userByEmail: Map; // Optional fallback client id (e.g. for address-only locations CSV) defaultClientId?: string | null; // Selected contact group (for unified contacts importer) From c7ecc750e165ec78920bbfe18d1cb84366d5270e 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 07:21:23 +0000 Subject: [PATCH 2/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 82 ++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 83a8503..524a260 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -327,28 +327,92 @@ const IMPORTERS: ImporterConfig[] = [ { key: "time_entries", label: "Time entries → Time", - description: "Billable time records.", + description: "Billable time records. Supports MyCase headers: Date, Activity, Time, Rate, Flat rate, Total, Description, User, Case Name, Invoice, Nonbillable, MyCase ID.", table: "time_entries", conflict: "external_id", required: ["case_id", "description", "hours"], aliases: { - id: "external_id", timeid: "external_id", externalid: "external_id", + id: "external_id", timeid: "external_id", externalid: "external_id", mycaseid: "external_id", caseid: "_caseext", matterid: "_caseext", casenumber: "_casenum", - description: "description", notes: "description", - hours: "hours", duration: "hours", + casename: "_casename", case: "_casename", matter: "_casename", mattername: "_casename", + description: "description", notes: "description", activity: "_activity", + hours: "hours", duration: "hours", time: "_time", rate: "hourly_rate", hourlyrate: "hourly_rate", - billable: "billable", + flatrate: "_flatrate", flatfee: "_flatrate", + total: "_total", amount: "_total", + billable: "billable", nonbillable: "_nonbillable", nonbill: "_nonbillable", date: "work_date", workdate: "work_date", + user: "_username", username: "_username", attorney: "_username", staff: "_username", timekeeper: "_username", + invoice: "_invoice", invoicenumber: "_invoice", }, numeric: ["hours", "hourly_rate"], boolCols: ["billable"], dateCols: ["work_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; + // Resolve case + 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; - r.user_id = ctx.userId; - if (r.hours == null) return null; + + // Resolve hours from "Time" (e.g. "1.5", "1:30", "01:30:00") if needed + if (r.hours == null && r._time != null) { + const s = String(r._time).trim(); + const colon = s.match(/^(\d+):(\d{1,2})(?::(\d{1,2}))?$/); + if (colon) { + const h = Number(colon[1]) + Number(colon[2]) / 60 + (colon[3] ? Number(colon[3]) / 3600 : 0); + r.hours = isNaN(h) ? null : h; + } else { + const n = toNum(s); + if (n != null) r.hours = n; + } + } + delete r._time; + + // Derive hourly_rate from total/flat rate when missing + const total = toNum(r._total); + const flat = toNum(r._flatrate); + if (r.hourly_rate == null && r.hours && total != null && r.hours > 0) { + r.hourly_rate = total / r.hours; + } + if (r.hourly_rate == null && flat != null && r.hours && r.hours > 0) { + r.hourly_rate = flat / r.hours; + } + if (r.hourly_rate == null) r.hourly_rate = 0; + delete r._total; delete r._flatrate; + + // Billable: explicit `nonbillable` truthy → false; otherwise default true + if (r._nonbillable != null) { + r.billable = !toBool(r._nonbillable); + } else if (r.billable == null) { + r.billable = true; + } + delete r._nonbillable; + + // Description: prefer Description, fall back to Activity + if (!r.description && r._activity) r.description = String(r._activity).trim(); + if (r.description && r._activity && !String(r.description).startsWith(String(r._activity))) { + // keep description as-is; activity is supplemental + } + delete r._activity; + + // User (timekeeper) lookup; fall back to current user so RLS insert succeeds + let uid: string | undefined; + if (r._username) { + const key = String(r._username).toLowerCase().trim(); + uid = ctx.userByName.get(key) || ctx.userByEmail.get(key); + } + 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; + + if (r.hours == null || r.hours <= 0) return null; + if (!r.description) return null; return r; }, }, From c41ff210a29656735f4dd171dafe9fd1df746c96 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 07:21:32 +0000 Subject: [PATCH 3/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 524a260..3e0ed67 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -544,6 +544,9 @@ function ImportPage() { invoiceByExt: new Map(), clientByName: new Map(), caseByNumber: new Map(), + caseByTitle: new Map(), + userByName: new Map(), + userByEmail: new Map(), defaultClientId: null, defaultContactType: null, }), [user?.id]); From 4dc14bf9bd27d999ee944ca8eb882bbc5e169626 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 07:21:46 +0000 Subject: [PATCH 4/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.import.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 3e0ed67..dde281a 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -595,15 +595,24 @@ function ImportPage() { } ctx.caseByExt.clear(); ctx.caseByNumber.clear(); - const cases = await fetchAllPaginated<{ id: string; case_number: string | null; external_id: string | null }>( + ctx.caseByTitle.clear(); + const cases = await fetchAllPaginated<{ id: string; case_number: string | null; title: string | null; external_id: string | null }>( "cases", - "id, case_number, external_id", + "id, case_number, title, external_id", ); 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); } - console.info(`[import] lookups loaded: clients=${ctx.clientByExt.size}/${ctx.clientByName.size} cases=${ctx.caseByExt.size}/${ctx.caseByNumber.size}`); + ctx.userByName.clear(); + ctx.userByEmail.clear(); + const { data: profs } = await supabase.from("profiles").select("id, full_name, email"); + for (const p of (profs ?? []) as { id: string; full_name: string | null; email: string | null }[]) { + if (p.full_name) ctx.userByName.set(p.full_name.toLowerCase().trim(), p.id); + if (p.email) ctx.userByEmail.set(p.email.toLowerCase().trim(), p.id); + } + console.info(`[import] lookups loaded: clients=${ctx.clientByExt.size}/${ctx.clientByName.size} cases=${ctx.caseByExt.size}/${ctx.caseByNumber.size}/${ctx.caseByTitle.size} users=${ctx.userByName.size}`); }; const handleFile = async (cfg: ImporterConfig, file: File) => {