Files
boursecu_admin/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue
T
2026-07-19 01:11:37 +03:30

272 lines
9.1 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<BasicModal
v-model:visible="visibleProxy"
:width="isMobile ? '94vw' : '40rem'"
: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>
<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>
<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>
<p v-if="profanityHit" class="m-0 text-xs text-red-500">
متن حاوی عبارت نامناسب است — قبل از ارسال اصلاح کنید.
</p>
<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">
نام اپ در تنظیمات خالی است؛ در پایان پیام از «اپ» استفاده می‌شود.
</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 {
DIRECT_SMS_MAX_LENGTH,
DIRECT_SMS_TEMPLATES,
findProfanity,
formatDirectSms,
resolveTemplateBody,
sendCustomerDirectSms,
type DirectSmsTemplate
} 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)
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 customerDisplayName = computed(
() => (props.customer?.name ?? '').trim() || 'مشتری'
)
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(
() =>
previewMessage.value.trim().length > 0 &&
Boolean(bodyText.value) &&
!profanityHit.value &&
!previewOverLimit.value &&
!loading.value
)
watch(
() => props.visible,
async (open) => {
if (!open) return
activeTab.value = 'templates'
manualBody.value = ''
selectedTemplateKey.value = DIRECT_SMS_TEMPLATES[0]?.key ?? ''
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 = ''
}
// اگر قالب اول به لینک نیاز دارد و لینک نیست، اولین قالب قابل‌استفاده را انتخاب کن
const firstOk = templates.find((t) => !isTemplateDisabled(t))
if (firstOk) selectedTemplateKey.value = firstOk.key
}
)
function close() {
visibleProxy.value = false
}
async function submit() {
if (!props.customer?.id || !canSubmit.value) return
const finalMessage = previewMessage.value.trim()
const hit = findProfanity(finalMessage)
if (hit) {
antMessage.error('متن حاوی محتوای نامناسب است')
return
}
try {
loading.value = true
await sendCustomerDirectSms({
customer_id: Number(props.customer.id),
message: finalMessage
})
antMessage.success('پیامک ارسال شد')
emit('sent')
close()
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
</script>