@@ -0,0 +1,507 @@
< script setup lang = "ts" >
import { computed , onMounted , ref } from 'vue' ;
import axios from 'axios' ;
import { api } from '@/lib/api' ;
import type {
CreateMessageAutomationRuleRequest ,
MessageAutomationChannel ,
MessageAutomationDelayUnit ,
MessageAutomationRuleDto ,
MessageAutomationTarget ,
MessageAutomationTrigger ,
UpdateMessageAutomationRuleRequest ,
} from '@mento/shared' ;
import {
MESSAGE _AUTOMATION _CHANNEL _LABELS ,
MESSAGE _AUTOMATION _TARGET _LABELS ,
MESSAGE _AUTOMATION _TRIGGER _LABELS ,
MESSAGE _AUTOMATION _TRIGGERS ,
MESSAGE _AUTOMATION _UNIT _LABELS ,
} from '@mento/shared' ;
const loading = ref ( false ) ;
const saving = ref ( false ) ;
const error = ref ( '' ) ;
const ok = ref ( '' ) ;
const items = ref < MessageAutomationRuleDto [ ] > ( [ ] ) ;
const editingId = ref < string | null > ( null ) ;
const name = ref ( '' ) ;
const isActive = ref ( true ) ;
const trigger = ref < MessageAutomationTrigger > ( 'invoice_issued' ) ;
const startDelayValue = ref ( 0 ) ;
const startDelayUnit = ref < MessageAutomationDelayUnit > ( 'minutes' ) ;
const repeatCount = ref ( 1 ) ;
const intervalValue = ref ( 2 ) ;
const intervalUnit = ref < MessageAutomationDelayUnit > ( 'days' ) ;
const notifyTarget = ref < MessageAutomationTarget > ( 'person' ) ;
const channel = ref < MessageAutomationChannel > ( 'sms' ) ;
const title = ref ( '' ) ;
const body = ref ( '' ) ;
const personNote = computed (
( ) =>
notifyTarget . value !== 'staff' &&
'ناتیف داخل برنامه برای کارکنان است؛ برای مشتری پیامک ارسال میشود.' ,
) ;
function resetForm ( ) {
editingId . value = null ;
name . value = '' ;
isActive . value = true ;
trigger . value = 'invoice_issued' ;
startDelayValue . value = 0 ;
startDelayUnit . value = 'minutes' ;
repeatCount . value = 1 ;
intervalValue . value = 2 ;
intervalUnit . value = 'days' ;
notifyTarget . value = 'person' ;
channel . value = 'sms' ;
title . value = '' ;
body . value = '' ;
ok . value = '' ;
}
function startEdit ( row : MessageAutomationRuleDto ) {
editingId . value = row . id ;
name . value = row . name ;
isActive . value = row . isActive ;
trigger . value = row . trigger ;
startDelayValue . value = row . startDelayValue ;
startDelayUnit . value = row . startDelayUnit ;
repeatCount . value = row . repeatCount ;
intervalValue . value = row . intervalValue ;
intervalUnit . value = row . intervalUnit ;
notifyTarget . value = row . notifyTarget ;
channel . value = row . channel ;
title . value = row . title ;
body . value = row . body ;
ok . value = '' ;
}
function scheduleLabel ( row : MessageAutomationRuleDto ) {
const start =
row . startDelayValue === 0
? 'همان لحظه'
: ` ${ row . startDelayValue } ${ MESSAGE _AUTOMATION _UNIT _LABELS [ row . startDelayUnit ] } بعد ` ;
if ( row . repeatCount <= 1 ) return start ;
return ` ${ start } · ${ row . repeatCount } بار، هر ${ row . intervalValue } ${ MESSAGE _AUTOMATION _UNIT _LABELS [ row . intervalUnit ] } ` ;
}
async function load ( ) {
loading . value = true ;
error . value = '' ;
try {
const { data } = await api . get < MessageAutomationRuleDto [ ] > ( '/message-automations' ) ;
items . value = data ;
} catch ( e ) {
error . value = axios . isAxiosError ( e ) && ! e . response
? 'اتصال به API برقرار نشد.'
: 'بارگذاری قوانین پیام ناموفق بود.' ;
} finally {
loading . value = false ;
}
}
function payload ( ) : CreateMessageAutomationRuleRequest {
return {
name : name . value . trim ( ) ,
isActive : isActive . value ,
trigger : trigger . value ,
startDelayValue : startDelayValue . value ,
startDelayUnit : startDelayUnit . value ,
repeatCount : repeatCount . value ,
intervalValue : intervalValue . value ,
intervalUnit : intervalUnit . value ,
notifyTarget : notifyTarget . value ,
channel : channel . value ,
title : title . value . trim ( ) ,
body : body . value . trim ( ) ,
} ;
}
async function save ( ) {
if ( ! name . value . trim ( ) || ! title . value . trim ( ) || ! body . value . trim ( ) ) {
error . value = 'عنوان قانون، عنوان پیام و متن را وارد کنید.' ;
return ;
}
saving . value = true ;
error . value = '' ;
ok . value = '' ;
try {
if ( editingId . value ) {
const bodyDto : UpdateMessageAutomationRuleRequest = payload ( ) ;
await api . patch ( ` /message-automations/ ${ editingId . value } ` , bodyDto ) ;
ok . value = 'قانون بهروز شد.' ;
} else {
await api . post ( '/message-automations' , payload ( ) ) ;
ok . value = 'قانون ساخته شد.' ;
}
resetForm ( ) ;
await load ( ) ;
} catch {
error . value = 'ذخیره قانون ناموفق بود.' ;
} finally {
saving . value = false ;
}
}
async function toggle ( row : MessageAutomationRuleDto ) {
try {
await api . patch ( ` /message-automations/ ${ row . id } ` , { isActive : ! row . isActive } ) ;
await load ( ) ;
} catch {
error . value = 'تغییر وضعیت ناموفق بود.' ;
}
}
async function remove ( row : MessageAutomationRuleDto ) {
if ( ! confirm ( ` قانون « ${ row . name } » حذف شود؟ ` ) ) return ;
try {
await api . delete ( ` /message-automations/ ${ row . id } ` ) ;
if ( editingId . value === row . id ) resetForm ( ) ;
await load ( ) ;
} catch {
error . value = 'حذف قانون ناموفق بود.' ;
}
}
onMounted ( load ) ;
< / script >
< template >
< section class = "page" >
< header class = "page-head" >
< div class = "head-copy" >
< h1 > پیام خودکار < / h1 >
< p class = "lead" >
جدا از اتوماسیون گردش کار . فقط برای پیامک و ناتیف : چه اتفاقی بیفتد ، چند بار و با چه فاصله ای .
< / p >
< / div >
< / header >
< p v-if = "error" class="error" > {{ error }} < / p >
< p v-if = "ok" class="ok" > {{ ok }} < / p >
< form class = "panel" @submit.prevent ="save" >
< h2 > { { editingId ? 'ویرایش قانون' : 'قانون جدید' } } < / h2 >
< label class = "field" >
عنوان قانون
< input v-model = "name" maxlength="120" placeholder="مثلاً تشکر بعد از فاکتور" / >
< / label >
< label class = "field" >
چه زمانی شروع شود
< select v-model = "trigger" >
< option
v-for = "item in MESSAGE_AUTOMATION_TRIGGERS"
:key = "item"
:value = "item"
>
{ { MESSAGE _AUTOMATION _TRIGGER _LABELS [ item ] } }
< / option >
< / select >
< / label >
< div class = "row-fields" >
< label class = "field" >
شروع ارسال
< input v -model .number = " startDelayValue " type = "number" min = "0" max = "365" / >
< / label >
< label class = "field" >
واحد شروع
< select v-model = "startDelayUnit" >
< option value = "minutes" > دقیقه بعد < / option >
< option value = "hours" > ساعت بعد < / option >
< option value = "days" > روز بعد < / option >
< / select >
< / label >
< label class = "field" >
تعداد ارسال
< input v -model .number = " repeatCount " type = "number" min = "1" max = "10" / >
< / label >
< / div >
< div v-if = "repeatCount > 1" class="row-fields" >
< label class = "field" >
فاصله بین پیام ها
< input v -model .number = " intervalValue " type = "number" min = "1" max = "365" / >
< / label >
< label class = "field" >
واحد فاصله
< select v-model = "intervalUnit" >
< option value = "minutes" > دقیقه < / option >
< option value = "hours" > ساعت < / option >
< option value = "days" > روز < / option >
< / select >
< / label >
< / div >
< p class = "hint" >
صفر یعنی همان لحظه . برای چندباره : مثلا ً ۳ بار ، هر ۲ روز .
< / p >
< div class = "row-fields" >
< label class = "field" >
برای چه کسی
< select v-model = "notifyTarget" >
< option value = "person" > مشتری < / option >
< option value = "staff" > مسئول پرونده < / option >
< option value = "both" > مشتری و مسئول < / option >
< / select >
< / label >
< label class = "field" >
از چه راهی
< select v-model = "channel" >
< option value = "sms" > پیامک < / option >
< option value = "app" > ناتیف داخل برنامه < / option >
< option value = "both" > پیامک و ناتیف < / option >
< / select >
< / label >
< / div >
< p v-if = "personNote" class="hint" > {{ personNote }} < / p >
< label class = "field" >
عنوان پیام
< input v-model = "title" maxlength="120" placeholder="عنوان ناتیف یا ابتدای پیامک" / >
< / label >
< label class = "field" >
متن پیام
< textarea
v-model = "body"
maxlength = "500"
placeholder = "میتوانید {title} را برای نام همان رکورد بگذارید"
/ >
< / label >
< label class = "check" >
< input v-model = "isActive" type="checkbox" / >
قانون فعال باشد
< / label >
< div class = "form-actions" >
< button class = "btn primary" type = "submit" :disabled = "saving" >
{ { saving ? '…' : editingId ? 'ذخیره تغییرات' : 'ساخت قانون' } }
< / button >
< button v-if = "editingId" class="btn ghost" type="button" @click="resetForm" >
انصراف
< / button >
< / div >
< / form >
< div class = "list" >
< article v-for = "row in items" :key="row.id" class="card" :class="{ off: !row.isActive }" >
< header >
< strong > { { row . name } } < / strong >
< button
class = "switch"
type = "button"
:aria-pressed = "row.isActive"
@click ="toggle(row)"
>
{ { row . isActive ? 'فعال' : 'خاموش' } }
< / button >
< / header >
< p >
{ { MESSAGE _AUTOMATION _TRIGGER _LABELS [ row . trigger ] } }
·
{ { scheduleLabel ( row ) } }
< / p >
< p class = "muted" >
{ { MESSAGE _AUTOMATION _TARGET _LABELS [ row . notifyTarget ] } }
·
{ { MESSAGE _AUTOMATION _CHANNEL _LABELS [ row . channel ] } }
< / p >
< p class = "preview" > { { row . body } } < / p >
< div class = "card-actions" >
< button class = "btn ghost" type = "button" @click ="startEdit(row)" > ویرایش < / button >
< button class = "btn danger" type = "button" @click ="remove(row)" > حذف < / button >
< / div >
< / article >
< p v-if = "!loading && !items.length" class="empty" > هنوز قانون پیامی نیست. < / p >
< / div >
< / section >
< / template >
< style scoped >
. page {
display : grid ;
gap : 1 rem ;
max - width : 760 px ;
min - width : 0 ;
}
. page - head {
display : flex ;
justify - content : space - between ;
gap : 1 rem ;
}
h1 {
margin : 0 0 0.3 rem ;
font - size : clamp ( 1.45 rem , 2.5 vw , 1.85 rem ) ;
font - weight : 800 ;
}
h2 {
margin : 0 ;
font - size : 1.05 rem ;
font - weight : 700 ;
}
. lead ,
. muted ,
. hint ,
. empty {
margin : 0 ;
color : # 64748 b ;
}
. hint {
font - size : 0.82 rem ;
}
. panel ,
. list {
display : grid ;
gap : 0.75 rem ;
}
. panel {
background : # fff ;
border : 1 px solid # e2e8f0 ;
border - radius : 18 px ;
box - shadow : 0 8 px 22 px rgba ( 15 , 23 , 42 , 0.04 ) ;
padding : 1.1 rem ;
}
. field {
display : grid ;
gap : 0.35 rem ;
color : # 64748 b ;
font - size : 0.9 rem ;
}
. row - fields {
display : flex ;
flex - wrap : wrap ;
gap : 0.75 rem ;
}
. row - fields . field {
flex : 1 ;
min - width : 8 rem ;
}
input ,
select ,
textarea {
border : 1 px solid # e2e8f0 ;
border - radius : var ( -- radius ) ;
min - height : var ( -- control - h ) ;
padding : 0 0.85 rem ;
font : inherit ;
background : # fff ;
width : 100 % ;
box - sizing : border - box ;
color : # 0 f172a ;
}
textarea {
min - height : 5.5 rem ;
padding : 0.65 rem 0.85 rem ;
}
. check {
display : flex ;
align - items : center ;
gap : 0.4 rem ;
color : # 334155 ;
}
. form - actions ,
. card - actions {
display : flex ;
gap : 0.5 rem ;
flex - wrap : wrap ;
}
. btn {
display : inline - flex ;
align - items : center ;
justify - content : center ;
min - height : var ( -- control - h ) ;
padding : 0 0.95 rem ;
border - radius : var ( -- radius ) ;
font - weight : 700 ;
border : 1 px solid transparent ;
cursor : pointer ;
font : inherit ;
}
. btn . primary {
background : # 14 b8a6 ;
color : # fff ;
}
. btn . ghost {
background : # fff ;
color : # 0 f766e ;
border - color : # 99 f6e4 ;
}
. btn . danger {
background : # fff ;
color : # b91c1c ;
border - color : # fecaca ;
}
. card {
background : # fff ;
border : 1 px solid # e2e8f0 ;
border - radius : 18 px ;
padding : 1 rem ;
display : grid ;
gap : 0.35 rem ;
}
. card . off {
opacity : 0.7 ;
}
. card header {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 0.75 rem ;
}
. card p {
margin : 0 ;
}
. preview {
color : # 334155 ;
font - size : 0.88 rem ;
}
. switch {
border : 0 ;
background : # ecfdf8 ;
color : # 0 f766e ;
font : inherit ;
font - weight : 800 ;
font - size : 0.78 rem ;
border - radius : 999 px ;
padding : 0.3 rem 0.7 rem ;
cursor : pointer ;
}
. card . off . switch {
background : # f1f5f9 ;
color : # 64748 b ;
}
. error {
margin : 0 ;
color : # b91c1c ;
}
. ok {
margin : 0 ;
color : # 0 f766e ;
font - weight : 600 ;
}
< / style >