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:
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user