|
|
@@ -8,6 +8,7 @@
|
|
|
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;
|
|
|
@@ -26,6 +27,10 @@
|
|
|
void fetchWalletTokens();
|
|
|
});
|
|
|
|
|
|
+ let tokenDetailModalVisible = false;
|
|
|
+ let tokenDetailSelected = null;
|
|
|
+ let tokenDetailEntries = [];
|
|
|
+
|
|
|
function openPayment() {
|
|
|
isPaymentOpen = true;
|
|
|
}
|
|
|
@@ -105,6 +110,10 @@
|
|
|
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';
|
|
|
}
|
|
|
@@ -130,6 +139,108 @@
|
|
|
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>
|
|
|
@@ -148,12 +259,18 @@
|
|
|
{: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">
|
|
|
- <StatsCard
|
|
|
- title={tokenCardLabel(token)}
|
|
|
- value={formatToken(tokenAmount(token))}
|
|
|
- change={tokenCardSubtitle(token)}
|
|
|
- iconSvg={tokensIcon}
|
|
|
- />
|
|
|
+ <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}
|
|
|
@@ -200,4 +317,49 @@
|
|
|
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>
|