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
+10 -1
View File
@@ -1,10 +1,19 @@
import type { TableInput } from '@core/type/baseTable'
import useAxios from '@core/utils/useAxios'
// import { Customer } from '@/model/customer'
import type { Charge, Wallet, Withdraw, SendWeight } from '@/model/wallet'
import type { Charge, SendWeight, Wallet, WalletChargeTarget, Withdraw } from '@/model/wallet'
export const getWalletTable = async (params?: TableInput) =>
await useAxios.post<Array<Wallet>>('/admin/wallet/table', params)
export const getWalletChargeTargets = async () =>
await useAxios.get<{ targets: WalletChargeTarget[] }>('/admin/wallet/charge-targets')
/** شارژ یکپارچه — نقدی یا محصول (product_id=null یعنی ریالی) */
export const chargeWalletUnified = async (params: Charge) =>
await useAxios.post<boolean>('/admin/wallet/charge_wallet', params)
/** @deprecated از chargeWalletUnified استفاده کنید */
export const chargeWallet = async (params: Charge, type: 'gold' | 'money') =>
type === 'money'
? await useAxios.post<boolean>('/admin/wallet/charge', params)
+1
View File
@@ -28,6 +28,7 @@ export interface Customer {
wallet: Wallet
wallet_cash?: number
wallet_gold?: number
wallet_metal_gold?: Record<string, number>
n_code: string | number
last_seen_at:string
profit_amount: number
+5
View File
@@ -2,6 +2,11 @@ import type { Customer } from '@/model/customer'
export interface Order {
amount?: number
amount_display?: number
amount_unit?: string
order_metal_id?: number
order_metal_label?: string
metal_prices?: Record<string, number>
created_at?: string
customer?: Customer
customer_id?: number
+11
View File
@@ -58,6 +58,16 @@ export interface SetActiveTime {
auto_close_at: string
}
export interface MetalBalanceItem {
metal_id: number
label: string
balance: number
goldBalance: number
cashBalance?: number
goldBalanceOfLiman?: number
goldBalanceOfKalhor?: number
}
export interface Balance {
cashBalance: number
cashBalanceOfKalhor: number
@@ -65,6 +75,7 @@ export interface Balance {
goldBalance: number
goldBalanceOfKalhor: number
goldBalanceOfLiman: number
metalBalances?: MetalBalanceItem[]
mazanehBalance: number
mazanehBalanceOfKalhor: number
income_money: number
+3
View File
@@ -14,6 +14,9 @@ export interface Transaction {
hash: string
id: number
note: number
metal_gold?: Record<string, number>
transaction_metal_id?: number
transaction_metal_label?: string
order_id: number
transaction_type: string
type: number
+33
View File
@@ -1,5 +1,13 @@
import type { User } from '@/model/user'
export interface WalletMetalBalance {
metal_id: number
label: string
gold: number
goldToCash: number
display_unit?: string
}
export interface Wallet {
owner: number
id: number
@@ -7,9 +15,18 @@ export interface Wallet {
cash: number
fund_id?: number | null
product_id?: number | null
metal_gold?: Record<string, number>
customer: User
}
export interface WalletTableSums {
cash?: number
gold?: number
cashToGold?: number
goldToCash?: number
metalBalances?: WalletMetalBalance[]
}
export interface WithdrawTransaction {
title?: string
gold?: number
@@ -37,6 +54,20 @@ export interface Withdraw {
export type WalletChargeDirection = 'charge' | 'discharge'
export type WalletChargeTargetKind = 'money' | 'metal'
export interface WalletChargeTarget {
key: string
kind: WalletChargeTargetKind
label: string
unit: string
product_id?: number | null
metal_id?: number
uses_main_wallet?: boolean
trade_by_quantity?: boolean
group_type?: string
}
export interface Charge {
value?: number
note?: string
@@ -44,6 +75,8 @@ export interface Charge {
talabBedehi?: boolean
direction?: WalletChargeDirection
amountAbs?: number | string
chargeTargetKey?: string | null
product_id?: number | null
}
export interface WithdrawStatus {
+18 -1
View File
@@ -27,13 +27,27 @@
un-checked-children="مشتری"
/>
</div>
<div class="mb-4 px-2" v-if="balance?.metalBalances?.length">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div
v-for="item in balance.metalBalances"
:key="item.metal_id"
class="rounded-lg border border-gray-200 bg-white px-3 py-2"
>
<p class="mb-1 text-xs text-gray-500">{{ metalBalanceTitleWithUnit(item.metal_id, item.label) }}</p>
<p class="mb-0 text-base font-bold">
{{ formatMetalWeightFromGrams(item.goldBalanceOfLiman ?? 0, item.metal_id) }}
</p>
</div>
</div>
</div>
<div class="px-2 col-span-2 grid grid-cols-4 gap-1">
<div class="bg-blue rounded text-white py-1 flex justify-center items-center">تراز ها</div>
<div class="bg-blue rounded text-white py-1 flex justify-center items-center">
تراز ریالی (ریال)
</div>
<div class="bg-blue rounded text-white py-1 flex justify-center items-center">
تراز طلایی (گرم)
{{ m('تراز طلایی (گرم)') }}
</div>
<div class="bg-blue rounded text-white py-1 flex justify-center items-center">
تراز مظنه (ریال)
@@ -187,6 +201,9 @@ import { balance, getTaraz } from '@core/utils/price'
import { usePrice } from '@core/utils/usePrice'
import { onMounted, ref } from 'vue'
import { getNow, toJalali } from '@core/utils/useDataTime'
import { m } from '@/utils/metalLabels'
import { metalBalanceTitleWithUnit } from '@/utils/adminMetalLabels'
import { formatMetalWeightFromGrams } from '@/utils/metalWeight'
const startTaraz = ref<string>('')
const endTaraz = ref<string>('')
const selectedItem = ref<object>({})
@@ -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',
@@ -14,7 +14,7 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template>
</MobileStatCard>
<MobileStatCard bordered variant="success" label="موجودی طلایی" :value="`${row.balance_gold ?? 0} گرم`">
<MobileStatCard bordered variant="success" :label="metalLabel.goldInventory()" :value="`${row.balance_gold ?? 0} گرم`">
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
</template>
@@ -26,7 +26,7 @@
<MobileDetailRow label="نام مشتری دعوت شده" :value="row.customer?.name || '-'" />
<MobileDetailRow label="زمان ثبت" :value="row.created_at ? toJalali(row.created_at) : '-'" />
<MobileDetailRow label="نقد (ریال)" :value="usePrice(row.cash ?? 0, true, '')" />
<MobileDetailRow label="طلا (گرم)" :value="`${row.gold ?? 0}`" />
<MobileDetailRow :label="m('طلا (گرم)')" :value="`${row.gold ?? 0}`" />
<MobileDetailRow label="توضیحات" full-width :value="row.note || '-'" />
</MobileInfoGrid>
</template>
@@ -44,6 +44,8 @@ import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
import { m } from '@/utils/metalLabels'
const props = defineProps<{
visible: boolean
@@ -7,7 +7,7 @@
<div class="text-xs text-gray-400">{{ createdAtLabel }}</div>
</div>
<span class="text-xs text-gray-600">نقد: <span class="font-600 text-gray-900">{{ cashLabel }}</span></span>
<span class="text-xs text-gray-600">طلا: <span class="font-600 text-gray-900">{{ goldLabel }}</span></span>
<span class="text-xs text-gray-600">{{ metalLabel.goldUnit() }}: <span class="font-600 text-gray-900">{{ goldLabel }}</span></span>
</div>
</div>
@@ -21,6 +21,7 @@ import type { TotalAccounting } from '@/model/accounting'
import MobileCard from '@/components/mobile/MobileCard.vue'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{
item: TotalAccounting
@@ -31,7 +31,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { getTotalAccountingTable } from '@/api/accounting'
import { cols } from '@/pages/menuPages/accounting/hooks/accountingColumns'
import { getAccountingColumns } from '@/pages/menuPages/accounting/hooks/accountingColumns'
import AccountingMobileCardList from '@/pages/menuPages/accounting/components/AccountingMobileCardList.vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
@@ -51,7 +51,7 @@ const [submit, reload] = useBaseTable()
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const tblProps = ref({
columns: cols,
columns: getAccountingColumns(),
api: (params: TableInput) => getTotalAccountingTable(params)
})
@@ -2,6 +2,7 @@ import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
@@ -21,7 +22,8 @@ const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
}
})*/
export const cols: BaseTableColumn[] = [
export function getAccountingColumns(): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
@@ -63,7 +65,7 @@ export const cols: BaseTableColumn[] = [
// }
},
{
title: 'طلا',
title: metalLabel.goldUnit(),
dataIndex: 'gold',
key: 'gold',
type: 'Number',
@@ -81,4 +83,5 @@ export const cols: BaseTableColumn[] = [
slot: 'action',
filterable: false
}
]
]
}
+7 -6
View File
@@ -20,7 +20,7 @@
<a-form :model="form" ref="formRef" autocomplete="off">
<div class="grid md:grid-cols-1 gap-4">
<div class="bg-gray-50 px-10 py-3 flex justify-center gap-4">
<div>قیمت {{ form.direction === 1 ? 'خرید' : 'فروش' }} طلا:</div>
<div>{{ metalLabel.goldTradeBuySell(form.direction) }}</div>
<div class="font-bold">
{{ usePrice(form.direction === 1 ? dataLast?.buy ?? 0 : dataLast?.sell ?? 0) }}
</div>
@@ -65,7 +65,7 @@
</a-form-item>
<BasicInfoItem
is-line
title="کیف پول طلا"
:title="metalLabel.goldWalletTitle()"
class="mt-2"
:value="getGoldBalance(selectedCustomer?.wallet) || '_'"
/>
@@ -95,7 +95,7 @@
addon-before="گرم"
addon-bg-color="#0A1B37"
addon-text-color="white"
place-holder="مقدار طلا"
:place-holder="metalLabel.goldPlaceholder()"
type="tel"
@onchange="calculateCash"
/>
@@ -143,6 +143,7 @@ import { getLastPrice } from '@/api/price'
import type { last } from '@/model/price'
import { createOrder } from '@/api/order'
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
//loading
const loading = ref<boolean>(false)
@@ -248,7 +249,7 @@ function info(val: Order) {
value: val.amount ?? '-'
},
{
key: 'قیمت هر گرم طلا برای مشتری (ریال)',
key: metalLabel.goldGramPriceCustomer(),
value: usePrice(val.price ?? 0) ?? '-'
},
{
@@ -256,7 +257,7 @@ function info(val: Order) {
value: usePrice(val.total_price ?? 0) ?? '-'
},
{
key: 'قیمت هر گرم طلا بدون سود (ریال)',
key: metalLabel.goldGramPriceOriginal(),
value: usePrice(val.price_original ?? 0) ?? '-'
},
{
@@ -284,7 +285,7 @@ function info(val: Order) {
value: usePrice(val.service_charge_price ?? 0)
},*/
{
key: 'کارمزد طلایی',
key: metalLabel.goldCommission(),
value: val.service_charge ?? '-' + ' گرم'
},
/* {
@@ -93,7 +93,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|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)/.test(
k
)
},
@@ -123,9 +123,9 @@
>
<BasicInfoItem
title="نام قلک"
:value="item?.fund ? item?.fund?.title : 'کیف پول طلایی'"
:value="item?.fund ? item?.fund?.title : metalLabel.goldWallet()"
/>
<BasicInfoItem title="مقدار طلا" :value="`${item?.gold} گرم`" />
<BasicInfoItem :title="metalLabel.goldAmount()" :value="`${item?.gold} گرم`" />
</div>
</div>
</div>
@@ -158,6 +158,7 @@ import type { FormInstance } from 'ant-design-vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { message } from 'ant-design-vue/es'
import type { Info } from '@/components/BasicInfoModal/model/Info'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
@@ -21,12 +21,12 @@
<CustomerMobileDetailSheet
v-model:visible="sheetVisible"
:item="selectedItem"
:metals="catalogMetals"
@info="forwardAndClose('info', $event)"
@edit="forwardAndClose('edit', $event)"
@archive="forwardAndClose('archive', $event)"
@toggleActive="forward('toggleActive', $event)"
@chargeMoney="forwardAndClose('chargeMoney', $event)"
@chargeGold="forwardAndClose('chargeGold', $event)"
@chargeWallet="forwardAndClose('chargeWallet', $event)"
@toggleTrade="forward('toggleTrade', $event)"
@confirmUser="forwardAndClose('confirmUser', $event)"
/>
@@ -40,14 +40,22 @@ import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileD
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
import CustomerMobileListItem from './CustomerMobileListItem.vue'
import CustomerMobileDetailSheet from './CustomerMobileDetailSheet.vue'
import type { CustomerMetalColumn } from '../hooks/customerColumns'
const catalogMetals = ref<CustomerMetalColumn[]>([])
async function fetchCustomers(params: Parameters<typeof getCustomerTable>[0]) {
const data = await getCustomerTable(params)
catalogMetals.value = data?.catalogMetals ?? []
return data
}
const emit = defineEmits<{
(e: 'info', item: Customer): void
(e: 'edit', item: Customer): void
(e: 'archive', item: Customer): void
(e: 'toggleActive', item: Customer): void
(e: 'chargeMoney', item: Customer): void
(e: 'chargeGold', item: Customer): void
(e: 'chargeWallet', item: Customer): void
(e: 'toggleTrade', item: Customer): void
(e: 'confirmUser', item: Customer): void
}>()
@@ -59,8 +67,7 @@ type SheetAction =
| 'edit'
| 'archive'
| 'toggleActive'
| 'chargeMoney'
| 'chargeGold'
| 'chargeWallet'
| 'toggleTrade'
| 'confirmUser'
@@ -74,7 +81,7 @@ function forwardAndClose(action: SheetAction, item: Customer) {
}
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<Customer>({
fetchApi: getCustomerTable,
fetchApi: fetchCustomers,
onFetched: (rows) => syncSelectedItem(rows)
})
@@ -4,12 +4,15 @@
<h2 class="text-base font-700 leading-relaxed text-gray-900">{{ row.name || '-' }}</h2>
<p v-if="row.mobile" class="text-sm text-gray-500" dir="ltr">{{ row.mobile }}</p>
<div class="grid grid-cols-2 gap-2">
<BaseButton class="w-full" size="middle" type="primary" icon="vuesax-linear:wallet-add" @click="closeAndEmit('chargeMoney', row)">
شارژ ریالی
</BaseButton>
<BaseButton class="w-full" size="middle" type="primary" icon="vuesax-linear:wallet-money" @click="closeAndEmit('chargeGold', row)">
شارژ طلایی
<div class="grid grid-cols-1 gap-2">
<BaseButton
class="w-full"
size="middle"
type="primary"
icon="vuesax-linear:wallet-add"
@click="closeAndEmit('chargeWallet', row)"
>
شارژ / دشارژ کیف پول
</BaseButton>
<BaseButton
v-if="!row.is_legal"
@@ -51,7 +54,14 @@
</div>
<div class="space-y-2">
<MobileStatCard bordered variant="success" label="موجودی طلایی" :value="`${goldBalance(row)} گرم`">
<MobileStatCard
v-for="entry in metalBalanceEntries(row)"
:key="entry.metal_id"
bordered
variant="success"
:label="customerWalletMetalBalanceTitle(entry.metal_id, entry.label)"
:value="entry.value"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
</template>
@@ -83,7 +93,6 @@
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف کاربر" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="گنج (گرم)" :value="row.gold ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid>
</template>
@@ -98,13 +107,17 @@ import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet'
import { getCashBalance } from '@/components/mobile/utils/wallet'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import type { CustomerMetalColumn } from '../hooks/customerColumns'
import { formatMetalDisplayValue, metalCellValue, metalDisplayUnit } from '@/utils/metalWeight'
const props = defineProps<{
visible: boolean
item: Customer | null
metals?: CustomerMetalColumn[]
}>()
const emit = defineEmits<{
@@ -113,8 +126,7 @@ const emit = defineEmits<{
(e: 'edit', item: Customer): void
(e: 'archive', item: Customer): void
(e: 'toggleActive', item: Customer): void
(e: 'chargeMoney', item: Customer): void
(e: 'chargeGold', item: Customer): void
(e: 'chargeWallet', item: Customer): void
(e: 'toggleTrade', item: Customer): void
(e: 'confirmUser', item: Customer): void
}>()
@@ -124,7 +136,7 @@ const visibleProxy = computed({
set: (value: boolean) => emit('update:visible', value)
})
type SheetEmit = 'archive' | 'chargeMoney' | 'chargeGold' | 'info' | 'confirmUser' | 'edit'
type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit'
function closeAndEmit(event: SheetEmit, row: Customer) {
visibleProxy.value = false
@@ -157,8 +169,24 @@ function cashBalance(row: Customer) {
return getCashBalance(row.wallet)
}
function goldBalance(row: Customer) {
return getGoldBalance(row.wallet)
function metalBalanceEntries(row: Customer) {
const metals = props.metals?.length
? props.metals
: [{ metal_id: 1, label: 'طلا' }]
return metals
.map((metal) => {
const value = metalCellValue(row as Record<string, unknown>, metal.metal_id, 'wallet_metal_gold')
return {
metal_id: metal.metal_id,
label: metal.label,
value: formatMetalDisplayValue(value, metal.metal_id)
}
})
.filter((entry) => {
const numeric = metalCellValue(row as Record<string, unknown>, entry.metal_id, 'wallet_metal_gold')
return numeric !== 0
})
}
function lastSeen(row: Customer) {
@@ -6,6 +6,18 @@
class="wallet-charge-form"
:class="formClass"
>
<a-form-item label="کیف پول / محصول" name="chargeTargetKey" :rules="targetRules">
<a-select
v-model:value="modelValue.chargeTargetKey"
show-search
placeholder="انتخاب کنید"
class="w-full"
:options="targetOptions"
:filter-option="filterTarget"
@change="onTargetChange"
/>
</a-form-item>
<a-form-item label="نوع عملیات" name="direction">
<a-radio-group
v-model:value="modelValue.direction"
@@ -36,10 +48,10 @@
<BasicInput
v-model:value="modelValue.amountAbs"
:addon-before="unit"
:type="variant === 'money' ? 'price' : 'number'"
:type="isMoney ? 'price' : 'number'"
:place-holder="amountPlaceholder"
/>
<div v-if="variant === 'money' && Number(modelValue.amountAbs) > 10" class="wallet-charge-form__hint">
<div v-if="isMoney && Number(modelValue.amountAbs) > 10" class="wallet-charge-form__hint">
معادل {{ usePrice(Number(modelValue.amountAbs) / 10, true, 'تومان') }}
</div>
</a-form-item>
@@ -51,22 +63,46 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import type { FormInstance } from 'ant-design-vue'
import type { Charge } from '@/model/wallet'
import type { Charge, WalletChargeTarget } from '@/model/wallet'
import { inputRules } from '@/utils/inputRules'
import { usePrice } from '@core/utils/usePrice'
import { applyChargeTarget, resolveChargeTarget, walletPreviewLabel } from '../utils/walletChargeForm'
const props = defineProps<{
variant: 'money' | 'gold'
modelValue: Charge
targets: WalletChargeTarget[]
}>()
const formRef = ref<FormInstance>()
const unit = computed(() => (props.variant === 'money' ? 'ریال' : 'گرم'))
const selectedTarget = computed(() =>
resolveChargeTarget(props.targets, props.modelValue.chargeTargetKey)
)
const isMoney = computed(() => selectedTarget.value?.kind === 'money')
const unit = computed(() => selectedTarget.value?.unit ?? 'ریال')
const isDischarge = computed(() => props.modelValue.direction === 'discharge')
const targetOptions = computed(() =>
props.targets.map((target) => ({
value: target.key,
label: target.label
}))
)
const targetRules = [
{
validator: (_rule: unknown, value: string | undefined) => {
if (!value || !resolveChargeTarget(props.targets, value)) {
return Promise.reject('کیف پول یا محصول را انتخاب کنید')
}
return Promise.resolve()
}
}
]
const formClass = computed(() =>
isDischarge.value ? 'wallet-charge-form--discharge' : 'wallet-charge-form--charge'
)
@@ -81,8 +117,7 @@ const previewSign = computed(() => (isDischarge.value ? '−' : '+'))
const previewText = computed(() => {
const action = isDischarge.value ? 'کسر از' : 'افزایش'
const wallet = props.variant === 'money' ? 'موجودی ریالی' : 'موجودی طلایی'
return `${action} ${wallet} کاربر`
return `${action} ${walletPreviewLabel(selectedTarget.value)}`
})
const amountLabel = computed(() =>
@@ -92,7 +127,7 @@ const amountLabel = computed(() =>
const noteLabel = computed(() => (isDischarge.value ? 'علت دشارژ' : 'علت شارژ'))
const amountPlaceholder = computed(() =>
props.variant === 'money' ? 'مثلاً ۱۰۰٬۰۰۰' : 'مثلاً ۰.۵'
isMoney.value ? 'مثلاً ۱۰۰٬۰۰۰' : 'مثلاً ۰.۵'
)
const notePlaceholder = computed(() =>
@@ -112,6 +147,28 @@ const amountRules = [
}
]
function filterTarget(input: string, option?: { label?: string }) {
return String(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
function onTargetChange(key: string) {
const target = resolveChargeTarget(props.targets, key)
if (!target) return
Object.assign(props.modelValue, applyChargeTarget(props.modelValue, target))
}
watch(
() => props.targets,
(targets) => {
if (!targets.length) return
const current = resolveChargeTarget(targets, props.modelValue.chargeTargetKey)
if (!current) {
Object.assign(props.modelValue, applyChargeTarget(props.modelValue, targets[0]))
}
},
{ immediate: true }
)
defineExpose({
validateFields: () => formRef.value?.validateFields()
})
@@ -49,14 +49,10 @@
:color="record.active ? '#54AD06' : '#ad0627'"
/>
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('chargeMoney', record)">
<template #title><span>شارژ کیف پول ریالی</span></template>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('chargeWallet', record)">
<template #title><span>شارژ / دشارژ کیف پول</span></template>
<Icon icon="vuesax-linear:wallet-add" color="#9333ea" />
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('chargeGold', record)">
<template #title><span>شارژ کیف پول طلایی</span></template>
<Icon icon="vuesax-linear:wallet-money" color="#ea580c" />
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
<template #title><span>مشخصات</span></template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
@@ -100,8 +96,7 @@
@edit="emits('edit', $event)"
@archive="handleArchive"
@toggleActive="(item) => changeCustomerActivationFunction(item.id)"
@chargeMoney="emits('chargeMoney', $event)"
@chargeGold="emits('chargeGold', $event)"
@chargeWallet="emits('chargeWallet', $event)"
@toggleTrade="(item) => changeCustomerOrderActivationFunction(item.id)"
@confirmUser="openConfirmUser"
/>
@@ -144,7 +139,7 @@ import {
confirmUser,
getCustomerTable
} from '@/api/customer'
import { cols } from '@/pages/menuPages/customers/base/hooks/customerColumns'
import { buildCustomerColumns } from '@/pages/menuPages/customers/base/hooks/customerColumns'
import CustomerMobileCardList from '@/pages/menuPages/customers/base/components/CustomerMobileCardList.vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
@@ -155,7 +150,7 @@ import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
import { isMobile } from '@core/utils/is'
const [submit, reload] = useBaseTable()
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'chargeMoney', 'chargeGold'])
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'chargeWallet'])
const loading = ref(false)
const confirmVisible = ref(false)
@@ -163,8 +158,13 @@ const confirmInfo = ref<Customer>()
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const tblProps = ref<any>({
columns: cols,
api: (params: TableInput) => getCustomerTable(params)
columns: buildCustomerColumns(),
api: async (params: TableInput) => {
const data = await getCustomerTable(params)
const metals = data?.catalogMetals ?? []
tblProps.value.columns = buildCustomerColumns(metals)
return data
}
})
const fetch = async () => {
@@ -1,212 +1,164 @@
import type { BaseTableColumn } from '@core/type/baseTable'
import {toJalali, toJalaliNoTime, toTime} from '@core/utils/useDataTime'
import { toJalali } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
import { getCashBalance } from '@/components/mobile/utils/wallet'
import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
//define store
//const store = useStaticApiListsStore()
//document types
/*let docTypes: DocItem[]*/
/*onMounted(async () => {
docTypes = await store.readyDocTypeList()
})
const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
return {
label: x.title,
value: x.id
}
})*/
export type CustomerMetalColumn = { metal_id: number; label: string; display_unit?: string }
export const cols: BaseTableColumn[] = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
customRender: (record: any) => {
return record.index + 1
}
},
{
title: 'کد کاربری',
dataIndex: 'id',
key: 'id',
type: 'Number'
// className: 'column-money'
},
{
title: 'نام و نام خانوادگی',
dataIndex: 'name',
key: 'name',
type: 'Text'
},
{
title: 'شماره موبایل',
dataIndex: 'mobile',
key: 'mobile',
type: 'Number'
},
{
title: 'نوع کاربر',
dataIndex: 'type_str',
key: 'type_str',
type: 'Text'
},
{
title: 'کد معرف',
dataIndex: 'referral_id',
key: 'referral_id',
type: 'Text'
},
{
title: 'نام معرف کاربر',
dataIndex: 'referral.name',
key: 'referral.name',
type: 'Text'
},
{
title: 'موجودی ریالی (ریال)',
dataIndex: 'wallet.cash',
key: 'wallet.cash',
type: 'Number',
customRender(data) {
return usePrice(getCashBalance(data.record?.wallet, { walletCash: data.record?.wallet_cash }), true, '')
}
},
{
title: 'موجودی طلایی (گرم)',
dataIndex: 'wallet.gold',
key: 'wallet.gold',
type: 'Number',
filterable: false,
customRender(data) {
return getGoldBalance(data.record?.wallet, { walletGold: data.record?.wallet_gold })
}
},
{
title: 'اخرین بازدید',
dataIndex: 'last_seen_at',
key: 'last_seen_at',
type: 'DateTime',
customRender(data) {
return toJalali(data.record.last_seen_at)
}
},
{
title: 'گنج(گرم)',
dataIndex: 'gold',
key: 'gold',
type: 'Number',
filterable: false,
},
{
title: 'وضعیت کاربر',
dataIndex: 'active',
key: 'active',
type: 'Boolean',
customRender: ({ record }) => {
return record.active ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
فعال
</CustomTag>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
غیر فعال
</CustomTag>
</div>
)
}
},
{
title: 'وضعیت سفارش دهی',
dataIndex: 'trade_active',
key: 'trade_active',
sortable: false,
type: 'Boolean',
customRender: ({ record }) => {
return record.trade_active ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
مجاز
</CustomTag>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
غیر مجاز
</CustomTag>
</div>
)
}
},
/*{
title: 'نقش',
dataIndex: 'user_type_record',
key: 'user_type_record',
type: 'Select',
export function buildCustomerColumns(metals: CustomerMetalColumn[] = []): BaseTableColumn[] {
const baseColumns: BaseTableColumn[] = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
filterComponentProps: {
multiple: true
},
filterField: 'user_type_obj.id',
filterComponent: ReadyUserTypeSelect,
/!* customRender: (record: any) => {
return record.record.user_type_record?.value
}*!/
customRender: ({ record }) => {
const items = record.is_admin ? (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ادمین
</CustomTag>
) : (
record.user_type_obj.map((item: { title: any }, index: any) => {
if (item) {
return (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
{item?.title}
</CustomTag>
)
}
})
)
return <div class="flex justify-center items-center gap-2">{items}</div>
customRender: (record: any) => {
return record.index + 1
}
},
{
title: 'حوزه',
dataIndex: 'section',
key: 'section'
title: 'کد کاربری',
dataIndex: 'id',
key: 'id',
type: 'Number'
},
{
title: 'نام و نام خانوادگی',
dataIndex: 'name',
key: 'name',
type: 'Text'
},
{
title: 'شماره موبایل',
dataIndex: 'mobile',
type: 'Number',
key: 'mobile'
key: 'mobile',
type: 'Number'
},
{
title: 'تاریخ ثبت',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: any) => {
return toJalaliNoTime(record.record.created_at)
title: 'نوع کاربر',
dataIndex: 'type_str',
key: 'type_str',
type: 'Text'
},
{
title: 'کد معرف',
dataIndex: 'referral_id',
key: 'referral_id',
type: 'Text'
},
{
title: 'نام معرف کاربر',
dataIndex: 'referral.name',
key: 'referral.name',
type: 'Text'
},
{
title: 'موجودی ریالی (ریال)',
dataIndex: 'wallet.cash',
key: 'wallet.cash',
type: 'Number',
customRender(data) {
return usePrice(getCashBalance(data.record?.wallet, { walletCash: data.record?.wallet_cash }), true, '')
}
},*/
{
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
filterable: false
}
]
}
]
const metalColumns: BaseTableColumn[] =
metals.length > 0
? metals.map((metal) => ({
title: customerWalletMetalBalanceTitle(metal.metal_id, metal.label),
dataIndex: `wallet_metal_gold_${metal.metal_id}`,
key: `wallet_metal_${metal.metal_id}`,
type: 'Number',
filterable: false,
customRender(data) {
const value = metalCellValue(data.record, metal.metal_id, 'wallet_metal_gold')
return formatMetalDisplayValue(value, metal.metal_id)
}
}))
: [
{
title: customerWalletMetalBalanceTitle(1, 'طلا'),
dataIndex: 'wallet.gold',
key: 'wallet.gold',
type: 'Number',
filterable: false,
customRender(data) {
const value = Number(data.record?.wallet_gold ?? 0)
return formatMetalDisplayValue(value, 1)
}
}
]
return [
...baseColumns,
...metalColumns,
{
title: 'اخرین بازدید',
dataIndex: 'last_seen_at',
key: 'last_seen_at',
type: 'DateTime',
customRender(data) {
return toJalali(data.record.last_seen_at)
}
},
{
title: 'وضعیت کاربر',
dataIndex: 'active',
key: 'active',
type: 'Boolean',
customRender: ({ record }) => {
return record.active ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
فعال
</CustomTag>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
غیر فعال
</CustomTag>
</div>
)
}
},
{
title: 'وضعیت سفارش دهی',
dataIndex: 'trade_active',
key: 'trade_active',
sortable: false,
type: 'Boolean',
customRender: ({ record }) => {
return record.trade_active ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
مجاز
</CustomTag>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
غیر مجاز
</CustomTag>
</div>
)
}
},
{
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
filterable: false
}
]
}
export function getCustomerColumns(): BaseTableColumn[] {
return buildCustomerColumns()
}
+71 -88
View File
@@ -6,8 +6,7 @@
@info="info"
@edit="edit"
@add="add"
@chargeMoney="walletMoneyCharge"
@chargeGold="walletGoldCharge"
@chargeWallet="walletCharge"
/>
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" />
@@ -66,9 +65,6 @@
<a-form-item label="میزان پاداش" name="profit_amount">
<BasicInput v-model:value="form.profit_amount" />
</a-form-item>
<a-form-item label="گنج" name="gold">
<BasicInput v-model:value="form.gold" />
</a-form-item>
<a-form-item label="حداکثر معرفی" name="add_user_limit">
<BasicInput v-model:value="form.add_user_limit" />
</a-form-item>
@@ -191,9 +187,9 @@
>
<BasicInfoItem
title="نام قلک"
:value="item?.fund ? item?.fund?.title : 'کیف پول طلایی'"
:value="item?.fund ? item?.fund?.title : metalLabel.goldWallet()"
/>
<BasicInfoItem title="مقدار طلا" :value="`${item?.gold} گرم`" />
<BasicInfoItem :title="metalLabel.goldAmount()" :value="`${item?.gold} گرم`" />
</div>
</div>
</div>
@@ -217,60 +213,39 @@
</BasicActionSheet>
<BasicActionSheet
v-if="isMobile"
v-model:visible="showMoneyWalletCharge"
:title="moneyChargeTitle"
modal-width="24rem"
v-model:visible="showWalletCharge"
:title="walletChargeModalTitle"
modal-width="26rem"
:loading="loading"
:show-button="true"
:show-cancel="true"
:show-ok="true"
:ok-text="moneyChargeOkText"
:ok-text="walletChargeOkLabel"
cancel-text="انصراف"
@ok="chargeMoneyWallet"
@close="showMoneyWalletCharge = false"
@ok="submitWalletCharge"
@close="closeWalletCharge"
>
<WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" />
<WalletChargeForm
ref="walletChargeFormRef"
:model-value="formCharge"
:targets="chargeTargets"
/>
</BasicActionSheet>
<BasicModal
v-else
v-model:visible="showMoneyWalletCharge"
width="22rem"
:title="moneyChargeTitle"
:ok-text="moneyChargeOkText"
@ok="chargeMoneyWallet"
v-model:visible="showWalletCharge"
width="24rem"
:title="walletChargeModalTitle"
:ok-text="walletChargeOkLabel"
@ok="submitWalletCharge"
:loading="loading"
@close="showMoneyWalletCharge = false"
@close="closeWalletCharge"
>
<WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" />
</BasicModal>
<BasicActionSheet
v-if="isMobile"
v-model:visible="showGoldWalletCharge"
:title="goldChargeTitle"
modal-width="24rem"
:loading="loading"
:show-button="true"
:show-cancel="true"
:show-ok="true"
:ok-text="goldChargeOkText"
cancel-text="انصراف"
@ok="chargeGoldWallet"
@close="showGoldWalletCharge = false"
>
<WalletChargeForm ref="goldChargeFormRef" :model-value="formCharge" variant="gold" />
</BasicActionSheet>
<BasicModal
v-else
v-model:visible="showGoldWalletCharge"
width="22rem"
:title="goldChargeTitle"
:ok-text="goldChargeOkText"
@ok="chargeGoldWallet"
:loading="loading"
@close="showGoldWalletCharge = false"
>
<WalletChargeForm ref="goldChargeFormRef" :model-value="formCharge" variant="gold" />
<WalletChargeForm
ref="walletChargeFormRef"
:model-value="formCharge"
:targets="chargeTargets"
/>
</BasicModal>
<BasicInfoModal
v-model:visible="userInfoOrderVisible"
@@ -302,9 +277,9 @@ import { cloneDeep } from 'lodash'
import type { Item } from '@/model/Item'
import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import type { Charge } from '@/model/wallet'
import type { Charge, WalletChargeTarget } from '@/model/wallet'
import customers from '@/router/routes/customers'
import { chargeWallet } from '@/api/wallet'
import { chargeWalletUnified, getWalletChargeTargets } from '@/api/wallet'
import { usePrice } from '@core/utils/usePrice'
import jalaliday from 'jalaliday'
import ordersTbl from '@/pages/menuPages/orders/components/table.vue'
@@ -317,12 +292,15 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { isMobile } from '@core/utils/is'
import WalletChargeForm from './components/WalletChargeForm.vue'
import {
applyChargeTarget,
buildChargePayload,
createEmptyChargeForm,
resolveChargeTarget,
walletChargeOkText,
walletChargeSuccessMessage,
walletChargeTitle
} from './utils/walletChargeForm'
import { metalLabel } from '@/utils/adminMetalLabels'
//tabs
const activeKey = ref('اطلاعات جامع')
@@ -376,21 +354,23 @@ const customerItems: Array<Item> = [
]
//wallet
const showMoneyWalletCharge = ref<boolean>(false)
const showGoldWalletCharge = ref<boolean>(false)
const showWalletCharge = ref<boolean>(false)
const chargeTargets = ref<WalletChargeTarget[]>([])
const userInfoOrderVisible = ref<boolean>(false)
//form
const form = ref<Partial<Customer>>({})
const formRef = ref<FormInstance>()
const formCharge = ref<Charge>({})
const moneyChargeFormRef = ref<InstanceType<typeof WalletChargeForm>>()
const goldChargeFormRef = ref<InstanceType<typeof WalletChargeForm>>()
const walletChargeFormRef = ref<InstanceType<typeof WalletChargeForm>>()
const moneyChargeTitle = computed(() => walletChargeTitle('money', formCharge.value.direction))
const goldChargeTitle = computed(() => walletChargeTitle('gold', formCharge.value.direction))
const moneyChargeOkText = computed(() => walletChargeOkText(formCharge.value.direction))
const goldChargeOkText = computed(() => walletChargeOkText(formCharge.value.direction))
const selectedChargeTarget = computed(() =>
resolveChargeTarget(chargeTargets.value, formCharge.value.chargeTargetKey)
)
const walletChargeModalTitle = computed(() =>
walletChargeTitle(selectedChargeTarget.value, formCharge.value.direction)
)
const walletChargeOkLabel = computed(() => walletChargeOkText(formCharge.value.direction))
//info
let infoParams = reactive<Info>({})
@@ -407,7 +387,7 @@ function infoOrder(val: Order) {
value: val.amount ?? '-'
},
{
key: 'قیمت هر گرم طلا برای مشتری (ریال)',
key: metalLabel.goldGramPriceCustomer(),
value: usePrice(val.price ?? 0) ?? '-'
},
{
@@ -415,7 +395,7 @@ function infoOrder(val: Order) {
value: usePrice(val.total_price ?? 0) ?? '-'
},
{
key: '/قیمت هر گرم طلا بدون سود (ریال)',
key: metalLabel.goldGramPriceOriginalSlash(),
value: usePrice(val.price_original ?? 0) ?? '-'
},
{
@@ -443,7 +423,7 @@ function infoOrder(val: Order) {
value: usePrice(val.service_charge_price ?? 0)
},*/
{
key: 'کارمزد طلایی',
key: metalLabel.goldCommission(),
value: val.service_charge ?? '-' + ' گرم'
},
/* {
@@ -511,40 +491,43 @@ async function add() {
function resetFormCharge(customerId: number) {
formCharge.value = createEmptyChargeForm(customerId)
const firstTarget = chargeTargets.value[0]
if (firstTarget) {
Object.assign(formCharge.value, applyChargeTarget(formCharge.value, firstTarget))
}
}
async function walletMoneyCharge(val: Customer) {
resetFormCharge(val.id)
showMoneyWalletCharge.value = true
async function loadChargeTargets() {
if (chargeTargets.value.length) return
const data = await getWalletChargeTargets()
chargeTargets.value = data?.targets ?? []
}
async function walletGoldCharge(val: Customer) {
resetFormCharge(val.id)
showGoldWalletCharge.value = true
async function walletCharge(val: Customer) {
try {
loading.value = true
await loadChargeTargets()
resetFormCharge(val.id)
showWalletCharge.value = true
} catch (e) {
console.error(e)
message.error('خطا در بارگذاری گزینه‌های شارژ')
} finally {
loading.value = false
}
}
async function chargeMoneyWallet() {
moneyChargeFormRef.value?.validateFields()?.then(async () => {
function closeWalletCharge() {
showWalletCharge.value = false
}
async function submitWalletCharge() {
walletChargeFormRef.value?.validateFields()?.then(async () => {
try {
loading.value = true
await chargeWallet(buildChargePayload(formCharge.value), 'money')
await chargeWalletUnified(buildChargePayload(formCharge.value))
message.success(walletChargeSuccessMessage(formCharge.value.direction))
showMoneyWalletCharge.value = false
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
})
}
async function chargeGoldWallet() {
goldChargeFormRef.value?.validateFields()?.then(async () => {
try {
loading.value = true
await chargeWallet(buildChargePayload(formCharge.value), 'gold')
message.success(walletChargeSuccessMessage(formCharge.value.direction))
showGoldWalletCharge.value = false
showWalletCharge.value = false
} catch (e) {
console.log(e)
} finally {
@@ -1,4 +1,4 @@
import type { Charge, WalletChargeDirection } from '@/model/wallet'
import type { Charge, WalletChargeDirection, WalletChargeTarget } from '@/model/wallet'
export function createEmptyChargeForm(customerId?: number): Charge {
return {
@@ -7,7 +7,9 @@ export function createEmptyChargeForm(customerId?: number): Charge {
note: undefined,
talabBedehi: false,
direction: 'charge',
amountAbs: undefined
amountAbs: undefined,
chargeTargetKey: 'money',
product_id: null
}
}
@@ -18,17 +20,35 @@ export function buildChargePayload(form: Charge): Charge {
value: signed,
note: form.note,
customer_id: form.customer_id,
talabBedehi: form.talabBedehi === true
talabBedehi: form.talabBedehi === true,
product_id: form.product_id ?? null
}
}
export function resolveChargeTarget(
targets: WalletChargeTarget[],
key?: string | null
): WalletChargeTarget | undefined {
if (!key) return targets[0]
return targets.find((target) => target.key === key)
}
export function applyChargeTarget(form: Charge, target?: WalletChargeTarget): Charge {
if (!target) return form
return {
...form,
chargeTargetKey: target.key,
product_id: target.kind === 'metal' ? target.product_id ?? null : null
}
}
export function walletChargeTitle(
variant: 'money' | 'gold',
target: WalletChargeTarget | undefined,
direction: WalletChargeDirection = 'charge'
) {
const wallet = variant === 'money' ? 'ریالی' : 'طلایی'
const action = direction === 'discharge' ? 'دشارژ' : 'شارژ'
return `${action} کیف پول ${wallet}`
const wallet = target?.label ?? 'کیف پول'
return `${action} ${wallet}`
}
export function walletChargeOkText(direction: WalletChargeDirection = 'charge') {
@@ -38,3 +58,10 @@ export function walletChargeOkText(direction: WalletChargeDirection = 'charge')
export function walletChargeSuccessMessage(direction: WalletChargeDirection = 'charge') {
return direction === 'discharge' ? 'دشارژ با موفقیت انجام شد' : 'شارژ با موفقیت انجام شد'
}
export function walletPreviewLabel(target: WalletChargeTarget | undefined): string {
if (!target) return 'موجودی کاربر'
if (target.kind === 'money') return 'موجودی ریالی'
if (target.uses_main_wallet) return `موجودی ${target.label.replace(/\s*\(کیف اصلی\)$/, '')} (کیف اصلی)`
return `موجودی ${target.label}`
}
@@ -42,6 +42,7 @@ import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{
visible: boolean
@@ -82,7 +83,7 @@ function statusClass(row: Giftcardcode) {
}
function unitLabel(row: Giftcardcode) {
return row.unit === 2 ? 'طلایی' : 'ریالی'
return row.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'
}
function unitTagClass(row: Giftcardcode) {
@@ -19,6 +19,7 @@
<script setup lang="ts">
import type { Giftcardcode } from '@/model/giftCard'
import MobileCard from '@/components/mobile/MobileCard.vue'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{
item: Giftcardcode
@@ -50,7 +51,7 @@ const statusClass = computed(() => {
}
})
const unitLabel = computed(() => (props.item.unit === 2 ? 'طلایی' : 'ریالی'))
const unitLabel = computed(() => (props.item.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'))
const unitTagClass = computed(() =>
props.item.unit === 2
? 'border-amber-300 bg-amber-50 text-amber-700'
@@ -28,7 +28,7 @@
<CustomTag v-else textColor="#C53030" bgColor="#FECACA">غیرفعال</CustomTag>
</template>
<template v-else-if="column.key === 'unit'">
<CustomTag v-if="record.unit === 2" textColor="#C53030" bgColor="#FECACA">طلایی</CustomTag>
<CustomTag v-if="record.unit === 2" textColor="#C53030" bgColor="#FECACA">{{ metalLabel.goldWalletShort() }}</CustomTag>
<CustomTag v-else textColor="#25855A" bgColor="#E3F4E9">ریالی</CustomTag>
</template>
<template v-else-if="column.key === 'amount'">
@@ -68,6 +68,7 @@ import BasicActionSheet from '@/components/BasicActionSheet.vue'
import CustomTag from '@/components/CustomTag.vue'
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { toJalali } from '@core/utils/useDataTime'
import GiftCardCodeMobileDetailSheet from './GiftCardCodeMobileDetailSheet.vue'
import GiftCardCodeMobileListItem from './GiftCardCodeMobileListItem.vue'
@@ -12,7 +12,7 @@
<a-form-item label="نوع کارت هدیه" name="unit" :rules="inputRules()">
<a-radio-group v-model:value="form.unit" button-style="solid">
<a-radio-button value="1">ریالی</a-radio-button>
<a-radio-button value="2">طلایی</a-radio-button>
<a-radio-button value="2">{{ metalLabel.goldWalletShort() }}</a-radio-button>
</a-radio-group>
</a-form-item>
@@ -48,6 +48,7 @@
import type { FormInstance } from 'ant-design-vue'
import type { GiftcardInput } from '@/model/giftCard'
import { inputLenRules, inputRules, inputRulesNumber } from '@/utils/inputRules'
import { metalLabel } from '@/utils/adminMetalLabels'
defineProps<{
form: GiftcardInput
@@ -63,6 +63,7 @@ import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is'
import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
type GiftCardRow = Giftcard & { customer?: { name?: string; mobile?: string } }
@@ -83,7 +84,7 @@ const visibleProxy = computed({
const sheetTitle = computed(() => props.item?.unique_code || `کارت هدیه #${props.item?.id ?? '-'}`)
function unitLabel(row: GiftCardRow) {
return row.unit === 2 ? 'طلایی' : 'ریالی'
return row.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'
}
function unitTagClass(row: GiftCardRow) {
@@ -31,6 +31,7 @@
import type { Giftcard } from '@/model/giftCard'
import MobileCard from '@/components/mobile/MobileCard.vue'
import MobileIconButton from '@/components/mobile/MobileIconButton.vue'
import { metalLabel } from '@/utils/adminMetalLabels'
type GiftCardRow = Giftcard & { customer?: { name?: string; mobile?: string } }
@@ -44,7 +45,7 @@ defineEmits<{
(e: 'giftCardCodes', item: GiftCardRow): void
}>()
const unitLabel = computed(() => (props.item.unit === 2 ? 'طلایی' : 'ریالی'))
const unitLabel = computed(() => (props.item.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'))
const unitTagClass = computed(() =>
props.item.unit === 2
? 'border-amber-300 bg-amber-50 text-amber-700'
@@ -59,7 +59,7 @@
import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { cols } from '../hooks/orderColumns'
import { getGiftCardColumns } from '../hooks/orderColumns'
import { usePrefix } from '@core/composable/usePrefix'
import { getGiftCardTable } from '@/api/giftCard'
import GiftCardMobileCardList from './GiftCardMobileCardList.vue'
@@ -79,7 +79,7 @@ const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const { prefixCls } = usePrefix('pophoverstatus')
const tblProps = ref<any>({
columns: cols,
columns: getGiftCardColumns(),
api: (params: TableInput) => {
if (props.customerId) {
params.filters = []
@@ -2,6 +2,7 @@ import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
@@ -21,7 +22,8 @@ const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
}
})*/
export const cols: BaseTableColumn[] = [
export function getGiftCardColumns(): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
@@ -50,7 +52,7 @@ export const cols: BaseTableColumn[] = [
return (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
طلایی
{metalLabel.goldWalletShort()}
</CustomTag>
</div>
)
@@ -236,4 +238,5 @@ export const cols: BaseTableColumn[] = [
fixed: 'right',
filterable: false
}
]
]
}
+2 -1
View File
@@ -106,6 +106,7 @@ import type { Giftcard, Giftcardcode, GiftcardInput } from '@/model/giftCard'
import { createGiftCard } from '@/api/giftCard'
import dayjs from 'dayjs'
import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { isStoreApp } from '@/utils/config'
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import BasicInput from '@/components/BasicInput.vue'
@@ -174,7 +175,7 @@ function info(val: Giftcard) {
},
{
key: 'نوع کارت هدیه',
value: val.unit === 3 ? 'تخفیف' : val.unit === 2 ? 'طلایی' : 'ریالی'
value: val.unit === 3 ? 'تخفیف' : val.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'
},
{
key: 'تعداد کدها',
@@ -2,7 +2,7 @@
<a-form :model="form" ref="formRef" autocomplete="off">
<div class="space-y-3">
<div class="flex flex-col items-center gap-1 rounded-xl bg-gray-50 px-4 py-3 sm:flex-row sm:justify-center sm:gap-4">
<span class="text-sm text-gray-600">قیمت {{ form.direction === 1 ? 'خرید' : 'فروش' }} طلا:</span>
<span class="text-sm text-gray-600">{{ metalLabel.goldTradeBuySell(form.direction) }}</span>
<span class="text-sm font-700 text-gray-900">{{ marketPriceLabel }}</span>
</div>
@@ -48,7 +48,7 @@
@change="$emit('changeUser', $event)"
/>
</a-form-item>
<BasicInfoItem is-line title="کیف پول طلا" :value="selectedCustomer?.wallet?.[0]?.gold ?? '_'" />
<BasicInfoItem is-line :title="metalLabel.goldWalletTitle()" :value="selectedCustomer?.wallet?.[0]?.gold ?? '_'" />
<BasicInfoItem is-line title="کیف پول ریالی" :value="selectedCustomer?.wallet?.[0]?.cash ?? '_'" />
</div>
@@ -71,7 +71,7 @@
addon-before="گرم"
addon-bg-color="#0A1B37"
addon-text-color="white"
place-holder="مقدار طلا"
:place-holder="metalLabel.goldPlaceholder()"
type="tel"
@onchange="$emit('calculateCash', $event)"
/>
@@ -88,6 +88,7 @@ import type { Order } from '@/model/order'
import type { last } from '@/model/price'
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{
form: Order
@@ -26,23 +26,18 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template>
</MobileStatCard>
<MobileStatCard bordered variant="warning" label="مقدار (گرم)" :value="`${row.amount ?? 0} گرم`">
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
</template>
</MobileStatCard>
<MobileStatCard bordered variant="warning" label="مقدار" :value="amountLabel(row)" />
<MobileStatCard bordered label="قیمت زمان معامله (ریال)" :value="usePrice(row.price ?? 0, true, '')" />
</div>
<div class="space-y-2">
<MobileStatCard bordered label="قیمت هر گرم طلا برای مشتری (ریال)" :value="usePrice(row.price ?? 0, true, '')" />
<MobileStatCard bordered label="قیمت هر گرم طلا بدون سود (ریال)" :value="usePrice(row.price_original ?? 0, true, '')" />
<!-- <MobileStatCard bordered label="قیمت کل لیمان (ریال)" :value="usePrice(row.total_price_liman ?? 0, true, '')" />-->
<MobileStatCard bordered label="کارمزد" :value="serviceChargeLabel(row)" />
<MobileStatCard bordered label="قیمت کل بدون سود (ریال)" :value="usePrice(row.total_price_original ?? 0, true, '')" />
<MobileStatCard bordered label="سود هر گرم (ریال)" :value="usePrice(row.fee ?? 0, true, '')" />
<MobileStatCard bordered label="کارمزد طلایی" :value="serviceChargeLabel(row)" />
<MobileStatCard bordered label="سود واحد (ریال)" :value="usePrice(row.fee ?? 0, true, '')" />
</div>
<MobileInfoGrid>
<MobileDetailRow label="نوع فلز" :value="row.order_metal_label ?? '-'" />
<MobileDetailRow label="کد سفارش" :value="row.id ?? '-'" />
<MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile || '-'" />
<MobileDetailRow label="سود کل (ریال)" :value="usePrice(row.total_fee ?? 0, true, '')" />
@@ -66,6 +61,7 @@ import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit'
type OrderRow = Order & { sts_str?: string; service_charge?: number | string }
@@ -113,10 +109,14 @@ function createdAtLabel(row: OrderRow) {
return row.created_at ? toJalali(row.created_at) : '-'
}
function amountLabel(row: OrderRow) {
return formatAmountWithUnit(row.amount_display ?? row.amount, row.amount_unit)
}
function serviceChargeLabel(row: OrderRow) {
if (row.service_charge == null || row.service_charge === '') return '-'
const value = Number(row.service_charge)
if (Number.isNaN(value)) return `${row.service_charge} گرم`
return `${parseFloat(value.toFixed(5))} گرم`
if (Number.isNaN(value)) return String(row.service_charge)
return formatAmountWithUnit(value, row.amount_unit)
}
</script>
@@ -6,6 +6,12 @@
<span class="shrink-0 rounded-md border px-1.5 py-0.5 text-xs font-600" :class="directionClass">
{{ directionLabel }}
</span>
<span
v-if="item.order_metal_label"
class="shrink-0 rounded-md border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-xs font-600 text-amber-700"
>
{{ item.order_metal_label }}
</span>
<span class="truncate text-sm font-700 text-gray-900">{{ customerName }}</span>
</div>
<span class="shrink-0 text-xs font-600" :class="statusValueClass">{{ statusLabel }}</span>
@@ -13,7 +19,7 @@
<div class="flex items-center justify-between gap-2 text-xs text-gray-600">
<span>
<span class="text-gray-500">طلا:</span>
<span class="text-gray-500">مقدار:</span>
<span class="font-600 text-gray-900">{{ amountLabel }}</span>
</span>
<span class="font-700 text-gray-900">{{ totalPriceLabel }}</span>
@@ -26,6 +32,7 @@
import type { Order } from '@/model/order'
import MobileCard from '@/components/mobile/MobileCard.vue'
import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit'
const props = defineProps<{
item: Order & { sts_str?: string; service_charge?: number }
@@ -44,9 +51,8 @@ const directionClass = computed(() =>
const customerName = computed(() => props.item.customer?.name || '-')
const amountLabel = computed(() => {
const amount = props.item.amount
if (amount == null) return '-'
return `${Number(amount.toFixed(3))} گرم`
const amount = props.item.amount_display ?? props.item.amount
return formatAmountWithUnit(amount, props.item.amount_unit)
})
const totalPriceLabel = computed(() => usePrice(props.item.total_price ?? 0, true, ''))
const statusLabel = computed(() => props.item.sts_str || '-')
@@ -42,7 +42,7 @@ import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeCustomerActivation, getCustomerTable } from '@/api/customer'
import { cols } from '../hooks/orderColumns'
import { buildOrderColumns } from '../hooks/orderColumns'
import { useUserStore } from '@/store/userStore'
import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue'
@@ -74,22 +74,22 @@ function checkPermission(temp: string) {
const table = ref(null)
const tblProps = ref<any>({
columns: cols,
api: (params: TableInput) => {
columns: buildOrderColumns(),
api: async (params: TableInput) => {
if (props.customerId) {
params.filters = []
params.filters?.push({
field: "customer_id",
type: "text",
field: 'customer_id',
type: 'text',
value: props.customerId,
cr: "and",
op: "eq",
cr: 'and',
op: 'eq',
hidden: false
})
}
return getOrderTable(params)
const data = await getOrderTable(params)
return data
}
//bordered: true
})
const loading = ref<boolean>(false)
+168 -229
View File
@@ -1,239 +1,178 @@
import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import { toJalali } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
import { formatAmountWithUnit } from '@/utils/weightUnit'
//define store
//const store = useStaticApiListsStore()
//document types
/*let docTypes: DocItem[]*/
/*onMounted(async () => {
docTypes = await store.readyDocTypeList()
})
const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
return {
label: x.title,
value: x.id
}
})*/
export type OrderMetalColumn = { metal_id: number; label: string; display_unit?: string }
export const cols: BaseTableColumn[] = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
customRender: (record: any) => {
return record.index + 1
}
},
{
title: 'کد سفارش',
dataIndex: 'id',
key: 'id',
type: 'Number'
// className: 'column-money'
},
{
title: 'نوع سفارش',
dataIndex: 'direction',
key: 'direction',
filterable: false,
customRender: ({ record }) => {
switch (record.direction) {
case 2:
return (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
فروش
</CustomTag>
</div>
)
case 1:
return (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
خرید
</CustomTag>
</div>
)
export function buildOrderColumns(_metals: OrderMetalColumn[] = []): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
customRender: (record: any) => {
return record.index + 1
}
}
},
{
title: 'نام و نام خانوادگی',
dataIndex: 'customer.name',
key: 'customer.name',
type: 'Text'
},
{
title: 'شماره موبایل',
dataIndex: 'customer.mobile',
key: 'customer.mobile',
type: 'Number'
},
{
title: 'مقدار (گرم)',
dataIndex: 'amount',
key: 'amount',
type: 'Text'
},
/*{
title: 'کارمزد ریالی',
dataIndex: 'service_charge_price',
key: 'service_charge_price',
type: 'Number',
customRender(data) {
return usePrice(data.record?.service_charge_price ?? 0, true, '')
}
// className: 'column-money'
},*/
{
title: 'کارمزد طلایی',
dataIndex: 'service_charge',
key: 'service_charge',
type: 'Number',
customRender(data) {
return data.record?.service_charge
}
// className: 'column-money'
},
{
title: 'قیمت هر گرم طلا برای مشتری (ریال)',
dataIndex: 'price',
key: 'price',
type: 'Text',
customRender(data) {
return usePrice(data.record?.price ?? 0, true, '')
}
},
/* {
title: 'قیمت هر گرم طلا در لیمان',
dataIndex: 'limanPrice',
key: 'limanPrice',
type: 'Text',
customRender(data) {
return usePrice(data.record?.total_price_liman ?? 0, true, '')
}
},*/
{
title: 'قیمت کل برای مشتری (ریال)',
dataIndex: 'total_price',
key: 'total_price',
type: 'Text',
customRender(data) {
return usePrice(data.record?.total_price ?? 0, true, '')
}
},
{
title: 'وضعیت',
dataIndex: 'sts',
key: 'sts',
filterable: false,
customRender: ({ record }) => {
switch (record.sts) {
case 1:
return (
<div class="flex justify-center">
<CustomTag textColor="#ca8a04" bgColor="#fef3c7">
{record.sts_str}
</CustomTag>
</div>
)
case 2:
return (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
{record.sts_str}
</CustomTag>
</div>
)
default:
return (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
{record.sts_str}
</CustomTag>
</div>
)
},
{
title: 'کد سفارش',
dataIndex: 'id',
key: 'id',
type: 'Number'
},
{
title: 'نوع سفارش',
dataIndex: 'direction',
key: 'direction',
filterable: false,
customRender: ({ record }) => {
switch (record.direction) {
case 2:
return (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
فروش
</CustomTag>
</div>
)
case 1:
return (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
خرید
</CustomTag>
</div>
)
}
}
}
},
{
title: 'تاریخ ثبت',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: any) => {
return toJalali(record.record.created_at)
}
},
/*{
title: 'نقش',
dataIndex: 'user_type_record',
key: 'user_type_record',
type: 'Select',
filterable: false,
sortable: false,
filterComponentProps: {
multiple: true
},
filterField: 'user_type_obj.id',
filterComponent: ReadyUserTypeSelect,
/!* customRender: (record: any) => {
return record.record.user_type_record?.value
}*!/
customRender: ({ record }) => {
const items = record.is_admin ? (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ادمین
},
{
title: 'نام و نام خانوادگی',
dataIndex: 'customer.name',
key: 'customer.name',
type: 'Text'
},
{
title: 'شماره موبایل',
dataIndex: 'customer.mobile',
key: 'customer.mobile',
type: 'Number'
},
{
title: 'نوع فلز',
dataIndex: 'order_metal_label',
key: 'order_metal_label',
type: 'Text',
filterable: false,
customRender({ record }) {
return (
<div class="flex justify-center">
<CustomTag textColor="#ca8a04" bgColor="#fef3c7" class={'font-bold px-6'}>
{record.order_metal_label ?? '-'}
</CustomTag>
) : (
record.user_type_obj.map((item: { title: any }, index: any) => {
if (item) {
return (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
{item?.title}
</CustomTag>
)
}
})
)
return <div class="flex justify-center items-center gap-2">{items}</div>
</div>
)
}
},
{
title: 'مقدار',
dataIndex: 'amount',
key: 'amount',
type: 'Text',
customRender({ record }) {
const amount = record.amount_display ?? record.amount
return formatAmountWithUnit(amount, record.amount_unit)
}
},
{
title: 'کارمزد',
dataIndex: 'service_charge',
key: 'service_charge',
type: 'Number',
customRender({ record }) {
const value = record.service_charge
if (value == null || value === '') {
return '-'
}
},
{
title: 'حوزه',
dataIndex: 'section',
key: 'section'
},
{
title: 'شماره موبایل',
dataIndex: 'mobile',
type: 'Number',
key: 'mobile'
},
{
title: 'تاریخ ثبت',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: any) => {
return toJalaliNoTime(record.record.created_at)
return formatAmountWithUnit(Number(value), record.amount_unit)
}
},
{
title: 'قیمت زمان معامله (ریال)',
dataIndex: 'price',
key: 'price',
type: 'Text',
customRender({ record }) {
return usePrice(record.price ?? 0, true, '')
}
},
{
title: 'قیمت کل برای مشتری (ریال)',
dataIndex: 'total_price',
key: 'total_price',
type: 'Text',
customRender(data) {
return usePrice(data.record?.total_price ?? 0, true, '')
}
},
{
title: 'وضعیت',
dataIndex: 'sts',
key: 'sts',
filterable: false,
customRender: ({ record }) => {
switch (record.sts) {
case 1:
return (
<div class="flex justify-center">
<CustomTag textColor="#ca8a04" bgColor="#fef3c7">
{record.sts_str}
</CustomTag>
</div>
)
case 2:
return (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
{record.sts_str}
</CustomTag>
</div>
)
default:
return (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
{record.sts_str}
</CustomTag>
</div>
)
}
},*/
{
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
fixed: 'right',
filterable: false
}
]
}
},
{
title: 'تاریخ ثبت',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: any) => {
return toJalali(record.record.created_at)
}
},
{
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
fixed: 'right',
filterable: false
}
]
}
export function getOrderColumns(): BaseTableColumn[] {
return buildOrderColumns()
}
+15 -6
View File
@@ -80,6 +80,8 @@ import { getLastPrice } from '@/api/price'
import type { last } from '@/model/price'
import { createOrder } from '@/api/order'
import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { formatAmountWithUnit } from '@/utils/weightUnit'
//loading
const loading = ref<boolean>(false)
@@ -182,11 +184,15 @@ function info(val: Order) {
infoParams.itemsTitle = 'اطلاعات کاربری'
infoParams.items = [
{
key: 'مقدار (گرم)',
value: val.amount ?? '-'
key: 'مقدار',
value: formatAmountWithUnit(val.amount_display ?? val.amount, val.amount_unit)
},
{
key: 'قیمت هر گرم طلا برای مشتری (ریال)',
key: 'نوع فلز',
value: val.order_metal_label ?? '-'
},
{
key: 'قیمت زمان معامله (ریال)',
value: usePrice(val.price ?? 0) ?? '-'
},
{
@@ -194,7 +200,7 @@ function info(val: Order) {
value: usePrice(val.total_price ?? 0) ?? '-'
},
{
key: 'قیمت هر گرم طلا بدون سود (ریال)',
key: metalLabel.goldGramPriceOriginal(),
value: usePrice(val.price_original ?? 0) ?? '-'
},
{
@@ -226,8 +232,11 @@ function info(val: Order) {
value: usePrice(val.service_charge_price ?? 0)
},*/
{
key: 'کارمزد طلایی',
value: val.service_charge ?? '-' + ' گرم'
key: 'کارمزد',
value:
val.service_charge != null && val.service_charge !== ''
? formatAmountWithUnit(Number(val.service_charge), val.amount_unit)
: '-'
},
/* {
key: 'تاریخ تولد',
@@ -4,7 +4,7 @@
<MobileSheetHeader :title="sheetTitle" :subtitle="dateTimeLabel(row)" :show-title="isMobile" />
<div class="space-y-2">
<MobileStatCard bordered variant="success" label="قیمت هر گرم طلا برای مشتری (ریال)" :value="usePrice(row.price ?? 0, true, '')">
<MobileStatCard bordered variant="success" :label="metalLabel.goldGramPriceCustomer()" :value="usePrice(row.price ?? 0, true, '')">
<template #prefix>
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template>
@@ -32,6 +32,7 @@ import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is'
import { toJalaliNoTime } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{
visible: boolean
@@ -2,6 +2,7 @@ import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
@@ -21,7 +22,8 @@ const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
}
})*/
export const cols: BaseTableColumn[] = [
export function getPriceColumns(): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
@@ -53,7 +55,7 @@ export const cols: BaseTableColumn[] = [
type: 'Text'
},
{
title: 'قیمت هر گرم طلا برای مشتری (ریال)',
title: metalLabel.goldGramPriceCustomer(),
dataIndex: 'price',
key: 'price',
type: 'Text',
@@ -157,4 +159,5 @@ export const cols: BaseTableColumn[] = [
fixed: 'right',
filterable: false
}*/
]
]
}
@@ -118,9 +118,9 @@
>
<BasicInfoItem
title="نام قلک"
:value="item?.fund ? item?.fund?.title : 'کیف پول طلایی'"
:value="item?.fund ? item?.fund?.title : metalLabel.goldWallet()"
/>
<BasicInfoItem title="مقدار طلا" :value="`${item?.gold} گرم`" />
<BasicInfoItem :title="metalLabel.goldAmount()" :value="`${item?.gold} گرم`" />
</div>
</div>
</div>
@@ -162,6 +162,7 @@ import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import type { Charge } from '@/model/wallet'
import customers from '@/router/routes/customers'
import { chargeWallet } from '@/api/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
//loading
const loading = ref<boolean>(false)
+304 -49
View File
@@ -1,6 +1,6 @@
<template>
<div class="flex h-full min-h-0 flex-col gap-4 p-4">
<div class="flex flex-wrap items-center gap-2">
<div class="category-page flex h-full min-h-0 flex-col gap-4 p-4">
<div class="flex shrink-0 flex-wrap items-center gap-2">
<BaseButton :loading="syncing" @click="handleSync">
همگام‌سازی از محصولات
</BaseButton>
@@ -9,47 +9,58 @@
</span>
</div>
<a-spin :spinning="loading">
<a-table
:columns="columns"
:data-source="items"
:pagination="false"
row-key="id"
size="middle"
class="min-h-0 flex-1"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'active'">
<a-switch
:checked="asBool(record.active)"
checked-children="فعال"
un-checked-children="مخفی"
@change="(checked: boolean) => toggleField(record, 'active', checked)"
/>
<div
ref="tableWrapRef"
class="category-page__table min-h-0 flex-1 overflow-hidden"
:class="{ 'category-page__table--dragging': dragFromId !== null }"
>
<a-spin :spinning="loading" class="category-page__spin">
<a-table
:columns="columns"
:data-source="items"
:pagination="false"
:scroll="tableScrollY ? { y: tableScrollY } : undefined"
row-key="id"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'drag'">
<span
class="category-page__drag-handle"
title="کشیدن برای تغییر ترتیب"
@pointerdown="onHandlePointerDown(record.id, $event)"
>
<BaseIcon icon="vuesax-linear:menu" size="18" />
</span>
</template>
<template v-else-if="column.key === 'active'">
<a-switch
:checked="asBool(record.active)"
checked-children="فعال"
un-checked-children="مخفی"
@change="(checked: boolean) => toggleField(record, 'active', checked)"
/>
</template>
<template v-else-if="column.key === 'show_in_select'">
<a-switch
:checked="asBool(record.show_in_select)"
@change="(checked: boolean) => toggleField(record, 'show_in_select', checked)"
/>
</template>
<template v-else-if="column.key === 'sort_order'">
<span class="text-gray-400">{{ record.sort_order }}</span>
</template>
</template>
<template v-else-if="column.key === 'show_in_select'">
<a-switch
:checked="asBool(record.show_in_select)"
@change="(checked: boolean) => toggleField(record, 'show_in_select', checked)"
/>
</template>
<template v-else-if="column.key === 'sort_order'">
<BasicInput
class="!w-16"
type="number"
v-model:value="record.sort_order"
@onchange="() => scheduleSortOrder(record)"
/>
</template>
</template>
</a-table>
</a-spin>
</a-table>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import type { TableColumnType } from 'ant-design-vue'
import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'lodash'
import { onMounted, onUnmounted, ref } from 'vue'
import { getCategories, syncCategoriesFromProducts, updateCategory } from '@/api/category'
@@ -57,7 +68,17 @@ import type { Category } from '@/model/category'
const loading = ref(false)
const syncing = ref(false)
const savingOrder = ref(false)
const items = ref<Category[]>([])
const tableWrapRef = ref<HTMLElement | null>(null)
const tableScrollY = ref<number>()
const dragFromId = ref<number | null>(null)
let highlightedRow: HTMLTableRowElement | null = null
let draggingRow: HTMLTableRowElement | null = null
let pointerDragActive = false
let dragOverId: number | null = null
let activePointerId: number | null = null
let saveOrderGeneration = 0
type CategoryField = 'active' | 'show_in_select' | 'sort_order'
@@ -66,32 +87,216 @@ const debouncedFlush = new Map<number, ReturnType<typeof debounce>>()
let syncInFlight: Promise<void> | null = null
const columns: TableColumnType<Category>[] = [
{
title: '',
key: 'drag',
width: 44,
align: 'center',
},
{ title: 'نام دسته', dataIndex: 'name', key: 'name' },
{
title: 'نمایش محصولات در اپ',
key: 'active',
width: 160,
align: 'center'
align: 'center',
},
{
title: 'سلکت افزودن محصول',
key: 'show_in_select',
width: 150,
align: 'center'
align: 'center',
},
{
title: 'ترتیب',
key: 'sort_order',
width: 90,
align: 'center'
}
width: 72,
align: 'center',
},
]
function updateTableScrollY() {
const height = tableWrapRef.value?.clientHeight ?? 0
tableScrollY.value = height > 120 ? height - 56 : undefined
}
useResizeObserver(tableWrapRef, () => {
updateTableScrollY()
})
/** پیش‌فرض: روشن — فقط صفر یا false یعنی خاموش */
function asBool(value: boolean | number | undefined | null) {
return value !== false && value !== 0
}
function getTableBodyEl(): HTMLElement | null {
return tableWrapRef.value?.querySelector('.ant-table-body') as HTMLElement | null
}
function parseRowId(row: HTMLTableRowElement): number | null {
const key = row.getAttribute('data-row-key')
if (!key) return null
const id = Number(key)
return Number.isFinite(id) ? id : null
}
function getRowFromPoint(clientX: number, clientY: number): HTMLTableRowElement | null {
const wrap = tableWrapRef.value
if (!wrap) return null
if (typeof document.elementsFromPoint === 'function') {
for (const element of document.elementsFromPoint(clientX, clientY)) {
if (!(element instanceof Element) || !wrap.contains(element)) continue
const row = element.closest('tr.ant-table-row')
if (row instanceof HTMLTableRowElement) return row
}
}
const rows = wrap.querySelectorAll('tr.ant-table-row')
for (const row of rows) {
const rect = row.getBoundingClientRect()
if (
clientY >= rect.top &&
clientY <= rect.bottom &&
clientX >= rect.left &&
clientX <= rect.right
) {
return row as HTMLTableRowElement
}
}
return null
}
function setDragHighlight(row: HTMLTableRowElement | null) {
if (highlightedRow && highlightedRow !== row) {
highlightedRow.classList.remove('category-page__row--over')
}
highlightedRow = row
row?.classList.add('category-page__row--over')
}
function autoScrollAtPointer(clientY: number) {
const body = getTableBodyEl()
if (!body) return
const rect = body.getBoundingClientRect()
const edge = 56
if (clientY < rect.top + edge) {
body.scrollTop -= Math.max(8, (rect.top + edge - clientY) / 3)
} else if (clientY > rect.bottom - edge) {
body.scrollTop += Math.max(8, (clientY - (rect.bottom - edge)) / 3)
}
}
function onHandlePointerDown(id: number, event: PointerEvent) {
if (event.button !== 0) return
event.preventDefault()
event.stopPropagation()
const handle = event.currentTarget as HTMLElement
handle.setPointerCapture(event.pointerId)
activePointerId = event.pointerId
pointerDragActive = true
dragFromId.value = id
dragOverId = id
draggingRow = handle.closest('tr.ant-table-row')
draggingRow?.classList.add('category-page__row--dragging')
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', onPointerUp)
window.addEventListener('pointercancel', onPointerUp)
}
function onPointerMove(event: PointerEvent) {
if (!pointerDragActive || activePointerId !== event.pointerId) return
event.preventDefault()
autoScrollAtPointer(event.clientY)
const row = getRowFromPoint(event.clientX, event.clientY)
setDragHighlight(row)
dragOverId = row ? parseRowId(row) : null
}
function onPointerUp(event: PointerEvent) {
if (!pointerDragActive || activePointerId !== event.pointerId) return
const fromId = dragFromId.value
const toId =
dragOverId ?? (() => {
const row = getRowFromPoint(event.clientX, event.clientY)
return row ? parseRowId(row) : null
})()
if (fromId !== null && toId !== null && fromId !== toId) {
const fromIndex = items.value.findIndex((item) => item.id === fromId)
const toIndex = items.value.findIndex((item) => item.id === toId)
if (fromIndex >= 0 && toIndex >= 0) {
void reorderByDrag(fromIndex, toIndex)
}
}
cleanupPointerDrag()
}
function cleanupPointerDrag() {
pointerDragActive = false
activePointerId = null
dragOverId = null
dragFromId.value = null
draggingRow?.classList.remove('category-page__row--dragging')
draggingRow = null
setDragHighlight(null)
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
window.removeEventListener('pointercancel', onPointerUp)
}
function applySortOrders(list: Category[]) {
list.forEach((item, index) => {
item.sort_order = index + 1
})
}
async function reorderByDrag(fromIndex: number, toIndex: number) {
if (fromIndex === toIndex) return
const list = [...items.value]
const [moved] = list.splice(fromIndex, 1)
list.splice(toIndex, 0, moved)
const previousOrders = new Map(items.value.map((item) => [item.id, item.sort_order]))
applySortOrders(list)
items.value = list
const updates = list.filter((item) => previousOrders.get(item.id) !== item.sort_order)
if (!updates.length) return
const generation = ++saveOrderGeneration
savingOrder.value = true
try {
await Promise.all(
updates.map((item) => updateCategory(item.id, { sort_order: item.sort_order }))
)
if (generation !== saveOrderGeneration) return
message.success('ترتیب ذخیره شد')
} catch (e) {
if (generation !== saveOrderGeneration) return
console.error(e)
message.error('خطا در ذخیره ترتیب')
await fetchList()
} finally {
if (generation === saveOrderGeneration) {
savingOrder.value = false
}
}
}
async function fetchList() {
loading.value = true
try {
@@ -101,6 +306,7 @@ async function fetchList() {
message.error('خطا در دریافت دسته‌بندی‌ها')
} finally {
loading.value = false
updateTableScrollY()
}
}
@@ -129,7 +335,7 @@ async function flushSave(id: number) {
const snapshot = {
active: record.active,
show_in_select: record.show_in_select,
sort_order: record.sort_order
sort_order: record.sort_order,
}
try {
@@ -154,12 +360,6 @@ function toggleField(
scheduleSave(record, { [field]: next })
}
function scheduleSortOrder(record: Category) {
const order = Number(record.sort_order)
if (Number.isNaN(order)) return
scheduleSave(record, { sort_order: order })
}
async function syncList(showMessage = false) {
if (syncInFlight) {
return syncInFlight
@@ -183,6 +383,7 @@ async function syncList(showMessage = false) {
} finally {
syncing.value = false
syncInFlight = null
updateTableScrollY()
}
})()
@@ -194,12 +395,66 @@ async function handleSync() {
}
onMounted(() => {
void syncList()
void fetchList()
updateTableScrollY()
})
onUnmounted(() => {
cleanupPointerDrag()
debouncedFlush.forEach((fn) => fn.cancel())
debouncedFlush.clear()
pendingPatches.clear()
})
</script>
<style scoped lang="less">
.category-page__spin {
display: block;
height: 100%;
:deep(.ant-spin-container) {
height: 100%;
}
}
.category-page__drag-handle {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: grab;
color: #9ca3af;
padding: 4px;
border-radius: 6px;
user-select: none;
touch-action: none;
&:hover {
color: #6b7280;
background: rgba(148, 163, 184, 0.16);
}
&:active {
cursor: grabbing;
}
}
.category-page__table--dragging {
user-select: none;
:deep(.ant-table-tbody > tr > td) {
pointer-events: none;
}
:deep(.category-page__drag-handle) {
pointer-events: auto;
}
}
:deep(.category-page__row--dragging > td) {
opacity: 0.45;
}
:deep(.category-page__row--over > td) {
background: rgba(59, 130, 246, 0.08) !important;
}
</style>
@@ -20,7 +20,11 @@
/>
</MobileCardList>
<TransactionMobileDetailSheet v-model:visible="sheetVisible" :item="selectedItem" />
<TransactionMobileDetailSheet
v-model:visible="sheetVisible"
:item="selectedItem"
:metals="catalogMetals"
/>
</template>
<script setup lang="ts">
@@ -32,6 +36,9 @@ import { useMobileTableList } from '@/components/mobile/composables/useMobileTab
import type { TableInput } from '@core/type/baseTable'
import TransactionMobileDetailSheet from './TransactionMobileDetailSheet.vue'
import TransactionMobileListItem from './TransactionMobileListItem.vue'
import type { TransactionMetalColumn } from '../hooks/transactionsColumns'
const catalogMetals = ref<TransactionMetalColumn[]>([])
const props = defineProps<{
customerId?: number
@@ -51,7 +58,10 @@ function fetchTransactions(params: TableInput) {
hidden: false
})
}
return getTransactionTable(params)
return getTransactionTable(params).then((data) => {
catalogMetals.value = data?.catalogMetals ?? []
return data
})
}
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<Transaction>({
@@ -3,21 +3,29 @@
<template #default="{ item: row }">
<MobileSheetHeader :title="sheetTitle" :subtitle="dateLabel(row)" :show-title="isMobile" />
<div class="flex items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-lg border px-2 py-0.5 text-xs font-600" :class="typeTagClass(row)">
{{ typeLabel(row) }}
</span>
<span
v-if="!isMoneyType(row) && row.transaction_metal_label"
class="rounded-lg border border-amber-300 bg-amber-50 px-2 py-0.5 text-xs font-600 text-amber-700"
>
{{ row.transaction_metal_label }}
</span>
</div>
<div class="space-y-2">
<MobileStatCard
v-for="entry in metalEntries(row)"
:key="entry.metal_id"
bordered
:variant="(row.gold ?? 0) > 0 ? 'success' : 'danger'"
label="مقدار طلا (گرم)"
:value="goldLabel(row)"
:variant="entry.value > 0 ? 'success' : 'danger'"
:label="transactionMetalAmountTitle(entry.metal_id, entry.label)"
:value="entry.display"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" :color="(row.gold ?? 0) > 0 ? '#15803d' : '#dc2626'" />
<BaseIcon icon="vuesax-linear:coin" size="18" :color="entry.value > 0 ? '#15803d' : '#dc2626'" />
</template>
</MobileStatCard>
<MobileStatCard
@@ -33,6 +41,7 @@
</div>
<MobileInfoGrid>
<MobileDetailRow label="نوع فلز" :value="isMoneyType(row) ? '-' : (row.transaction_metal_label ?? '-')" />
<MobileDetailRow label="کد تراکنش" :value="row.id ?? '-'" />
<MobileDetailRow label="نام تراکنش" :value="row.type_str || '-'" />
<MobileDetailRow label="علت تراکنش" full-width :value="noteLabel(row)" />
@@ -53,10 +62,14 @@ import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is'
import { toJalaliNoTime } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel, transactionMetalAmountTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import type { TransactionMetalColumn } from '../hooks/transactionsColumns'
const props = defineProps<{
visible: boolean
item: Transaction | null
metals?: TransactionMetalColumn[]
}>()
const emit = defineEmits<{
@@ -75,7 +88,7 @@ function isMoneyType(row: Transaction) {
}
function typeLabel(row: Transaction) {
return isMoneyType(row) ? 'پول' : 'طلا'
return isMoneyType(row) ? 'پول' : 'فلز'
}
function typeTagClass(row: Transaction) {
@@ -88,14 +101,26 @@ function dateLabel(row: Transaction) {
return row.created_at ? toJalaliNoTime(row.created_at) : '-'
}
function goldLabel(row: Transaction) {
const gold = row.gold
if (gold == null) return '-'
return `${Number(Number(gold).toFixed(3))} گرم`
}
function noteLabel(row: Transaction) {
if (row.note == null) return '-'
return String(row.note)
}
function metalEntries(row: Transaction) {
const metals = props.metals?.length
? props.metals
: [{ metal_id: row.transaction_metal_id ?? 1, label: row.transaction_metal_label ?? 'طلا' }]
return metals
.map((metal) => {
const value = metalCellValue(row as Record<string, unknown>, metal.metal_id)
return {
metal_id: metal.metal_id,
label: metal.label,
value,
display: value ? formatMetalDisplayValue(value, metal.metal_id) : '-'
}
})
.filter((entry) => entry.value !== 0)
}
</script>
@@ -6,6 +6,12 @@
<span class="shrink-0 rounded-md border px-1.5 py-0.5 text-xs font-600" :class="typeTagClass">
{{ typeLabel }}
</span>
<span
v-if="!isMoneyType && item.transaction_metal_label"
class="shrink-0 rounded-md border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-xs font-600 text-amber-700"
>
{{ item.transaction_metal_label }}
</span>
<span class="truncate text-sm font-700 text-gray-900">{{ item.type_str || '-' }}</span>
</div>
<span class="shrink-0 text-xs text-gray-400">{{ dateLabel }}</span>
@@ -31,7 +37,7 @@ defineEmits<{
}>()
const isMoneyType = computed(() => props.item.type === 4 || props.item.type === 3)
const typeLabel = computed(() => (isMoneyType.value ? 'پول' : 'طلا'))
const typeLabel = computed(() => (isMoneyType.value ? 'پول' : 'فلز'))
const typeTagClass = computed(() =>
isMoneyType.value
? 'border-green-300 bg-green-50 text-green-700'
@@ -67,7 +67,7 @@ import type {TableInput} from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import {changeCustomerActivation, getCustomerTable} from '@/api/customer'
import {cols} from '../hooks/transactionsColumns'
import { buildTransactionsColumns } from '../hooks/transactionsColumns'
import {useUserStore} from '@/store/userStore'
import {usePrefix} from '@core/composable/usePrefix'
import {message} from 'ant-design-vue'
@@ -102,23 +102,24 @@ const {prefixCls} = usePrefix('pophoverstatus')
const table = ref(null)
const tblProps = ref<any>({
columns: cols,
api: (params: TableInput) => {
// console.log(props.customerId)
columns: buildTransactionsColumns(),
api: async (params: TableInput) => {
if (props.customerId) {
params.filters = []
params.filters?.push({
field: "customer_id",
type: "text",
field: 'customer_id',
type: 'text',
value: props.customerId,
cr: "and",
op: "eq",
cr: 'and',
op: 'eq',
hidden: false
})
}
return getTransactionTable(params)
const data = await getTransactionTable(params)
const metals = data?.catalogMetals ?? []
tblProps.value.columns = buildTransactionsColumns(metals)
return data
}
//bordered: true
})
const loading = ref<boolean>(false)
/*async function changeCustomerActivationFunction(id: number) {
@@ -1,203 +1,165 @@
import type {BaseTableColumn} from '@core/type/baseTable'
import {toJalaliNoTime, toTime} from '@core/utils/useDataTime'
import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalaliNoTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import {usePrice} from '@core/utils/usePrice'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
import { usePrice } from '@core/utils/usePrice'
import { metalLabel, transactionMetalAmountTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValuePlain, metalCellValue } from '@/utils/metalWeight'
//define store
//const store = useStaticApiListsStore()
//document types
/*let docTypes: DocItem[]*/
/*onMounted(async () => {
docTypes = await store.readyDocTypeList()
})
const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
return {
label: x.title,
value: x.id
export type TransactionMetalColumn = { metal_id: number; label: string; display_unit?: string }
function renderSignedMetalCell(value: number, metalId: number) {
if (!value) {
return (
<div class="flex justify-center">
<div class="px-6">0</div>
</div>
)
}
})*/
export const cols: BaseTableColumn[] = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
customRender: (record: any) => {
return record.index + 1
}
},
{
title: 'کد تراکنش',
dataIndex: 'id',
key: 'id',
type: 'Number'
// className: 'column-money'
},
{
title: 'نام تراکنش',
dataIndex: 'type_str',
key: 'type_str',
type: 'Text'
// className: 'column-money'
},
{
title: 'علت تراکنش',
dataIndex: 'note',
key: 'note',
type: 'Text',
const cls = value > 0 ? 'text-green' : 'text-red'
const formatted = formatMetalDisplayValuePlain(Math.abs(value), metalId)
return (
<div class="flex justify-center">
<div class={`font-bold px-6 ${cls}`}>{value > 0 ? formatted : `-${formatted}`}</div>
</div>
)
}
// className: 'column-money'
function isMoneyTransaction(record: { type?: number }) {
return record.type === 4 || record.type === 3
}
export function buildTransactionsColumns(metals: TransactionMetalColumn[] = []): BaseTableColumn[] {
const baseColumns: BaseTableColumn[] = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
customRender: (record: any) => {
return record.index + 1
}
},
{
title: 'نوع تراکنش',
dataIndex: 'active',
key: 'active',
filterable: false,
sortable: false,
customRender: ({record}) => {
return record.type === 4 || record.type === 3 ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
پول
</CustomTag>
</div>
) : (
<div class="flex justify-center">
{/*<CustomTag textColor="rgb(255 120 0)" bgColor="rgb(255 221 0)">*/}
<CustomTag textColor="#ca8a04" bgColor="#fef3c7" class={'font-bold px-6'}>
طلا
</CustomTag>
</div>
)
}
},
{
title: 'نام مشتری',
dataIndex: 'customer.name',
key: 'customer.name',
type: 'Text'
},
{
title: 'مقدار طلا طی تراکنش (گرم)',
dataIndex: 'gold',
key: 'gold',
type: 'Number',
customRender: ({record}) => {
return record.gold > 0 ? (
<div class="flex justify-center">
<div class={'font-bold px-6 text-green'}>{record.gold}</div>
</div>
) : (
<div class="flex justify-center">
{/*<CustomTag textColor="rgb(255 120 0)" bgColor="rgb(255 221 0)">*/}
<div class={'font-bold px-6 text-red'}>{record.gold}</div>
</div>
)
}
},
{
title: 'مقدار پول طی تراکنش (ریال)',
dataIndex: 'cash',
key: 'cash',
type: 'Number',
customRender: ({record}) => {
return record.cash > 0 ? (
<div class="flex justify-center">
<div class={'font-bold px-6 text-green'}>{usePrice(record?.cash ?? 0, true, '')}</div>
</div>
) : (
<div class="flex justify-center">
{/*<CustomTag textColor="rgb(255 120 0)" bgColor="rgb(255 221 0)">*/}
<div class={'font-bold px-6 text-red'}>{usePrice(record?.cash ?? 0, true, '')}</div>
</div>
)
}
},
/* {
title: 'شماره سفارش',
dataIndex: 'order_id',
key: 'order_id',
type: 'Text'
},*/
/* {
title: 'شماره موبایل',
dataIndex: 'mobile',
key: 'mobile',
title: 'کد تراکنش',
dataIndex: 'id',
key: 'id',
type: 'Number'
},*/
/* {
title: 'نام فروشگاه',
dataIndex: 'shop_name',
key: 'shop_name',
type: 'Text'
},*/
},
{
title: 'تاریخ تراکنش',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: any) => {
return toJalaliNoTime(record.record.created_at)
}
}
/*{
title: 'نقش',
dataIndex: 'user_type_record',
key: 'user_type_record',
type: 'Select',
filterable: false,
sortable: false,
filterComponentProps: {
multiple: true
},
filterField: 'user_type_obj.id',
filterComponent: ReadyUserTypeSelect,
/!* customRender: (record: any) => {
return record.record.user_type_record?.value
}*!/
customRender: ({ record }) => {
const items = record.is_admin ? (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ادمین
title: 'نام تراکنش',
dataIndex: 'type_str',
key: 'type_str',
type: 'Text'
},
{
title: 'علت تراکنش',
dataIndex: 'note',
key: 'note',
type: 'Text'
},
{
title: 'نوع تراکنش',
dataIndex: 'active',
key: 'active',
filterable: false,
sortable: false,
customRender: ({ record }) => {
return isMoneyTransaction(record) ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
پول
</CustomTag>
) : (
record.user_type_obj.map((item: { title: any }, index: any) => {
if (item) {
return (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
{item?.title}
</CustomTag>
)
}
})
)
return <div class="flex justify-center items-center gap-2">{items}</div>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#ca8a04" bgColor="#fef3c7" class={'font-bold px-6'}>
فلز
</CustomTag>
</div>
)
}
},
{
title: 'نوع فلز',
dataIndex: 'transaction_metal_label',
key: 'transaction_metal_label',
filterable: false,
sortable: false,
customRender: ({ record }) => {
if (isMoneyTransaction(record)) {
return '-'
}
},
{
title: 'حوزه',
dataIndex: 'section',
key: 'section'
},
{
title: 'شماره موبایل',
dataIndex: 'mobile',
type: 'Number',
key: 'mobile'
},*/
/* {
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
fixed: 'right',
filterable: false
}*/
]
return (
<div class="flex justify-center">
<CustomTag textColor="#ca8a04" bgColor="#fef3c7" class={'font-bold px-6'}>
{record.transaction_metal_label ?? metalLabel.goldUnit()}
</CustomTag>
</div>
)
}
},
{
title: 'نام مشتری',
dataIndex: 'customer.name',
key: 'customer.name',
type: 'Text'
}
]
const metalColumns: BaseTableColumn[] =
metals.length > 0
? metals.map((metal) => ({
title: transactionMetalAmountTitle(metal.metal_id, metal.label),
dataIndex: `metal_gold_${metal.metal_id}`,
key: `metal_${metal.metal_id}`,
type: 'Number',
customRender: ({ record }) => renderSignedMetalCell(metalCellValue(record, metal.metal_id), metal.metal_id)
}))
: [
{
title: metalLabel.goldInTransaction(),
dataIndex: 'gold',
key: 'gold',
type: 'Number',
customRender: ({ record }) => renderSignedMetalCell(Number(record.gold ?? 0), 1)
}
]
return [
...baseColumns,
...metalColumns,
{
title: 'مقدار پول طی تراکنش (ریال)',
dataIndex: 'cash',
key: 'cash',
type: 'Number',
customRender: ({ record }) => {
return record.cash > 0 ? (
<div class="flex justify-center">
<div class={'font-bold px-6 text-green'}>{usePrice(record?.cash ?? 0, true, '')}</div>
</div>
) : (
<div class="flex justify-center">
<div class={'font-bold px-6 text-red'}>{usePrice(record?.cash ?? 0, true, '')}</div>
</div>
)
}
},
{
title: 'تاریخ تراکنش',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: any) => {
return toJalaliNoTime(record.record.created_at)
}
}
]
}
export function getTransactionsColumns(): BaseTableColumn[] {
return buildTransactionsColumns()
}
+2 -1
View File
@@ -93,6 +93,7 @@ import type { Item } from '@/model/Item'
import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import type { Transaction } from '@/model/transaction'
import { metalLabel } from '@/utils/adminMetalLabels'
//loading
const loading = ref<boolean>(false)
@@ -127,7 +128,7 @@ let infoParams = reactive<Info>({})
function info(val: Transaction) {
infoParams.name = val.customer.name
infoParams.type = val.transaction_type === '1' ? 'نقدی' : 'طلا'
infoParams.type = val.transaction_type === '1' ? 'نقدی' : metalLabel.goldUnit()
infoParams.date = toJalaliNoTime(val.created_at)
infoParams.time = toTime(val.created_at)
infoParams.itemsTitle = 'اطلاعات کاربری'
@@ -15,6 +15,7 @@
v-for="item in items"
:key="item.id"
:item="item"
:metals="catalogMetals"
@open="openSheet"
/>
</MobileCardList>
@@ -22,36 +23,39 @@
<WalletMobileDetailSheet
v-model:visible="sheetVisible"
:item="selectedItem"
:metals="catalogMetals"
/>
</template>
<script setup lang="ts">
import { getWalletTable } from '@/api/wallet'
import type { Wallet } from '@/model/wallet'
import type { Wallet, WalletTableSums } from '@/model/wallet'
import MobileCardList from '@/components/mobile/MobileCardList.vue'
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
import type { TableInput, TableOutput } from '@core/type/baseTable'
import type { WalletMetalColumn } from '../hooks/walletColumns'
import WalletMobileDetailSheet from './WalletMobileDetailSheet.vue'
import WalletMobileListItem from './WalletMobileListItem.vue'
type WalletTableResponse = TableOutput<Wallet> & {
sums?: {
cash?: number
gold?: number
cashToGold?: number
goldToCash?: number
}
sums?: WalletTableSums
}
const emit = defineEmits<{
(e: 'sums', sums: WalletTableResponse['sums']): void
}>()
const catalogMetals = ref<WalletMetalColumn[]>([])
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Wallet>()
async function fetchWallet(params: TableInput) {
const data = (await getWalletTable(params)) as WalletTableResponse
catalogMetals.value =
data?.sums?.metalBalances?.map((metal) => ({
metal_id: metal.metal_id,
label: metal.label
})) ?? []
emit('sums', data?.sums)
return data
}
@@ -9,7 +9,15 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template>
</MobileStatCard>
<MobileStatCard bordered variant="warning" label="موجودی طلایی" :value="goldLabel(row)">
<MobileStatCard
v-for="entry in metalEntries(row)"
:key="entry.metal_id"
bordered
variant="warning"
:label="walletMetalBalanceTitle(entry.metal_id, entry.label)"
:value="entry.display"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
</template>
@@ -34,10 +42,14 @@ import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is'
import { usePrice } from '@core/utils/usePrice'
import { walletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import type { WalletMetalColumn } from '../hooks/walletColumns'
const props = defineProps<{
visible: boolean
item: Wallet | null
metals?: WalletMetalColumn[]
}>()
const emit = defineEmits<{
@@ -51,9 +63,20 @@ const visibleProxy = computed({
const sheetTitle = computed(() => props.item?.customer?.name || `کیف پول #${props.item?.id ?? '-'}`)
function goldLabel(row: Wallet) {
const gold = row.gold
if (gold == null) return '-'
return `${Number(Number(gold).toFixed(3))} گرم`
function metalEntries(row: Wallet) {
const metals = props.metals?.length
? props.metals
: [{ metal_id: 1, label: 'طلا' }]
return metals
.map((metal) => {
const value = metalCellValue(row as Record<string, unknown>, metal.metal_id)
return {
metal_id: metal.metal_id,
label: metal.label,
display: formatMetalDisplayValue(value, metal.metal_id)
}
})
.filter((entry) => metalCellValue(row as Record<string, unknown>, entry.metal_id) !== 0)
}
</script>
@@ -11,9 +11,19 @@
<span class="text-gray-500">ریال:</span>
<span class="font-600 text-gray-900">{{ cashLabel }}</span>
</span>
<span>
<span class="text-gray-500">طلا:</span>
<span class="font-700 text-gray-900">{{ goldLabel }}</span>
<span v-if="metalEntries.length === 1">
<span class="text-gray-500">{{ metalEntries[0].label }}:</span>
<span class="font-700 text-gray-900">{{ metalEntries[0].display }}</span>
</span>
</div>
<div
v-if="metalEntries.length > 1"
class="flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-600"
>
<span v-for="entry in metalEntries" :key="entry.metal_id">
<span class="text-gray-500">{{ entry.label }}:</span>
<span class="font-700 text-gray-900">{{ entry.display }}</span>
</span>
</div>
</div>
@@ -24,9 +34,12 @@
import type { Wallet } from '@/model/wallet'
import MobileCard from '@/components/mobile/MobileCard.vue'
import { usePrice } from '@core/utils/usePrice'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import type { WalletMetalColumn } from '../hooks/walletColumns'
const props = defineProps<{
item: Wallet
metals?: WalletMetalColumn[]
}>()
defineEmits<{
@@ -35,9 +48,25 @@ defineEmits<{
const customerName = computed(() => props.item.customer?.name || '-')
const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, ''))
const goldLabel = computed(() => {
const gold = props.item.gold
if (gold == null) return '-'
return `${Number(Number(gold).toFixed(3))} گرم`
function metalValue(metalId: number): number {
return metalCellValue(props.item as Record<string, unknown>, metalId)
}
const metalEntries = computed(() => {
const metals = props.metals?.length
? props.metals
: [{ metal_id: 1, label: 'طلا' }]
return metals
.map((metal) => {
const value = metalValue(metal.metal_id)
return {
metal_id: metal.metal_id,
label: metal.label,
display: formatMetalDisplayValue(value, metal.metal_id)
}
})
.filter((entry) => metalValue(entry.metal_id) !== 0)
})
</script>
@@ -2,7 +2,7 @@
<div class="h-full min-h-0">
<BasicShowContext title="لیست کیف پول">
<div class="flex h-full min-h-0 flex-col gap-3">
<div class="hidden shrink-0 md:grid md:grid-cols-1 md:gap-3 lg:grid-cols-2">
<div class="hidden shrink-0 md:grid md:grid-cols-1 md:gap-3 lg:grid-cols-2 xl:grid-cols-3">
<div
class="rounded-lg p-2.5 text-sm"
:class="walletSumTone(sums?.cash)"
@@ -23,20 +23,22 @@
</div>
<div
v-for="metal in metalBalanceCards"
:key="metal.metal_id"
class="rounded-lg p-2.5 text-sm"
:class="walletSumTone(sums?.gold)"
:class="walletSumTone(metal.gold)"
>
<div class="mb-2 font-medium">مجموع موجودی کیف طلا</div>
<div class="mb-2 font-medium">{{ totalMetalWalletTitle(metal.metal_id, metal.label) }}</div>
<div class="grid grid-cols-2 gap-2">
<div class="stat-inner rounded-lg px-2 py-1.5 text-center">
<div class="text-xs text-gray-500">گرم</div>
<div class="text-xs text-gray-500">{{ metal.display_unit ?? metalDisplayUnit(metal.metal_id) }}</div>
<div class="mt-1 font-bold">
{{ usePrice(sums?.gold ?? 0, true, 'گرم') }}
{{ formatMetalDisplayValue(metal.gold ?? 0, metal.metal_id) }}
</div>
</div>
<div class="stat-inner rounded-lg px-2 py-1.5 text-center">
<div class="text-xs text-gray-500">ریال</div>
<div class="mt-1 font-bold break-all">{{ usePrice(sums?.goldToCash ?? 0) }}</div>
<div class="mt-1 font-bold break-all">{{ usePrice(metal.goldToCash ?? 0) }}</div>
</div>
</div>
</div>
@@ -77,13 +79,16 @@ import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeCustomerActivation } from '@/api/customer'
import { cols } from '../hooks/walletColumns'
import { buildWalletColumns } from '../hooks/walletColumns'
import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue'
import { getWalletTable } from '@/api/wallet'
import { usePrice } from '../../../../core/utils/usePrice'
import WalletMobileCardList from './WalletMobileCardList.vue'
import { isMobile } from '@core/utils/is'
import { totalMetalWalletTitle } from '@/utils/adminMetalLabels'
import type { WalletTableSums } from '@/model/wallet'
import { formatMetalDisplayValue, metalDisplayUnit } from '@/utils/metalWeight'
const [submit, reload] = useBaseTable()
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
@@ -92,13 +97,38 @@ const emits = defineEmits(['delete', 'edit', 'add', 'info', 'sums'])
const { prefixCls } = usePrefix('pophoverstatus')
const table = ref(null)
const sums = ref<any>()
const sums = ref<WalletTableSums>()
const metalBalanceCards = computed(() => {
if (sums.value?.metalBalances?.length) {
return sums.value.metalBalances
}
if (sums.value?.gold != null || sums.value?.goldToCash != null) {
return [
{
metal_id: 1,
label: 'طلا',
gold: sums.value.gold ?? 0,
goldToCash: sums.value.goldToCash ?? 0
}
]
}
return []
})
const tblProps = ref<any>({
columns: cols,
columns: buildWalletColumns(),
api: async (params: TableInput) => {
const data = await getWalletTable(params)
sums.value = data?.sums
const metals =
data?.sums?.metalBalances?.map((metal) => ({
metal_id: metal.metal_id,
label: metal.label
})) ?? []
tblProps.value.columns = buildWalletColumns(metals)
emits('sums', data?.sums)
return data
}
@@ -1,157 +1,75 @@
import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
import { metalLabel, walletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
//define store
//const store = useStaticApiListsStore()
//document types
/*let docTypes: DocItem[]*/
/*onMounted(async () => {
docTypes = await store.readyDocTypeList()
})
const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
return {
label: x.title,
value: x.id
}
})*/
export type WalletMetalColumn = { metal_id: number; label: string; display_unit?: string }
export const cols: BaseTableColumn[] = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
customRender: (record: any) => {
return record.index + 1
}
},
{
title: 'کد کیف پول',
dataIndex: 'id',
key: 'id',
type: 'Number'
// className: 'column-money'
},
{
title: 'نام مشتری',
dataIndex: 'customer.name',
key: 'customer.name',
type: 'Text'
},
{
title: 'موجودی کیف پول (ریال)',
dataIndex: 'cash',
key: 'cash',
type: 'Text',
customRender(data) {
return usePrice(data.record.cash, true, '')
}
},
{
title: 'موجودی کیف طلا (گرم)',
dataIndex: 'gold',
key: 'gold',
type: 'Text'
}
/* {
title: 'شماره موبایل',
dataIndex: 'mobile',
key: 'mobile',
type: 'Number'
},*/
/* {
title: 'نام فروشگاه',
dataIndex: 'shop_name',
key: 'shop_name',
type: 'Text'
},*/
/* {
title: 'وضعیت',
dataIndex: 'active',
key: 'active',
customRender: ({ record }) => {
return record.active ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
فعال
</CustomTag>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA">
غیر فعال
</CustomTag>
</div>
)
}
},*/
/*{
title: 'نقش',
dataIndex: 'user_type_record',
key: 'user_type_record',
type: 'Select',
export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableColumn[] {
const baseColumns: BaseTableColumn[] = [
{
title: 'ردیف',
dataIndex: 'id',
key: 'id',
filterable: false,
sortable: false,
filterComponentProps: {
multiple: true
},
filterField: 'user_type_obj.id',
filterComponent: ReadyUserTypeSelect,
/!* customRender: (record: any) => {
return record.record.user_type_record?.value
}*!/
customRender: ({ record }) => {
const items = record.is_admin ? (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ادمین
</CustomTag>
) : (
record.user_type_obj.map((item: { title: any }, index: any) => {
if (item) {
return (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
{item?.title}
</CustomTag>
)
}
})
)
return <div class="flex justify-center items-center gap-2">{items}</div>
}
},
{
title: 'حوزه',
dataIndex: 'section',
key: 'section'
},
{
title: 'شماره موبایل',
dataIndex: 'mobile',
type: 'Number',
key: 'mobile'
},
{
title: 'تاریخ ثبت',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: any) => {
return toJalaliNoTime(record.record.created_at)
return record.index + 1
}
},*/
/* {
title: 'عملیات',
dataIndex: 'action',
key: 'action',
slot: 'action',
fixed: 'right',
filterable: false
}*/
]
},
{
title: 'کد کیف پول',
dataIndex: 'id',
key: 'id',
type: 'Number'
},
{
title: 'نام مشتری',
dataIndex: 'customer.name',
key: 'customer.name',
type: 'Text'
},
{
title: 'موجودی کیف پول (ریال)',
dataIndex: 'cash',
key: 'cash',
type: 'Text',
customRender(data) {
return usePrice(data.record.cash, true, '')
}
}
]
if (metals.length === 0) {
return [
...baseColumns,
{
title: metalLabel.goldWalletBalanceGram(),
dataIndex: 'gold',
key: 'gold',
type: 'Text',
customRender(data) {
const value = Number(data.record?.gold ?? 0)
return formatMetalDisplayValue(value, 1)
}
}
]
}
const metalColumns: BaseTableColumn[] = metals.map((metal) => ({
title: walletMetalBalanceTitle(metal.metal_id, metal.label),
dataIndex: `metal_gold_${metal.metal_id}`,
key: `metal_${metal.metal_id}`,
type: 'Text',
customRender(data) {
const value = metalCellValue(data.record, metal.metal_id)
return formatMetalDisplayValue(value, metal.metal_id)
}
}))
return [...baseColumns, ...metalColumns]
}
export function getWalletColumns(): BaseTableColumn[] {
return buildWalletColumns()
}
+56 -35
View File
@@ -15,57 +15,45 @@
</div>
</div>
<div class="gap-5 text-xs">
<div>مجموع موجودی کیف طلا:</div>
<div>{{ metalLabel.totalGoldWallet() }}:</div>
<div class="font-bold">{{ usePrice(sums?.gold ?? 0, true, 'گرم') }}</div>
</div>
<div class="gap-5 text-xs">
<div>مجموع موجودی کیف طلا:</div>
<div>{{ metalLabel.totalGoldWallet() }}:</div>
<div class="font-bold">{{ usePrice(sums?.goldToCash ?? 0) }}</div>
</div>
</div>-->
<div class="grid grid-cols-3 gap-1 text-xs">
<div class="bg-blue-600 text-white rounded p-1">مجموع</div>
<div class="bg-blue-600 text-white rounded p-1">ریال</div>
<div class="bg-blue-600 text-white rounded p-1">گرم</div>
<div class="bg-blue-600 text-white rounded p-1">کیف طلا</div>
<div
class="text-white rounded p-1"
:class="
sums?.goldToCash >= 0
? sums?.goldToCash === 0
? 'bg-amber-600'
: 'bg-green-600'
: 'bg-red-600'
"
>
{{ usePrice(sums?.goldToCash ?? 0, true, '') }}
</div>
<div
class="text-white rounded p-1"
:class="
sums?.gold >= 0 ? (sums?.gold === 0 ? 'bg-amber-600' : 'bg-green-600') : 'bg-red-600'
"
>
{{ usePrice(sums?.gold ?? 0, true, '') }}
</div>
<div class="bg-blue-600 text-white rounded p-1">وزن</div>
<template v-for="metal in mobileMetalRows" :key="`metal-${metal.metal_id}`">
<div class="bg-blue-600 text-white rounded p-1">{{ metal.label }}</div>
<div
class="text-white rounded p-1"
:class="sumToneClass(metal.goldToCash)"
>
{{ usePrice(metal.goldToCash ?? 0, true, '') }}
</div>
<div
class="text-white rounded p-1"
:class="sumToneClass(metal.gold)"
>
{{ formatMetalDisplayValue(metal.gold ?? 0, metal.metal_id) }}
</div>
</template>
<div class="bg-blue-600 text-white rounded p-1">کیف پول</div>
<div
class="text-white rounded p-1"
:class="
sums?.cash >= 0 ? (sums?.cash === 0 ? 'bg-amber-600' : 'bg-green-600') : 'bg-red-600'
"
:class="sumToneClass(sums?.cash)"
>
{{ usePrice(sums?.cash ?? 0, true, '') }}
</div>
<div
class="text-white rounded p-1"
:class="
sums?.cashToGold >= 0
? sums?.cashToGold === 0
? 'bg-amber-600'
: 'bg-green-600'
: 'bg-red-600'
"
:class="sumToneClass(sums?.cashToGold)"
>
{{ usePrice(sums?.cashToGold ?? 0, true, '') }}
</div>
@@ -165,6 +153,8 @@ import type { Item } from '@/model/Item'
import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import { usePrice } from '@core/utils/usePrice'
import type { WalletTableSums } from '@/model/wallet'
import { formatMetalDisplayValue } from '@/utils/metalWeight'
//loading
const loading = ref<boolean>(false)
@@ -172,7 +162,38 @@ const loading = ref<boolean>(false)
//define store
const store = useStaticApiListsStore()
const sums = ref()
const sums = ref<WalletTableSums>()
const mobileMetalRows = computed(() => {
if (sums.value?.metalBalances?.length) {
return sums.value.metalBalances.map((metal) => ({
metal_id: metal.metal_id,
label: metal.label,
gold: metal.gold ?? 0,
goldToCash: metal.goldToCash ?? 0
}))
}
if (sums.value?.gold != null || sums.value?.goldToCash != null) {
return [
{
metal_id: 1,
label: 'طلا',
gold: sums.value.gold ?? 0,
goldToCash: sums.value.goldToCash ?? 0
}
]
}
return []
})
function sumToneClass(value?: number) {
if (value == null || value >= 0) {
return value === 0 ? 'bg-amber-600' : 'bg-green-600'
}
return 'bg-red-600'
}
//define tbl
const table = ref(null)
@@ -68,7 +68,7 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#dc2626" />
</template>
</MobileStatCard>
<MobileStatCard bordered variant="warning" label="طلا" :value="goldLabel(row)">
<MobileStatCard bordered variant="warning" :label="metalLabel.goldUnit()" :value="goldLabel(row)">
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
</template>
@@ -107,6 +107,7 @@ import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
import { toNumber } from 'lodash'
import { useRouter } from 'vue-router'
@@ -89,7 +89,7 @@ import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeCustomerActivation, getCustomerTable } from '@/api/customer'
import { cols } from '../hooks/withdrawColumns'
import { getWithdrawColumns } from '../hooks/withdrawColumns'
import { useUserStore } from '@/store/userStore'
import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue'
@@ -133,7 +133,7 @@ const confirmButtonLabel = computed(() =>
)
const tblProps = computed(() => ({
columns: cols,
columns: getWithdrawColumns(),
api: async (params: TableInput) => {
const payload = { ...params, flow_type: props.flowType }
lastTableParams.value = payload
@@ -3,6 +3,7 @@ import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
import WithdrawStatus from "@/pages/menuPages/withdraw/components/WithdrawStatus.vue";
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
@@ -22,7 +23,8 @@ const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
}
})*/
export const cols: BaseTableColumn[] = [
export function getWithdrawColumns(): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
@@ -82,7 +84,7 @@ export const cols: BaseTableColumn[] = [
}
},
{
title: 'طلا',
title: metalLabel.goldUnit(),
dataIndex: 'gold',
key: 'gold',
type: 'Number',
@@ -232,4 +234,5 @@ export const cols: BaseTableColumn[] = [
fixed: 'right',
filterable: false
}*/
]
]
}
+5 -27
View File
@@ -1,35 +1,13 @@
import type { RouteRecordRaw } from 'vue-router'
const Orders: RouteRecordRaw = {
/** ریدایرکت آدرس قدیمی — منو زیر پیکربندی است */
const Accounting: RouteRecordRaw = {
name: 'accounting',
path: '/accounting',
component: () => import('@core/layouts/BasicLayout.vue'),
redirect: { name: 'accountingMain' },
meta: {
icon: 'vuesax-linear:calculator',
permissionsKey: 'accounting',
title: 'حسابداری',
disabled: false,
hideMenu: false,
hideChildren: true,
order: 7
},
children: [
{
name: 'accountingMain',
path: '',
component: () => import('@/pages/menuPages/accounting/index.vue'),
meta: {
permissionsKey: 'project',
icon: 'vuesax-linear:empty-wallet-time',
order: 3,
title: 'حسابداری',
disabled: false,
hideMenu: false
//hideChildren: false
}
}
]
hideMenu: true
}
}
export default Orders
export default Accounting
+11
View File
@@ -136,6 +136,17 @@ const Setting: RouteRecordRaw = {
title: 'مدیران',
order: 9
}
},
{
name: 'accountingMain',
path: 'accounting',
component: () => import('@/pages/menuPages/accounting/index.vue'),
meta: {
permissionsKey: 'accounting',
icon: 'vuesax-linear:calculator',
title: 'حسابداری',
order: 10
}
}
]
}
+131
View File
@@ -0,0 +1,131 @@
import { m } from '@/utils/metalLabels'
import { metalDisplayUnit } from '@/utils/metalWeight'
/** برچسب‌های پرتکرار پنل ادمین که «طلا» در متن دارند */
export const metalLabel = {
goldBalance: () => m('تراز طلایی'),
goldCommission: () => m('کارمزد طلایی'),
goldWallet: () => m('کیف پول طلایی'),
goldWalletShort: () => m('طلایی'),
goldAmount: () => m('مقدار طلا'),
goldInventory: () => m('موجودی طلایی'),
goldGramPriceCustomer: () => m('قیمت هر گرم طلا برای مشتری (ریال)'),
goldGramPriceOriginal: () => m('قیمت هر گرم طلا بدون سود (ریال)'),
goldGramPriceOriginalSlash: () => m('/قیمت هر گرم طلا بدون سود (ریال)'),
customersGold: () => m('طلای مشتریان'),
goldValueInRial: () => m('ارزش طلا به ریال'),
goldWalletCharge: () => m('شارژ کیف پول طلایی'),
goldWalletBalance: () => m('موجودی طلایی (گرم)'),
goldWalletBalanceGram: () => m('موجودی کیف طلا (گرم)'),
goldInTransaction: () => m('مقدار طلا طی تراکنش (گرم)'),
goldWalletTitle: () => m('کیف پول طلا'),
goldPlaceholder: () => m('مقدار طلا'),
goldUnit: () => m('طلا'),
goldChargeShort: () => m('شارژ طلایی'),
manageGoldCash: () => m('مدیریت موجودی ریالی و طلایی'),
goldGifts: () => m('هدایای طلا'),
goldTradeBuySell: (direction: 1 | 2 | number) =>
m(`قیمت ${direction === 1 ? 'خرید' : 'فروش'} طلا:`),
totalGoldWallet: () => m('مجموع موجودی کیف طلا'),
goldWalletTab: () => m('کیف طلا'),
}
export function walletMetalBalanceTitle(metalId: number, label?: string) {
const unit = metalDisplayUnit(metalId)
switch (metalId) {
case 1:
return m(`موجودی کیف طلا (${unit})`)
case 2:
return m(`موجودی کیف نقره (${unit})`)
case 3:
return m(`موجودی کیف مس (${unit})`)
default:
return label ? `موجودی کیف ${label} (${unit})` : metalLabel.goldWalletBalanceGram()
}
}
export function customerWalletMetalBalanceTitle(metalId: number, label?: string) {
const unit = metalDisplayUnit(metalId)
switch (metalId) {
case 1:
return m(`موجودی طلایی (${unit})`)
case 2:
return m(`موجودی نقره (${unit})`)
case 3:
return m(`موجودی مس (${unit})`)
default:
return label ? `موجودی ${label} (${unit})` : metalLabel.goldWalletBalance()
}
}
export function totalMetalWalletTitle(metalId: number, label?: string) {
const unit = metalDisplayUnit(metalId)
switch (metalId) {
case 1:
return m(`مجموع موجودی کیف طلا`)
case 2:
return m(`مجموع موجودی کیف نقره`)
case 3:
return m(`مجموع موجودی کیف مس`)
default:
return label ? `مجموع موجودی کیف ${label}` : metalLabel.totalGoldWallet()
}
}
export function metalBalanceTitle(metalId: number, label?: string) {
switch (metalId) {
case 1:
return metalLabel.goldBalance()
case 2:
return m('تراز نقره')
case 3:
return m('تراز مس')
default:
return label ? `تراز ${label}` : metalLabel.goldBalance()
}
}
export function metalBalanceTitleWithUnit(metalId: number, label?: string) {
const unit = metalDisplayUnit(metalId)
return `${metalBalanceTitle(metalId, label)} (${unit})`
}
export function transactionMetalAmountTitle(metalId: number, label?: string) {
const unit = metalDisplayUnit(metalId)
switch (metalId) {
case 1:
return m(`مقدار طلا طی تراکنش (${unit})`)
case 2:
return m(`مقدار نقره طی تراکنش (${unit})`)
case 3:
return m(`مقدار مس طی تراکنش (${unit})`)
default:
return label ? `مقدار ${label} طی تراکنش (${unit})` : metalLabel.goldInTransaction()
}
}
export function dealPriceAtTransactionTitle(metalId: number, label?: string) {
switch (metalId) {
case 1:
return m('قیمت زمان معامله طلا (ریال)')
case 2:
return m('قیمت زمان معامله نقره (ریال)')
case 3:
return m('قیمت زمان معامله مس (ریال)')
default:
return label ? `قیمت زمان معامله ${label} (ریال)` : 'قیمت زمان معامله (ریال)'
}
}
export function metalCommissionTitle(metalId: number, label?: string) {
switch (metalId) {
case 1:
return metalLabel.goldCommission()
case 2:
return m('کارمزد نقره')
case 3:
return m('کارمزد مس')
default:
return label ? `کارمزد ${label}` : metalLabel.goldCommission()
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ export function mapCategoriesToSelectItems(
}))
}
/** همان منبع جدول دسته‌بندی‌ها — قبل از خواندن، همگام‌سازی می‌کند */
/** همان منبع جدول دسته‌بندی‌ها — دسته‌های جدید از محصولات اضافه می‌شوند؛ ترتیب دستی حفظ می‌شود */
export async function syncAndLoadCategories(options?: {
selectOnly?: boolean
metalHint?: string
+5
View File
@@ -1,3 +1,5 @@
import { setCachedMainMetal } from '@/utils/metalLabels'
export function getConfigApp(key: string) {
const config = window.__APP_CONFIG__ ? window.__APP_CONFIG__.user : {}
@@ -29,11 +31,14 @@ async function bootstrapSettingsFromApi(): Promise<void> {
const list = Array.isArray(rows) ? rows : []
const isStoreRow = list.find((item) => item.key === 'is_store')
const mainWalletRow = list.find((item) => item.key === 'main_wallet_products')
const mainMetalRow = list.find((item) => item.key === 'main_metal')
storeModeFromSettings = isStoreRow ? isSwitchOn(isStoreRow.value) : false
setCachedMainMetal(mainMetalRow?.value)
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
setCachedMainWalletProductIds(parseMainWalletProductIds(mainWalletRow?.value))
} catch {
storeModeFromSettings = false
setCachedMainMetal(null)
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
setCachedMainWalletProductIds([])
} finally {
+36
View File
@@ -0,0 +1,36 @@
import { ref } from 'vue'
export const DEFAULT_MAIN_METAL = 'طلا'
const mainMetalRef = ref(DEFAULT_MAIN_METAL)
export function resolveMainMetal(value?: string | number | null): string {
const normalized = value?.toString().trim()
return normalized || DEFAULT_MAIN_METAL
}
export function setCachedMainMetal(value?: string | number | null): void {
mainMetalRef.value = resolveMainMetal(value)
}
export function getMainMetal(): string {
return mainMetalRef.value
}
export function useMainMetalRef() {
return mainMetalRef
}
/** جایگزینی طلایی/طلای/طلا با نام فلز — بدون صفت مسی/طلایی */
export function applyMainMetalText(text: string, mainMetal = getMainMetal()): string {
if (!text || mainMetal === DEFAULT_MAIN_METAL) return text
return text
.replace(/طلایی/g, mainMetal)
.replace(/طلای/g, mainMetal)
.replace(/طلا/g, mainMetal)
}
export function m(text: string): string {
return applyMainMetalText(text)
}
+68
View File
@@ -0,0 +1,68 @@
import { TRADE_WEIGHT_UNIT_GRAM, TRADE_WEIGHT_UNIT_KILOGRAM } from '@/utils/weightUnit'
export const METAL_ID_GOLD = 1
export const METAL_ID_SILVER = 2
export const METAL_ID_COPPER = 3
/** واحد نمایش موجودی/تراز هر فلز — مس کیلوگرم، بقیه گرم */
export function metalDisplayUnit(metalId: number): string {
return metalId === METAL_ID_COPPER ? TRADE_WEIGHT_UNIT_KILOGRAM : TRADE_WEIGHT_UNIT_GRAM
}
export function isMetalKilogram(metalId: number): boolean {
return metalId === METAL_ID_COPPER
}
/** تبدیل مقدار ذخیره‌شده به گرم (برای محاسبات ریالی) */
export function gramsToMetalDisplay(grams: number, metalId: number): number {
return isMetalKilogram(metalId) ? grams / 1000 : grams
}
/** مقدار نمایشی از گرم */
export function formatMetalWeightFromGrams(
grams: number,
metalId: number,
decimals = 3
): string {
const value = gramsToMetalDisplay(grams, metalId)
return formatMetalDisplayValue(value, metalId, decimals)
}
/** مقدار از قبل در واحد نمایشی فلز */
export function formatMetalDisplayValue(
value: number,
metalId: number,
decimals = 3
): string {
const unit = metalDisplayUnit(metalId)
return `${Number(Number(value).toFixed(decimals))} ${unit}`
}
export function formatMetalDisplayValuePlain(
value: number,
metalId: number,
decimals = 3
): string {
return Number(Number(value).toFixed(decimals)).toLocaleString('fa-IR', {
maximumFractionDigits: decimals
})
}
export function metalCellValue(
record: Record<string, unknown>,
metalId: number,
prefix = 'metal_gold'
): number {
const flatKey = `${prefix}_${metalId}`
if (record[flatKey] != null) {
return Number(record[flatKey])
}
const mapKey = prefix === 'wallet_metal_gold' ? 'wallet_metal_gold' : 'metal_gold'
const map = record[mapKey] as Record<string, number> | undefined
if (!map) {
return 0
}
return Number(map[String(metalId)] ?? 0)
}
+14
View File
@@ -13,3 +13,17 @@ export function resolveTradeWeightUnit(settingValue?: string | number | null) {
export function weightFieldLabelForUnit(unit?: string | null) {
return isKilogramUnit(unit) ? 'وزن به کیلوگرم' : 'وزن به گرم'
}
export function formatAmountWithUnit(
value: number | null | undefined,
unit?: string | null,
decimals = 3
): string {
if (value == null || Number.isNaN(Number(value))) {
return '-'
}
const resolvedUnit = (unit ?? TRADE_WEIGHT_UNIT_GRAM).trim() || TRADE_WEIGHT_UNIT_GRAM
return `${Number(Number(value).toFixed(decimals))} ${resolvedUnit}`
}