From 7cd3fac28ba8d01bd88ad7b329c0f0481d86f8f9 Mon Sep 17 00:00:00 2001 From: adonis Date: Sat, 25 Jul 2026 12:13:30 +0330 Subject: [PATCH] fix pargar mande --- src/api/appNotifications.ts | 24 +++++ src/app/setupRouter.ts | 22 +++++ src/composables/useAppNotificationBadge.ts | 61 ++++++++++++ src/layouts/desktop/DesktopSidebar.vue | 10 +- src/layouts/mobile/MobileOverflowMenu.vue | 7 +- src/router/index.ts | 5 + src/store/settingsStore.ts | 13 +++ .../composables/useAppNotificationsPage.ts | 94 +++++++++++++++++++ .../desktop/DesktopAppNotificationsView.vue | 88 +++++++++++++++++ src/views/base/appNotifications/index.vue | 23 +++++ .../mobile/MobileAppNotificationsView.vue | 86 +++++++++++++++++ src/views/base/fund/info/components/Item.vue | 4 +- .../components/TransactionDetailContent.vue | 8 +- src/views/base/home/components/AppHeader.vue | 70 ++++++++++++-- .../home/components/DesktopHomeTopbar.vue | 48 ++++++++++ .../desktop/components/DesktopHomeTopbar.vue | 48 ++++++++++ .../components/TransactionDetailPanel.vue | 8 +- .../base/user/composables/useUserPage.ts | 4 +- src/views/base/user/mobile/MobileUserView.vue | 8 +- src/views/base/wallet/components/Item.vue | 18 +--- 20 files changed, 608 insertions(+), 41 deletions(-) create mode 100644 src/api/appNotifications.ts create mode 100644 src/composables/useAppNotificationBadge.ts create mode 100644 src/views/base/appNotifications/composables/useAppNotificationsPage.ts create mode 100644 src/views/base/appNotifications/desktop/DesktopAppNotificationsView.vue create mode 100644 src/views/base/appNotifications/index.vue create mode 100644 src/views/base/appNotifications/mobile/MobileAppNotificationsView.vue diff --git a/src/api/appNotifications.ts b/src/api/appNotifications.ts new file mode 100644 index 0000000..099fd86 --- /dev/null +++ b/src/api/appNotifications.ts @@ -0,0 +1,24 @@ +import useAxios from '@/utils/useAxios' + +export type AppNotificationSeverity = 'info' | 'success' | 'warning' | 'danger' + +export interface CustomerAppNotification { + id: number + title: string + body: string + severity: AppNotificationSeverity + created_at?: string + is_read: boolean +} + +export const getAppNotifications = async () => + await useAxios.get(`/customer/app_notifications`) + +export const getAppNotificationsUnreadCount = async () => + await useAxios.get<{ unread_count: number }>(`/customer/app_notifications/unread-count`) + +export const markAppNotificationRead = async (id: number) => + await useAxios.post(`/customer/app_notifications/${id}/read`) + +export const markAllAppNotificationsRead = async () => + await useAxios.post(`/customer/app_notifications/read-all`) diff --git a/src/app/setupRouter.ts b/src/app/setupRouter.ts index 7c409b0..87e2699 100644 --- a/src/app/setupRouter.ts +++ b/src/app/setupRouter.ts @@ -55,6 +55,28 @@ export function setupRouterGuards() { clearFingerTimeout() setFingerTimeOut(to) + if (to.name === 'notifyTrigger' && getToken()) { + const { useSettingsStore } = await import('@/store/settingsStore') + const settingsStore = useSettingsStore() + if (!settingsStore.loaded) { + await settingsStore.fetchSettings() + } + if (!settingsStore.isSettingTruthy('has_page_price_trigger')) { + return next({ name: (from.name as string) || 'user' }) + } + } + + if (to.name === 'appNotifications' && getToken()) { + const { useSettingsStore } = await import('@/store/settingsStore') + const settingsStore = useSettingsStore() + if (!settingsStore.loaded) { + await settingsStore.fetchSettings() + } + if (!settingsStore.isSettingTruthy('has_page_notification')) { + return next({ name: (from.name as string) || 'home' }) + } + } + if (isFirst) { isFirst = false diff --git a/src/composables/useAppNotificationBadge.ts b/src/composables/useAppNotificationBadge.ts new file mode 100644 index 0000000..3ae90b7 --- /dev/null +++ b/src/composables/useAppNotificationBadge.ts @@ -0,0 +1,61 @@ +import { computed, onMounted, onUnmounted, ref, watch } from 'vue' +import { getAppNotificationsUnreadCount } from '@/api/appNotifications' +import { useSettingsStore } from '@/store/settingsStore' +import { isToken } from '@/utils' + +const unreadCount = ref(0) +let pollTimer: ReturnType | null = null +let started = false + +async function refreshUnreadCount() { + const settingsStore = useSettingsStore() + if (!isToken() || !settingsStore.isSettingTruthy('has_page_notification')) { + unreadCount.value = 0 + return + } + try { + const data = await getAppNotificationsUnreadCount() + unreadCount.value = Number(data?.unread_count ?? 0) || 0 + } catch { + // silent — badge optional + } +} + +function clearUnreadLocally() { + unreadCount.value = 0 +} + +export function useAppNotificationBadge() { + const settingsStore = useSettingsStore() + const enabled = computed(() => settingsStore.isSettingTruthy('has_page_notification')) + + onMounted(() => { + if (started) { + void refreshUnreadCount() + return + } + started = true + void refreshUnreadCount() + pollTimer = setInterval(() => { + void refreshUnreadCount() + }, 60000) + }) + + onUnmounted(() => { + // keep poll for other mounted headers; only clear if needed later + }) + + watch(enabled, (on) => { + if (!on) unreadCount.value = 0 + else void refreshUnreadCount() + }) + + return { + enabled, + unreadCount, + refreshUnreadCount, + clearUnreadLocally, + } +} + +export { refreshUnreadCount, clearUnreadLocally } diff --git a/src/layouts/desktop/DesktopSidebar.vue b/src/layouts/desktop/DesktopSidebar.vue index ec77a14..5d6a778 100644 --- a/src/layouts/desktop/DesktopSidebar.vue +++ b/src/layouts/desktop/DesktopSidebar.vue @@ -43,6 +43,7 @@ import logoFull from '@/assets/img/logo_full_.png' import { isToken } from '@/utils' import { useCartStore } from '@/store/cartStore' import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature' +import { useSettingsStore } from '@/store/settingsStore' import { getPrimaryNavItems, getSecondaryNavItems } from '../config/navigation' import { useAppNavigation } from '../composables/useAppNavigation' import DesktopNavItem from './DesktopNavItem.vue' @@ -51,13 +52,20 @@ const cartStore = useCartStore() const authenticated = computed(() => isToken()) const navLogo = logoFull const { trackingSystemsEnabled } = useTrackingSystemsFeature() +const settingsStore = useSettingsStore() const primaryItems = computed(() => getPrimaryNavItems(authenticated.value)) /** «لیست دریافتی‌ها» فقط در موبایل نمایش داده می‌شود */ const DESKTOP_HIDDEN_NAV_IDS = ['list'] const secondaryItems = computed(() => getSecondaryNavItems(authenticated.value, { includeRecoverySites: trackingSystemsEnabled.value, - }).filter((item) => !DESKTOP_HIDDEN_NAV_IDS.includes(item.id)), + }).filter((item) => { + if (DESKTOP_HIDDEN_NAV_IDS.includes(item.id)) return false + if (item.id === 'notifyTrigger') { + return settingsStore.isSettingTruthy('has_page_price_trigger') + } + return true + }), ) const cartCount = computed(() => cartStore.itemCount || 0) diff --git a/src/layouts/mobile/MobileOverflowMenu.vue b/src/layouts/mobile/MobileOverflowMenu.vue index 894751b..ff03193 100644 --- a/src/layouts/mobile/MobileOverflowMenu.vue +++ b/src/layouts/mobile/MobileOverflowMenu.vue @@ -38,7 +38,7 @@ isToken()) const { trackingSystemsEnabled } = useTrackingSystemsFeature() +const settingsStore = useSettingsStore() +const priceTriggerEnabled = computed(() => + settingsStore.isSettingTruthy('has_page_price_trigger'), +) function go(routeName: string) { emit('navigate', routeName) diff --git a/src/router/index.ts b/src/router/index.ts index 5ab2f84..d311fbc 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -75,6 +75,11 @@ const router = createRouter({ name: 'notifyTrigger', component: () => import('@/views/base/priceTrigger/index.vue') }, + { + path: 'appNotifications', + name: 'appNotifications', + component: () => import('@/views/base/appNotifications/index.vue') + }, { path: 'transfer', name: 'transfer', diff --git a/src/store/settingsStore.ts b/src/store/settingsStore.ts index 70d6e63..3760fa3 100644 --- a/src/store/settingsStore.ts +++ b/src/store/settingsStore.ts @@ -24,6 +24,18 @@ export const useSettingsStore = defineStore('settings', () => { return item?.value ?? null } + function isSettingTruthy(key: string) { + const value = getSettingByKey(key) + if (value == null) { + return false + } + if (value === true) { + return true + } + const normalized = String(value).trim().toLowerCase() + return normalized === 'true' || normalized === '1' || normalized === 'yes' + } + function mergeSettings(items?: AppSetting[] | null) { if (!items?.length) { return @@ -80,6 +92,7 @@ export const useSettingsStore = defineStore('settings', () => { fetchSettings, mergeSettings, getSettingByKey, + isSettingTruthy, metalTypeSettingValue, metalTypeLabel, meltedGoldLabel, diff --git a/src/views/base/appNotifications/composables/useAppNotificationsPage.ts b/src/views/base/appNotifications/composables/useAppNotificationsPage.ts new file mode 100644 index 0000000..be0393b --- /dev/null +++ b/src/views/base/appNotifications/composables/useAppNotificationsPage.ts @@ -0,0 +1,94 @@ +import { computed, inject, onMounted, provide, ref, type InjectionKey } from 'vue' +import { + getAppNotifications, + markAllAppNotificationsRead, + markAppNotificationRead, + type CustomerAppNotification, +} from '@/api/appNotifications' +import { useSettingsStore } from '@/store/settingsStore' + +export interface AppNotificationsPageContext { + loading: ReturnType> + items: ReturnType> + enabled: ReturnType> + refresh: () => Promise + markRead: (id: number) => Promise + markAllRead: () => Promise +} + +export const APP_NOTIFICATIONS_PAGE_KEY: InjectionKey = + Symbol('appNotificationsPage') + +export function useAppNotificationsPage() { + const settingsStore = useSettingsStore() + const loading = ref(false) + const items = ref([]) + + const enabled = computed(() => settingsStore.isSettingTruthy('has_page_notification')) + + async function refresh() { + if (!enabled.value) { + items.value = [] + return + } + loading.value = true + try { + const data = await getAppNotifications() + items.value = Array.isArray(data) ? data : [] + } catch (e) { + console.error(e) + items.value = [] + } finally { + loading.value = false + } + } + + async function markRead(id: number) { + try { + await markAppNotificationRead(id) + const row = items.value.find((item) => item.id === id) + if (row) row.is_read = true + } catch (e) { + console.error(e) + } + } + + async function markAllRead() { + try { + await markAllAppNotificationsRead() + items.value = items.value.map((item) => ({ ...item, is_read: true })) + const { clearUnreadLocally } = await import('@/composables/useAppNotificationBadge') + clearUnreadLocally() + } catch (e) { + console.error(e) + } + } + + onMounted(() => { + void refresh().then(() => { + const hasUnread = items.value.some((item) => !item.is_read) + if (hasUnread) { + void markAllRead() + } + }) + }) + + const ctx: AppNotificationsPageContext = { + loading, + items, + enabled, + refresh, + markRead, + markAllRead, + } + provide(APP_NOTIFICATIONS_PAGE_KEY, ctx) + return ctx +} + +export function useAppNotificationsPageContext() { + const ctx = inject(APP_NOTIFICATIONS_PAGE_KEY) + if (!ctx) { + throw new Error('useAppNotificationsPageContext must be used within useAppNotificationsPage') + } + return ctx +} diff --git a/src/views/base/appNotifications/desktop/DesktopAppNotificationsView.vue b/src/views/base/appNotifications/desktop/DesktopAppNotificationsView.vue new file mode 100644 index 0000000..48edc69 --- /dev/null +++ b/src/views/base/appNotifications/desktop/DesktopAppNotificationsView.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/views/base/appNotifications/index.vue b/src/views/base/appNotifications/index.vue new file mode 100644 index 0000000..18b239d --- /dev/null +++ b/src/views/base/appNotifications/index.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/src/views/base/appNotifications/mobile/MobileAppNotificationsView.vue b/src/views/base/appNotifications/mobile/MobileAppNotificationsView.vue new file mode 100644 index 0000000..eaa839f --- /dev/null +++ b/src/views/base/appNotifications/mobile/MobileAppNotificationsView.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/views/base/fund/info/components/Item.vue b/src/views/base/fund/info/components/Item.vue index 8ed0f81..fe5e93b 100644 --- a/src/views/base/fund/info/components/Item.vue +++ b/src/views/base/fund/info/components/Item.vue @@ -13,7 +13,7 @@
{{ item.title }}
-
{{ toJalaliNotime(item.expire_at) }}
+
{{ toJalali(item.expire_at) }}
تاریخ:
-
{{ toJalaliNotime(item.expire_at) }}
-
-
-
زمان:
-
{{ toTime(item.expire_at) }}
+
{{ toJalali(item.expire_at) }}
@@ -39,7 +35,7 @@ + + diff --git a/src/views/base/home/desktop/components/DesktopHomeTopbar.vue b/src/views/base/home/desktop/components/DesktopHomeTopbar.vue index 30183b4..b2f861c 100644 --- a/src/views/base/home/desktop/components/DesktopHomeTopbar.vue +++ b/src/views/base/home/desktop/components/DesktopHomeTopbar.vue @@ -10,6 +10,21 @@ حساب کاربری +
تاریخ:
-
{{ toJalaliNotime(item.expire_at) }}
-
-
-
زمان:
-
{{ toTime(item.expire_at) }}
+
{{ toJalali(item.expire_at) }}
مهلت ارسال فیش:
@@ -130,7 +126,7 @@ import { usePriceWithoutType, } from '@/utils/usePrice' import type { Transaction } from '@/models/transaction' -import { toJalaliNotime, toTime } from '@/utils/useDateTime' +import { toJalali } from '@/utils/useDateTime' const props = withDefaults( defineProps<{ diff --git a/src/views/base/user/composables/useUserPage.ts b/src/views/base/user/composables/useUserPage.ts index aee0976..3b9899a 100644 --- a/src/views/base/user/composables/useUserPage.ts +++ b/src/views/base/user/composables/useUserPage.ts @@ -13,6 +13,7 @@ import { message } from 'ant-design-vue' import { updateMe, updatePass } from '@/api/me' import { useUserStore } from '@/store/userStore' import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature' +import { useSettingsStore } from '@/store/settingsStore' export interface UserMenuAction { id: string @@ -62,6 +63,7 @@ export function useUserPage() { const editPassVisible = ref(false) const editPasswordVisible = ref(false) const { trackingSystemsEnabled } = useTrackingSystemsFeature() + const settingsStore = useSettingsStore() const isLegal = computed(() => { const result = { title: '', color: '', icon: '' } @@ -97,7 +99,7 @@ export function useUserPage() { label: 'هشدار قیمت', icon: 'vuesax-linear:bill', onClick: () => router.push({ name: 'notifyTrigger' }), - visible: true, + visible: settingsStore.isSettingTruthy('has_page_price_trigger'), }, { id: 'about', diff --git a/src/views/base/user/mobile/MobileUserView.vue b/src/views/base/user/mobile/MobileUserView.vue index 1d66810..7ed234d 100644 --- a/src/views/base/user/mobile/MobileUserView.vue +++ b/src/views/base/user/mobile/MobileUserView.vue @@ -87,6 +87,7 @@
--> () const logoutLoading = ref(false) const { y } = useWindowScroll() const { trackingSystemsEnabled } = useTrackingSystemsFeature() +const settingsStore = useSettingsStore() +const priceTriggerEnabled = computed(() => + settingsStore.isSettingTruthy('has_page_price_trigger'), +) const { editPassVisible, editPasswordVisible, copyCode, shareCode } = useUserPageContext() diff --git a/src/views/base/wallet/components/Item.vue b/src/views/base/wallet/components/Item.vue index 594205f..722d5b2 100644 --- a/src/views/base/wallet/components/Item.vue +++ b/src/views/base/wallet/components/Item.vue @@ -20,7 +20,7 @@
{{ item.type_str }}
-
{{ toJalaliNotime(item.created_at) }}
+
{{ toJalali(item.created_at) }}
@@ -111,20 +111,8 @@
تاریخ:
-
{{ toJalaliNotime(item.expire_at) }}
+
{{ toJalali(item.expire_at) }}
-
-
زمان:
-
{{ toTime(item.expire_at) }}
-
-
مهلت ارسال فیش:
{{ item.eaj }}
@@ -143,7 +131,7 @@ import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue"; import {computed, ref} from "vue"; import BasicButton from "@/components/BasicPacks/BasicButton.vue"; import type {Transaction} from "@/models/transaction"; -import {toJalaliNotime, toJalaliOutOfUTC, toTime} from "../../../../utils/useDateTime"; +import {toJalali, toJalaliOutOfUTC} from "../../../../utils/useDateTime"; const infoVisible = ref(false)