From a2f3e320d56125b4fac85435f1418d8f08bd5bb2 Mon Sep 17 00:00:00 2001 From: adonis Date: Sat, 18 Jul 2026 22:22:21 +0330 Subject: [PATCH] fix some bugs 4 --- src/api/customerDirectSms.ts | 55 +++++++ .../menuPages/configuration/settingGroups.ts | 8 +- .../menuPages/configuration/settingTypes.ts | 76 ++++++++- .../components/CustomerDirectSmsModal.vue | 145 ++++++++++++++++++ .../components/CustomerMobileCardList.vue | 3 + .../components/CustomerMobileDetailSheet.vue | 14 +- .../customers/base/components/table.vue | 30 +++- src/utils/config.ts | 20 ++- 8 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 src/api/customerDirectSms.ts create mode 100644 src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue diff --git a/src/api/customerDirectSms.ts b/src/api/customerDirectSms.ts new file mode 100644 index 0000000..62c7445 --- /dev/null +++ b/src/api/customerDirectSms.ts @@ -0,0 +1,55 @@ +import type { Customer } from '@/model/customer' +import useAxios from '@core/utils/useAxios' + +export const sendCustomerDirectSms = async (payload: { + customer_id: number + message: string +}) => useAxios.post<{ ok: boolean }>('/admin/messages/send_customer', payload) + +export type DirectSmsSnippet = { + key: string + label: string + insert: 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 !== '') +} + +const BLOCKED = [ + 'fuck', 'shit', 'bitch', 'asshole', 'bastard', 'dick', 'pussy', 'cunt', + 'motherfucker', 'nigger', 'faggot', 'slut', 'whore', + 'کیر', 'کس', 'کص', 'جنده', 'گایید', 'گاییدن', 'گایدم', 'بیناموس', + 'بی‌ناموس', 'بی ناموس', 'حرومزاده', 'حرامزاده', 'مادرجنده', 'ننه جنده', + 'کونی', 'کون', 'لاشی', 'عن', 'اشغال', 'دیوث', 'قرمساق' +] + +function normalize(text: string): string { + return text + .toLowerCase() + .replace(/[ي]/g, 'ی') + .replace(/[ك]/g, 'ک') + .replace(/[‌\u200c]/g, '') + .replace(/[^\p{L}\p{N}\s]+/gu, ' ') + .replace(/\s+/g, ' ') + .trim() +} + +export function findProfanity(message: string): string | null { + const normalized = normalize(message) + if (!normalized) return null + for (const word of BLOCKED) { + const needle = normalize(word) + if (needle && normalized.includes(needle)) return word + } + return null +} diff --git a/src/pages/menuPages/configuration/settingGroups.ts b/src/pages/menuPages/configuration/settingGroups.ts index c727ea5..b9697d3 100644 --- a/src/pages/menuPages/configuration/settingGroups.ts +++ b/src/pages/menuPages/configuration/settingGroups.ts @@ -92,7 +92,11 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [ id: 'notifications', title: 'پیامک و اعلان', icon: 'vuesax-linear:notification', - match: (k) => /^charge_notif/.test(k) + match: (k) => + k === 'charge_notif' || + k === 'group_sms' || + k === 'register_referral_sms_to_referrer' || + /^sms_notif_/.test(k) }, { id: 'features', @@ -115,7 +119,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [ title: 'عمومی و برند', icon: 'vuesax-linear:home-2', match: (k) => - /^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_enabled|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|has_page_earn_money|sms_panel)/.test( + /^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_enabled|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|customer_direct_sms|has_page_earn_money|sms_panel)/.test( k ) }, diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index 671dc2d..eceb22f 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -259,7 +259,23 @@ const KEY_TYPE_FALLBACK: Record = { payping_round_toman: 'switch', payping_strict_failed_withdraw: 'switch', has_page_earn_money: 'switch', + customer_direct_sms: 'switch', charge_notif: 'switch', + sms_notif_order_admin: 'switch', + sms_notif_order_customer: 'switch', + sms_notif_withdraw_admin: 'switch', + sms_notif_withdraw_otp: 'switch', + sms_notif_login_otp: 'switch', + sms_notif_gateway_charge_admin: 'switch', + sms_notif_register_customer: 'switch', + sms_notif_register_admin: 'switch', + sms_notif_jibit_admin: 'switch', + sms_notif_bank_card_admin: 'switch', + sms_notif_bank_card_customer: 'switch', + sms_notif_fund_withdraw_admin: 'switch', + sms_notif_fund_withdraw_customer: 'switch', + sms_notif_price_trigger: 'switch', + sms_notif_wallet_alert: 'switch', group_sms: 'switch', montzar_closed: 'switch', montzar_open_buy: 'switch', @@ -505,8 +521,10 @@ const KEY_HELP_FALLBACK: Record = { 'روشن: کاربران جدید می‌توانند ثبت‌نام کنند. خاموش: فقط ورود کاربران قبلی مجاز است.', sms_panel: 'روشن: پنل پیامک و کیف پول billing فعال است. خاموش: پیامک‌ها مثل قبل ارسال می‌شوند بدون billing.', + customer_direct_sms: + 'روشن: در لیست مشتریان دکمه ارسال پیامک تکی نمایش داده می‌شود و ادمین می‌تواند برای هر مشتری متن دلخواه بفرستد. خاموش: دکمه مخفی است. فقط از صفحه دیپلوی تغییر می‌کند.', auto_active_customer: - 'روشن: حساب بلافاصله پس از ثبت‌نام فعال می‌شود. خاموش: نیاز به تأیید دستی شما در پنل دارد.', + 'روشن: بعد از ثبت‌نام، کاربر می‌تواند وارد اپ شود (بدون تأیید دستی ادمین). خاموش: ورود فقط بعد از فعال‌سازی دستی شما در پنل. این سوییچ اجازهٔ خرید/فروش نمی‌دهد — معامله فقط بعد از احراز هویت (is_legal) ممکن است.', close_price: 'فیلتر اصلی ساعت کاری فروشگاه. روشن: خرید، فروش و نمایش قیمت فقط بین «ساعت شروع» و «ساعت پایان» انجام می‌شود و خارج از این بازه به مشتری «فروشگاه بسته است» نشان داده می‌شود. خاموش: محدودیت ساعتی سراسری اعمال نمی‌شود (مگر برای محصولی ساعت اختصاصی تعریف کرده باشید).', startDay: @@ -610,7 +628,37 @@ const KEY_HELP_FALLBACK: Record = { deposit_sms_support_number: 'شماره تماس پشتیبانی برای راهنمای واریز و دکمه پیامک داخل مودال شارژ. برای نمایش در منوی شناور، باید «تماس تلفنی» یا «پیامک» را جداگانه در لیست پیام‌رسان‌های پشتیبانی اضافه کنید.', charge_notif: - 'روشن: پس از شارژ کیف توسط ادمین، پیامک به مشتری ارسال می‌شود. خاموش: پیامکی پس از شارژ ارسال نمی‌شود.', + 'روشن: بعد از شارژ یا کسر کیف توسط ادمین، پیامک به مشتری و ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: هیچ‌کدام ارسال نمی‌شود.', + sms_notif_order_admin: + 'روشن: بعد از خرید یا فروش موفق، پیامک خلاصه معامله به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: ادمین پیامک معامله نمی‌گیرد.', + sms_notif_order_customer: + 'روشن: بعد از خرید یا فروش موفق، پیامک رسید (مقدار، مبلغ، موجودی) به مشتری می‌رود. خاموش: مشتری پیامک معامله نمی‌گیرد.', + sms_notif_withdraw_admin: + 'روشن: وقتی مشتری درخواست برداشت ریالی می‌دهد، پیامک به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: اعلان برداشت به ادمین نمی‌رود.', + sms_notif_withdraw_otp: + 'روشن: رمز یکبارمصرف برداشت برای مشتری پیامک می‌شود. خاموش: رمز پیامک نمی‌شود و برداشت با OTP عملاً کار نمی‌کند. خاموش کردن توصیه نمی‌شود.', + sms_notif_login_otp: + 'روشن: رمز یکبارمصرف ورود پیامک می‌شود. خاموش: ورود با OTP ممکن نیست. خاموش کردن توصیه نمی‌شود.', + sms_notif_gateway_charge_admin: + 'روشن: بعد از شارژ موفق از درگاه پرداخت، پیامک به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: اعلان شارژ درگاه نمی‌رود.', + sms_notif_register_customer: + 'روشن: بعد از ثبت‌نام، پیامک خوش‌آمد (نام کاربری و لینک ورود) به مشتری می‌رود. خاموش: پیامک خوش‌آمد ارسال نمی‌شود.', + sms_notif_register_admin: + 'روشن: وقتی کسی با کد معرف ثبت‌نام می‌کند و پیامک به معرف خاموش است، اعلان به ادمین می‌رود. خاموش: این اعلان ادمین نمی‌رود.', + sms_notif_jibit_admin: + 'روشن: نتیجه احراز هویت جیبیت در ثبت‌نام برای ادمین پیامک می‌شود. خاموش: اعلان جیبیت نمی‌رود.', + sms_notif_bank_card_admin: + 'روشن: ثبت کارت بانکی جدید یا شکست احراز کارت برای ادمین پیامک می‌شود. خاموش: اعلان کارت به ادمین نمی‌رود.', + sms_notif_bank_card_customer: + 'روشن: بعد از تایید کارت بانکی توسط ادمین، پیامک تایید به مشتری می‌رود. خاموش: مشتری پیامک تایید کارت نمی‌گیرد.', + sms_notif_fund_withdraw_admin: + 'روشن: برداشت از قلک برای ادمین پیامک می‌شود. خاموش: اعلان برداشت قلک به ادمین نمی‌رود.', + sms_notif_fund_withdraw_customer: + 'روشن: بعد از برداشت از قلک، پیامک رسید به مشتری می‌رود. خاموش: مشتری پیامک برداشت قلک نمی‌گیرد.', + sms_notif_price_trigger: + 'روشن: وقتی هشدار قیمت مشتری (اقدام=پیامک) فعال شود، پیامک به مشتری می‌رود. خاموش: هشدار قیمت پیامک نمی‌فرستد.', + sms_notif_wallet_alert: + 'روشن: وقتی اعتبار پنل پیامک صفر یا منفی شود، هشدار به سوپرادمین و ادمین‌ها می‌رود. خاموش: این هشدار نمی‌رود.', group_sms: 'روشن: امکان ارسال پیامک گروهی از پنل ادمین فعال است. خاموش: ارسال پیامک گروهی غیرفعال است.', profit_gold_api_enabled: @@ -729,6 +777,23 @@ export const KEY_LABEL_FALLBACK: Record = { referral_gift_on_first_order: 'پاداش معرف پس از اولین سفارش', open_register: 'ثبت‌نام باز', sms_panel: 'پنل پیامک (کیف پول)', + customer_direct_sms: 'پیامک تکی به مشتری از لیست', + charge_notif: 'پیامک شارژ ادمین', + sms_notif_order_admin: 'پیامک خرید/فروش به ادمین', + sms_notif_order_customer: 'پیامک خرید/فروش به مشتری', + sms_notif_withdraw_admin: 'پیامک درخواست برداشت به ادمین', + sms_notif_withdraw_otp: 'پیامک رمز برداشت', + sms_notif_login_otp: 'پیامک رمز ورود', + sms_notif_gateway_charge_admin: 'پیامک شارژ درگاه به ادمین', + sms_notif_register_customer: 'پیامک خوش‌آمد ثبت‌نام', + sms_notif_register_admin: 'پیامک ثبت‌نام به ادمین', + sms_notif_jibit_admin: 'پیامک احراز هویت به ادمین', + sms_notif_bank_card_admin: 'پیامک کارت بانکی به ادمین', + sms_notif_bank_card_customer: 'پیامک تایید کارت به مشتری', + sms_notif_fund_withdraw_admin: 'پیامک برداشت قلک به ادمین', + sms_notif_fund_withdraw_customer: 'پیامک برداشت قلک به مشتری', + sms_notif_price_trigger: 'پیامک هشدار قیمت', + sms_notif_wallet_alert: 'پیامک هشدار اعتبار پنل پیامک', auto_active_customer: 'فعال‌سازی خودکار پس از ثبت‌نام', balance_report_exclude_customer_id: 'حذف مشتری از گزارش تراز', chart_use_local_data: 'نمودار از داده داخلی', @@ -777,6 +842,7 @@ export const SETTING_LOCKED_KEYS = new Set([ 'external_order_api_enabled', 'fee_deduct_from_gold', 'group_sms', + 'customer_direct_sms', 'has_page_earn_money', 'is_store', 'sms_panel', @@ -811,6 +877,12 @@ export const SETTING_EDITABLE_KEYS = new Set([ 'name_fa', 'auto_active_customer', 'balance_report_exclude_customer_id', 'buy_fee', 'charge_notif', 'close_price', + 'sms_notif_order_admin', 'sms_notif_order_customer', + 'sms_notif_withdraw_admin', 'sms_notif_withdraw_otp', 'sms_notif_login_otp', + 'sms_notif_gateway_charge_admin', 'sms_notif_register_customer', 'sms_notif_register_admin', + 'sms_notif_jibit_admin', 'sms_notif_bank_card_admin', 'sms_notif_bank_card_customer', + 'sms_notif_fund_withdraw_admin', 'sms_notif_fund_withdraw_customer', + 'sms_notif_price_trigger', 'sms_notif_wallet_alert', 'deposit_sms_support_number', 'deposit_support_message_text', 'deposit_account_holder', 'deposit_card_number', 'deposit_sheba', 'endDay', diff --git a/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue b/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue new file mode 100644 index 0000000..f4250d7 --- /dev/null +++ b/src/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue @@ -0,0 +1,145 @@ + + + diff --git a/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue b/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue index 7cc9da5..e2e4686 100644 --- a/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue +++ b/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue @@ -28,6 +28,7 @@ @chargeWallet="forwardAndClose('chargeWallet', $event)" @toggleTrade="forward('toggleTrade', $event)" @confirmUser="forwardAndClose('confirmUser', $event)" + @sendSms="forwardAndClose('sendSms', $event)" /> @@ -57,6 +58,7 @@ const emit = defineEmits<{ (e: 'chargeWallet', item: Customer): void (e: 'toggleTrade', item: Customer): void (e: 'confirmUser', item: Customer): void + (e: 'sendSms', item: Customer): void }>() const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet() @@ -69,6 +71,7 @@ type SheetAction = | 'chargeWallet' | 'toggleTrade' | 'confirmUser' + | 'sendSms' function forward(action: SheetAction, item: Customer) { emit(action, item) diff --git a/src/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue b/src/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue index 03c1f50..3cbab5c 100644 --- a/src/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue +++ b/src/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue @@ -46,6 +46,15 @@ بایگانی + + پیامک + () const chargeTargets = ref([]) +const customerDirectSmsEnabled = customerDirectSmsEnabledSetting const visibleProxy = computed({ get: () => props.visible, @@ -122,7 +134,7 @@ const visibleProxy = computed({ const sheetTitle = computed(() => props.item?.name?.trim() || 'مشتری') -type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit' +type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit' | 'sendSms' function closeAndEmit(event: SheetEmit, row: Customer) { visibleProxy.value = false diff --git a/src/pages/menuPages/customers/base/components/table.vue b/src/pages/menuPages/customers/base/components/table.vue index 50c689a..67f959f 100644 --- a/src/pages/menuPages/customers/base/components/table.vue +++ b/src/pages/menuPages/customers/base/components/table.vue @@ -93,6 +93,15 @@ :color="record.trade_active === 1 ? '#54AD06' : '#ad0627'" /> + + + + + + () const pargarEnabled = pargarEnabledSetting +const customerDirectSmsEnabled = customerDirectSmsEnabledSetting +const directSmsVisible = ref(false) +const directSmsCustomer = ref(null) const mobileListRef = ref Promise }>(null) const tblProps = ref({ @@ -219,8 +241,14 @@ defineExpose({ fetch }) onMounted(() => { void initAccountingSettingsFromApi() + void initWalletSettingsFromApi() }) +function openDirectSms(user: Customer) { + directSmsCustomer.value = user + directSmsVisible.value = true +} + async function changeCustomerActivationFunction(id: number) { try { loading.value = true diff --git a/src/utils/config.ts b/src/utils/config.ts index 5edf3c0..ce651fe 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -28,16 +28,20 @@ let settingsBootstrapped = false /** reactive — برای فیلتر منوی «بنرها» در پیکربندی */ export const bannerEnabledSetting = ref(false) -/** reactive — نمایش UI ته‌حsاب در حسابداری و ... */ +/** reactive — نمایش UI ته‌حساب در حسابداری و ... */ export const tahesabEnabledSetting = ref(false) -/** reactive — نمایش UI پرگar در حسابداری و ... */ +/** reactive — نمایش UI پرگار در حسابداری و ... */ export const pargarEnabledSetting = ref(false) +/** reactive — دکمه پیامک تکی در لیست مشتریان */ +export const customerDirectSmsEnabledSetting = ref(false) + export const accountingProviderSetting = ref('none') let tahesabEnabledFromSettings: boolean | null = null let pargarEnabledFromSettings: boolean | null = null +let customerDirectSmsFromSettings: boolean | null = null let accountingProviderFromSettings: AccountingProvider | null = null function isSwitchOn(value: string | undefined | null): boolean { @@ -96,11 +100,21 @@ export function setPargarEnabledFromSettings(value: boolean): void { pargarEnabledSetting.value = value } +export function setCustomerDirectSmsEnabledFromSettings(value: boolean): void { + customerDirectSmsFromSettings = value + customerDirectSmsEnabledSetting.value = value +} + export function applyBannerEnabledFromRows(rows: Array<{ key?: string; value?: string | null }>) { const row = rows.find((item) => item.key === 'banner_enabled') setBannerEnabledFromSettings(row ? isSwitchOn(row.value) : false) } +export function applyCustomerDirectSmsFromRows(rows: Array<{ key?: string; value?: string | null }>) { + const row = rows.find((item) => item.key === 'customer_direct_sms') + setCustomerDirectSmsEnabledFromSettings(row ? isSwitchOn(row.value) : false) +} + async function bootstrapSettingsFromApi(): Promise { if (settingsBootstrapped) return @@ -115,12 +129,14 @@ async function bootstrapSettingsFromApi(): Promise { storeModeFromSettings = isStoreRow ? isSwitchOn(isStoreRow.value) : false applyAccountingProviderFromRows(list) applyBannerEnabledFromRows(list) + applyCustomerDirectSmsFromRows(list) setCachedMainMetal(mainMetalRow?.value) const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet') setCachedMainWalletProductIds(parseMainWalletProductIds(mainWalletRow?.value)) } catch { storeModeFromSettings = false setBannerEnabledFromSettings(false) + setCustomerDirectSmsEnabledFromSettings(false) applyAccountingProviderFromRows([]) setCachedMainMetal(null) const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')