|
|
@@ -0,0 +1,732 @@
|
|
|
+<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 res = await fetch(`${apiUrl}/cpr/monitoring/create`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ Authorization: `Bearer ${token}`
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ cpr_id: cprId,
|
|
|
+ preview: Boolean(createPreview),
|
|
|
+ description,
|
|
|
+ link
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ 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 res = await fetch(`${apiUrl}/cpr/monitoring/update`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ Authorization: `Bearer ${token}`
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ id,
|
|
|
+ preview: Boolean(editPreview),
|
|
|
+ description,
|
|
|
+ link
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ 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' || e.key === 'Enter' || e.key === ' ') && !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">
|
|
|
+ <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' || e.key === 'Enter' || e.key === ' ') && !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">
|
|
|
+ <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>
|