+page.svelte 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. <script>
  2. import { onMount } from 'svelte';
  3. import { get } from 'svelte/store';
  4. import Header from '$lib/layout/Header.svelte';
  5. import StatsCard from '$lib/components/StatsCard.svelte';
  6. import commoditiesIcon from '$lib/assets/icons/sidebar/commodities.svg?raw';
  7. import operationsIcon from '$lib/assets/icons/sidebar/operations.svg?raw';
  8. import cprIcon from '$lib/assets/icons/sidebar/cpr.svg?raw';
  9. import usersIcon from '$lib/assets/icons/sidebar/users.svg?raw';
  10. import { authToken } from '$lib/utils/stores';
  11. const apiUrl = import.meta.env.VITE_API_URL;
  12. const breadcrumb = [{ label: 'Início', active: true }];
  13. let summaryLoading = false;
  14. let summaryError = '';
  15. let summary = null;
  16. const fallbackStats = [
  17. { title: 'Total em Commodities', value: '-', change: 'Carregando...', iconSvg: commoditiesIcon },
  18. { title: 'Operações Ativas', value: '—', change: 'Aguardando dados', iconSvg: operationsIcon },
  19. { title: 'CPRs Emitidas', value: '—', change: 'Aguardando dados', iconSvg: cprIcon },
  20. { title: 'Usuários Ativos', value: '—', change: 'Aguardando dados', iconSvg: usersIcon }
  21. ];
  22. onMount(() => {
  23. void fetchCompanySummary();
  24. });
  25. function formatCurrency(value) {
  26. return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(value || 0));
  27. }
  28. function formatInteger(value) {
  29. return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(
  30. Number(value || 0)
  31. );
  32. }
  33. async function fetchCompanySummary() {
  34. if (!apiUrl) return;
  35. summaryLoading = true;
  36. summaryError = '';
  37. try {
  38. const token = get(authToken);
  39. const headers = {
  40. 'content-type': 'application/json',
  41. ...(token ? { Authorization: `Bearer ${token}` } : {})
  42. };
  43. const bodyPayload = {};
  44. const res = await fetch(`${apiUrl}/company/summary`, {
  45. method: 'POST',
  46. headers,
  47. body: JSON.stringify(bodyPayload)
  48. });
  49. const raw = await res.text();
  50. const data = raw ? JSON.parse(raw) : null;
  51. if (!res.ok) {
  52. throw new Error(data?.msg ?? data?.message ?? 'Falha ao carregar resumo da empresa.');
  53. }
  54. const resolvedSummary = data?.summary ?? data?.data?.summary ?? data;
  55. if (!resolvedSummary || typeof resolvedSummary !== 'object') {
  56. throw new Error('Resposta inválida do resumo da empresa.');
  57. }
  58. summary = resolvedSummary;
  59. } catch (err) {
  60. console.error('[Dashboard] Erro ao buscar resumo da empresa:', err);
  61. summary = null;
  62. summaryError = err?.message ?? 'Não foi possível carregar o resumo da empresa.';
  63. } finally {
  64. summaryLoading = false;
  65. }
  66. }
  67. $: stats = summary
  68. ? [
  69. {
  70. title: 'Total em tokens disponíveis',
  71. value: formatInteger(summary.total_tokens),
  72. change: '',
  73. iconSvg: commoditiesIcon
  74. },
  75. {
  76. title: 'Operações Ativas',
  77. value: formatInteger(summary.active_operations),
  78. change: '',
  79. iconSvg: operationsIcon
  80. },
  81. {
  82. title: 'CPRs Emitidas',
  83. value: formatInteger(summary.total_cprs),
  84. change: '',
  85. iconSvg: cprIcon
  86. },
  87. {
  88. title: 'Usuários Ativos',
  89. value: formatInteger(summary.total_users),
  90. change: 'Usuários vinculados à empresa',
  91. iconSvg: usersIcon
  92. }
  93. ]
  94. : fallbackStats;
  95. </script>
  96. <Header title="Início" subtitle="Visão geral do sistema de commodities" breadcrumb={breadcrumb} />
  97. <div class="p-8 space-y-4">
  98. {#if summaryError}
  99. <div class="rounded border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-sm">
  100. {summaryError}
  101. </div>
  102. {/if}
  103. <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
  104. {#each stats as stat}
  105. <StatsCard {...stat} loading={summaryLoading && !summary} />
  106. {/each}
  107. </div>
  108. </div>