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 type { TableInput } from '@core/type/baseTable'
import useAxios from '@core/utils/useAxios' import useAxios from '@core/utils/useAxios'
// import { Customer } from '@/model/customer' // 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) => export const getWalletTable = async (params?: TableInput) =>
await useAxios.post<Array<Wallet>>('/admin/wallet/table', params) 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') => export const chargeWallet = async (params: Charge, type: 'gold' | 'money') =>
type === 'money' type === 'money'
? await useAxios.post<boolean>('/admin/wallet/charge', params) ? await useAxios.post<boolean>('/admin/wallet/charge', params)
+1
View File
@@ -28,6 +28,7 @@ export interface Customer {
wallet: Wallet wallet: Wallet
wallet_cash?: number wallet_cash?: number
wallet_gold?: number wallet_gold?: number
wallet_metal_gold?: Record<string, number>
n_code: string | number n_code: string | number
last_seen_at:string last_seen_at:string
profit_amount: number profit_amount: number
+5
View File
@@ -2,6 +2,11 @@ import type { Customer } from '@/model/customer'
export interface Order { export interface Order {
amount?: number amount?: number
amount_display?: number
amount_unit?: string
order_metal_id?: number
order_metal_label?: string
metal_prices?: Record<string, number>
created_at?: string created_at?: string
customer?: Customer customer?: Customer
customer_id?: number customer_id?: number
+11
View File
@@ -58,6 +58,16 @@ export interface SetActiveTime {
auto_close_at: string 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 { export interface Balance {
cashBalance: number cashBalance: number
cashBalanceOfKalhor: number cashBalanceOfKalhor: number
@@ -65,6 +75,7 @@ export interface Balance {
goldBalance: number goldBalance: number
goldBalanceOfKalhor: number goldBalanceOfKalhor: number
goldBalanceOfLiman: number goldBalanceOfLiman: number
metalBalances?: MetalBalanceItem[]
mazanehBalance: number mazanehBalance: number
mazanehBalanceOfKalhor: number mazanehBalanceOfKalhor: number
income_money: number income_money: number
+3
View File
@@ -14,6 +14,9 @@ export interface Transaction {
hash: string hash: string
id: number id: number
note: number note: number
metal_gold?: Record<string, number>
transaction_metal_id?: number
transaction_metal_label?: string
order_id: number order_id: number
transaction_type: string transaction_type: string
type: number type: number
+33
View File
@@ -1,5 +1,13 @@
import type { User } from '@/model/user' import type { User } from '@/model/user'
export interface WalletMetalBalance {
metal_id: number
label: string
gold: number
goldToCash: number
display_unit?: string
}
export interface Wallet { export interface Wallet {
owner: number owner: number
id: number id: number
@@ -7,9 +15,18 @@ export interface Wallet {
cash: number cash: number
fund_id?: number | null fund_id?: number | null
product_id?: number | null product_id?: number | null
metal_gold?: Record<string, number>
customer: User customer: User
} }
export interface WalletTableSums {
cash?: number
gold?: number
cashToGold?: number
goldToCash?: number
metalBalances?: WalletMetalBalance[]
}
export interface WithdrawTransaction { export interface WithdrawTransaction {
title?: string title?: string
gold?: number gold?: number
@@ -37,6 +54,20 @@ export interface Withdraw {
export type WalletChargeDirection = 'charge' | 'discharge' 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 { export interface Charge {
value?: number value?: number
note?: string note?: string
@@ -44,6 +75,8 @@ export interface Charge {
talabBedehi?: boolean talabBedehi?: boolean
direction?: WalletChargeDirection direction?: WalletChargeDirection
amountAbs?: number | string amountAbs?: number | string
chargeTargetKey?: string | null
product_id?: number | null
} }
export interface WithdrawStatus { export interface WithdrawStatus {
+18 -1
View File
@@ -27,13 +27,27 @@
un-checked-children="مشتری" un-checked-children="مشتری"
/> />
</div> </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="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"> <div class="bg-blue rounded text-white py-1 flex justify-center items-center">
تراز ریالی (ریال) تراز ریالی (ریال)
</div> </div>
<div class="bg-blue rounded text-white py-1 flex justify-center items-center"> <div class="bg-blue rounded text-white py-1 flex justify-center items-center">
تراز طلایی (گرم) {{ m('تراز طلایی (گرم)') }}
</div> </div>
<div class="bg-blue rounded text-white py-1 flex justify-center items-center"> <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 { usePrice } from '@core/utils/usePrice'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { getNow, toJalali } from '@core/utils/useDataTime' 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 startTaraz = ref<string>('')
const endTaraz = ref<string>('') const endTaraz = ref<string>('')
const selectedItem = ref<object>({}) const selectedItem = ref<object>({})
@@ -38,7 +38,7 @@
</span> </span>
<span class="dash-quick-sheet__featured-text"> <span class="dash-quick-sheet__featured-text">
<span class="dash-quick-sheet__featured-label">شارژ / دشارژ کیف پول</span> <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> </span>
<BaseIcon icon="vuesax-linear:arrow-left-2" :size="16" class="dash-quick-sheet__featured-arrow" /> <BaseIcon icon="vuesax-linear:arrow-left-2" :size="16" class="dash-quick-sheet__featured-arrow" />
</button> </button>
@@ -106,6 +106,7 @@ import type { RouteLocationRaw } from 'vue-router'
import BasicActionSheet from '@/components/BasicActionSheet.vue' import BasicActionSheet from '@/components/BasicActionSheet.vue'
import DashboardQuickActionBtn from '@/pages/dashboard/components/DashboardQuickActionBtn.vue' import DashboardQuickActionBtn from '@/pages/dashboard/components/DashboardQuickActionBtn.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
type ActionVariant = 'default' | 'accent' | 'danger' | 'primary' type ActionVariant = 'default' | 'accent' | 'danger' | 'primary'
type ItemTone = 'slate' | 'emerald' | 'rose' | 'amber' | 'sky' | 'violet' type ItemTone = 'slate' | 'emerald' | 'rose' | 'amber' | 'sky' | 'violet'
@@ -2,7 +2,7 @@
<BasicActionSheet <BasicActionSheet
v-model:visible="visibleProxy" v-model:visible="visibleProxy"
:title="modalTitle" :title="modalTitle"
modal-width="22rem" modal-width="24rem"
drawer-height="auto" drawer-height="auto"
compact compact
:loading="loading" :loading="loading"
@@ -24,33 +24,18 @@
/> />
</div> </div>
<div <div v-if="selectedCustomer" class="dashboard-wallet-modal__balances">
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> <div>
<span class="text-slate-500">ریالی</span> <span class="text-slate-500">ریالی</span>
<p class="font-semibold text-slate-800 dark:text-slate-100">{{ formatCash(selectedCustomer) }}</p> <p class="font-semibold text-slate-800 dark:text-slate-100">{{ formatCash(selectedCustomer) }}</p>
</div> </div>
</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 <WalletChargeForm
ref="chargeFormRef" ref="chargeFormRef"
class="dashboard-wallet-modal__form" class="dashboard-wallet-modal__form"
:model-value="formCharge" :model-value="formCharge"
:variant="walletVariant" :targets="chargeTargets"
/> />
<div class="dashboard-wallet-modal__footer"> <div class="dashboard-wallet-modal__footer">
@@ -78,16 +63,18 @@ import { message } from 'ant-design-vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue' import BasicActionSheet from '@/components/BasicActionSheet.vue'
import WalletChargeForm from '@/pages/menuPages/customers/base/components/WalletChargeForm.vue' import WalletChargeForm from '@/pages/menuPages/customers/base/components/WalletChargeForm.vue'
import { import {
applyChargeTarget,
buildChargePayload, buildChargePayload,
createEmptyChargeForm, createEmptyChargeForm,
resolveChargeTarget,
walletChargeOkText, walletChargeOkText,
walletChargeSuccessMessage, walletChargeSuccessMessage,
walletChargeTitle, walletChargeTitle,
} from '@/pages/menuPages/customers/base/utils/walletChargeForm' } from '@/pages/menuPages/customers/base/utils/walletChargeForm'
import { getCustomersList } from '@/api/customer' import { getCustomersList } from '@/api/customer'
import { chargeWallet } from '@/api/wallet' import { chargeWalletUnified, getWalletChargeTargets } from '@/api/wallet'
import type { Customer } from '@/model/customer' 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' import { usePrice } from '@core/utils/usePrice'
const props = defineProps<{ visible: boolean }>() const props = defineProps<{ visible: boolean }>()
@@ -101,7 +88,7 @@ const visibleProxy = computed({
const loading = ref(false) const loading = ref(false)
const submitting = ref(false) const submitting = ref(false)
const customers = ref<Customer[]>([]) const customers = ref<Customer[]>([])
const walletVariant = ref<'money' | 'gold'>('money') const chargeTargets = ref<WalletChargeTarget[]>([])
const formCharge = ref<Charge>(createEmptyChargeForm()) const formCharge = ref<Charge>(createEmptyChargeForm())
const chargeFormRef = ref<InstanceType<typeof WalletChargeForm>>() const chargeFormRef = ref<InstanceType<typeof WalletChargeForm>>()
@@ -123,8 +110,12 @@ const selectedCustomer = computed(() =>
customers.value.find((customer) => customer.id === formCharge.value.customer_id), customers.value.find((customer) => customer.id === formCharge.value.customer_id),
) )
const selectedTarget = computed(() =>
resolveChargeTarget(chargeTargets.value, formCharge.value.chargeTargetKey),
)
const modalTitle = computed(() => const modalTitle = computed(() =>
walletChargeTitle(walletVariant.value, formCharge.value.direction), walletChargeTitle(selectedTarget.value, formCharge.value.direction),
) )
const okText = computed(() => walletChargeOkText(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 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) { function formatCash(customer: Customer) {
const cash = customerWallet(customer)?.cash ?? customer.wallet_cash const cash = customerWallet(customer)?.cash ?? customer.wallet_cash
return usePrice(Number(cash ?? 0), true, '') 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() { async function loadCustomers() {
loading.value = true loading.value = true
try { try {
customers.value = (await getCustomersList()) ?? [] const [customerList] = await Promise.all([getCustomersList(), loadChargeTargets()])
customers.value = customerList ?? []
} catch (e) { } catch (e) {
console.error(e) console.error(e)
message.error('خطا در بارگذاری لیست مشتریان') message.error('خطا در بارگذاری لیست مشتریان')
@@ -161,8 +157,11 @@ async function loadCustomers() {
} }
function resetForm() { function resetForm() {
walletVariant.value = 'money'
formCharge.value = createEmptyChargeForm() formCharge.value = createEmptyChargeForm()
const firstTarget = chargeTargets.value[0]
if (firstTarget) {
Object.assign(formCharge.value, applyChargeTarget(formCharge.value, firstTarget))
}
} }
function handleClose() { function handleClose() {
@@ -188,7 +187,7 @@ async function submit() {
submitting.value = true submitting.value = true
try { try {
await chargeWallet(buildChargePayload(formCharge.value), walletVariant.value) await chargeWalletUnified(buildChargePayload(formCharge.value))
message.success(walletChargeSuccessMessage(formCharge.value.direction)) message.success(walletChargeSuccessMessage(formCharge.value.direction))
closeModal() closeModal()
} catch (e) { } catch (e) {
@@ -231,7 +230,7 @@ watch(
} }
.dashboard-wallet-modal__balances { .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 { .dashboard-wallet-modal__footer {
@@ -12,6 +12,7 @@ import {
toggleGateway, toggleGateway,
} from '@/api/dashboard' } from '@/api/dashboard'
import { getWalletTable } from '@/api/wallet' import { getWalletTable } from '@/api/wallet'
import { formatMetalWeightFromGrams } from '@/utils/metalWeight'
import type { import type {
DashboardFlowPoint, DashboardFlowPoint,
DashboardFinance, DashboardFinance,
@@ -23,6 +24,7 @@ import type { Order } from '@/model/order'
import type { Balance } from '@/model/product' import type { Balance } from '@/model/product'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { metalLabel } from '@/utils/adminMetalLabels'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { isStoreApp } from '@/utils/config' import { isStoreApp } from '@/utils/config'
import { useAdminTheme } from '@core/composable/useAdminTheme' import { useAdminTheme } from '@core/composable/useAdminTheme'
@@ -229,7 +231,10 @@ export function useDashboardPage() {
master.value = await getDashboard() 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 })} گرم` return `${Number(value ?? 0).toLocaleString('fa-IR', { maximumFractionDigits: 3 })} گرم`
} }
@@ -277,7 +282,7 @@ export function useDashboardPage() {
return { return {
...chartSurfaceOptions(), ...chartSurfaceOptions(),
chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4, foreColor: chartSurfaceOptions().chart.foreColor }, chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4, foreColor: chartSurfaceOptions().chart.foreColor },
labels: ['سود معامله', 'کارمزد طلایی', 'هدایای طلا'], labels: ['سود معامله', metalLabel.goldCommission(), metalLabel.goldGifts()],
colors: ['#10b981', '#f59e0b', '#8b5cf6'], colors: ['#10b981', '#f59e0b', '#8b5cf6'],
legend: { legend: {
position: 'bottom', position: 'bottom',
@@ -576,6 +581,8 @@ export function useDashboardPage() {
} }
}) })
const metalBalanceItems = computed(() => balance.value?.metalBalances ?? [])
onMounted(() => { onMounted(() => {
void refreshAll() void refreshAll()
}) })
@@ -588,6 +595,7 @@ export function useDashboardPage() {
master, master,
finance, finance,
balance, balance,
metalBalanceItems,
walletSums, walletSums,
recentOrders, recentOrders,
customerStats, customerStats,
+95 -107
View File
@@ -1,5 +1,7 @@
/** توضیح و فرمول فارسی — برای tooltip آیکون راهنما */ /** توضیح و فرمول فارسی — برای tooltip آیکون راهنما */
import { m } from '@/utils/metalLabels'
export type DashboardHelp = { export type DashboardHelp = {
description: string description: string
formula: string formula: string
@@ -7,133 +9,119 @@ export type DashboardHelp = {
export const MESGHAL_FACTOR = '۴.۳۳۱۸۰۲' export const MESGHAL_FACTOR = '۴.۳۳۱۸۰۲'
export const dashboardHero: DashboardHelp = { function help(description: string, formula: string): DashboardHelp {
description: 'آخرین قیمت پایه هر گرم طلا در سیستم.', return {
formula: `قیمت هر مثقال = قیمت هر گرم × ${MESGHAL_FACTOR}`, description: m(description),
formula: m(formula),
}
} }
export const dashboardGateway: DashboardHelp = { export const dashboardHero = help(
description: 'وقتی فعال باشد مشتری می‌تواند از درگاه بانکی واریز کند؛ در حالت غیرفعال فقط واریز دستی ممکن است.', 'آخرین قیمت پایه هر گرم طلا در سیستم.',
formula: 'وضعیت: فعال یا غیرفعال', `قیمت هر مثقال = قیمت هر گرم × ${MESGHAL_FACTOR}`
} )
export const dashboardGateway = help(
'وقتی فعال باشد مشتری می‌تواند از درگاه بانکی واریز کند؛ در حالت غیرفعال فقط واریز دستی ممکن است.',
'وضعیت: فعال یا غیرفعال'
)
export const dashboardKpi = { export const dashboardKpi = {
profit: { profit: help(
description: 'مجموع سود اسپرد (تفاوت قیمت خرید/فروش با مشتری) روی همه سفارش‌های ثبت‌شده در بازه.', 'مجموع سود اسپرد (تفاوت قیمت خرید/فروش با مشتری) روی همه سفارش‌های ثبت‌شده در بازه.',
formula: 'سود کل = جمع «سود هر سفارش»', 'سود کل = جمع «سود هر سفارش»'
}, ),
commission: { commission: help(
description: 'مجموع کارمزد طلایی محصول که روی همان سفارش‌ها محاسبه شده.', 'مجموع کارمزد طلایی محصول که روی همان سفارش‌ها محاسبه شده.',
formula: 'کارمزد کل = جمع «کارمزد هر سفارش»', 'کارمزد کل = جمع «کارمزد هر سفارش»'
}, ),
orderCount: { orderCount: help(
description: 'تعداد همه سفارش‌هایی که در بازه انتخابی ثبت شده‌اند (هر وضعیتی).', 'تعداد همه سفارش‌هایی که در بازه انتخابی ثبت شده‌اند (هر وضعیتی).',
formula: 'تعداد = شمارش سفارش‌ها بین دو تاریخ', 'تعداد = شمارش سفارش‌ها بین دو تاریخ'
}, ),
goldBalance: { goldBalance: help(
description: 'تراز وزنی طلا از سفارشات موفق؛ مثبت یعنی به مشتری بیشتر فروخته‌اید.', 'تراز وزنی طلا از سفارشات موفق؛ مثبت یعنی به مشتری بیشتر فروخته‌اید.',
formula: 'تراز طلا = گرم فروش − گرم خرید (سفارش موفق)', 'تراز طلا = گرم فروش − گرم خرید (سفارش موفق)'
}, ),
cashBalance: { cashBalance: help(
description: 'تراز ریالی همان سفارشات موفق؛ ورود و خروج نقد از معاملات.', 'تراز ریالی همان سفارشات موفق؛ ورود و خروج نقد از معاملات.',
formula: 'تراز ریال = مبلغ فروش − مبلغ خرید', 'تراز ریال = مبلغ فروش − مبلغ خرید'
}, ),
mazanehBalance: { mazanehBalance: help(
description: 'میانگین موزون قیمت معاملات، تبدیل‌شده به مثقال.', 'میانگین موزون قیمت معاملات، تبدیل‌شده به مثقال.',
formula: `(جمع مبلغ معاملات ÷ جمع گرم) × ${MESGHAL_FACTOR}`, `(جمع مبلغ معاملات ÷ جمع گرم) × ${MESGHAL_FACTOR}`
}, ),
} as const satisfies Record<string, DashboardHelp> } as const satisfies Record<string, DashboardHelp>
export const dashboardDateRange: DashboardHelp = { export const dashboardDateRange = help(
description: 'تاریخ شروع و پایان برای محاسبه کارت‌ها، تراز و نمودارها.', 'تاریخ شروع و پایان برای محاسبه کارت‌ها، تراز و نمودارها.',
formula: 'گزارش = از تاریخ … تا تاریخ …', 'گزارش = از تاریخ … تا تاریخ …'
} )
export const dashboardDateMetrics = { export const dashboardDateMetrics = {
bankDeposit: { bankDeposit: help(
description: 'کل واریزهای موفق مشتریان از درگاه آنلاین در این بازه.', 'کل واریزهای موفق مشتریان از درگاه آنلاین در این بازه.',
formula: 'جمع مبلغ واریزهای تأییدشده از درگاه', 'جمع مبلغ واریزهای تأییدشده از درگاه'
}, ),
netIncome: { netIncome: help(
description: 'درآمد پس از کسر ارزش طلای هدیه‌داده‌شده.', 'درآمد پس از کسر ارزش طلای هدیه‌داده‌شده.',
formula: 'درآمد خالص = (اختلاف قیمت اصلی و پرداختی) − (گرم هدیه × مظنه)', 'درآمد خالص = (اختلاف قیمت اصلی و پرداختی) − (گرم هدیه × مظنه)'
}, ),
gifts: { gifts: help(
description: 'طلایی که ادمین به مشتریان هدیه داده در همین بازه.', 'طلایی که ادمین به مشتریان هدیه داده در همین بازه.',
formula: 'گرم هدیه = جمع طلای ثبت‌شده در تراکنش‌های ادمین', 'گرم هدیه = جمع طلای ثبت‌شده در تراکنش‌های ادمین'
}, ),
} as const satisfies Record<string, DashboardHelp> } as const satisfies Record<string, DashboardHelp>
export const dashboardCharts = { export const dashboardCharts = {
incomeMix: { incomeMix: help(
description: 'نشان می‌دهد سود معامله، کارمزد و هدایا چقدر از درآمد کل را تشکیل می‌دهند.', 'نشان می‌دهد سود معامله، کارمزد و هدایا چقدر از درآمد کل را تشکیل می‌دهند.',
formula: 'درآمد = سود معامله + کارمزد + (گرم هدیه × مظنه)', 'درآمد = سود معامله + کارمزد + (گرم هدیه × مظنه)'
}, ),
buySellVolume: { buySellVolume: help(
description: 'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).', 'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
formula: 'هر روز: گرم خرید مشتری و گرم فروش به مشتری', 'هر روز: گرم خرید مشتری و گرم فروش به مشتری'
}, ),
priceChart: { priceChart: help(
description: 'روند تغییر قیمت پایه هر گرم در ۲۴ ساعت، هفته یا ماه.', 'روند تغییر قیمت پایه هر گرم در ۲۴ ساعت، هفته یا ماه.',
formula: 'نمودار = قیمت پایه در هر لحظه', 'نمودار = قیمت پایه در هر لحظه'
}, ),
productActivity: { productActivity: help(
description: 'مقایسه حجم خرید و فروش هر محصول در بازه انتخابی.', 'مقایسه حجم خرید و فروش هر محصول در بازه انتخابی.',
formula: 'خرید = جمع گرم مثبت | فروش = جمع گرم منفی (معاملات همان بازه)', 'خرید = جمع گرم مثبت | فروش = جمع گرم منفی (معاملات همان بازه)'
}, ),
} as const satisfies Record<string, DashboardHelp> } as const satisfies Record<string, DashboardHelp>
export const dashboardOrders = { export const dashboardOrders = {
peakHours: { peakHours: help(
description: 'توزیع سفارش‌های موفق مشتریان خانگی در ۲۴ ساعت شبانه‌روز؛ برای شناخت ساعات شلوغ.', 'توزیع سفارش‌های موفق مشتریان خانگی در ۲۴ ساعت شبانه‌روز؛ برای شناخت ساعات شلوغ.',
formula: 'هر ستون = تعداد سفارش در آن ساعت (جمع چند روز اخیر)', 'هر ستون = تعداد سفارش در آن ساعت (جمع چند روز اخیر)'
}, ),
recent: { recent: help(
description: '۱۴ سفارش آخر با جزئیات مظنه و سود.', '۱۴ سفارش آخر با جزئیات مظنه و سود.',
formula: 'مظنه = قیمت پایه | قیمت مشتری = مظنه ± اسپرد | سود/گرم = تفاوت اسپرد', 'مظنه = قیمت پایه | قیمت مشتری = مظنه ± اسپرد | سود/گرم = تفاوت اسپرد'
}, ),
} as const satisfies Record<string, DashboardHelp> } as const satisfies Record<string, DashboardHelp>
export const dashboardCustomers: DashboardHelp = { export const dashboardCustomers = help(
description: 'آمار مشتریان خانگی ثبت‌شده در سیستم.', 'آمار مشتریان خانگی ثبت‌شده در سیستم.',
formula: 'فعال = دارای کیف پول | بدون احراز = هویت تأیید نشده', 'فعال = دارای کیف پول | بدون احراز = هویت تأیید نشده'
} )
export const dashboardCustomerMetrics = { export const dashboardCustomerMetrics = {
total: { total: help('همه مشتریان خانگی ثبت‌شده.', 'شمارش کل مشتریان'),
description: 'همه مشتریان خانگی ثبت‌شده.', new: help('مشتریانی که در ۳ ماه اخیر ثبت‌نام کرده‌اند.', 'ثبت‌نام در ۹۰ روز گذشته'),
formula: 'شمارش کل مشتریان', active: help('مشتریانی که حداقل یک کیف پول دارند.', 'دارای موجودی در کیف'),
}, notVerified: help('مشتریانی که احراز هویت را تکمیل نکرده‌اند.', 'وضعیت احراز: ناقص'),
new: {
description: 'مشتریانی که در ۳ ماه اخیر ثبت‌نام کرده‌اند.',
formula: 'ثبت‌نام در ۹۰ روز گذشته',
},
active: {
description: 'مشتریانی که حداقل یک کیف پول دارند.',
formula: 'دارای موجودی در کیف',
},
notVerified: {
description: 'مشتریانی که احراز هویت را تکمیل نکرده‌اند.',
formula: 'وضعیت احراز: ناقص',
},
} as const satisfies Record<string, DashboardHelp> } as const satisfies Record<string, DashboardHelp>
export const dashboardWallet: DashboardHelp = { export const dashboardWallet = help(
description: 'مجموع موجودی طلا و ریال همه مشتریان.', 'مجموع موجودی طلا و ریال همه مشتریان.',
formula: 'ارزش طلا به ریال = جمع گرم × مظنه لحظه‌ای', 'ارزش طلا به ریال = جمع گرم × مظنه لحظه‌ای'
} )
export const dashboardWalletMetrics = { export const dashboardWalletMetrics = {
gold: { gold: help('مجموع طلای موجود در کیف همه مشتریان.', 'جمع گرم طلا در کیف‌ها'),
description: 'مجموع طلای موجود در کیف همه مشتریان.', cash: help('مجموع ریال موجود در کیف همه مشتریان.', 'جمع ریال در کیف‌ها'),
formula: 'جمع گرم طلا در کیف‌ها', goldToCash: help('ارزش ریالی کل طلای مشتریان با مظنه فعلی.', 'جمع گرم × قیمت هر گرم'),
},
cash: {
description: 'مجموع ریال موجود در کیف همه مشتریان.',
formula: 'جمع ریال در کیف‌ها',
},
goldToCash: {
description: 'ارزش ریالی کل طلای مشتریان با مظنه فعلی.',
formula: 'جمع گرم × قیمت هر گرم',
},
} as const satisfies Record<string, DashboardHelp> } as const satisfies Record<string, DashboardHelp>
+18 -4
View File
@@ -57,7 +57,7 @@
<DashboardStatCard <DashboardStatCard
tone="amber" tone="amber"
icon="vuesax-linear:coin" icon="vuesax-linear:coin"
title="کارمزد طلایی" :title="metalLabel.goldCommission()"
:value="usePrice(finance?.fullPriceKarmozd ?? finance?.fullPriceKaermozd ?? 0, true, '')" :value="usePrice(finance?.fullPriceKarmozd ?? finance?.fullPriceKaermozd ?? 0, true, '')"
:help="copy.kpi.commission" :help="copy.kpi.commission"
/> />
@@ -68,10 +68,22 @@
:value="String(finance?.orderCount ?? 0)" :value="String(finance?.orderCount ?? 0)"
:help="copy.kpi.orderCount" :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 <DashboardStatCard
v-else
tone="blue" tone="blue"
icon="vuesax-linear:empty-wallet" icon="vuesax-linear:empty-wallet"
title="تراز طلایی" :title="metalLabel.goldBalance()"
:value="formatGram(balance?.goldBalance)" :value="formatGram(balance?.goldBalance)"
:help="copy.kpi.goldBalance" :help="copy.kpi.goldBalance"
/> />
@@ -452,6 +464,7 @@ import {
} from '@/pages/dashboard/dashboardSectionCopy' } from '@/pages/dashboard/dashboardSectionCopy'
import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage' import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage'
import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions' import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions'
import { metalBalanceTitle, metalLabel } from '@/utils/adminMetalLabels'
const copy = { const copy = {
hero: dashboardHero, hero: dashboardHero,
@@ -472,6 +485,7 @@ const {
master, master,
finance, finance,
balance, balance,
metalBalanceItems,
walletSums, walletSums,
recentOrders, recentOrders,
peakHoursRange, peakHoursRange,
@@ -573,7 +587,7 @@ const customerMetricItems = computed(() => [
const walletMetricItems = computed(() => [ const walletMetricItems = computed(() => [
{ {
label: 'طلای مشتریان', label: metalLabel.customersGold(),
value: formatGram(walletSums.value?.gold), value: formatGram(walletSums.value?.gold),
help: dashboardWalletMetrics.gold, help: dashboardWalletMetrics.gold,
variant: 'amber', variant: 'amber',
@@ -585,7 +599,7 @@ const walletMetricItems = computed(() => [
variant: 'sky', variant: 'sky',
}, },
{ {
label: 'ارزش طلا به ریال', label: metalLabel.goldValueInRial(),
value: usePrice(walletSums.value?.goldToCash ?? 0, true, ''), value: usePrice(walletSums.value?.goldToCash ?? 0, true, ''),
help: dashboardWalletMetrics.goldToCash, help: dashboardWalletMetrics.goldToCash,
variant: 'violet', variant: 'violet',
@@ -14,7 +14,7 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" /> <BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template> </template>
</MobileStatCard> </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> <template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" /> <BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
</template> </template>
@@ -26,7 +26,7 @@
<MobileDetailRow label="نام مشتری دعوت شده" :value="row.customer?.name || '-'" /> <MobileDetailRow label="نام مشتری دعوت شده" :value="row.customer?.name || '-'" />
<MobileDetailRow label="زمان ثبت" :value="row.created_at ? toJalali(row.created_at) : '-'" /> <MobileDetailRow label="زمان ثبت" :value="row.created_at ? toJalali(row.created_at) : '-'" />
<MobileDetailRow label="نقد (ریال)" :value="usePrice(row.cash ?? 0, true, '')" /> <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 || '-'" /> <MobileDetailRow label="توضیحات" full-width :value="row.note || '-'" />
</MobileInfoGrid> </MobileInfoGrid>
</template> </template>
@@ -44,6 +44,8 @@ import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
import { m } from '@/utils/metalLabels'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
@@ -7,7 +7,7 @@
<div class="text-xs text-gray-400">{{ createdAtLabel }}</div> <div class="text-xs text-gray-400">{{ createdAtLabel }}</div>
</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">{{ 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>
</div> </div>
@@ -21,6 +21,7 @@ import type { TotalAccounting } from '@/model/accounting'
import MobileCard from '@/components/mobile/MobileCard.vue' import MobileCard from '@/components/mobile/MobileCard.vue'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{ const props = defineProps<{
item: TotalAccounting item: TotalAccounting
@@ -31,7 +31,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { getTotalAccountingTable } from '@/api/accounting' 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 AccountingMobileCardList from '@/pages/menuPages/accounting/components/AccountingMobileCardList.vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable' 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 mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const tblProps = ref({ const tblProps = ref({
columns: cols, columns: getAccountingColumns(),
api: (params: TableInput) => getTotalAccountingTable(params) 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 { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue' /*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect' 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: 'ردیف', title: 'ردیف',
dataIndex: 'id', dataIndex: 'id',
@@ -63,7 +65,7 @@ export const cols: BaseTableColumn[] = [
// } // }
}, },
{ {
title: 'طلا', title: metalLabel.goldUnit(),
dataIndex: 'gold', dataIndex: 'gold',
key: 'gold', key: 'gold',
type: 'Number', type: 'Number',
@@ -81,4 +83,5 @@ export const cols: BaseTableColumn[] = [
slot: 'action', slot: 'action',
filterable: false filterable: false
} }
] ]
}
+7 -6
View File
@@ -20,7 +20,7 @@
<a-form :model="form" ref="formRef" autocomplete="off"> <a-form :model="form" ref="formRef" autocomplete="off">
<div class="grid md:grid-cols-1 gap-4"> <div class="grid md:grid-cols-1 gap-4">
<div class="bg-gray-50 px-10 py-3 flex justify-center 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"> <div class="font-bold">
{{ usePrice(form.direction === 1 ? dataLast?.buy ?? 0 : dataLast?.sell ?? 0) }} {{ usePrice(form.direction === 1 ? dataLast?.buy ?? 0 : dataLast?.sell ?? 0) }}
</div> </div>
@@ -65,7 +65,7 @@
</a-form-item> </a-form-item>
<BasicInfoItem <BasicInfoItem
is-line is-line
title="کیف پول طلا" :title="metalLabel.goldWalletTitle()"
class="mt-2" class="mt-2"
:value="getGoldBalance(selectedCustomer?.wallet) || '_'" :value="getGoldBalance(selectedCustomer?.wallet) || '_'"
/> />
@@ -95,7 +95,7 @@
addon-before="گرم" addon-before="گرم"
addon-bg-color="#0A1B37" addon-bg-color="#0A1B37"
addon-text-color="white" addon-text-color="white"
place-holder="مقدار طلا" :place-holder="metalLabel.goldPlaceholder()"
type="tel" type="tel"
@onchange="calculateCash" @onchange="calculateCash"
/> />
@@ -143,6 +143,7 @@ import { getLastPrice } from '@/api/price'
import type { last } from '@/model/price' import type { last } from '@/model/price'
import { createOrder } from '@/api/order' import { createOrder } from '@/api/order'
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet' import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
//loading //loading
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
@@ -248,7 +249,7 @@ function info(val: Order) {
value: val.amount ?? '-' value: val.amount ?? '-'
}, },
{ {
key: 'قیمت هر گرم طلا برای مشتری (ریال)', key: metalLabel.goldGramPriceCustomer(),
value: usePrice(val.price ?? 0) ?? '-' value: usePrice(val.price ?? 0) ?? '-'
}, },
{ {
@@ -256,7 +257,7 @@ function info(val: Order) {
value: usePrice(val.total_price ?? 0) ?? '-' value: usePrice(val.total_price ?? 0) ?? '-'
}, },
{ {
key: 'قیمت هر گرم طلا بدون سود (ریال)', key: metalLabel.goldGramPriceOriginal(),
value: usePrice(val.price_original ?? 0) ?? '-' value: usePrice(val.price_original ?? 0) ?? '-'
}, },
{ {
@@ -284,7 +285,7 @@ function info(val: Order) {
value: usePrice(val.service_charge_price ?? 0) value: usePrice(val.service_charge_price ?? 0)
},*/ },*/
{ {
key: 'کارمزد طلایی', key: metalLabel.goldCommission(),
value: val.service_charge ?? '-' + ' گرم' value: val.service_charge ?? '-' + ' گرم'
}, },
/* { /* {
@@ -93,7 +93,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
title: 'عمومی و برند', title: 'عمومی و برند',
icon: 'vuesax-linear:home-2', icon: 'vuesax-linear:home-2',
match: (k) => 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 k
) )
}, },
@@ -123,9 +123,9 @@
> >
<BasicInfoItem <BasicInfoItem
title="نام قلک" 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> </div>
</div> </div>
@@ -158,6 +158,7 @@ import type { FormInstance } from 'ant-design-vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue' import BasicActionSheet from '@/components/BasicActionSheet.vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.vue' import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { message } from 'ant-design-vue/es' import { message } from 'ant-design-vue/es'
import type { Info } from '@/components/BasicInfoModal/model/Info' import type { Info } from '@/components/BasicInfoModal/model/Info'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime' import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
@@ -21,12 +21,12 @@
<CustomerMobileDetailSheet <CustomerMobileDetailSheet
v-model:visible="sheetVisible" v-model:visible="sheetVisible"
:item="selectedItem" :item="selectedItem"
:metals="catalogMetals"
@info="forwardAndClose('info', $event)" @info="forwardAndClose('info', $event)"
@edit="forwardAndClose('edit', $event)" @edit="forwardAndClose('edit', $event)"
@archive="forwardAndClose('archive', $event)" @archive="forwardAndClose('archive', $event)"
@toggleActive="forward('toggleActive', $event)" @toggleActive="forward('toggleActive', $event)"
@chargeMoney="forwardAndClose('chargeMoney', $event)" @chargeWallet="forwardAndClose('chargeWallet', $event)"
@chargeGold="forwardAndClose('chargeGold', $event)"
@toggleTrade="forward('toggleTrade', $event)" @toggleTrade="forward('toggleTrade', $event)"
@confirmUser="forwardAndClose('confirmUser', $event)" @confirmUser="forwardAndClose('confirmUser', $event)"
/> />
@@ -40,14 +40,22 @@ import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileD
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList' import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
import CustomerMobileListItem from './CustomerMobileListItem.vue' import CustomerMobileListItem from './CustomerMobileListItem.vue'
import CustomerMobileDetailSheet from './CustomerMobileDetailSheet.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<{ const emit = defineEmits<{
(e: 'info', item: Customer): void (e: 'info', item: Customer): void
(e: 'edit', item: Customer): void (e: 'edit', item: Customer): void
(e: 'archive', item: Customer): void (e: 'archive', item: Customer): void
(e: 'toggleActive', item: Customer): void (e: 'toggleActive', item: Customer): void
(e: 'chargeMoney', item: Customer): void (e: 'chargeWallet', item: Customer): void
(e: 'chargeGold', item: Customer): void
(e: 'toggleTrade', item: Customer): void (e: 'toggleTrade', item: Customer): void
(e: 'confirmUser', item: Customer): void (e: 'confirmUser', item: Customer): void
}>() }>()
@@ -59,8 +67,7 @@ type SheetAction =
| 'edit' | 'edit'
| 'archive' | 'archive'
| 'toggleActive' | 'toggleActive'
| 'chargeMoney' | 'chargeWallet'
| 'chargeGold'
| 'toggleTrade' | 'toggleTrade'
| 'confirmUser' | 'confirmUser'
@@ -74,7 +81,7 @@ function forwardAndClose(action: SheetAction, item: Customer) {
} }
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<Customer>({ const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<Customer>({
fetchApi: getCustomerTable, fetchApi: fetchCustomers,
onFetched: (rows) => syncSelectedItem(rows) onFetched: (rows) => syncSelectedItem(rows)
}) })
@@ -4,12 +4,15 @@
<h2 class="text-base font-700 leading-relaxed text-gray-900">{{ row.name || '-' }}</h2> <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> <p v-if="row.mobile" class="text-sm text-gray-500" dir="ltr">{{ row.mobile }}</p>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-1 gap-2">
<BaseButton class="w-full" size="middle" type="primary" icon="vuesax-linear:wallet-add" @click="closeAndEmit('chargeMoney', row)"> <BaseButton
شارژ ریالی class="w-full"
</BaseButton> size="middle"
<BaseButton class="w-full" size="middle" type="primary" icon="vuesax-linear:wallet-money" @click="closeAndEmit('chargeGold', row)"> type="primary"
شارژ طلایی icon="vuesax-linear:wallet-add"
@click="closeAndEmit('chargeWallet', row)"
>
شارژ / دشارژ کیف پول
</BaseButton> </BaseButton>
<BaseButton <BaseButton
v-if="!row.is_legal" v-if="!row.is_legal"
@@ -51,7 +54,14 @@
</div> </div>
<div class="space-y-2"> <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> <template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" /> <BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
</template> </template>
@@ -83,7 +93,6 @@
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" /> <MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" /> <MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف کاربر" :value="row.referral?.name ?? '-'" /> <MobileDetailRow label="نام معرف کاربر" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="گنج (گرم)" :value="row.gold ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" /> <MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid> </MobileInfoGrid>
</template> </template>
@@ -98,13 +107,17 @@ import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
import MobileStatCard from '@/components/mobile/MobileStatCard.vue' import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue' import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.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 { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' 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<{ const props = defineProps<{
visible: boolean visible: boolean
item: Customer | null item: Customer | null
metals?: CustomerMetalColumn[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -113,8 +126,7 @@ const emit = defineEmits<{
(e: 'edit', item: Customer): void (e: 'edit', item: Customer): void
(e: 'archive', item: Customer): void (e: 'archive', item: Customer): void
(e: 'toggleActive', item: Customer): void (e: 'toggleActive', item: Customer): void
(e: 'chargeMoney', item: Customer): void (e: 'chargeWallet', item: Customer): void
(e: 'chargeGold', item: Customer): void
(e: 'toggleTrade', item: Customer): void (e: 'toggleTrade', item: Customer): void
(e: 'confirmUser', item: Customer): void (e: 'confirmUser', item: Customer): void
}>() }>()
@@ -124,7 +136,7 @@ const visibleProxy = computed({
set: (value: boolean) => emit('update:visible', value) 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) { function closeAndEmit(event: SheetEmit, row: Customer) {
visibleProxy.value = false visibleProxy.value = false
@@ -157,8 +169,24 @@ function cashBalance(row: Customer) {
return getCashBalance(row.wallet) return getCashBalance(row.wallet)
} }
function goldBalance(row: Customer) { function metalBalanceEntries(row: Customer) {
return getGoldBalance(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, '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) { function lastSeen(row: Customer) {
@@ -6,6 +6,18 @@
class="wallet-charge-form" class="wallet-charge-form"
:class="formClass" :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-form-item label="نوع عملیات" name="direction">
<a-radio-group <a-radio-group
v-model:value="modelValue.direction" v-model:value="modelValue.direction"
@@ -36,10 +48,10 @@
<BasicInput <BasicInput
v-model:value="modelValue.amountAbs" v-model:value="modelValue.amountAbs"
:addon-before="unit" :addon-before="unit"
:type="variant === 'money' ? 'price' : 'number'" :type="isMoney ? 'price' : 'number'"
:place-holder="amountPlaceholder" :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, 'تومان') }} معادل {{ usePrice(Number(modelValue.amountAbs) / 10, true, 'تومان') }}
</div> </div>
</a-form-item> </a-form-item>
@@ -51,22 +63,46 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref, watch } from 'vue'
import type { FormInstance } from 'ant-design-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 { inputRules } from '@/utils/inputRules'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { applyChargeTarget, resolveChargeTarget, walletPreviewLabel } from '../utils/walletChargeForm'
const props = defineProps<{ const props = defineProps<{
variant: 'money' | 'gold'
modelValue: Charge modelValue: Charge
targets: WalletChargeTarget[]
}>() }>()
const formRef = ref<FormInstance>() 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 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(() => const formClass = computed(() =>
isDischarge.value ? 'wallet-charge-form--discharge' : 'wallet-charge-form--charge' isDischarge.value ? 'wallet-charge-form--discharge' : 'wallet-charge-form--charge'
) )
@@ -81,8 +117,7 @@ const previewSign = computed(() => (isDischarge.value ? '−' : '+'))
const previewText = computed(() => { const previewText = computed(() => {
const action = isDischarge.value ? 'کسر از' : 'افزایش' const action = isDischarge.value ? 'کسر از' : 'افزایش'
const wallet = props.variant === 'money' ? 'موجودی ریالی' : 'موجودی طلایی' return `${action} ${walletPreviewLabel(selectedTarget.value)}`
return `${action} ${wallet} کاربر`
}) })
const amountLabel = computed(() => const amountLabel = computed(() =>
@@ -92,7 +127,7 @@ const amountLabel = computed(() =>
const noteLabel = computed(() => (isDischarge.value ? 'علت دشارژ' : 'علت شارژ')) const noteLabel = computed(() => (isDischarge.value ? 'علت دشارژ' : 'علت شارژ'))
const amountPlaceholder = computed(() => const amountPlaceholder = computed(() =>
props.variant === 'money' ? 'مثلاً ۱۰۰٬۰۰۰' : 'مثلاً ۰.۵' isMoney.value ? 'مثلاً ۱۰۰٬۰۰۰' : 'مثلاً ۰.۵'
) )
const notePlaceholder = computed(() => 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({ defineExpose({
validateFields: () => formRef.value?.validateFields() validateFields: () => formRef.value?.validateFields()
}) })
@@ -49,14 +49,10 @@
:color="record.active ? '#54AD06' : '#ad0627'" :color="record.active ? '#54AD06' : '#ad0627'"
/> />
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('chargeMoney', record)"> <a-tooltip placement="bottom" class="cursor-pointer" @click="emits('chargeWallet', record)">
<template #title><span>شارژ کیف پول ریالی</span></template> <template #title><span>شارژ / دشارژ کیف پول</span></template>
<Icon icon="vuesax-linear:wallet-add" color="#9333ea" /> <Icon icon="vuesax-linear:wallet-add" color="#9333ea" />
</a-tooltip> </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)"> <a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
<template #title><span>مشخصات</span></template> <template #title><span>مشخصات</span></template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" /> <Icon icon="vuesax-linear:stickynote" color="#10913A" />
@@ -100,8 +96,7 @@
@edit="emits('edit', $event)" @edit="emits('edit', $event)"
@archive="handleArchive" @archive="handleArchive"
@toggleActive="(item) => changeCustomerActivationFunction(item.id)" @toggleActive="(item) => changeCustomerActivationFunction(item.id)"
@chargeMoney="emits('chargeMoney', $event)" @chargeWallet="emits('chargeWallet', $event)"
@chargeGold="emits('chargeGold', $event)"
@toggleTrade="(item) => changeCustomerOrderActivationFunction(item.id)" @toggleTrade="(item) => changeCustomerOrderActivationFunction(item.id)"
@confirmUser="openConfirmUser" @confirmUser="openConfirmUser"
/> />
@@ -144,7 +139,7 @@ import {
confirmUser, confirmUser,
getCustomerTable getCustomerTable
} from '@/api/customer' } 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 CustomerMobileCardList from '@/pages/menuPages/customers/base/components/CustomerMobileCardList.vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable' 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' import { isMobile } from '@core/utils/is'
const [submit, reload] = useBaseTable() 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 loading = ref(false)
const confirmVisible = ref(false) const confirmVisible = ref(false)
@@ -163,8 +158,13 @@ const confirmInfo = ref<Customer>()
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null) const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const tblProps = ref<any>({ const tblProps = ref<any>({
columns: cols, columns: buildCustomerColumns(),
api: (params: TableInput) => getCustomerTable(params) api: async (params: TableInput) => {
const data = await getCustomerTable(params)
const metals = data?.catalogMetals ?? []
tblProps.value.columns = buildCustomerColumns(metals)
return data
}
}) })
const fetch = async () => { const fetch = async () => {
@@ -1,212 +1,164 @@
import type { BaseTableColumn } from '@core/type/baseTable' 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 CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet' import { getCashBalance } from '@/components/mobile/utils/wallet'
/*import BudgetSelect from '@/components/BudgetSelect.vue' import { customerWalletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
//define store export type CustomerMetalColumn = { metal_id: number; label: string; display_unit?: string }
//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 const cols: BaseTableColumn[] = [ export function buildCustomerColumns(metals: CustomerMetalColumn[] = []): BaseTableColumn[] {
{ const baseColumns: BaseTableColumn[] = [
title: 'ردیف', {
dataIndex: 'id', title: 'ردیف',
key: 'id', dataIndex: 'id',
filterable: false, key: 'id',
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',
filterable: false, filterable: false,
sortable: false, sortable: false,
filterComponentProps: { customRender: (record: any) => {
multiple: true return record.index + 1
},
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: 'حوزه', title: 'کد کاربری',
dataIndex: 'section', dataIndex: 'id',
key: 'section' key: 'id',
type: 'Number'
},
{
title: 'نام و نام خانوادگی',
dataIndex: 'name',
key: 'name',
type: 'Text'
}, },
{ {
title: 'شماره موبایل', title: 'شماره موبایل',
dataIndex: 'mobile', dataIndex: 'mobile',
type: 'Number', key: 'mobile',
key: 'mobile' type: 'Number'
}, },
{ {
title: 'تاریخ ثبت', title: 'نوع کاربر',
dataIndex: 'created_at', dataIndex: 'type_str',
key: 'created_at', key: 'type_str',
type: 'DateTime', type: 'Text'
customRender: (record: any) => { },
return toJalaliNoTime(record.record.created_at) {
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', const metalColumns: BaseTableColumn[] =
key: 'action', metals.length > 0
slot: 'action', ? metals.map((metal) => ({
filterable: false 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" @info="info"
@edit="edit" @edit="edit"
@add="add" @add="add"
@chargeMoney="walletMoneyCharge" @chargeWallet="walletCharge"
@chargeGold="walletGoldCharge"
/> />
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" /> <!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" />
@@ -66,9 +65,6 @@
<a-form-item label="میزان پاداش" name="profit_amount"> <a-form-item label="میزان پاداش" name="profit_amount">
<BasicInput v-model:value="form.profit_amount" /> <BasicInput v-model:value="form.profit_amount" />
</a-form-item> </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"> <a-form-item label="حداکثر معرفی" name="add_user_limit">
<BasicInput v-model:value="form.add_user_limit" /> <BasicInput v-model:value="form.add_user_limit" />
</a-form-item> </a-form-item>
@@ -191,9 +187,9 @@
> >
<BasicInfoItem <BasicInfoItem
title="نام قلک" 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> </div>
</div> </div>
@@ -217,60 +213,39 @@
</BasicActionSheet> </BasicActionSheet>
<BasicActionSheet <BasicActionSheet
v-if="isMobile" v-if="isMobile"
v-model:visible="showMoneyWalletCharge" v-model:visible="showWalletCharge"
:title="moneyChargeTitle" :title="walletChargeModalTitle"
modal-width="24rem" modal-width="26rem"
:loading="loading" :loading="loading"
:show-button="true" :show-button="true"
:show-cancel="true" :show-cancel="true"
:show-ok="true" :show-ok="true"
:ok-text="moneyChargeOkText" :ok-text="walletChargeOkLabel"
cancel-text="انصراف" cancel-text="انصراف"
@ok="chargeMoneyWallet" @ok="submitWalletCharge"
@close="showMoneyWalletCharge = false" @close="closeWalletCharge"
> >
<WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" /> <WalletChargeForm
ref="walletChargeFormRef"
:model-value="formCharge"
:targets="chargeTargets"
/>
</BasicActionSheet> </BasicActionSheet>
<BasicModal <BasicModal
v-else v-else
v-model:visible="showMoneyWalletCharge" v-model:visible="showWalletCharge"
width="22rem" width="24rem"
:title="moneyChargeTitle" :title="walletChargeModalTitle"
:ok-text="moneyChargeOkText" :ok-text="walletChargeOkLabel"
@ok="chargeMoneyWallet" @ok="submitWalletCharge"
:loading="loading" :loading="loading"
@close="showMoneyWalletCharge = false" @close="closeWalletCharge"
> >
<WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" /> <WalletChargeForm
</BasicModal> ref="walletChargeFormRef"
:model-value="formCharge"
<BasicActionSheet :targets="chargeTargets"
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" />
</BasicModal> </BasicModal>
<BasicInfoModal <BasicInfoModal
v-model:visible="userInfoOrderVisible" v-model:visible="userInfoOrderVisible"
@@ -302,9 +277,9 @@ import { cloneDeep } from 'lodash'
import type { Item } from '@/model/Item' import type { Item } from '@/model/Item'
import type { Customer } from '@/model/customer' import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/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 customers from '@/router/routes/customers'
import { chargeWallet } from '@/api/wallet' import { chargeWalletUnified, getWalletChargeTargets } from '@/api/wallet'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import jalaliday from 'jalaliday' import jalaliday from 'jalaliday'
import ordersTbl from '@/pages/menuPages/orders/components/table.vue' 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 { isMobile } from '@core/utils/is'
import WalletChargeForm from './components/WalletChargeForm.vue' import WalletChargeForm from './components/WalletChargeForm.vue'
import { import {
applyChargeTarget,
buildChargePayload, buildChargePayload,
createEmptyChargeForm, createEmptyChargeForm,
resolveChargeTarget,
walletChargeOkText, walletChargeOkText,
walletChargeSuccessMessage, walletChargeSuccessMessage,
walletChargeTitle walletChargeTitle
} from './utils/walletChargeForm' } from './utils/walletChargeForm'
import { metalLabel } from '@/utils/adminMetalLabels'
//tabs //tabs
const activeKey = ref('اطلاعات جامع') const activeKey = ref('اطلاعات جامع')
@@ -376,21 +354,23 @@ const customerItems: Array<Item> = [
] ]
//wallet //wallet
const showMoneyWalletCharge = ref<boolean>(false) const showWalletCharge = ref<boolean>(false)
const showGoldWalletCharge = ref<boolean>(false) const chargeTargets = ref<WalletChargeTarget[]>([])
const userInfoOrderVisible = ref<boolean>(false) const userInfoOrderVisible = ref<boolean>(false)
//form //form
const form = ref<Partial<Customer>>({}) const form = ref<Partial<Customer>>({})
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const formCharge = ref<Charge>({}) const formCharge = ref<Charge>({})
const moneyChargeFormRef = ref<InstanceType<typeof WalletChargeForm>>() const walletChargeFormRef = ref<InstanceType<typeof WalletChargeForm>>()
const goldChargeFormRef = ref<InstanceType<typeof WalletChargeForm>>()
const moneyChargeTitle = computed(() => walletChargeTitle('money', formCharge.value.direction)) const selectedChargeTarget = computed(() =>
const goldChargeTitle = computed(() => walletChargeTitle('gold', formCharge.value.direction)) resolveChargeTarget(chargeTargets.value, formCharge.value.chargeTargetKey)
const moneyChargeOkText = computed(() => walletChargeOkText(formCharge.value.direction)) )
const goldChargeOkText = computed(() => walletChargeOkText(formCharge.value.direction)) const walletChargeModalTitle = computed(() =>
walletChargeTitle(selectedChargeTarget.value, formCharge.value.direction)
)
const walletChargeOkLabel = computed(() => walletChargeOkText(formCharge.value.direction))
//info //info
let infoParams = reactive<Info>({}) let infoParams = reactive<Info>({})
@@ -407,7 +387,7 @@ function infoOrder(val: Order) {
value: val.amount ?? '-' value: val.amount ?? '-'
}, },
{ {
key: 'قیمت هر گرم طلا برای مشتری (ریال)', key: metalLabel.goldGramPriceCustomer(),
value: usePrice(val.price ?? 0) ?? '-' value: usePrice(val.price ?? 0) ?? '-'
}, },
{ {
@@ -415,7 +395,7 @@ function infoOrder(val: Order) {
value: usePrice(val.total_price ?? 0) ?? '-' value: usePrice(val.total_price ?? 0) ?? '-'
}, },
{ {
key: '/قیمت هر گرم طلا بدون سود (ریال)', key: metalLabel.goldGramPriceOriginalSlash(),
value: usePrice(val.price_original ?? 0) ?? '-' value: usePrice(val.price_original ?? 0) ?? '-'
}, },
{ {
@@ -443,7 +423,7 @@ function infoOrder(val: Order) {
value: usePrice(val.service_charge_price ?? 0) value: usePrice(val.service_charge_price ?? 0)
},*/ },*/
{ {
key: 'کارمزد طلایی', key: metalLabel.goldCommission(),
value: val.service_charge ?? '-' + ' گرم' value: val.service_charge ?? '-' + ' گرم'
}, },
/* { /* {
@@ -511,40 +491,43 @@ async function add() {
function resetFormCharge(customerId: number) { function resetFormCharge(customerId: number) {
formCharge.value = createEmptyChargeForm(customerId) formCharge.value = createEmptyChargeForm(customerId)
const firstTarget = chargeTargets.value[0]
if (firstTarget) {
Object.assign(formCharge.value, applyChargeTarget(formCharge.value, firstTarget))
}
} }
async function walletMoneyCharge(val: Customer) { async function loadChargeTargets() {
resetFormCharge(val.id) if (chargeTargets.value.length) return
showMoneyWalletCharge.value = true const data = await getWalletChargeTargets()
chargeTargets.value = data?.targets ?? []
} }
async function walletGoldCharge(val: Customer) { async function walletCharge(val: Customer) {
resetFormCharge(val.id) try {
showGoldWalletCharge.value = true 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() { function closeWalletCharge() {
moneyChargeFormRef.value?.validateFields()?.then(async () => { showWalletCharge.value = false
}
async function submitWalletCharge() {
walletChargeFormRef.value?.validateFields()?.then(async () => {
try { try {
loading.value = true loading.value = true
await chargeWallet(buildChargePayload(formCharge.value), 'money') await chargeWalletUnified(buildChargePayload(formCharge.value))
message.success(walletChargeSuccessMessage(formCharge.value.direction)) message.success(walletChargeSuccessMessage(formCharge.value.direction))
showMoneyWalletCharge.value = false showWalletCharge.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
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} finally { } 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 { export function createEmptyChargeForm(customerId?: number): Charge {
return { return {
@@ -7,7 +7,9 @@ export function createEmptyChargeForm(customerId?: number): Charge {
note: undefined, note: undefined,
talabBedehi: false, talabBedehi: false,
direction: 'charge', direction: 'charge',
amountAbs: undefined amountAbs: undefined,
chargeTargetKey: 'money',
product_id: null
} }
} }
@@ -18,17 +20,35 @@ export function buildChargePayload(form: Charge): Charge {
value: signed, value: signed,
note: form.note, note: form.note,
customer_id: form.customer_id, 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( export function walletChargeTitle(
variant: 'money' | 'gold', target: WalletChargeTarget | undefined,
direction: WalletChargeDirection = 'charge' direction: WalletChargeDirection = 'charge'
) { ) {
const wallet = variant === 'money' ? 'ریالی' : 'طلایی'
const action = direction === 'discharge' ? 'دشارژ' : 'شارژ' const action = direction === 'discharge' ? 'دشارژ' : 'شارژ'
return `${action} کیف پول ${wallet}` const wallet = target?.label ?? 'کیف پول'
return `${action} ${wallet}`
} }
export function walletChargeOkText(direction: WalletChargeDirection = 'charge') { export function walletChargeOkText(direction: WalletChargeDirection = 'charge') {
@@ -38,3 +58,10 @@ export function walletChargeOkText(direction: WalletChargeDirection = 'charge')
export function walletChargeSuccessMessage(direction: WalletChargeDirection = 'charge') { export function walletChargeSuccessMessage(direction: WalletChargeDirection = 'charge') {
return direction === 'discharge' ? 'دشارژ با موفقیت انجام شد' : 'شارژ با موفقیت انجام شد' 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 MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
@@ -82,7 +83,7 @@ function statusClass(row: Giftcardcode) {
} }
function unitLabel(row: Giftcardcode) { function unitLabel(row: Giftcardcode) {
return row.unit === 2 ? 'طلایی' : 'ریالی' return row.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'
} }
function unitTagClass(row: Giftcardcode) { function unitTagClass(row: Giftcardcode) {
@@ -19,6 +19,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Giftcardcode } from '@/model/giftCard' import type { Giftcardcode } from '@/model/giftCard'
import MobileCard from '@/components/mobile/MobileCard.vue' import MobileCard from '@/components/mobile/MobileCard.vue'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{ const props = defineProps<{
item: Giftcardcode 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(() => const unitTagClass = computed(() =>
props.item.unit === 2 props.item.unit === 2
? 'border-amber-300 bg-amber-50 text-amber-700' ? 'border-amber-300 bg-amber-50 text-amber-700'
@@ -28,7 +28,7 @@
<CustomTag v-else textColor="#C53030" bgColor="#FECACA">غیرفعال</CustomTag> <CustomTag v-else textColor="#C53030" bgColor="#FECACA">غیرفعال</CustomTag>
</template> </template>
<template v-else-if="column.key === 'unit'"> <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> <CustomTag v-else textColor="#25855A" bgColor="#E3F4E9">ریالی</CustomTag>
</template> </template>
<template v-else-if="column.key === 'amount'"> <template v-else-if="column.key === 'amount'">
@@ -68,6 +68,7 @@ import BasicActionSheet from '@/components/BasicActionSheet.vue'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet' import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import GiftCardCodeMobileDetailSheet from './GiftCardCodeMobileDetailSheet.vue' import GiftCardCodeMobileDetailSheet from './GiftCardCodeMobileDetailSheet.vue'
import GiftCardCodeMobileListItem from './GiftCardCodeMobileListItem.vue' import GiftCardCodeMobileListItem from './GiftCardCodeMobileListItem.vue'
@@ -12,7 +12,7 @@
<a-form-item label="نوع کارت هدیه" name="unit" :rules="inputRules()"> <a-form-item label="نوع کارت هدیه" name="unit" :rules="inputRules()">
<a-radio-group v-model:value="form.unit" button-style="solid"> <a-radio-group v-model:value="form.unit" button-style="solid">
<a-radio-button value="1">ریالی</a-radio-button> <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-radio-group>
</a-form-item> </a-form-item>
@@ -48,6 +48,7 @@
import type { FormInstance } from 'ant-design-vue' import type { FormInstance } from 'ant-design-vue'
import type { GiftcardInput } from '@/model/giftCard' import type { GiftcardInput } from '@/model/giftCard'
import { inputLenRules, inputRules, inputRulesNumber } from '@/utils/inputRules' import { inputLenRules, inputRules, inputRulesNumber } from '@/utils/inputRules'
import { metalLabel } from '@/utils/adminMetalLabels'
defineProps<{ defineProps<{
form: GiftcardInput form: GiftcardInput
@@ -63,6 +63,7 @@ import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime' import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
type GiftCardRow = Giftcard & { customer?: { name?: string; mobile?: string } } 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 ?? '-'}`) const sheetTitle = computed(() => props.item?.unique_code || `کارت هدیه #${props.item?.id ?? '-'}`)
function unitLabel(row: GiftCardRow) { function unitLabel(row: GiftCardRow) {
return row.unit === 2 ? 'طلایی' : 'ریالی' return row.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'
} }
function unitTagClass(row: GiftCardRow) { function unitTagClass(row: GiftCardRow) {
@@ -31,6 +31,7 @@
import type { Giftcard } from '@/model/giftCard' import type { Giftcard } from '@/model/giftCard'
import MobileCard from '@/components/mobile/MobileCard.vue' import MobileCard from '@/components/mobile/MobileCard.vue'
import MobileIconButton from '@/components/mobile/MobileIconButton.vue' import MobileIconButton from '@/components/mobile/MobileIconButton.vue'
import { metalLabel } from '@/utils/adminMetalLabels'
type GiftCardRow = Giftcard & { customer?: { name?: string; mobile?: string } } type GiftCardRow = Giftcard & { customer?: { name?: string; mobile?: string } }
@@ -44,7 +45,7 @@ defineEmits<{
(e: 'giftCardCodes', item: GiftCardRow): void (e: 'giftCardCodes', item: GiftCardRow): void
}>() }>()
const unitLabel = computed(() => (props.item.unit === 2 ? 'طلایی' : 'ریالی')) const unitLabel = computed(() => (props.item.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'))
const unitTagClass = computed(() => const unitTagClass = computed(() =>
props.item.unit === 2 props.item.unit === 2
? 'border-amber-300 bg-amber-50 text-amber-700' ? 'border-amber-300 bg-amber-50 text-amber-700'
@@ -59,7 +59,7 @@
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable' import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue' 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 { usePrefix } from '@core/composable/usePrefix'
import { getGiftCardTable } from '@/api/giftCard' import { getGiftCardTable } from '@/api/giftCard'
import GiftCardMobileCardList from './GiftCardMobileCardList.vue' import GiftCardMobileCardList from './GiftCardMobileCardList.vue'
@@ -79,7 +79,7 @@ const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const { prefixCls } = usePrefix('pophoverstatus') const { prefixCls } = usePrefix('pophoverstatus')
const tblProps = ref<any>({ const tblProps = ref<any>({
columns: cols, columns: getGiftCardColumns(),
api: (params: TableInput) => { api: (params: TableInput) => {
if (props.customerId) { if (props.customerId) {
params.filters = [] params.filters = []
@@ -2,6 +2,7 @@ import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime' import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue' /*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect' 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: 'ردیف', title: 'ردیف',
dataIndex: 'id', dataIndex: 'id',
@@ -50,7 +52,7 @@ export const cols: BaseTableColumn[] = [
return ( return (
<div class="flex justify-center"> <div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA"> <CustomTag textColor="#C53030" bgColor="#FECACA">
طلایی {metalLabel.goldWalletShort()}
</CustomTag> </CustomTag>
</div> </div>
) )
@@ -236,4 +238,5 @@ export const cols: BaseTableColumn[] = [
fixed: 'right', fixed: 'right',
filterable: false filterable: false
} }
] ]
}
+2 -1
View File
@@ -106,6 +106,7 @@ import type { Giftcard, Giftcardcode, GiftcardInput } from '@/model/giftCard'
import { createGiftCard } from '@/api/giftCard' import { createGiftCard } from '@/api/giftCard'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { isStoreApp } from '@/utils/config' import { isStoreApp } from '@/utils/config'
import { inputRules, inputRulesNumber } from '@/utils/inputRules' import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import BasicInput from '@/components/BasicInput.vue' import BasicInput from '@/components/BasicInput.vue'
@@ -174,7 +175,7 @@ function info(val: Giftcard) {
}, },
{ {
key: 'نوع کارت هدیه', key: 'نوع کارت هدیه',
value: val.unit === 3 ? 'تخفیف' : val.unit === 2 ? 'طلایی' : 'ریالی' value: val.unit === 3 ? 'تخفیف' : val.unit === 2 ? metalLabel.goldWalletShort() : 'ریالی'
}, },
{ {
key: 'تعداد کدها', key: 'تعداد کدها',
@@ -2,7 +2,7 @@
<a-form :model="form" ref="formRef" autocomplete="off"> <a-form :model="form" ref="formRef" autocomplete="off">
<div class="space-y-3"> <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"> <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> <span class="text-sm font-700 text-gray-900">{{ marketPriceLabel }}</span>
</div> </div>
@@ -48,7 +48,7 @@
@change="$emit('changeUser', $event)" @change="$emit('changeUser', $event)"
/> />
</a-form-item> </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 ?? '_'" /> <BasicInfoItem is-line title="کیف پول ریالی" :value="selectedCustomer?.wallet?.[0]?.cash ?? '_'" />
</div> </div>
@@ -71,7 +71,7 @@
addon-before="گرم" addon-before="گرم"
addon-bg-color="#0A1B37" addon-bg-color="#0A1B37"
addon-text-color="white" addon-text-color="white"
place-holder="مقدار طلا" :place-holder="metalLabel.goldPlaceholder()"
type="tel" type="tel"
@onchange="$emit('calculateCash', $event)" @onchange="$emit('calculateCash', $event)"
/> />
@@ -88,6 +88,7 @@ import type { Order } from '@/model/order'
import type { last } from '@/model/price' import type { last } from '@/model/price'
import { inputRules, inputRulesNumber } from '@/utils/inputRules' import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{ const props = defineProps<{
form: Order form: Order
@@ -26,23 +26,18 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" /> <BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template> </template>
</MobileStatCard> </MobileStatCard>
<MobileStatCard bordered variant="warning" label="مقدار (گرم)" :value="`${row.amount ?? 0} گرم`"> <MobileStatCard bordered variant="warning" label="مقدار" :value="amountLabel(row)" />
<template #prefix> <MobileStatCard bordered label="قیمت زمان معامله (ریال)" :value="usePrice(row.price ?? 0, true, '')" />
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
</template>
</MobileStatCard>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<MobileStatCard bordered label="قیمت هر گرم طلا برای مشتری (ریال)" :value="usePrice(row.price ?? 0, true, '')" /> <MobileStatCard bordered label="کارمزد" :value="serviceChargeLabel(row)" />
<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="usePrice(row.total_price_original ?? 0, true, '')" /> <MobileStatCard bordered label="قیمت کل بدون سود (ریال)" :value="usePrice(row.total_price_original ?? 0, true, '')" />
<MobileStatCard bordered label="سود هر گرم (ریال)" :value="usePrice(row.fee ?? 0, true, '')" /> <MobileStatCard bordered label="سود واحد (ریال)" :value="usePrice(row.fee ?? 0, true, '')" />
<MobileStatCard bordered label="کارمزد طلایی" :value="serviceChargeLabel(row)" />
</div> </div>
<MobileInfoGrid> <MobileInfoGrid>
<MobileDetailRow label="نوع فلز" :value="row.order_metal_label ?? '-'" />
<MobileDetailRow label="کد سفارش" :value="row.id ?? '-'" /> <MobileDetailRow label="کد سفارش" :value="row.id ?? '-'" />
<MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile || '-'" /> <MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile || '-'" />
<MobileDetailRow label="سود کل (ریال)" :value="usePrice(row.total_fee ?? 0, true, '')" /> <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 { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit'
type OrderRow = Order & { sts_str?: string; service_charge?: number | string } 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) : '-' 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) { function serviceChargeLabel(row: OrderRow) {
if (row.service_charge == null || row.service_charge === '') return '-' if (row.service_charge == null || row.service_charge === '') return '-'
const value = Number(row.service_charge) const value = Number(row.service_charge)
if (Number.isNaN(value)) return `${row.service_charge} گرم` if (Number.isNaN(value)) return String(row.service_charge)
return `${parseFloat(value.toFixed(5))} گرم` return formatAmountWithUnit(value, row.amount_unit)
} }
</script> </script>
@@ -6,6 +6,12 @@
<span class="shrink-0 rounded-md border px-1.5 py-0.5 text-xs font-600" :class="directionClass"> <span class="shrink-0 rounded-md border px-1.5 py-0.5 text-xs font-600" :class="directionClass">
{{ directionLabel }} {{ directionLabel }}
</span> </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> <span class="truncate text-sm font-700 text-gray-900">{{ customerName }}</span>
</div> </div>
<span class="shrink-0 text-xs font-600" :class="statusValueClass">{{ statusLabel }}</span> <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"> <div class="flex items-center justify-between gap-2 text-xs text-gray-600">
<span> <span>
<span class="text-gray-500">طلا:</span> <span class="text-gray-500">مقدار:</span>
<span class="font-600 text-gray-900">{{ amountLabel }}</span> <span class="font-600 text-gray-900">{{ amountLabel }}</span>
</span> </span>
<span class="font-700 text-gray-900">{{ totalPriceLabel }}</span> <span class="font-700 text-gray-900">{{ totalPriceLabel }}</span>
@@ -26,6 +32,7 @@
import type { Order } from '@/model/order' import type { Order } from '@/model/order'
import MobileCard from '@/components/mobile/MobileCard.vue' import MobileCard from '@/components/mobile/MobileCard.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit'
const props = defineProps<{ const props = defineProps<{
item: Order & { sts_str?: string; service_charge?: number } item: Order & { sts_str?: string; service_charge?: number }
@@ -44,9 +51,8 @@ const directionClass = computed(() =>
const customerName = computed(() => props.item.customer?.name || '-') const customerName = computed(() => props.item.customer?.name || '-')
const amountLabel = computed(() => { const amountLabel = computed(() => {
const amount = props.item.amount const amount = props.item.amount_display ?? props.item.amount
if (amount == null) return '-' return formatAmountWithUnit(amount, props.item.amount_unit)
return `${Number(amount.toFixed(3))} گرم`
}) })
const totalPriceLabel = computed(() => usePrice(props.item.total_price ?? 0, true, '')) const totalPriceLabel = computed(() => usePrice(props.item.total_price ?? 0, true, ''))
const statusLabel = computed(() => props.item.sts_str || '-') 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 useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeCustomerActivation, getCustomerTable } from '@/api/customer' import { changeCustomerActivation, getCustomerTable } from '@/api/customer'
import { cols } from '../hooks/orderColumns' import { buildOrderColumns } from '../hooks/orderColumns'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { usePrefix } from '@core/composable/usePrefix' import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@@ -74,22 +74,22 @@ function checkPermission(temp: string) {
const table = ref(null) const table = ref(null)
const tblProps = ref<any>({ const tblProps = ref<any>({
columns: cols, columns: buildOrderColumns(),
api: (params: TableInput) => { api: async (params: TableInput) => {
if (props.customerId) { if (props.customerId) {
params.filters = [] params.filters = []
params.filters?.push({ params.filters?.push({
field: "customer_id", field: 'customer_id',
type: "text", type: 'text',
value: props.customerId, value: props.customerId,
cr: "and", cr: 'and',
op: "eq", op: 'eq',
hidden: false hidden: false
}) })
} }
return getOrderTable(params) const data = await getOrderTable(params)
return data
} }
//bordered: true
}) })
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
+168 -229
View File
@@ -1,239 +1,178 @@
import type { BaseTableColumn } from '@core/type/baseTable' 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 CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
/*import BudgetSelect from '@/components/BudgetSelect.vue' import { formatAmountWithUnit } from '@/utils/weightUnit'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
//define store export type OrderMetalColumn = { metal_id: number; label: string; display_unit?: string }
//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 const cols: BaseTableColumn[] = [ export function buildOrderColumns(_metals: OrderMetalColumn[] = []): BaseTableColumn[] {
{ return [
title: 'ردیف', {
dataIndex: 'id', title: 'ردیف',
key: 'id', dataIndex: 'id',
filterable: false, key: 'id',
sortable: false, filterable: false,
customRender: (record: any) => { sortable: false,
return record.index + 1 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>
)
} }
} },
}, {
{ title: 'کد سفارش',
title: 'نام و نام خانوادگی', dataIndex: 'id',
dataIndex: 'customer.name', key: 'id',
key: 'customer.name', type: 'Number'
type: 'Text' },
}, {
{ title: 'نوع سفارش',
title: 'شماره موبایل', dataIndex: 'direction',
dataIndex: 'customer.mobile', key: 'direction',
key: 'customer.mobile', filterable: false,
type: 'Number' customRender: ({ record }) => {
}, switch (record.direction) {
{ case 2:
title: 'مقدار (گرم)', return (
dataIndex: 'amount', <div class="flex justify-center">
key: 'amount', <CustomTag textColor="#C53030" bgColor="#FECACA">
type: 'Text' فروش
}, </CustomTag>
/*{ </div>
title: 'کارمزد ریالی', )
dataIndex: 'service_charge_price', case 1:
key: 'service_charge_price', return (
type: 'Number', <div class="flex justify-center">
customRender(data) { <CustomTag textColor="#25855A" bgColor="#E3F4E9">
return usePrice(data.record?.service_charge_price ?? 0, true, '') خرید
} </CustomTag>
// className: 'column-money' </div>
},*/ )
{ }
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: 'نام و نام خانوادگی',
title: 'تاریخ ثبت', dataIndex: 'customer.name',
dataIndex: 'created_at', key: 'customer.name',
key: 'created_at', type: 'Text'
type: 'DateTime', },
customRender: (record: any) => { {
return toJalali(record.record.created_at) title: 'شماره موبایل',
} dataIndex: 'customer.mobile',
}, key: 'customer.mobile',
/*{ type: 'Number'
title: 'نقش', },
dataIndex: 'user_type_record', {
key: 'user_type_record', title: 'نوع فلز',
type: 'Select', dataIndex: 'order_metal_label',
filterable: false, key: 'order_metal_label',
sortable: false, type: 'Text',
filterComponentProps: { filterable: false,
multiple: true customRender({ record }) {
}, return (
filterField: 'user_type_obj.id', <div class="flex justify-center">
filterComponent: ReadyUserTypeSelect, <CustomTag textColor="#ca8a04" bgColor="#fef3c7" class={'font-bold px-6'}>
/!* customRender: (record: any) => { {record.order_metal_label ?? '-'}
return record.record.user_type_record?.value
}*!/
customRender: ({ record }) => {
const items = record.is_admin ? (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ادمین
</CustomTag> </CustomTag>
) : ( </div>
record.user_type_obj.map((item: { title: any }, index: any) => { )
if (item) { }
return ( },
<CustomTag textColor="#25855A" bgColor="#E3F4E9"> {
{item?.title} title: 'مقدار',
</CustomTag> dataIndex: 'amount',
) key: 'amount',
} type: 'Text',
}) customRender({ record }) {
) const amount = record.amount_display ?? record.amount
return <div class="flex justify-center items-center gap-2">{items}</div> 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 '-'
} }
}, return formatAmountWithUnit(Number(value), record.amount_unit)
{ }
title: 'حوزه', },
dataIndex: 'section', {
key: 'section' title: 'قیمت زمان معامله (ریال)',
}, dataIndex: 'price',
{ key: 'price',
title: 'شماره موبایل', type: 'Text',
dataIndex: 'mobile', customRender({ record }) {
type: 'Number', return usePrice(record.price ?? 0, true, '')
key: 'mobile' }
}, },
{ {
title: 'تاریخ ثبت', title: 'قیمت کل برای مشتری (ریال)',
dataIndex: 'created_at', dataIndex: 'total_price',
key: 'created_at', key: 'total_price',
type: 'DateTime', type: 'Text',
customRender: (record: any) => { customRender(data) {
return toJalaliNoTime(record.record.created_at) 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', title: 'تاریخ ثبت',
key: 'action', dataIndex: 'created_at',
slot: 'action', key: 'created_at',
fixed: 'right', type: 'DateTime',
filterable: false 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 type { last } from '@/model/price'
import { createOrder } from '@/api/order' import { createOrder } from '@/api/order'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { formatAmountWithUnit } from '@/utils/weightUnit'
//loading //loading
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
@@ -182,11 +184,15 @@ function info(val: Order) {
infoParams.itemsTitle = 'اطلاعات کاربری' infoParams.itemsTitle = 'اطلاعات کاربری'
infoParams.items = [ infoParams.items = [
{ {
key: 'مقدار (گرم)', key: 'مقدار',
value: val.amount ?? '-' value: formatAmountWithUnit(val.amount_display ?? val.amount, val.amount_unit)
}, },
{ {
key: 'قیمت هر گرم طلا برای مشتری (ریال)', key: 'نوع فلز',
value: val.order_metal_label ?? '-'
},
{
key: 'قیمت زمان معامله (ریال)',
value: usePrice(val.price ?? 0) ?? '-' value: usePrice(val.price ?? 0) ?? '-'
}, },
{ {
@@ -194,7 +200,7 @@ function info(val: Order) {
value: usePrice(val.total_price ?? 0) ?? '-' value: usePrice(val.total_price ?? 0) ?? '-'
}, },
{ {
key: 'قیمت هر گرم طلا بدون سود (ریال)', key: metalLabel.goldGramPriceOriginal(),
value: usePrice(val.price_original ?? 0) ?? '-' value: usePrice(val.price_original ?? 0) ?? '-'
}, },
{ {
@@ -226,8 +232,11 @@ function info(val: Order) {
value: usePrice(val.service_charge_price ?? 0) value: usePrice(val.service_charge_price ?? 0)
},*/ },*/
{ {
key: 'کارمزد طلایی', key: 'کارمزد',
value: val.service_charge ?? '-' + ' گرم' value:
val.service_charge != null && val.service_charge !== ''
? formatAmountWithUnit(Number(val.service_charge), val.amount_unit)
: '-'
}, },
/* { /* {
key: 'تاریخ تولد', key: 'تاریخ تولد',
@@ -4,7 +4,7 @@
<MobileSheetHeader :title="sheetTitle" :subtitle="dateTimeLabel(row)" :show-title="isMobile" /> <MobileSheetHeader :title="sheetTitle" :subtitle="dateTimeLabel(row)" :show-title="isMobile" />
<div class="space-y-2"> <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> <template #prefix>
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" /> <BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template> </template>
@@ -32,6 +32,7 @@ import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { toJalaliNoTime } from '@core/utils/useDataTime' import { toJalaliNoTime } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
@@ -2,6 +2,7 @@ import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime' import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue' /*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect' 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: 'ردیف', title: 'ردیف',
dataIndex: 'id', dataIndex: 'id',
@@ -53,7 +55,7 @@ export const cols: BaseTableColumn[] = [
type: 'Text' type: 'Text'
}, },
{ {
title: 'قیمت هر گرم طلا برای مشتری (ریال)', title: metalLabel.goldGramPriceCustomer(),
dataIndex: 'price', dataIndex: 'price',
key: 'price', key: 'price',
type: 'Text', type: 'Text',
@@ -157,4 +159,5 @@ export const cols: BaseTableColumn[] = [
fixed: 'right', fixed: 'right',
filterable: false filterable: false
}*/ }*/
] ]
}
@@ -118,9 +118,9 @@
> >
<BasicInfoItem <BasicInfoItem
title="نام قلک" 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> </div>
</div> </div>
@@ -162,6 +162,7 @@ import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import type { Charge } from '@/model/wallet' import type { Charge } from '@/model/wallet'
import customers from '@/router/routes/customers' import customers from '@/router/routes/customers'
import { chargeWallet } from '@/api/wallet' import { chargeWallet } from '@/api/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
//loading //loading
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
+304 -49
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="flex h-full min-h-0 flex-col gap-4 p-4"> <div class="category-page flex h-full min-h-0 flex-col gap-4 p-4">
<div class="flex flex-wrap items-center gap-2"> <div class="flex shrink-0 flex-wrap items-center gap-2">
<BaseButton :loading="syncing" @click="handleSync"> <BaseButton :loading="syncing" @click="handleSync">
همگام‌سازی از محصولات همگام‌سازی از محصولات
</BaseButton> </BaseButton>
@@ -9,47 +9,58 @@
</span> </span>
</div> </div>
<a-spin :spinning="loading"> <div
<a-table ref="tableWrapRef"
:columns="columns" class="category-page__table min-h-0 flex-1 overflow-hidden"
:data-source="items" :class="{ 'category-page__table--dragging': dragFromId !== null }"
:pagination="false" >
row-key="id" <a-spin :spinning="loading" class="category-page__spin">
size="middle" <a-table
class="min-h-0 flex-1" :columns="columns"
> :data-source="items"
<template #bodyCell="{ column, record }"> :pagination="false"
<template v-if="column.key === 'active'"> :scroll="tableScrollY ? { y: tableScrollY } : undefined"
<a-switch row-key="id"
:checked="asBool(record.active)" size="middle"
checked-children="فعال" >
un-checked-children="مخفی" <template #bodyCell="{ column, record }">
@change="(checked: boolean) => toggleField(record, 'active', checked)" <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>
<template v-else-if="column.key === 'show_in_select'"> </a-table>
<a-switch </a-spin>
:checked="asBool(record.show_in_select)" </div>
@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>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { TableColumnType } from 'ant-design-vue' import type { TableColumnType } from 'ant-design-vue'
import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import { getCategories, syncCategoriesFromProducts, updateCategory } from '@/api/category' import { getCategories, syncCategoriesFromProducts, updateCategory } from '@/api/category'
@@ -57,7 +68,17 @@ import type { Category } from '@/model/category'
const loading = ref(false) const loading = ref(false)
const syncing = ref(false) const syncing = ref(false)
const savingOrder = ref(false)
const items = ref<Category[]>([]) 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' 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 let syncInFlight: Promise<void> | null = null
const columns: TableColumnType<Category>[] = [ const columns: TableColumnType<Category>[] = [
{
title: '',
key: 'drag',
width: 44,
align: 'center',
},
{ title: 'نام دسته', dataIndex: 'name', key: 'name' }, { title: 'نام دسته', dataIndex: 'name', key: 'name' },
{ {
title: 'نمایش محصولات در اپ', title: 'نمایش محصولات در اپ',
key: 'active', key: 'active',
width: 160, width: 160,
align: 'center' align: 'center',
}, },
{ {
title: 'سلکت افزودن محصول', title: 'سلکت افزودن محصول',
key: 'show_in_select', key: 'show_in_select',
width: 150, width: 150,
align: 'center' align: 'center',
}, },
{ {
title: 'ترتیب', title: 'ترتیب',
key: 'sort_order', key: 'sort_order',
width: 90, width: 72,
align: 'center' align: 'center',
} },
] ]
function updateTableScrollY() {
const height = tableWrapRef.value?.clientHeight ?? 0
tableScrollY.value = height > 120 ? height - 56 : undefined
}
useResizeObserver(tableWrapRef, () => {
updateTableScrollY()
})
/** پیش‌فرض: روشن — فقط صفر یا false یعنی خاموش */ /** پیش‌فرض: روشن — فقط صفر یا false یعنی خاموش */
function asBool(value: boolean | number | undefined | null) { function asBool(value: boolean | number | undefined | null) {
return value !== false && value !== 0 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() { async function fetchList() {
loading.value = true loading.value = true
try { try {
@@ -101,6 +306,7 @@ async function fetchList() {
message.error('خطا در دریافت دسته‌بندی‌ها') message.error('خطا در دریافت دسته‌بندی‌ها')
} finally { } finally {
loading.value = false loading.value = false
updateTableScrollY()
} }
} }
@@ -129,7 +335,7 @@ async function flushSave(id: number) {
const snapshot = { const snapshot = {
active: record.active, active: record.active,
show_in_select: record.show_in_select, show_in_select: record.show_in_select,
sort_order: record.sort_order sort_order: record.sort_order,
} }
try { try {
@@ -154,12 +360,6 @@ function toggleField(
scheduleSave(record, { [field]: next }) 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) { async function syncList(showMessage = false) {
if (syncInFlight) { if (syncInFlight) {
return syncInFlight return syncInFlight
@@ -183,6 +383,7 @@ async function syncList(showMessage = false) {
} finally { } finally {
syncing.value = false syncing.value = false
syncInFlight = null syncInFlight = null
updateTableScrollY()
} }
})() })()
@@ -194,12 +395,66 @@ async function handleSync() {
} }
onMounted(() => { onMounted(() => {
void syncList() void fetchList()
updateTableScrollY()
}) })
onUnmounted(() => { onUnmounted(() => {
cleanupPointerDrag()
debouncedFlush.forEach((fn) => fn.cancel()) debouncedFlush.forEach((fn) => fn.cancel())
debouncedFlush.clear() debouncedFlush.clear()
pendingPatches.clear() pendingPatches.clear()
}) })
</script> </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> </MobileCardList>
<TransactionMobileDetailSheet v-model:visible="sheetVisible" :item="selectedItem" /> <TransactionMobileDetailSheet
v-model:visible="sheetVisible"
:item="selectedItem"
:metals="catalogMetals"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -32,6 +36,9 @@ import { useMobileTableList } from '@/components/mobile/composables/useMobileTab
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
import TransactionMobileDetailSheet from './TransactionMobileDetailSheet.vue' import TransactionMobileDetailSheet from './TransactionMobileDetailSheet.vue'
import TransactionMobileListItem from './TransactionMobileListItem.vue' import TransactionMobileListItem from './TransactionMobileListItem.vue'
import type { TransactionMetalColumn } from '../hooks/transactionsColumns'
const catalogMetals = ref<TransactionMetalColumn[]>([])
const props = defineProps<{ const props = defineProps<{
customerId?: number customerId?: number
@@ -51,7 +58,10 @@ function fetchTransactions(params: TableInput) {
hidden: false 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>({ const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<Transaction>({
@@ -3,21 +3,29 @@
<template #default="{ item: row }"> <template #default="{ item: row }">
<MobileSheetHeader :title="sheetTitle" :subtitle="dateLabel(row)" :show-title="isMobile" /> <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)"> <span class="rounded-lg border px-2 py-0.5 text-xs font-600" :class="typeTagClass(row)">
{{ typeLabel(row) }} {{ typeLabel(row) }}
</span> </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>
<div class="space-y-2"> <div class="space-y-2">
<MobileStatCard <MobileStatCard
v-for="entry in metalEntries(row)"
:key="entry.metal_id"
bordered bordered
:variant="(row.gold ?? 0) > 0 ? 'success' : 'danger'" :variant="entry.value > 0 ? 'success' : 'danger'"
label="مقدار طلا (گرم)" :label="transactionMetalAmountTitle(entry.metal_id, entry.label)"
:value="goldLabel(row)" :value="entry.display"
> >
<template #prefix> <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> </template>
</MobileStatCard> </MobileStatCard>
<MobileStatCard <MobileStatCard
@@ -33,6 +41,7 @@
</div> </div>
<MobileInfoGrid> <MobileInfoGrid>
<MobileDetailRow label="نوع فلز" :value="isMoneyType(row) ? '-' : (row.transaction_metal_label ?? '-')" />
<MobileDetailRow label="کد تراکنش" :value="row.id ?? '-'" /> <MobileDetailRow label="کد تراکنش" :value="row.id ?? '-'" />
<MobileDetailRow label="نام تراکنش" :value="row.type_str || '-'" /> <MobileDetailRow label="نام تراکنش" :value="row.type_str || '-'" />
<MobileDetailRow label="علت تراکنش" full-width :value="noteLabel(row)" /> <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 { isMobile } from '@core/utils/is'
import { toJalaliNoTime } from '@core/utils/useDataTime' import { toJalaliNoTime } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' 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<{ const props = defineProps<{
visible: boolean visible: boolean
item: Transaction | null item: Transaction | null
metals?: TransactionMetalColumn[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -75,7 +88,7 @@ function isMoneyType(row: Transaction) {
} }
function typeLabel(row: Transaction) { function typeLabel(row: Transaction) {
return isMoneyType(row) ? 'پول' : 'طلا' return isMoneyType(row) ? 'پول' : 'فلز'
} }
function typeTagClass(row: Transaction) { function typeTagClass(row: Transaction) {
@@ -88,14 +101,26 @@ function dateLabel(row: Transaction) {
return row.created_at ? toJalaliNoTime(row.created_at) : '-' 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) { function noteLabel(row: Transaction) {
if (row.note == null) return '-' if (row.note == null) return '-'
return String(row.note) 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> </script>
@@ -6,6 +6,12 @@
<span class="shrink-0 rounded-md border px-1.5 py-0.5 text-xs font-600" :class="typeTagClass"> <span class="shrink-0 rounded-md border px-1.5 py-0.5 text-xs font-600" :class="typeTagClass">
{{ typeLabel }} {{ typeLabel }}
</span> </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> <span class="truncate text-sm font-700 text-gray-900">{{ item.type_str || '-' }}</span>
</div> </div>
<span class="shrink-0 text-xs text-gray-400">{{ dateLabel }}</span> <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 isMoneyType = computed(() => props.item.type === 4 || props.item.type === 3)
const typeLabel = computed(() => (isMoneyType.value ? 'پول' : 'طلا')) const typeLabel = computed(() => (isMoneyType.value ? 'پول' : 'فلز'))
const typeTagClass = computed(() => const typeTagClass = computed(() =>
isMoneyType.value isMoneyType.value
? 'border-green-300 bg-green-50 text-green-700' ? '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 useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import {changeCustomerActivation, getCustomerTable} from '@/api/customer' import {changeCustomerActivation, getCustomerTable} from '@/api/customer'
import {cols} from '../hooks/transactionsColumns' import { buildTransactionsColumns } from '../hooks/transactionsColumns'
import {useUserStore} from '@/store/userStore' import {useUserStore} from '@/store/userStore'
import {usePrefix} from '@core/composable/usePrefix' import {usePrefix} from '@core/composable/usePrefix'
import {message} from 'ant-design-vue' import {message} from 'ant-design-vue'
@@ -102,23 +102,24 @@ const {prefixCls} = usePrefix('pophoverstatus')
const table = ref(null) const table = ref(null)
const tblProps = ref<any>({ const tblProps = ref<any>({
columns: cols, columns: buildTransactionsColumns(),
api: (params: TableInput) => { api: async (params: TableInput) => {
// console.log(props.customerId)
if (props.customerId) { if (props.customerId) {
params.filters = [] params.filters = []
params.filters?.push({ params.filters?.push({
field: "customer_id", field: 'customer_id',
type: "text", type: 'text',
value: props.customerId, value: props.customerId,
cr: "and", cr: 'and',
op: "eq", op: 'eq',
hidden: false 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) const loading = ref<boolean>(false)
/*async function changeCustomerActivationFunction(id: number) { /*async function changeCustomerActivationFunction(id: number) {
@@ -1,203 +1,165 @@
import type {BaseTableColumn} from '@core/type/baseTable' import type { BaseTableColumn } from '@core/type/baseTable'
import {toJalaliNoTime, toTime} from '@core/utils/useDataTime' import { toJalaliNoTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
import {usePrice} from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
/*import BudgetSelect from '@/components/BudgetSelect.vue' import { metalLabel, transactionMetalAmountTitle } from '@/utils/adminMetalLabels'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { formatMetalDisplayValuePlain, metalCellValue } from '@/utils/metalWeight'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
//define store export type TransactionMetalColumn = { metal_id: number; label: string; display_unit?: string }
//const store = useStaticApiListsStore()
//document types function renderSignedMetalCell(value: number, metalId: number) {
/*let docTypes: DocItem[]*/ if (!value) {
/*onMounted(async () => { return (
docTypes = await store.readyDocTypeList() <div class="flex justify-center">
}) <div class="px-6">0</div>
const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => { </div>
return { )
label: x.title,
value: x.id
} }
})*/
export const cols: BaseTableColumn[] = [ const cls = value > 0 ? 'text-green' : 'text-red'
{ const formatted = formatMetalDisplayValuePlain(Math.abs(value), metalId)
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',
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: 'نوع تراکنش', title: 'کد تراکنش',
dataIndex: 'active', dataIndex: 'id',
key: 'active', key: 'id',
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',
type: 'Number' type: 'Number'
},*/ },
/* {
title: 'نام فروشگاه',
dataIndex: 'shop_name',
key: 'shop_name',
type: 'Text'
},*/
{ {
title: 'تاریخ تراکنش', title: 'نام تراکنش',
dataIndex: 'created_at', dataIndex: 'type_str',
key: 'created_at', key: 'type_str',
type: 'DateTime', type: 'Text'
customRender: (record: any) => { },
return toJalaliNoTime(record.record.created_at) {
} title: 'علت تراکنش',
} dataIndex: 'note',
/*{ key: 'note',
title: 'نقش', type: 'Text'
dataIndex: 'user_type_record', },
key: 'user_type_record', {
type: 'Select', title: 'نوع تراکنش',
filterable: false, dataIndex: 'active',
sortable: false, key: 'active',
filterComponentProps: { filterable: false,
multiple: true sortable: false,
}, customRender: ({ record }) => {
filterField: 'user_type_obj.id', return isMoneyTransaction(record) ? (
filterComponent: ReadyUserTypeSelect, <div class="flex justify-center">
/!* customRender: (record: any) => { <CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
return record.record.user_type_record?.value پول
}*!/
customRender: ({ record }) => {
const items = record.is_admin ? (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ادمین
</CustomTag> </CustomTag>
) : ( </div>
record.user_type_obj.map((item: { title: any }, index: any) => { ) : (
if (item) { <div class="flex justify-center">
return ( <CustomTag textColor="#ca8a04" bgColor="#fef3c7" class={'font-bold px-6'}>
<CustomTag textColor="#25855A" bgColor="#E3F4E9"> فلز
{item?.title} </CustomTag>
</CustomTag> </div>
) )
} }
}) },
) {
return <div class="flex justify-center items-center gap-2">{items}</div> title: 'نوع فلز',
dataIndex: 'transaction_metal_label',
key: 'transaction_metal_label',
filterable: false,
sortable: false,
customRender: ({ record }) => {
if (isMoneyTransaction(record)) {
return '-'
} }
}, return (
{ <div class="flex justify-center">
title: 'حوزه', <CustomTag textColor="#ca8a04" bgColor="#fef3c7" class={'font-bold px-6'}>
dataIndex: 'section', {record.transaction_metal_label ?? metalLabel.goldUnit()}
key: 'section' </CustomTag>
}, </div>
{ )
title: 'شماره موبایل', }
dataIndex: 'mobile', },
type: 'Number', {
key: 'mobile' title: 'نام مشتری',
},*/ dataIndex: 'customer.name',
/* { key: 'customer.name',
title: 'عملیات', type: 'Text'
dataIndex: 'action', }
key: 'action', ]
slot: 'action',
fixed: 'right', const metalColumns: BaseTableColumn[] =
filterable: false 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 type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer' import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import type { Transaction } from '@/model/transaction' import type { Transaction } from '@/model/transaction'
import { metalLabel } from '@/utils/adminMetalLabels'
//loading //loading
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
@@ -127,7 +128,7 @@ let infoParams = reactive<Info>({})
function info(val: Transaction) { function info(val: Transaction) {
infoParams.name = val.customer.name 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.date = toJalaliNoTime(val.created_at)
infoParams.time = toTime(val.created_at) infoParams.time = toTime(val.created_at)
infoParams.itemsTitle = 'اطلاعات کاربری' infoParams.itemsTitle = 'اطلاعات کاربری'
@@ -15,6 +15,7 @@
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
:item="item" :item="item"
:metals="catalogMetals"
@open="openSheet" @open="openSheet"
/> />
</MobileCardList> </MobileCardList>
@@ -22,36 +23,39 @@
<WalletMobileDetailSheet <WalletMobileDetailSheet
v-model:visible="sheetVisible" v-model:visible="sheetVisible"
:item="selectedItem" :item="selectedItem"
:metals="catalogMetals"
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getWalletTable } from '@/api/wallet' 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 MobileCardList from '@/components/mobile/MobileCardList.vue'
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet' import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList' import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
import type { TableInput, TableOutput } from '@core/type/baseTable' import type { TableInput, TableOutput } from '@core/type/baseTable'
import type { WalletMetalColumn } from '../hooks/walletColumns'
import WalletMobileDetailSheet from './WalletMobileDetailSheet.vue' import WalletMobileDetailSheet from './WalletMobileDetailSheet.vue'
import WalletMobileListItem from './WalletMobileListItem.vue' import WalletMobileListItem from './WalletMobileListItem.vue'
type WalletTableResponse = TableOutput<Wallet> & { type WalletTableResponse = TableOutput<Wallet> & {
sums?: { sums?: WalletTableSums
cash?: number
gold?: number
cashToGold?: number
goldToCash?: number
}
} }
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'sums', sums: WalletTableResponse['sums']): void (e: 'sums', sums: WalletTableResponse['sums']): void
}>() }>()
const catalogMetals = ref<WalletMetalColumn[]>([])
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Wallet>() const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Wallet>()
async function fetchWallet(params: TableInput) { async function fetchWallet(params: TableInput) {
const data = (await getWalletTable(params)) as WalletTableResponse 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) emit('sums', data?.sums)
return data return data
} }
@@ -9,7 +9,15 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" /> <BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template> </template>
</MobileStatCard> </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> <template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" /> <BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
</template> </template>
@@ -34,10 +42,14 @@ import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
import MobileStatCard from '@/components/mobile/MobileStatCard.vue' import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { usePrice } from '@core/utils/usePrice' 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<{ const props = defineProps<{
visible: boolean visible: boolean
item: Wallet | null item: Wallet | null
metals?: WalletMetalColumn[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -51,9 +63,20 @@ const visibleProxy = computed({
const sheetTitle = computed(() => props.item?.customer?.name || `کیف پول #${props.item?.id ?? '-'}`) const sheetTitle = computed(() => props.item?.customer?.name || `کیف پول #${props.item?.id ?? '-'}`)
function goldLabel(row: Wallet) { function metalEntries(row: Wallet) {
const gold = row.gold const metals = props.metals?.length
if (gold == null) return '-' ? props.metals
return `${Number(Number(gold).toFixed(3))} گرم` : [{ 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> </script>
@@ -11,9 +11,19 @@
<span class="text-gray-500">ریال:</span> <span class="text-gray-500">ریال:</span>
<span class="font-600 text-gray-900">{{ cashLabel }}</span> <span class="font-600 text-gray-900">{{ cashLabel }}</span>
</span> </span>
<span> <span v-if="metalEntries.length === 1">
<span class="text-gray-500">طلا:</span> <span class="text-gray-500">{{ metalEntries[0].label }}:</span>
<span class="font-700 text-gray-900">{{ goldLabel }}</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> </span>
</div> </div>
</div> </div>
@@ -24,9 +34,12 @@
import type { Wallet } from '@/model/wallet' import type { Wallet } from '@/model/wallet'
import MobileCard from '@/components/mobile/MobileCard.vue' import MobileCard from '@/components/mobile/MobileCard.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import type { WalletMetalColumn } from '../hooks/walletColumns'
const props = defineProps<{ const props = defineProps<{
item: Wallet item: Wallet
metals?: WalletMetalColumn[]
}>() }>()
defineEmits<{ defineEmits<{
@@ -35,9 +48,25 @@ defineEmits<{
const customerName = computed(() => props.item.customer?.name || '-') const customerName = computed(() => props.item.customer?.name || '-')
const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, '')) const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, ''))
const goldLabel = computed(() => {
const gold = props.item.gold function metalValue(metalId: number): number {
if (gold == null) return '-' return metalCellValue(props.item as Record<string, unknown>, metalId)
return `${Number(Number(gold).toFixed(3))} گرم` }
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> </script>
@@ -2,7 +2,7 @@
<div class="h-full min-h-0"> <div class="h-full min-h-0">
<BasicShowContext title="لیست کیف پول"> <BasicShowContext title="لیست کیف پول">
<div class="flex h-full min-h-0 flex-col gap-3"> <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 <div
class="rounded-lg p-2.5 text-sm" class="rounded-lg p-2.5 text-sm"
:class="walletSumTone(sums?.cash)" :class="walletSumTone(sums?.cash)"
@@ -23,20 +23,22 @@
</div> </div>
<div <div
v-for="metal in metalBalanceCards"
:key="metal.metal_id"
class="rounded-lg p-2.5 text-sm" 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="grid grid-cols-2 gap-2">
<div class="stat-inner rounded-lg px-2 py-1.5 text-center"> <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"> <div class="mt-1 font-bold">
{{ usePrice(sums?.gold ?? 0, true, 'گرم') }} {{ formatMetalDisplayValue(metal.gold ?? 0, metal.metal_id) }}
</div> </div>
</div> </div>
<div class="stat-inner rounded-lg px-2 py-1.5 text-center"> <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">ریال</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> </div>
</div> </div>
@@ -77,13 +79,16 @@ import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable' import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeCustomerActivation } from '@/api/customer' import { changeCustomerActivation } from '@/api/customer'
import { cols } from '../hooks/walletColumns' import { buildWalletColumns } from '../hooks/walletColumns'
import { usePrefix } from '@core/composable/usePrefix' import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { getWalletTable } from '@/api/wallet' import { getWalletTable } from '@/api/wallet'
import { usePrice } from '../../../../core/utils/usePrice' import { usePrice } from '../../../../core/utils/usePrice'
import WalletMobileCardList from './WalletMobileCardList.vue' import WalletMobileCardList from './WalletMobileCardList.vue'
import { isMobile } from '@core/utils/is' 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 [submit, reload] = useBaseTable()
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null) 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 { prefixCls } = usePrefix('pophoverstatus')
const table = ref(null) 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>({ const tblProps = ref<any>({
columns: cols, columns: buildWalletColumns(),
api: async (params: TableInput) => { api: async (params: TableInput) => {
const data = await getWalletTable(params) const data = await getWalletTable(params)
sums.value = data?.sums 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) emits('sums', data?.sums)
return data return data
} }
@@ -1,157 +1,75 @@
import type { BaseTableColumn } from '@core/type/baseTable' 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 { usePrice } from '@core/utils/usePrice'
/*import BudgetSelect from '@/components/BudgetSelect.vue' import { metalLabel, walletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
//define store export type WalletMetalColumn = { metal_id: number; label: string; display_unit?: string }
//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 const cols: BaseTableColumn[] = [ export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableColumn[] {
{ const baseColumns: BaseTableColumn[] = [
title: 'ردیف', {
dataIndex: 'id', title: 'ردیف',
key: 'id', dataIndex: 'id',
filterable: false, key: 'id',
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',
filterable: false, filterable: false,
sortable: 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) => { customRender: (record: any) => {
return toJalaliNoTime(record.record.created_at) return record.index + 1
} }
},*/ },
/* { {
title: 'عملیات', title: 'کد کیف پول',
dataIndex: 'action', dataIndex: 'id',
key: 'action', key: 'id',
slot: 'action', type: 'Number'
fixed: 'right', },
filterable: false {
}*/ 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> </div>
<div class="gap-5 text-xs"> <div class="gap-5 text-xs">
<div>مجموع موجودی کیف طلا:</div> <div>{{ metalLabel.totalGoldWallet() }}:</div>
<div class="font-bold">{{ usePrice(sums?.gold ?? 0, true, 'گرم') }}</div> <div class="font-bold">{{ usePrice(sums?.gold ?? 0, true, 'گرم') }}</div>
</div> </div>
<div class="gap-5 text-xs"> <div class="gap-5 text-xs">
<div>مجموع موجودی کیف طلا:</div> <div>{{ metalLabel.totalGoldWallet() }}:</div>
<div class="font-bold">{{ usePrice(sums?.goldToCash ?? 0) }}</div> <div class="font-bold">{{ usePrice(sums?.goldToCash ?? 0) }}</div>
</div> </div>
</div>--> </div>-->
<div class="grid grid-cols-3 gap-1 text-xs"> <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="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 <template v-for="metal in mobileMetalRows" :key="`metal-${metal.metal_id}`">
class="text-white rounded p-1" <div class="bg-blue-600 text-white rounded p-1">{{ metal.label }}</div>
:class=" <div
sums?.goldToCash >= 0 class="text-white rounded p-1"
? sums?.goldToCash === 0 :class="sumToneClass(metal.goldToCash)"
? 'bg-amber-600' >
: 'bg-green-600' {{ usePrice(metal.goldToCash ?? 0, true, '') }}
: 'bg-red-600' </div>
" <div
> class="text-white rounded p-1"
{{ usePrice(sums?.goldToCash ?? 0, true, '') }} :class="sumToneClass(metal.gold)"
</div> >
<div {{ formatMetalDisplayValue(metal.gold ?? 0, metal.metal_id) }}
class="text-white rounded p-1" </div>
:class=" </template>
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> <div class="bg-blue-600 text-white rounded p-1">کیف پول</div>
<div <div
class="text-white rounded p-1" class="text-white rounded p-1"
:class=" :class="sumToneClass(sums?.cash)"
sums?.cash >= 0 ? (sums?.cash === 0 ? 'bg-amber-600' : 'bg-green-600') : 'bg-red-600'
"
> >
{{ usePrice(sums?.cash ?? 0, true, '') }} {{ usePrice(sums?.cash ?? 0, true, '') }}
</div> </div>
<div <div
class="text-white rounded p-1" class="text-white rounded p-1"
:class=" :class="sumToneClass(sums?.cashToGold)"
sums?.cashToGold >= 0
? sums?.cashToGold === 0
? 'bg-amber-600'
: 'bg-green-600'
: 'bg-red-600'
"
> >
{{ usePrice(sums?.cashToGold ?? 0, true, '') }} {{ usePrice(sums?.cashToGold ?? 0, true, '') }}
</div> </div>
@@ -165,6 +153,8 @@ import type { Item } from '@/model/Item'
import type { Customer } from '@/model/customer' import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer' import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import type { WalletTableSums } from '@/model/wallet'
import { formatMetalDisplayValue } from '@/utils/metalWeight'
//loading //loading
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
@@ -172,7 +162,38 @@ const loading = ref<boolean>(false)
//define store //define store
const store = useStaticApiListsStore() 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 //define tbl
const table = ref(null) const table = ref(null)
@@ -68,7 +68,7 @@
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#dc2626" /> <BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#dc2626" />
</template> </template>
</MobileStatCard> </MobileStatCard>
<MobileStatCard bordered variant="warning" label="طلا" :value="goldLabel(row)"> <MobileStatCard bordered variant="warning" :label="metalLabel.goldUnit()" :value="goldLabel(row)">
<template #prefix> <template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" /> <BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
</template> </template>
@@ -107,6 +107,7 @@ import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
import { toNumber } from 'lodash' import { toNumber } from 'lodash'
import { useRouter } from 'vue-router' 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 useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeCustomerActivation, getCustomerTable } from '@/api/customer' import { changeCustomerActivation, getCustomerTable } from '@/api/customer'
import { cols } from '../hooks/withdrawColumns' import { getWithdrawColumns } from '../hooks/withdrawColumns'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { usePrefix } from '@core/composable/usePrefix' import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@@ -133,7 +133,7 @@ const confirmButtonLabel = computed(() =>
) )
const tblProps = computed(() => ({ const tblProps = computed(() => ({
columns: cols, columns: getWithdrawColumns(),
api: async (params: TableInput) => { api: async (params: TableInput) => {
const payload = { ...params, flow_type: props.flowType } const payload = { ...params, flow_type: props.flowType }
lastTableParams.value = payload lastTableParams.value = payload
@@ -3,6 +3,7 @@ import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import WithdrawStatus from "@/pages/menuPages/withdraw/components/WithdrawStatus.vue"; import WithdrawStatus from "@/pages/menuPages/withdraw/components/WithdrawStatus.vue";
import { metalLabel } from '@/utils/adminMetalLabels'
/*import BudgetSelect from '@/components/BudgetSelect.vue' /*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect' 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: 'ردیف', title: 'ردیف',
dataIndex: 'id', dataIndex: 'id',
@@ -82,7 +84,7 @@ export const cols: BaseTableColumn[] = [
} }
}, },
{ {
title: 'طلا', title: metalLabel.goldUnit(),
dataIndex: 'gold', dataIndex: 'gold',
key: 'gold', key: 'gold',
type: 'Number', type: 'Number',
@@ -232,4 +234,5 @@ export const cols: BaseTableColumn[] = [
fixed: 'right', fixed: 'right',
filterable: false filterable: false
}*/ }*/
] ]
}
+5 -27
View File
@@ -1,35 +1,13 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
const Orders: RouteRecordRaw = { /** ریدایرکت آدرس قدیمی — منو زیر پیکربندی است */
const Accounting: RouteRecordRaw = {
name: 'accounting', name: 'accounting',
path: '/accounting', path: '/accounting',
component: () => import('@core/layouts/BasicLayout.vue'),
redirect: { name: 'accountingMain' }, redirect: { name: 'accountingMain' },
meta: { meta: {
icon: 'vuesax-linear:calculator', hideMenu: true
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
}
}
]
} }
export default Orders export default Accounting
+11
View File
@@ -136,6 +136,17 @@ const Setting: RouteRecordRaw = {
title: 'مدیران', title: 'مدیران',
order: 9 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?: { export async function syncAndLoadCategories(options?: {
selectOnly?: boolean selectOnly?: boolean
metalHint?: string metalHint?: string
+5
View File
@@ -1,3 +1,5 @@
import { setCachedMainMetal } from '@/utils/metalLabels'
export function getConfigApp(key: string) { export function getConfigApp(key: string) {
const config = window.__APP_CONFIG__ ? window.__APP_CONFIG__.user : {} 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 list = Array.isArray(rows) ? rows : []
const isStoreRow = list.find((item) => item.key === 'is_store') const isStoreRow = list.find((item) => item.key === 'is_store')
const mainWalletRow = list.find((item) => item.key === 'main_wallet_products') 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 storeModeFromSettings = isStoreRow ? isSwitchOn(isStoreRow.value) : false
setCachedMainMetal(mainMetalRow?.value)
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet') const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
setCachedMainWalletProductIds(parseMainWalletProductIds(mainWalletRow?.value)) setCachedMainWalletProductIds(parseMainWalletProductIds(mainWalletRow?.value))
} catch { } catch {
storeModeFromSettings = false storeModeFromSettings = false
setCachedMainMetal(null)
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet') const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
setCachedMainWalletProductIds([]) setCachedMainWalletProductIds([])
} finally { } 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) { export function weightFieldLabelForUnit(unit?: string | null) {
return isKilogramUnit(unit) ? 'وزن به کیلوگرم' : 'وزن به گرم' 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}`
}