Files
boursecu_admin/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue
T

272 lines
9.1 KiB
Vue
Raw Normal View History

2026-07-18 22:22:21 +03:30
<template>
<BasicModal
v-model:visible="visibleProxy"
2026-07-19 01:11:37 +03:30
:width="isMobile ? '94vw' : '40rem'"
2026-07-18 22:22:21 +03:30
:title="modalTitle"
ok-text="ارسال پیامک"
:loading="loading"
:ok-button-props="{ disabled: !canSubmit }"
@ok="submit"
@close="close"
>
<div class="customer-sms-modal flex flex-col gap-3">
<p class="m-0 text-xs" style="color: var(--admin-text-secondary)">
گیرنده:
<strong dir="ltr" class="text-primary">{{ customer?.mobile || '—' }}</strong>
</p>
2026-07-19 01:11:37 +03:30
<a-tabs v-model:activeKey="activeTab" size="small" class="customer-sms-modal__tabs">
<a-tab-pane key="templates" tab="پیام‌های پیش‌فرض">
<div class="flex flex-col gap-3 pt-1">
<a-radio-group v-model:value="selectedTemplateKey" class="flex flex-col gap-2">
<a-radio
v-for="tpl in templates"
:key="tpl.key"
:value="tpl.key"
:disabled="isTemplateDisabled(tpl)"
class="!ms-0 whitespace-normal"
>
<span>{{ tpl.label }}</span>
<span
v-if="isTemplateDisabled(tpl)"
class="ms-1 text-xs"
style="color: var(--admin-text-secondary)"
>
(لینک اپ تنظیم نشده)
</span>
</a-radio>
</a-radio-group>
2026-07-18 22:22:21 +03:30
2026-07-19 01:11:37 +03:30
<p
v-if="!appUrl && needsAppUrlHint"
class="m-0 text-xs text-amber-600"
>
برای قالب‌های دارای لینک، مقدار «لینک اپ» را در تنظیمات پر کنید.
</p>
</div>
</a-tab-pane>
<a-tab-pane key="manual" tab="نوشتن دستی">
<div class="flex flex-col gap-2 pt-1">
<p class="m-0 text-xs" style="color: var(--admin-text-secondary)">
فقط متن میانی را بنویسید. نام مشتری و نام اپ به‌صورت خودکار اضافه می‌شوند.
</p>
<div
class="rounded border px-3 py-2 text-sm"
style="border-color: var(--admin-border, #434343); color: var(--admin-text-secondary)"
>
{{ customerDisplayName }} عزیز
</div>
<a-textarea
v-model:value="manualBody"
:rows="5"
:maxlength="manualMaxLength"
placeholder="متن پیامک را بنویسید…"
class="customer-sms-modal__textarea"
/>
<div
class="rounded border px-3 py-2 text-sm"
style="border-color: var(--admin-border, #434343); color: var(--admin-text-secondary)"
>
{{ appDisplayName }}
</div>
</div>
</a-tab-pane>
</a-tabs>
<div class="customer-sms-modal__preview flex flex-col gap-1.5">
<div class="flex items-center justify-between gap-2">
<span class="text-xs font-medium">پیش‌نمایش پیامک ارسالی</span>
<span
class="text-xs tabular-nums"
:class="previewOverLimit ? 'text-red-500' : ''"
style="color: var(--admin-text-secondary)"
>
{{ previewLength }} / {{ DIRECT_SMS_MAX_LENGTH }}
</span>
</div>
<div
class="customer-sms-modal__preview-box whitespace-pre-wrap break-words rounded border px-3 py-2.5 text-sm leading-relaxed"
style="border-color: var(--admin-border, #434343); background: rgba(255, 255, 255, 0.03); min-height: 5.5rem"
>
<template v-if="previewMessage">{{ previewMessage }}</template>
<span v-else style="color: var(--admin-text-secondary)">متن پیامک اینجا نمایش داده می‌شود…</span>
</div>
<p class="m-0 text-xs" style="color: var(--admin-text-secondary)">
همین متن دقیقاً برای گیرنده ارسال می‌شود. قبل از ارسال پیش‌نمایش را بررسی کنید.
</p>
</div>
2026-07-18 22:22:21 +03:30
<p v-if="profanityHit" class="m-0 text-xs text-red-500">
متن حاوی عبارت نامناسب است — قبل از ارسال اصلاح کنید.
</p>
2026-07-19 01:11:37 +03:30
<p v-if="previewOverLimit" class="m-0 text-xs text-red-500">
طول پیامک بیش از {{ DIRECT_SMS_MAX_LENGTH }} کاراکتر است.
</p>
<p v-if="!appName" class="m-0 text-xs text-amber-600">
نام اپ در تنظیمات خالی است؛ در پایان پیام از «اپ» استفاده می‌شود.
2026-07-18 22:22:21 +03:30
</p>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { message as antMessage } from 'ant-design-vue'
import type { Customer } from '@/model/customer'
import { isMobile } from '@core/utils/is'
import BasicModal from '@/components/BasicModal.vue'
import {
2026-07-19 01:11:37 +03:30
DIRECT_SMS_MAX_LENGTH,
DIRECT_SMS_TEMPLATES,
2026-07-18 22:22:21 +03:30
findProfanity,
2026-07-19 01:11:37 +03:30
formatDirectSms,
resolveTemplateBody,
sendCustomerDirectSms,
type DirectSmsTemplate
2026-07-18 22:22:21 +03:30
} from '@/api/customerDirectSms'
import { getConfiguration } from '@/api/configuration'
const props = defineProps<{
visible: boolean
customer: Customer | null
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'sent'): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (v: boolean) => emit('update:visible', v)
})
const loading = ref(false)
2026-07-19 01:11:37 +03:30
const activeTab = ref<'templates' | 'manual'>('templates')
const selectedTemplateKey = ref(DIRECT_SMS_TEMPLATES[0]?.key ?? '')
const manualBody = ref('')
2026-07-18 22:22:21 +03:30
const appName = ref('')
const appUrl = ref('')
2026-07-19 01:11:37 +03:30
const templates = DIRECT_SMS_TEMPLATES
2026-07-18 22:22:21 +03:30
const modalTitle = computed(() =>
props.customer?.name ? `پیامک به ${props.customer.name}` : 'ارسال پیامک به مشتری'
)
2026-07-19 01:11:37 +03:30
const customerDisplayName = computed(
() => (props.customer?.name ?? '').trim() || 'مشتری'
2026-07-18 22:22:21 +03:30
)
2026-07-19 01:11:37 +03:30
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))
2026-07-18 22:22:21 +03:30
const canSubmit = computed(
2026-07-19 01:11:37 +03:30
() =>
previewMessage.value.trim().length > 0 &&
Boolean(bodyText.value) &&
!profanityHit.value &&
!previewOverLimit.value &&
!loading.value
2026-07-18 22:22:21 +03:30
)
watch(
() => props.visible,
async (open) => {
if (!open) return
2026-07-19 01:11:37 +03:30
activeTab.value = 'templates'
manualBody.value = ''
selectedTemplateKey.value = DIRECT_SMS_TEMPLATES[0]?.key ?? ''
2026-07-18 22:22:21 +03:30
try {
const rows = await getConfiguration()
const list = Array.isArray(rows) ? rows : []
appName.value = String(list.find((r) => r.key === 'name_fa')?.value ?? '').trim()
appUrl.value = String(list.find((r) => r.key === 'app_url')?.value ?? '').trim()
} catch {
appName.value = ''
appUrl.value = ''
}
2026-07-19 01:11:37 +03:30
// اگر قالب اول به لینک نیاز دارد و لینک نیست، اولین قالب قابل‌استفاده را انتخاب کن
const firstOk = templates.find((t) => !isTemplateDisabled(t))
if (firstOk) selectedTemplateKey.value = firstOk.key
2026-07-18 22:22:21 +03:30
}
)
function close() {
visibleProxy.value = false
}
async function submit() {
if (!props.customer?.id || !canSubmit.value) return
2026-07-19 01:11:37 +03:30
const finalMessage = previewMessage.value.trim()
const hit = findProfanity(finalMessage)
2026-07-18 22:22:21 +03:30
if (hit) {
antMessage.error('متن حاوی محتوای نامناسب است')
return
}
try {
loading.value = true
await sendCustomerDirectSms({
customer_id: Number(props.customer.id),
2026-07-19 01:11:37 +03:30
message: finalMessage
2026-07-18 22:22:21 +03:30
})
antMessage.success('پیامک ارسال شد')
emit('sent')
close()
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
</script>