|
|
@@ -1,23 +1,34 @@
|
|
|
<script>
|
|
|
import Header from '$lib/layout/Header.svelte';
|
|
|
import TabelaOrdens from '$lib/components/trading/TabelaOrdens.svelte';
|
|
|
- import GraficoCandlestick from '$lib/components/trading/GraficoCandlestick.svelte';
|
|
|
import BoletaCompra from '$lib/components/trading/BoletaCompra.svelte';
|
|
|
import BoletaVenda from '$lib/components/trading/BoletaVenda.svelte';
|
|
|
- import { writable } from 'svelte/store';
|
|
|
- import { onMount } from 'svelte';
|
|
|
+ import ModalBase from '$lib/components/trading/ModalBase.svelte';
|
|
|
+ import { writable, get } from 'svelte/store';
|
|
|
+ import { onMount, onDestroy } from 'svelte';
|
|
|
import { browser } from '$app/environment';
|
|
|
- import { ordensCompra as ordensCompraMock, ordensVenda as ordensVendaMock, startOrdensSimulator } from '$lib/mock/ordens.js';
|
|
|
+ import { authToken } from '$lib/utils/stores';
|
|
|
|
|
|
const breadcrumb = [{ label: 'Início' }, { label: 'Trading', active: true }];
|
|
|
+ const apiUrl = import.meta.env.VITE_API_URL;
|
|
|
|
|
|
const orders = writable([]);
|
|
|
|
|
|
let currentPrice = 100;
|
|
|
|
|
|
- onMount(() => {
|
|
|
+ let ordensCompra = [];
|
|
|
+ let ordensVenda = [];
|
|
|
+ let ultimaVenda = { valor: 0, quantidade: 0 };
|
|
|
+ let orderbookLoading = false;
|
|
|
+ let orderbookError = '';
|
|
|
+ let initialized = false;
|
|
|
+ let lastFetchKey = '';
|
|
|
|
|
|
- //remover a parte que ele salva no localstorage porque vai ser conectado o endpoint
|
|
|
+ let commodities = [];
|
|
|
+ let commoditiesLoading = false;
|
|
|
+ let commoditiesError = '';
|
|
|
+
|
|
|
+ onMount(async () => {
|
|
|
if (browser) {
|
|
|
try {
|
|
|
const saved = localStorage.getItem('tradingOrders');
|
|
|
@@ -25,27 +36,149 @@
|
|
|
} catch {}
|
|
|
}
|
|
|
|
|
|
- const stopSimCompra = startOrdensSimulator({
|
|
|
- get: () => ordensCompra,
|
|
|
- set: (next) => { ordensCompra = next; },
|
|
|
- intervalMs: 1500,
|
|
|
- maxItems: 60,
|
|
|
- minItems: 8
|
|
|
- });
|
|
|
+ await fetchCommoditiesOptions();
|
|
|
+ initialized = true;
|
|
|
+ lastFetchKey = `${selectedState}|${selectedCommodity}`;
|
|
|
+ fetchOrderbook(selectedState, selectedCommodity);
|
|
|
+ });
|
|
|
|
|
|
- const stopSimVenda = startOrdensSimulator({
|
|
|
- get: () => ordensVenda,
|
|
|
- set: (next) => { ordensVenda = next; },
|
|
|
- intervalMs: 1500,
|
|
|
- maxItems: 60,
|
|
|
- minItems: 8
|
|
|
- });
|
|
|
+ async function fetchCommoditiesOptions() {
|
|
|
+ if (!apiUrl) return;
|
|
|
+ commoditiesLoading = true;
|
|
|
+ commoditiesError = '';
|
|
|
+ try {
|
|
|
+ const token = get(authToken);
|
|
|
+ const res = await fetch(`${apiUrl}/commodities/get`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
|
+ },
|
|
|
+ body: JSON.stringify({})
|
|
|
+ });
|
|
|
|
|
|
- return () => {
|
|
|
- stopSimCompra?.();
|
|
|
- stopSimVenda?.();
|
|
|
- };
|
|
|
- });
|
|
|
+ let payload = null;
|
|
|
+ try {
|
|
|
+ payload = await res.json();
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Falha ao interpretar resposta de /commodities/get:', err);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!res.ok || payload?.status !== 'ok') {
|
|
|
+ throw new Error(payload?.msg ?? 'Falha ao carregar commodities.');
|
|
|
+ }
|
|
|
+
|
|
|
+ const mapped = Array.isArray(payload?.data)
|
|
|
+ ? payload.data
|
|
|
+ .map((item) => ({
|
|
|
+ id: item?.commodities_id != null ? String(item.commodities_id) : (item?.id != null ? String(item.id) : ''),
|
|
|
+ label: (item?.commodities_name ?? item?.name ?? '').trim() || 'Sem nome',
|
|
|
+ raw: item
|
|
|
+ }))
|
|
|
+ .filter((item) => item.id)
|
|
|
+ : [];
|
|
|
+
|
|
|
+ commodities = mapped;
|
|
|
+ if (!mapped.length) {
|
|
|
+ selectedCommodity = '';
|
|
|
+ } else if (!mapped.some((c) => c.id === selectedCommodity)) {
|
|
|
+ selectedCommodity = mapped[0].id;
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Erro ao carregar commodities:', err);
|
|
|
+ commodities = [];
|
|
|
+ selectedCommodity = '';
|
|
|
+ commoditiesError = err?.message ?? 'Falha ao carregar commodities.';
|
|
|
+ } finally {
|
|
|
+ commoditiesLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const resetOrderbook = () => {
|
|
|
+ ordensCompra = [];
|
|
|
+ ordensVenda = [];
|
|
|
+ ultimaVenda = { valor: 0, quantidade: 0 };
|
|
|
+ };
|
|
|
+
|
|
|
+ function commodityPayloadValue(tokenId) {
|
|
|
+ const commodityObj = commodities.find((t) => t.id === tokenId);
|
|
|
+ if (!commodityObj) return '';
|
|
|
+ const label = (commodityObj.label ?? '').toString();
|
|
|
+ return label ? label.trim() : '';
|
|
|
+ }
|
|
|
+
|
|
|
+ async function fetchOrderbook(state = selectedState, commodityId = selectedCommodity) {
|
|
|
+ if (!apiUrl) {
|
|
|
+ orderbookError = 'URL da API não configurada.';
|
|
|
+ resetOrderbook();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const commodityPayload = commodityPayloadValue(commodityId);
|
|
|
+ if (!state || !commodityPayload) {
|
|
|
+ resetOrderbook();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ orderbookLoading = true;
|
|
|
+ orderbookError = '';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const token = get(authToken);
|
|
|
+ const res = await fetch(`${apiUrl}/token/get`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ token_uf: state,
|
|
|
+ commodities_name: commodityPayload
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ let payload = null;
|
|
|
+ try {
|
|
|
+ payload = await res.json();
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Falha ao interpretar resposta de /token/get:', err);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!res.ok || payload?.status !== 'ok') {
|
|
|
+ throw new Error(payload?.msg ?? 'Falha ao carregar ordens.');
|
|
|
+ }
|
|
|
+
|
|
|
+ const mapped = Array.isArray(payload?.data)
|
|
|
+ ? payload.data
|
|
|
+ .map((item) => ({
|
|
|
+ valor: Number(item?.token_commodities_value ?? 0),
|
|
|
+ quantidade: Number(item?.token_commodities_amount ?? 0),
|
|
|
+ city: item?.token_city ?? ''
|
|
|
+ }))
|
|
|
+ .filter((item) => Number.isFinite(item.valor) && Number.isFinite(item.quantidade))
|
|
|
+ : [];
|
|
|
+
|
|
|
+ mapped.sort((a, b) => a.valor - b.valor);
|
|
|
+
|
|
|
+ ordensCompra = [];
|
|
|
+ ordensVenda = mapped;
|
|
|
+ ultimaVenda = mapped.length
|
|
|
+ ? { valor: mapped[mapped.length - 1].valor, quantidade: mapped[mapped.length - 1].quantidade }
|
|
|
+ : { valor: 0, quantidade: 0 };
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Erro ao buscar ordens:', err);
|
|
|
+ orderbookError = err?.message ?? 'Falha ao carregar ordens.';
|
|
|
+ resetOrderbook();
|
|
|
+ } finally {
|
|
|
+ orderbookLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $: currentFilterKey = `${selectedState}|${selectedCommodity}`;
|
|
|
+ $: if (initialized && currentFilterKey !== lastFetchKey) {
|
|
|
+ lastFetchKey = currentFilterKey;
|
|
|
+ fetchOrderbook(selectedState, selectedCommodity);
|
|
|
+ }
|
|
|
|
|
|
function addOrder(order) {
|
|
|
orders.update((arr) => {
|
|
|
@@ -87,6 +220,7 @@
|
|
|
addOrder({ side: 'Compra', price, amount, total: valueFiat, status: 'Aberta', type: buyTab });
|
|
|
buyAmountFiat = '';
|
|
|
buyLimitPrice = '';
|
|
|
+ triggerInsufficient('buy');
|
|
|
}
|
|
|
|
|
|
function confirmSell() {
|
|
|
@@ -97,6 +231,7 @@
|
|
|
addOrder({ side: 'Venda', price, amount, total, status: 'Aberta', type: sellTab });
|
|
|
sellAmountEasyToken = '';
|
|
|
sellLimitPrice = '';
|
|
|
+ triggerInsufficient('sell');
|
|
|
}
|
|
|
|
|
|
const orderColumns = [
|
|
|
@@ -126,73 +261,50 @@ $: displayPendingSells = pendingSells.map((o) => ({
|
|
|
status: o.status
|
|
|
}));
|
|
|
|
|
|
- const tokens = [
|
|
|
- { id: 'TK-SOJA', label: 'Soja (saca 60kg)', price: 125.50 },
|
|
|
- { id: 'TK-MILHO', label: 'Milho (saca 60kg)', price: 78.90 },
|
|
|
- { id: 'TK-CAFE', name: 'Café (saca 60kg)' },
|
|
|
- ];
|
|
|
-
|
|
|
const stateOptions = ['SP','PR','RS','MG','MT','GO','MS','BA','SC','RO','PA'];
|
|
|
let selectedState = 'SP';
|
|
|
- let selectedCommodity = tokens?.[0]?.id ?? '';
|
|
|
+ let selectedCommodity = '';
|
|
|
|
|
|
let showBuyModal = false;
|
|
|
let showSellModal = false;
|
|
|
let prefillBuy = null;
|
|
|
|
|
|
- $: selectedCommodityObj = tokens.find((t) => t.id === selectedCommodity);
|
|
|
- $: selectedCommodityLabel = selectedCommodityObj?.label || selectedCommodityObj?.name;
|
|
|
-
|
|
|
- let ordensCompra = ordensCompraMock.slice();
|
|
|
- let ordensVenda = ordensVendaMock.slice();
|
|
|
- $: ultimaVenda = (ordensVenda && ordensVenda.length)
|
|
|
- ? { valor: Number(ordensVenda[ordensVenda.length - 1]?.valor ?? 0), quantidade: Number(ordensVenda[ordensVenda.length - 1]?.quantidade ?? 0) }
|
|
|
- : { valor: 0, quantidade: 0 };
|
|
|
-
|
|
|
- const dadosMock = [
|
|
|
- { date: '2025-10-01', valor: 9.98, high: 10.05, low: 9.90 },
|
|
|
- { date: '2025-10-02', valor: 10.12, high: 10.25, low: 9.95 },
|
|
|
- { date: '2025-10-03', valor: 9.85, high: 9.92, low: 9.60 },
|
|
|
- { date: '2025-10-04', valor: 10.30, high: 10.55, low: 10.10 },
|
|
|
- { date: '2025-10-05', valor: 10.05, high: 10.20, low: 9.85 },
|
|
|
- { date: '2025-10-06', valor: 9.70, high: 9.85, low: 9.45 },
|
|
|
- { date: '2025-10-07', valor: 10.45, high: 10.75, low: 10.25 },
|
|
|
- { date: '2025-10-08', valor: 10.60, high: 10.80, low: 10.40 },
|
|
|
- { date: '2025-10-09', valor: 9.55, high: 9.70, low: 9.20 }, // forte queda
|
|
|
- { date: '2025-10-10', valor: 9.30, high: 9.50, low: 9.00 }, // fundo do poço
|
|
|
- { date: '2025-10-11', valor: 9.85, high: 10.00, low: 9.60 }, // leve recuperação
|
|
|
- { date: '2025-10-12', valor: 10.50, high: 10.80, low: 10.30 }, // disparada
|
|
|
- { date: '2025-10-13', valor: 10.75, high: 11.10, low: 10.60 },
|
|
|
- { date: '2025-10-14', valor: 10.95, high: 11.20, low: 10.80 }, // pico máximo
|
|
|
- { date: '2025-10-15', valor: 10.40, high: 10.60, low: 10.10 },
|
|
|
- { date: '2025-10-16', valor: 10.10, high: 10.25, low: 9.90 },
|
|
|
- { date: '2025-10-17', valor: 9.85, high: 9.95, low: 9.60 },
|
|
|
- { date: '2025-10-18', valor: 9.20, high: 9.35, low: 8.95 }, // nova queda brusca
|
|
|
- { date: '2025-10-19', valor: 8.85, high: 9.00, low: 8.55 },
|
|
|
- { date: '2025-10-20', valor: 9.50, high: 9.70, low: 9.20 },
|
|
|
- { date: '2025-10-21', valor: 9.95, high: 10.10, low: 9.80 },
|
|
|
- { date: '2025-10-22', valor: 10.25, high: 10.50, low: 10.05 },
|
|
|
- { date: '2025-10-23', valor: 10.90, high: 11.15, low: 10.75 }, // novo pico
|
|
|
- { date: '2025-10-24', valor: 11.40, high: 11.75, low: 11.10 },
|
|
|
- { date: '2025-10-25', valor: 13.75, high: 13.95, low: 13.50 },
|
|
|
- { date: '2025-10-26', valor: 13.60, high: 13.80, low: 13.40 },
|
|
|
- { date: '2025-10-27', valor: 9.30, high: 9.45, low: 9.00 }, // queda acentuada
|
|
|
- { date: '2025-10-28', valor: 9.10, high: 9.25, low: 8.80 },
|
|
|
- { date: '2025-10-29', valor: 9.60, high: 9.80, low: 9.40 },
|
|
|
- { date: '2025-10-30', valor: 10.25, high: 10.50, low: 10.05 },
|
|
|
- { date: '2025-10-31', valor: 10.80, high: 11.00, low: 10.65 }, // novo rally
|
|
|
- { date: '2025-11-01', valor: 11.20, high: 11.45, low: 11.00 },
|
|
|
- { date: '2025-11-02', valor: 10.95, high: 11.10, low: 10.70 },
|
|
|
- { date: '2025-11-03', valor: 10.50, high: 10.65, low: 10.30 },
|
|
|
- { date: '2025-11-04', valor: 9.80, high: 9.95, low: 9.55 }, // nova correção
|
|
|
- { date: '2025-11-05', valor: 10.05, high: 10.30, low: 9.85 }
|
|
|
- ];
|
|
|
+ let insufficientVisible = false;
|
|
|
+ let insufficientType = 'buy';
|
|
|
+ let insufficientTimer = null;
|
|
|
+
|
|
|
+ function triggerInsufficient(type) {
|
|
|
+ insufficientType = type;
|
|
|
+ insufficientVisible = true;
|
|
|
+ if (insufficientTimer) clearTimeout(insufficientTimer);
|
|
|
+ insufficientTimer = setTimeout(() => {
|
|
|
+ insufficientVisible = false;
|
|
|
+ insufficientTimer = null;
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ function closeInsufficient() {
|
|
|
+ insufficientVisible = false;
|
|
|
+ if (insufficientTimer) {
|
|
|
+ clearTimeout(insufficientTimer);
|
|
|
+ insufficientTimer = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ onDestroy(() => {
|
|
|
+ if (insufficientTimer) clearTimeout(insufficientTimer);
|
|
|
+ });
|
|
|
+
|
|
|
+ $: selectedCommodityObj = commodities.find((t) => t.id === selectedCommodity);
|
|
|
+ $: selectedCommodityLabel = selectedCommodityObj?.label;
|
|
|
+
|
|
|
+ $: ultimaVenda;
|
|
|
</script>
|
|
|
|
|
|
<div>
|
|
|
<Header title="Trading" subtitle="Trading do sistema" breadcrumb={breadcrumb}>
|
|
|
<svelte:fragment slot="extra">
|
|
|
- <div class="flex w-full items-center justify-between gap-4 md:gap-10">
|
|
|
+ <div class="flex w-full items-end justify-between gap-4 md:gap-10">
|
|
|
<div class="flex gap-8">
|
|
|
<div class="flex flex-col items-center gap-2">
|
|
|
<span class="text-xs text-gray-600 dark:text-gray-300">Estado</span>
|
|
|
@@ -204,10 +316,19 @@ $: displayPendingSells = pendingSells.map((o) => ({
|
|
|
</div>
|
|
|
<div class="flex flex-col items-center gap-2">
|
|
|
<span class="text-xs text-gray-600 dark:text-gray-300">Commodity</span>
|
|
|
- <select bind:value={selectedCommodity} class="block w-56 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
- {#each tokens as t}
|
|
|
- <option value={t.id}>{t.label || t.name}</option>
|
|
|
- {/each}
|
|
|
+ {#if commoditiesError}
|
|
|
+ <div class="text-xs text-red-500 mb-1">{commoditiesError}</div>
|
|
|
+ {/if}
|
|
|
+ <select bind:value={selectedCommodity} class="block w-56 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60" disabled={commoditiesLoading || !commodities.length}>
|
|
|
+ {#if commoditiesLoading}
|
|
|
+ <option>Carregando...</option>
|
|
|
+ {:else if !commodities.length}
|
|
|
+ <option value="">Sem opções</option>
|
|
|
+ {:else}
|
|
|
+ {#each commodities as t}
|
|
|
+ <option value={t.id}>{t.label}</option>
|
|
|
+ {/each}
|
|
|
+ {/if}
|
|
|
</select>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -222,17 +343,49 @@ $: displayPendingSells = pendingSells.map((o) => ({
|
|
|
|
|
|
<div class="p-4 space-y-4">
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
- <div class="md:col-span-1">
|
|
|
+ <div class="md:col-span-1 space-y-2">
|
|
|
+ {#if orderbookError}
|
|
|
+ <div class="rounded border border-red-300 bg-red-50 text-sm text-red-700 px-3 py-2 dark:border-red-700 dark:bg-red-900/30 dark:text-red-200">{orderbookError}</div>
|
|
|
+ {:else if orderbookLoading}
|
|
|
+ <div class="rounded border border-gray-200 bg-gray-50 text-sm text-gray-700 px-3 py-2 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-200">Carregando orderbook...</div>
|
|
|
+ {/if}
|
|
|
+
|
|
|
<TabelaOrdens {ordensCompra} {ordensVenda} {ultimaVenda}
|
|
|
on:selectSellOrder={(e) => { const { valor, quantidade } = e.detail; prefillBuy = { valorSaca: valor, quantidade }; showBuyModal = true; }}
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="md:col-span-2">
|
|
|
- <GraficoCandlestick {dadosMock} />
|
|
|
+ <div class="h-full min-h-[24rem] rounded border border-dashed border-gray-300 dark:border-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800">
|
|
|
+ <div class="text-center">
|
|
|
+ <p class="font-semibold">Gráfico indisponível</p>
|
|
|
+ <p class="text-sm">Aguardando integração</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <BoletaCompra visible={showBuyModal} onClose={() => (showBuyModal = false)} state={selectedState} commodity={selectedCommodityLabel} prefill={prefillBuy} />
|
|
|
- <BoletaVenda visible={showSellModal} onClose={() => (showSellModal = false)} state={selectedState} commodity={selectedCommodityLabel} />
|
|
|
+ <BoletaCompra visible={showBuyModal} onClose={() => (showBuyModal = false)} state={selectedState} commodity={selectedCommodityLabel} prefill={prefillBuy}
|
|
|
+ on:confirm={(e) => {
|
|
|
+ const { valorSaca, quantidade } = e.detail || {};
|
|
|
+ buyTab = 'limit';
|
|
|
+ buyLimitPrice = valorSaca ? String(valorSaca) : '';
|
|
|
+ buyAmountFiat = valorSaca && quantidade ? String(valorSaca * quantidade) : '';
|
|
|
+ confirmBuy();
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <BoletaVenda visible={showSellModal} onClose={() => (showSellModal = false)} state={selectedState} commodity={selectedCommodityLabel}
|
|
|
+ on:confirm={(e) => {
|
|
|
+ const { valorSaca, quantidade } = e.detail || {};
|
|
|
+ sellTab = 'limit';
|
|
|
+ sellLimitPrice = valorSaca ? String(valorSaca) : '';
|
|
|
+ sellAmountEasyToken = quantidade ? String(quantidade) : '';
|
|
|
+ confirmSell();
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <ModalBase title="Saldo insuficiente" visible={insufficientVisible} onClose={closeInsufficient}>
|
|
|
+ <p class="text-sm text-gray-800 dark:text-gray-100">
|
|
|
+ Saldo insuficiente para {insufficientType === 'buy' ? 'iniciar uma compra' : 'iniciar uma venda'}.
|
|
|
+ </p>
|
|
|
+ </ModalBase>
|
|
|
</div>
|