diff --git a/src/api/customerDirectSms.ts b/src/api/customerDirectSms.ts
index 62c7445..0a77741 100644
--- a/src/api/customerDirectSms.ts
+++ b/src/api/customerDirectSms.ts
@@ -1,4 +1,3 @@
-import type { Customer } from '@/model/customer'
import useAxios from '@core/utils/useAxios'
export const sendCustomerDirectSms = async (payload: {
@@ -6,23 +5,82 @@ export const sendCustomerDirectSms = async (payload: {
message: string
}) => useAxios.post<{ ok: boolean }>('/admin/messages/send_customer', payload)
-export type DirectSmsSnippet = {
- key: string
- label: string
- insert: string
+export const DIRECT_SMS_MAX_LENGTH = 700
+
+export type DirectSmsTemplateContext = {
+ appUrl: string
+ appName: string
}
-/** دکمههای آماده برای درج در متن پیامک */
-export function buildDirectSmsSnippets(customer?: Customer | null, appName = '', appUrl = ''): DirectSmsSnippet[] {
- const name = (customer?.name ?? '').trim() || 'مشتری'
- return [
- { key: 'name', label: 'نام مشتری', insert: name },
- { key: 'app_name', label: 'نام اپ', insert: appName || 'اپ' },
- { key: 'app_url', label: 'لینک اپ', insert: appUrl || '' },
- { key: 'name_ph', label: '%نام%', insert: '%نام%' },
- { key: 'app_name_ph', label: '%نام_اپ%', insert: '%نام_اپ%' },
- { key: 'app_url_ph', label: '%لینک_اپ%', insert: '%لینک_اپ%' }
- ].filter((s) => s.insert !== '')
+export type DirectSmsTemplate = {
+ key: string
+ label: string
+ /** بدنهٔ پیام (بدون نام و نام اپ — آنها خودکار اضافه میشوند) */
+ body: (ctx: DirectSmsTemplateContext) => string
+ /** اگر true باشد و لینک اپ خالی باشد، قالب غیرفعال است */
+ requiresAppUrl?: boolean
+}
+
+/** قالبهای آماده — همیشه با «نام عزیز» شروع و با «نام اپ» تمام میشوند */
+export const DIRECT_SMS_TEMPLATES: DirectSmsTemplate[] = [
+ {
+ key: 'app_login_link',
+ label: 'لینک ورود به اپ',
+ requiresAppUrl: true,
+ body: ({ appUrl }) =>
+ ['برای ورود به اپلیکیشن از لینک زیر استفاده کنید:', appUrl].join('\n')
+ },
+ {
+ key: 'invite_app',
+ label: 'دعوت به استفاده از اپ',
+ requiresAppUrl: true,
+ body: ({ appUrl }) =>
+ [
+ 'از شما دعوت میکنیم وارد اپلیکیشن شوید و از خدمات خرید و فروش طلا بهرهمند شوید.',
+ 'لینک ورود:',
+ appUrl
+ ].join('\n')
+ },
+ {
+ key: 'complete_kyc',
+ label: 'تکمیل احراز هویت',
+ body: () => 'لطفاً برای ادامه فعالیت، احراز هویت خود را در اپلیکیشن تکمیل کنید.'
+ },
+ {
+ key: 'charge_wallet',
+ label: 'یادآوری شارژ حساب',
+ body: () => 'برای شارژ حساب و ادامه معاملات، از بخش کیف پول در اپلیکیشن اقدام کنید.'
+ },
+ {
+ key: 'welcome',
+ label: 'خوشآمدگویی',
+ body: () => 'به جمع کاربران ما خوش آمدید. امیدواریم تجربهٔ خوبی از خدمات ما داشته باشید.'
+ }
+]
+
+/**
+ * فرمت اجباری همهٔ پیامکها:
+ * خط ۱: «نام عزیز»
+ * خطهای میانی: متن
+ * خط آخر: نام اپ
+ */
+export function formatDirectSms(params: {
+ customerName: string
+ body: string
+ appName: string
+}): string {
+ const name = (params.customerName ?? '').trim() || 'مشتری'
+ const app = (params.appName ?? '').trim() || 'اپ'
+ const body = (params.body ?? '').trim()
+ if (!body) return `${name} عزیز\n${app}`
+ return `${name} عزیز\n${body}\n${app}`
+}
+
+export function resolveTemplateBody(
+ template: DirectSmsTemplate,
+ ctx: DirectSmsTemplateContext
+): string {
+ return template.body(ctx).trim()
}
const BLOCKED = [
diff --git a/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue b/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue
index f4250d7..ac10370 100644
--- a/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue
+++ b/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue
@@ -1,7 +1,7 @@
+ برای قالبهای دارای لینک، مقدار «لینک اپ» را در تنظیمات پر کنید. +
++ فقط متن میانی را بنویسید. نام مشتری و نام اپ بهصورت خودکار اضافه میشوند. +
++ همین متن دقیقاً برای گیرنده ارسال میشود. قبل از ارسال پیشنمایش را بررسی کنید. +
+متن حاوی عبارت نامناسب است — قبل از ارسال اصلاح کنید.
-- متغیرها در سرور جایگزین میشوند: %نام% · %نام_اپ% · %لینک_اپ% +
+ طول پیامک بیش از {{ DIRECT_SMS_MAX_LENGTH }} کاراکتر است. +
++ نام اپ در تنظیمات خالی است؛ در پایان پیام از «اپ» استفاده میشود.
@@ -52,9 +117,13 @@ import type { Customer } from '@/model/customer' import { isMobile } from '@core/utils/is' import BasicModal from '@/components/BasicModal.vue' import { - buildDirectSmsSnippets, + DIRECT_SMS_MAX_LENGTH, + DIRECT_SMS_TEMPLATES, findProfanity, - sendCustomerDirectSms + formatDirectSms, + resolveTemplateBody, + sendCustomerDirectSms, + type DirectSmsTemplate } from '@/api/customerDirectSms' import { getConfiguration } from '@/api/configuration' @@ -74,29 +143,89 @@ const visibleProxy = computed({ }) const loading = ref(false) -const message = ref('') +const activeTab = ref<'templates' | 'manual'>('templates') +const selectedTemplateKey = ref(DIRECT_SMS_TEMPLATES[0]?.key ?? '') +const manualBody = ref('') const appName = ref('') const appUrl = ref('') +const templates = DIRECT_SMS_TEMPLATES + const modalTitle = computed(() => props.customer?.name ? `پیامک به ${props.customer.name}` : 'ارسال پیامک به مشتری' ) -const snippets = computed(() => - buildDirectSmsSnippets(props.customer, appName.value, appUrl.value) +const customerDisplayName = computed( + () => (props.customer?.name ?? '').trim() || 'مشتری' ) -const profanityHit = computed(() => findProfanity(message.value)) +const appDisplayName = computed(() => appName.value.trim() || 'اپ') + +const templateCtx = computed(() => ({ + appUrl: appUrl.value.trim(), + appName: appDisplayName.value +})) + +function isTemplateDisabled(tpl: DirectSmsTemplate): boolean { + return Boolean(tpl.requiresAppUrl && !appUrl.value.trim()) +} + +const needsAppUrlHint = computed(() => + templates.some((t) => t.requiresAppUrl) +) + +const selectedTemplate = computed( + () => templates.find((t) => t.key === selectedTemplateKey.value) ?? null +) + +const bodyText = computed(() => { + if (activeTab.value === 'manual') return manualBody.value.trim() + const tpl = selectedTemplate.value + if (!tpl || isTemplateDisabled(tpl)) return '' + return resolveTemplateBody(tpl, templateCtx.value) +}) + +const previewMessage = computed(() => { + if (!bodyText.value) return '' + return formatDirectSms({ + customerName: customerDisplayName.value, + body: bodyText.value, + appName: appDisplayName.value + }) +}) + +const previewLength = computed(() => previewMessage.value.length) + +const previewOverLimit = computed(() => previewLength.value > DIRECT_SMS_MAX_LENGTH) + +/** سقف تقریبی برای بدنهٔ دستی تا کل پیام از ۷۰۰ رد نشود */ +const manualMaxLength = computed(() => { + const wrapperLen = formatDirectSms({ + customerName: customerDisplayName.value, + body: '', + appName: appDisplayName.value + }).length + return Math.max(50, DIRECT_SMS_MAX_LENGTH - wrapperLen - 1) +}) + +const profanityHit = computed(() => findProfanity(previewMessage.value)) const canSubmit = computed( - () => message.value.trim().length > 0 && !profanityHit.value && !loading.value + () => + previewMessage.value.trim().length > 0 && + Boolean(bodyText.value) && + !profanityHit.value && + !previewOverLimit.value && + !loading.value ) watch( () => props.visible, async (open) => { if (!open) return - message.value = '' + activeTab.value = 'templates' + manualBody.value = '' + selectedTemplateKey.value = DIRECT_SMS_TEMPLATES[0]?.key ?? '' try { const rows = await getConfiguration() const list = Array.isArray(rows) ? rows : [] @@ -106,23 +235,20 @@ watch( appName.value = '' appUrl.value = '' } + // اگر قالب اول به لینک نیاز دارد و لینک نیست، اولین قالب قابلاستفاده را انتخاب کن + const firstOk = templates.find((t) => !isTemplateDisabled(t)) + if (firstOk) selectedTemplateKey.value = firstOk.key } ) -function appendSnippet(text: string) { - if (!text) return - const current = message.value - const needsSpace = current.length > 0 && !/\s$/.test(current) - message.value = current + (needsSpace ? ' ' : '') + text -} - function close() { visibleProxy.value = false } async function submit() { if (!props.customer?.id || !canSubmit.value) return - const hit = findProfanity(message.value) + const finalMessage = previewMessage.value.trim() + const hit = findProfanity(finalMessage) if (hit) { antMessage.error('متن حاوی محتوای نامناسب است') return @@ -131,7 +257,7 @@ async function submit() { loading.value = true await sendCustomerDirectSms({ customer_id: Number(props.customer.id), - message: message.value.trim() + message: finalMessage }) antMessage.success('پیامک ارسال شد') emit('sent')