| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- <script>
- import { onMount } from 'svelte';
- import { get } from 'svelte/store';
- import Header from '$lib/layout/Header.svelte';
- import StatsCard from '$lib/components/StatsCard.svelte';
- import PaymentMenu from '$lib/components/wallet/PaymentMenu.svelte';
- import SellMenu from '$lib/components/wallet/SellMenu.svelte';
- import tokensIcon from '$lib/assets/icons/sidebar/tokens.svg?raw';
- import walletIcon from '$lib/assets/icons/sidebar/wallet.svg?raw';
- import { authToken } from '$lib/utils/stores';
- import ModalBase from '$lib/components/trading/ModalBase.svelte';
- const breadcrumb = [{ label: 'Início' }, { label: 'wallet', active: true }];
- const apiUrl = import.meta.env.VITE_API_URL;
- let walletTokens = [];
- let easyCoinBalance = 0;
- let tokensLoading = false;
- let tokensError = '';
- let isPaymentOpen = false;
- let isSellOpen = false;
- let rateBRLPerEasyCoin = 100;
- onMount(() => {
- void fetchWalletTokens();
- });
- let tokenDetailModalVisible = false;
- let tokenDetailSelected = null;
- let tokenDetailEntries = [];
- function openPayment() {
- isPaymentOpen = true;
- }
- function closePayment() {
- isPaymentOpen = false;
- }
- function confirmPayment(method) {
- isPaymentOpen = false;
- }
- function openSell() {
- isSellOpen = true;
- }
- function closeSell() {
- isSellOpen = false;
- }
- function confirmSell(e) {
- isSellOpen = false;
- }
- async function parseResponse(res) {
- const raw = await res.text();
- return raw ? JSON.parse(raw) : null;
- }
- async function fetchWalletTokens() {
- if (!apiUrl) return;
- tokensLoading = true;
- tokensError = '';
- try {
- const token = get(authToken);
- if (!token) {
- throw new Error('Sessão expirada. Faça login novamente.');
- }
- const res = await fetch(`${apiUrl}/wallet/tokens`, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- Authorization: `Bearer ${token}`
- }
- });
- const body = await parseResponse(res);
- if (!res.ok || body?.status !== 'ok') {
- throw new Error(body?.msg ?? 'Falha ao carregar tokens da wallet.');
- }
- const payload = body?.data ?? {};
- walletTokens = Array.isArray(payload?.tokens) ? payload.tokens : [];
- easyCoinBalance = resolveEasyCoinBalance(payload);
- } catch (err) {
- console.error('[Wallet] Erro ao buscar tokens:', err);
- walletTokens = [];
- easyCoinBalance = 0;
- tokensError = err?.message ?? 'Não foi possível carregar os tokens.';
- } finally {
- tokensLoading = false;
- }
- }
- function resolveEasyCoinBalance(payload) {
- const candidates = [
- payload?.wallet?.easycoin_balance,
- payload?.wallet?.easyCoinBalance,
- payload?.wallet?.balance,
- payload?.easycoin_balance,
- payload?.easyCoinBalance,
- payload?.easycoin,
- payload?.balance
- ];
- const firstValue = candidates.find((value) => value !== undefined && value !== null && value !== '');
- return Number(firstValue ?? 0);
- }
- function formatToken(n) {
- return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 6 }).format(Number(n || 0));
- }
- function formatCoin(n) {
- return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(n || 0));
- }
- function formatCurrencyBRL(n) {
- return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(n || 0));
- }
- function tokenCardLabel(token) {
- return token?.cpr_product_name ?? token?.token_content ?? token?.token_external_id ?? token?.token_id ?? 'EasyToken';
- }
- function tokenCardSubtitle(token) {
- if (token?.token_city && token?.token_uf) {
- return `${token.token_city}/${token.token_uf}`;
- }
- if (token?.token_external_id) {
- return `ID ${token.token_external_id}`;
- }
- return 'Saldo atual';
- }
- function tokenAmount(token) {
- const candidates = [
- token?.token_commodities_amount,
- token?.token_amount,
- token?.token_balance,
- token?.balance,
- token?.amount
- ];
- const value = candidates.find((item) => item !== undefined && item !== null && item !== '');
- return Number(value ?? 0);
- }
- function formatTokenDetailKey(key = '') {
- return key
- .replace(/_/g, ' ')
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .replace(/\s+/g, ' ')
- .trim()
- .replace(/^./, (c) => c.toUpperCase());
- }
- const hiddenTokenDetailKeys = new Set([
- 'token_id',
- 'token_external_id',
- 'token_commodities_amount',
- 'token_commodities_value',
- 'token_uf',
- 'token_city',
- 'token_content',
- 'wallet_id',
- 'chain_id',
- 'commodities_id'
- ]);
- const tokenDetailFieldConfig = [
- { key: 'cpr_product_name', label: 'Produto' },
- {
- key: 'cpr_product_quantity',
- label: 'Quantidade do Produto',
- formatter: (value, token) => {
- const formattedQuantity = formatToken(value);
- const unit = token?.cpr_measure_unit_name ?? token?.cpr_packaging_way_name;
- return unit ? `${formattedQuantity} ${unit}` : formattedQuantity;
- }
- },
- { key: 'cpr_packaging_way_name', label: 'Forma de Embalagem' },
- { key: 'cpr_measure_unit_name', label: 'Unidade de Medida' },
- {
- key: 'cpr_issue_value',
- label: 'Valor da Emissão',
- formatter: (value) => formatCurrencyBRL(value)
- },
- { key: 'cpr_delivery_place_city_name', label: 'Cidade de Entrega' },
- { key: 'cpr_delivery_place_state_acronym', label: 'Estado de Entrega' },
- { key: 'cpr_production_place_name', label: 'Local de Produção' },
- { key: 'cpr_id', label: 'Número da CPR' },
- { key: 'user_id', label: 'Código do Usuário' }
- ];
- const tokenDetailKeyTranslations = tokenDetailFieldConfig.reduce((acc, field) => {
- if (field.key) {
- acc[field.key] = field.label;
- }
- return acc;
- }, {});
- function buildTokenDetailEntries(token = {}) {
- if (!token || typeof token !== 'object') return [];
- const entries = [];
- const usedKeys = new Set();
- for (const field of tokenDetailFieldConfig) {
- const rawValue = token?.[field.key];
- if (rawValue === null || rawValue === undefined || rawValue === '') continue;
- usedKeys.add(field.key);
- const formattedValue = field.formatter
- ? field.formatter(rawValue, token)
- : typeof rawValue === 'number'
- ? formatToken(rawValue)
- : Array.isArray(rawValue)
- ? rawValue.join(', ')
- : String(rawValue);
- entries.push({ key: field.key, label: field.label, value: formattedValue });
- }
- Object.entries(token)
- .filter(([key, value]) => !hiddenTokenDetailKeys.has(key) && !usedKeys.has(key) && value !== null && value !== undefined && value !== '')
- .forEach(([key, value]) => {
- const formattedValue = typeof value === 'number'
- ? formatToken(value)
- : Array.isArray(value)
- ? value.join(', ')
- : String(value);
- const label = tokenDetailKeyTranslations[key] ?? formatTokenDetailKey(key);
- entries.push({ key, label, value: formattedValue });
- });
- return entries;
- }
- function openTokenDetailModal(token) {
- if (!token) return;
- tokenDetailSelected = token;
- tokenDetailEntries = buildTokenDetailEntries(token);
- tokenDetailModalVisible = true;
- }
- function closeTokenDetailModal() {
- tokenDetailModalVisible = false;
- tokenDetailSelected = null;
- tokenDetailEntries = [];
- }
- </script>
- <div>
- <Header title="Wallet" subtitle="Saldos e operações" breadcrumb={breadcrumb} />
- <div class="p-4 space-y-6">
- {#if tokensError}
- <div class="rounded border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-sm">{tokensError}</div>
- {/if}
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
- {#if tokensLoading && !walletTokens.length}
- <div class="rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-white/60 dark:bg-gray-800/60 p-4 text-center text-sm text-gray-500">
- Carregando tokens...
- </div>
- {:else if walletTokens.length}
- {#each walletTokens as token (token?.token_external_id ?? token?.token_id ?? token?.cpr_id ?? token)}
- <div class="rounded-lg overflow-hidden">
- <button
- type="button"
- class="block w-full text-left focus:outline-none"
- on:click={() => openTokenDetailModal(token)}
- >
- <StatsCard
- title={tokenCardLabel(token)}
- value={formatToken(tokenAmount(token))}
- change={tokenCardSubtitle(token)}
- iconSvg={tokensIcon}
- />
- </button>
- </div>
- {/each}
- {:else}
- <div class="rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-white/60 dark:bg-gray-800/60 p-4 text-center text-sm text-gray-500">
- Nenhum token encontrado para sua conta.
- </div>
- {/if}
- <div class="rounded-lg overflow-hidden">
- <StatsCard
- title="EasyCoin"
- value={formatCoin(easyCoinBalance)}
- change={tokensLoading ? 'Atualizando...' : 'Saldo atual'}
- iconSvg={walletIcon}
- />
- </div>
- </div>
- <div class="flex gap-3">
- <button
- class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white shadow"
- on:click={openPayment}
- >
- Comprar BRLA
- </button>
- <button
- class="inline-flex items-center gap-2 px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-100"
- on:click={openSell}
- >
- Vender BRLA
- </button>
- </div>
- </div>
- <PaymentMenu
- isOpen={isPaymentOpen}
- onClose={closePayment}
- onConfirm={confirmPayment}
- />
- <SellMenu
- isOpen={isSellOpen}
- onClose={closeSell}
- onConfirm={(e) => confirmSell(e.detail || e)}
- rateBRLPerEasyCoin={rateBRLPerEasyCoin}
- />
- <ModalBase
- title="Detalhes do token"
- visible={tokenDetailModalVisible}
- onClose={closeTokenDetailModal}
- >
- {#if tokenDetailSelected}
- <div class="space-y-4 text-sm text-gray-800 dark:text-gray-100">
- <div class="rounded border border-gray-200 dark:border-gray-700 p-3 space-y-2 bg-gray-50 dark:bg-gray-800/60">
- <div class="text-base font-semibold">{tokenCardLabel(tokenDetailSelected)}</div>
- <div class="flex justify-between">
- <span>Quantidade</span>
- <span>{formatToken(tokenAmount(tokenDetailSelected))}</span>
- </div>
- <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400">
- <span>Local</span>
- <span>
- {#if tokenDetailSelected?.token_city || tokenDetailSelected?.token_uf}
- {tokenDetailSelected?.token_city ?? '—'}/{tokenDetailSelected?.token_uf ?? '—'}
- {:else}
- Não informado
- {/if}
- </span>
- </div>
- </div>
- <div class="max-h-64 overflow-y-auto pr-1 space-y-1">
- {#if tokenDetailEntries.length === 0}
- <p class="text-xs text-gray-500">Sem dados adicionais para este token.</p>
- {:else}
- <dl class="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded">
- {#each tokenDetailEntries as entry}
- <div class="flex items-start justify-between px-3 py-2">
- <dt class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 w-1/2 pr-2">{entry.label}</dt>
- <dd class="text-sm text-gray-900 dark:text-gray-100 w-1/2 text-right break-words">{entry.value}</dd>
- </div>
- {/each}
- </dl>
- {/if}
- </div>
- </div>
- {:else}
- <p class="text-sm text-gray-500">Selecione um token para ver detalhes.</p>
- {/if}
- </ModalBase>
- </div>
|