|
@@ -1,13 +1,13 @@
|
|
|
<script>
|
|
<script>
|
|
|
|
|
+ import { onMount } from 'svelte';
|
|
|
|
|
+ import { get } from 'svelte/store';
|
|
|
import Header from '$lib/layout/Header.svelte';
|
|
import Header from '$lib/layout/Header.svelte';
|
|
|
import Tabs from '$lib/components/Tabs.svelte';
|
|
import Tabs from '$lib/components/Tabs.svelte';
|
|
|
import RegisterCpr from '$lib/components/commodities/cpr/RegisterCpr.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 ContractCpr from '$lib/components/commodities/cpr/ContractCpr.svelte';
|
|
|
import EmissionCpr from '$lib/components/commodities/cpr/EmissionCpr.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;
|
|
const apiUrl = import.meta.env.VITE_API_URL;
|
|
|
|
|
|
|
@@ -276,28 +276,27 @@
|
|
|
let submitSuccess = '';
|
|
let submitSuccess = '';
|
|
|
let isSubmitting = false;
|
|
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 }];
|
|
const breadcrumb = [{ label: 'Início' }, { label: 'CPR', active: true }];
|
|
|
let activeTab = 4;
|
|
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) {
|
|
function handleFieldChange(key, value) {
|
|
|
submitError = '';
|
|
submitError = '';
|
|
@@ -353,49 +352,173 @@
|
|
|
activeTab = 0;
|
|
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() {
|
|
function getMissingRequiredFields() {
|
|
|
const missing = [];
|
|
const missing = [];
|
|
|
requiredFields.forEach((key) => {
|
|
requiredFields.forEach((key) => {
|
|
@@ -523,22 +646,100 @@
|
|
|
{/if}
|
|
{/if}
|
|
|
|
|
|
|
|
{#if activeTab === 4}
|
|
{#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}
|
|
{:else if activeTab === 0}
|
|
|
<Tabs {tabs} bind:active={activeTab} showCloseIcon={true} on:close={handleCancel} />
|
|
<Tabs {tabs} bind:active={activeTab} showCloseIcon={true} on:close={handleCancel} />
|
|
|
<div class="mt-4">
|
|
<div class="mt-4">
|
|
@@ -620,14 +821,3 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</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>
|
|
|