Files
CRM_Montzar/apps/web/src/components/automations/AutomationInspector.vue
T

339 lines
9.5 KiB
Vue
Raw Normal View History

<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>