Added default client picker

X-Lovable-Edit-ID: edt-b2f60862-6d26-49e3-8990-7fe86dad4d4e
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 03:30:26 +00:00
co-authored by renee-png
+37 -5
View File
@@ -1,5 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import Papa from "papaparse";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
@@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
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";
@@ -51,6 +52,8 @@ interface ImportCtx {
// Name → uuid lookups (for files that reference by name only)
clientByName: Map<string, string>;
caseByNumber: Map<string, string>;
// Optional fallback client id (e.g. for address-only locations CSV)
defaultClientId?: string | null;
}
const norm = (s: string) =>
@@ -208,7 +211,7 @@ const IMPORTERS: ImporterConfig[] = [
transform: (r, ctx) => {
const cid = r._clientext ? ctx.clientByExt.get(String(r._clientext)) : null;
const cidByName = r._clientname ? ctx.clientByName.get(String(r._clientname).toLowerCase()) : null;
r.client_id = cid ?? cidByName ?? null;
r.client_id = cid ?? cidByName ?? ctx.defaultClientId ?? null;
delete r._clientext; delete r._clientname;
if (!r.client_id) return null;
if (!r.first_name && r._ownername) {
@@ -216,6 +219,10 @@ const IMPORTERS: ImporterConfig[] = [
r.first_name = first_name; r.last_name = last_name;
}
delete r._ownername;
// Allow address-only rows: synthesize a placeholder owner name from the address
if (!r.first_name) {
r.first_name = r.address || r.unit_number || "Unit";
}
if (!r.last_name) r.last_name = "";
return r;
},
@@ -509,6 +516,8 @@ interface ImportResult {
function ImportPage() {
const { user } = useAuth();
const [results, setResults] = useState<Record<string, ImportResult | "running">>({});
const [clientList, setClientList] = useState<{ id: string; name: string }[]>([]);
const [defaultClientId, setDefaultClientId] = useState<string>("");
const ctx = useMemo<ImportCtx>(() => ({
userId: user?.id ?? "",
clientByExt: new Map(),
@@ -518,8 +527,18 @@ function ImportPage() {
invoiceByExt: new Map(),
clientByName: new Map(),
caseByNumber: new Map(),
defaultClientId: null,
}), [user?.id]);
useEffect(() => {
void supabase
.from("clients")
.select("id,name")
.is("archived_at", null)
.order("name")
.then(({ data }) => setClientList((data ?? []) as { id: string; name: string }[]));
}, []);
// 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).
@@ -569,6 +588,7 @@ function ImportPage() {
const handleFile = async (cfg: ImporterConfig, file: File) => {
if (!user?.id) return;
setResults((r) => ({ ...r, [cfg.key]: "running" }));
ctx.defaultClientId = cfg.key === "locations" ? (defaultClientId || null) : null;
await ensureLookupsLoaded();
const parsed = await new Promise<Papa.ParseResult<Record<string, string>>>((resolve, reject) => {
@@ -746,7 +766,19 @@ function ImportPage() {
</CardTitle>
<p className="text-xs text-muted-foreground mt-1">{cfg.description}</p>
</div>
<div className="shrink-0">
<div className="shrink-0 flex items-center gap-2">
{cfg.key === "locations" && (
<Select value={defaultClientId} onValueChange={setDefaultClientId}>
<SelectTrigger className="h-8 w-[220px] text-xs">
<SelectValue placeholder="Assign all rows to client…" />
</SelectTrigger>
<SelectContent>
{clientList.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
)}
<input
type="file"
accept=".csv,text/csv"
@@ -757,9 +789,9 @@ function ImportPage() {
if (f) handleFile(cfg, f);
e.target.value = "";
}}
disabled={running}
disabled={running || (cfg.key === "locations" && !defaultClientId)}
/>
<Button asChild size="sm" variant="outline" disabled={running}>
<Button asChild size="sm" variant="outline" disabled={running || (cfg.key === "locations" && !defaultClientId)}>
<label htmlFor={`file-${cfg.key}`} className="cursor-pointer">
{running ? <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" /> : <Upload className="h-3.5 w-3.5 mr-1.5" />}
{running ? "Importing…" : "Upload CSV"}