diff --git a/apps/api/src/automations/automations.service.ts b/apps/api/src/automations/automations.service.ts index 69d0df8..1b371cd 100644 --- a/apps/api/src/automations/automations.service.ts +++ b/apps/api/src/automations/automations.service.ts @@ -22,6 +22,7 @@ import { automationStepToConfig, automationUsesAdvanced, firstAutomationAction, + formConditionValues, matchFormFieldCondition, normalizeAutomationSteps, packAutomationConfig, @@ -668,7 +669,7 @@ function matchCondition(step: AutomationStep, facts: AutomationFacts): boolean { if (field === 'form_field') { const key = step.conditionKey?.trim(); if (!key) return false; - return matchFormFieldCondition(op, facts.formValues[key], step.conditionValue); + return matchFormFieldCondition(op, facts.formValues[key], formConditionValues(step)); } return false; diff --git a/apps/api/test/automation-branches.test.ts b/apps/api/test/automation-branches.test.ts index 3369d50..a5be24b 100644 --- a/apps/api/test/automation-branches.test.ts +++ b/apps/api/test/automation-branches.test.ts @@ -2,6 +2,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { automationUsesAdvanced, + formConditionValues, matchFormFieldCondition, splitAutomationBranches, planAutomationActions, @@ -66,6 +67,21 @@ describe('اتوماسیون شاخه‌ای فرم', () => { assert.equal(matchFormFieldCondition('is_empty', '', ''), true); }); + it('شرط با چند گزینهٔ انتخابی', () => { + const many = ['آزاد', 'کارگر']; + assert.equal(matchFormFieldCondition('contains', 'آزاد, کارگر, کارمند دولتی', many), true); + assert.equal(matchFormFieldCondition('contains', 'آزاد', many), false); + assert.equal(matchFormFieldCondition('contains_any', 'کارگر', many), true); + assert.equal(matchFormFieldCondition('contains_any', 'کارمند دولتی', many), false); + assert.equal(matchFormFieldCondition('not_contains', 'کارمند دولتی', many), true); + assert.equal(matchFormFieldCondition('not_contains', 'کارگر', many), false); + assert.equal(matchFormFieldCondition('eq', 'کارگر, آزاد', many), true); + assert.equal(matchFormFieldCondition('eq', 'آزاد, کارگر, کارمند دولتی', many), false); + assert.equal(matchFormFieldCondition('neq', 'آزاد', many), true); + assert.deepEqual(formConditionValues({ conditionValue: 'آزاد', conditionValues: [] }), ['آزاد']); + assert.deepEqual(formConditionValues({ conditionValue: 'x', conditionValues: many }), many); + }); + it('هر گزینه فقط شاخهٔ خودش را اجرا می‌کند', () => { assert.deepEqual(run(BSJ, 'بنکداری'), ['a1']); assert.deepEqual(run(BSJ, 'آنلاین شاپ'), ['a2']); diff --git a/apps/web/src/components/automations/AutomationCanvas.vue b/apps/web/src/components/automations/AutomationCanvas.vue index a6e8b21..d516297 100644 --- a/apps/web/src/components/automations/AutomationCanvas.vue +++ b/apps/web/src/components/automations/AutomationCanvas.vue @@ -10,6 +10,7 @@ import { AUTOMATION_NOTIFY_CHANNEL_LABELS, AUTOMATION_TRIGGER_LABELS, MAX_AUTOMATION_STEPS, + formConditionValues, isAutomationCondition, splitAutomationBranches, type AutomationAction, @@ -174,7 +175,8 @@ function stepHint(step: AutomationStep) { 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 ?? ''}»`; + const joiner = op === 'contains_any' || op === 'not_contains' ? ' یا ' : ' و '; + return `${opLabel} ${formConditionValues(step).map((v) => `«${v}»`).join(joiner) || '«»'}`; } if (step.conditionField === 'deal_amount' && step.conditionValue != null) { return `${opLabel} ${toFaDigits(Number(step.conditionValue))}`; diff --git a/apps/web/src/components/automations/AutomationInspector.vue b/apps/web/src/components/automations/AutomationInspector.vue index d956272..5d026f5 100644 --- a/apps/web/src/components/automations/AutomationInspector.vue +++ b/apps/web/src/components/automations/AutomationInspector.vue @@ -28,6 +28,7 @@ import { AUTOMATION_TRIGGER_LABELS, conditionFieldsForTrigger, conditionOpsForField, + formConditionValues, isAutomationCondition, } from '@mento/shared'; @@ -127,6 +128,24 @@ function setConditionKey(step: AutomationStep, key: string) { step.conditionKey = key || null; const field = formFields.value.find((f) => f.key === key); step.conditionValue = field?.options?.[0] ?? ''; + step.conditionValues = step.conditionValue ? [step.conditionValue] : []; +} + +function toggleConditionValue(step: AutomationStep, option: string, on: boolean) { + const cur = formConditionValues(step).filter((v) => v !== option); + if (on) cur.push(option); + const order = conditionFormField.value?.options ?? []; + cur.sort((a, b) => order.indexOf(a) - order.indexOf(b)); + step.conditionValues = cur; + step.conditionValue = cur[0] ?? ''; +} + +function multiValueHint(op: AutomationConditionOp | undefined) { + if (op === 'contains_any') return 'کافی است حداقل یکی از این گزینه‌ها انتخاب شده باشد.'; + if (op === 'not_contains') return 'هیچ‌کدام از این گزینه‌ها نباید انتخاب شده باشد.'; + if (op === 'eq') return 'دقیقاً همین گزینه‌ها (نه بیشتر و نه کمتر) باید انتخاب شده باشند.'; + if (op === 'neq') return 'برقرار است مگر این‌که دقیقاً همین گزینه‌ها انتخاب شده باشند.'; + return 'همهٔ این گزینه‌ها باید با هم انتخاب شده باشند.'; } const activityTypes: Array<{ value: ActivityType; label: string }> = [ @@ -155,6 +174,7 @@ function setAction(step: AutomationStep, action: AutomationAction) { function setConditionField(step: AutomationStep, field: AutomationConditionField) { step.conditionField = field; step.conditionKey = null; + step.conditionValues = []; if (field === 'deal_amount') { step.conditionOp = 'gte'; step.conditionValue = typeof step.conditionValue === 'number' ? step.conditionValue : 0; @@ -289,12 +309,20 @@ watch(