edit SMSpanel

This commit is contained in:
2026-07-04 14:21:27 +03:30
parent 40d04a2dfb
commit f7cc5180b1
19 changed files with 2745 additions and 249 deletions
@@ -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: 'نمودار از داده داخلی',
+6
View File
@@ -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