feat(user): gate menus by SuperAdmin flags and show physical delivery

Hide fund/earn/gift entries when access is off, add matching route guards, and surface physical delivery instead of price alert on mobile profile.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 14:19:26 +03:30
co-authored by Cursor
parent 2238a34c53
commit 16ade71598
8 changed files with 112 additions and 61 deletions
+28
View File
@@ -99,6 +99,34 @@ export function setupRouterGuards() {
}
}
if (
getToken() &&
(to.name === 'piggy' ||
to.name === 'fund' ||
to.name === 'fund_info' ||
to.name === 'fund_add')
) {
const { useSettingsStore } = await import('@/store/settingsStore')
const settingsStore = useSettingsStore()
if (!settingsStore.loaded) {
await settingsStore.fetchSettings()
}
if (!settingsStore.isSettingTruthy('enable_product_funds')) {
return next({ name: (from.name as string) || 'home' })
}
}
if (to.name === 'earnMoney' && getToken()) {
const { useSettingsStore } = await import('@/store/settingsStore')
const settingsStore = useSettingsStore()
if (!settingsStore.loaded) {
await settingsStore.fetchSettings()
}
if (!settingsStore.isSettingTruthy('has_page_earn_money')) {
return next({ name: (from.name as string) || 'user' })
}
}
if (isFirst) {
isFirst = false
+10 -7
View File
@@ -50,6 +50,7 @@ import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeatu
import { useSettingsStore } from '@/store/settingsStore'
import { getPrimaryNavItems, getSecondaryNavItems } from '../config/navigation'
import { useAppNavigation } from '../composables/useAppNavigation'
import { featureSettingForRoute } from '@/utils/superAdminMenuGates'
import DesktopNavItem from './DesktopNavItem.vue'
import AppCreditLink from '@/components/AppCreditLink.vue'
@@ -58,7 +59,13 @@ const authenticated = computed(() => isToken())
const navLogo = logoFull
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
const settingsStore = useSettingsStore()
const primaryItems = computed(() => getPrimaryNavItems(authenticated.value))
const primaryItems = computed(() =>
getPrimaryNavItems(authenticated.value).filter((item) => {
const featureKey = featureSettingForRoute(item.routeName)
if (featureKey) return settingsStore.isSettingTruthy(featureKey)
return true
}),
)
/** «لیست دریافتی‌ها» فقط در موبایل نمایش داده می‌شود */
const DESKTOP_HIDDEN_NAV_IDS = ['list']
const secondaryItems = computed(() =>
@@ -66,12 +73,8 @@ const secondaryItems = computed(() =>
includeRecoverySites: trackingSystemsEnabled.value,
}).filter((item) => {
if (DESKTOP_HIDDEN_NAV_IDS.includes(item.id)) return false
if (item.id === 'notifyTrigger') {
return settingsStore.isSettingTruthy('has_page_price_trigger')
}
if (item.id === 'giftCard') {
return settingsStore.isSettingTruthy('has_page_gift_card')
}
const featureKey = featureSettingForRoute(item.routeName)
if (featureKey) return settingsStore.isSettingTruthy(featureKey)
return true
}),
)
+1 -15
View File
@@ -34,18 +34,7 @@
>
<div class="flex gap-2 items-center">
<BasicIcon icon="vuesax-linear:direct-inbox" size="16" class="absolute right-2" />
<div>دریافت طلای فیزیکی</div>
</div>
</BasicButton>
<BasicButton
v-if="authenticated && priceTriggerEnabled"
icon-size="16"
class="!font-bold w-15rem"
@click="go('notifyTrigger')"
>
<div class="flex gap-2 items-center">
<BasicIcon icon="vuesax-linear:bill" size="16" class="absolute right-2" />
<div>هشدار قیمت</div>
<div>تحویل فیزیکی</div>
</div>
</BasicButton>
<BasicButton
@@ -135,9 +124,6 @@ const emit = defineEmits<{
const authenticated = computed(() => isToken())
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
const settingsStore = useSettingsStore()
const priceTriggerEnabled = computed(() =>
settingsStore.isSettingTruthy('has_page_price_trigger'),
)
const giftCardEnabled = computed(() =>
settingsStore.isSettingTruthy('has_page_gift_card'),
)
+37
View File
@@ -0,0 +1,37 @@
/**
* نگاشت منو/مسیر اپ کاربر → کلید دسترسی سوپرادمین.
* مقدار false/غایب = مخفی (پیش‌فرض امن).
*/
export const ROUTE_FEATURE_SETTING: Record<string, string> = {
fund: 'enable_product_funds',
fund_info: 'enable_product_funds',
fund_add: 'enable_product_funds',
piggy: 'enable_product_funds',
earnMoney: 'has_page_earn_money',
giftCard: 'has_page_gift_card',
notifyTrigger: 'has_page_price_trigger',
appNotifications: 'has_page_notification',
conditionalOrder: 'enable_conditional_order',
permissions: 'permision__enabled',
}
export const MENU_ID_FEATURE_SETTING: Record<string, string> = {
fund: 'enable_product_funds',
piggy: 'enable_product_funds',
earnMoney: 'has_page_earn_money',
giftCard: 'has_page_gift_card',
notify: 'has_page_price_trigger',
notifyTrigger: 'has_page_price_trigger',
conditionalOrder: 'enable_conditional_order',
permissions: 'permision__enabled',
}
export function featureSettingForRoute(routeName: string | null | undefined): string | null {
if (!routeName) return null
return ROUTE_FEATURE_SETTING[String(routeName)] ?? null
}
export function featureSettingForMenuId(id: string | null | undefined): string | null {
if (!id) return null
return MENU_ID_FEATURE_SETTING[String(id)] ?? null
}
@@ -227,22 +227,26 @@ const mainActions = computed(() => {
{ name: 'buySell', icon: 'vuesax-linear:money-receive', label: 'خرید و فروش', handler: () => router.push({ name: 'gold' }) },
{ name: 'deposit', icon: 'vuesax-linear:card-add', label: 'واریز نقدی', handler: () => router.push({ name: 'wallet', query: { state: 'deposit' } }) },
{ name: 'balance', icon: 'vuesax-linear:receipt-item', label: 'موجودی', handler: () => emitBalance() },
{ name: 'fund', icon: 'vuesax-linear:archive-1', label: 'دارایی ها', handler: () => router.push({ name: 'fund' }) },
]
if (settingsStore.isSettingTruthy('enable_product_funds')) {
actions.push(
{ name: 'fund', icon: 'vuesax-linear:archive-1', label: 'دارایی ها', handler: () => router.push({ name: 'fund' }) },
{
name: 'piggy',
icon: 'vuesax-linear:safe-home',
label: 'قلک',
handler: () => router.push({ name: 'piggy' }),
},
)
}
if (settingsStore.isSettingTruthy('has_page_earn_money')) {
actions.push({
name: 'piggy',
icon: 'vuesax-linear:safe-home',
label: 'قلک',
handler: () => router.push({ name: 'piggy' }),
name: 'earn',
icon: 'vuesax-linear:dollar-circle',
label: 'کسب درآمد',
handler: () => router.push({ name: 'earnMoney' }),
})
}
actions.push({
name: 'earn',
icon: 'vuesax-linear:dollar-circle',
label: 'کسب درآمد',
handler: () => router.push({ name: 'earnMoney' }),
})
return actions
})
+3
View File
@@ -99,6 +99,7 @@
<div class="home-quick-action__label">موجودی</div>
</div>
<div
v-if="productFundsEnabled"
class="home-quick-action home_appear_fade_in_down"
style="text-align: -webkit-center"
@click="router.push({ name: 'fund' })"
@@ -120,6 +121,7 @@
<div class="home-quick-action__label">قلک</div>
</div>
<div
v-if="earnMoneyEnabled"
class="home-quick-action home_appear_fade_in_down"
style="text-align: -webkit-center"
@click="router.push({ name: 'earnMoney' })"
@@ -452,6 +454,7 @@ const walletStore = useWalletStore()
const settingsStore = useSettingsStore()
const isDesktop = useDesktopBreakpoint()
const productFundsEnabled = computed(() => settingsStore.isSettingTruthy('enable_product_funds'))
const earnMoneyEnabled = computed(() => settingsStore.isSettingTruthy('has_page_earn_money'))
const chartLoading = ref<boolean>(false)
const openModal = ref<boolean>(false)
const wholeBalanceVisible = ref<boolean>(false)
+12 -5
View File
@@ -113,11 +113,11 @@ export function useUserPage() {
visible: true,
},
{
id: 'notify',
label: 'هشدار قیمت',
icon: 'vuesax-linear:bill',
onClick: () => router.push({ name: 'notifyTrigger' }),
visible: settingsStore.isSettingTruthy('has_page_price_trigger'),
id: 'physicalReceive',
label: 'تحویل فیزیکی',
icon: 'vuesax-linear:direct-inbox',
onClick: () => router.push({ name: 'physicalReceive' }),
visible: true,
},
{
id: 'piggy',
@@ -140,6 +140,13 @@ export function useUserPage() {
onClick: () => router.push({ name: 'giftCard' }),
visible: settingsStore.isSettingTruthy('has_page_gift_card'),
},
{
id: 'earnMoney',
label: 'کسب درآمد',
icon: 'vuesax-linear:dollar-circle',
onClick: () => router.push({ name: 'earnMoney' }),
visible: settingsStore.isSettingTruthy('has_page_earn_money'),
},
{
id: 'support',
label: 'پشتیبانی',
+6 -23
View File
@@ -100,14 +100,15 @@
</div>
</BasicButton>-->
<BasicButton
v-if="priceTriggerEnabled"
class="w-full mt-2"
class="mt-2 w-full"
icon=""
iconSize="18"
size="large"
@click="$router.push({ name: 'notifyTrigger' })"
@click="$router.push({ name: 'physicalReceive' })"
>
<div class="flex">
<BasicIcon icon="vuesax-linear:bill" class="absolute right-8" />
<div>هشدار قیمت</div>
<BasicIcon icon="vuesax-linear:direct-inbox" class="absolute right-8" />
<div>تحویل فیزیکی</div>
</div>
</BasicButton>
<BasicButton
@@ -135,18 +136,6 @@
<div>سایت‌های ری‌گیری</div>
</div>
</BasicButton>
<!-- <BasicButton-->
<!-- class="mt-2 w-full"-->
<!-- icon=""-->
<!-- iconSize="18"-->
<!-- size="large"-->
<!-- @click="$router.push({ name: 'physicalReceive' })"-->
<!-- >-->
<!-- <div class="flex">-->
<!-- <BasicIcon icon="vuesax-linear:direct-inbox" class="absolute right-8" />-->
<!-- <div>دریافت طلای فیزیکی</div>-->
<!-- </div>-->
<!-- </BasicButton>-->
<BasicButton
class="mt-2 w-full"
icon=""
@@ -210,9 +199,7 @@ import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import InstallButton from '@/components/InstallButton.vue'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { computed } from 'vue'
import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature'
import { useSettingsStore } from '@/store/settingsStore'
import { useUserStore } from '@/store/userStore'
import { useUserPageContext } from '../composables/useUserPage'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
@@ -220,10 +207,6 @@ import { openGoldDayCalculator } from '@/composables/useGoldDayCalculator'
const userStore = useUserStore()
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
const settingsStore = useSettingsStore()
const priceTriggerEnabled = computed(() =>
settingsStore.isSettingTruthy('has_page_price_trigger'),
)
const { editPassVisible, editPasswordVisible, copyCode, shareCode, requestLogout } =
useUserPageContext()