Browse Source

add the history cpr

gdias 3 tuần trước cách đây
mục cha
commit
3ee7edf3b8

+ 89 - 0
src/lib/components/commodities/cpr/CprDetailModal.svelte

@@ -0,0 +1,89 @@
+<script>
+  import { createEventDispatcher } from 'svelte';
+
+  export let visible = false;
+  export let title = 'Detalhes da CPR';
+  export let detailId = null;
+  export let loading = false;
+  export let error = '';
+  export let detail = null;
+  export let formatKey = (key) => key;
+  export let formatValue = (value) => value ?? '—';
+
+  const dispatch = createEventDispatcher();
+
+  function handleClose() {
+    dispatch('close');
+  }
+
+  function handleRetry() {
+    dispatch('retry');
+  }
+</script>
+
+{#if visible}
+  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
+    <div class="w-full max-w-4xl rounded-xl bg-white shadow-xl dark:bg-gray-900 dark:text-gray-100">
+      <div class="flex items-center justify-between border-b border-gray-200 px-6 py-4 dark:border-gray-800">
+        <div>
+          <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
+            {title} {#if detailId}<span class="text-gray-400 dark:text-gray-500">#{detailId}</span>{/if}
+          </h2>
+          {#if detailId}
+            <p class="text-sm text-gray-500 dark:text-gray-400">
+              Informações completas da CPR selecionada.
+            </p>
+          {/if}
+        </div>
+        <button
+          class="text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
+          type="button"
+          on:click={handleClose}
+          aria-label="Fechar"
+        >
+          ✕
+        </button>
+      </div>
+
+      <div class="max-h-[70vh] overflow-y-auto px-6 py-5">
+        {#if loading}
+          <p class="text-sm text-gray-600 dark:text-gray-300">Carregando detalhes...</p>
+        {:else if error}
+          <div class="space-y-3">
+            <div class="rounded border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-200">
+              {error}
+            </div>
+            <button
+              class="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
+              type="button"
+              on:click={handleRetry}
+            >
+              Tentar novamente
+            </button>
+          </div>
+        {:else if detail && Object.keys(detail).length}
+          <div class="grid gap-4 md:grid-cols-2">
+            {#each Object.entries(detail) as [key, value]}
+              <div class="rounded border border-gray-200 bg-white/60 p-3 dark:border-gray-700 dark:bg-gray-800/60">
+                <p class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">{formatKey(key)}</p>
+                <p class="text-sm font-medium text-gray-900 dark:text-gray-100">{formatValue(value)}</p>
+              </div>
+            {/each}
+          </div>
+        {:else}
+          <p class="text-sm text-gray-600 dark:text-gray-300">Nenhuma informação disponível para esta CPR.</p>
+        {/if}
+      </div>
+
+      <div class="flex justify-end gap-2 border-t border-gray-200 px-6 py-4 dark:border-gray-800">
+        <button
+          class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
+          type="button"
+          on:click={handleClose}
+        >
+          Fechar
+        </button>
+      </div>
+    </div>
+  </div>
+{/if}

+ 267 - 77
src/routes/cpr/+page.svelte

@@ -1,13 +1,13 @@
 <script>
+  import { onMount } from 'svelte';
+  import { get } from 'svelte/store';
   import Header from '$lib/layout/Header.svelte';
   import Tabs from '$lib/components/Tabs.svelte';
   import RegisterCpr from '$lib/components/commodities/cpr/RegisterCpr.svelte';
-  import Tables from '$lib/components/Tables.svelte';
   import ContractCpr from '$lib/components/commodities/cpr/ContractCpr.svelte';
   import EmissionCpr from '$lib/components/commodities/cpr/EmissionCpr.svelte';
-  import CprEditModal from '$lib/components/commodities/cpr/CprEditModal.svelte';
-  import ConfirmModal from '$lib/components/ui/PopUpDelete.svelte';
-  import { authToken } from '$lib/utils/stores';
+  import CprDetailModal from '$lib/components/commodities/cpr/CprDetailModal.svelte';
+  import { authToken, companyId as companyIdStore } from '$lib/utils/stores';
 
   const apiUrl = import.meta.env.VITE_API_URL;
 
@@ -276,28 +276,27 @@
   let submitSuccess = '';
   let isSubmitting = false;
 
-  // EditModal
-  let showCprEdit = false;
-  let selectedRow = null;
-  let editValue = {};
-
-  // Delete confirmation
-  let showDeleteConfirm = false;
-  let rowToDelete = null;
-
   const breadcrumb = [{ label: 'Início' }, { label: 'CPR', active: true }];
   let activeTab = 4;
-  const tabs = ["Contrato", "Registro", "Emissão"];
-
-  let columns = [
-    { key: "nome", label: "Nome" },
-    { key: "status", label: "Status" }
-  ];
-
-  let data = [
-    { nome: "CPR A", status: "Disponível" },
-    { nome: "CPR B", status: "Indisponível" }
+  const tabs = ['Contrato', 'Registro', 'Emissão'];
+
+  const historyEndpoint = `${apiUrl}/cpr/history`;
+  const historyColumns = [
+    { key: 'cpr_id', label: 'CPR ID' },
+    { key: 'cpr_product_class_name', label: 'Produto' },
+    { key: 'cpr_issue_date', label: 'Data de emissão' },
+    { key: 'cpr_issuer_name', label: 'Emitente' },
+    { key: 'cpr_issue_financial_value', label: 'Valor' }
   ];
+  let historyRows = [];
+  let historyLoading = false;
+  let historyError = '';
+  let detailLoading = false;
+  let detailError = '';
+  let selectedDetail = null;
+  let selectedDetailId = null;
+  let showDetailModal = false;
+  let historyInitialized = false;
 
   function handleFieldChange(key, value) {
     submitError = '';
@@ -353,49 +352,173 @@
     activeTab = 0;
   }
 
-  function handleDeleteRow(e) {
-    const { row } = e.detail;
-    rowToDelete = row;
-    showDeleteConfirm = true;
+  function handleCancel() {
+    activeTab = 4;
+    submitError = '';
   }
 
-  function handleEditRow(e) {
-    const { row } = e.detail;
-    selectedRow = row;
-    editValue = { ...row };
-    showCprEdit = true;
+  function ensureAuthContext() {
+    const token = get(authToken);
+    if (!token) {
+      throw new Error('Sessão expirada. Faça login novamente.');
+    }
+    const company = Number(get(companyIdStore));
+    if (!company || Number.isNaN(company) || company <= 0) {
+      throw new Error('company_id inválido. Refaça o login.');
+    }
+    return { token, company_id: company };
   }
 
-  function handleCprSave(e) {
-    const { value } = e.detail;
-    if (selectedRow) {
-      data = data.map((r) => (r === selectedRow ? { ...r, ...value } : r));
+  async function parseJsonResponse(res) {
+    const raw = await res.text();
+    if (!raw) return null;
+    const trimmed = raw.trim();
+
+    if (trimmed.startsWith('<')) {
+      console.error('Resposta HTML inesperada do endpoint de histórico/detalhe de CPR:', trimmed.slice(0, 300));
+      const statusMsg =
+        res.status === 404
+          ? 'Endpoint /cpr/history não encontrado (404). Verifique a URL da API.'
+          : `Resposta inesperada do servidor (status ${res.status}).`;
+      throw new Error(statusMsg);
+    }
+
+    try {
+      return JSON.parse(trimmed);
+    } catch (err) {
+      console.error('Resposta inválida do histórico de CPRs:', err, trimmed);
+      throw new Error('Resposta inválida do servidor.');
     }
-    handleCprCancel();
   }
 
-  function handleCprCancel() {
-    showCprEdit = false;
-    selectedRow = null;
-    editValue = {};
+  function normalizeHistoryList(payload) {
+    if (Array.isArray(payload)) return payload;
+    if (Array.isArray(payload?.data)) return payload.data;
+    return [];
   }
 
-  function handleCancel() {
-    activeTab = 4;
-    submitError = '';
+  function sanitizeDetail(detail) {
+    if (!detail || typeof detail !== 'object') return null;
+    const cleaned = {};
+    Object.entries(detail).forEach(([key, value]) => {
+      if (value === null || value === undefined) return;
+      if (typeof value === 'string' && value.trim().toUpperCase() === 'NA') return;
+      cleaned[key] = value;
+    });
+    return Object.keys(cleaned).length ? cleaned : null;
+  }
+
+  function formatDetailValue(value) {
+    if (value == null) return '—';
+    if (typeof value === 'number') {
+      return value.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 4 });
+    }
+    return String(value);
   }
 
-  function confirmDelete() {
-    data = data.filter((r) => r !== rowToDelete);
-    showDeleteConfirm = false;
-    rowToDelete = null;
+  function humanizeKey(key = '') {
+    if (!key) return '';
+    return key
+      .replace(/^cpr_/i, '')
+      .replace(/_/g, ' ')
+      .replace(/\b\w/g, (match) => match.toUpperCase());
   }
 
-  function cancelDelete() {
-    showDeleteConfirm = false;
-    rowToDelete = null;
+  function formatCurrency(value) {
+    if (value == null || value === '') return '—';
+    const numeric = Number(value);
+    if (Number.isNaN(numeric)) return value;
+    return numeric.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
   }
 
+  async function fetchHistory() {
+    historyLoading = true;
+    historyError = '';
+    detailError = '';
+    detailLoading = false;
+    try {
+      const { token, company_id } = ensureAuthContext();
+      const res = await fetch(historyEndpoint, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        },
+        body: JSON.stringify({ company_id })
+      });
+      const body = await parseJsonResponse(res);
+      if (!res.ok) {
+        throw new Error(body?.message ?? 'Falha ao carregar histórico.');
+      }
+      historyRows = normalizeHistoryList(body);
+      selectedDetail = null;
+      selectedDetailId = null;
+      historyInitialized = true;
+    } catch (err) {
+      historyRows = [];
+      historyError = err?.message ?? 'Falha ao carregar histórico.';
+    } finally {
+      historyLoading = false;
+    }
+  }
+
+  async function fetchDetail(cprId) {
+    if (!cprId) return;
+    detailLoading = true;
+    detailError = '';
+    try {
+      const { token, company_id } = ensureAuthContext();
+      const res = await fetch(historyEndpoint, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        },
+        body: JSON.stringify({ company_id, cpr_id: cprId })
+      });
+      const body = await parseJsonResponse(res);
+      if (!res.ok) {
+        throw new Error(body?.message ?? 'Falha ao carregar detalhes da CPR.');
+      }
+      const detail = Array.isArray(body) ? body[0] : body?.data ?? body;
+      selectedDetail = sanitizeDetail(detail) ?? detail ?? {};
+    } catch (err) {
+      detailError = err?.message ?? 'Falha ao carregar detalhes da CPR.';
+      selectedDetail = null;
+    } finally {
+      detailLoading = false;
+    }
+  }
+
+  function handleViewDetails(cprId) {
+    if (!cprId) return;
+    selectedDetailId = cprId;
+    selectedDetail = null;
+    detailError = '';
+    showDetailModal = true;
+    void fetchDetail(cprId);
+  }
+
+  function handleCloseDetail() {
+    selectedDetail = null;
+    selectedDetailId = null;
+    detailError = '';
+    showDetailModal = false;
+    detailLoading = false;
+  }
+
+  function handleRetryDetail() {
+    if (selectedDetailId) {
+      detailError = '';
+      detailLoading = true;
+      void fetchDetail(selectedDetailId);
+    }
+  }
+
+  onMount(() => {
+    void fetchHistory();
+  });
+
   function getMissingRequiredFields() {
     const missing = [];
     requiredFields.forEach((key) => {
@@ -523,22 +646,100 @@
       {/if}
 
       {#if activeTab === 4}
-        <Tables
-          title="CPRs"
-          columns={columns}
-          data={data}
-          on:addTop={handleAddTop}
-          on:editRow={handleEditRow}
-          on:deleteRow={handleDeleteRow}
-          
+        <section class="space-y-4">
+          <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+            <div>
+              <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-50">Histórico de CPRs</h2>
+              <p class="text-sm text-gray-500 dark:text-gray-400">
+                Consulte as últimas CPRs emitidas e visualize os detalhes completos.
+              </p>
+            </div>
+            <div class="flex gap-2">
+              <button
+                type="button"
+                class="inline-flex items-center gap-2 rounded-md border border-gray-300 dark:border-gray-600 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-60"
+                on:click={handleAddTop}
+              >
+                Nova CPR
+              </button>
+              <button
+                type="button"
+                class="inline-flex items-center gap-2 rounded-md border border-gray-300 dark:border-gray-600 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-60"
+                on:click={() => fetchHistory()}
+                disabled={historyLoading}
+              >
+                {historyLoading ? 'Atualizando...' : 'Atualizar'}
+              </button>
+            </div>
+          </div>
+
+          {#if historyError}
+            <div class="rounded border border-red-200 bg-red-50 dark:border-red-900/40 dark:bg-red-900/20 px-3 py-2 text-sm text-red-700 dark:text-red-300">
+              {historyError}
+            </div>
+          {/if}
+
+          <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
+            <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
+              <thead class="bg-gray-50 dark:bg-gray-900/40 text-gray-600 dark:text-gray-300 uppercase text-xs">
+                <tr>
+                  {#each historyColumns as column}
+                    <th class="px-4 py-3 text-left font-semibold">{column.label}</th>
+                  {/each}
+                  <th class="px-4 py-3 text-left font-semibold">Ação</th>
+                </tr>
+              </thead>
+              <tbody class="divide-y divide-gray-200 dark:divide-gray-700 text-gray-800 dark:text-gray-100">
+                {#if historyLoading && !historyInitialized}
+                  <tr>
+                    <td class="px-4 py-6 text-center" colspan={historyColumns.length + 1}>
+                      Carregando histórico...
+                    </td>
+                  </tr>
+                {:else if !historyRows.length}
+                  <tr>
+                    <td class="px-4 py-6 text-center text-gray-500 dark:text-gray-400" colspan={historyColumns.length + 1}>
+                      Nenhuma CPR encontrada.
+                    </td>
+                  </tr>
+                {:else}
+                  {#each historyRows as row}
+                    <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/60">
+                      <td class="px-4 py-3 font-medium">{row.cpr_id ?? '—'}</td>
+                      <td class="px-4 py-3">{row.cpr_product_class_name ?? '—'}</td>
+                      <td class="px-4 py-3">{row.cpr_issue_date ?? '—'}</td>
+                      <td class="px-4 py-3">{row.cpr_issuer_name ?? '—'}</td>
+                      <td class="px-4 py-3">{formatCurrency(row.cpr_issue_financial_value)}</td>
+                      <td class="px-4 py-3">
+                        <button
+                          type="button"
+                          class="text-sm font-medium text-blue-600 hover:text-blue-500 disabled:opacity-60"
+                          on:click={() => handleViewDetails(row.cpr_id)}
+                          disabled={!row.cpr_id || detailLoading && selectedDetailId === row.cpr_id}
+                        >
+                          {detailLoading && selectedDetailId === row.cpr_id ? 'Carregando...' : 'Mais informações'}
+                        </button>
+                      </td>
+                    </tr>
+                  {/each}
+                {/if}
+              </tbody>
+            </table>
+          </div>
+
+        </section>
+        <CprDetailModal
+          visible={showDetailModal}
+          title="Detalhes da CPR"
+          detailId={selectedDetailId}
+          loading={detailLoading}
+          error={detailError}
+          detail={selectedDetail}
+          formatKey={humanizeKey}
+          formatValue={formatDetailValue}
+          on:close={handleCloseDetail}
+          on:retry={handleRetryDetail}
         />
-        <CprEditModal
-        visible={showCprEdit}
-        title="Editar CPR"
-        value={editValue}
-        on:save={handleCprSave}
-        on:cancel={handleCprCancel}
-      />
       {:else if activeTab === 0}
       <Tabs {tabs} bind:active={activeTab} showCloseIcon={true} on:close={handleCancel} />
       <div class="mt-4">
@@ -620,14 +821,3 @@
     </div>
   </div>
 </div>
-
-<ConfirmModal
-  visible={showDeleteConfirm}
-  title="Confirmar exclusão"
-  confirmText="Excluir"
-  cancelText="Cancelar"
-  on:confirm={confirmDelete}
-  on:cancel={cancelDelete}
->
-  <p>Tem certeza que deseja excluir a CPR "{rowToDelete?.nome}"?</p>
-</ConfirmModal>