import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useEffect, useMemo, useState } from "react"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer, PageHeader } from "@/components/app-shell"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { SearchableSelect } from "@/components/ui/searchable-select"; import { Switch } from "@/components/ui/switch"; import { FL_CIRCUITS } from "@/lib/florida"; import { Packer } from "docx"; import { buildPleadingDoc, downloadPleading, type PleadingFootnote, type PleadingSignature, type ServiceContact } from "@/lib/docx-pleading"; import { downloadPleadingPdf } from "@/lib/pdf-pleading"; import { RichTextEditor } from "@/components/documents/rich-text-editor"; import { ContactPickerPopover } from "@/components/clients/contact-picker-popover"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { toast } from "sonner"; import { Download, FileText, Save, Loader2, Plus, Trash2, PenLine, Users, Variable, Copy } from "lucide-react"; import { applyVars, buildChips, buildVarMap, loadPleadingContext, type LoadedContext, type VarChip } from "@/lib/pleading-variables"; import { promptFilename } from "@/lib/prompt-filename"; import { Badge } from "@/components/ui/badge"; export const Route = createFileRoute("/documents/pleading/new")({ component: PleadingNewPage, validateSearch: (search: Record) => ({ from: typeof search.from === "string" ? search.from : undefined, }), }); function PleadingNewPage() { const { user } = useAuth(); const navigate = useNavigate(); const { from } = Route.useSearch(); const [courtType, setCourtType] = useState<"CIRCUIT" | "COUNTY">("CIRCUIT"); const [circuit, setCircuit] = useState("ELEVENTH"); const [county, setCounty] = useState("Miami-Dade"); const [plaintiffs, setPlaintiffs] = useState(""); const [defendants, setDefendants] = useState(""); const [caseNumber, setCaseNumber] = useState(""); const [title, setTitle] = useState(""); const [bodyHtml, setBodyHtml] = useState("

"); const [footnotes, setFootnotes] = useState([]); const [footerLeft, setFooterLeft] = useState(""); const [footerCenter, setFooterCenter] = useState(""); const [footerRight, setFooterRight] = useState(""); const [docName, setDocName] = useState("Pleading"); const [saving, setSaving] = useState(false); const [serviceList, setServiceList] = useState([]); const [serviceListTitle, setServiceListTitle] = useState("SERVICE LIST"); // Client/case linkage for variable substitution const [clientId, setClientId] = useState(""); const [caseId, setCaseId] = useState(""); const [clientOptions, setClientOptions] = useState<{ id: string; name: string }[]>([]); const [caseOptions, setCaseOptions] = useState<{ id: string; case_number: string; title: string; client_id: string | null }[]>([]); const [varCtx, setVarCtx] = useState({ clientFields: [], caseFields: [] }); const [chips, setChips] = useState([]); const [chipFilter, setChipFilter] = useState(""); useEffect(() => { (async () => { const [{ data: cl }, { data: ks }] = await Promise.all([ supabase.from("clients").select("id, name").is("archived_at", null).order("name"), supabase.from("cases").select("id, case_number, title, client_id").is("archived_at", null).order("case_number", { ascending: false }).limit(500), ]); setClientOptions((cl as any) || []); setCaseOptions((ks as any) || []); })(); }, []); useEffect(() => { (async () => { const ctx = await loadPleadingContext(clientId || null, caseId || null); setVarCtx(ctx); setChips(buildChips(ctx)); })(); }, [clientId, caseId]); const filteredCases = useMemo( () => (clientId ? caseOptions.filter((k) => k.client_id === clientId) : caseOptions), [caseOptions, clientId], ); const filteredChips = useMemo(() => { const q = chipFilter.trim().toLowerCase(); if (!q) return chips; return chips.filter((c) => c.token.toLowerCase().includes(q) || c.label.toLowerCase().includes(q)); }, [chips, chipFilter]); const copyChip = async (token: string) => { try { await navigator.clipboard.writeText(token); toast.success(`Copied ${token}`); } catch { toast.error("Copy failed"); } }; // Attorney signature (loaded from profile) const [includeSignature, setIncludeSignature] = useState(true); const [sigBlock, setSigBlock] = useState(""); const [sigTyped, setSigTyped] = useState(""); const [sigImagePath, setSigImagePath] = useState(""); const [sigImageBytes, setSigImageBytes] = useState(null); const [sigImageDataUrl, setSigImageDataUrl] = useState(""); const [sigImageType, setSigImageType] = useState<"png" | "jpg" | "jpeg">("png"); useEffect(() => { if (!user) return; (async () => { const { data } = await supabase .from("profiles") .select("*") .eq("id", user.id) .maybeSingle(); if (!data) return; const d = data as any; setSigBlock(d.signature_block ?? ""); setSigTyped(d.signature_typed ?? ""); setSigImagePath(d.signature_image_path ?? ""); if (d.signature_image_path) { try { const url = supabase.storage .from("avatars") .getPublicUrl(d.signature_image_path).data.publicUrl; const res = await fetch(url); const blob = await res.blob(); const buf = new Uint8Array(await blob.arrayBuffer()); setSigImageBytes(buf); const reader = new FileReader(); reader.onload = () => setSigImageDataUrl(String(reader.result || "")); reader.readAsDataURL(blob); const ext = (d.signature_image_path.split(".").pop() || "png").toLowerCase(); setSigImageType(ext === "jpg" || ext === "jpeg" ? "jpg" : "png"); } catch { // Ignore – signature image is optional } } })(); }, [user?.id]); const buildSignature = (): PleadingSignature | undefined => { if (!includeSignature) return undefined; if (!sigBlock.trim() && !sigImageBytes && !sigTyped.trim()) return undefined; return { block: sigBlock, typed: sigTyped || undefined, imageBytes: sigImageBytes ?? undefined, imageDataUrl: sigImageDataUrl || undefined, imageType: sigImageType, }; }; useEffect(() => { if (!from) return; (async () => { const { data, error } = await supabase .from("generated_documents") .select("name, payload") .eq("id", from) .maybeSingle(); if (error || !data) return; const p = (data.payload || {}) as any; if (p.courtType) setCourtType(p.courtType); if (p.circuit) setCircuit(p.circuit); if (p.county) setCounty(p.county); if (typeof p.plaintiffs === "string") setPlaintiffs(p.plaintiffs); if (typeof p.defendants === "string") setDefendants(p.defendants); if (typeof p.caseNumber === "string") setCaseNumber(p.caseNumber); if (typeof p.title === "string") setTitle(p.title); if (typeof p.bodyHtml === "string") setBodyHtml(p.bodyHtml); if (Array.isArray(p.footnotes)) setFootnotes(p.footnotes); if (typeof p.footerLeft === "string") setFooterLeft(p.footerLeft); if (typeof p.footerCenter === "string") setFooterCenter(p.footerCenter); if (typeof p.footerRight === "string") setFooterRight(p.footerRight); if (Array.isArray(p.serviceList)) setServiceList(p.serviceList); if (typeof p.serviceListTitle === "string") setServiceListTitle(p.serviceListTitle); if (typeof p.clientId === "string") setClientId(p.clientId); if (typeof p.caseId === "string") setCaseId(p.caseId); setDocName(`${data.name} (copy)`); toast.success("Loaded saved pleading"); })(); }, [from]); const counties = useMemo(() => { const c = FL_CIRCUITS.find((x) => x.value === circuit); return c?.counties ?? []; }, [circuit]); const handleCircuitChange = (v: string) => { setCircuit(v); const c = FL_CIRCUITS.find((x) => x.value === v); if (c && !c.counties.includes(county)) setCounty(c.counties[0]); }; const varMap = useMemo(() => buildVarMap(varCtx), [varCtx]); const sub = (s: string) => applyVars(s, varMap); const subFootnotes = (fns: PleadingFootnote[]) => fns.map((f) => ({ ...f, text: sub(f.text) })); const subServiceList = (list: ServiceContact[]) => list.map((c) => ({ ...c, name: sub(c.name), role: c.role ? sub(c.role) : c.role, company: c.company ? sub(c.company) : c.company, email: c.email ? sub(c.email) : c.email, phone: c.phone ? sub(c.phone) : c.phone, address: c.address ? sub(c.address) : c.address, })); const input = { courtType, circuit, county, plaintiffs: sub(plaintiffs), defendants: sub(defendants), caseNumber: sub(caseNumber), title: sub(title), bodyHtml: sub(bodyHtml), footnotes: subFootnotes(footnotes), footerLeft: sub(footerLeft), footerCenter: sub(footerCenter), footerRight: sub(footerRight), signature: buildSignature(), serviceList: subServiceList(serviceList), serviceListTitle: sub(serviceListTitle), }; // Raw input (no substitution) for saving the source so tokens persist const rawInput = { courtType, circuit, county, plaintiffs, defendants, caseNumber, title, bodyHtml, footnotes, footerLeft, footerCenter, footerRight, signature: buildSignature(), serviceList, serviceListTitle, clientId: clientId || null, caseId: caseId || null, }; const headerLine1 = `IN THE ${courtType} COURT OF THE ${circuit} JUDICIAL CIRCUIT,`; const headerLine2 = `IN AND FOR ${county.toUpperCase()} COUNTY, FLORIDA`; const onDownload = async () => { const name = promptFilename(docName || "Pleading", "docx"); if (!name) return; await downloadPleading(input, name); }; const onDownloadPdf = async () => { const name = promptFilename(docName || "Pleading", "pdf"); if (!name) return; await downloadPleadingPdf(input, name); }; const onSave = async () => { setSaving(true); try { const doc = buildPleadingDoc(input); const blob = await Packer.toBlob(doc); const safe = (docName || "Pleading").replace(/[^a-zA-Z0-9._-]/g, "_"); const path = `${user?.id}/${Date.now()}-${safe}.docx`; const { error: upErr } = await supabase.storage.from("generated-documents").upload(path, blob, { contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", }); if (upErr) throw upErr; const { error: insErr } = await supabase.from("generated_documents").insert({ name: docName || "Pleading", kind: "pleading", payload: rawInput as any, storage_path: path, created_by: user?.id, }); if (insErr) throw insErr; toast.success("Pleading saved"); navigate({ to: "/documents" }); } catch (e: any) { toast.error(e.message ?? "Save failed"); } finally { setSaving(false); } }; const addFootnote = () => { const nextId = (footnotes[footnotes.length - 1]?.id ?? 0) + 1; setFootnotes([...footnotes, { id: nextId, text: "" }]); }; const updateFootnote = (id: number, text: string) => setFootnotes(footnotes.map((f) => (f.id === id ? { ...f, text } : f))); const removeFootnote = (id: number) => { const remaining = footnotes.filter((f) => f.id !== id).map((f, i) => ({ ...f, id: i + 1 })); setFootnotes(remaining); }; return ( } /> {/* Client / case linkage + variable picker */}

Pick a client and/or case to expose variables (including custom fields). Insert tokens like{" "} {`{{client.name}}`} anywhere in the body, caption, footnotes, or footer — they’ll be substituted on download & save.

{ const next = v === "__none__" ? "" : v; setClientId(next); if (next && caseId) { const k = caseOptions.find((x) => x.id === caseId); if (k && k.client_id !== next) setCaseId(""); } }} placeholder="None" searchPlaceholder="Search clients…" emptyText="No clients found." options={[ { value: "__none__", label: "— None —" }, ...clientOptions.map((c) => ({ value: c.id, label: c.name, keywords: c.name })), ]} />
setCaseId(v === "__none__" ? "" : v)} placeholder="None" searchPlaceholder="Search cases…" emptyText="No cases found." options={[ { value: "__none__", label: "— None —" }, ...filteredCases.map((k) => ({ value: k.id, label: `${k.case_number} — ${k.title}`, keywords: `${k.case_number} ${k.title}`, })), ]} />
{chips.length > 0 ? (
setChipFilter(e.target.value)} placeholder="Filter…" className="h-8 max-w-[220px]" />
{filteredChips.length === 0 ? ( No matching variables. ) : ( filteredChips.map((c) => ( )) )}

Click a chip to copy its token, then paste it where you want the value to appear.

) : (
Select a client or case to see available variables.
)}
{/* Top row: settings + caption preview */}
setDocName(e.target.value)} placeholder="e.g. Complaint - Smith v. Jones" />