Give automations a visual chain with conditions and SMS, and match the pipeline board to the sales mockup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 13:12:13 +03:30
co-authored by Cursor
parent 48d3469eea
commit 3ec87ff912
17 changed files with 2355 additions and 410 deletions
+181
View File
@@ -1739,13 +1739,123 @@ export const AUTOMATION_ACTION_LABELS: Record<AutomationAction, string> = {
add_loyalty_points: "افزودن امتیاز باشگاه",
};
export const MAX_AUTOMATION_STEPS = 8;
export const AUTOMATION_ACTIONS = [
"create_activity",
"create_notification",
"add_loyalty_points",
] as const satisfies readonly AutomationAction[];
export const AUTOMATION_TRIGGERS = [
"deal_won",
"ticket_resolved",
] as const satisfies readonly AutomationTrigger[];
export type AutomationStepKind = "action" | "condition";
export type AutomationConditionField =
| "deal_amount"
| "has_person"
| "has_company"
| "ticket_priority";
export type AutomationConditionOp =
| "gte"
| "lte"
| "eq"
| "neq"
| "is_set"
| "is_empty";
export type AutomationNotifyChannel = "app" | "sms" | "both";
export type AutomationNotifyTarget = "staff" | "person";
export const AUTOMATION_CONDITION_FIELD_LABELS: Record<
AutomationConditionField,
string
> = {
deal_amount: "مبلغ معامله",
has_person: "مخاطب دارد",
has_company: "شرکت دارد",
ticket_priority: "اولویت تیکت",
};
export const AUTOMATION_CONDITION_OP_LABELS: Record<
AutomationConditionOp,
string
> = {
gte: "بزرگ‌تر یا مساوی",
lte: "کوچک‌تر یا مساوی",
eq: "مساوی",
neq: "نامساوی",
is_set: "دارد",
is_empty: "ندارد",
};
export const AUTOMATION_NOTIFY_CHANNEL_LABELS: Record<
AutomationNotifyChannel,
string
> = {
app: "ناتیفیکیشن داخلی",
sms: "پیامک",
both: "هر دو",
};
export const AUTOMATION_NOTIFY_CHANNELS = [
"app",
"sms",
"both",
] as const satisfies readonly AutomationNotifyChannel[];
export function isAutomationCondition(step: Pick<AutomationStep, "kind">): boolean {
return step.kind === "condition";
}
export function firstAutomationAction(
steps: AutomationStep[],
): AutomationAction | null {
const step = steps.find((s) => !isAutomationCondition(s) && s.action);
return step?.action ?? null;
}
export function conditionFieldsForTrigger(
trigger: AutomationTrigger,
): AutomationConditionField[] {
if (trigger === "deal_won") {
return ["deal_amount", "has_person", "has_company"];
}
return ["ticket_priority", "has_person", "has_company"];
}
export interface AutomationStep {
id: string;
kind?: AutomationStepKind;
action?: AutomationAction;
title?: string;
body?: string;
assigneeId?: string | null;
notifyUserId?: string | null;
notifyChannel?: AutomationNotifyChannel;
notifyTarget?: AutomationNotifyTarget;
activityType?: "call" | "meeting" | "task" | "note";
points?: number;
conditionField?: AutomationConditionField;
conditionOp?: AutomationConditionOp;
conditionValue?: string | number;
}
export interface AutomationRuleConfig {
title?: string;
body?: string;
assigneeId?: string | null;
notifyUserId?: string | null;
notifyChannel?: AutomationNotifyChannel;
notifyTarget?: AutomationNotifyTarget;
activityType?: "call" | "meeting" | "task" | "note";
points?: number;
/** زنجیرهٔ اعمال؛ اگر خالی باشد همان action تکی استفاده می‌شود */
steps?: AutomationStep[];
}
export interface AutomationRuleDto {
@@ -1769,6 +1879,77 @@ export interface CreateAutomationRuleRequest {
export type UpdateAutomationRuleRequest = Partial<CreateAutomationRuleRequest>;
export function normalizeAutomationSteps(
action: AutomationAction,
config?: AutomationRuleConfig | null,
): AutomationStep[] {
const cfg = config ?? {};
if (Array.isArray(cfg.steps) && cfg.steps.length) {
return cfg.steps
.filter((s) => s && (s.kind === "condition" || s.action))
.map((s, i) => ({
id: s.id || `step-${i}`,
kind: s.kind === "condition" ? "condition" : "action",
action: s.action,
title: s.title,
body: s.body,
assigneeId: s.assigneeId,
notifyUserId: s.notifyUserId,
notifyChannel: s.notifyChannel,
notifyTarget: s.notifyTarget,
activityType: s.activityType,
points: s.points,
conditionField: s.conditionField,
conditionOp: s.conditionOp,
conditionValue: s.conditionValue,
}));
}
return [
{
id: "step-0",
kind: "action",
action,
title: cfg.title,
body: cfg.body,
assigneeId: cfg.assigneeId,
notifyUserId: cfg.notifyUserId,
notifyChannel: cfg.notifyChannel,
notifyTarget: cfg.notifyTarget,
activityType: cfg.activityType,
points: cfg.points,
},
];
}
export function packAutomationConfig(steps: AutomationStep[]): AutomationRuleConfig {
const first =
steps.find((s) => (s.kind ?? "action") !== "condition") ?? steps[0];
return {
steps,
title: first?.title,
body: first?.body,
assigneeId: first?.assigneeId,
notifyUserId: first?.notifyUserId,
notifyChannel: first?.notifyChannel,
notifyTarget: first?.notifyTarget,
activityType: first?.activityType,
points: first?.points,
};
}
export function automationStepToConfig(step: AutomationStep): AutomationRuleConfig {
return {
title: step.title,
body: step.body,
assigneeId: step.assigneeId,
notifyUserId: step.notifyUserId,
notifyChannel: step.notifyChannel,
notifyTarget: step.notifyTarget,
activityType: step.activityType,
points: step.points,
};
}
// —— ته‌حساب / حسابداری ——
export const TAHSAB_SANAD_TYPES = [