fix some bugs

This commit is contained in:
2026-06-23 22:31:07 +03:30
parent 4e7d6bca00
commit 71764f7e9a
68 changed files with 1941 additions and 1288 deletions
@@ -38,7 +38,7 @@
</span>
<span class="dash-quick-sheet__featured-text">
<span class="dash-quick-sheet__featured-label">شارژ / دشارژ کیف پول</span>
<span class="dash-quick-sheet__featured-hint">مدیریت موجودی ریالی و طلایی</span>
<span class="dash-quick-sheet__featured-hint">{{ metalLabel.manageGoldCash() }}</span>
</span>
<BaseIcon icon="vuesax-linear:arrow-left-2" :size="16" class="dash-quick-sheet__featured-arrow" />
</button>
@@ -106,6 +106,7 @@ import type { RouteLocationRaw } from 'vue-router'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import DashboardQuickActionBtn from '@/pages/dashboard/components/DashboardQuickActionBtn.vue'
import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
type ActionVariant = 'default' | 'accent' | 'danger' | 'primary'
type ItemTone = 'slate' | 'emerald' | 'rose' | 'amber' | 'sky' | 'violet'
@@ -2,7 +2,7 @@
<BasicActionSheet
v-model:visible="visibleProxy"
:title="modalTitle"
modal-width="22rem"
modal-width="24rem"
drawer-height="auto"
compact
:loading="loading"
@@ -24,33 +24,18 @@
/>
</div>
<div
v-if="selectedCustomer"
class="dashboard-wallet-modal__balances"
>
<div>
<span class="text-slate-500">طلایی</span>
<p class="font-semibold text-slate-800 dark:text-slate-100">{{ formatGold(selectedCustomer) }}</p>
</div>
<div v-if="selectedCustomer" class="dashboard-wallet-modal__balances">
<div>
<span class="text-slate-500">ریالی</span>
<p class="font-semibold text-slate-800 dark:text-slate-100">{{ formatCash(selectedCustomer) }}</p>
</div>
</div>
<div class="dashboard-wallet-modal__section">
<label class="dashboard-wallet-modal__label">نوع کیف پول</label>
<a-radio-group v-model:value="walletVariant" button-style="solid" class="flex w-full">
<a-radio-button value="money" class="w-1/2 text-center">ریالی</a-radio-button>
<a-radio-button value="gold" class="w-1/2 text-center">طلایی</a-radio-button>
</a-radio-group>
</div>
<WalletChargeForm
ref="chargeFormRef"
class="dashboard-wallet-modal__form"
:model-value="formCharge"
:variant="walletVariant"
:targets="chargeTargets"
/>
<div class="dashboard-wallet-modal__footer">
@@ -78,16 +63,18 @@ import { message } from 'ant-design-vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import WalletChargeForm from '@/pages/menuPages/customers/base/components/WalletChargeForm.vue'
import {
applyChargeTarget,
buildChargePayload,
createEmptyChargeForm,
resolveChargeTarget,
walletChargeOkText,
walletChargeSuccessMessage,
walletChargeTitle,
} from '@/pages/menuPages/customers/base/utils/walletChargeForm'
import { getCustomersList } from '@/api/customer'
import { chargeWallet } from '@/api/wallet'
import { chargeWalletUnified, getWalletChargeTargets } from '@/api/wallet'
import type { Customer } from '@/model/customer'
import type { Charge, Wallet } from '@/model/wallet'
import type { Charge, Wallet, WalletChargeTarget } from '@/model/wallet'
import { usePrice } from '@core/utils/usePrice'
const props = defineProps<{ visible: boolean }>()
@@ -101,7 +88,7 @@ const visibleProxy = computed({
const loading = ref(false)
const submitting = ref(false)
const customers = ref<Customer[]>([])
const walletVariant = ref<'money' | 'gold'>('money')
const chargeTargets = ref<WalletChargeTarget[]>([])
const formCharge = ref<Charge>(createEmptyChargeForm())
const chargeFormRef = ref<InstanceType<typeof WalletChargeForm>>()
@@ -123,8 +110,12 @@ const selectedCustomer = computed(() =>
customers.value.find((customer) => customer.id === formCharge.value.customer_id),
)
const selectedTarget = computed(() =>
resolveChargeTarget(chargeTargets.value, formCharge.value.chargeTargetKey),
)
const modalTitle = computed(() =>
walletChargeTitle(walletVariant.value, formCharge.value.direction),
walletChargeTitle(selectedTarget.value, formCharge.value.direction),
)
const okText = computed(() => walletChargeOkText(formCharge.value.direction))
@@ -138,20 +129,25 @@ function customerWallet(customer: Customer): Wallet | undefined {
return Array.isArray(wallet) ? wallet[0] : wallet
}
function formatGold(customer: Customer) {
const gold = customerWallet(customer)?.gold ?? customer.wallet_gold ?? customer.gold
return `${Number(gold ?? 0).toLocaleString('fa-IR', { maximumFractionDigits: 3 })} گرم`
}
function formatCash(customer: Customer) {
const cash = customerWallet(customer)?.cash ?? customer.wallet_cash
return usePrice(Number(cash ?? 0), true, '')
}
async function loadChargeTargets() {
const data = await getWalletChargeTargets()
chargeTargets.value = data?.targets ?? []
const firstTarget = chargeTargets.value[0]
if (firstTarget) {
Object.assign(formCharge.value, applyChargeTarget(formCharge.value, firstTarget))
}
}
async function loadCustomers() {
loading.value = true
try {
customers.value = (await getCustomersList()) ?? []
const [customerList] = await Promise.all([getCustomersList(), loadChargeTargets()])
customers.value = customerList ?? []
} catch (e) {
console.error(e)
message.error('خطا در بارگذاری لیست مشتریان')
@@ -161,8 +157,11 @@ async function loadCustomers() {
}
function resetForm() {
walletVariant.value = 'money'
formCharge.value = createEmptyChargeForm()
const firstTarget = chargeTargets.value[0]
if (firstTarget) {
Object.assign(formCharge.value, applyChargeTarget(formCharge.value, firstTarget))
}
}
function handleClose() {
@@ -188,7 +187,7 @@ async function submit() {
submitting.value = true
try {
await chargeWallet(buildChargePayload(formCharge.value), walletVariant.value)
await chargeWalletUnified(buildChargePayload(formCharge.value))
message.success(walletChargeSuccessMessage(formCharge.value.direction))
closeModal()
} catch (e) {
@@ -231,7 +230,7 @@ watch(
}
.dashboard-wallet-modal__balances {
@apply grid grid-cols-2 gap-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs dark:border-slate-600 dark:bg-slate-800/60;
@apply grid grid-cols-1 gap-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs dark:border-slate-600 dark:bg-slate-800/60;
}
.dashboard-wallet-modal__footer {
@@ -12,6 +12,7 @@ import {
toggleGateway,
} from '@/api/dashboard'
import { getWalletTable } from '@/api/wallet'
import { formatMetalWeightFromGrams } from '@/utils/metalWeight'
import type {
DashboardFlowPoint,
DashboardFinance,
@@ -23,6 +24,7 @@ import type { Order } from '@/model/order'
import type { Balance } from '@/model/product'
import { usePrice } from '@core/utils/usePrice'
import { toJalali } from '@core/utils/useDataTime'
import { metalLabel } from '@/utils/adminMetalLabels'
import { isMobile } from '@core/utils/is'
import { isStoreApp } from '@/utils/config'
import { useAdminTheme } from '@core/composable/useAdminTheme'
@@ -229,7 +231,10 @@ export function useDashboardPage() {
master.value = await getDashboard()
}
function formatGram(value?: number) {
function formatGram(value?: number, metalId?: number) {
if (metalId != null) {
return formatMetalWeightFromGrams(value ?? 0, metalId)
}
return `${Number(value ?? 0).toLocaleString('fa-IR', { maximumFractionDigits: 3 })} گرم`
}
@@ -277,7 +282,7 @@ export function useDashboardPage() {
return {
...chartSurfaceOptions(),
chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4, foreColor: chartSurfaceOptions().chart.foreColor },
labels: ['سود معامله', 'کارمزد طلایی', 'هدایای طلا'],
labels: ['سود معامله', metalLabel.goldCommission(), metalLabel.goldGifts()],
colors: ['#10b981', '#f59e0b', '#8b5cf6'],
legend: {
position: 'bottom',
@@ -576,6 +581,8 @@ export function useDashboardPage() {
}
})
const metalBalanceItems = computed(() => balance.value?.metalBalances ?? [])
onMounted(() => {
void refreshAll()
})
@@ -588,6 +595,7 @@ export function useDashboardPage() {
master,
finance,
balance,
metalBalanceItems,
walletSums,
recentOrders,
customerStats,
+95 -107
View File
@@ -1,5 +1,7 @@
/** توضیح و فرمول فارسی — برای tooltip آیکون راهنما */
import { m } from '@/utils/metalLabels'
export type DashboardHelp = {
description: string
formula: string
@@ -7,133 +9,119 @@ export type DashboardHelp = {
export const MESGHAL_FACTOR = '۴.۳۳۱۸۰۲'
export const dashboardHero: DashboardHelp = {
description: 'آخرین قیمت پایه هر گرم طلا در سیستم.',
formula: `قیمت هر مثقال = قیمت هر گرم × ${MESGHAL_FACTOR}`,
function help(description: string, formula: string): DashboardHelp {
return {
description: m(description),
formula: m(formula),
}
}
export const dashboardGateway: DashboardHelp = {
description: 'وقتی فعال باشد مشتری می‌تواند از درگاه بانکی واریز کند؛ در حالت غیرفعال فقط واریز دستی ممکن است.',
formula: 'وضعیت: فعال یا غیرفعال',
}
export const dashboardHero = help(
'آخرین قیمت پایه هر گرم طلا در سیستم.',
`قیمت هر مثقال = قیمت هر گرم × ${MESGHAL_FACTOR}`
)
export const dashboardGateway = help(
'وقتی فعال باشد مشتری می‌تواند از درگاه بانکی واریز کند؛ در حالت غیرفعال فقط واریز دستی ممکن است.',
'وضعیت: فعال یا غیرفعال'
)
export const dashboardKpi = {
profit: {
description: 'مجموع سود اسپرد (تفاوت قیمت خرید/فروش با مشتری) روی همه سفارش‌های ثبت‌شده در بازه.',
formula: 'سود کل = جمع «سود هر سفارش»',
},
commission: {
description: 'مجموع کارمزد طلایی محصول که روی همان سفارش‌ها محاسبه شده.',
formula: 'کارمزد کل = جمع «کارمزد هر سفارش»',
},
orderCount: {
description: 'تعداد همه سفارش‌هایی که در بازه انتخابی ثبت شده‌اند (هر وضعیتی).',
formula: 'تعداد = شمارش سفارش‌ها بین دو تاریخ',
},
goldBalance: {
description: 'تراز وزنی طلا از سفارشات موفق؛ مثبت یعنی به مشتری بیشتر فروخته‌اید.',
formula: 'تراز طلا = گرم فروش − گرم خرید (سفارش موفق)',
},
cashBalance: {
description: 'تراز ریالی همان سفارشات موفق؛ ورود و خروج نقد از معاملات.',
formula: 'تراز ریال = مبلغ فروش − مبلغ خرید',
},
mazanehBalance: {
description: 'میانگین موزون قیمت معاملات، تبدیل‌شده به مثقال.',
formula: `(جمع مبلغ معاملات ÷ جمع گرم) × ${MESGHAL_FACTOR}`,
},
profit: help(
'مجموع سود اسپرد (تفاوت قیمت خرید/فروش با مشتری) روی همه سفارش‌های ثبت‌شده در بازه.',
'سود کل = جمع «سود هر سفارش»'
),
commission: help(
'مجموع کارمزد طلایی محصول که روی همان سفارش‌ها محاسبه شده.',
'کارمزد کل = جمع «کارمزد هر سفارش»'
),
orderCount: help(
'تعداد همه سفارش‌هایی که در بازه انتخابی ثبت شده‌اند (هر وضعیتی).',
'تعداد = شمارش سفارش‌ها بین دو تاریخ'
),
goldBalance: help(
'تراز وزنی طلا از سفارشات موفق؛ مثبت یعنی به مشتری بیشتر فروخته‌اید.',
'تراز طلا = گرم فروش − گرم خرید (سفارش موفق)'
),
cashBalance: help(
'تراز ریالی همان سفارشات موفق؛ ورود و خروج نقد از معاملات.',
'تراز ریال = مبلغ فروش − مبلغ خرید'
),
mazanehBalance: help(
'میانگین موزون قیمت معاملات، تبدیل‌شده به مثقال.',
`(جمع مبلغ معاملات ÷ جمع گرم) × ${MESGHAL_FACTOR}`
),
} as const satisfies Record<string, DashboardHelp>
export const dashboardDateRange: DashboardHelp = {
description: 'تاریخ شروع و پایان برای محاسبه کارت‌ها، تراز و نمودارها.',
formula: 'گزارش = از تاریخ … تا تاریخ …',
}
export const dashboardDateRange = help(
'تاریخ شروع و پایان برای محاسبه کارت‌ها، تراز و نمودارها.',
'گزارش = از تاریخ … تا تاریخ …'
)
export const dashboardDateMetrics = {
bankDeposit: {
description: 'کل واریزهای موفق مشتریان از درگاه آنلاین در این بازه.',
formula: 'جمع مبلغ واریزهای تأییدشده از درگاه',
},
netIncome: {
description: 'درآمد پس از کسر ارزش طلای هدیه‌داده‌شده.',
formula: 'درآمد خالص = (اختلاف قیمت اصلی و پرداختی) − (گرم هدیه × مظنه)',
},
gifts: {
description: 'طلایی که ادمین به مشتریان هدیه داده در همین بازه.',
formula: 'گرم هدیه = جمع طلای ثبت‌شده در تراکنش‌های ادمین',
},
bankDeposit: help(
'کل واریزهای موفق مشتریان از درگاه آنلاین در این بازه.',
'جمع مبلغ واریزهای تأییدشده از درگاه'
),
netIncome: help(
'درآمد پس از کسر ارزش طلای هدیه‌داده‌شده.',
'درآمد خالص = (اختلاف قیمت اصلی و پرداختی) − (گرم هدیه × مظنه)'
),
gifts: help(
'طلایی که ادمین به مشتریان هدیه داده در همین بازه.',
'گرم هدیه = جمع طلای ثبت‌شده در تراکنش‌های ادمین'
),
} as const satisfies Record<string, DashboardHelp>
export const dashboardCharts = {
incomeMix: {
description: 'نشان می‌دهد سود معامله، کارمزد و هدایا چقدر از درآمد کل را تشکیل می‌دهند.',
formula: 'درآمد = سود معامله + کارمزد + (گرم هدیه × مظنه)',
},
buySellVolume: {
description: 'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
formula: 'هر روز: گرم خرید مشتری و گرم فروش به مشتری',
},
priceChart: {
description: 'روند تغییر قیمت پایه هر گرم در ۲۴ ساعت، هفته یا ماه.',
formula: 'نمودار = قیمت پایه در هر لحظه',
},
productActivity: {
description: 'مقایسه حجم خرید و فروش هر محصول در بازه انتخابی.',
formula: 'خرید = جمع گرم مثبت | فروش = جمع گرم منفی (معاملات همان بازه)',
},
incomeMix: help(
'نشان می‌دهد سود معامله، کارمزد و هدایا چقدر از درآمد کل را تشکیل می‌دهند.',
'درآمد = سود معامله + کارمزد + (گرم هدیه × مظنه)'
),
buySellVolume: help(
'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
'هر روز: گرم خرید مشتری و گرم فروش به مشتری'
),
priceChart: help(
'روند تغییر قیمت پایه هر گرم در ۲۴ ساعت، هفته یا ماه.',
'نمودار = قیمت پایه در هر لحظه'
),
productActivity: help(
'مقایسه حجم خرید و فروش هر محصول در بازه انتخابی.',
'خرید = جمع گرم مثبت | فروش = جمع گرم منفی (معاملات همان بازه)'
),
} as const satisfies Record<string, DashboardHelp>
export const dashboardOrders = {
peakHours: {
description: 'توزیع سفارش‌های موفق مشتریان خانگی در ۲۴ ساعت شبانه‌روز؛ برای شناخت ساعات شلوغ.',
formula: 'هر ستون = تعداد سفارش در آن ساعت (جمع چند روز اخیر)',
},
recent: {
description: '۱۴ سفارش آخر با جزئیات مظنه و سود.',
formula: 'مظنه = قیمت پایه | قیمت مشتری = مظنه ± اسپرد | سود/گرم = تفاوت اسپرد',
},
peakHours: help(
'توزیع سفارش‌های موفق مشتریان خانگی در ۲۴ ساعت شبانه‌روز؛ برای شناخت ساعات شلوغ.',
'هر ستون = تعداد سفارش در آن ساعت (جمع چند روز اخیر)'
),
recent: help(
'۱۴ سفارش آخر با جزئیات مظنه و سود.',
'مظنه = قیمت پایه | قیمت مشتری = مظنه ± اسپرد | سود/گرم = تفاوت اسپرد'
),
} as const satisfies Record<string, DashboardHelp>
export const dashboardCustomers: DashboardHelp = {
description: 'آمار مشتریان خانگی ثبت‌شده در سیستم.',
formula: 'فعال = دارای کیف پول | بدون احراز = هویت تأیید نشده',
}
export const dashboardCustomers = help(
'آمار مشتریان خانگی ثبت‌شده در سیستم.',
'فعال = دارای کیف پول | بدون احراز = هویت تأیید نشده'
)
export const dashboardCustomerMetrics = {
total: {
description: 'همه مشتریان خانگی ثبت‌شده.',
formula: 'شمارش کل مشتریان',
},
new: {
description: 'مشتریانی که در ۳ ماه اخیر ثبت‌نام کرده‌اند.',
formula: 'ثبت‌نام در ۹۰ روز گذشته',
},
active: {
description: 'مشتریانی که حداقل یک کیف پول دارند.',
formula: 'دارای موجودی در کیف',
},
notVerified: {
description: 'مشتریانی که احراز هویت را تکمیل نکرده‌اند.',
formula: 'وضعیت احراز: ناقص',
},
total: help('همه مشتریان خانگی ثبت‌شده.', 'شمارش کل مشتریان'),
new: help('مشتریانی که در ۳ ماه اخیر ثبت‌نام کرده‌اند.', 'ثبت‌نام در ۹۰ روز گذشته'),
active: help('مشتریانی که حداقل یک کیف پول دارند.', 'دارای موجودی در کیف'),
notVerified: help('مشتریانی که احراز هویت را تکمیل نکرده‌اند.', 'وضعیت احراز: ناقص'),
} as const satisfies Record<string, DashboardHelp>
export const dashboardWallet: DashboardHelp = {
description: 'مجموع موجودی طلا و ریال همه مشتریان.',
formula: 'ارزش طلا به ریال = جمع گرم × مظنه لحظه‌ای',
}
export const dashboardWallet = help(
'مجموع موجودی طلا و ریال همه مشتریان.',
'ارزش طلا به ریال = جمع گرم × مظنه لحظه‌ای'
)
export const dashboardWalletMetrics = {
gold: {
description: 'مجموع طلای موجود در کیف همه مشتریان.',
formula: 'جمع گرم طلا در کیف‌ها',
},
cash: {
description: 'مجموع ریال موجود در کیف همه مشتریان.',
formula: 'جمع ریال در کیف‌ها',
},
goldToCash: {
description: 'ارزش ریالی کل طلای مشتریان با مظنه فعلی.',
formula: 'جمع گرم × قیمت هر گرم',
},
gold: help('مجموع طلای موجود در کیف همه مشتریان.', 'جمع گرم طلا در کیف‌ها'),
cash: help('مجموع ریال موجود در کیف همه مشتریان.', 'جمع ریال در کیف‌ها'),
goldToCash: help('ارزش ریالی کل طلای مشتریان با مظنه فعلی.', 'جمع گرم × قیمت هر گرم'),
} as const satisfies Record<string, DashboardHelp>
+18 -4
View File
@@ -57,7 +57,7 @@
<DashboardStatCard
tone="amber"
icon="vuesax-linear:coin"
title="کارمزد طلایی"
:title="metalLabel.goldCommission()"
:value="usePrice(finance?.fullPriceKarmozd ?? finance?.fullPriceKaermozd ?? 0, true, '')"
:help="copy.kpi.commission"
/>
@@ -68,10 +68,22 @@
:value="String(finance?.orderCount ?? 0)"
:help="copy.kpi.orderCount"
/>
<template v-if="metalBalanceItems.length">
<DashboardStatCard
v-for="item in metalBalanceItems"
:key="item.metal_id"
tone="blue"
icon="vuesax-linear:empty-wallet"
:title="metalBalanceTitle(item.metal_id, item.label)"
:value="formatGram(item.goldBalance, item.metal_id)"
:help="copy.kpi.goldBalance"
/>
</template>
<DashboardStatCard
v-else
tone="blue"
icon="vuesax-linear:empty-wallet"
title="تراز طلایی"
:title="metalLabel.goldBalance()"
:value="formatGram(balance?.goldBalance)"
:help="copy.kpi.goldBalance"
/>
@@ -452,6 +464,7 @@ import {
} from '@/pages/dashboard/dashboardSectionCopy'
import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage'
import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions'
import { metalBalanceTitle, metalLabel } from '@/utils/adminMetalLabels'
const copy = {
hero: dashboardHero,
@@ -472,6 +485,7 @@ const {
master,
finance,
balance,
metalBalanceItems,
walletSums,
recentOrders,
peakHoursRange,
@@ -573,7 +587,7 @@ const customerMetricItems = computed(() => [
const walletMetricItems = computed(() => [
{
label: 'طلای مشتریان',
label: metalLabel.customersGold(),
value: formatGram(walletSums.value?.gold),
help: dashboardWalletMetrics.gold,
variant: 'amber',
@@ -585,7 +599,7 @@ const walletMetricItems = computed(() => [
variant: 'sky',
},
{
label: 'ارزش طلا به ریال',
label: metalLabel.goldValueInRial(),
value: usePrice(walletSums.value?.goldToCash ?? 0, true, ''),
help: dashboardWalletMetrics.goldToCash,
variant: 'violet',