Files
boursecu_admin/src/components/customer/CustomerWalletBalancePanel.vue
T

416 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="customer-wallet-balance" :class="compact ? 'space-y-1.5' : 'space-y-3'">
<div
v-if="loading"
class="rounded-xl border border-gray-200 bg-gray-50 text-center text-sm text-gray-500"
:class="compact ? 'px-3 py-3' : 'px-4 py-6'"
>
در حال بارگذاری موجودی…
</div>
<div
v-else-if="error"
class="rounded-xl border border-red-200 bg-red-50 text-sm text-red-700"
:class="compact ? 'px-3 py-2' : 'px-4 py-3'"
>
{{ error }}
</div>
<div v-else-if="summary && compact" class="customer-wallet-balance__minimal">
<template v-if="chargeTargets.length">
<button
v-for="target in chargeTargets"
:key="target.key"
type="button"
class="customer-wallet-balance__minimal-item"
:class="[
targetToneClass(target),
{ 'customer-wallet-balance__minimal-item--inactive': target.active === false },
{ 'customer-wallet-balance__minimal-item--selected': target.key === selectedChargeKey },
]"
:title="target.label"
@click="emit('selectChargeTarget', target.key)"
>
<span class="customer-wallet-balance__minimal-label">{{ minimalTargetLabel(target) }}</span>
<span class="customer-wallet-balance__minimal-value">{{ balanceForTarget(target) }}</span>
</button>
</template>
<template v-else>
<div
v-for="metal in summary.metal_totals"
:key="metal.metal_id"
class="customer-wallet-balance__minimal-item"
:class="minimalMetalToneClass(metal)"
>
<span class="customer-wallet-balance__minimal-label">{{ minimalMetalLabel(metal) }}</span>
<span class="customer-wallet-balance__minimal-value">{{ formatMetalTotal(metal) }}</span>
</div>
<div class="customer-wallet-balance__minimal-item customer-wallet-balance__minimal-item--cash">
<span class="customer-wallet-balance__minimal-label">نقدی</span>
<span class="customer-wallet-balance__minimal-value">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
</div>
</template>
</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 { ChargeTarget } from '@/model/wallet'
import type { WalletAssetsSummary, WalletInventoryCard, WalletMetalTotal } from '@/model/walletAssets'
import { usePrice } from '@core/utils/usePrice'
import { formatMetalDisplayValue } from '@/utils/metalWeight'
const props = withDefaults(
defineProps<{
customerId?: number | null
/** نمایش مینیمال برای مودال‌ها */
compact?: boolean
/** گزینه‌های شارژ — برای نمایش موجودی هر محصول */
chargeTargets?: ChargeTarget[]
selectedChargeKey?: string
}>(),
{
compact: false,
chargeTargets: () => [],
selectedChargeKey: '',
},
)
const emit = defineEmits<{
(e: 'selectChargeTarget', key: string): void
}>()
function minimalMetalLabel(metal: WalletMetalTotal) {
switch (Number(metal.metal_id)) {
case 1:
return 'طلا'
case 2:
return 'نقره'
case 3:
return 'مس'
default: {
const raw = (metal.metal_label || metal.label || '').trim()
return raw.replace(/^کل\s*دارایی\s*/u, '') || raw || 'فلز'
}
}
}
function minimalMetalToneClass(metal: WalletMetalTotal) {
switch (Number(metal.metal_id)) {
case 1:
return 'customer-wallet-balance__minimal-item--gold'
case 2:
return 'customer-wallet-balance__minimal-item--silver'
case 3:
return 'customer-wallet-balance__minimal-item--copper'
default:
return ''
}
}
function minimalTargetLabel(target: ChargeTarget) {
if (target.kind === 'money') return 'نقدی'
const label = target.label.replace(/\s*\(کیف اصلی\)\s*$/u, '').trim()
if (label.length <= 14) return label
return `${label.slice(0, 13)}…`
}
function targetToneClass(target: ChargeTarget) {
if (target.kind === 'money') return 'customer-wallet-balance__minimal-item--cash'
switch (Number(target.metal_id)) {
case 1:
return 'customer-wallet-balance__minimal-item--gold'
case 2:
return 'customer-wallet-balance__minimal-item--silver'
case 3:
return 'customer-wallet-balance__minimal-item--copper'
default:
return 'customer-wallet-balance__minimal-item--product'
}
}
function inventoryCards() {
if (!summary.value) return [] as WalletInventoryCard[]
return [...(summary.value.inventory_cards ?? []), ...(summary.value.count_products ?? [])]
}
function balanceForTarget(target: ChargeTarget) {
if (!summary.value) return '—'
if (target.kind === 'money') {
return usePrice(summary.value.cash_wallet ?? 0, true, '')
}
const card = inventoryCards().find((row) => Number(row.product_id) === Number(target.product_id))
if (card) return formatCard(card)
const unit = target.unit || (target.trade_by_quantity ? 'عدد' : 'گرم')
if (target.trade_by_quantity) {
return `۰ ${unit}`
}
return formatMetalDisplayValue(0, target.metal_id ?? 1)
}
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>
<style scoped>
.customer-wallet-balance__minimal {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(4.75rem, 1fr));
gap: 0.35rem;
padding: 0.45rem;
border-radius: 0.6rem;
border: 1px solid #e2e8f0;
background: #f8fafc;
max-height: 14rem;
overflow-y: auto;
}
.customer-wallet-balance__minimal-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.1rem;
min-width: 0;
padding: 0.35rem 0.25rem;
border-radius: 0.45rem;
background: #fff;
text-align: center;
border: 1px solid transparent;
cursor: default;
}
button.customer-wallet-balance__minimal-item {
cursor: pointer;
transition: box-shadow 0.15s ease, transform 0.15s ease;
}
button.customer-wallet-balance__minimal-item:hover {
transform: translateY(-1px);
}
.customer-wallet-balance__minimal-item--selected {
box-shadow: 0 0 0 2px #2563eb;
}
.customer-wallet-balance__minimal-item--inactive {
opacity: 0.72;
}
.customer-wallet-balance__minimal-item--gold {
background: #fffbeb;
border-color: #fde68a;
}
.customer-wallet-balance__minimal-item--silver {
background: #f8fafc;
border-color: #cbd5e1;
}
.customer-wallet-balance__minimal-item--copper {
background: #fff7ed;
border-color: #fdba74;
}
.customer-wallet-balance__minimal-item--cash {
background: #eff6ff;
border-color: #93c5fd;
}
.customer-wallet-balance__minimal-item--product {
background: #f8fafc;
border-color: #e2e8f0;
}
.customer-wallet-balance__minimal-label {
font-size: 0.6875rem;
font-weight: 600;
color: #64748b;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.customer-wallet-balance__minimal-value {
font-size: 0.75rem;
font-weight: 700;
color: #0f172a;
white-space: nowrap;
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-label {
color: #92400e;
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-value {
color: #b45309;
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-label {
color: #64748b;
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-value {
color: #334155;
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-label {
color: #c2410c;
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-value {
color: #9a3412;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-label {
color: #0369a1;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-value {
color: #0284c7;
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-label {
color: #475569;
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-value {
color: #1e293b;
}
</style>