From de97d7caa6cce4c2509e6375439a1422777cac5b Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 10 Aug 2026 16:48:53 +0330 Subject: [PATCH] Add customer multi-level referral profit editor and related settings UI. Expose enable_customer_level_profit in config and let admins edit per-customer level percents from the customers table/mobile sheets. Co-authored-by: Cursor --- src/api/chatFocus.ts | 4 +- src/core/utils/useAxios.ts | 34 +++-- src/model/customer.ts | 7 + .../menuPages/configuration/settingGroups.ts | 2 +- .../menuPages/configuration/settingTypes.ts | 20 ++- .../menuPages/configuration/templateFields.ts | 4 +- .../components/CustomerLevelProfitModal.vue | 144 ++++++++++++++++++ .../components/CustomerMobileCardList.vue | 3 + .../components/CustomerMobileDetailSheet.vue | 12 ++ .../customers/base/components/table.vue | 26 ++++ src/utils/adminChat.ts | 2 +- src/utils/config.ts | 15 ++ 12 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 src/pages/menuPages/customers/base/components/CustomerLevelProfitModal.vue diff --git a/src/api/chatFocus.ts b/src/api/chatFocus.ts index 7c80ebf..2d2d216 100644 --- a/src/api/chatFocus.ts +++ b/src/api/chatFocus.ts @@ -14,4 +14,6 @@ export const postAdminChatUnfocus = async (room_id: string) => await useAxios.post('/admin/chat/unfocus', { room_id }) export const postAdminChatEnsureWebhook = async () => - await useAxios.post('/admin/chat/ensure-webhook') + await useAxios.post('/admin/chat/ensure-webhook', undefined, { + meta: { silent: true }, + }) diff --git a/src/core/utils/useAxios.ts b/src/core/utils/useAxios.ts index 0e04dda..fb8b38a 100644 --- a/src/core/utils/useAxios.ts +++ b/src/core/utils/useAxios.ts @@ -87,29 +87,35 @@ _axios.interceptors.response.use( await router.push({ name: 'login' }) } else if (error.response && error.response.status == 403) { if (!error.config?.meta?.silent) { - message.error('شما دسترسی لازم برای انجام این عملیات را ندارید') + message.error('شما دسترسی لازم برای انجام این عملیات را ندارید (403)') } } else if (error.response && error.response.status == 422) { - if (!error.config?.meta?.silent) { - message.error(error.response.data?.message) - } - } else if (error.response) { if (!error.config?.meta?.silent) { const apiMessage = error.response.data?.message message.error( typeof apiMessage === 'string' && apiMessage.trim() - ? formatApiErrorMessage(apiMessage) - : 'خطا در بارگذاری اطلاعات', + ? `${apiMessage} (422)` + : 'خطا در اعتبارسنجی (422)', ) } + } else if (error.response) { + if (!error.config?.meta?.silent) { + const status = Number(error.response.status) + if (status >= 500) { + message.error(`خطا در برقراری ارتباط با سرور (${status})`) + } else { + const apiMessage = error.response.data?.message + const base = + typeof apiMessage === 'string' && apiMessage.trim() + ? formatApiErrorMessage(apiMessage) + : 'خطا در بارگذاری اطلاعات' + message.error(`${base} (${status})`) + } + } + } else if (!error.config?.meta?.silent) { + // بدون status (نه 4xx نه 5xx): قطع شبکه، timeout، DNS، … + message.error('لطفا اینترنت خود را بررسی کنید و مجددا تلاش کنید') } - // Any status codes that falls outside the range of 2xx cause this function to trigger - // Do something with response error - - //const { response, code, message, config } = error || {} - //const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none' - //const msg: string = response?.data?.error?.message ?? '' - //checkStatus(error?.response?.status, msg) return Promise.reject(error) } ) diff --git a/src/model/customer.ts b/src/model/customer.ts index 427eb06..4847add 100644 --- a/src/model/customer.ts +++ b/src/model/customer.ts @@ -41,5 +41,12 @@ export interface Customer { gold: number moshtari_code?: string | number | null pargar_account_id?: string | number | null + /** درصد سود چندسطحی از کارمزد زیرمجموعه‌ها — `{ level, present }` */ + profit_amount_by_lvl?: ProfitByLevel[] // print_name: string } + +export interface ProfitByLevel { + level: number + present: number +} diff --git a/src/pages/menuPages/configuration/settingGroups.ts b/src/pages/menuPages/configuration/settingGroups.ts index 55713fa..910f8e9 100644 --- a/src/pages/menuPages/configuration/settingGroups.ts +++ b/src/pages/menuPages/configuration/settingGroups.ts @@ -134,7 +134,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [ title: 'دسترسی مشتری به قابلیت‌ها', icon: 'vuesax-linear:people', match: (k) => - /^(enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|tracking_systems_enabled|permision__enabled|show_to_users_chat)$/.test( + /^(enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|tracking_systems_enabled|permision__enabled|show_to_users_chat)$/.test( k ) }, diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index fa13a1a..26884de 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -263,6 +263,8 @@ const KEY_TYPE_FALLBACK: Record = { enable_product_funds: 'switch', has_page_price_trigger: 'switch', has_page_notification: 'switch', + has_page_gift_card: 'switch', + enable_customer_level_profit: 'switch', min_withdraw: 'price', min_deposit: 'price', min_first_buy: 'price', @@ -363,6 +365,7 @@ const KEY_TYPE_FALLBACK: Record = { deposit_support_message_text: 'longtext', deposit_account_holder: 'text', deposit_card_number: 'text', + deposit_account_number: 'text', deposit_sheba: 'text', invite_link_text: 'longtext', support_messenger: 'select', @@ -649,6 +652,10 @@ const KEY_HELP_FALLBACK: Record = { 'روشن: قابلیت سفارش شرطی سراسری فعال است. خاموش: منو و ارزیابی اجرا نمی‌شود. فقط از صفحه دیپلوی قابل تغییر است.', enable_product_funds: 'روشن: ساخت و مدیریت قلک per-product در اپ مشتری فعال است (یک محصول در هر قلک؛ انتقال بدون سند حسابداری). خاموش: منو و API ساخت قلک جدید در دسترس نیست. فقط از دیپلوی قابل تغییر است.', + has_page_gift_card: + 'روشن: منوی «ثبت کارت هدیه» و صفحه ثبت کد در اپ کاربر فعال است. خاموش: منو و صفحه مخفی است و API استفاده از کد هدیه در دسترس نیست. فقط از دیپلوی قابل تغییر است.', + enable_customer_level_profit: + 'روشن: در لیست مشتریان آیکون تنظیم درصد سود هر سطح زیرمجموعه فعال است و پس از سفارش، سهم کارمزد طبق درصدهای هر معرف به کیف طلا واریز می‌شود. خاموش: آیکون و واریز سود چندسطحی غیرفعال است. فقط از دیپلوی قابل تغییر است.', has_page_price_trigger: 'روشن: منوی هشدار قیمت در اپ مشتری نمایش داده می‌شود. خاموش: منو و صفحه مخفی است. فقط از صفحه دیپلوی قابل تغییر است.', has_page_notification: @@ -756,13 +763,15 @@ const KEY_HELP_FALLBACK: Record = { support_messenger_id: 'شناسه تماس در پیام‌رسان انتخاب‌شده: یوزرنیم، آیدی یا لینک کامل. فقط روی آیکون پیام‌رسان و دکمه شناور اعمال می‌شود؛ تماس تلفنی از فیلد «شماره پشتیبانی» خوانده می‌شود.', support_messenger_text: - 'متن راهنمای واریز در مودال شارژ حساب. متن را بنویسید و متغیرها (نام صاحب حساب، شماره کارت، شبا) را با کلیک اضافه کنید؛ پیش‌نمایش نتیجه را با مقادیر نمونه نشان می‌دهد. خالی = متن پیش‌فرض اپ.', + 'متن راهنمای واریز در مودال شارژ حساب. متن را بنویسید و متغیرها (نام صاحب حساب، شماره کارت، شماره حساب، شبا) را با کلیک اضافه کنید؛ پیش‌نمایش نتیجه را با مقادیر نمونه نشان می‌دهد. خالی = متن پیش‌فرض اپ.', deposit_support_message_text: 'متن پیش‌نویس فقط وقتی کاربر از مودال راهنمای واریز روی «پیام به پشتیبانی» یا SMS می‌زند. منوی شناور پشتیبانی این متن را نمی‌گذارد. خالی = متن پیش‌فرض اپ.', deposit_account_holder: 'نام صاحب حساب واریز — در متن راهنمای واریز با ٪نام یا %نام جایگزین می‌شود. در پیام پشتیبانی فقط وقتی نام مشتری موجود نباشد استفاده می‌شود.', deposit_card_number: 'شماره کارت واریز — در متن‌ها با ٪کارت یا %کارت جایگزین می‌شود.', + deposit_account_number: + 'شماره حساب بانکی واریز — در مودال راهنمای شارژ کنار کارت و شبا نمایش داده می‌شود. در متن‌ها با ٪حساب یا %حساب جایگزین می‌شود.', deposit_sheba: 'شماره شبا واریز — در متن‌ها با ٪شبا یا %شبا جایگزین می‌شود.', deposit_sms_support_number: @@ -939,6 +948,8 @@ export const KEY_LABEL_FALLBACK: Record = { fee_conditional_order: 'کارمزد سفارش شرطی', enable_conditional_order: 'سفارش شرطی', enable_product_funds: 'قلک محصول', + has_page_gift_card: 'کارت هدیه', + enable_customer_level_profit: 'سود چندسطحی معرف', has_page_price_trigger: 'هشدار قیمت', has_page_notification: 'اعلان داخل اپ', jibit_enabled: 'احراز هویت جیبیت', @@ -1016,6 +1027,7 @@ export const KEY_LABEL_FALLBACK: Record = { deposit_support_message_text: 'متن پیام به پشتیبانی (واریز)', deposit_account_holder: 'نام صاحب حساب واریز', deposit_card_number: 'شماره کارت واریز', + deposit_account_number: 'شماره حساب واریز', deposit_sheba: 'شبا واریز', deposit_sms_support_number: 'شماره تماس پشتیبانی', price_channel_enabled: 'ارسال قیمت به کانال', @@ -1056,6 +1068,8 @@ export const SETTING_LOCKED_KEYS = new Set([ 'enable_product_funds', 'has_page_price_trigger', 'has_page_notification', + 'has_page_gift_card', + 'enable_customer_level_profit', 'jibit_enabled', 'has_page_earn_money', 'is_store', @@ -1103,7 +1117,7 @@ export const SETTING_EDITABLE_KEYS = new Set([ 'sms_notif_fund_withdraw_admin', 'sms_notif_fund_withdraw_customer', 'sms_notif_price_trigger', 'sms_notif_wallet_alert', 'sms_notif_support_admin', 'deposit_sms_support_number', 'deposit_support_message_text', - 'deposit_account_holder', 'deposit_card_number', 'deposit_sheba', + 'deposit_account_holder', 'deposit_card_number', 'deposit_account_number', 'deposit_sheba', 'endDay', 'fee_free', 'fee_service_charge_percentage', 'gift_for_referral', 'gift_for_referrer', @@ -1122,7 +1136,7 @@ export const SETTING_EDITABLE_KEYS = new Set([ 'startDay', 'support_messengers', 'support_messenger_text', 'has_chat', - 'deposit_support_message_text', 'deposit_account_holder', 'deposit_card_number', 'deposit_sheba', + 'deposit_support_message_text', 'deposit_account_holder', 'deposit_card_number', 'deposit_account_number', 'deposit_sheba', 'text_about', 'text_rule', 'zaarbox_price_key', 'price_channel_enabled', 'price_channel_messengers', 'price_channel_diff', diff --git a/src/pages/menuPages/configuration/templateFields.ts b/src/pages/menuPages/configuration/templateFields.ts index 3819749..6d37126 100644 --- a/src/pages/menuPages/configuration/templateFields.ts +++ b/src/pages/menuPages/configuration/templateFields.ts @@ -182,7 +182,7 @@ export const TEMPLATE_FIELD_CONFIGS: Record = { previewTitle: 'پیش‌نمایش راهنما', placeholder: 'متن راهنمای واریز در مودال شارژ حساب را بنویسید…', hint: 'روی هر متغیر کلیک کنید تا در محل نشانگر اضافه شود. اگر خالی بگذارید، متن پیش‌فرض اپ استفاده می‌شود.', - legend: 'مقادیر بالا نمونه‌اند؛ در اپ، نام/کارت/شبا از فیلدهای «نام صاحب حساب واریز»، «شماره کارت واریز» و «شبا واریز» پر می‌شوند.', + legend: 'مقادیر بالا نمونه‌اند؛ در اپ، نام/کارت/حساب/شبا از فیلدهای مربوطه پر می‌شوند.', rows: 8, defaultTemplate: DEPOSIT_GUIDE_DEFAULT, variableGroups: [ @@ -192,6 +192,7 @@ export const TEMPLATE_FIELD_CONFIGS: Record = { items: [ { token: 'نام', label: 'صاحب حساب', help: 'نام صاحب حساب (از فیلد «نام صاحب حساب واریز»)' }, { token: 'کارت', label: 'شماره کارت', help: 'شماره کارت (از فیلد «شماره کارت واریز»)' }, + { token: 'حساب', label: 'شماره حساب', help: 'شماره حساب (از فیلد «شماره حساب واریز»)' }, { token: 'شبا', label: 'شماره شبا', help: 'شماره شبا (از فیلد «شبا واریز»)' }, ], }, @@ -199,6 +200,7 @@ export const TEMPLATE_FIELD_CONFIGS: Record = { sampleValues: { نام: 'علی رضایی', کارت: '۶۰۳۷-۹۹۷۹-۱۲۳۴-۵۶۷۸', + حساب: '۰۱۰۸۱۷۹۰۹۰۰۲', شبا: 'IR۸۲۰۵۴۰۱۰۲۶۸۰۰۲۰۸۱۷۹۰۹۰۰۲', }, }, diff --git a/src/pages/menuPages/customers/base/components/CustomerLevelProfitModal.vue b/src/pages/menuPages/customers/base/components/CustomerLevelProfitModal.vue new file mode 100644 index 0000000..52be2fe --- /dev/null +++ b/src/pages/menuPages/customers/base/components/CustomerLevelProfitModal.vue @@ -0,0 +1,144 @@ + + + diff --git a/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue b/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue index 4b2816d..ae33fe4 100644 --- a/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue +++ b/src/pages/menuPages/customers/base/components/CustomerMobileCardList.vue @@ -30,6 +30,7 @@ @confirmUser="forwardAndClose('confirmUser', $event)" @sendSms="forwardAndClose('sendSms', $event)" @editAccounting="forwardAndClose('editAccounting', $event)" + @levelProfit="forwardAndClose('levelProfit', $event)" /> @@ -61,6 +62,7 @@ const emit = defineEmits<{ (e: 'confirmUser', item: Customer): void (e: 'sendSms', item: Customer): void (e: 'editAccounting', item: Customer): void + (e: 'levelProfit', item: Customer): void }>() const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet() @@ -75,6 +77,7 @@ type SheetAction = | 'confirmUser' | 'sendSms' | 'editAccounting' + | 'levelProfit' 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 7df30ce..d12e2f4 100644 --- a/src/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue +++ b/src/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue @@ -53,6 +53,15 @@ ویرایش + + سود چندسطحی + بایگانی @@ -121,6 +130,7 @@ import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBala import { accountingProviderSetting, customerDirectSmsEnabledSetting, + customerLevelProfitEnabledSetting, initAccountingSettingsFromApi } from '@/utils/config' import { displayAccountingCode } from '@/utils/accountingCode' @@ -142,10 +152,12 @@ const emit = defineEmits<{ (e: 'confirmUser', item: Customer): void (e: 'sendSms', item: Customer): void (e: 'editAccounting', item: Customer): void + (e: 'levelProfit', item: Customer): void }>() const chargeTargets = ref([]) const customerDirectSmsEnabled = customerDirectSmsEnabledSetting +const customerLevelProfitEnabled = customerLevelProfitEnabledSetting const accountingCodeLabel = computed(() => { const provider = accountingProviderSetting.value diff --git a/src/pages/menuPages/customers/base/components/table.vue b/src/pages/menuPages/customers/base/components/table.vue index 2fc5034..50a7850 100644 --- a/src/pages/menuPages/customers/base/components/table.vue +++ b/src/pages/menuPages/customers/base/components/table.vue @@ -108,6 +108,15 @@ + + + + + + () const pargarEnabled = pargarEnabledSetting const customerDirectSmsEnabled = customerDirectSmsEnabledSetting +const customerLevelProfitEnabled = customerLevelProfitEnabledSetting +const levelProfitVisible = ref(false) +const levelProfitCustomer = ref(null) const accountingCodeVisible = ref(false) const accountingCodeCustomer = ref(null) const accountingEnabled = computed(() => { @@ -329,6 +350,11 @@ function openDirectSms(user: Customer) { directSmsVisible.value = true } +function openLevelProfit(user: Customer) { + levelProfitCustomer.value = user + levelProfitVisible.value = true +} + async function changeCustomerActivationFunction(id: number) { try { loading.value = true diff --git a/src/utils/adminChat.ts b/src/utils/adminChat.ts index 9a6daf7..e1d0061 100644 --- a/src/utils/adminChat.ts +++ b/src/utils/adminChat.ts @@ -1707,7 +1707,7 @@ export const bootstrapAdminChat = async ( bootstrapPromise = (async () => { try { void postAdminChatEnsureWebhook().catch((error) => { - console.error('admin chat ensure webhook failed:', error) + console.warn('admin chat ensure webhook failed:', error) }) await fetchAdminRooms() diff --git a/src/utils/config.ts b/src/utils/config.ts index a3aa68e..d74a450 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -49,6 +49,9 @@ export const conditionalOrderEnabledSetting = ref(false) /** reactive — قلک per-product در اپ مشتری */ export const productFundsEnabledSetting = ref(false) +/** reactive — سود چندسطحی معرف در لیست مشتریان */ +export const customerLevelProfitEnabledSetting = ref(false) + /** reactive — منو و UI هشدار قیمت */ export const priceTriggerEnabledSetting = ref(false) @@ -154,6 +157,10 @@ export function setProductFundsEnabledFromSettings(value: boolean): void { productFundsEnabledSetting.value = value } +export function setCustomerLevelProfitEnabledFromSettings(value: boolean): void { + customerLevelProfitEnabledSetting.value = value +} + export function setPriceTriggerEnabledFromSettings(value: boolean): void { priceTriggerEnabledSetting.value = value } @@ -257,6 +264,13 @@ export function applyProductFundsEnabledFromRows( if (row) setProductFundsEnabledFromSettings(isSwitchOn(row.value)) } +export function applyCustomerLevelProfitEnabledFromRows( + rows: Array<{ key?: string; value?: string | null }> +) { + const row = rows.find((item) => item.key === 'enable_customer_level_profit') + if (row) setCustomerLevelProfitEnabledFromSettings(isSwitchOn(row.value)) +} + export function applyPriceTriggerEnabledFromRows( rows: Array<{ key?: string; value?: string | null }> ) { @@ -305,6 +319,7 @@ async function bootstrapSettingsFromApi(): Promise { applyCustomerDirectSmsFromRows(list) applyConditionalOrderEnabledFromRows(list) applyProductFundsEnabledFromRows(list) + applyCustomerLevelProfitEnabledFromRows(list) applyPriceTriggerEnabledFromRows(list) applyAppNotificationEnabledFromRows(list) applySupportChatMenuEnabledFromRows(list)