diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index dde281a..51629a1 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -695,6 +695,16 @@ function ImportPage() { bumpSkip("client not found in existing clients (import companies first)"); } else if ((cfg.table === "cases" || cfg.table === "invoices") && !row.client_id) { 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 { bumpSkip("rejected by transform"); } @@ -736,7 +746,55 @@ function ImportPage() { const withConflict = chunk.filter((r) => r[cfg.conflict]); const withoutConflict = chunk.filter((r) => !r[cfg.conflict]); - if (withConflict.length > 0) { + if (cfg.table === "time_entries" && withConflict.length > 0) { + const externalIds = withConflict.map((r) => String(r[cfg.conflict])); + const { data: existing, error: lookupError } = await supabase + .from("time_entries") + .select("id, external_id") + .in("external_id", externalIds); + + if (lookupError) { + errors.push(`Batch ${i / chunkSize + 1} (lookup): ${lookupError.message}`); + } else { + const existingByExternalId = new Map( + (existing ?? []).map((entry: { id: string; external_id: string | null }) => [entry.external_id, entry.id]), + ); + const toUpdate = withConflict + .filter((r) => existingByExternalId.has(String(r.external_id))) + .map((r) => ({ ...r, id: existingByExternalId.get(String(r.external_id)) })); + const toInsert = withConflict.filter((r) => !existingByExternalId.has(String(r.external_id))); + + if (toUpdate.length > 0) { + const updateResults = await Promise.all( + toUpdate.map(async (entry) => { + const { id, ...payload } = entry; + return supabase.from("time_entries").update(payload as any).eq("id", id).select("*").single(); + }), + ); + for (const result of updateResults) { + if (result.error) { + errors.push(`Batch ${i / chunkSize + 1} (update): ${result.error.message}`); + } else if (result.data) { + inserted += 1; + inserted_rows.push(result.data); + } + } + } + + if (toInsert.length > 0) { + const { data, error } = await supabase + .from("time_entries") + .insert(toInsert as any) + .select("*"); + if (error) { + errors.push(`Batch ${i / chunkSize + 1} (insert): ${error.message}`); + } else if (data) { + inserted += data.length; + inserted_rows.push(...data); + } + } + } + } else if (withConflict.length > 0) { const { data, error } = await supabase .from(cfg.table as any) .upsert(withConflict as any, { onConflict: cfg.conflict, ignoreDuplicates: false })