Files
service-finder/frontend_app/src/components/finance/WalletBalanceCard.vue
2026-07-27 08:39:18 +00:00

189 lines
6.9 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
<h2 class="text-lg font-bold text-slate-800">💰 {{ t('finance.walletBreakdown') }}</h2>
<span class="text-xs text-slate-400">{{ t('finance.live') }}</span>
</div>
<!-- Loading -->
<div
v-if="isLoading"
class="flex items-center justify-center py-16"
>
<div class="h-10 w-10 animate-spin rounded-full border-4 border-slate-200 border-t-sf-accent" />
</div>
<!-- Error -->
<div
v-else-if="error"
class="flex flex-col items-center justify-center py-12 text-center px-6"
>
<svg class="w-12 h-12 text-amber-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<p class="text-sm text-slate-500">{{ error }}</p>
<button
@click="retry"
class="mt-3 text-sm text-sf-accent hover:underline font-medium"
>
{{ t('common.retry') }}
</button>
</div>
<!-- Wallet Pockets Grid -->
<div v-else class="p-6 space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- EARNED -->
<div
class="rounded-xl border p-4 transition-all duration-300"
:class="highlight === 'earned'
? 'border-emerald-400 bg-emerald-50 ring-2 ring-emerald-200 scale-[1.02]'
: 'border-slate-200 bg-slate-50 hover:bg-slate-100'"
>
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider">
🏆 {{ t('finance.earned') }}
</span>
<span class="text-xs text-slate-400">{{ t('finance.referralRewards') }}</span>
</div>
<p class="text-2xl font-extrabold text-emerald-600">{{ formatCredits(balance.earned) }}</p>
</div>
<!-- PURCHASED -->
<div
class="rounded-xl border p-4 transition-all duration-300"
:class="highlight === 'purchased'
? 'border-blue-400 bg-blue-50 ring-2 ring-blue-200 scale-[1.02]'
: 'border-slate-200 bg-slate-50 hover:bg-slate-100'"
>
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider">
💳 {{ t('finance.purchased') }}
</span>
<span class="text-xs text-slate-400">{{ t('finance.topUpWallet') }}</span>
</div>
<p class="text-2xl font-extrabold text-blue-600">{{ formatCredits(balance.purchased) }}</p>
</div>
<!-- SERVICE_COINS (default highlight target) -->
<div
ref="serviceCoinsRef"
class="rounded-xl border p-4 transition-all duration-300"
:class="highlight === 'service-coins'
? 'border-amber-400 bg-amber-50 ring-2 ring-amber-200 scale-[1.02]'
: 'border-slate-200 bg-slate-50 hover:bg-slate-100'"
>
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider">
🪙 {{ t('finance.rewards') }}
</span>
<span class="text-xs text-slate-400">{{ t('finance.bonus') }}</span>
</div>
<p class="text-2xl font-extrabold text-amber-600">{{ formatCredits(balance.service_coins) }}</p>
</div>
<!-- VOUCHER -->
<div
class="rounded-xl border p-4 transition-all duration-300"
:class="highlight === 'voucher'
? 'border-purple-400 bg-purple-50 ring-2 ring-purple-200 scale-[1.02]'
: 'border-slate-200 bg-slate-50 hover:bg-slate-100'"
>
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider">
🎟 {{ t('finance.voucher') }}
</span>
<span class="text-xs text-slate-400">{{ t('finance.activeVouchers') }}</span>
</div>
<p class="text-2xl font-extrabold text-purple-600">{{ formatCredits(balance.voucher) }}</p>
</div>
</div>
<!-- Total Balance Bar -->
<div class="rounded-xl bg-gradient-to-r from-sf-accent to-emerald-500 p-4 text-white">
<div class="flex items-center justify-between">
<span class="text-sm font-semibold opacity-90">{{ t('finance.totalBalance') }}</span>
<span class="text-2xl font-extrabold">{{ formatCredits(balance.total) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* WalletBalanceCard.vue
*
* Displays the 4 wallet pockets (EARNED, PURCHASED, SERVICE_COINS, VOUCHER)
* fetched from GET /billing/wallet/balance via the financeStore.
*
* Props:
* highlight - optional string matching a wallet type (e.g. "service-coins")
* to visually emphasize a specific pocket on page load.
*/
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFinanceStore } from '../../stores/finance'
import type { WalletBalance } from '../../stores/finance'
const { t } = useI18n()
const financeStore = useFinanceStore()
const props = defineProps<{
highlight?: string
}>()
const emit = defineEmits<{
loaded: []
}>()
// ── Template refs ──
const serviceCoinsRef = ref<HTMLDivElement | null>(null)
// ── Computed from store ──
const balance = computed<WalletBalance>(() => {
if (financeStore.balance) {
return financeStore.balance as WalletBalance
}
// Fallback shape while loading
return { earned: 0, purchased: 0, service_coins: 0, voucher: 0, total: 0 }
})
const isLoading = computed(() => financeStore.isLoading)
const error = computed(() => financeStore.error)
// ── Helpers ──
function formatCredits(value: number): string {
if (Number.isInteger(value)) {
return value.toLocaleString()
}
return value.toFixed(2)
}
function retry() {
financeStore.fetchWalletBalance()
}
// ── Highlight auto-scroll ──
watch(() => props.highlight, (h) => {
if (h === 'service-coins' && serviceCoinsRef.value) {
nextTick(() => {
serviceCoinsRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
})
}
})
// ── Fetch on mount & emit loaded ──
onMounted(async () => {
await financeStore.fetchWalletBalance()
// Auto-scroll if highlight is service-coins
if (props.highlight === 'service-coins' && serviceCoinsRef.value) {
nextTick(() => {
serviceCoinsRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
})
}
emit('loaded')
})
</script>