feat(automations): use Ant TimePicker with minute precision and Jalali dates

Replace hour-only select with HH:mm picker, show last run and send logs in Jalali, and display scheduled times as HH:mm in the table.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 22:13:06 +03:30
co-authored by Cursor
parent eb435fe3ab
commit 7a2254bc2d
4 changed files with 84 additions and 42 deletions
+4
View File
@@ -30,7 +30,9 @@ export interface Automation {
starts_at?: string | null starts_at?: string | null
ends_at?: string | null ends_at?: string | null
sms_send_hour?: number | null sms_send_hour?: number | null
sms_send_minute?: number | null
in_app_send_hour?: number | null in_app_send_hour?: number | null
in_app_send_minute?: number | null
trigger_type: AutomationTriggerType trigger_type: AutomationTriggerType
trigger_config?: AutomationTriggerConfig trigger_config?: AutomationTriggerConfig
audience_mode?: AutomationAudienceMode audience_mode?: AutomationAudienceMode
@@ -92,7 +94,9 @@ export function emptyAutomation(): Automation {
conditions: [], conditions: [],
channels: ['sms'], channels: ['sms'],
sms_send_hour: 10, sms_send_hour: 10,
sms_send_minute: 0,
in_app_send_hour: 10, in_app_send_hour: 10,
in_app_send_minute: 0,
cooldown_hours: 24, cooldown_hours: 24,
starts_at: null, starts_at: null,
ends_at: null, ends_at: null,
@@ -299,16 +299,16 @@
</template> </template>
<div class="auto-editor__channel"> <div class="auto-editor__channel">
<a-switch :checked="hasSms" @change="(v) => toggleChannel('sms', !!v)" /> <a-switch :checked="hasSms" @change="(v) => toggleChannel('sms', !!v)" />
<BasicSelect <a-time-picker
v-if="hasSms && showChannelHours" v-if="hasSms && showChannelHours"
class="auto-editor__hour-select" class="auto-editor__hour-picker"
:value="form.sms_send_hour" :value="smsHourValue"
:items="catalog?.hours || []" format="HH:mm"
key-name="key" :minute-step="1"
value-name="name" :show-second="false"
placeholder="ساعت" placeholder="ساعت"
:has-tooltip="false" allow-clear
@update:value="(v) => (form.sms_send_hour = v)" @update:value="setSmsTime"
/> />
</div> </div>
</a-form-item> </a-form-item>
@@ -318,16 +318,16 @@
</template> </template>
<div class="auto-editor__channel"> <div class="auto-editor__channel">
<a-switch :checked="hasInApp" @change="(v) => toggleChannel('in_app', !!v)" /> <a-switch :checked="hasInApp" @change="(v) => toggleChannel('in_app', !!v)" />
<BasicSelect <a-time-picker
v-if="hasInApp && showChannelHours" v-if="hasInApp && showChannelHours"
class="auto-editor__hour-select" class="auto-editor__hour-picker"
:value="form.in_app_send_hour" :value="inAppHourValue"
:items="catalog?.hours || []" format="HH:mm"
key-name="key" :minute-step="1"
value-name="name" :show-second="false"
placeholder="ساعت" placeholder="ساعت"
:has-tooltip="false" allow-clear
@update:value="(v) => (form.in_app_send_hour = v)" @update:value="setInAppTime"
/> />
</div> </div>
</a-form-item> </a-form-item>
@@ -375,7 +375,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue' import { computed, nextTick, onMounted, ref, watch } from 'vue'
import type { FormInstance } from 'ant-design-vue' import type { FormInstance } from 'ant-design-vue'
import dayjs, { type Dayjs } from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import { cloneDeep, debounce, isEmpty } from 'lodash' import { cloneDeep, debounce, isEmpty } from 'lodash'
dayjs.extend(customParseFormat)
import { inputRules } from '@/utils/inputRules' import { inputRules } from '@/utils/inputRules'
import { getAutomationCatalog, previewAutomationAudience } from '@/api/automations' import { getAutomationCatalog, previewAutomationAudience } from '@/api/automations'
import BaseDatePicker from '@core/components/BaseDatePicker' import BaseDatePicker from '@core/components/BaseDatePicker'
@@ -499,6 +503,37 @@ const previewText = computed(() => {
const hasSms = computed(() => (props.form.channels || []).includes('sms')) const hasSms = computed(() => (props.form.channels || []).includes('sms'))
const hasInApp = computed(() => (props.form.channels || []).includes('in_app')) const hasInApp = computed(() => (props.form.channels || []).includes('in_app'))
function timeToPicker(hour?: number | null, minute?: number | null): Dayjs | undefined {
if (hour == null || Number.isNaN(Number(hour))) return undefined
return dayjs()
.hour(Number(hour))
.minute(Number(minute ?? 0))
.second(0)
.millisecond(0)
}
function pickerToTime(val: Dayjs | string | null | undefined): { hour: number | null; minute: number | null } {
if (val == null || val === '') return { hour: null, minute: null }
const parsed = dayjs.isDayjs(val) ? val : dayjs(String(val), 'HH:mm', true)
if (!parsed.isValid()) return { hour: null, minute: null }
return { hour: parsed.hour(), minute: parsed.minute() }
}
const smsHourValue = computed(() => timeToPicker(props.form.sms_send_hour, props.form.sms_send_minute))
const inAppHourValue = computed(() => timeToPicker(props.form.in_app_send_hour, props.form.in_app_send_minute))
function setSmsTime(val: Dayjs | string | null) {
const time = pickerToTime(val)
props.form.sms_send_hour = time.hour
props.form.sms_send_minute = time.hour == null ? null : time.minute
}
function setInAppTime(val: Dayjs | string | null) {
const time = pickerToTime(val)
props.form.in_app_send_hour = time.hour
props.form.in_app_send_minute = time.hour == null ? null : time.minute
}
function ensureTriggerConfig() { function ensureTriggerConfig() {
if (!props.form.trigger_config) { if (!props.form.trigger_config) {
props.form.trigger_config = { props.form.trigger_config = {
@@ -702,31 +737,14 @@ defineExpose({
@apply flex items-center gap-3; @apply flex items-center gap-3;
} }
.auto-editor__hour-select { .auto-editor__hour-picker {
min-width: 7rem; min-width: 7rem;
flex: 1; flex: 1;
} }
.auto-editor__hour-select :deep(.ant-select-selector) { .auto-editor__hour-picker :deep(.ant-picker) {
display: flex !important; width: 100%;
align-items: center !important; border-radius: 10px;
height: 32px !important;
}
.auto-editor__hour-select :deep(.ant-select-selection-search),
.auto-editor__hour-select :deep(.ant-select-selection-item),
.auto-editor__hour-select :deep(.ant-select-selection-placeholder) {
display: flex !important;
align-items: center !important;
line-height: 30px !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.auto-editor__hour-select :deep(.ant-select-selection-item > div) {
line-height: 30px;
white-space: nowrap;
padding: 0;
} }
.auto-editor__hint { .auto-editor__hint {
@@ -1,4 +1,5 @@
import type { BaseTableColumn } from '@core/type/baseTable' import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali } from '@core/utils/useDataTime'
const triggerLabel: Record<string, string> = { const triggerLabel: Record<string, string> = {
schedule: 'زمان‌دار', schedule: 'زمان‌دار',
@@ -14,6 +15,13 @@ function channelText(record: any): string {
return parts.join(' / ') || '—' return parts.join(' / ') || '—'
} }
function formatSendTime(hour?: number | null, minute?: number | null): string {
if (hour == null) return '—'
const h = String(hour).padStart(2, '0')
const m = String(minute ?? 0).padStart(2, '0')
return `${h}:${m}`
}
function previewBody(text: unknown): string { function previewBody(text: unknown): string {
const s = String(text || '').replace(/\s+/g, ' ').trim() const s = String(text || '').replace(/\s+/g, ' ').trim()
if (!s) return '—' if (!s) return '—'
@@ -66,14 +74,13 @@ export const cols: BaseTableColumn[] = [
title: 'ساعت پیامک', title: 'ساعت پیامک',
dataIndex: 'sms_send_hour', dataIndex: 'sms_send_hour',
key: 'sms_send_hour', key: 'sms_send_hour',
customRender: ({ record }) => customRender: ({ record }) => formatSendTime(record.sms_send_hour, record.sms_send_minute),
record.sms_send_hour == null ? '—' : `${String(record.sms_send_hour).padStart(2, '0')}:۰۰`,
}, },
{ {
title: 'آخرین اجرا', title: 'آخرین اجرا',
dataIndex: 'last_sms_run_at', dataIndex: 'last_sms_run_at',
key: 'last_sms_run_at', key: 'last_sms_run_at',
customRender: ({ record }) => record.last_sms_run_at || '—', customRender: ({ record }) => (record.last_sms_run_at ? toJalali(record.last_sms_run_at) : '—'),
}, },
{ {
title: 'فعال', title: 'فعال',
+15 -2
View File
@@ -52,7 +52,7 @@
{{ row.status === 'sent' ? 'موفق' : 'ناموفق' }} {{ row.status === 'sent' ? 'موفق' : 'ناموفق' }}
</span> </span>
</div> </div>
<div class="text-xs text-gray-500">{{ row.created_at }}</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 v-if="row.error" class="mt-1 text-xs text-red-600">{{ row.error }}</div>
</div> </div>
</div> </div>
@@ -79,6 +79,7 @@ import {
} from '@/api/automations' } from '@/api/automations'
import { confirmDeleteItem } from '@/utils/confirmModal' import { confirmDeleteItem } from '@/utils/confirmModal'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
const tableRef = ref<null | { fetch: () => void }>(null) const tableRef = ref<null | { fetch: () => void }>(null)
const editorRef = ref<{ const editorRef = ref<{
@@ -97,6 +98,11 @@ const sendsRows = ref<Array<Record<string, unknown>>>([])
const sheetTitle = computed(() => (editId.value ? 'ویرایش اتوماسیون' : 'افزودن اتوماسیون')) const sheetTitle = computed(() => (editId.value ? 'ویرایش اتوماسیون' : 'افزودن اتوماسیون'))
function formatSendDate(value: unknown) {
if (!value) return '—'
return toJalali(String(value))
}
function refreshTable() { function refreshTable() {
tableRef.value?.fetch() tableRef.value?.fetch()
} }
@@ -168,11 +174,18 @@ function deleteItem(id: number) {
confirmDeleteItem('اتوماسیون', id, deleteAutomation, refreshTable) 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) { function runNow(record: Automation) {
if (!record.id) return if (!record.id) return
Modal.confirm({ Modal.confirm({
title: 'اجرای آزمایشی', title: 'اجرای آزمایشی',
content: `پیام «${record.name}» همین الان برای مخاطبان واجد شرط ارسال شود؟ (بدون انتظار ساعت ${record.sms_send_hour ?? '—'})`, content: `پیام «${record.name}» همین الان برای مخاطبان واجد شرط ارسال شود؟ (بدون انتظار ${formatAutomationTime(record)})`,
okText: 'ارسال', okText: 'ارسال',
cancelText: 'انصراف', cancelText: 'انصراف',
async onOk() { async onOk() {