Explorar o código

orderbook changes

ljoaquim hai 2 semanas
pai
achega
bdc56df172

+ 2 - 0
.env.example

@@ -0,0 +1,2 @@
+# Base URL for the backend API consumed by the Svelte front-end
+VITE_API_URL=https://api.example.com

+ 4 - 0
src/lib/components/trading/BoletaCompra.svelte

@@ -3,6 +3,7 @@
   import flatpickr from 'flatpickr';
   import 'flatpickr/dist/flatpickr.min.css';
   import { Portuguese } from 'flatpickr/dist/l10n/pt.js';
+  import { createEventDispatcher } from 'svelte';
 
   export let visible = false;
   export let onClose = () => {};
@@ -10,6 +11,8 @@
   export let commodity = '';
   export let prefill = null;
 
+  const dispatch = createEventDispatcher();
+
   let valorSaca = '';
   let quantidade = '';
   let referencia = '';
@@ -64,6 +67,7 @@
       commodity
     };
     console.log('BoletaCompra confirmada', dados);
+    dispatch('confirm', dados);
     try { onClose?.(); } catch {}
   }
 

+ 3 - 0
src/lib/components/trading/BoletaVenda.svelte

@@ -3,10 +3,12 @@
   import flatpickr from 'flatpickr';
   import 'flatpickr/dist/flatpickr.min.css';
   import { Portuguese } from 'flatpickr/dist/l10n/pt.js';
+  import { createEventDispatcher } from 'svelte';
   export let visible = false;
   export let onClose = () => {};
   export let state = '';
   export let commodity = '';
+  const dispatch = createEventDispatcher();
   let valorSaca = '';
   let quantidade = '';
   let referencia = 'BRL : 12';
@@ -40,6 +42,7 @@
   function confirmar() {
     const dados = { tipo: 'venda', valorSaca: Number(valorSaca)||0, quantidade: Number(quantidade)||0, referencia, total, validade: validadeInput, calendario, estado: state, commodity };
     console.log('BoletaVenda confirmada', dados);
+    dispatch('confirm', dados);
     try { onClose?.(); } catch {}
   }
   function formatBRL(n) { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(n||0)); }

+ 85 - 4
src/lib/components/trading/TabelaOrdens.svelte

@@ -7,9 +7,9 @@
   function formatBRL(n) { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(n || 0)); }
   function formatQty(n) { return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(Number(n || 0)); }
 
-  $: listaCompra = (ordensCompra || []).map((d) => ({ valor: Number(d?.valor ?? 0), quantidade: Number(d?.quantidade ?? 0) }));
+  $: listaCompra = (ordensCompra || []).map((d) => ({ valor: Number(d?.valor ?? 0), quantidade: Number(d?.quantidade ?? 0), city: d?.city ?? '' }));
   $: listaCompraView = (listaCompra || []).map((o, __idx) => ({ ...o, __idx })).slice().reverse();
-  $: listaVenda = (ordensVenda || []).map((d) => ({ valor: Number(d?.valor ?? 0), quantidade: Number(d?.quantidade ?? 0) }));
+  $: listaVenda = (ordensVenda || []).map((d) => ({ valor: Number(d?.valor ?? 0), quantidade: Number(d?.quantidade ?? 0), city: d?.city ?? '' }));
   $: listaVendaView = (listaVenda || []).map((o, __idx) => ({ ...o, __idx })).slice().reverse();
   // `ultimaVenda` agora vem por prop do pai
 
@@ -18,6 +18,36 @@
   let prevCompraLen = listaCompra?.length || 0;
   let prevVendaLen = listaVenda?.length || 0;
 
+  let hoverTimer = null;
+  let tooltipVisible = false;
+  let tooltipCity = '';
+  let tooltipPrice = 0;
+  let tooltipQty = 0;
+  let tooltipStyle = 'top:0px;left:0px;';
+
+  function scheduleTooltip(event, order) {
+    if (hoverTimer) clearTimeout(hoverTimer);
+    const rect = event.currentTarget?.getBoundingClientRect?.();
+    if (!rect) return;
+    const top = rect.top + (typeof window !== 'undefined' ? window.scrollY : 0) + rect.height / 2;
+    const left = rect.right + (typeof window !== 'undefined' ? window.scrollX : 0) + 12;
+    hoverTimer = setTimeout(() => {
+      tooltipCity = order?.city || '';
+      tooltipPrice = Number(order?.valor ?? 0);
+      tooltipQty = Number(order?.quantidade ?? 0);
+      tooltipStyle = `top:${top}px;left:${left}px;`;
+      tooltipVisible = true;
+    }, 300);
+  }
+
+  function hideTooltip() {
+    if (hoverTimer) {
+      clearTimeout(hoverTimer);
+      hoverTimer = null;
+    }
+    tooltipVisible = false;
+  }
+
   $: {
     const curr = listaCompra?.length || 0;
     if (curr > prevCompraLen) {
@@ -55,7 +85,11 @@
       </div>
       <div class="space-y-1">
         {#each listaCompraView as o}
-        <div class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 ${flashCompra.has(o.__idx) ? 'bg-green-100 dark:bg-green-900/30' : ''}`}>
+        <div
+          class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 ${flashCompra.has(o.__idx) ? 'bg-green-100 dark:bg-green-900/30' : ''}`}
+          on:mouseenter={(event) => scheduleTooltip(event, o)}
+          on:mouseleave={hideTooltip}
+        >
           <span class="text-green-600">{formatBRL(o.valor)}</span>
           <span class="text-gray-700 dark:text-gray-200">{formatQty(o.quantidade)}</span>
         </div>
@@ -69,7 +103,12 @@
       </div>
       <div class="space-y-1">
         {#each listaVendaView as o}
-        <div class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 cursor-pointer hover:bg-red-50 dark:hover:bg-red-900/40 ${flashVenda.has(o.__idx) ? 'bg-red-100 dark:bg-red-900/30' : ''}`} on:click={() => dispatch('selectSellOrder', { valor: o.valor, quantidade: o.quantidade })}>
+        <div
+          class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 cursor-pointer hover:bg-red-50 dark:hover:bg-red-900/40 ${flashVenda.has(o.__idx) ? 'bg-red-100 dark:bg-red-900/30' : ''}`}
+          on:mouseenter={(event) => scheduleTooltip(event, o)}
+          on:mouseleave={hideTooltip}
+          on:click={() => dispatch('selectSellOrder', { valor: o.valor, quantidade: o.quantidade })}
+        >
           <span class="text-red-600">{formatBRL(o.valor)}</span>
           <span class="text-gray-700 dark:text-gray-200">{formatQty(o.quantidade)}</span>
         </div>
@@ -77,6 +116,31 @@
       </div>
     </div>
   </div>
+
+  {#if tooltipVisible}
+    <div class="order-tooltip" style={tooltipStyle}>
+      <div class="space-y-1">
+        <div>
+          <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Cidade</div>
+          <div class="text-sm text-gray-800 dark:text-gray-100">{tooltipCity || 'Não informado'}</div>
+        </div>
+        <div class="grid grid-cols-2 gap-3 text-sm">
+          <div>
+            <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Preço</div>
+            <div class="text-gray-800 dark:text-gray-100">{formatBRL(tooltipPrice)}</div>
+          </div>
+          <div>
+            <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Quantidade</div>
+            <div class="text-gray-800 dark:text-gray-100">{formatQty(tooltipQty)}</div>
+          </div>
+        </div>
+        <div class="text-sm pt-1 border-t border-gray-100 dark:border-gray-700">
+          <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Valor total</div>
+          <div class="text-gray-900 dark:text-gray-100 font-semibold">{formatBRL(tooltipPrice * tooltipQty)}</div>
+        </div>
+      </div>
+    </div>
+  {/if}
 </div>
 
 <style>
@@ -107,4 +171,21 @@
     background-color: #374151;             /* gray-700 */
     border-color: #1f2937;                 /* match dark track */
   }
+
+  .order-tooltip {
+    position: fixed;
+    z-index: 40;
+    padding: 0.5rem 0.65rem;
+    background: #fff;
+    border-radius: 0.375rem;
+    border: 1px solid #e5e7eb;
+    box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
+    pointer-events: none;
+    min-width: 120px;
+    transform: translateY(-50%);
+  }
+  :global(.dark) .order-tooltip {
+    background: #1f2937;
+    border-color: #374151;
+  }
 </style>

+ 241 - 88
src/routes/trading/+page.svelte

@@ -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>