From 8925c77ae0e869a4f0b9860c7ba34296fba8b5f1 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sun, 13 Sep 2026 21:22:41 +0330 Subject: [PATCH] Edit SMS automation --- .../migration.sql | 2 + apps/api/prisma/schema.prisma | 2 + .../__tests__/message-automation.check.ts | 1 + apps/api/src/message-automations/dto.ts | 2 +- .../message-automation.util.ts | 31 +- .../src/components/MessageIntervalPicker.vue | 342 ++++++++++++++++++ apps/web/src/views/MessageAutomationsView.vue | 77 ++-- packages/shared/src/index.ts | 9 +- 8 files changed, 410 insertions(+), 56 deletions(-) create mode 100644 apps/api/prisma/migrations/20260913200000_message_delay_weeks_months/migration.sql create mode 100644 apps/web/src/components/MessageIntervalPicker.vue diff --git a/apps/api/prisma/migrations/20260913200000_message_delay_weeks_months/migration.sql b/apps/api/prisma/migrations/20260913200000_message_delay_weeks_months/migration.sql new file mode 100644 index 0000000..313a851 --- /dev/null +++ b/apps/api/prisma/migrations/20260913200000_message_delay_weeks_months/migration.sql @@ -0,0 +1,2 @@ +ALTER TYPE "MessageAutomationDelayUnit" ADD VALUE IF NOT EXISTS 'weeks'; +ALTER TYPE "MessageAutomationDelayUnit" ADD VALUE IF NOT EXISTS 'months'; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index f963c31..49f18c4 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -1104,6 +1104,8 @@ enum MessageAutomationDelayUnit { minutes hours days + weeks + months } enum MessageAutomationChannel { diff --git a/apps/api/src/message-automations/__tests__/message-automation.check.ts b/apps/api/src/message-automations/__tests__/message-automation.check.ts index dff4156..abefba6 100644 --- a/apps/api/src/message-automations/__tests__/message-automation.check.ts +++ b/apps/api/src/message-automations/__tests__/message-automation.check.ts @@ -14,6 +14,7 @@ function testDelay() { assert(delayMs(0, 'minutes') === 0, 'zero delay'); assert(delayMs(2, 'hours') === 2 * 3_600_000, '2 hours'); assert(delayMs(2, 'days') === 2 * 86_400_000, '2 days'); + assert(delayMs(1, 'weeks') === 7 * 86_400_000, '1 week'); } function testSendAts() { diff --git a/apps/api/src/message-automations/dto.ts b/apps/api/src/message-automations/dto.ts index 3342581..4ab96a7 100644 --- a/apps/api/src/message-automations/dto.ts +++ b/apps/api/src/message-automations/dto.ts @@ -18,7 +18,7 @@ import { type MessageAutomationTrigger, } from '@mento/shared'; -const UNITS = ['minutes', 'hours', 'days'] as const; +const UNITS = ['minutes', 'hours', 'days', 'weeks', 'months'] as const; const CHANNELS = ['app', 'sms', 'both'] as const; const TARGETS = ['staff', 'person', 'both'] as const; diff --git a/apps/api/src/message-automations/message-automation.util.ts b/apps/api/src/message-automations/message-automation.util.ts index 2362fa9..094d324 100644 --- a/apps/api/src/message-automations/message-automation.util.ts +++ b/apps/api/src/message-automations/message-automation.util.ts @@ -1,4 +1,4 @@ -export type MessageDelayUnit = 'minutes' | 'hours' | 'days'; +export type MessageDelayUnit = 'minutes' | 'hours' | 'days' | 'weeks' | 'months'; export const MESSAGE_AUTOMATION_TRIGGERS = [ 'person_created', @@ -22,7 +22,23 @@ export function delayMs(value: number, unit: MessageDelayUnit): number { const n = Math.max(0, Math.trunc(Number(value) || 0)); if (unit === 'minutes') return n * 60_000; if (unit === 'hours') return n * 3_600_000; - return n * 86_400_000; + if (unit === 'days') return n * 86_400_000; + if (unit === 'weeks') return n * 7 * 86_400_000; + return n * 30 * 86_400_000; +} + +export function addDelay( + from: Date, + value: number, + unit: MessageDelayUnit, +): Date { + const n = Math.max(0, Math.trunc(Number(value) || 0)); + if (unit === 'months') { + const next = new Date(from.getTime()); + next.setMonth(next.getMonth() + n); + return next; + } + return new Date(from.getTime() + delayMs(n, unit)); } export function buildSendAts( @@ -37,9 +53,14 @@ export function buildSendAts( MAX_MESSAGE_REPEAT, Math.max(1, Math.trunc(Number(repeatCount) || 1)), ); - const first = new Date(now.getTime() + delayMs(startValue, startUnit)); - const gap = delayMs(Math.max(1, intervalValue), intervalUnit); - return Array.from({ length: count }, (_, i) => new Date(first.getTime() + i * gap)); + const first = addDelay(now, startValue, startUnit); + const times = [first]; + let cursor = first; + for (let i = 1; i < count; i += 1) { + cursor = addDelay(cursor, Math.max(1, intervalValue), intervalUnit); + times.push(cursor); + } + return times; } export function applyMessageTemplate(text: string, title: string): string { diff --git a/apps/web/src/components/MessageIntervalPicker.vue b/apps/web/src/components/MessageIntervalPicker.vue new file mode 100644 index 0000000..aa3a6e8 --- /dev/null +++ b/apps/web/src/components/MessageIntervalPicker.vue @@ -0,0 +1,342 @@ + + + + + diff --git a/apps/web/src/views/MessageAutomationsView.vue b/apps/web/src/views/MessageAutomationsView.vue index 5f4263f..68effe4 100644 --- a/apps/web/src/views/MessageAutomationsView.vue +++ b/apps/web/src/views/MessageAutomationsView.vue @@ -2,8 +2,8 @@ import { computed, onMounted, ref } from 'vue'; import axios from 'axios'; import { api } from '@/lib/api'; -import JalaliDateInput from '@/components/JalaliDateInput.vue'; -import { formatLocalDateTimeInput, parseLocalInput } from '@/lib/jalali'; +import MessageIntervalPicker from '@/components/MessageIntervalPicker.vue'; +import { toFaDigits } from '@/lib/jalali'; import type { CreateMessageAutomationRuleRequest, MessageAutomationChannel, @@ -30,9 +30,11 @@ const editingId = ref(null); const name = ref(''); const trigger = ref('invoice_issued'); -const startAt = ref(''); +const startDelayValue = ref(0); +const startDelayUnit = ref('minutes'); const repeatCount = ref(1); -const intervalAt = ref(''); +const intervalValue = ref(2); +const intervalUnit = ref('days'); const notifyTarget = ref('person'); const channel = ref('sms'); const title = ref(''); @@ -44,38 +46,15 @@ const personNote = computed( 'ناتیف داخل برنامه برای کارکنان است؛ برای مشتری پیامک ارسال می‌شود.', ); -function delayMs(value: number, unit: MessageAutomationDelayUnit) { - const n = Math.max(0, Math.trunc(Number(value) || 0)); - if (unit === 'minutes') return n * 60_000; - if (unit === 'hours') return n * 3_600_000; - return n * 86_400_000; -} - -function delayToIso(value: number, unit: MessageAutomationDelayUnit) { - if (!value) return ''; - return formatLocalDateTimeInput(new Date(Date.now() + delayMs(value, unit))); -} - -function isoToDelay(iso: string): { - value: number; - unit: MessageAutomationDelayUnit; -} { - const date = parseLocalInput(iso); - if (!date) return { value: 0, unit: 'minutes' }; - const mins = Math.max(0, Math.round((date.getTime() - Date.now()) / 60_000)); - if (mins === 0) return { value: 0, unit: 'minutes' }; - if (mins % (24 * 60) === 0) return { value: mins / (24 * 60), unit: 'days' }; - if (mins % 60 === 0) return { value: mins / 60, unit: 'hours' }; - return { value: mins, unit: 'minutes' }; -} - function resetForm() { editingId.value = null; name.value = ''; trigger.value = 'invoice_issued'; - startAt.value = ''; + startDelayValue.value = 0; + startDelayUnit.value = 'minutes'; repeatCount.value = 1; - intervalAt.value = delayToIso(2, 'days'); + intervalValue.value = 2; + intervalUnit.value = 'days'; notifyTarget.value = 'person'; channel.value = 'sms'; title.value = ''; @@ -87,9 +66,11 @@ function startEdit(row: MessageAutomationRuleDto) { editingId.value = row.id; name.value = row.name; trigger.value = row.trigger; - startAt.value = delayToIso(row.startDelayValue, row.startDelayUnit); + startDelayValue.value = row.startDelayValue; + startDelayUnit.value = row.startDelayUnit; repeatCount.value = row.repeatCount; - intervalAt.value = delayToIso(row.intervalValue, row.intervalUnit); + intervalValue.value = row.intervalValue; + intervalUnit.value = row.intervalUnit; notifyTarget.value = row.notifyTarget; channel.value = row.channel; title.value = row.title; @@ -101,9 +82,9 @@ function scheduleLabel(row: MessageAutomationRuleDto) { const start = row.startDelayValue === 0 ? 'همان لحظه' - : `${row.startDelayValue} ${MESSAGE_AUTOMATION_UNIT_LABELS[row.startDelayUnit]} بعد`; + : `${toFaDigits(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]}`; + return `${start} · ${toFaDigits(row.repeatCount)} بار، هر ${toFaDigits(row.intervalValue)} ${MESSAGE_AUTOMATION_UNIT_LABELS[row.intervalUnit]}`; } async function load() { @@ -122,17 +103,15 @@ async function load() { } function payload(): CreateMessageAutomationRuleRequest { - const start = isoToDelay(startAt.value); - const interval = isoToDelay(intervalAt.value); return { name: name.value.trim(), isActive: true, trigger: trigger.value, - startDelayValue: start.value, - startDelayUnit: start.unit, + startDelayValue: startDelayValue.value, + startDelayUnit: startDelayUnit.value, repeatCount: repeatCount.value, - intervalValue: Math.max(1, interval.value || 1), - intervalUnit: interval.value ? interval.unit : 'days', + intervalValue: Math.max(1, intervalValue.value || 1), + intervalUnit: intervalUnit.value, notifyTarget: notifyTarget.value, channel: channel.value, title: title.value.trim(), @@ -230,10 +209,10 @@ onMounted(load);
شروع ارسال -
فاصله بین پیام‌ها -