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 { Switch } from "@/components/ui/switch"; import { FL_CIRCUITS } from "@/lib/florida"; import { Packer } from "docx"; import { buildPleadingDoc, downloadPleading, type PleadingFootnote, type PleadingSignature } from "@/lib/docx-pleading"; import { downloadPleadingPdf } from "@/lib/pdf-pleading"; import { RichTextEditor } from "@/components/documents/rich-text-editor"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { toast } from "sonner"; import { Download, FileText, Save, Loader2, Plus, Trash2, PenLine } from "lucide-react"; 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); // 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); 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 input = { courtType, circuit, county, plaintiffs, defendants, caseNumber, title, bodyHtml, footnotes, footerLeft, footerCenter, footerRight, signature: buildSignature(), }; const headerLine1 = `IN THE ${courtType} COURT OF THE ${circuit} JUDICIAL CIRCUIT,`; const headerLine2 = `IN AND FOR ${county.toUpperCase()} COUNTY, FLORIDA`; const onDownload = async () => { await downloadPleading(input, docName || "Pleading"); }; const onDownloadPdf = async () => { await downloadPleadingPdf(input, docName || "Pleading"); }; 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: input 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 ( } /> {/* Top row: settings + caption preview */}
setDocName(e.target.value)} placeholder="e.g. Complaint - Smith v. Jones" />