diff --git a/src/routes/settings.import.tsx b/src/routes/settings.import.tsx index 9ad0ff9..f076933 100644 --- a/src/routes/settings.import.tsx +++ b/src/routes/settings.import.tsx @@ -8,7 +8,9 @@ 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { Upload, CheckCircle2, AlertCircle, Loader2, FileSpreadsheet, ClipboardPaste } from "lucide-react"; import { toast } from "sonner"; import { CONTACT_TYPES } from "@/components/contacts/contact-form-dialog"; @@ -572,6 +574,8 @@ function ImportPage() { const [clientList, setClientList] = useState<{ id: string; name: string }[]>([]); const [defaultClientId, setDefaultClientId] = useState(""); const [contactGroup, setContactGroup] = useState("client"); + const [pasteFor, setPasteFor] = useState(null); + const [pasteText, setPasteText] = useState(""); const ctx = useMemo(() => ({ userId: user?.id ?? "", clientByExt: new Map(), @@ -676,7 +680,7 @@ function ImportPage() { 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) => { + const handleFile = async (cfg: ImporterConfig, source: File | string) => { if (!user?.id) return; setResults((r) => ({ ...r, [cfg.key]: "running" })); ctx.defaultClientId = cfg.key === "locations" ? (defaultClientId || null) : null; @@ -684,12 +688,20 @@ function ImportPage() { await ensureLookupsLoaded(); const parsed = await new Promise>>((resolve, reject) => { - Papa.parse>(file, { - header: true, - skipEmptyLines: true, - complete: resolve, - error: reject, - }); + if (typeof source === "string") { + const result = Papa.parse>(source, { + header: true, + skipEmptyLines: true, + }); + resolve(result); + } else { + Papa.parse>(source, { + header: true, + skipEmptyLines: true, + complete: resolve, + error: reject, + }); + } }); const rawRows = parsed.data; @@ -1042,6 +1054,18 @@ function ImportPage() { {running ? "Importing…" : "Upload CSV"} + @@ -1081,6 +1105,39 @@ function ImportPage() { ); })} + + !o && setPasteFor(null)}> + + + Paste CSV — {pasteFor?.label} + + Paste CSV (or tab-separated) data including the header row. Same column rules as file upload apply. + + +