diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx
index af80017..14f90f6 100644
--- a/src/components/forms/custom-form-builder.tsx
+++ b/src/components/forms/custom-form-builder.tsx
@@ -1130,6 +1130,12 @@ export function CustomFormBuilder() {
Click to insert at cursor.
+
+ Control case with |upper,{" "}
+ |lower, |title, or{" "}
+ |sentence. Example:{" "}
+ {`{{ownerName|upper}}`}
+
diff --git a/src/lib/forms-shared.ts b/src/lib/forms-shared.ts
index 61174a5..930bd46 100644
--- a/src/lib/forms-shared.ts
+++ b/src/lib/forms-shared.ts
@@ -134,6 +134,47 @@ export async function fetchCustomFieldDefs(): Promise {
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";
+
+export function applyCase(value: string, modifier?: string | null): string {
+ if (!value || !modifier) return value ?? "";
+ const m = modifier.toLowerCase().trim();
+ switch (m) {
+ case "upper":
+ return value.toUpperCase();
+ case "lower":
+ return value.toLowerCase();
+ case "title":
+ // Capitalize each word; preserve internal punctuation/whitespace.
+ return value.toLowerCase().replace(/\b([a-z\u00C0-\u024F])/g, (c) => c.toUpperCase());
+ case "sentence":
+ case "capitalize": {
+ // Lowercase the whole string, then capitalize the first letter of each sentence.
+ const lower = value.toLowerCase();
+ return lower.replace(/(^\s*[a-z\u00C0-\u024F])|([.!?]\s+[a-z\u00C0-\u024F])/g, (s) =>
+ s.toUpperCase(),
+ );
+ }
+ default:
+ return value;
+ }
+}
+
+// Replace any occurrence of {{name}} or {{name|modifier}} using the provided lookup.
+function replaceWithModifier(
+ body: string,
+ pattern: RegExp,
+ resolver: (name: string) => string | undefined,
+): string {
+ return body.replace(pattern, (_match, rawName: string, _pipe: string | undefined, mod: string | undefined) => {
+ const value = resolver(rawName.trim());
+ if (value == null) return "";
+ return applyCase(value, mod);
+ });
+}
+
export function applyVariables(
body: string,
ctx: {
@@ -145,32 +186,40 @@ export function applyVariables(
},
): string {
const today = format(new Date(), "MMMM d, yyyy");
- const replacements: Record = {
- "{{clientName}}": ctx.client?.name ?? "",
- "{{ownerName}}": ownerFullName(ctx.homeowner),
- "{{propertyAddress}}": ctx.homeowner?.address ?? "",
- "{{unitNumber}}": ctx.homeowner?.unit_number ?? "",
- "{{accountNumber}}": ctx.homeowner?.unit_number ?? "",
- "{{balance}}": (ctx.homeowner?.opening_balance ?? 0).toFixed(2),
- "{{currentDate}}": today,
- "{{firmName}}": ctx.firmName ?? "",
+ const systemValues: Record = {
+ clientName: ctx.client?.name ?? "",
+ ownerName: ownerFullName(ctx.homeowner),
+ propertyAddress: ctx.homeowner?.address ?? "",
+ unitNumber: ctx.homeowner?.unit_number ?? "",
+ accountNumber: ctx.homeowner?.unit_number ?? "",
+ balance: (ctx.homeowner?.opening_balance ?? 0).toFixed(2),
+ currentDate: today,
+ firmName: ctx.firmName ?? "",
};
+
let out = body;
- for (const [k, v] of Object.entries(replacements)) {
- out = out.split(k).join(v);
- }
- // Custom case-field variables like {{custom.mortgage_holder}}
- if (ctx.customValues) {
- for (const [k, v] of Object.entries(ctx.customValues)) {
- out = out.split(`{{custom.${k}}}`).join(v ?? "");
- }
- }
- // Per-form fillable fields like {{field.tenant_name}}
- if (ctx.fieldValues) {
- for (const [k, v] of Object.entries(ctx.fieldValues)) {
- out = out.split(`{{field.${k}}}`).join(v ?? "");
- }
- }
+
+ // System variables: {{name}} or {{name|modifier}}
+ out = replaceWithModifier(
+ out,
+ /\{\{\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(\|\s*([a-zA-Z]+)\s*)?\}\}/g,
+ (name) => systemValues[name],
+ );
+
+ // Custom case-field variables: {{custom.key}} or {{custom.key|modifier}}
+ out = replaceWithModifier(
+ out,
+ /\{\{\s*custom\.([a-zA-Z0-9_]+)\s*(\|\s*([a-zA-Z]+)\s*)?\}\}/g,
+ (name) => ctx.customValues?.[name],
+ );
+
+ // Per-form fillable fields: {{field.key}} or {{field.key|modifier}}
+ out = replaceWithModifier(
+ out,
+ /\{\{\s*field\.([a-zA-Z0-9_]+)\s*(\|\s*([a-zA-Z]+)\s*)?\}\}/g,
+ (name) => ctx.fieldValues?.[name],
+ );
+
return out;
}