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

708 lines
21 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 ? 'customer-wallet-balance--compact space-y-1.5' : 'space-y-3',
layout === 'sidebar' ? 'customer-wallet-balance--sidebar' : '',
]"
>
<div
v-if="loading"
class="customer-wallet-balance__placeholder rounded-xl text-center text-sm"
: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"
ref="minimalTrackRef"
class="customer-wallet-balance__minimal-track"
>
<div class="customer-wallet-balance__minimal">
<template v-if="chargeTargets.length">
<component
:is="interactive ? 'button' : 'div'"
v-for="target in chargeTargets"
:key="target.key"
:type="interactive ? 'button' : undefined"
class="customer-wallet-balance__minimal-item"
:class="[
targetToneClass(target),
{ 'customer-wallet-balance__minimal-item--inactive': target.active === false },
{ 'customer-wallet-balance__minimal-item--selected': interactive && target.key === selectedChargeKey },
]"
:title="target.label"
@click="interactive ? emit('selectChargeTarget', target.key) : undefined"
>
<span class="customer-wallet-balance__minimal-label">{{ minimalTargetLabel(target) }}</span>
<span class="customer-wallet-balance__minimal-value">{{ balanceForTarget(target) }}</span>
</component>
</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>
</div>
<div v-else-if="summary" class="customer-wallet-balance__full">
<p v-if="layout === 'sidebar'" class="customer-wallet-balance__sidebar-title">موجودی دارایی‌ها</p>
<div class="customer-wallet-balance__overview">
<article
v-for="metal in summary.metal_totals"
:key="metal.metal_id"
class="customer-wallet-balance__summary-card"
:class="groupToneClass(metal.metal_id)"
>
<span class="customer-wallet-balance__summary-label">{{ minimalMetalLabel(metal) }}</span>
<span class="customer-wallet-balance__summary-value">{{ formatMetalTotal(metal) }}</span>
</article>
<article class="customer-wallet-balance__summary-card customer-wallet-balance__group--cash">
<span class="customer-wallet-balance__summary-label">ریالی</span>
<span class="customer-wallet-balance__summary-value">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</article>
</div>
<div class="customer-wallet-balance__details">
<div class="customer-wallet-balance__details-title">جزئیات موجودی</div>
<template v-for="metal in summary.metal_totals" :key="`detail-${metal.metal_id}`">
<section
v-if="linesForMetal(metal.metal_id).length"
class="customer-wallet-balance__detail-group"
:class="groupToneClass(metal.metal_id)"
>
<div class="customer-wallet-balance__detail-group-title">
<span class="customer-wallet-balance__group-dot" aria-hidden="true" />
<span>{{ metal.label }}</span>
</div>
<div
v-for="line in linesForMetal(metal.metal_id)"
:key="line.key"
class="customer-wallet-balance__detail-row"
:class="{ 'customer-wallet-balance__detail-row--strong': line.key === 'total' }"
>
<span class="customer-wallet-balance__detail-label">{{ line.label }}</span>
<span class="customer-wallet-balance__detail-value">{{ line.value }}</span>
</div>
</section>
</template>
<section class="customer-wallet-balance__detail-group customer-wallet-balance__group--cash">
<div class="customer-wallet-balance__detail-group-title">
<span class="customer-wallet-balance__group-dot" aria-hidden="true" />
<span>دارایی ریالی</span>
</div>
<div class="customer-wallet-balance__detail-row">
<span class="customer-wallet-balance__detail-label">کیف پول ریالی</span>
<span class="customer-wallet-balance__detail-value">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
</div>
<div
v-if="Number(summary.assets_rial_value ?? 0) > 0"
class="customer-wallet-balance__detail-row"
>
<span class="customer-wallet-balance__detail-label">ارزش ریالی فلزات</span>
<span class="customer-wallet-balance__detail-value">{{ usePrice(summary.assets_rial_value ?? 0, true, '') }}</span>
</div>
<div class="customer-wallet-balance__detail-row customer-wallet-balance__detail-row--strong">
<span class="customer-wallet-balance__detail-label">مجموع کل</span>
<span class="customer-wallet-balance__detail-value">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</div>
</section>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { getCustomerAssetsSummary } from '@/api/customer'
import { useHorizontalTouchScroll } from '@/components/mobile/composables/useHorizontalTouchScroll'
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
/** چیدمان ستون کناری در اطلاعات کاربر */
layout?: 'default' | 'sidebar'
/** گزینه‌های شارژ — برای نمایش موجودی هر محصول */
chargeTargets?: ChargeTarget[]
selectedChargeKey?: string
/** false = فقط نمایش، بدون کلیک */
interactive?: boolean
}>(),
{
compact: false,
layout: 'default',
chargeTargets: () => [],
selectedChargeKey: '',
interactive: true,
},
)
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 'نقدی'
return target.label
.replace(/\s*\(کیف اصلی\)\s*$/u, '')
.replace(/\s*\(غیرفعال\)\s*$/u, '')
.trim()
}
function groupToneClass(metalId: number | string | undefined) {
switch (Number(metalId)) {
case 1:
return 'customer-wallet-balance__group--gold'
case 2:
return 'customer-wallet-balance__group--silver'
case 3:
return 'customer-wallet-balance__group--copper'
default:
return 'customer-wallet-balance__group--product'
}
}
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)
const minimalTrackRef = ref<HTMLElement | null>(null)
useHorizontalTouchScroll(minimalTrackRef)
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--compact {
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-wallet-balance__placeholder {
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-muted, #f8fafc);
color: var(--admin-text-secondary, #64748b);
}
/* ---------- نمای کامل (خلاصه + جزئیات) ---------- */
.customer-wallet-balance__full {
width: 100%;
min-width: 0;
}
.customer-wallet-balance--sidebar .customer-wallet-balance__full {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.customer-wallet-balance__sidebar-title {
margin: 0;
font-size: 0.8125rem;
font-weight: 700;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance--sidebar .customer-wallet-balance__overview {
grid-template-columns: 1fr;
}
.customer-wallet-balance--sidebar .customer-wallet-balance__details {
max-height: min(52vh, 26rem);
overflow-y: auto;
}
.customer-wallet-balance__overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
gap: 0.5rem;
}
.customer-wallet-balance__summary-card {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
border-radius: 0.65rem;
border: 1px solid color-mix(in srgb, var(--wallet-accent, #94a3b8) 28%, transparent);
background: color-mix(in srgb, var(--wallet-accent, #94a3b8) 10%, var(--admin-bg-elevated, #fff));
padding: 0.55rem 0.7rem;
}
.customer-wallet-balance__summary-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance__summary-value {
flex: 0 0 auto;
font-size: 0.8125rem;
font-weight: 800;
color: var(--wallet-accent-strong, var(--admin-text, #0f172a));
white-space: nowrap;
}
.customer-wallet-balance__details {
margin-top: 0.6rem;
overflow: hidden;
border-radius: 0.75rem;
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-elevated, #fff);
}
.customer-wallet-balance__details-title {
padding: 0.55rem 0.8rem;
border-bottom: 1px solid var(--admin-border, #e2e8f0);
color: var(--admin-text-secondary, #64748b);
font-size: 0.75rem;
font-weight: 700;
}
.customer-wallet-balance__detail-group + .customer-wallet-balance__detail-group {
border-top: 1px solid var(--admin-border, #e2e8f0);
}
.customer-wallet-balance__detail-group-title {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.55rem 0.8rem 0.25rem;
color: var(--admin-text, #0f172a);
font-size: 0.75rem;
font-weight: 800;
}
.customer-wallet-balance__group-dot {
width: 0.45rem;
height: 0.45rem;
flex: 0 0 auto;
border-radius: 9999px;
background: var(--wallet-accent, var(--admin-text-secondary, #94a3b8));
}
.customer-wallet-balance__detail-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
padding: 0.45rem 0.8rem;
font-size: 0.8125rem;
}
.customer-wallet-balance__detail-row + .customer-wallet-balance__detail-row {
border-top: 1px solid color-mix(in srgb, var(--admin-border, #e2e8f0) 70%, transparent);
}
.customer-wallet-balance__detail-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance__detail-value {
font-weight: 600;
color: var(--admin-text, #0f172a);
white-space: nowrap;
}
.customer-wallet-balance__detail-row--strong {
background: color-mix(in srgb, var(--wallet-accent, #94a3b8) 8%, transparent);
}
.customer-wallet-balance__detail-row--strong .customer-wallet-balance__detail-label,
.customer-wallet-balance__detail-row--strong .customer-wallet-balance__detail-value {
font-weight: 800;
color: var(--admin-text, #0f172a);
}
.customer-wallet-balance__group--gold {
--wallet-accent: #d97706;
--wallet-accent-strong: #b45309;
}
.customer-wallet-balance__group--silver {
--wallet-accent: #64748b;
--wallet-accent-strong: #475569;
}
.customer-wallet-balance__group--copper {
--wallet-accent: #ea580c;
--wallet-accent-strong: #c2410c;
}
.customer-wallet-balance__group--cash {
--wallet-accent: #0284c7;
--wallet-accent-strong: #0369a1;
}
.customer-wallet-balance__group--product {
--wallet-accent: var(--admin-text-secondary, #64748b);
--wallet-accent-strong: var(--admin-text, #0f172a);
}
:global(html.is-dark) .customer-wallet-balance__group--gold {
--wallet-accent: #fbbf24;
--wallet-accent-strong: #fcd34d;
}
:global(html.is-dark) .customer-wallet-balance__group--silver {
--wallet-accent: #94a3b8;
--wallet-accent-strong: #cbd5e1;
}
:global(html.is-dark) .customer-wallet-balance__group--copper {
--wallet-accent: #fb923c;
--wallet-accent-strong: #fdba74;
}
:global(html.is-dark) .customer-wallet-balance__group--cash {
--wallet-accent: #60a5fa;
--wallet-accent-strong: #93c5fd;
}
.customer-wallet-balance__minimal-track {
display: block;
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: thin;
scrollbar-color: var(--admin-border-strong, #cbd5e1) transparent;
padding: 0.45rem 0.35rem;
border-radius: 0.6rem;
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-muted, #f8fafc);
direction: ltr;
}
.customer-wallet-balance__minimal-track::-webkit-scrollbar {
height: 0.35rem;
}
.customer-wallet-balance__minimal-track::-webkit-scrollbar-thumb {
border-radius: 9999px;
background: var(--admin-border-strong, #cbd5e1);
}
.customer-wallet-balance__minimal {
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: stretch;
gap: 0.35rem;
width: max-content;
min-width: 100%;
direction: rtl;
}
.customer-wallet-balance__minimal-item {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
flex: 0 0 6.25rem;
width: 6.25rem;
min-width: 6.25rem;
min-height: 4rem;
gap: 0.25rem;
padding: 0.45rem 0.35rem;
border-radius: 0.45rem;
background: var(--wallet-tile-bg, var(--admin-bg-elevated, #fff));
text-align: center;
border: 1px solid var(--wallet-tile-border, var(--admin-border, #e2e8f0));
cursor: default;
box-sizing: border-box;
}
button.customer-wallet-balance__minimal-item {
cursor: pointer;
transition: box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
}
button.customer-wallet-balance__minimal-item:hover {
transform: translateY(-1px);
}
.customer-wallet-balance__minimal-item--selected {
border-width: 2px;
padding: calc(0.45rem - 1px) calc(0.35rem - 1px);
box-shadow: none;
}
.customer-wallet-balance__minimal-item--inactive {
opacity: 0.72;
}
.customer-wallet-balance__minimal-item--gold {
--wallet-tile-bg: #fffbeb;
--wallet-tile-border: #fde68a;
--wallet-tile-label: #92400e;
--wallet-tile-value: #b45309;
}
.customer-wallet-balance__minimal-item--silver {
--wallet-tile-bg: #f8fafc;
--wallet-tile-border: #cbd5e1;
--wallet-tile-label: #64748b;
--wallet-tile-value: #334155;
}
.customer-wallet-balance__minimal-item--copper {
--wallet-tile-bg: #fff7ed;
--wallet-tile-border: #fdba74;
--wallet-tile-label: #c2410c;
--wallet-tile-value: #9a3412;
}
.customer-wallet-balance__minimal-item--cash {
--wallet-tile-bg: #eff6ff;
--wallet-tile-border: #93c5fd;
--wallet-tile-label: #0369a1;
--wallet-tile-value: #0284c7;
}
.customer-wallet-balance__minimal-item--product {
--wallet-tile-bg: var(--admin-bg-elevated, #fff);
--wallet-tile-border: var(--admin-border, #e2e8f0);
--wallet-tile-label: #475569;
--wallet-tile-value: #1e293b;
}
.customer-wallet-balance__minimal-label {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
align-self: start;
min-height: 0;
max-height: 2.45em;
font-size: 0.625rem;
font-weight: 600;
line-height: 1.25;
color: var(--wallet-tile-label, var(--admin-text-secondary, #64748b));
width: 100%;
overflow: hidden;
white-space: normal;
word-break: break-word;
}
.customer-wallet-balance__minimal-value {
align-self: end;
font-size: 0.6875rem;
font-weight: 700;
line-height: 1.25;
color: var(--wallet-tile-value, var(--admin-text, #0f172a));
white-space: nowrap;
}
:global(html.is-dark) .customer-wallet-balance__minimal-item--gold {
--wallet-tile-bg: rgba(245, 158, 11, 0.14);
--wallet-tile-border: rgba(251, 191, 36, 0.28);
--wallet-tile-label: #fbbf24;
--wallet-tile-value: #fcd34d;
}
:global(html.is-dark) .customer-wallet-balance__minimal-item--silver {
--wallet-tile-bg: rgba(148, 163, 184, 0.12);
--wallet-tile-border: rgba(148, 163, 184, 0.28);
--wallet-tile-label: #94a3b8;
--wallet-tile-value: #cbd5e1;
}
:global(html.is-dark) .customer-wallet-balance__minimal-item--copper {
--wallet-tile-bg: rgba(249, 115, 22, 0.14);
--wallet-tile-border: rgba(251, 146, 60, 0.28);
--wallet-tile-label: #fb923c;
--wallet-tile-value: #fdba74;
}
:global(html.is-dark) .customer-wallet-balance__minimal-item--cash {
--wallet-tile-bg: rgba(59, 130, 246, 0.14);
--wallet-tile-border: rgba(96, 165, 250, 0.32);
--wallet-tile-label: #60a5fa;
--wallet-tile-value: #93c5fd;
}
:global(html.is-dark) .customer-wallet-balance__minimal-item--product {
--wallet-tile-bg: rgba(255, 255, 255, 0.04);
--wallet-tile-border: var(--admin-border);
--wallet-tile-label: var(--admin-text-secondary);
--wallet-tile-value: var(--admin-text);
}
</style>