Added client custom vars support

X-Lovable-Edit-ID: edt-2fe21d13-e759-4a99-8f55-5a3c405faf6c
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 17:33:08 +00:00
co-authored by renee-png
2 changed files with 60 additions and 4 deletions
+26 -1
View File
@@ -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<FirmInfo | null>(null);
const [search, setSearch] = useState("");
const [customDefs, setCustomDefs] = useState<CustomFieldVar[]>([]);
const [clientCustomDefs, setClientCustomDefs] = useState<CustomFieldVar[]>([]);
const [clientCustomValues, setClientCustomValues] = useState<Record<string, string>>({});
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();
+34 -3
View File
@@ -134,6 +134,15 @@ export async function fetchCustomFieldDefs(): Promise<CustomFieldVar[]> {
return (data ?? []) as CustomFieldVar[];
}
export async function fetchClientCustomFieldDefs(): Promise<CustomFieldVar[]> {
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<string, string>;
clientCustomValues?: Record<string, string>;
fieldValues?: Record<string, string>;
},
): 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<Record<stri
});
return map;
}
export async function fetchClientCustomValues(clientId: string): Promise<Record<string, string>> {
const { data } = await supabase
.from("client_field_values")
.select("value, field:custom_client_fields(key)")
.eq("client_id", clientId);
const map: Record<string, string> = {};
(data ?? []).forEach((row: any) => {
const k = row.field?.key;
if (k) map[k] = row.value ?? "";
});
return map;
}