TimePicker closes on select without Ok button; send log shows skipped entries from cooldown/max limits. Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
6.7 KiB
Vue
232 lines
6.7 KiB
Vue
<template>
|
|
<div class="flex h-full w-full">
|
|
<tbl
|
|
ref="tableRef"
|
|
class="h-full w-full"
|
|
@add="openAdd"
|
|
@edit="openEdit"
|
|
@toggle="toggleActive"
|
|
@delete="deleteItem"
|
|
@run-now="runNow"
|
|
@sends="openSends"
|
|
/>
|
|
|
|
<BasicActionSheet
|
|
v-model:visible="editorVisible"
|
|
:title="sheetTitle"
|
|
modal-width="80rem"
|
|
drawer-height="80vh"
|
|
:loading="loading"
|
|
:show-button="true"
|
|
:show-cancel="true"
|
|
:show-ok="true"
|
|
ok-text="ذخیره"
|
|
cancel-text="انصراف"
|
|
@ok="submitForm"
|
|
@close="closeEditor"
|
|
@cancel="closeEditor"
|
|
>
|
|
<div v-if="isMobile" class="mb-3 text-base font-bold">{{ sheetTitle }}</div>
|
|
<AutomationEditorFormBody ref="editorRef" :form="form" />
|
|
</BasicActionSheet>
|
|
|
|
<BasicModal
|
|
v-model:visible="sendsVisible"
|
|
:title="sendsTitle"
|
|
:show-footer="false"
|
|
width="48rem"
|
|
>
|
|
<a-spin :spinning="sendsLoading">
|
|
<div v-if="!sendsLoading && sendsRows.length === 0" class="py-6 text-center text-gray-500">
|
|
هنوز ارسالی ثبت نشده
|
|
</div>
|
|
<div v-else class="flex flex-col gap-2">
|
|
<div
|
|
v-for="row in sendsRows"
|
|
:key="row.id"
|
|
class="rounded-lg border border-gray-100 px-3 py-2 text-sm dark:border-slate-700"
|
|
>
|
|
<div class="flex items-center justify-between gap-2">
|
|
<span>{{ row.customer_name || '—' }} · {{ row.customer_mobile || '—' }}</span>
|
|
<span
|
|
:class="
|
|
row.status === 'sent'
|
|
? 'text-green-600'
|
|
: row.status === 'skipped'
|
|
? 'text-amber-600'
|
|
: 'text-red-600'
|
|
"
|
|
>
|
|
{{
|
|
row.status === 'sent'
|
|
? 'موفق'
|
|
: row.status === 'skipped'
|
|
? 'رد شده'
|
|
: 'ناموفق'
|
|
}}
|
|
</span>
|
|
</div>
|
|
<div class="text-xs text-gray-500">{{ formatSendDate(row.created_at) }}</div>
|
|
<div v-if="row.error" class="mt-1 text-xs text-red-600">{{ row.error }}</div>
|
|
</div>
|
|
</div>
|
|
</a-spin>
|
|
</BasicModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { Modal, message } from 'ant-design-vue'
|
|
import { cloneDeep } from 'lodash'
|
|
import tbl from './components/table.vue'
|
|
import AutomationEditorFormBody from './components/AutomationEditorFormBody.vue'
|
|
import BasicModal from '@/components/BasicModal.vue'
|
|
import { emptyAutomation, type Automation } from '@/model/automations'
|
|
import {
|
|
createAutomation,
|
|
deleteAutomation,
|
|
getAutomationSendsTable,
|
|
runAutomationNow,
|
|
toggleAutomationActive,
|
|
updateAutomation,
|
|
} from '@/api/automations'
|
|
import { confirmDeleteItem } from '@/utils/confirmModal'
|
|
import { isMobile } from '@core/utils/is'
|
|
import { toJalali } from '@core/utils/useDataTime'
|
|
|
|
const tableRef = ref<null | { fetch: () => void }>(null)
|
|
const editorRef = ref<{
|
|
validateFields?: () => Promise<unknown>
|
|
resetFields?: () => void
|
|
}>()
|
|
|
|
const loading = ref(false)
|
|
const editorVisible = ref(false)
|
|
const editId = ref<number | undefined>()
|
|
const form = ref<Automation>(emptyAutomation())
|
|
const sendsVisible = ref(false)
|
|
const sendsLoading = ref(false)
|
|
const sendsTitle = ref('لاگ ارسال')
|
|
const sendsRows = ref<Array<Record<string, unknown>>>([])
|
|
|
|
const sheetTitle = computed(() => (editId.value ? 'ویرایش اتوماسیون' : 'افزودن اتوماسیون'))
|
|
|
|
function formatSendDate(value: unknown) {
|
|
if (!value) return '—'
|
|
return toJalali(String(value))
|
|
}
|
|
|
|
function refreshTable() {
|
|
tableRef.value?.fetch()
|
|
}
|
|
|
|
function closeEditor() {
|
|
editorVisible.value = false
|
|
}
|
|
|
|
function openAdd() {
|
|
editId.value = undefined
|
|
form.value = emptyAutomation()
|
|
editorRef.value?.resetFields?.()
|
|
editorVisible.value = true
|
|
}
|
|
|
|
function openEdit(record: Automation) {
|
|
editId.value = record.id
|
|
const conditions = record.conditions || []
|
|
form.value = cloneDeep({
|
|
...emptyAutomation(),
|
|
...record,
|
|
audience_mode: conditions.length ? 'filter' : 'all',
|
|
trigger_config: {
|
|
delay_hours: 0,
|
|
every_days: 1,
|
|
direction: 'both',
|
|
price_window: 'calendar_day',
|
|
...(record.trigger_config || {}),
|
|
},
|
|
conditions,
|
|
channels: record.channels || [],
|
|
})
|
|
editorVisible.value = true
|
|
}
|
|
|
|
async function submitForm() {
|
|
try {
|
|
await editorRef.value?.validateFields?.()
|
|
loading.value = true
|
|
const payload = cloneDeep(form.value)
|
|
if (editId.value) {
|
|
await updateAutomation(editId.value, payload)
|
|
} else {
|
|
await createAutomation(payload)
|
|
}
|
|
message.success('اتوماسیون ذخیره شد')
|
|
editorVisible.value = false
|
|
refreshTable()
|
|
} catch (e) {
|
|
console.error(e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function toggleActive(record: Automation) {
|
|
if (!record.id) return
|
|
try {
|
|
await toggleAutomationActive(record.id)
|
|
message.success(record.is_active ? 'غیرفعال شد' : 'فعال شد')
|
|
refreshTable()
|
|
} catch (e) {
|
|
console.error(e)
|
|
message.error('تغییر وضعیت انجام نشد')
|
|
}
|
|
}
|
|
|
|
function deleteItem(id: number) {
|
|
confirmDeleteItem('اتوماسیون', id, deleteAutomation, refreshTable)
|
|
}
|
|
|
|
function formatAutomationTime(record: Automation) {
|
|
if (record.sms_send_hour == null) return '—'
|
|
const h = String(record.sms_send_hour).padStart(2, '0')
|
|
const m = String(record.sms_send_minute ?? 0).padStart(2, '0')
|
|
return `${h}:${m}`
|
|
}
|
|
|
|
function runNow(record: Automation) {
|
|
if (!record.id) return
|
|
Modal.confirm({
|
|
title: 'اجرای آزمایشی',
|
|
content: `پیام «${record.name}» همین الان برای مخاطبان واجد شرط ارسال شود؟ (بدون انتظار ${formatAutomationTime(record)})`,
|
|
okText: 'ارسال',
|
|
cancelText: 'انصراف',
|
|
async onOk() {
|
|
const res = await runAutomationNow(record.id!)
|
|
message.success(res?.message || 'اجرای آزمایشی انجام شد')
|
|
refreshTable()
|
|
},
|
|
})
|
|
}
|
|
|
|
async function openSends(record: Automation) {
|
|
if (!record.id) return
|
|
sendsTitle.value = `لاگ ارسال — ${record.name}`
|
|
sendsVisible.value = true
|
|
sendsLoading.value = true
|
|
try {
|
|
const res = await getAutomationSendsTable(record.id, { page: 1, limit: 30, select: [] })
|
|
const rows = (res as { rows?: Array<Record<string, unknown>> })?.rows
|
|
?? (Array.isArray(res) ? res : [])
|
|
sendsRows.value = rows
|
|
} catch (e) {
|
|
console.error(e)
|
|
message.error('بارگذاری لاگ ناموفق بود')
|
|
sendsRows.value = []
|
|
} finally {
|
|
sendsLoading.value = false
|
|
}
|
|
}
|
|
</script>
|