| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746 |
- <script>
- import Header from '$lib/layout/Header.svelte';
- import Tables from '$lib/components/Tables.svelte';
- import ConfirmModal from '$lib/components/ui/PopUpDelete.svelte';
- import { authToken } from '$lib/utils/stores';
- import { page } from '$app/stores';
- import { onMount } from 'svelte';
- const apiUrl = import.meta.env.VITE_API_URL;
- const cprHistoryEndpoint = `${apiUrl}/cpr/history`;
- const breadcrumb = [{ label: 'Início' }, { label: 'CPR' }, { label: 'Monitoring', active: true }];
- const columns = [
- { key: 'id', label: 'ID' },
- { key: 'preview', label: 'Preview' },
- { key: 'description', label: 'Descrição' },
- { key: 'link', label: 'Link' }
- ];
- let cprOptions = [];
- let cprOptionsLoading = false;
- let cprOptionsError = '';
- let selectedCprId = '';
- let cprOptionsLoadedOnce = false;
- let requestedCprId = '';
- let requestedCprApplied = false;
- let data = [];
- let loadError = '';
- let successMessage = '';
- let isLoading = false;
- let showCreate = false;
- let createLoading = false;
- let createError = '';
- let createPreview = false;
- let createDescription = '';
- let createLink = '';
- let showEdit = false;
- let editLoading = false;
- let editError = '';
- let editId = null;
- let editPreview = false;
- let editDescription = '';
- let editLink = '';
- let showDeleteConfirm = false;
- let rowToDelete = null;
- let deleteLoading = false;
- let deleteError = '';
- function toCprOptionLabel(row) {
- const id = row?.cpr_id ?? row?.id;
- const product = row?.cpr_product_class_name ?? row?.cpr_product_name;
- const issuer = row?.cpr_issuer_name;
- const parts = [`#${id}`];
- if (product) parts.push(String(product));
- if (issuer) parts.push(String(issuer));
- return parts.join(' - ');
- }
- async function fetchCprOptions() {
- if (cprOptionsLoading) return;
- cprOptionsLoading = true;
- cprOptionsError = '';
- try {
- const token = $authToken;
- if (!token) {
- throw new Error('Sessão expirada. Faça login novamente.');
- }
- const res = await fetch(cprHistoryEndpoint, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- Authorization: `Bearer ${token}`
- },
- body: JSON.stringify({})
- });
- const raw = await res.text();
- const body = safeParseJson(raw);
- if (!res.ok) {
- throw new Error(body?.message ?? body?.msg ?? `Falha ao carregar CPRs (HTTP ${res.status}).`);
- }
- const list = normalizeList(body);
- cprOptions = list
- .map((row) => {
- const id = Number(row?.cpr_id ?? row?.id);
- if (!Number.isInteger(id) || id <= 0) return null;
- return {
- id,
- label: toCprOptionLabel({ ...row, cpr_id: id })
- };
- })
- .filter(Boolean);
- if (!requestedCprApplied && requestedCprId) {
- const wantedId = Number(requestedCprId);
- const exists = cprOptions.some((opt) => opt.id === wantedId);
- if (exists) {
- selectedCprId = String(wantedId);
- }
- requestedCprApplied = true;
- }
- if (!selectedCprId && cprOptions.length) {
- selectedCprId = String(cprOptions[0].id);
- }
- } catch (err) {
- console.error('[CPR Monitoring] Erro ao buscar CPRs:', err);
- cprOptionsError = err?.message ?? 'Falha ao carregar CPRs.';
- cprOptions = [];
- } finally {
- cprOptionsLoading = false;
- cprOptionsLoadedOnce = true;
- }
- }
- $: if (cprOptionsLoadedOnce && !cprOptionsLoading && selectedCprId) {
- void fetchMonitoring();
- }
- function safeParseJson(raw) {
- if (!raw) return null;
- try {
- return JSON.parse(raw);
- } catch {
- return null;
- }
- }
- function normalizeList(body) {
- if (Array.isArray(body)) return body;
- if (Array.isArray(body?.data)) return body.data;
- return [];
- }
- function mapMonitoringRow(item) {
- const previewValue = item?.preview;
- const previewLabel =
- previewValue === true || previewValue === 1 || previewValue === 't' || previewValue === 'true' ? 'Sim' : 'Não';
- return {
- __raw: item,
- id: item?.id ?? '-',
- preview: previewLabel,
- description: item?.description ?? '-',
- link: item?.link ?? '-'
- };
- }
- function resolveMonitoringId(row) {
- return row?.__raw?.id ?? row?.id;
- }
- function openCreate() {
- showCreate = true;
- createError = '';
- createLoading = false;
- createPreview = false;
- createDescription = '';
- createLink = '';
- if (!cprOptions.length) {
- void fetchCprOptions();
- }
- }
- function closeCreate() {
- showCreate = false;
- createLoading = false;
- createError = '';
- createPreview = false;
- createDescription = '';
- createLink = '';
- }
- function openEditFromRow(row) {
- const raw = row?.__raw ?? row;
- const id = Number(raw?.id);
- if (!Number.isInteger(id) || id <= 0) {
- loadError = 'Registro inválido para edição.';
- return;
- }
- editId = id;
- editPreview = Boolean(raw?.preview);
- editDescription = String(raw?.description ?? '');
- editLink = String(raw?.link ?? '');
- editError = '';
- editLoading = false;
- showEdit = true;
- }
- function closeEdit() {
- showEdit = false;
- editLoading = false;
- editError = '';
- editId = null;
- editPreview = false;
- editDescription = '';
- editLink = '';
- }
- async function fetchMonitoring() {
- if (isLoading) return;
- const cprId = Number(selectedCprId);
- if (!Number.isInteger(cprId) || cprId <= 0) {
- loadError = 'Selecione uma CPR válida para listar.';
- data = [];
- return;
- }
- isLoading = true;
- loadError = '';
- try {
- const token = $authToken;
- if (!token) {
- throw new Error('Sessão expirada. Faça login novamente.');
- }
- const res = await fetch(`${apiUrl}/cpr/monitoring/list`, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- Authorization: `Bearer ${token}`
- },
- body: JSON.stringify({ cpr_id: cprId })
- });
- if (res.status === 204) {
- data = [];
- return;
- }
- const raw = await res.text();
- const body = safeParseJson(raw);
- if (!res.ok || body?.status !== 'ok') {
- throw new Error(body?.msg ?? body?.message ?? `Falha ao carregar registros (HTTP ${res.status}).`);
- }
- const list = normalizeList(body);
- data = list.map(mapMonitoringRow);
- } catch (err) {
- console.error('[CPR Monitoring] Erro ao buscar registros:', err);
- loadError = err?.message ?? 'Falha ao carregar registros.';
- data = [];
- } finally {
- isLoading = false;
- }
- }
- function handleEditRow(e) {
- const { row } = e?.detail || {};
- if (!row) return;
- openEditFromRow(row);
- }
- function handleDeleteRow(e) {
- const { row } = e?.detail || {};
- if (!row) return;
- rowToDelete = row;
- deleteError = '';
- deleteLoading = false;
- showDeleteConfirm = true;
- }
- async function handleCreateSubmit() {
- if (createLoading) return;
- createError = '';
- const cprId = Number(selectedCprId);
- if (!Number.isInteger(cprId) || cprId <= 0) {
- createError = 'Selecione uma CPR válida.';
- return;
- }
- const description = String(createDescription ?? '').trim();
- if (!description || description.length > 5000) {
- createError = 'Informe uma descrição válida (1 a 5000 caracteres).';
- return;
- }
- const link = String(createLink ?? '').trim();
- if (!link || link.length > 2048) {
- createError = 'Informe um link válido (1 a 2048 caracteres).';
- return;
- }
- const token = $authToken;
- if (!token) {
- createError = 'Sessão expirada. Faça login novamente.';
- return;
- }
- createLoading = true;
- try {
- const createPayload = {
- cpr_id: cprId,
- preview: createPreview === true,
- description,
- link
- };
- const res = await fetch(`${apiUrl}/cpr/monitoring/create`, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- Authorization: `Bearer ${token}`
- },
- body: JSON.stringify(createPayload)
- });
- const raw = await res.text();
- const body = safeParseJson(raw);
- const isSuccess = res.ok && body?.status === 'ok' && body?.code === 'S_CREATED' && body?.data?.id != null;
- if (!isSuccess) {
- throw new Error(body?.msg ?? body?.message ?? `Falha ao criar registro (HTTP ${res.status}).`);
- }
- successMessage = body?.msg ?? body?.message ?? 'Registro criado com sucesso!';
- closeCreate();
- await fetchMonitoring();
- } catch (err) {
- console.error('[CPR Monitoring] Erro ao criar registro:', err);
- createError = err?.message ?? 'Falha ao criar registro.';
- } finally {
- createLoading = false;
- }
- }
- async function handleEditSubmit() {
- if (editLoading) return;
- editError = '';
- const id = Number(editId);
- if (!Number.isInteger(id) || id <= 0) {
- editError = 'ID inválido.';
- return;
- }
- const description = String(editDescription ?? '').trim();
- if (!description || description.length > 5000) {
- editError = 'Informe uma descrição válida (1 a 5000 caracteres).';
- return;
- }
- const link = String(editLink ?? '').trim();
- if (!link || link.length > 2048) {
- editError = 'Informe um link válido (1 a 2048 caracteres).';
- return;
- }
- const token = $authToken;
- if (!token) {
- editError = 'Sessão expirada. Faça login novamente.';
- return;
- }
- editLoading = true;
- try {
- const editPayload = {
- id,
- preview: editPreview === true,
- description,
- link
- };
- const res = await fetch(`${apiUrl}/cpr/monitoring/update`, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- Authorization: `Bearer ${token}`
- },
- body: JSON.stringify(editPayload)
- });
- if (res.status === 204) {
- throw new Error('Registro não encontrado ou não atualizado.');
- }
- const raw = await res.text();
- const body = safeParseJson(raw);
- const isSuccess = res.ok && body?.status === 'ok' && body?.code === 'S_UPDATED' && body?.data?.id != null;
- if (!isSuccess) {
- throw new Error(body?.msg ?? body?.message ?? `Falha ao atualizar registro (HTTP ${res.status}).`);
- }
- successMessage = body?.msg ?? body?.message ?? 'Registro atualizado com sucesso!';
- closeEdit();
- await fetchMonitoring();
- } catch (err) {
- console.error('[CPR Monitoring] Erro ao atualizar registro:', err);
- editError = err?.message ?? 'Falha ao atualizar registro.';
- } finally {
- editLoading = false;
- }
- }
- async function confirmDelete() {
- if (deleteLoading) return;
- deleteError = '';
- const idRaw = resolveMonitoringId(rowToDelete);
- const id = Number(idRaw);
- if (!Number.isInteger(id) || id <= 0) {
- deleteError = 'ID inválido para exclusão.';
- return;
- }
- const token = $authToken;
- if (!token) {
- deleteError = 'Sessão expirada. Faça login novamente.';
- return;
- }
- deleteLoading = true;
- let deleted = false;
- try {
- const res = await fetch(`${apiUrl}/cpr/monitoring/delete`, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- Authorization: `Bearer ${token}`
- },
- body: JSON.stringify({ id })
- });
- if (res.status === 204) {
- throw new Error('Registro não encontrado.');
- }
- const raw = await res.text();
- const body = safeParseJson(raw);
- const isSuccess = res.ok && body?.status === 'ok' && body?.code === 'S_DELETED' && body?.data?.deleted === true;
- if (!isSuccess) {
- throw new Error(body?.msg ?? body?.message ?? `Falha ao excluir registro (HTTP ${res.status}).`);
- }
- deleted = true;
- successMessage = body?.msg ?? body?.message ?? 'Registro excluído com sucesso!';
- await fetchMonitoring();
- } catch (err) {
- console.error('[CPR Monitoring] Erro ao excluir registro:', err);
- deleteError = err?.message ?? 'Falha ao excluir registro.';
- } finally {
- deleteLoading = false;
- if (deleted) {
- showDeleteConfirm = false;
- rowToDelete = null;
- }
- }
- }
- function cancelDelete() {
- showDeleteConfirm = false;
- rowToDelete = null;
- deleteError = '';
- deleteLoading = false;
- }
- onMount(() => {
- loadError = '';
- requestedCprId = String($page.url.searchParams.get('cpr_id') ?? '');
- requestedCprApplied = false;
- void fetchCprOptions();
- });
- </script>
- <div>
- <Header title="CPR - Monitoring" subtitle="Gestão de registros de monitoramento por CPR" breadcrumb={breadcrumb} />
- <div class="p-4">
- <div class="max-w-6xl mx-auto mt-4">
- {#if loadError}
- <div class="mb-4 rounded border border-red-300 bg-red-50 text-red-700 px-3 py-2 text-sm dark:border-red-700 dark:bg-red-900/30 dark:text-red-200">{loadError}</div>
- {/if}
- {#if successMessage}
- <div class="mb-4 rounded border border-green-300 bg-green-50 text-green-700 px-3 py-2 text-sm dark:border-green-700 dark:bg-green-900/20 dark:text-green-200">
- {successMessage}
- </div>
- {/if}
- <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">Registros de monitoramento</h2>
- <p class="text-sm text-gray-500 dark:text-gray-400">Liste, crie, edite ou remova registros vinculados a uma CPR.</p>
- </div>
- <div class="flex flex-col gap-2 sm:flex-row sm:items-center">
- <select
- class="min-w-64 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
- bind:value={selectedCprId}
- disabled={isLoading || cprOptionsLoading}
- >
- <option value="" disabled>Selecione uma CPR</option>
- {#each cprOptions as opt}
- <option value={String(opt.id)}>{opt.label}</option>
- {/each}
- </select>
- {#if cprOptionsError}
- <div class="text-xs text-red-600 dark:text-red-400">{cprOptionsError}</div>
- {/if}
- <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={fetchMonitoring}
- disabled={isLoading}
- >
- {isLoading ? 'Atualizando...' : 'Atualizar'}
- </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={openCreate}
- disabled={isLoading}
- >
- Novo registro
- </button>
- </div>
- </div>
- <Tables
- title="Registros"
- {columns}
- {data}
- on:editRow={handleEditRow}
- on:deleteRow={handleDeleteRow}
- showAdd={false}
- />
- </section>
- {#if showCreate}
- <div
- class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"
- role="button"
- tabindex="0"
- on:click={(e) => {
- if (e.target === e.currentTarget && !createLoading) closeCreate();
- }}
- on:keydown={(e) => {
- if (e.key === 'Escape' && !createLoading) closeCreate();
- }}
- >
- <div
- class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg w-full max-w-2xl p-6"
- role="dialog"
- aria-modal="true"
- on:keydown|stopPropagation
- >
- <div class="flex items-center justify-between mb-4">
- <h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Criar registro</h4>
- <button type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" on:click={() => (!createLoading ? closeCreate() : null)}>✕</button>
- </div>
- <div class="space-y-4">
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">CPR</label>
- <select
- class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-800 dark:text-gray-100 disabled:opacity-60"
- bind:value={selectedCprId}
- disabled={createLoading || cprOptionsLoading}
- >
- <option value="" disabled>Selecione uma CPR</option>
- {#each cprOptions as opt}
- <option value={String(opt.id)}>{opt.label}</option>
- {/each}
- </select>
- {#if cprOptionsError}
- <div class="mt-1 text-xs text-red-600 dark:text-red-400">{cprOptionsError}</div>
- {/if}
- </div>
- <div class="flex items-center gap-2">
- <input id="create-preview" type="checkbox" bind:checked={createPreview} disabled={createLoading} />
- <label for="create-preview" class="text-sm text-gray-700 dark:text-gray-200">Preview</label>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Descrição</label>
- <textarea
- class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-800 dark:text-gray-100"
- rows="6"
- maxlength="5000"
- bind:value={createDescription}
- disabled={createLoading}
- ></textarea>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Link</label>
- <input
- class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-800 dark:text-gray-100"
- type="text"
- maxlength="2048"
- bind:value={createLink}
- disabled={createLoading}
- />
- </div>
- {#if createError}
- <div class="rounded border border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-200 px-3 py-2 text-sm">
- {createError}
- </div>
- {/if}
- <div class="grid grid-cols-1 md:grid-cols-2 gap-2 pt-2">
- <button
- type="button"
- class="rounded border border-gray-300 dark:border-gray-600 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-60"
- on:click={closeCreate}
- disabled={createLoading}
- >
- Cancelar
- </button>
- <button
- type="button"
- class="rounded bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 disabled:opacity-60"
- on:click={handleCreateSubmit}
- disabled={createLoading}
- >
- {createLoading ? 'Criando...' : 'Criar'}
- </button>
- </div>
- </div>
- </div>
- </div>
- {/if}
- {#if showEdit}
- <div
- class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"
- role="button"
- tabindex="0"
- on:click={(e) => {
- if (e.target === e.currentTarget && !editLoading) closeEdit();
- }}
- on:keydown={(e) => {
- if (e.key === 'Escape' && !editLoading) closeEdit();
- }}
- >
- <div
- class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg w-full max-w-2xl p-6"
- role="dialog"
- aria-modal="true"
- on:keydown|stopPropagation
- >
- <div class="flex items-center justify-between mb-4">
- <h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Editar registro #{editId}</h4>
- <button type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" on:click={() => (!editLoading ? closeEdit() : null)}>✕</button>
- </div>
- <div class="space-y-4">
- <div class="flex items-center gap-2">
- <input id="edit-preview" type="checkbox" bind:checked={editPreview} disabled={editLoading} />
- <label for="edit-preview" class="text-sm text-gray-700 dark:text-gray-200">Preview</label>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Descrição</label>
- <textarea
- class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-800 dark:text-gray-100"
- rows="6"
- maxlength="5000"
- bind:value={editDescription}
- disabled={editLoading}
- ></textarea>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Link</label>
- <input
- class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-800 dark:text-gray-100"
- type="text"
- maxlength="2048"
- bind:value={editLink}
- disabled={editLoading}
- />
- </div>
- {#if editError}
- <div class="rounded border border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-200 px-3 py-2 text-sm">
- {editError}
- </div>
- {/if}
- <div class="grid grid-cols-1 md:grid-cols-2 gap-2 pt-2">
- <button
- type="button"
- class="rounded border border-gray-300 dark:border-gray-600 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-60"
- on:click={closeEdit}
- disabled={editLoading}
- >
- Cancelar
- </button>
- <button
- type="button"
- class="rounded bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 disabled:opacity-60"
- on:click={handleEditSubmit}
- disabled={editLoading}
- >
- {editLoading ? 'Salvando...' : 'Salvar'}
- </button>
- </div>
- </div>
- </div>
- </div>
- {/if}
- <ConfirmModal
- visible={showDeleteConfirm}
- title="Confirmar exclusão"
- confirmText={deleteLoading ? 'Excluindo...' : 'Excluir'}
- cancelText="Cancelar"
- disableBackdropClose={deleteLoading}
- confirmDisabled={deleteLoading}
- on:confirm={confirmDelete}
- on:cancel={cancelDelete}
- >
- <p>Tem certeza que deseja excluir o registro "{rowToDelete?.id}"?</p>
- {#if deleteError}
- <p class="mt-3 text-sm text-red-600 dark:text-red-400">{deleteError}</p>
- {/if}
- </ConfirmModal>
- </div>
- </div>
- </div>
|