feat(automations): form-driven branching rules, round-robin assignment, lead-linked activities

This commit is contained in:
2026-10-11 14:12:36 +03:30
parent b30f68563d
commit b3b92abae9
27 changed files with 1160 additions and 91 deletions
@@ -0,0 +1,51 @@
export type FormFieldLabel = { key: string; label: string };
export type TemplateVars = {
title: string;
mobile?: string | null;
formName?: string | null;
formValues?: Record<string, unknown> | null;
};
/** counter یک‌واحد افزایش‌یافته از AutomationCursor است (از ۱ شروع می‌شود) */
export function pickRoundRobin(memberIds: string[], counter: number): string | null {
const ids = [...new Set(memberIds.filter(Boolean))].sort();
if (!ids.length) return null;
const n = Math.max(1, Math.trunc(counter));
return ids[(n - 1) % ids.length] ?? null;
}
/** جایگزینی {{title}}، {{mobile}}، {{form}} و {{field.<key>}} */
export function renderAutomationTemplate(text: string, vars: TemplateVars): string {
return text.replace(/\{\{\s*([\w.\u0600-\u06FF-]+)\s*\}\}/g, (_m, name: string) => {
if (name === 'title') return vars.title ?? '';
if (name === 'mobile') return vars.mobile ?? '';
if (name === 'form') return vars.formName ?? '';
if (name.startsWith('field.')) {
const v = vars.formValues?.[name.slice(6)];
return v == null ? '' : String(v);
}
return '';
});
}
export function formatFormAnswers(
fields: FormFieldLabel[] | null | undefined,
values: Record<string, unknown> | null | undefined,
): string {
if (!values) return '';
const lines: string[] = [];
const seen = new Set<string>();
for (const f of fields ?? []) {
seen.add(f.key);
const raw = values[f.key];
const v = raw == null ? '' : String(raw).trim();
if (v) lines.push(`${f.label}: ${v}`);
}
for (const [key, raw] of Object.entries(values)) {
if (seen.has(key)) continue;
const v = raw == null ? '' : String(raw).trim();
if (v) lines.push(`${key}: ${v}`);
}
return lines.join('\n');
}