From 1b9fd4ab7a7c519ff34e74e6c91044538fef7c31 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:09:52 +0000 Subject: [PATCH 1/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/lib/pleading-variables.ts | 148 ++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/lib/pleading-variables.ts diff --git a/src/lib/pleading-variables.ts b/src/lib/pleading-variables.ts new file mode 100644 index 0000000..7d10811 --- /dev/null +++ b/src/lib/pleading-variables.ts @@ -0,0 +1,148 @@ +import { supabase } from "@/integrations/supabase/client"; + +export type VarMap = Record; + +export type LoadedContext = { + client?: any; + cases?: any; + clientFields: { key: string; label: string; value: string }[]; + caseFields: { key: string; label: string; value: string }[]; +}; + +export type VarChip = { token: string; label: string; value: string }; + +export async function loadPleadingContext( + clientId: string | null, + caseId: string | null, +): Promise { + const result: LoadedContext = { clientFields: [], caseFields: [] }; + + if (clientId) { + const { data: client } = await supabase.from("clients").select("*").eq("id", clientId).maybeSingle(); + if (client) result.client = client; + const [{ data: defs }, { data: vals }] = await Promise.all([ + supabase.from("custom_client_fields").select("id, key, label").eq("active", true).order("sort_order"), + supabase.from("client_field_values").select("field_id, value").eq("client_id", clientId), + ]); + const valMap = new Map((vals || []).map((v: any) => [v.field_id, v.value || ""])); + result.clientFields = (defs || []).map((d: any) => ({ + key: d.key, + label: d.label, + value: valMap.get(d.id) || "", + })); + } + + if (caseId) { + const { data: caseRow } = await supabase.from("cases").select("*").eq("id", caseId).maybeSingle(); + if (caseRow) result.cases = caseRow; + const [{ data: defs }, { data: vals }] = await Promise.all([ + supabase.from("custom_case_fields").select("id, key, label").eq("active", true).order("sort_order"), + supabase.from("case_field_values").select("field_id, value").eq("case_id", caseId), + ]); + const valMap = new Map((vals || []).map((v: any) => [v.field_id, v.value || ""])); + result.caseFields = (defs || []).map((d: any) => ({ + key: d.key, + label: d.label, + value: valMap.get(d.id) || "", + })); + } + + return result; +} + +function fmtAddress(o: any): string { + if (!o) return ""; + const parts = [ + o.address_line1, + o.address_line2, + [o.city, o.state, o.postal_code].filter(Boolean).join(", "), + ].filter(Boolean); + return parts.join("\n"); +} + +export function buildVarMap(ctx: LoadedContext): VarMap { + const m: VarMap = {}; + const c = ctx.client; + if (c) { + m["client.name"] = c.name || ""; + m["client.address"] = fmtAddress(c); + m["client.address_line1"] = c.address_line1 || ""; + m["client.address_line2"] = c.address_line2 || ""; + m["client.city"] = c.city || ""; + m["client.state"] = c.state || ""; + m["client.postal_code"] = c.postal_code || ""; + m["client.contact_name"] = c.primary_contact_name || ""; + m["client.contact_email"] = c.primary_contact_email || ""; + m["client.contact_phone"] = c.primary_contact_phone || ""; + m["client.management_company"] = c.management_company || ""; + } + const k = ctx.cases; + if (k) { + m["case.number"] = k.case_number || ""; + m["case.title"] = k.title || ""; + m["case.caption"] = k.case_caption || ""; + m["case.court"] = k.court || ""; + m["case.court_case_number"] = k.court_case_number || ""; + m["case.judge"] = k.judge || ""; + m["case.jurisdiction"] = k.jurisdiction || ""; + m["case.opposing_party"] = k.opposing_party || ""; + m["case.opposing_counsel"] = k.opposing_counsel || ""; + m["case.opposing_counsel_firm"] = k.opposing_counsel_firm || ""; + m["case.opposing_counsel_email"] = k.opposing_counsel_email || ""; + m["case.opposing_counsel_phone"] = k.opposing_counsel_phone || ""; + m["case.filing_date"] = k.filing_date || ""; + m["case.next_hearing_date"] = k.next_hearing_date || ""; + } + ctx.clientFields.forEach((f) => { + m[`client.field.${f.key}`] = f.value; + }); + ctx.caseFields.forEach((f) => { + m[`case.field.${f.key}`] = f.value; + }); + return m; +} + +export function buildChips(ctx: LoadedContext): VarChip[] { + const m = buildVarMap(ctx); + const out: VarChip[] = []; + const labelMap: Record = { + "client.name": "Client name", + "client.address": "Client address (full)", + "client.contact_name": "Client primary contact", + "client.contact_email": "Client contact email", + "client.contact_phone": "Client contact phone", + "client.management_company": "Management company", + "case.number": "Case number", + "case.title": "Case title", + "case.caption": "Case caption", + "case.court": "Court", + "case.court_case_number": "Court case number", + "case.judge": "Judge", + "case.opposing_party": "Opposing party", + "case.opposing_counsel": "Opposing counsel", + "case.opposing_counsel_firm": "Opposing counsel firm", + "case.filing_date": "Filing date", + "case.next_hearing_date": "Next hearing date", + }; + for (const key of Object.keys(m)) { + let label = labelMap[key]; + if (!label) { + if (key.startsWith("client.field.")) label = `Client · ${key.replace("client.field.", "")}`; + else if (key.startsWith("case.field.")) label = `Case · ${key.replace("case.field.", "")}`; + else label = key; + } + out.push({ token: `{{${key}}}`, label, value: m[key] }); + } + return out; +} + +const TOKEN_RE = /\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g; + +export function applyVars(text: string, map: VarMap): string { + if (!text) return text; + return text.replace(TOKEN_RE, (_, key) => (key in map ? map[key] : `{{${key}}}`)); +} + +export function applyVarsToHtml(html: string, map: VarMap): string { + return applyVars(html, map); +} From c54b4313b103d0c7e8933dcad8bf168157f29b3b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:10:01 +0000 Subject: [PATCH 2/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/documents.pleading.new.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index 3bd20e5..12bcf6b 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -20,7 +20,9 @@ import { ContactPickerPopover } from "@/components/clients/contact-picker-popove import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { toast } from "sonner"; -import { Download, FileText, Save, Loader2, Plus, Trash2, PenLine, Users } from "lucide-react"; +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 { Badge } from "@/components/ui/badge"; export const Route = createFileRoute("/documents/pleading/new")({ component: PleadingNewPage, From 94191d88f965abbd533529e3710c285cb0c9b55e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:10:17 +0000 Subject: [PATCH 3/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/documents.pleading.new.tsx | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index 12bcf6b..14d1807 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -52,6 +52,54 @@ function PleadingNewPage() { 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(""); From b2ef7bf21bfcebaa6fd11bb10f3674e40668b1e8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:10:32 +0000 Subject: [PATCH 4/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/documents.pleading.new.tsx | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index 14d1807..df671d2 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -195,7 +195,38 @@ function PleadingNewPage() { 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, @@ -203,6 +234,8 @@ function PleadingNewPage() { signature: buildSignature(), serviceList, serviceListTitle, + clientId: clientId || null, + caseId: caseId || null, }; const headerLine1 = `IN THE ${courtType} COURT OF THE ${circuit} JUDICIAL CIRCUIT,`; From 7f3d47bd291c2af56358cde4caedd738aa698271 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:10:42 +0000 Subject: [PATCH 5/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/documents.pleading.new.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index df671d2..843c4b6 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -263,7 +263,7 @@ function PleadingNewPage() { const { error: insErr } = await supabase.from("generated_documents").insert({ name: docName || "Pleading", kind: "pleading", - payload: input as any, + payload: rawInput as any, storage_path: path, created_by: user?.id, }); From 8d00f61f2ce70fc7addf4ad441f8ef9cf0c2a7f8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:10:56 +0000 Subject: [PATCH 6/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/documents.pleading.new.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index 843c4b6..b35cfa2 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -179,6 +179,8 @@ function PleadingNewPage() { 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 dda1a3b574ee87613689b230b2eb0f2c0dbc2bc4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:11:22 +0000 Subject: [PATCH 7/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/documents.pleading.new.tsx | 97 +++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index b35cfa2..42998ac 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -308,6 +308,103 @@ function PleadingNewPage() { } /> + {/* 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. +

+
+
+ +
+
+ + +
+
+ + +
+
+ + {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 */}