From 3c41a3d23fe0286a13f79c843f68936c07149bb2 Mon Sep 17 00:00:00 2001 From: mohammad sedighi Date: Fri, 24 Jul 2026 17:01:40 +0330 Subject: [PATCH] fix notif --- src/api/appNotifications.ts | 6 +- src/model/appNotifications.ts | 13 ++++ .../appNotifications/components/table.vue | 12 ++- .../hooks/appNotificationsColumns.tsx | 10 +++ .../menuPages/appNotifications/index.vue | 76 ++++++++++++++++++- src/pages/menuPages/configuration/index.vue | 2 +- .../menuPages/configuration/settingGroups.ts | 20 +++-- .../menuPages/configuration/settingTypes.ts | 10 ++- .../orders/utils/orderDetailDisplay.ts | 4 +- .../base/components/ProductEditorFormBody.vue | 16 ++-- src/utils/productEditorPrice.ts | 2 +- 11 files changed, 147 insertions(+), 24 deletions(-) diff --git a/src/api/appNotifications.ts b/src/api/appNotifications.ts index d5ec3f0..125b83a 100644 --- a/src/api/appNotifications.ts +++ b/src/api/appNotifications.ts @@ -1,6 +1,6 @@ import type { TableInput } from '@core/type/baseTable' import useAxios from '@core/utils/useAxios' -import type { AppNotification } from '@/model/appNotifications' +import type { AppNotification, AppNotificationReadersResponse } from '@/model/appNotifications' export const getAppNotificationsTable = async (params: TableInput) => await useAxios.post>('/admin/app_notifications/table', params) @@ -16,3 +16,7 @@ export const toggleAppNotificationActive = async (id: number) => export const deleteAppNotification = async (id: number) => await useAxios.delete(`/admin/app_notifications/${id}`) + +export const getAppNotificationReaders = async (id: number) => + await useAxios.get(`/admin/app_notifications/${id}/readers`) + diff --git a/src/model/appNotifications.ts b/src/model/appNotifications.ts index 18bf27d..1bd36c1 100644 --- a/src/model/appNotifications.ts +++ b/src/model/appNotifications.ts @@ -7,6 +7,7 @@ export interface AppNotification { created_by?: number | null created_at?: string updated_at?: string + reads_count?: number } export const APP_NOTIFICATION_SEVERITIES = [ @@ -15,3 +16,15 @@ export const APP_NOTIFICATION_SEVERITIES = [ { value: 'warning', label: 'هشدار (warning)' }, { value: 'danger', label: 'مهم / خطر (danger)' }, ] as const + +export interface AppNotificationReader { + customer_id: number + name: string + mobile?: string | null + read_at?: string | null +} + +export interface AppNotificationReadersResponse { + reads_count: number + readers: AppNotificationReader[] +} diff --git a/src/pages/menuPages/appNotifications/components/table.vue b/src/pages/menuPages/appNotifications/components/table.vue index 0ee8748..ed36d9a 100644 --- a/src/pages/menuPages/appNotifications/components/table.vue +++ b/src/pages/menuPages/appNotifications/components/table.vue @@ -16,6 +16,15 @@ @@ -37,15 +75,17 @@ import { cloneDeep } from 'lodash' import tbl from './components/table.vue' import BasicActionSheet from '@/components/BasicActionSheet.vue' import AppNotificationEditorFormBody from './components/AppNotificationEditorFormBody.vue' -import type { AppNotification } from '@/model/appNotifications' +import type { AppNotification, AppNotificationReader } from '@/model/appNotifications' import { createAppNotification, deleteAppNotification, + getAppNotificationReaders, toggleAppNotificationActive, updateAppNotification, } from '@/api/appNotifications' import { confirmDeleteItem } from '@/utils/confirmModal' import { isMobile } from '@core/utils/is' +import { toJalali } from '@core/utils/useDataTime' const tableRef = ref void }>(null) const editorRef = ref<{ @@ -57,6 +97,11 @@ const loading = ref(false) const editorVisible = ref(false) const editId = ref() +const readersVisible = ref(false) +const readersLoading = ref(false) +const readers = ref([]) +const readersNotifTitle = ref('') + const defaultForm = (): Partial => ({ title: '', body: '', @@ -67,6 +112,9 @@ const defaultForm = (): Partial => ({ const form = ref>(defaultForm()) const sheetTitle = computed(() => (editId.value ? 'ویرایش اعلان' : 'افزودن اعلان')) +const readersTitle = computed(() => + readersNotifTitle.value ? `بازدیدکنندگان — ${readersNotifTitle.value}` : 'بازدیدکنندگان اعلان' +) function refreshTable() { tableRef.value?.fetch() @@ -94,6 +142,24 @@ function openEdit(record: AppNotification) { editorVisible.value = true } +async function openReaders(record: AppNotification) { + if (!record.id) return + readersNotifTitle.value = record.title || '' + readersVisible.value = true + readersLoading.value = true + readers.value = [] + try { + const data = await getAppNotificationReaders(record.id) + readers.value = Array.isArray(data?.readers) ? data.readers : [] + refreshTable() + } catch (e) { + console.error(e) + message.error('بارگذاری لیست بازدیدکنندگان ناموفق بود') + } finally { + readersLoading.value = false + } +} + async function submitForm() { try { await editorRef.value?.validateFields?.() @@ -142,3 +208,11 @@ async function deleteItem(id: number) { confirmDeleteItem('اعلان', id, deleteAppNotification, refreshTable) } + + diff --git a/src/pages/menuPages/configuration/index.vue b/src/pages/menuPages/configuration/index.vue index f8859ec..27f33cf 100644 --- a/src/pages/menuPages/configuration/index.vue +++ b/src/pages/menuPages/configuration/index.vue @@ -322,7 +322,7 @@ const defaultOpenGroupId = computed(() => { const visibleIds = visibleSettingGroups.value.map((g) => g.id) const preferred = isSystemMode.value ? ['chart', 'pricing', 'payping', 'accounting', 'store', 'general'] - : ['general', 'content', 'pricing', 'price_source', 'referral'] + : ['general', 'content', 'fees', 'limits', 'price_source', 'referral'] const fromPreferred = preferred.find((id) => visibleIds.includes(id)) return fromPreferred ?? visibleIds[0] }) diff --git a/src/pages/menuPages/configuration/settingGroups.ts b/src/pages/menuPages/configuration/settingGroups.ts index e0808c0..c59e7a0 100644 --- a/src/pages/menuPages/configuration/settingGroups.ts +++ b/src/pages/menuPages/configuration/settingGroups.ts @@ -27,16 +27,24 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [ id: 'trading_hours', title: 'ساعت کاری و نمایش قیمت', icon: 'vuesax-linear:clock', - match: (k) => /^(close_price|startDay|endDay|show_last_price|show_gold_ounce_price)$/.test(k) + match: (k) => + /^(close_price|startDay|endDay|show_last_price|show_home_price_when_closed|show_gold_ounce_price)$/.test( + k + ) }, { - id: 'pricing', - title: 'کارمزد و محدودیت‌ها', + id: 'fees', + title: 'کارمزدها', icon: 'vuesax-linear:dollar-circle', match: (k) => - /^(fee_|buy_fee|sell_fee|min_|minimum_|up_price|down_price|price_extreme_date|balance_report_exclude)/.test( - k - ) || k.includes('charge_percentage') + /^(fee_|buy_fee|sell_fee|up_price|down_price|price_extreme_date)/.test(k) || + k.includes('charge_percentage') + }, + { + id: 'limits', + title: 'محدودیت‌ها', + icon: 'vuesax-linear:lock', + match: (k) => /^(min_|minimum_|balance_report_exclude)/.test(k) }, { id: 'price_source', diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index 9d3e99a..d810f12 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -266,6 +266,7 @@ const KEY_TYPE_FALLBACK: Record = { startDay: 'time', endDay: 'time', show_last_price: 'switch', + show_home_price_when_closed: 'switch', show_gold_ounce_price: 'switch', send_to_tahesab: 'switch', accounting_provider: 'select', @@ -549,13 +550,15 @@ const KEY_HELP_FALLBACK: Record = { auto_active_customer: 'روشن: بعد از ثبت‌نام، کاربر می‌تواند وارد اپ شود (بدون تأیید دستی ادمین). خاموش: ورود فقط بعد از فعال‌سازی دستی شما در پنل. این سوییچ اجازهٔ خرید/فروش نمی‌دهد — معامله فقط بعد از احراز هویت (is_legal) ممکن است.', close_price: - 'فیلتر اصلی ساعت کاری فروشگاه. روشن: خرید، فروش و نمایش قیمت فقط بین «ساعت شروع» و «ساعت پایان» انجام می‌شود و خارج از این بازه به مشتری «فروشگاه بسته است» نشان داده می‌شود. خاموش: محدودیت ساعتی سراسری اعمال نمی‌شود (مگر برای محصولی ساعت اختصاصی تعریف کرده باشید).', + 'فیلتر اصلی ساعت کاری فروشگاه. روشن: خرید و فروش فقط بین «ساعت شروع» و «ساعت پایان» انجام می‌شود. نمایش قیمت صفحه اصلی در خارج از این بازه با سوییچ «نمایش قیمت صفحه اصلی خارج از ساعت کاری» کنترل می‌شود. خاموش: محدودیت ساعتی سراسری اعمال نمی‌شود (مگر برای محصولی ساعت اختصاصی تعریف کرده باشید).', startDay: 'ساعت شروع فعالیت روزانه فروشگاه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال می‌شود. از ساعت‌شمار انتخاب کنید (مثلاً ۰۹:۰۰).', endDay: 'ساعت پایان فعالیت روزانه فروشگاه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال می‌شود. بعد از این ساعت فروشگاه بسته می‌شود (مثلاً ۱۷:۰۰ یا ۲۲:۰۰).', show_last_price: 'رفتار هنگام در دسترس نبودن قیمت زنده. روشن: آخرین قیمت ذخیره‌شده نشان داده می‌شود. خاموش: به‌جای صفر پیام «مظنه در دسترس نیست» نمایش داده می‌شود.', + show_home_price_when_closed: + 'روشن: وقتی فروشگاه خارج از ساعت کاری است، قیمت صفحه اصلی اپ همچنان نمایش داده می‌شود و پیام «فروشگاه بسته است» نشان داده نمی‌شود. خرید/فروش همچنان بسته می‌ماند. خاموش (پیش‌فرض): خارج از ساعت پیام بسته بودن نشان داده می‌شود.', show_gold_ounce_price: 'روشن: اگر آدرس API تابلو (zaarbox_api_url) تنظیم شده باشد، قیمت انس طلا در اپ مشتری نمایش داده می‌شود. خاموش: نمایش داده نمی‌شود.', fee_single: @@ -785,7 +788,7 @@ const KEY_HELP_FALLBACK: Record = { price_channel_messengers: 'کدام پیام‌رسان‌ها قیمت را دریافت کنند؟ می‌توانید چند مورد را انتخاب کنید (بله، تلگرام، روبیکا). برای هر پیام‌رسان باید توکن بات و «مقصد ارسال» هم پر شده باشد.', price_channel_diff: - 'اسپرد اضافه فقط برای کانال (ریال): به قیمت خرید کانال اضافه و از قیمت فروش کم می‌شود. صفر یعنی همان قیمت اپ.', + 'اسپرد اضافه فقط برای کانال (ریال): به قیمت خرید کانال اضافه و از قیمت فروش کم می‌شود. صفر یعنی همان قیمت صفحه اصلی اپلیکیشن.', price_channel_min_seconds: 'حداقل فاصله (ثانیه) بین دو ارسال متوالی به کانال. جلوی سیل پیام هنگام نوسان قیمت را می‌گیرد. صفر = هر تغییر قیمت ارسال شود.', price_channel_footer_id: @@ -814,6 +817,7 @@ export const KEY_LABEL_FALLBACK: Record = { startDay: 'ساعت شروع', endDay: 'ساعت پایان', show_last_price: 'نمایش آخرین قیمت ذخیره‌شده', + show_home_price_when_closed: 'نمایش قیمت صفحه اصلی خارج از ساعت کاری', show_gold_ounce_price: 'نمایش قیمت انس طلا', fee_single: 'اختلاف پایه خرید/فروش', buy_fee: 'اضافه قیمت خرید', @@ -974,7 +978,7 @@ export const SETTING_EDITABLE_KEYS = new Set([ 'open_register', 'referral_id', 'referral_limit', 'referral_gift_on_first_order', 'register_jibit_verify', 'register_referral_bonus_message', 'register_referral_sms_to_referrer', - 'sell_fee', 'show_last_price', 'show_gold_ounce_price', 'startDay', + 'sell_fee', 'show_last_price', 'show_home_price_when_closed', 'show_gold_ounce_price', 'startDay', 'support_messengers', 'support_messenger_text', 'deposit_support_message_text', 'deposit_account_holder', 'deposit_card_number', 'deposit_sheba', 'text_about', 'text_rule', diff --git a/src/pages/menuPages/orders/utils/orderDetailDisplay.ts b/src/pages/menuPages/orders/utils/orderDetailDisplay.ts index 492dd34..070f79e 100644 --- a/src/pages/menuPages/orders/utils/orderDetailDisplay.ts +++ b/src/pages/menuPages/orders/utils/orderDetailDisplay.ts @@ -42,8 +42,8 @@ export function resolveOrderFeeSourceLabel(order: Order): string { if (order.fee_source === 'app+product') { return order.direction === 2 - ? 'قیمت اپ (sell_fee) + تب اختلاف محصول' - : 'قیمت اپ (buy_fee) + تب اختلاف محصول' + ? 'قیمت صفحه اصلی اپلیکیشن (sell_fee) + تب اختلاف محصول' + : 'قیمت صفحه اصلی اپلیکیشن (buy_fee) + تب اختلاف محصول' } if (order.fee_source === 'settings') { const key = order.fee_source_key || (order.direction === 2 ? 'sell_fee' : 'buy_fee') diff --git a/src/pages/menuPages/products/base/components/ProductEditorFormBody.vue b/src/pages/menuPages/products/base/components/ProductEditorFormBody.vue index 6cc7606..cb06568 100644 --- a/src/pages/menuPages/products/base/components/ProductEditorFormBody.vue +++ b/src/pages/menuPages/products/base/components/ProductEditorFormBody.vue @@ -23,7 +23,7 @@ - قیمت اپ + قیمت صفحه اصلی اپلیکیشن ثابت مرجع متفاوت @@ -70,7 +70,7 @@
@@ -91,7 +91,7 @@ دستی اتوماتیک - قیمت اپ + قیمت صفحه اصلی اپلیکیشن
@@ -145,7 +145,7 @@
@@ -496,11 +496,11 @@ import { computed, onMounted, ref, watch } from 'vue' const priceSourceHelp: DashboardHelp = { description: - 'دستی: خودتان قیمت را وارد می‌کنید. اتوماتیک: قیمت از مرجع بیرونی (zaarbox) خوانده می‌شود. قیمت اپ: از قیمت زندهٔ صفحهٔ اصلی اپ بر اساس وزن محاسبه می‌شود (هر گرم × وزن محصول).', + 'دستی: خودتان قیمت را وارد می‌کنید. اتوماتیک: قیمت از مرجع بیرونی (zaarbox) خوانده می‌شود. قیمت صفحه اصلی اپلیکیشن: از قیمت زندهٔ صفحهٔ اصلی اپ بر اساس وزن محاسبه می‌شود (هر گرم × وزن محصول).', } const meltedPriceSourceHelp: DashboardHelp = { - description: 'قیمت اپ: از صفحه اصلی اپ. ثابت: عدد دستی. مرجع متفاوت: انتخاب منبع دیگر.', + description: 'قیمت صفحه اصلی اپلیکیشن: از صفحه اصلی اپ. ثابت: عدد دستی. مرجع متفاوت: انتخاب منبع دیگر.', } const manualPriceHelp: DashboardHelp = { @@ -588,8 +588,8 @@ const referencePriceOffsetHelp: DashboardHelp = { } const appPriceOffsetHelp: DashboardHelp = { - description: 'قیمت خرید/فروش از قیمت صفحه اصلی اپ محاسبه می‌شود.', - formula: 'مثبت = گران‌تر، منفی = ارزان‌تر نسبت به قیمت اپ', + description: 'قیمت خرید/فروش از قیمت صفحه اصلی اپلیکیشن محاسبه می‌شود.', + formula: 'مثبت = گران‌تر، منفی = ارزان‌تر نسبت به قیمت صفحه اصلی اپلیکیشن', } const useLastPriceIfZeroHelp: DashboardHelp = { diff --git a/src/utils/productEditorPrice.ts b/src/utils/productEditorPrice.ts index 69256bd..837f9dc 100644 --- a/src/utils/productEditorPrice.ts +++ b/src/utils/productEditorPrice.ts @@ -192,7 +192,7 @@ export function computeProductEditorPricePreview(input: { source: 'آخرین قیمت ذخیره‌شده × وزن' } } - return { status: 'empty', message: 'قیمت اپ در دسترس نیست' } + return { status: 'empty', message: 'قیمت صفحه اصلی اپلیکیشن در دسترس نیست' } } if (!input.form.reading_method) {