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
@@ -11,8 +11,12 @@ import {
MAX_AUTOMATION_STEPS,
isAutomationCondition,
type AutomationAction,
type AutomationChainMode,
type AutomationStep,
type AutomationTrigger,
type LeadFormDto,
type OrgUserDto,
type TeamDto,
} from '@mento/shared';
import { toFaDigits } from '@/lib/jalali';
@@ -20,11 +24,44 @@ export type AutomationAddPayload =
| { kind: 'action'; action: AutomationAction }
| { kind: 'condition' };
const props = defineProps<{
trigger: AutomationTrigger;
steps: AutomationStep[];
selected: 'trigger' | string;
}>();
const props = withDefaults(
defineProps<{
trigger: AutomationTrigger;
steps: AutomationStep[];
selected: 'trigger' | string;
chainMode?: AutomationChainMode;
form?: LeadFormDto | null;
users?: OrgUserDto[];
teams?: TeamDto[];
}>(),
{ chainMode: 'stop', form: null, users: () => [], teams: () => [] },
);
/** شرطی که یک شاخهٔ جدید را شروع می‌کند (گام قبلی شرط نیست) */
function isBranchStart(index: number) {
if (props.chainMode !== 'branches') return false;
const step = props.steps[index];
if (!step || !isAutomationCondition(step)) return false;
const prev = props.steps[index - 1];
return !prev || !isAutomationCondition(prev);
}
function userName(id?: string | null) {
return props.users.find((u) => u.id === id)?.fullName ?? 'کاربر';
}
function assignHint(step: AutomationStep) {
const mode = step.assignMode ?? (step.assigneeId ? 'user' : 'owner');
if (mode === 'user') return userName(step.assigneeId);
if (mode === 'team_round_robin') {
const t = props.teams.find((x) => x.id === step.assigneeTeamId);
return `نوبتی: ${t?.name ?? 'تیم'}`;
}
if (mode === 'users_round_robin') {
return `نوبتی بین ${toFaDigits((step.assigneeUserIds ?? []).length)} نفر`;
}
return 'مسئول پرونده';
}
const emit = defineEmits<{
select: [id: 'trigger' | string];
@@ -55,6 +92,10 @@ function canRemove(step: AutomationStep) {
function stepTitle(step: AutomationStep) {
if (isAutomationCondition(step)) {
const field = step.conditionField ?? 'has_person';
if (field === 'form_field') {
const f = props.form?.fields.find((x) => x.key === step.conditionKey);
return f?.label ?? AUTOMATION_CONDITION_FIELD_LABELS[field];
}
return AUTOMATION_CONDITION_FIELD_LABELS[field];
}
return AUTOMATION_ACTION_LABELS[step.action ?? 'create_activity'];
@@ -64,6 +105,10 @@ function stepHint(step: AutomationStep) {
if (isAutomationCondition(step)) {
const op = step.conditionOp ?? 'is_set';
const opLabel = AUTOMATION_CONDITION_OP_LABELS[op];
if (step.conditionField === 'form_field') {
if (op === 'is_set' || op === 'is_empty') return opLabel;
return `${opLabel} «${step.conditionValue ?? ''}»`;
}
if (step.conditionField === 'deal_amount' && step.conditionValue != null) {
return `${opLabel} ${toFaDigits(Number(step.conditionValue))}`;
}
@@ -90,7 +135,7 @@ function stepHint(step: AutomationStep) {
task: 'وظیفه',
note: 'یادداشت',
};
return types[step.activityType ?? 'task'] ?? 'وظیفه';
return `${types[step.activityType ?? 'task'] ?? 'وظیفه'} — ${assignHint(step)}`;
}
function openAdd(index: number) {
@@ -148,10 +193,11 @@ function nodeClass(step: AutomationStep) {
</div>
<div class="node-wrap">
<span v-if="isBranchStart(i)" class="branch-tag">شاخه</span>
<button
class="node action"
type="button"
:class="[nodeClass(step), { on: selected === step.id }]"
:class="[nodeClass(step), { on: selected === step.id, 'branch-start': isBranchStart(i) }]"
@click="emit('select', step.id)"
>
<span class="icon">
@@ -301,6 +347,25 @@ function nodeClass(step: AutomationStep) {
filter: drop-shadow(0 8px 14px rgba(180, 83, 9, 0.14));
}
.branch-tag {
position: absolute;
top: -1.35rem;
inset-inline-start: 50%;
transform: translateX(50%);
font-size: 0.68rem;
font-weight: 700;
color: #b45309;
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 999px;
padding: 0.05rem 0.5rem;
white-space: nowrap;
}
.node.condition.branch-start {
background: #fef3c7;
}
.node.on {
border-color: #14b8a6;
box-shadow: