2026-09-09 13:12:13 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, watch } from 'vue';
|
|
|
|
|
|
import type {
|
|
|
|
|
|
ActivityType,
|
2026-10-11 14:12:36 +03:30
|
|
|
|
ActivityTypeDefDto,
|
2026-09-09 13:12:13 +03:30
|
|
|
|
AutomationAction,
|
2026-10-11 14:12:36 +03:30
|
|
|
|
AutomationAssignMode,
|
|
|
|
|
|
AutomationChainMode,
|
2026-09-09 13:12:13 +03:30
|
|
|
|
AutomationConditionField,
|
|
|
|
|
|
AutomationConditionOp,
|
|
|
|
|
|
AutomationStep,
|
|
|
|
|
|
AutomationTrigger,
|
2026-10-11 14:12:36 +03:30
|
|
|
|
LeadFormDto,
|
2026-09-09 13:12:13 +03:30
|
|
|
|
OrgUserDto,
|
2026-10-11 14:12:36 +03:30
|
|
|
|
TeamDto,
|
2026-09-09 13:12:13 +03:30
|
|
|
|
TicketPriority,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
import {
|
|
|
|
|
|
AUTOMATION_ACTION_LABELS,
|
|
|
|
|
|
AUTOMATION_ACTIONS,
|
2026-10-11 14:12:36 +03:30
|
|
|
|
AUTOMATION_ASSIGN_MODE_LABELS,
|
|
|
|
|
|
AUTOMATION_CHAIN_MODE_LABELS,
|
2026-09-09 13:12:13 +03:30
|
|
|
|
AUTOMATION_CONDITION_FIELD_LABELS,
|
|
|
|
|
|
AUTOMATION_CONDITION_OP_LABELS,
|
|
|
|
|
|
AUTOMATION_NOTIFY_CHANNEL_LABELS,
|
|
|
|
|
|
AUTOMATION_NOTIFY_CHANNELS,
|
2026-09-09 21:43:17 +03:30
|
|
|
|
AUTOMATION_TRIGGER_GROUPS,
|
2026-09-09 13:12:13 +03:30
|
|
|
|
AUTOMATION_TRIGGER_LABELS,
|
|
|
|
|
|
conditionFieldsForTrigger,
|
2026-10-11 14:12:36 +03:30
|
|
|
|
conditionOpsForField,
|
2026-09-09 13:12:13 +03:30
|
|
|
|
isAutomationCondition,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
selected: 'trigger' | string;
|
|
|
|
|
|
users: OrgUserDto[];
|
2026-10-11 14:12:36 +03:30
|
|
|
|
forms: LeadFormDto[];
|
|
|
|
|
|
teams: TeamDto[];
|
|
|
|
|
|
activityTypeDefs: ActivityTypeDefDto[];
|
2026-10-11 14:48:12 +03:30
|
|
|
|
advanced: boolean;
|
2026-09-09 13:12:13 +03:30
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const trigger = defineModel<AutomationTrigger>('trigger', { required: true });
|
|
|
|
|
|
const steps = defineModel<AutomationStep[]>('steps', { required: true });
|
2026-10-11 14:12:36 +03:30
|
|
|
|
const sourceId = defineModel<string | null>('sourceId', { default: null });
|
|
|
|
|
|
const chainMode = defineModel<AutomationChainMode>('chainMode', { default: 'branches' });
|
2026-09-09 13:12:13 +03:30
|
|
|
|
|
|
|
|
|
|
const selectedStep = computed(() => {
|
|
|
|
|
|
if (props.selected === 'trigger') return null;
|
|
|
|
|
|
return steps.value.find((s) => s.id === props.selected) ?? null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const isCondition = computed(
|
|
|
|
|
|
() => !!selectedStep.value && isAutomationCondition(selectedStep.value),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-10-11 14:48:12 +03:30
|
|
|
|
const conditionFields = computed(() =>
|
|
|
|
|
|
conditionFieldsForTrigger(trigger.value).filter(
|
|
|
|
|
|
(f) =>
|
|
|
|
|
|
props.advanced || f !== 'form_field' || selectedStep.value?.conditionField === f,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-09-09 13:12:13 +03:30
|
|
|
|
|
2026-10-11 14:12:36 +03:30
|
|
|
|
const conditionOps = computed((): AutomationConditionOp[] =>
|
|
|
|
|
|
conditionOpsForField(selectedStep.value?.conditionField),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const selectedForm = computed(() =>
|
|
|
|
|
|
sourceId.value ? props.forms.find((f) => f.id === sourceId.value) ?? null : null,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const formFields = computed(() =>
|
|
|
|
|
|
(selectedForm.value?.fields ?? []).filter((f) => f.fieldType !== 'heading'),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const conditionFormField = computed(() => {
|
|
|
|
|
|
const key = selectedStep.value?.conditionKey;
|
|
|
|
|
|
return key ? formFields.value.find((f) => f.key === key) ?? null : null;
|
2026-09-09 13:12:13 +03:30
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-11 14:12:36 +03:30
|
|
|
|
const needsConditionValue = computed(() => {
|
|
|
|
|
|
const op = selectedStep.value?.conditionOp;
|
|
|
|
|
|
return op !== 'is_set' && op !== 'is_empty';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-11 14:48:12 +03:30
|
|
|
|
const assignModes = computed(() =>
|
|
|
|
|
|
(Object.keys(AUTOMATION_ASSIGN_MODE_LABELS) as AutomationAssignMode[]).filter(
|
|
|
|
|
|
(m) =>
|
|
|
|
|
|
props.advanced ||
|
|
|
|
|
|
(m !== 'team_round_robin' && m !== 'users_round_robin') ||
|
|
|
|
|
|
selectedStep.value?.assignMode === m,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-10-11 14:12:36 +03:30
|
|
|
|
|
|
|
|
|
|
const dueUnits = [
|
|
|
|
|
|
{ value: 'now', label: 'همان لحظه (امروز)' },
|
|
|
|
|
|
{ value: 'hours', label: 'چند ساعت بعد' },
|
|
|
|
|
|
{ value: 'days', label: 'چند روز بعد' },
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
type DueUnit = (typeof dueUnits)[number]['value'];
|
|
|
|
|
|
|
|
|
|
|
|
function dueUnitOf(step: AutomationStep): DueUnit {
|
|
|
|
|
|
const m = step.dueOffsetMinutes ?? 0;
|
|
|
|
|
|
if (!m) return 'now';
|
|
|
|
|
|
return m % 1440 === 0 ? 'days' : 'hours';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function dueAmountOf(step: AutomationStep): number {
|
|
|
|
|
|
const m = step.dueOffsetMinutes ?? 0;
|
|
|
|
|
|
if (!m) return 0;
|
|
|
|
|
|
return m % 1440 === 0 ? m / 1440 : Math.round(m / 60);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setDue(step: AutomationStep, unit: DueUnit, amount: number) {
|
|
|
|
|
|
const n = Math.max(1, Math.trunc(Number(amount) || 1));
|
|
|
|
|
|
step.dueOffsetMinutes = unit === 'now' ? 0 : unit === 'days' ? n * 1440 : n * 60;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleUser(step: AutomationStep, id: string, on: boolean) {
|
|
|
|
|
|
const cur = new Set(step.assigneeUserIds ?? []);
|
|
|
|
|
|
if (on) cur.add(id);
|
|
|
|
|
|
else cur.delete(id);
|
|
|
|
|
|
step.assigneeUserIds = [...cur];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setConditionKey(step: AutomationStep, key: string) {
|
|
|
|
|
|
step.conditionKey = key || null;
|
|
|
|
|
|
const field = formFields.value.find((f) => f.key === key);
|
|
|
|
|
|
step.conditionValue = field?.options?.[0] ?? '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 13:12:13 +03:30
|
|
|
|
const activityTypes: Array<{ value: ActivityType; label: string }> = [
|
|
|
|
|
|
{ value: 'task', label: 'وظیفه' },
|
|
|
|
|
|
{ value: 'call', label: 'تماس' },
|
|
|
|
|
|
{ value: 'meeting', label: 'جلسه' },
|
|
|
|
|
|
{ value: 'note', label: 'یادداشت' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const ticketPriorities: Array<{ value: TicketPriority; label: string }> = [
|
|
|
|
|
|
{ value: 'low', label: 'کم' },
|
|
|
|
|
|
{ value: 'normal', label: 'عادی' },
|
|
|
|
|
|
{ value: 'high', label: 'بالا' },
|
|
|
|
|
|
{ value: 'urgent', label: 'فوری' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function setAction(step: AutomationStep, action: AutomationAction) {
|
|
|
|
|
|
step.kind = 'action';
|
|
|
|
|
|
step.action = action;
|
|
|
|
|
|
if (action === 'add_loyalty_points' && !step.points) step.points = 100;
|
|
|
|
|
|
if (action === 'create_activity' && !step.activityType) step.activityType = 'task';
|
2026-10-11 14:12:36 +03:30
|
|
|
|
if (action === 'create_activity' && !step.assignMode) step.assignMode = 'owner';
|
2026-09-09 13:12:13 +03:30
|
|
|
|
if (action === 'create_notification' && !step.notifyChannel) step.notifyChannel = 'app';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setConditionField(step: AutomationStep, field: AutomationConditionField) {
|
|
|
|
|
|
step.conditionField = field;
|
2026-10-11 14:12:36 +03:30
|
|
|
|
step.conditionKey = null;
|
2026-09-09 13:12:13 +03:30
|
|
|
|
if (field === 'deal_amount') {
|
|
|
|
|
|
step.conditionOp = 'gte';
|
|
|
|
|
|
step.conditionValue = typeof step.conditionValue === 'number' ? step.conditionValue : 0;
|
|
|
|
|
|
} else if (field === 'ticket_priority') {
|
|
|
|
|
|
step.conditionOp = 'eq';
|
|
|
|
|
|
step.conditionValue = 'normal';
|
2026-10-11 14:12:36 +03:30
|
|
|
|
} else if (field === 'form_field') {
|
|
|
|
|
|
step.conditionOp = 'contains';
|
|
|
|
|
|
step.conditionValue = '';
|
2026-09-09 13:12:13 +03:30
|
|
|
|
} else {
|
|
|
|
|
|
step.conditionOp = 'is_set';
|
|
|
|
|
|
step.conditionValue = undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
selectedStep,
|
|
|
|
|
|
(step) => {
|
|
|
|
|
|
if (!step || isAutomationCondition(step) || step.action !== 'create_notification') return;
|
|
|
|
|
|
if (!step.notifyChannel) step.notifyChannel = 'app';
|
|
|
|
|
|
if (!step.notifyTarget) step.notifyTarget = 'staff';
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
);
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<aside class="inspector">
|
|
|
|
|
|
<template v-if="selected === 'trigger'">
|
|
|
|
|
|
<h2>گره محرک</h2>
|
|
|
|
|
|
<p class="hint">وقتی این اتفاق بیفتد، زنجیره از راست به چپ اجرا میشود.</p>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
اگر
|
|
|
|
|
|
<select v-model="trigger">
|
2026-09-09 21:43:17 +03:30
|
|
|
|
<optgroup
|
|
|
|
|
|
v-for="group in AUTOMATION_TRIGGER_GROUPS"
|
|
|
|
|
|
:key="group.label"
|
|
|
|
|
|
:label="group.label"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option v-for="t in group.items" :key="t" :value="t">
|
|
|
|
|
|
{{ AUTOMATION_TRIGGER_LABELS[t] }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</optgroup>
|
2026-09-09 13:12:13 +03:30
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2026-10-11 14:12:36 +03:30
|
|
|
|
<label v-if="trigger === 'lead_submitted'">
|
|
|
|
|
|
فرم
|
|
|
|
|
|
<select v-model="sourceId">
|
|
|
|
|
|
<option :value="null">همه فرمها</option>
|
|
|
|
|
|
<option v-for="f in forms" :key="f.id" :value="f.id">{{ f.name }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p v-if="trigger === 'lead_submitted' && !sourceId" class="hint">
|
|
|
|
|
|
برای شرط روی پاسخهای فرم، یک فرم مشخص را انتخاب کنید.
|
|
|
|
|
|
</p>
|
2026-10-11 14:48:12 +03:30
|
|
|
|
<label v-if="advanced || chainMode === 'branches'">
|
2026-10-11 14:12:36 +03:30
|
|
|
|
رفتار شرطها
|
|
|
|
|
|
<select v-model="chainMode">
|
|
|
|
|
|
<option value="branches">{{ AUTOMATION_CHAIN_MODE_LABELS.branches }}</option>
|
|
|
|
|
|
<option value="stop">{{ AUTOMATION_CHAIN_MODE_LABELS.stop }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p v-if="chainMode === 'branches'" class="hint">
|
2026-10-11 14:48:12 +03:30
|
|
|
|
شاخهها روی بوم موازی نمایش داده میشوند و هر کدام جدا بررسی میشود. شرطهای یک شاخه با «و»
|
|
|
|
|
|
ترکیب میشوند؛ مثلاً اگر دو گزینه تیک بخورد، هر دو شاخه اجرا میشود. برای شاخهٔ تازه از
|
|
|
|
|
|
دکمهٔ «شاخهٔ جدید» زیر بوم استفاده کنید.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p v-else-if="!advanced" class="hint">
|
|
|
|
|
|
اتوماسیون ساده: شرطها پشتسرهم بررسی میشوند و اولین شرط ناموفق بقیهٔ زنجیره را متوقف
|
|
|
|
|
|
میکند. اتوماسیون چندشاخه در نسخهٔ حرفهای فعال است.
|
2026-10-11 14:12:36 +03:30
|
|
|
|
</p>
|
2026-09-09 13:12:13 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="selectedStep && isCondition">
|
|
|
|
|
|
<h2>گره شرط</h2>
|
|
|
|
|
|
<p class="hint">
|
2026-10-11 14:12:36 +03:30
|
|
|
|
{{
|
|
|
|
|
|
chainMode === 'branches'
|
|
|
|
|
|
? 'اگر شرط برقرار نباشد، فقط عملهای این شاخه (تا شرط بعدی) اجرا نمیشوند.'
|
|
|
|
|
|
: 'اگر شرط برقرار نباشد، عملهای بعد از این گره اجرا نمیشوند.'
|
|
|
|
|
|
}}
|
2026-09-09 13:12:13 +03:30
|
|
|
|
</p>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
فیلد
|
|
|
|
|
|
<select
|
|
|
|
|
|
:value="selectedStep.conditionField"
|
|
|
|
|
|
@change="
|
|
|
|
|
|
setConditionField(
|
|
|
|
|
|
selectedStep,
|
|
|
|
|
|
($event.target as HTMLSelectElement).value as AutomationConditionField,
|
|
|
|
|
|
)
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option v-for="f in conditionFields" :key="f" :value="f">
|
|
|
|
|
|
{{ AUTOMATION_CONDITION_FIELD_LABELS[f] }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2026-10-11 14:12:36 +03:30
|
|
|
|
<template v-if="selectedStep.conditionField === 'form_field'">
|
|
|
|
|
|
<p v-if="!selectedForm" class="hint warn">
|
|
|
|
|
|
ابتدا در گره محرک یک فرم مشخص انتخاب کنید.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<label v-else>
|
|
|
|
|
|
فیلد فرم
|
|
|
|
|
|
<select
|
|
|
|
|
|
:value="selectedStep.conditionKey ?? ''"
|
|
|
|
|
|
@change="setConditionKey(selectedStep, ($event.target as HTMLSelectElement).value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="" disabled>انتخاب فیلد…</option>
|
|
|
|
|
|
<option v-for="f in formFields" :key="f.key" :value="f.key">{{ f.label }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</template>
|
2026-09-09 13:12:13 +03:30
|
|
|
|
<label>
|
|
|
|
|
|
مقایسه
|
|
|
|
|
|
<select v-model="selectedStep.conditionOp">
|
|
|
|
|
|
<option v-for="op in conditionOps" :key="op" :value="op">
|
|
|
|
|
|
{{ AUTOMATION_CONDITION_OP_LABELS[op] }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label v-if="selectedStep.conditionField === 'deal_amount'">
|
|
|
|
|
|
مبلغ
|
|
|
|
|
|
<input v-model.number="selectedStep.conditionValue" type="number" min="0" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label v-if="selectedStep.conditionField === 'ticket_priority'">
|
|
|
|
|
|
اولویت
|
|
|
|
|
|
<select v-model="selectedStep.conditionValue">
|
|
|
|
|
|
<option v-for="p in ticketPriorities" :key="p.value" :value="p.value">
|
|
|
|
|
|
{{ p.label }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2026-10-11 14:12:36 +03:30
|
|
|
|
<template v-if="selectedStep.conditionField === 'form_field' && needsConditionValue">
|
|
|
|
|
|
<label v-if="conditionFormField?.options?.length">
|
|
|
|
|
|
گزینه
|
|
|
|
|
|
<select v-model="selectedStep.conditionValue">
|
|
|
|
|
|
<option v-for="o in conditionFormField.options" :key="o" :value="o">{{ o }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label v-else>
|
|
|
|
|
|
مقدار
|
|
|
|
|
|
<input v-model.trim="selectedStep.conditionValue" type="text" maxlength="200" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</template>
|
2026-09-09 13:12:13 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="selectedStep">
|
|
|
|
|
|
<h2>گره عمل</h2>
|
|
|
|
|
|
<p class="hint">بعد از محرک (و شرطها)، این کار به ترتیب انجام میشود.</p>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
آنگاه
|
|
|
|
|
|
<select
|
|
|
|
|
|
:value="selectedStep.action"
|
|
|
|
|
|
@change="
|
|
|
|
|
|
setAction(
|
|
|
|
|
|
selectedStep,
|
|
|
|
|
|
($event.target as HTMLSelectElement).value as AutomationAction,
|
|
|
|
|
|
)
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option v-for="a in AUTOMATION_ACTIONS" :key="a" :value="a">
|
|
|
|
|
|
{{ AUTOMATION_ACTION_LABELS[a] }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
عنوان
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="selectedStep.title"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
maxlength="200"
|
|
|
|
|
|
placeholder="اختیاری"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
توضیح / متن پیام
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="selectedStep.body"
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
maxlength="1000"
|
|
|
|
|
|
placeholder="اختیاری"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<label v-if="selectedStep.action === 'create_activity'">
|
|
|
|
|
|
نوع فعالیت
|
|
|
|
|
|
<select v-model="selectedStep.activityType">
|
|
|
|
|
|
<option v-for="t in activityTypes" :key="t.value" :value="t.value">
|
|
|
|
|
|
{{ t.label }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2026-10-11 14:12:36 +03:30
|
|
|
|
<template v-if="selectedStep.action === 'create_activity'">
|
|
|
|
|
|
<label v-if="activityTypeDefs.length">
|
|
|
|
|
|
نوع سفارشی (اختیاری)
|
|
|
|
|
|
<select v-model="selectedStep.activityTypeDefId">
|
|
|
|
|
|
<option :value="null">— بدون نوع سفارشی —</option>
|
|
|
|
|
|
<option v-for="t in activityTypeDefs" :key="t.id" :value="t.id">{{ t.name }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
تخصیص به
|
|
|
|
|
|
<select v-model="selectedStep.assignMode">
|
|
|
|
|
|
<option v-for="m in assignModes" :key="m" :value="m">
|
|
|
|
|
|
{{ AUTOMATION_ASSIGN_MODE_LABELS[m] }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label v-if="selectedStep.assignMode === 'user'">
|
|
|
|
|
|
کاربر
|
|
|
|
|
|
<select v-model="selectedStep.assigneeId">
|
|
|
|
|
|
<option :value="null" disabled>انتخاب کاربر…</option>
|
|
|
|
|
|
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.fullName }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<template v-else-if="selectedStep.assignMode === 'team_round_robin'">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
تیم
|
|
|
|
|
|
<select v-model="selectedStep.assigneeTeamId">
|
|
|
|
|
|
<option :value="null" disabled>انتخاب تیم…</option>
|
|
|
|
|
|
<option v-for="t in teams" :key="t.id" :value="t.id">
|
|
|
|
|
|
{{ t.name }} ({{ t.memberCount }} نفر)
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p v-if="!teams.length" class="hint warn">
|
|
|
|
|
|
هنوز تیمی تعریف نشده؛ از «تنظیمات ← تیمها و سطح دسترسی» بسازید.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div v-else-if="selectedStep.assignMode === 'users_round_robin'" class="user-picks">
|
|
|
|
|
|
<p class="field-label">کاربران</p>
|
|
|
|
|
|
<label v-for="u in users" :key="u.id" class="ch">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
:checked="(selectedStep.assigneeUserIds ?? []).includes(u.id)"
|
|
|
|
|
|
@change="toggleUser(selectedStep, u.id, ($event.target as HTMLInputElement).checked)"
|
|
|
|
|
|
/>
|
2026-09-09 13:12:13 +03:30
|
|
|
|
{{ u.fullName }}
|
2026-10-11 14:12:36 +03:30
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-if="selectedStep.assignMode === 'team_round_robin' || selectedStep.assignMode === 'users_round_robin'"
|
|
|
|
|
|
class="hint"
|
|
|
|
|
|
>
|
|
|
|
|
|
هر بار نفر بعدی به نوبت انتخاب میشود تا کارها مساوی تقسیم شوند.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="due">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
موعد
|
|
|
|
|
|
<select
|
|
|
|
|
|
:value="dueUnitOf(selectedStep)"
|
|
|
|
|
|
@change="
|
|
|
|
|
|
setDue(
|
|
|
|
|
|
selectedStep,
|
|
|
|
|
|
($event.target as HTMLSelectElement).value as DueUnit,
|
|
|
|
|
|
dueAmountOf(selectedStep) || 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option v-for="u in dueUnits" :key="u.value" :value="u.value">{{ u.label }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label v-if="dueUnitOf(selectedStep) !== 'now'">
|
|
|
|
|
|
تعداد
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
:value="dueAmountOf(selectedStep)"
|
|
|
|
|
|
@input="
|
|
|
|
|
|
setDue(
|
|
|
|
|
|
selectedStep,
|
|
|
|
|
|
dueUnitOf(selectedStep),
|
|
|
|
|
|
Number(($event.target as HTMLInputElement).value),
|
|
|
|
|
|
)
|
|
|
|
|
|
"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<p v-if="selectedStep.action === 'create_activity' || selectedStep.action === 'create_notification'" class="hint">
|
|
|
|
|
|
در عنوان و متن میتوانید از
|
|
|
|
|
|
<code v-pre>{{title}}</code>، <code v-pre>{{mobile}}</code>، <code v-pre>{{form}}</code> و
|
|
|
|
|
|
<code v-pre>{{field.کلید}}</code> استفاده کنید.
|
|
|
|
|
|
<template v-if="formFields.length">
|
|
|
|
|
|
کلیدها:
|
|
|
|
|
|
<span v-for="(f, i) in formFields" :key="f.key">
|
|
|
|
|
|
<code>{{ f.key }}</code> ({{ f.label }})<template v-if="i < formFields.length - 1">، </template>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</p>
|
2026-09-09 13:12:13 +03:30
|
|
|
|
|
|
|
|
|
|
<template v-if="selectedStep.action === 'create_notification'">
|
|
|
|
|
|
<p class="field-label">نحوه ارسال</p>
|
|
|
|
|
|
<div class="channels">
|
|
|
|
|
|
<label
|
|
|
|
|
|
v-for="ch in AUTOMATION_NOTIFY_CHANNELS"
|
|
|
|
|
|
:key="ch"
|
|
|
|
|
|
class="ch"
|
|
|
|
|
|
>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="selectedStep.notifyChannel"
|
|
|
|
|
|
type="radio"
|
|
|
|
|
|
:value="ch"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{{ AUTOMATION_NOTIFY_CHANNEL_LABELS[ch] }}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
گیرنده
|
|
|
|
|
|
<select v-model="selectedStep.notifyTarget">
|
|
|
|
|
|
<option value="staff">کاربر تیم</option>
|
|
|
|
|
|
<option value="person">مخاطب معامله / تیکت</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label v-if="selectedStep.notifyTarget !== 'person'">
|
|
|
|
|
|
کاربر
|
|
|
|
|
|
<select v-model="selectedStep.notifyUserId">
|
|
|
|
|
|
<option :value="null">مسئول معامله / تیکت</option>
|
|
|
|
|
|
<option v-for="u in users" :key="u.id" :value="u.id">
|
|
|
|
|
|
{{ u.fullName }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p v-if="selectedStep.notifyTarget === 'person'" class="hint">
|
|
|
|
|
|
پیامک به موبایل مخاطب ارسال میشود. ناتیفیکیشن داخلی برای مخاطب نیست.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-else-if="selectedStep.notifyChannel === 'sms' || selectedStep.notifyChannel === 'both'"
|
|
|
|
|
|
class="hint"
|
|
|
|
|
|
>
|
|
|
|
|
|
پیامک به موبایل همان کاربر ارسال میشود.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<label v-if="selectedStep.action === 'add_loyalty_points'">
|
|
|
|
|
|
امتیاز
|
|
|
|
|
|
<input v-model.number="selectedStep.points" type="number" min="1" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-else class="hint">یک گره را از بوم انتخاب کنید.</p>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.inspector {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
align-content: start;
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-09 13:12:13 +03:30
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h2 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 0.98rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
font-weight: 600;
|
2026-09-09 13:12:13 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hint {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
label,
|
|
|
|
|
|
.field-label {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
font-size: 0.84rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-label {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
input,
|
|
|
|
|
|
select,
|
|
|
|
|
|
textarea {
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-09 13:12:13 +03:30
|
|
|
|
padding: 0.55rem 0.7rem;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.channels {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.55rem 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ch {
|
|
|
|
|
|
display: inline-flex !important;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.32rem;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ch input {
|
|
|
|
|
|
accent-color: #14b8a6;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
2026-10-11 14:12:36 +03:30
|
|
|
|
|
|
|
|
|
|
.hint.warn {
|
|
|
|
|
|
color: #b45309;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hint code {
|
|
|
|
|
|
font-size: 0.76rem;
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 0 0.25rem;
|
|
|
|
|
|
direction: ltr;
|
|
|
|
|
|
unicode-bidi: embed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-picks {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
max-height: 12rem;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.due {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr auto;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
align-items: end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.due input {
|
|
|
|
|
|
width: 5.5rem;
|
|
|
|
|
|
}
|
2026-09-09 13:12:13 +03:30
|
|
|
|
</style>
|