339 lines
9.5 KiB
Vue
339 lines
9.5 KiB
Vue
<script setup lang="ts">
|
||||
|
|
import { computed, watch } from 'vue';
|
|||
|
|
import type {
|
|||
|
|
ActivityType,
|
|||
|
|
AutomationAction,
|
|||
|
|
AutomationConditionField,
|
|||
|
|
AutomationConditionOp,
|
|||
|
|
AutomationStep,
|
|||
|
|
AutomationTrigger,
|
|||
|
|
OrgUserDto,
|
|||
|
|
TicketPriority,
|
|||
|
|
} from '@mento/shared';
|
|||
|
|
import {
|
|||
|
|
AUTOMATION_ACTION_LABELS,
|
|||
|
|
AUTOMATION_ACTIONS,
|
|||
|
|
AUTOMATION_CONDITION_FIELD_LABELS,
|
|||
|
|
AUTOMATION_CONDITION_OP_LABELS,
|
|||
|
|
AUTOMATION_NOTIFY_CHANNEL_LABELS,
|
|||
|
|
AUTOMATION_NOTIFY_CHANNELS,
|
|||
|
|
AUTOMATION_TRIGGER_LABELS,
|
|||
|
|
AUTOMATION_TRIGGERS,
|
|||
|
|
conditionFieldsForTrigger,
|
|||
|
|
isAutomationCondition,
|
|||
|
|
} from '@mento/shared';
|
|||
|
|
|
|||
|
|
const props = defineProps<{
|
|||
|
|
selected: 'trigger' | string;
|
|||
|
|
users: OrgUserDto[];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const trigger = defineModel<AutomationTrigger>('trigger', { required: true });
|
|||
|
|
const steps = defineModel<AutomationStep[]>('steps', { required: true });
|
|||
|
|
|
|||
|
|
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),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const conditionFields = computed(() => conditionFieldsForTrigger(trigger.value));
|
|||
|
|
|
|||
|
|
const conditionOps = computed((): AutomationConditionOp[] => {
|
|||
|
|
const field = selectedStep.value?.conditionField;
|
|||
|
|
if (field === 'deal_amount') return ['gte', 'lte', 'eq', 'neq'];
|
|||
|
|
if (field === 'ticket_priority') return ['eq', 'neq'];
|
|||
|
|
return ['is_set', 'is_empty'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
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';
|
|||
|
|
if (action === 'create_notification' && !step.notifyChannel) step.notifyChannel = 'app';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setConditionField(step: AutomationStep, field: AutomationConditionField) {
|
|||
|
|
step.conditionField = field;
|
|||
|
|
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';
|
|||
|
|
} 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">
|
|||
|
|
<option v-for="t in AUTOMATION_TRIGGERS" :key="t" :value="t">
|
|||
|
|
{{ AUTOMATION_TRIGGER_LABELS[t] }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</label>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<template v-else-if="selectedStep && isCondition">
|
|||
|
|
<h2>گره شرط</h2>
|
|||
|
|
<p class="hint">
|
|||
|
|
اگر شرط برقرار نباشد، عملهای بعد از این گره اجرا نمیشوند.
|
|||
|
|
</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>
|
|||
|
|
<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>
|
|||
|
|
</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>
|
|||
|
|
<label v-if="selectedStep.action === 'create_activity'">
|
|||
|
|
مسئول فعالیت
|
|||
|
|
<select v-model="selectedStep.assigneeId">
|
|||
|
|
<option :value="null">مسئول معامله / تیکت</option>
|
|||
|
|
<option v-for="u in users" :key="u.id" :value="u.id">
|
|||
|
|
{{ u.fullName }}
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<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;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h2 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 0.98rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
</style>
|