diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx index 053efbc..8b837d3 100644 --- a/src/components/forms/custom-form-builder.tsx +++ b/src/components/forms/custom-form-builder.tsx @@ -59,8 +59,10 @@ import { applyVariables, fetchFirm, fetchCustomFieldDefs, + fetchClientCustomFieldDefs, fetchAccessibleCases, fetchCaseCustomValues, + fetchClientCustomValues, savePdfToDocuments, SYSTEM_VARIABLES, type ClientLite, @@ -259,6 +261,8 @@ export function CustomFormBuilder() { const [firm, setFirm] = useState(null); const [search, setSearch] = useState(""); const [customDefs, setCustomDefs] = useState([]); + const [clientCustomDefs, setClientCustomDefs] = useState([]); + const [clientCustomValues, setClientCustomValues] = useState>({}); const [fontFamily, setFontFamily] = useState("Bookman Old Style"); const [fontSize, setFontSize] = useState(12); const [lineHeight, setLineHeight] = useState(1.5); @@ -314,9 +318,19 @@ export function CustomFormBuilder() { useEffect(() => { fetchFirm().then(setFirm); fetchCustomFieldDefs().then(setCustomDefs); + fetchClientCustomFieldDefs().then(setClientCustomDefs); loadTemplates(); }, []); + // Load client custom field values whenever the selected client changes. + useEffect(() => { + if (!client?.id) { + setClientCustomValues({}); + return; + } + fetchClientCustomValues(client.id).then(setClientCustomValues); + }, [client?.id]); + const loadTemplates = async () => { const { data } = await supabase .from("custom_form_templates") @@ -662,11 +676,17 @@ export function CustomFormBuilder() { if (selectedCaseId) { customValues = await fetchCaseCustomValues(selectedCaseId); } + // Always re-fetch the latest client custom values at render time so edits + // made in another tab show up without needing to reselect the client. + const freshClientCustomValues = client?.id + ? await fetchClientCustomValues(client.id) + : {}; return { client, homeowner, firmName: firm?.company_name ?? "", customValues, + clientCustomValues: freshClientCustomValues, fieldValues, }; }; @@ -1172,6 +1192,11 @@ export function CustomFormBuilder() { description: d.label + (d.description ? ` — ${d.description}` : ""), kind: "custom" as const, })), + ...clientCustomDefs.map((d) => ({ + key: `{{client.custom.${d.key}}}`, + description: `Client: ${d.label}` + (d.description ? ` — ${d.description}` : ""), + kind: "client-custom" as const, + })), ...formFields.map((f) => ({ key: `{{field.${f.key}}}`, description: `${f.label} (${f.type})`, @@ -1183,7 +1208,7 @@ export function CustomFormBuilder() { v.key.toLowerCase().includes(search.toLowerCase()) || v.description.toLowerCase().includes(search.toLowerCase()), ); - }, [customDefs, formFields, search]); + }, [customDefs, clientCustomDefs, formFields, search]); const filteredCases = useMemo(() => { const s = caseSearch.toLowerCase(); diff --git a/src/lib/forms-shared.ts b/src/lib/forms-shared.ts index f123154..2749753 100644 --- a/src/lib/forms-shared.ts +++ b/src/lib/forms-shared.ts @@ -134,6 +134,15 @@ export async function fetchCustomFieldDefs(): Promise { return (data ?? []) as CustomFieldVar[]; } +export async function fetchClientCustomFieldDefs(): Promise { + const { data } = await supabase + .from("custom_client_fields") + .select("key,label,description") + .eq("active", true) + .order("sort_order"); + return (data ?? []) as CustomFieldVar[]; +} + // Case modifiers usable in templates as {{var|modifier}}. // Supported: upper, lower, title, sentence, capitalize (alias for sentence). export type CaseModifier = "upper" | "lower" | "title" | "sentence" | "capitalize"; @@ -189,6 +198,7 @@ export function applyVariables( homeowner?: HomeownerLite | null; firmName?: string; customValues?: Record; + clientCustomValues?: Record; fieldValues?: Record; }, ): string { @@ -206,11 +216,12 @@ export function applyVariables( let out = body; - // System variables: {{name}} or {{name|modifier}} + // Client custom-field variables: {{client.custom.key}} or {{client.custom.key|modifier}} + // Must run BEFORE the generic {{custom.key}} pattern so the prefix isn't swallowed. out = replaceWithModifier( out, - /\{\{\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(\|\s*([a-zA-Z]+)\s*)?\}\}/g, - (name) => systemValues[name], + /\{\{\s*client\.custom\.([a-zA-Z0-9_]+)\s*(\|\s*([a-zA-Z]+)\s*)?\}\}/g, + (name) => ctx.clientCustomValues?.[name], ); // Custom case-field variables: {{custom.key}} or {{custom.key|modifier}} @@ -227,6 +238,13 @@ export function applyVariables( (name) => ctx.fieldValues?.[name], ); + // System variables: {{name}} or {{name|modifier}} — run LAST so dotted vars above win. + out = replaceWithModifier( + out, + /\{\{\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(\|\s*([a-zA-Z]+)\s*)?\}\}/g, + (name) => systemValues[name], + ); + return out; } @@ -337,3 +355,16 @@ export async function fetchCaseCustomValues(caseId: string): Promise> { + const { data } = await supabase + .from("client_field_values") + .select("value, field:custom_client_fields(key)") + .eq("client_id", clientId); + const map: Record = {}; + (data ?? []).forEach((row: any) => { + const k = row.field?.key; + if (k) map[k] = row.value ?? ""; + }); + return map; +}