feat(automations): add test run button and send log modal in admin
Expose run-now and sends APIs in the automations UI, show SMS hour and last run columns, and group advanced settings toolbar actions together. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,3 +22,15 @@ export const toggleAutomationActive = async (id: number) =>
|
|||||||
|
|
||||||
export const deleteAutomation = async (id: number) =>
|
export const deleteAutomation = async (id: number) =>
|
||||||
await useAxios.delete(`/admin/automations/${id}`)
|
await useAxios.delete(`/admin/automations/${id}`)
|
||||||
|
|
||||||
|
export const runAutomationNow = async (id: number, force = true) =>
|
||||||
|
await useAxios.post<{
|
||||||
|
message?: string
|
||||||
|
audience?: number
|
||||||
|
sent?: number
|
||||||
|
failed?: number
|
||||||
|
skipped?: number
|
||||||
|
}>(`/admin/automations/${id}/run-now`, { force })
|
||||||
|
|
||||||
|
export const getAutomationSendsTable = async (id: number, params: TableInput) =>
|
||||||
|
await useAxios.post<Array<Record<string, unknown>>>(`/admin/automations/${id}/sends/table`, params)
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ export const refreshSuperAdminSettings = async () =>
|
|||||||
ok?: boolean
|
ok?: boolean
|
||||||
message?: string
|
message?: string
|
||||||
count?: number
|
count?: number
|
||||||
enable_automation?: string | null
|
items?: Record<string, string>
|
||||||
source?: string
|
source?: string
|
||||||
}>(`/admin/settings/refresh-superadmin`)
|
}>(`/admin/settings/refresh-superadmin`)
|
||||||
|
|||||||
@@ -50,6 +50,24 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<div class="flex justify-center gap-2">
|
<div class="flex justify-center gap-2">
|
||||||
|
<a-tooltip placement="bottom">
|
||||||
|
<template #title>اجرای آزمایشی</template>
|
||||||
|
<Icon
|
||||||
|
icon="vuesax-linear:play"
|
||||||
|
color="#16a34a"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="emits('runNow', record)"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip placement="bottom">
|
||||||
|
<template #title>لاگ ارسال</template>
|
||||||
|
<Icon
|
||||||
|
icon="vuesax-linear:document-text"
|
||||||
|
color="#64748b"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="emits('sends', record)"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
<a-tooltip placement="bottom">
|
<a-tooltip placement="bottom">
|
||||||
<template #title>ویرایش</template>
|
<template #title>ویرایش</template>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -93,6 +111,8 @@ const emits = defineEmits<{
|
|||||||
edit: [record: Automation]
|
edit: [record: Automation]
|
||||||
toggle: [record: Automation]
|
toggle: [record: Automation]
|
||||||
delete: [id: number]
|
delete: [id: number]
|
||||||
|
runNow: [record: Automation]
|
||||||
|
sends: [record: Automation]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tblProps = {
|
const tblProps = {
|
||||||
|
|||||||
@@ -62,6 +62,19 @@ export const cols: BaseTableColumn[] = [
|
|||||||
key: 'channels',
|
key: 'channels',
|
||||||
customRender: ({ record }) => channelText(record),
|
customRender: ({ record }) => channelText(record),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'ساعت پیامک',
|
||||||
|
dataIndex: 'sms_send_hour',
|
||||||
|
key: 'sms_send_hour',
|
||||||
|
customRender: ({ record }) =>
|
||||||
|
record.sms_send_hour == null ? '—' : `${String(record.sms_send_hour).padStart(2, '0')}:۰۰`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'آخرین اجرا',
|
||||||
|
dataIndex: 'last_sms_run_at',
|
||||||
|
key: 'last_sms_run_at',
|
||||||
|
customRender: ({ record }) => record.last_sms_run_at || '—',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'فعال',
|
title: 'فعال',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
@@ -90,4 +103,4 @@ export const cols: BaseTableColumn[] = [
|
|||||||
filterable: false,
|
filterable: false,
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
@edit="openEdit"
|
@edit="openEdit"
|
||||||
@toggle="toggleActive"
|
@toggle="toggleActive"
|
||||||
@delete="deleteItem"
|
@delete="deleteItem"
|
||||||
|
@run-now="runNow"
|
||||||
|
@sends="openSends"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BasicActionSheet
|
<BasicActionSheet
|
||||||
@@ -27,19 +29,51 @@
|
|||||||
<div v-if="isMobile" class="mb-3 text-base font-bold">{{ sheetTitle }}</div>
|
<div v-if="isMobile" class="mb-3 text-base font-bold">{{ sheetTitle }}</div>
|
||||||
<AutomationEditorFormBody ref="editorRef" :form="form" />
|
<AutomationEditorFormBody ref="editorRef" :form="form" />
|
||||||
</BasicActionSheet>
|
</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' : 'text-red-600'">
|
||||||
|
{{ row.status === 'sent' ? 'موفق' : 'ناموفق' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import tbl from './components/table.vue'
|
import tbl from './components/table.vue'
|
||||||
import AutomationEditorFormBody from './components/AutomationEditorFormBody.vue'
|
import AutomationEditorFormBody from './components/AutomationEditorFormBody.vue'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import { emptyAutomation, type Automation } from '@/model/automations'
|
import { emptyAutomation, type Automation } from '@/model/automations'
|
||||||
import {
|
import {
|
||||||
createAutomation,
|
createAutomation,
|
||||||
deleteAutomation,
|
deleteAutomation,
|
||||||
|
getAutomationSendsTable,
|
||||||
|
runAutomationNow,
|
||||||
toggleAutomationActive,
|
toggleAutomationActive,
|
||||||
updateAutomation,
|
updateAutomation,
|
||||||
} from '@/api/automations'
|
} from '@/api/automations'
|
||||||
@@ -56,6 +90,10 @@ const loading = ref(false)
|
|||||||
const editorVisible = ref(false)
|
const editorVisible = ref(false)
|
||||||
const editId = ref<number | undefined>()
|
const editId = ref<number | undefined>()
|
||||||
const form = ref<Automation>(emptyAutomation())
|
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 ? 'ویرایش اتوماسیون' : 'افزودن اتوماسیون'))
|
const sheetTitle = computed(() => (editId.value ? 'ویرایش اتوماسیون' : 'افزودن اتوماسیون'))
|
||||||
|
|
||||||
@@ -129,4 +167,38 @@ async function toggleActive(record: Automation) {
|
|||||||
function deleteItem(id: number) {
|
function deleteItem(id: number) {
|
||||||
confirmDeleteItem('اتوماسیون', id, deleteAutomation, refreshTable)
|
confirmDeleteItem('اتوماسیون', id, deleteAutomation, refreshTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runNow(record: Automation) {
|
||||||
|
if (!record.id) return
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'اجرای آزمایشی',
|
||||||
|
content: `پیام «${record.name}» همین الان برای مخاطبان واجد شرط ارسال شود؟ (بدون انتظار ساعت ${record.sms_send_hour ?? '—'})`,
|
||||||
|
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>
|
</script>
|
||||||
|
|||||||
@@ -26,27 +26,26 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <a-button type="primary" :loading="loading" @click="add">افزودن تنظیم</a-button>-->
|
<div class="settings-toolbar__actions">
|
||||||
<a-button
|
<a-button
|
||||||
v-if="isSystemMode"
|
v-if="isSystemMode"
|
||||||
type="primary"
|
type="primary"
|
||||||
class="settings-toolbar__action"
|
:loading="loading"
|
||||||
:loading="loading"
|
@click="add"
|
||||||
@click="add"
|
>
|
||||||
>
|
افزودن تنظیم
|
||||||
افزودن تنظیم
|
</a-button>
|
||||||
</a-button>
|
<a-button
|
||||||
<a-button
|
v-if="isSystemMode"
|
||||||
v-if="isSystemMode"
|
:loading="refreshingSuperAdmin"
|
||||||
class="settings-toolbar__action"
|
@click="refreshFromSuperAdmin"
|
||||||
:loading="refreshingSuperAdmin"
|
>
|
||||||
@click="refreshFromSuperAdmin"
|
همگامسازی از سوپرادمین
|
||||||
>
|
</a-button>
|
||||||
همگامسازی از سوپرادمین
|
<a-button :loading="loading" @click="initConfig">
|
||||||
</a-button>
|
بروزرسانی
|
||||||
<a-button class="settings-toolbar__action" :loading="loading" @click="initConfig">
|
</a-button>
|
||||||
بروزرسانی
|
</div>
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-spin :spinning="loading && !baseData.length">
|
<a-spin :spinning="loading && !baseData.length">
|
||||||
@@ -210,7 +209,7 @@ import SettingValueField from './SettingValueField.vue'
|
|||||||
import SettingCard from './SettingCard.vue'
|
import SettingCard from './SettingCard.vue'
|
||||||
import { refreshAppModeMenu } from '@/router'
|
import { refreshAppModeMenu } from '@/router'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings } from '@/utils/config'
|
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, applyCustomerDirectSmsFromRows, applyCustomerLevelProfitEnabledFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings } from '@/utils/config'
|
||||||
import {
|
import {
|
||||||
EDITABLE_SETTING_GROUPS,
|
EDITABLE_SETTING_GROUPS,
|
||||||
SYSTEM_SETTING_GROUPS,
|
SYSTEM_SETTING_GROUPS,
|
||||||
@@ -493,14 +492,9 @@ async function refreshFromSuperAdmin() {
|
|||||||
refreshingSuperAdmin.value = true
|
refreshingSuperAdmin.value = true
|
||||||
try {
|
try {
|
||||||
const res = await refreshSuperAdminSettings()
|
const res = await refreshSuperAdminSettings()
|
||||||
const auto = res?.enable_automation
|
|
||||||
message.success(
|
|
||||||
auto != null && auto !== ''
|
|
||||||
? `همگام شد (${res?.count ?? 0} کلید) — اتوماسیون: ${isSwitchOn(auto) ? 'روشن' : 'خاموش'}`
|
|
||||||
: (res?.message || `همگام شد (${res?.count ?? 0} کلید)`)
|
|
||||||
)
|
|
||||||
await initConfig()
|
await initConfig()
|
||||||
refreshMenuAfterStoreChange()
|
refreshMenuAfterStoreChange()
|
||||||
|
message.success(res?.message || `همگام شد (${res?.count ?? 0} کلید دسترسی از سوپرادمین)`)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
||||||
@@ -523,6 +517,8 @@ async function initConfig() {
|
|||||||
applyConditionalOrderEnabledFromRows(baseData.value)
|
applyConditionalOrderEnabledFromRows(baseData.value)
|
||||||
applyAutomationEnabledFromRows(baseData.value)
|
applyAutomationEnabledFromRows(baseData.value)
|
||||||
applyProductFundsEnabledFromRows(baseData.value)
|
applyProductFundsEnabledFromRows(baseData.value)
|
||||||
|
applyCustomerLevelProfitEnabledFromRows(baseData.value)
|
||||||
|
applyCustomerDirectSmsFromRows(baseData.value)
|
||||||
applyPriceTriggerEnabledFromRows(baseData.value)
|
applyPriceTriggerEnabledFromRows(baseData.value)
|
||||||
applyAppNotificationEnabledFromRows(baseData.value)
|
applyAppNotificationEnabledFromRows(baseData.value)
|
||||||
applyPriceProviderFromRows(baseData.value)
|
applyPriceProviderFromRows(baseData.value)
|
||||||
@@ -815,7 +811,11 @@ async function ok() {
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action {
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user