edit SMSpanel
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import useAxios from '@core/utils/useAxios'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
|
||||
export type SmsPanelOverview = {
|
||||
wallet: {
|
||||
balance: number
|
||||
price_per_part: number
|
||||
is_blocked: boolean
|
||||
alert_days_sent: number
|
||||
last_alert_date: string | null
|
||||
}
|
||||
stats: {
|
||||
today: { parts: number; amount: number; messages: number }
|
||||
month: { parts: number; amount: number; messages: number }
|
||||
}
|
||||
}
|
||||
|
||||
export type SmsPanelReport = {
|
||||
from_date: string
|
||||
to_date: string
|
||||
balance: number
|
||||
summary: { parts: number; amount: number; messages: number }
|
||||
by_provider: Array<{
|
||||
provider: string
|
||||
messages: number
|
||||
parts: number
|
||||
amount: number
|
||||
}>
|
||||
}
|
||||
|
||||
export type SmsPanelChargeReportItem = {
|
||||
id: number
|
||||
amount: number
|
||||
balance_after: number
|
||||
note: string | null
|
||||
created_by: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type SmsPanelChargeReport = {
|
||||
from_date: string | null
|
||||
to_date: string | null
|
||||
summary: { charges: number; amount: number }
|
||||
items: SmsPanelChargeReportItem[]
|
||||
}
|
||||
|
||||
export const getSmsPanelOverview = () => useAxios.get<SmsPanelOverview>('/admin/sms-panel/overview')
|
||||
|
||||
export const chargeSmsWallet = (payload: { amount: number; note?: string }) =>
|
||||
useAxios.post<{ message: string; balance: number }>('/admin/sms-panel/charge', payload)
|
||||
|
||||
export const updateSmsPrice = (price_per_part: number) =>
|
||||
useAxios.put<{ message: string; price_per_part: number }>('/admin/sms-panel/price', {
|
||||
price_per_part
|
||||
})
|
||||
|
||||
export const updateSmsBlock = (is_blocked: boolean) =>
|
||||
useAxios.put<{ message: string; is_blocked: boolean }>('/admin/sms-panel/block', { is_blocked })
|
||||
|
||||
export const getSmsPanelLogsTable = (params: TableInput) =>
|
||||
useAxios.post('/admin/sms-panel/logs/table', params)
|
||||
|
||||
export const getSmsPanelLedgerTable = (params: TableInput) =>
|
||||
useAxios.post('/admin/sms-panel/ledger/table', params)
|
||||
|
||||
export const getSmsPanelReport = (params?: { from_date?: string; to_date?: string }) =>
|
||||
useAxios.get<SmsPanelReport>('/admin/sms-panel/report', { params })
|
||||
|
||||
export const getSmsPanelChargeReport = (params?: { from_date?: string; to_date?: string }) =>
|
||||
useAxios.get<SmsPanelChargeReport>('/admin/sms-panel/report/charges', { params })
|
||||
@@ -16,6 +16,9 @@ export interface User {
|
||||
token_sms?: string
|
||||
user_type?: string
|
||||
send_sms?:boolean
|
||||
settings?: Const[]
|
||||
sms_billing_enabled?: boolean
|
||||
sms_panel_access?: boolean
|
||||
}
|
||||
|
||||
export interface AddId {
|
||||
|
||||
@@ -105,7 +105,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|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)/.test(
|
||||
/^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_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(
|
||||
k
|
||||
)
|
||||
},
|
||||
|
||||
@@ -108,6 +108,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
register_referral_sms_to_referrer: 'switch',
|
||||
register_referral_bonus_message: 'switch',
|
||||
open_register: 'switch',
|
||||
sms_panel: 'switch',
|
||||
auto_active_customer: 'switch',
|
||||
close_price: 'switch',
|
||||
startDay: 'time',
|
||||
@@ -268,6 +269,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'نام فارسی برند که در پیامکها، عنوان صفحات و بخشهای عمومی اپ به مشتری نشان داده میشود.',
|
||||
open_register:
|
||||
'روشن: کاربران جدید میتوانند ثبتنام کنند. خاموش: فقط ورود کاربران قبلی مجاز است.',
|
||||
sms_panel:
|
||||
'روشن: پنل پیامک و کیف پول billing فعال است. خاموش: پیامکها مثل قبل ارسال میشوند بدون billing.',
|
||||
auto_active_customer:
|
||||
'روشن: حساب بلافاصله پس از ثبتنام فعال میشود. خاموش: نیاز به تأیید دستی شما در پنل دارد.',
|
||||
close_price:
|
||||
@@ -436,6 +439,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
referral_id: 'کد معرف اجباری',
|
||||
referral_gift_on_first_order: 'پاداش معرف پس از اولین سفارش',
|
||||
open_register: 'ثبتنام باز',
|
||||
sms_panel: 'پنل پیامک (کیف پول)',
|
||||
auto_active_customer: 'فعالسازی خودکار پس از ثبتنام',
|
||||
balance_report_exclude_customer_id: 'حذف مشتری از گزارش تراز',
|
||||
chart_use_local_data: 'نمودار از داده داخلی',
|
||||
|
||||
@@ -25,6 +25,7 @@ import BasicMenu from '@/components/BasicMenu/index.vue'
|
||||
import { useRouterStore } from '@core/stores/router'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||
import { hasSmsPanelAccess, isSmsBillingEnabled } from '@/utils/smsPanelAccess'
|
||||
import { bannerEnabledSetting } from '@/utils/config'
|
||||
import { get } from 'lodash'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
@@ -38,6 +39,11 @@ const menu = computed(() => {
|
||||
return children
|
||||
.filter((child) => !child.meta?.hideMenu)
|
||||
.filter((child) => child.name !== 'banners' || bannerEnabledSetting.value)
|
||||
.filter(
|
||||
(child) =>
|
||||
!child.meta?.requiresSmsPanelAccess ||
|
||||
(isSmsBillingEnabled(userStore.userInfo) && hasSmsPanelAccess(userStore.userInfo))
|
||||
)
|
||||
.filter(
|
||||
(child) =>
|
||||
!child.meta?.deployAdminOnly || isDeployAdmin(userStore.userInfo?.mobile)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<span
|
||||
class="sms-log-body-expandable block max-w-full text-[0.8125rem] leading-relaxed text-[var(--admin-text-muted)]"
|
||||
:class="expanded ? 'sms-log-body-expandable--open' : 'sms-log-body-expandable--clamped'"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="toggle"
|
||||
@keydown.enter.prevent="toggle"
|
||||
>
|
||||
{{ text || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
text?: string | null
|
||||
}>()
|
||||
|
||||
const expanded = ref(false)
|
||||
|
||||
function toggle() {
|
||||
if (!props.text) return
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sms-log-body-expandable--clamped {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sms-log-body-expandable--open {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="sms-panel-table min-w-0 w-full">
|
||||
<BaseTable v-bind="tblProps" @register="register" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import { ledgerCols } from '../hooks/ledgerColumns'
|
||||
import { getSmsPanelLedgerTable } from '@/api/smsPanel'
|
||||
|
||||
const props = defineProps<{
|
||||
fromDate?: string
|
||||
toDate?: string
|
||||
}>()
|
||||
|
||||
const [register, reload] = useBaseTable()
|
||||
|
||||
const tblProps = computed(() => ({
|
||||
columns: ledgerCols,
|
||||
api: (params: TableInput) =>
|
||||
getSmsPanelLedgerTable({
|
||||
...params,
|
||||
from_date: props.fromDate || undefined,
|
||||
to_date: props.toDate || undefined
|
||||
})
|
||||
}))
|
||||
|
||||
function fetch() {
|
||||
reload()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.fromDate, props.toDate],
|
||||
() => fetch()
|
||||
)
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="sms-panel-table min-w-0 w-full">
|
||||
<BaseTable v-bind="tblProps" @register="register" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import { logsCols } from '../hooks/logsColumns'
|
||||
import { getSmsPanelLogsTable } from '@/api/smsPanel'
|
||||
|
||||
const props = defineProps<{
|
||||
fromDate?: string
|
||||
toDate?: string
|
||||
}>()
|
||||
|
||||
const [register, reload] = useBaseTable()
|
||||
|
||||
const tblProps = computed(() => ({
|
||||
columns: logsCols,
|
||||
api: (params: TableInput) =>
|
||||
getSmsPanelLogsTable({
|
||||
...params,
|
||||
from_date: props.fromDate || undefined,
|
||||
to_date: props.toDate || undefined
|
||||
})
|
||||
}))
|
||||
|
||||
function fetch() {
|
||||
reload()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.fromDate, props.toDate],
|
||||
() => fetch()
|
||||
)
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||
import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime'
|
||||
import { Tooltip } from 'ant-design-vue'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
import SmsLogBodyExpandable from '../components/SmsLogBodyExpandable.vue'
|
||||
|
||||
function formatJalaliDateTime(value: string | null | undefined, empty = '—') {
|
||||
if (!value) return empty
|
||||
const full = toJalali(value)
|
||||
const short = toJalaliNoTime(value)
|
||||
return (
|
||||
<Tooltip title={full} placement="top">
|
||||
<span class="cursor-help whitespace-nowrap">{short}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function formatAmount(value: number | null | undefined) {
|
||||
if (value == null) return '—'
|
||||
const n = Number(value)
|
||||
const formatted = Math.abs(n).toLocaleString('fa-IR')
|
||||
if (n > 0) return `+${formatted}`
|
||||
if (n < 0) return `-${formatted}`
|
||||
return formatted
|
||||
}
|
||||
|
||||
const typeLabels: Record<string, { text: string; bg: string; fg: string }> = {
|
||||
charge: { text: 'شارژ', bg: '#dcfce7', fg: '#16a34a' },
|
||||
debit: { text: 'کسر', bg: '#fee2e2', fg: '#dc2626' },
|
||||
alert_debit: { text: 'هشدار', bg: '#ffedd5', fg: '#ea580c' }
|
||||
}
|
||||
|
||||
export const ledgerCols: BaseTableColumn[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'row_index',
|
||||
filterable: false,
|
||||
hideInCard: true,
|
||||
width: 56,
|
||||
customRender: (record: any) => record.index + 1
|
||||
},
|
||||
{
|
||||
title: 'نوع',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
filterable: false,
|
||||
cardGroup: 'summary',
|
||||
cardGroupLayout: 'grid-2',
|
||||
width: 88,
|
||||
customRender: (ctx: any) => {
|
||||
const type = String(ctx.record?.type ?? '')
|
||||
const meta = typeLabels[type] ?? { text: type || '—', bg: '#f4f4f5', fg: '#52525b' }
|
||||
return (
|
||||
<CustomTag bg-color={meta.bg} text-color={meta.fg}>
|
||||
{meta.text}
|
||||
</CustomTag>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'مبلغ',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
filterable: false,
|
||||
cardGroup: 'summary',
|
||||
width: 110,
|
||||
customRender: (ctx: any) => formatAmount(ctx.record?.amount)
|
||||
},
|
||||
{
|
||||
title: 'موجودی بعد',
|
||||
dataIndex: 'balance_after',
|
||||
key: 'balance_after',
|
||||
filterable: false,
|
||||
cardGroup: 'summary',
|
||||
width: 110,
|
||||
customRender: (ctx: any) => Number(ctx.record?.balance_after ?? 0).toLocaleString('fa-IR')
|
||||
},
|
||||
{
|
||||
title: 'پارت',
|
||||
dataIndex: 'parts',
|
||||
key: 'parts',
|
||||
type: 'Number',
|
||||
hideInCard: true,
|
||||
width: 72
|
||||
},
|
||||
{
|
||||
title: 'قیمت پارت',
|
||||
dataIndex: 'price_per_part',
|
||||
key: 'price_per_part',
|
||||
filterable: false,
|
||||
hideInCard: true,
|
||||
width: 96,
|
||||
customRender: (record: any) => {
|
||||
const v = record.record.price_per_part
|
||||
return v != null ? Number(v).toLocaleString('fa-IR') : '—'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'توضیح',
|
||||
dataIndex: 'note',
|
||||
key: 'note',
|
||||
type: 'Text',
|
||||
ellipsis: true,
|
||||
cardRole: 'stacked',
|
||||
cardLabel: 'توضیح',
|
||||
customRender: (ctx: { record: any; mobileCard?: boolean }) => {
|
||||
const note = ctx.record?.note ?? ''
|
||||
if (ctx.mobileCard) {
|
||||
if (!note) return <span>—</span>
|
||||
return <SmsLogBodyExpandable text={note} />
|
||||
}
|
||||
return <span class="block max-w-full">{note || '—'}</span>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'تاریخ',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'DateTime',
|
||||
cardRole: 'header',
|
||||
cardShowRowBadge: true,
|
||||
width: 118,
|
||||
customRender: (ctx: any) => formatJalaliDateTime(ctx.record?.created_at)
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||
import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime'
|
||||
import { Tooltip } from 'ant-design-vue'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
import SmsLogBodyExpandable from '../components/SmsLogBodyExpandable.vue'
|
||||
function formatJalaliDateTime(value: string | null | undefined, empty = '—') {
|
||||
if (!value) return empty
|
||||
const full = toJalali(value)
|
||||
const short = toJalaliNoTime(value)
|
||||
return (
|
||||
<Tooltip title={full} placement="top">
|
||||
<span class="cursor-help whitespace-nowrap">{short}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function formatAmount(value: number | null | undefined) {
|
||||
if (value == null) return '—'
|
||||
return Number(value).toLocaleString('fa-IR')
|
||||
}
|
||||
|
||||
export const logsCols: BaseTableColumn[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'row_index',
|
||||
filterable: false,
|
||||
hideInCard: true,
|
||||
width: 56,
|
||||
customRender: (record: any) => record.index + 1
|
||||
},
|
||||
{
|
||||
title: 'گیرنده',
|
||||
dataIndex: 'receiver',
|
||||
key: 'receiver',
|
||||
type: 'Text',
|
||||
cardGroup: 'summary',
|
||||
cardGroupLayout: 'grid-2',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: 'پارت',
|
||||
dataIndex: 'parts',
|
||||
key: 'parts',
|
||||
type: 'Number',
|
||||
cardGroup: 'summary',
|
||||
width: 72
|
||||
},
|
||||
{
|
||||
title: 'قیمت پارت',
|
||||
dataIndex: 'price_per_part',
|
||||
key: 'price_per_part',
|
||||
filterable: false,
|
||||
hideInCard: true,
|
||||
width: 96,
|
||||
customRender: (record: any) => formatAmount(record.record.price_per_part)
|
||||
},
|
||||
{
|
||||
title: 'مبلغ',
|
||||
dataIndex: 'total_amount',
|
||||
key: 'total_amount',
|
||||
filterable: false,
|
||||
cardGroup: 'summary',
|
||||
width: 96,
|
||||
customRender: (record: any) => formatAmount(record.record.total_amount)
|
||||
},
|
||||
{
|
||||
title: 'ارائهدهنده',
|
||||
dataIndex: 'provider',
|
||||
key: 'provider',
|
||||
type: 'Text',
|
||||
hideInCard: true,
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: 'منبع',
|
||||
dataIndex: 'source',
|
||||
key: 'source',
|
||||
type: 'Text',
|
||||
hideInCard: true,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: 'نوع',
|
||||
dataIndex: 'is_alert',
|
||||
key: 'is_alert',
|
||||
filterable: false,
|
||||
cardGroup: 'summary',
|
||||
width: 88,
|
||||
customRender: (record: any) =>
|
||||
record.record.is_alert ? (
|
||||
<CustomTag bg-color="#ffedd5" text-color="#ea580c">
|
||||
هشدار
|
||||
</CustomTag>
|
||||
) : (
|
||||
<CustomTag bg-color="#dbeafe" text-color="#2563eb">
|
||||
عادی
|
||||
</CustomTag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'متن',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
type: 'Text',
|
||||
ellipsis: true,
|
||||
cardRole: 'stacked',
|
||||
cardLabel: 'متن پیام',
|
||||
customRender: (ctx: { record: any; mobileCard?: boolean }) => {
|
||||
const body = ctx.record?.body ?? ''
|
||||
if (ctx.mobileCard) {
|
||||
return <SmsLogBodyExpandable text={body} />
|
||||
}
|
||||
return <span class="block max-w-full">{body || '—'}</span>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'تاریخ',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'DateTime',
|
||||
cardRole: 'header',
|
||||
cardShowRowBadge: true,
|
||||
width: 118,
|
||||
customRender: (record: any) => formatJalaliDateTime(record.record.created_at)
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ import { useRouterStore } from '@core/stores/router'
|
||||
import type { Router, RouteRecordRaw } from 'vue-router'
|
||||
import { getConfigApp, isStoreApp } from '@/utils/config'
|
||||
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||
import { hasSmsPanelAccess, isSmsBillingEnabled } from '@/utils/smsPanelAccess'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { cloneDeep } from 'lodash'
|
||||
//const _router = useRouter()
|
||||
//import { cloneDeep } from 'lodash'
|
||||
@@ -46,6 +48,20 @@ function filterRoutesByApp(routes: RouteRecordRaw, appName: string | null) {
|
||||
routes.children.forEach((child) => filterRoutesByApp(child, appName))
|
||||
}
|
||||
|
||||
/** زیرمنوی پنل پیامک — فقط deploy admin و وقتی billing فعال است */
|
||||
function filterSmsPanelRoutes(route: RouteRecordRaw) {
|
||||
const user = useUserStore().userInfo
|
||||
|
||||
if (!route.children?.length) return
|
||||
|
||||
route.children = route.children.filter((child) => {
|
||||
if (!child.meta?.requiresSmsPanelAccess) return true
|
||||
return isSmsBillingEnabled(user) && hasSmsPanelAccess(user)
|
||||
})
|
||||
|
||||
route.children.forEach((child) => filterSmsPanelRoutes(child))
|
||||
}
|
||||
|
||||
/** زیرمنوی تنظیمات پیشرفته — فقط برای ادمین دیپلoyi */
|
||||
function filterDeployAdminRoutes(route: RouteRecordRaw, mobile?: string | number | null) {
|
||||
if (!route.children?.length) return
|
||||
@@ -90,6 +106,7 @@ function prepareRoute(route: RouteRecordRaw, appName: string | null, mobile?: st
|
||||
applyAppModeMeta(route, appName)
|
||||
filterRoutesByApp(route, appName)
|
||||
filterDeployAdminRoutes(route, mobile)
|
||||
filterSmsPanelRoutes(route)
|
||||
}
|
||||
|
||||
function loadDynamicRoutes(appName: string | null, mobile?: string | number | null): RouteRecordRaw[] {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||
import { hasSmsPanelAccess } from '@/utils/smsPanelAccess'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { bannerEnabledSetting } from '@/utils/config'
|
||||
|
||||
@@ -71,6 +72,23 @@ const Setting: RouteRecordRaw = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'smsPanelMain',
|
||||
path: 'sms-panel',
|
||||
component: () => import('@/pages/menuPages/smsPanel/index.vue'),
|
||||
meta: {
|
||||
icon: 'vuesax-linear:wallet-3',
|
||||
title: 'پنل پیامک',
|
||||
order: 3.5,
|
||||
requiresSmsPanelAccess: true
|
||||
},
|
||||
beforeEnter() {
|
||||
const userStore = useUserStore()
|
||||
if (!hasSmsPanelAccess(userStore.userInfo)) {
|
||||
return { name: 'configuration' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'banners',
|
||||
path: 'banners',
|
||||
|
||||
+14
-1
@@ -3,6 +3,7 @@ import type { User } from '@/model/user'
|
||||
import { getMyInformation } from '@/api/user'
|
||||
import { get } from 'lodash'
|
||||
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||
import { hasSmsPanelAccess, isSmsBillingEnabled } from '@/utils/smsPanelAccess'
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userInfo = ref<User | undefined>(undefined)
|
||||
|
||||
@@ -32,5 +33,17 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
const isDeployAdminUser = computed(() => isDeployAdmin(userInfo.value?.mobile))
|
||||
|
||||
return { userInfo, getMyInfo, permissions, canAccess, isAdmin, isDeployAdminUser }
|
||||
const smsPanelAccess = computed(() => hasSmsPanelAccess(userInfo.value))
|
||||
const smsBillingEnabled = computed(() => isSmsBillingEnabled(userInfo.value))
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
getMyInfo,
|
||||
permissions,
|
||||
canAccess,
|
||||
isAdmin,
|
||||
isDeployAdminUser,
|
||||
smsPanelAccess,
|
||||
smsBillingEnabled
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { User } from '@/model/user'
|
||||
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||
|
||||
export const SMS_PANEL_OWNER_MOBILE = '09128081392'
|
||||
|
||||
export function normalizeMobile(mobile: unknown): string {
|
||||
let digits = String(mobile ?? '').replace(/\D/g, '')
|
||||
|
||||
if (digits.startsWith('98') && digits.length === 12) {
|
||||
digits = `0${digits.slice(2)}`
|
||||
} else if (digits.length === 10 && digits.startsWith('9')) {
|
||||
digits = `0${digits}`
|
||||
}
|
||||
|
||||
return digits
|
||||
}
|
||||
|
||||
function parseSettingBool(value: unknown, defaultValue = true): boolean {
|
||||
if (value == null || String(value).trim() === '') {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const normalized = String(value).trim().toLowerCase()
|
||||
|
||||
if (['false', '0', 'no', 'off'].includes(normalized)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** sms_panel = false → بدون کیف پول/billing؛ ارسال مثل قبل */
|
||||
export function isSmsBillingEnabled(user?: User | null): boolean {
|
||||
if (!user) return false
|
||||
if (user.sms_billing_enabled === false) return false
|
||||
|
||||
const panel = user.settings?.find((s) => s.key === 'sms_panel')?.value
|
||||
|
||||
return parseSettingBool(panel, true)
|
||||
}
|
||||
|
||||
export function hasSmsPanelAccess(user?: User | null): boolean {
|
||||
if (!isSmsBillingEnabled(user)) return false
|
||||
if (user?.sms_panel_access === true) return true
|
||||
|
||||
return isDeployAdmin(user?.mobile)
|
||||
}
|
||||
Reference in New Issue
Block a user