fix wallet ui

This commit is contained in:
2026-06-26 22:16:44 +03:30
parent 61c948b6c6
commit 34be7a66dc
6 changed files with 208 additions and 71 deletions
+4
View File
@@ -2,6 +2,7 @@ import useAxios from '@core/utils/useAxios'
import type { AddId, Customer } from '@/model/customer' import type { AddId, Customer } from '@/model/customer'
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
import type {CheckCardRequest, CheckCardResponse} from "@/model/user"; import type {CheckCardRequest, CheckCardResponse} from "@/model/user";
import type { WalletAssetsSummary } from '@/model/walletAssets'
export const createCustomer = async (obj: Customer) => export const createCustomer = async (obj: Customer) =>
await useAxios.post<AddId>(`/admin/customer`, obj) await useAxios.post<AddId>(`/admin/customer`, obj)
@@ -13,6 +14,9 @@ export const updateCustomer = async (id: number, obj: Customer) =>
export const getCustomerTable = async (params: TableInput) => export const getCustomerTable = async (params: TableInput) =>
await useAxios.post<Array<Customer>>('/admin/customer/table', params) await useAxios.post<Array<Customer>>('/admin/customer/table', params)
export const getCustomerAssetsSummary = async (customerId: number) =>
await useAxios.get<WalletAssetsSummary>(`/admin/customer/${customerId}/assets`)
export const getCustomersList = async () => await useAxios.get<Customer[]>(`/admin/customer/list`) export const getCustomersList = async () => await useAxios.get<Customer[]>(`/admin/customer/list`)
export const changeCustomerActivation = async (id: number) => export const changeCustomerActivation = async (id: number) =>
@@ -0,0 +1,156 @@
<template>
<div class="customer-wallet-balance space-y-3">
<div v-if="loading" class="rounded-xl border border-gray-200 bg-gray-50 px-4 py-6 text-center text-sm text-gray-500">
در حال بارگذاری موجودی…
</div>
<div v-else-if="error" class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{{ error }}
</div>
<template v-else-if="summary">
<div
v-for="metal in summary.metal_totals"
:key="metal.metal_id"
class="overflow-hidden rounded-xl border border-gray-200"
>
<div class="flex items-center justify-between gap-3 bg-emerald-50 px-4 py-3">
<div class="flex min-w-0 items-center gap-2">
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
<span class="truncate font-700 text-emerald-900">{{ metal.label }}</span>
</div>
<span class="shrink-0 font-800 text-emerald-800">{{ formatMetalTotal(metal) }}</span>
</div>
<div v-if="linesForMetal(metal.metal_id).length" class="divide-y divide-gray-100 bg-white">
<div
v-for="line in linesForMetal(metal.metal_id)"
:key="line.key"
class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm"
>
<span class="min-w-0 truncate text-gray-600">{{ line.label }}</span>
<span class="shrink-0 font-600 text-gray-900">{{ line.value }}</span>
</div>
</div>
</div>
<div class="overflow-hidden rounded-xl border border-gray-200">
<div class="flex items-center justify-between gap-3 bg-sky-50 px-4 py-3">
<div class="flex items-center gap-2">
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#0369a1" />
<span class="font-700 text-sky-900">کل دارایی ریالی</span>
</div>
<span class="font-800 text-sky-800">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</div>
<div class="divide-y divide-gray-100 bg-white">
<div class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm">
<span class="text-gray-600">کیف پول ریالی</span>
<span class="font-600">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
</div>
<div
v-if="Number(summary.assets_rial_value ?? 0) > 0"
class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm"
>
<span class="text-gray-600">ارزش ریالی فلزات</span>
<span class="font-600">{{ usePrice(summary.assets_rial_value ?? 0, true, '') }}</span>
</div>
<div class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm font-700">
<span>مجموع کل</span>
<span>{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { getCustomerAssetsSummary } from '@/api/customer'
import type { WalletAssetsSummary, WalletInventoryCard, WalletMetalTotal } from '@/model/walletAssets'
import { usePrice } from '@core/utils/usePrice'
import { formatMetalDisplayValue } from '@/utils/metalWeight'
const props = defineProps<{
customerId?: number | null
}>()
const loading = ref(false)
const error = ref<string | null>(null)
const summary = ref<WalletAssetsSummary | null>(null)
watch(
() => props.customerId,
(id) => {
if (id) {
void load(id)
} else {
summary.value = null
error.value = null
}
},
{ immediate: true }
)
async function load(customerId: number) {
loading.value = true
error.value = null
try {
summary.value = await getCustomerAssetsSummary(customerId)
} catch {
error.value = 'بارگذاری جزئیات موجودی ناموفق بود'
summary.value = null
} finally {
loading.value = false
}
}
function formatMetalTotal(metal: WalletMetalTotal) {
return formatMetalDisplayValue(Number(metal.total_weight ?? 0), metal.metal_id)
}
function formatCard(card: WalletInventoryCard) {
const qty = Number(card.quantity ?? 0)
if (card.display_type === 'count') {
return `${qty.toLocaleString('fa-IR')} ${card.unit || 'عدد'}`
}
return `${qty.toLocaleString('fa-IR', { maximumFractionDigits: 3 })} ${card.unit || 'گرم'}`
}
function linesForMetal(metalId: number) {
const cards = summary.value?.inventory_cards ?? []
const countCards = summary.value?.count_products ?? []
const metal = summary.value?.metal_totals?.find((row) => row.metal_id === metalId)
const productLines = [...cards, ...countCards]
.filter((card) => Number(card.metal_id) === metalId && Number(card.quantity ?? 0) > 0)
.map((card) => ({
key: `card-${card.product_id}`,
label: card.title,
value: formatCard(card)
}))
const lines: Array<{ key: string; label: string; value: string }> = [...productLines]
if (metal && Number(metal.melted_weight ?? 0) > 0 && !productLines.some((line) => line.label.includes('آبشده'))) {
lines.push({
key: 'melted',
label: 'موجودی آبشده / کیف اصلی',
value: formatMetalDisplayValue(Number(metal.melted_weight), metalId)
})
}
if (metal && lines.length > 1) {
lines.push({
key: 'total',
label: 'مجموع',
value: formatMetalTotal(metal)
})
}
return lines
}
defineExpose({ reload: () => props.customerId && load(props.customerId) })
</script>
+37
View File
@@ -0,0 +1,37 @@
export interface WalletInventoryCard {
product_id: number
title: string
group_type: string
metal_id: number
quantity: number
unit: string
display_type: 'count' | 'weight'
unit_weight?: string | null
show_in_assets: boolean
is_melted?: boolean
is_main_wallet?: boolean
}
export interface WalletMetalTotal {
metal_id: number
metal_label: string
label: string
product_weight: number
melted_weight: number
total_weight: number
unit: string
}
export interface WalletAssetsSummary {
meta: {
metal_ids: number[]
weight_unit: string
single_metal: boolean
}
cash_wallet: number
inventory_cards: WalletInventoryCard[]
count_products: WalletInventoryCard[]
metal_totals: WalletMetalTotal[]
assets_rial_value: number
total_rial_value: number
}
@@ -21,7 +21,6 @@
<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)"
@@ -50,30 +50,7 @@
</BaseButton> </BaseButton>
</div> </div>
<div class="space-y-2"> <CustomerWalletBalancePanel :customer-id="row?.id" />
<MobileStatCard
v-for="entry in metalBalanceEntries(row)"
:key="entry.metal_id"
bordered
variant="success"
:label="customerWalletMetalBalanceTitle(entry.metal_id, entry.label)"
:value="entry.value"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
</template>
</MobileStatCard>
<MobileStatCard
bordered
variant="success"
label="موجودی ریالی"
:value="usePrice(cashBalance(row), true, '')"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
</template>
</MobileStatCard>
</div>
<MobileInfoGrid> <MobileInfoGrid>
<MobileDetailRow label="وضعیت کاربر" full-width :value-class="row.active ? 'text-green-700' : 'text-red-600'"> <MobileDetailRow label="وضعیت کاربر" full-width :value-class="row.active ? 'text-green-700' : 'text-red-600'">
@@ -101,20 +78,15 @@ import { Modal } from 'ant-design-vue'
import type { Customer } from '@/model/customer' import type { Customer } from '@/model/customer'
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex' import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue' import MobileDetailRow from '@/components/mobile/MobileDetailRow.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 } from '@/components/mobile/utils/wallet' import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { metalLabel } from '@/utils/adminMetalLabels'
import { customerWalletMetalBalanceTitle, metalLabel } 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<{
@@ -163,30 +135,6 @@ function confirmToggleTrade(row: Customer) {
}) })
} }
function cashBalance(row: Customer) {
return getCashBalance(row.wallet)
}
function metalBalanceEntries(row: Customer) {
const metals = props.metals?.length
? props.metals
: [{ metal_id: 1, label: 'طلا' }]
return metals
.map((metal) => {
const value = metalCellValue(row as Record<string, unknown>, metal.metal_id, 'wallet_metal_gold')
return {
metal_id: metal.metal_id,
label: metal.label,
value: formatMetalDisplayValue(value, metal.metal_id)
}
})
.filter((entry) => {
const numeric = metalCellValue(row as Record<string, unknown>, entry.metal_id, 'wallet_metal_gold')
return numeric !== 0
})
}
function lastSeen(row: Customer) { function lastSeen(row: Customer) {
return row.last_seen_at ? toJalali(row.last_seen_at) : '-' return row.last_seen_at ? toJalali(row.last_seen_at) : '-'
} }
+8 -15
View File
@@ -180,19 +180,7 @@
</BasicInfoItem> </BasicInfoItem>
</div> </div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2"> <CustomerWalletBalancePanel v-if="userInfo?.id" :customer-id="userInfo.id" class="mt-2" />
<div
v-for="(item, index) in userInfo.wallet"
:key="index"
class="grid grid-cols-2 bg-orange-100 dark:bg-orange-500/15 gap-2 rounded-lg p-2"
>
<BasicInfoItem
title="نام قلک"
:value="item?.fund ? item?.fund?.title : metalLabel.goldWallet()"
/>
<BasicInfoItem :title="metalLabel.goldAmount()" :value="`${item?.gold} گرم`" />
</div>
</div>
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="لیست سفارشات" tab="لیست سفارشات"> <a-tab-pane key="لیست سفارشات" tab="لیست سفارشات">
@@ -226,18 +214,20 @@
@ok="chargeMoneyWallet" @ok="chargeMoneyWallet"
@close="showMoneyWalletCharge = false" @close="showMoneyWalletCharge = false"
> >
<CustomerWalletBalancePanel v-if="formCharge.customer_id" :customer-id="formCharge.customer_id" class="mb-3" />
<WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" /> <WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" />
</BasicActionSheet> </BasicActionSheet>
<BasicModal <BasicModal
v-else v-else
v-model:visible="showMoneyWalletCharge" v-model:visible="showMoneyWalletCharge"
width="22rem" width="28rem"
:title="moneyChargeTitle" :title="moneyChargeTitle"
:ok-text="moneyChargeOkText" :ok-text="moneyChargeOkText"
@ok="chargeMoneyWallet" @ok="chargeMoneyWallet"
:loading="loading" :loading="loading"
@close="showMoneyWalletCharge = false" @close="showMoneyWalletCharge = false"
> >
<CustomerWalletBalancePanel v-if="formCharge.customer_id" :customer-id="formCharge.customer_id" class="mb-3" />
<WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" /> <WalletChargeForm ref="moneyChargeFormRef" :model-value="formCharge" variant="money" />
</BasicModal> </BasicModal>
@@ -255,18 +245,20 @@
@ok="chargeGoldWallet" @ok="chargeGoldWallet"
@close="showGoldWalletCharge = false" @close="showGoldWalletCharge = false"
> >
<CustomerWalletBalancePanel v-if="formCharge.customer_id" :customer-id="formCharge.customer_id" class="mb-3" />
<WalletChargeForm ref="goldChargeFormRef" :model-value="formCharge" variant="gold" /> <WalletChargeForm ref="goldChargeFormRef" :model-value="formCharge" variant="gold" />
</BasicActionSheet> </BasicActionSheet>
<BasicModal <BasicModal
v-else v-else
v-model:visible="showGoldWalletCharge" v-model:visible="showGoldWalletCharge"
width="22rem" width="28rem"
:title="goldChargeTitle" :title="goldChargeTitle"
:ok-text="goldChargeOkText" :ok-text="goldChargeOkText"
@ok="chargeGoldWallet" @ok="chargeGoldWallet"
:loading="loading" :loading="loading"
@close="showGoldWalletCharge = false" @close="showGoldWalletCharge = false"
> >
<CustomerWalletBalancePanel v-if="formCharge.customer_id" :customer-id="formCharge.customer_id" class="mb-3" />
<WalletChargeForm ref="goldChargeFormRef" :model-value="formCharge" variant="gold" /> <WalletChargeForm ref="goldChargeFormRef" :model-value="formCharge" variant="gold" />
</BasicModal> </BasicModal>
<BasicInfoModal <BasicInfoModal
@@ -313,6 +305,7 @@ import type { Order } from '@/model/order'
import Icon from '@core/components/BaseIcon/src/Icon.vue' 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 CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
import { import {
buildChargePayload, buildChargePayload,
createEmptyChargeForm, createEmptyChargeForm,