TabelaOrdens.svelte 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <script>
  2. import { createEventDispatcher } from 'svelte';
  3. export let ordensCompra = [];
  4. export let ordensVenda = [];
  5. export let ultimaVenda = { valor: 0, quantidade: 0 };
  6. export let emptyMessage = '';
  7. const dispatch = createEventDispatcher();
  8. function formatBRL(n) { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(n || 0)); }
  9. function formatQty(n) { return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(Number(n || 0)); }
  10. $: listaCompra = (ordensCompra || []).map((d) => ({
  11. ...d,
  12. valor: Number(d?.valor ?? 0),
  13. quantidade: Number(d?.quantidade ?? 0),
  14. city: d?.city ?? d?.cityName ?? '',
  15. cityName: d?.cityName ?? d?.city ?? '',
  16. measureUnitName: d?.measureUnitName ?? ''
  17. }));
  18. $: listaCompraView = (listaCompra || []).map((o, __idx) => ({ ...o, __idx })).slice().reverse();
  19. $: listaVenda = (ordensVenda || []).map((d) => ({
  20. ...d,
  21. valor: Number(d?.valor ?? 0),
  22. quantidade: Number(d?.quantidade ?? 0),
  23. city: d?.city ?? d?.cityName ?? '',
  24. cityName: d?.cityName ?? d?.city ?? '',
  25. measureUnitName: d?.measureUnitName ?? '',
  26. raw: d?.raw ?? d
  27. }));
  28. $: listaVendaView = (listaVenda || []).map((o, __idx) => ({ ...o, __idx })).slice().reverse();
  29. // `ultimaVenda` agora vem por prop do pai
  30. let flashCompra = new Set();
  31. let flashVenda = new Set();
  32. let prevCompraLen = listaCompra?.length || 0;
  33. let prevVendaLen = listaVenda?.length || 0;
  34. let hoverTimer = null;
  35. let tooltipVisible = false;
  36. let tooltipCity = '';
  37. let tooltipPrice = 0;
  38. let tooltipQty = 0;
  39. let tooltipStyle = 'top:0px;left:0px;';
  40. let tooltipTotal = 0;
  41. function scheduleTooltip(event, order) {
  42. if (hoverTimer) clearTimeout(hoverTimer);
  43. const rect = event.currentTarget?.getBoundingClientRect?.();
  44. if (!rect) return;
  45. const top = rect.top + (typeof window !== 'undefined' ? window.scrollY : 0) + rect.height / 2;
  46. const left = rect.right + (typeof window !== 'undefined' ? window.scrollX : 0) + 12;
  47. hoverTimer = setTimeout(() => {
  48. tooltipCity = order?.cityName || order?.city || '';
  49. tooltipPrice = Number(order?.valor ?? 0);
  50. tooltipQty = Number(order?.quantidade ?? 0);
  51. tooltipTotal = Number(order?.total ?? order?.raw?.token_commodities_value ?? (tooltipPrice * tooltipQty));
  52. tooltipStyle = `top:${top}px;left:${left}px;`;
  53. tooltipVisible = true;
  54. }, 300);
  55. }
  56. function hideTooltip() {
  57. if (hoverTimer) {
  58. clearTimeout(hoverTimer);
  59. hoverTimer = null;
  60. }
  61. tooltipVisible = false;
  62. }
  63. $: {
  64. const curr = listaCompra?.length || 0;
  65. if (curr > prevCompraLen) {
  66. for (let i = prevCompraLen; i < curr; i++) {
  67. flashCompra.add(i);
  68. setTimeout(() => { flashCompra.delete(i); }, 1500);
  69. }
  70. }
  71. prevCompraLen = curr;
  72. }
  73. $: {
  74. const curr = listaVenda?.length || 0;
  75. if (curr > prevVendaLen) {
  76. for (let i = prevVendaLen; i < curr; i++) {
  77. flashVenda.add(i);
  78. setTimeout(() => { flashVenda.delete(i); }, 1500);
  79. }
  80. }
  81. prevVendaLen = curr;
  82. }
  83. </script>
  84. <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm h-full">
  85. <div class="px-4 py-2 bg-yellow-100 dark:bg-yellow-900/40 border-b border-yellow-300 dark:border-yellow-700 text-sm text-yellow-900 dark:text-yellow-200">
  86. <div class="flex items-center justify-between">
  87. <span>Última Venda</span>
  88. <span class="font-medium">{formatBRL(ultimaVenda?.valor)} · {formatQty(ultimaVenda?.quantidade)}</span>
  89. </div>
  90. </div>
  91. <div class="h-[70vh] overflow-auto grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700 scroll-ordens">
  92. <div class="p-4">
  93. <div class="flex flex-col items-center justify-between mb-2">
  94. <h3 class="text-sm font-semibold text-green-600">Ordem de Compra</h3>
  95. <div class="text-xs text-gray-500">VALOR · QUANTIDADE</div>
  96. </div>
  97. <div class="space-y-1">
  98. {#if listaCompraView.length === 0}
  99. <div class="text-xs text-gray-500 dark:text-gray-400 text-center py-4 border border-dashed border-gray-200 dark:border-gray-700 rounded">
  100. {emptyMessage || 'Nenhuma ordem de compra disponível.'}
  101. </div>
  102. {:else}
  103. {#each listaCompraView as o}
  104. <div
  105. 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' : ''}`}
  106. role="listitem"
  107. on:mouseenter={(event) => scheduleTooltip(event, o)}
  108. on:mouseleave={hideTooltip}
  109. >
  110. <span class="text-green-600">{formatBRL(o.valor)}</span>
  111. <span class="text-gray-700 dark:text-gray-200">{formatQty(o.quantidade)}</span>
  112. </div>
  113. {/each}
  114. {/if}
  115. </div>
  116. </div>
  117. <div class="p-4">
  118. <div class="flex flex-col items-center justify-between mb-2">
  119. <h3 class="text-sm font-semibold text-red-600">Ordem de Venda</h3>
  120. <div class="text-xs text-gray-500">VALOR · QUANTIDADE</div>
  121. </div>
  122. <div class="space-y-1">
  123. {#if listaVendaView.length === 0}
  124. <div class="text-xs text-gray-500 dark:text-gray-400 text-center py-4 border border-dashed border-gray-200 dark:border-gray-700 rounded">
  125. {emptyMessage || 'Nenhuma ordem de venda disponível.'}
  126. </div>
  127. {:else}
  128. {#each listaVendaView as o}
  129. <button
  130. type="button"
  131. class={`flex w-full 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' : ''}`}
  132. on:mouseenter={(event) => scheduleTooltip(event, o)}
  133. on:mouseleave={hideTooltip}
  134. on:focus={(event) => scheduleTooltip(event, o)}
  135. on:blur={hideTooltip}
  136. on:click={() =>
  137. dispatch('selectSellOrder', {
  138. valor: o.valor,
  139. quantidade: o.quantidade,
  140. city: o.city,
  141. cityName: o.cityName,
  142. measureUnitName: o.measureUnitName,
  143. orderbookId: o.orderbookId ?? o.orderbook_id ?? o.raw?.orderbook_id ?? null,
  144. tokenExternalId: o.tokenExternalId ?? o.token_external_id ?? o.raw?.token_external_id ?? '',
  145. raw: o.raw ?? o
  146. })}
  147. >
  148. <span class="text-red-600">{formatBRL(o.valor)}</span>
  149. <span class="text-gray-700 dark:text-gray-200">{formatQty(o.quantidade)}</span>
  150. </button>
  151. {/each}
  152. {/if}
  153. </div>
  154. </div>
  155. </div>
  156. {#if tooltipVisible}
  157. <div class="order-tooltip" style={tooltipStyle}>
  158. <div class="space-y-1">
  159. <div>
  160. <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Cidade</div>
  161. <div class="text-sm text-gray-800 dark:text-gray-100">{tooltipCity || 'Não informado'}</div>
  162. </div>
  163. <div class="grid grid-cols-2 gap-3 text-sm">
  164. <div>
  165. <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Preço</div>
  166. <div class="text-gray-800 dark:text-gray-100">{formatBRL(tooltipPrice)}</div>
  167. </div>
  168. <div>
  169. <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Quantidade</div>
  170. <div class="text-gray-800 dark:text-gray-100">{formatQty(tooltipQty)}</div>
  171. </div>
  172. </div>
  173. <div class="text-sm pt-1 border-t border-gray-100 dark:border-gray-700">
  174. <div class="text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500">Valor total</div>
  175. <div class="text-gray-900 dark:text-gray-100 font-semibold">{formatBRL(tooltipTotal)}</div>
  176. </div>
  177. </div>
  178. </div>
  179. {/if}
  180. </div>
  181. <style>
  182. :global(.scroll-ordens) {
  183. scrollbar-width: thin; /* Firefox */
  184. scrollbar-color: #cbd5e1 #f3f4f6; /* thumb track */
  185. }
  186. :global(.dark .scroll-ordens) {
  187. scrollbar-color: #374151 #1f2937; /* thumb track */
  188. }
  189. :global(.scroll-ordens::-webkit-scrollbar) {
  190. width: 10px;
  191. height: 10px;
  192. }
  193. :global(.scroll-ordens::-webkit-scrollbar-track) {
  194. background: #f3f4f6; /* gray-100 */
  195. }
  196. :global(.scroll-ordens::-webkit-scrollbar-thumb) {
  197. background-color: #cbd5e1; /* slate-300 */
  198. border-radius: 9999px;
  199. border: 2px solid #f3f4f6; /* match track */
  200. }
  201. :global(.dark .scroll-ordens::-webkit-scrollbar-track) {
  202. background: #1f2937; /* gray-800 */
  203. }
  204. :global(.dark .scroll-ordens::-webkit-scrollbar-thumb) {
  205. background-color: #374151; /* gray-700 */
  206. border-color: #1f2937; /* match dark track */
  207. }
  208. .order-tooltip {
  209. position: fixed;
  210. z-index: 40;
  211. padding: 0.5rem 0.65rem;
  212. background: #fff;
  213. border-radius: 0.375rem;
  214. border: 1px solid #e5e7eb;
  215. box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
  216. pointer-events: none;
  217. min-width: 120px;
  218. transform: translateY(-50%);
  219. }
  220. :global(.dark) .order-tooltip {
  221. background: #1f2937;
  222. border-color: #374151;
  223. }
  224. </style>