feat: enhance LGPD compliance features by adding configurable data protection officer details, consent term settings, and improved error handling across various components
This commit is contained in:
@@ -1,6 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { Shield, FileText, Mail, Phone, Calendar } from 'lucide-svelte';
|
import { Shield, FileText, Mail, Phone, Calendar } from 'lucide-svelte';
|
||||||
|
import { useQuery } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
|
||||||
|
const configLGPD = useQuery(api.lgpd.obterConfiguracaoLGPD, {});
|
||||||
|
|
||||||
|
const encarregadoNome = $derived(configLGPD?.data?.encarregadoNome || 'Encarregado de Proteção de Dados');
|
||||||
|
const encarregadoEmail = $derived(configLGPD?.data?.encarregadoEmail || 'lgpd@esportes.pe.gov.br');
|
||||||
|
const encarregadoTelefone = $derived(configLGPD?.data?.encarregadoTelefone || '(81) 3184-XXXX');
|
||||||
|
const encarregadoHorario = $derived(configLGPD?.data?.encarregadoHorarioAtendimento || 'Segunda a Sexta, das 8h às 17h');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
@@ -360,31 +369,40 @@
|
|||||||
entre em contato com nosso Encarregado de Proteção de Dados:
|
entre em contato com nosso Encarregado de Proteção de Dados:
|
||||||
</p>
|
</p>
|
||||||
<div class="bg-primary/5 p-6 rounded-lg space-y-3">
|
<div class="bg-primary/5 p-6 rounded-lg space-y-3">
|
||||||
|
{#if encarregadoNome && encarregadoNome !== 'Encarregado de Proteção de Dados'}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<FileText class="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold">Nome</p>
|
||||||
|
<p class="text-base-content/70">{encarregadoNome}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Mail class="h-5 w-5 text-primary" />
|
<Mail class="h-5 w-5 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold">E-mail</p>
|
<p class="text-sm font-semibold">E-mail</p>
|
||||||
<p class="text-base-content/70">lgpd@esportes.pe.gov.br</p>
|
<p class="text-base-content/70">{encarregadoEmail}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Phone class="h-5 w-5 text-primary" />
|
<Phone class="h-5 w-5 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold">Telefone</p>
|
<p class="text-sm font-semibold">Telefone</p>
|
||||||
<p class="text-base-content/70">(81) 3184-XXXX</p>
|
<p class="text-base-content/70">{encarregadoTelefone}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<FileText class="h-5 w-5 text-primary" />
|
<FileText class="h-5 w-5 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold">Horário de Atendimento</p>
|
<p class="text-sm font-semibold">Horário de Atendimento</p>
|
||||||
<p class="text-base-content/70">Segunda a Sexta, das 8h às 17h</p>
|
<p class="text-base-content/70">{encarregadoHorario}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mt-4">
|
<div class="alert alert-info mt-4">
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
As solicitações serão respondidas em até 15 (quinze) dias, conforme previsto na
|
As solicitações serão respondidas em até {configLGPD?.data?.prazoRespostaPadrao || 15} (quinze) dias, conforme previsto na
|
||||||
LGPD.
|
LGPD.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -115,8 +115,9 @@
|
|||||||
tipoSelecionado = null;
|
tipoSelecionado = null;
|
||||||
dadosSolicitados = '';
|
dadosSolicitados = '';
|
||||||
observacoes = '';
|
observacoes = '';
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(error.message || 'Erro ao criar solicitação');
|
const message = error instanceof Error ? error.message : 'Erro ao criar solicitação';
|
||||||
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
carregando = false;
|
carregando = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,25 @@
|
|||||||
const registrarConsentimento = useMutation(api.lgpd.registrarConsentimento);
|
const registrarConsentimento = useMutation(api.lgpd.registrarConsentimento);
|
||||||
|
|
||||||
const jaAceitou = $derived(
|
const jaAceitou = $derived(
|
||||||
consentimentoQuery?.data?.aceito === true && consentimentoQuery?.data?.versao === '1.0'
|
consentimentoQuery?.data?.aceito === true &&
|
||||||
|
consentimentoQuery?.data?.versao === consentimentoQuery?.data?.versaoTermoAtual
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const termoObrigatorio = $derived(consentimentoQuery?.data?.termoObrigatorio ?? false);
|
||||||
|
const versaoTermoAtual = $derived(consentimentoQuery?.data?.versaoTermoAtual ?? '1.0');
|
||||||
|
|
||||||
async function aceitarTermo() {
|
async function aceitarTermo() {
|
||||||
if (!aceito) {
|
if (termoObrigatorio && !aceito) {
|
||||||
erro = 'Você precisa aceitar o termo para continuar';
|
erro = 'Você precisa aceitar o termo para continuar';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!aceito) {
|
||||||
|
// Se não é obrigatório e não aceitou, apenas redireciona
|
||||||
|
window.location.href = resolve('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
carregando = true;
|
carregando = true;
|
||||||
erro = null;
|
erro = null;
|
||||||
|
|
||||||
@@ -30,11 +40,11 @@
|
|||||||
await registrarConsentimento({
|
await registrarConsentimento({
|
||||||
tipo: 'termo_uso',
|
tipo: 'termo_uso',
|
||||||
aceito: true,
|
aceito: true,
|
||||||
versao: '1.0'
|
versao: versaoTermoAtual
|
||||||
});
|
});
|
||||||
sucesso = true;
|
sucesso = true;
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
erro = e.message || 'Erro ao registrar consentimento';
|
erro = e instanceof Error ? e.message : 'Erro ao registrar consentimento';
|
||||||
} finally {
|
} finally {
|
||||||
carregando = false;
|
carregando = false;
|
||||||
}
|
}
|
||||||
@@ -68,7 +78,7 @@
|
|||||||
<CheckCircle class="h-16 w-16 text-success mx-auto mb-4" strokeWidth={2} />
|
<CheckCircle class="h-16 w-16 text-success mx-auto mb-4" strokeWidth={2} />
|
||||||
<h2 class="card-title text-2xl justify-center mb-4">Termo Já Aceito</h2>
|
<h2 class="card-title text-2xl justify-center mb-4">Termo Já Aceito</h2>
|
||||||
<p class="text-base-content/80 mb-6">
|
<p class="text-base-content/80 mb-6">
|
||||||
Você já aceitou este termo de consentimento. Se desejar revogar seu consentimento ou
|
Você já aceitou este termo de consentimento (versão {versaoTermoAtual}). Se desejar revogar seu consentimento ou
|
||||||
gerenciar suas preferências de privacidade, acesse a página de privacidade.
|
gerenciar suas preferências de privacidade, acesse a página de privacidade.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
@@ -208,14 +218,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="alert alert-warning mb-6">
|
{#if termoObrigatorio}
|
||||||
<AlertCircle class="h-5 w-5" />
|
<div class="alert alert-warning mb-6">
|
||||||
<p class="text-sm">
|
<AlertCircle class="h-5 w-5" />
|
||||||
<strong>Atenção:</strong> O aceite deste termo é obrigatório para utilização do
|
<p class="text-sm">
|
||||||
sistema. Ao aceitar, você confirma que leu, compreendeu e concorda com todos os
|
<strong>Atenção:</strong> O aceite deste termo é obrigatório para utilização do
|
||||||
termos e condições estabelecidos.
|
sistema. Ao aceitar, você confirma que leu, compreendeu e concorda com todos os
|
||||||
</p>
|
termos e condições estabelecidos.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="alert alert-info mb-6">
|
||||||
|
<AlertCircle class="h-5 w-5" />
|
||||||
|
<p class="text-sm">
|
||||||
|
<strong>Informação:</strong> O aceite deste termo é opcional. Você pode aceitar
|
||||||
|
voluntariamente ou continuar sem aceitar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,7 +273,7 @@
|
|||||||
<div class="flex flex-col sm:flex-row gap-4">
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
<button
|
<button
|
||||||
onclick={aceitarTermo}
|
onclick={aceitarTermo}
|
||||||
disabled={!aceito || carregando}
|
disabled={(termoObrigatorio && !aceito) || carregando}
|
||||||
class="btn btn-primary btn-lg flex-1"
|
class="btn btn-primary btn-lg flex-1"
|
||||||
>
|
>
|
||||||
{#if carregando}
|
{#if carregando}
|
||||||
|
|||||||
@@ -4,8 +4,15 @@
|
|||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { Shield, FileText, AlertTriangle, CheckCircle, Settings, Users } from 'lucide-svelte';
|
import { Shield, FileText, AlertTriangle, CheckCircle, Settings, Users } from 'lucide-svelte';
|
||||||
import StatsCard from '$lib/components/ti/StatsCard.svelte';
|
import StatsCard from '$lib/components/ti/StatsCard.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const estatisticas = useQuery(api.lgpd.obterEstatisticasLGPD, {});
|
const estatisticas = useQuery(api.lgpd.obterEstatisticasLGPD, {});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (estatisticas?.error) {
|
||||||
|
console.error('Erro ao carregar estatísticas LGPD:', estatisticas.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
@@ -23,11 +30,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Cards -->
|
<!-- Stats Cards -->
|
||||||
{#if estatisticas}
|
{#if estatisticas === undefined}
|
||||||
|
<div class="flex justify-center items-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if estatisticas?.error}
|
||||||
|
<div class="alert alert-error mb-6">
|
||||||
|
<AlertTriangle class="h-5 w-5" />
|
||||||
|
<span>Erro ao carregar estatísticas: {estatisticas.error}</span>
|
||||||
|
</div>
|
||||||
|
{:else if estatisticas?.data}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Solicitações Pendentes"
|
title="Solicitações Pendentes"
|
||||||
value={estatisticas.solicitacoesPendentes}
|
value={estatisticas.data.solicitacoesPendentes}
|
||||||
description="Aguardando resposta"
|
description="Aguardando resposta"
|
||||||
Icon={AlertTriangle}
|
Icon={AlertTriangle}
|
||||||
color="warning"
|
color="warning"
|
||||||
@@ -35,7 +51,7 @@
|
|||||||
|
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Solicitações Vencendo"
|
title="Solicitações Vencendo"
|
||||||
value={estatisticas.solicitacoesVencendo}
|
value={estatisticas.data.solicitacoesVencendo}
|
||||||
description="Prazo próximo"
|
description="Prazo próximo"
|
||||||
Icon={AlertTriangle}
|
Icon={AlertTriangle}
|
||||||
color="error"
|
color="error"
|
||||||
@@ -43,7 +59,7 @@
|
|||||||
|
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Total de Solicitações"
|
title="Total de Solicitações"
|
||||||
value={estatisticas.totalSolicitacoes}
|
value={estatisticas.data.totalSolicitacoes}
|
||||||
description="Todas as solicitações"
|
description="Todas as solicitações"
|
||||||
Icon={FileText}
|
Icon={FileText}
|
||||||
color="info"
|
color="info"
|
||||||
@@ -51,16 +67,12 @@
|
|||||||
|
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Consentimentos Ativos"
|
title="Consentimentos Ativos"
|
||||||
value={estatisticas.consentimentosAtivos}
|
value={estatisticas.data.consentimentosAtivos}
|
||||||
description="Consentimentos válidos"
|
description="Consentimentos válidos"
|
||||||
Icon={CheckCircle}
|
Icon={CheckCircle}
|
||||||
color="success"
|
color="success"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<div class="flex justify-center items-center py-20">
|
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Ações Rápidas -->
|
<!-- Ações Rápidas -->
|
||||||
@@ -87,14 +99,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Informações -->
|
<!-- Informações -->
|
||||||
{#if estatisticas}
|
{#if estatisticas?.data}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<!-- Solicitações por Tipo -->
|
<!-- Solicitações por Tipo -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-xl mb-4">Solicitações por Tipo</h2>
|
<h2 class="card-title text-xl mb-4">Solicitações por Tipo</h2>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each Object.entries(estatisticas.solicitacoesPorTipo) as [tipo, quantidade]}
|
{#each Object.entries(estatisticas.data.solicitacoesPorTipo) as [tipo, quantidade]}
|
||||||
<div class="flex justify-between items-center p-2 bg-base-200 rounded">
|
<div class="flex justify-between items-center p-2 bg-base-200 rounded">
|
||||||
<span class="text-sm font-medium">{tipo}</span>
|
<span class="text-sm font-medium">{tipo}</span>
|
||||||
<span class="badge badge-primary">{quantidade}</span>
|
<span class="badge badge-primary">{quantidade}</span>
|
||||||
@@ -111,19 +123,19 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>Total de ROTs:</span>
|
<span>Total de ROTs:</span>
|
||||||
<span class="font-semibold">{estatisticas.totalROTs}</span>
|
<span class="font-semibold">{estatisticas.data.totalROTs}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>ROTs Ativos:</span>
|
<span>ROTs Ativos:</span>
|
||||||
<span class="font-semibold text-success">{estatisticas.rotsAtivos}</span>
|
<span class="font-semibold text-success">{estatisticas.data.rotsAtivos}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>Total de Consentimentos:</span>
|
<span>Total de Consentimentos:</span>
|
||||||
<span class="font-semibold">{estatisticas.totalConsentimentos}</span>
|
<span class="font-semibold">{estatisticas.data.totalConsentimentos}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>Consentimentos Ativos:</span>
|
<span>Consentimentos Ativos:</span>
|
||||||
<span class="font-semibold text-success">{estatisticas.consentimentosAtivos}</span>
|
<span class="font-semibold text-success">{estatisticas.data.consentimentosAtivos}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { useQuery, useMutation } from 'convex-svelte';
|
import { useQuery, useMutation } from 'convex-svelte';
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { Shield, Save, Mail, Phone, User, Calendar } from 'lucide-svelte';
|
import { Shield, Save, Mail, Phone, User, Calendar, ToggleLeft, ToggleRight } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const config = useQuery(api.lgpd.obterConfiguracaoLGPD, {});
|
const config = useQuery(api.lgpd.obterConfiguracaoLGPD, {});
|
||||||
@@ -11,8 +11,11 @@
|
|||||||
let encarregadoNome = $state('');
|
let encarregadoNome = $state('');
|
||||||
let encarregadoEmail = $state('');
|
let encarregadoEmail = $state('');
|
||||||
let encarregadoTelefone = $state('');
|
let encarregadoTelefone = $state('');
|
||||||
|
let encarregadoHorarioAtendimento = $state('Segunda a Sexta, das 8h às 17h');
|
||||||
let prazoRespostaPadrao = $state(15);
|
let prazoRespostaPadrao = $state(15);
|
||||||
let diasAlertaVencimento = $state(3);
|
let diasAlertaVencimento = $state(3);
|
||||||
|
let termoObrigatorio = $state(false);
|
||||||
|
let versaoTermoAtual = $state('1.0');
|
||||||
let carregando = $state(false);
|
let carregando = $state(false);
|
||||||
|
|
||||||
// Sincronizar com query
|
// Sincronizar com query
|
||||||
@@ -21,8 +24,11 @@
|
|||||||
encarregadoNome = config.data.encarregadoNome || '';
|
encarregadoNome = config.data.encarregadoNome || '';
|
||||||
encarregadoEmail = config.data.encarregadoEmail || '';
|
encarregadoEmail = config.data.encarregadoEmail || '';
|
||||||
encarregadoTelefone = config.data.encarregadoTelefone || '';
|
encarregadoTelefone = config.data.encarregadoTelefone || '';
|
||||||
|
encarregadoHorarioAtendimento = config.data.encarregadoHorarioAtendimento || 'Segunda a Sexta, das 8h às 17h';
|
||||||
prazoRespostaPadrao = config.data.prazoRespostaPadrao;
|
prazoRespostaPadrao = config.data.prazoRespostaPadrao;
|
||||||
diasAlertaVencimento = config.data.diasAlertaVencimento;
|
diasAlertaVencimento = config.data.diasAlertaVencimento;
|
||||||
|
termoObrigatorio = config.data.termoObrigatorio;
|
||||||
|
versaoTermoAtual = config.data.versaoTermoAtual;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,13 +40,17 @@
|
|||||||
encarregadoNome: encarregadoNome || undefined,
|
encarregadoNome: encarregadoNome || undefined,
|
||||||
encarregadoEmail: encarregadoEmail || undefined,
|
encarregadoEmail: encarregadoEmail || undefined,
|
||||||
encarregadoTelefone: encarregadoTelefone || undefined,
|
encarregadoTelefone: encarregadoTelefone || undefined,
|
||||||
|
encarregadoHorarioAtendimento: encarregadoHorarioAtendimento || undefined,
|
||||||
prazoRespostaPadrao,
|
prazoRespostaPadrao,
|
||||||
diasAlertaVencimento
|
diasAlertaVencimento,
|
||||||
|
termoObrigatorio,
|
||||||
|
versaoTermoAtual: versaoTermoAtual || '1.0'
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Configurações salvas com sucesso!');
|
toast.success('Configurações salvas com sucesso!');
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(error.message || 'Erro ao salvar configurações');
|
const message = error instanceof Error ? error.message : 'Erro ao salvar configurações';
|
||||||
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
carregando = false;
|
carregando = false;
|
||||||
}
|
}
|
||||||
@@ -120,6 +130,73 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Horário de Atendimento</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<Calendar class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={encarregadoHorarioAtendimento}
|
||||||
|
placeholder="Segunda a Sexta, das 8h às 17h"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-base-content/60">
|
||||||
|
Horário de atendimento do Encarregado de Dados
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configurações de Termo de Consentimento -->
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Termo de Consentimento</h2>
|
||||||
|
<p class="text-base-content/60 mb-6">
|
||||||
|
Configure se o termo de consentimento é obrigatório para acesso ao sistema.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="cursor-pointer label justify-start gap-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={termoObrigatorio}
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="label-text font-semibold">Termo de Consentimento Obrigatório</span>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
Quando habilitado, os usuários precisam aceitar o termo de consentimento
|
||||||
|
antes de acessar o sistema.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Versão Atual do Termo</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={versaoTermoAtual}
|
||||||
|
placeholder="1.0"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-base-content/60">
|
||||||
|
Versão do termo de consentimento atual. Ao alterar, usuários precisarão aceitar
|
||||||
|
novamente.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,8 +87,9 @@
|
|||||||
compartilhamentoTerceiros = false;
|
compartilhamentoTerceiros = false;
|
||||||
terceiros = [];
|
terceiros = [];
|
||||||
descricao = '';
|
descricao = '';
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(error.message || 'Erro ao criar registro');
|
const message = error instanceof Error ? error.message : 'Erro ao criar registro';
|
||||||
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
carregando = false;
|
carregando = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { useQuery, useMutation } from 'convex-svelte';
|
import { useQuery, useMutation } from 'convex-svelte';
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import {
|
import {
|
||||||
Shield,
|
Shield,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await responderSolicitacao({
|
await responderSolicitacao({
|
||||||
solicitacaoId: solicitacaoSelecionada as any,
|
solicitacaoId: solicitacaoSelecionada as Id<'solicitacoesLGPD'>,
|
||||||
resposta: resposta.trim(),
|
resposta: resposta.trim(),
|
||||||
status: statusResposta
|
status: statusResposta
|
||||||
});
|
});
|
||||||
@@ -114,8 +115,9 @@
|
|||||||
toast.success('Solicitação respondida com sucesso!');
|
toast.success('Solicitação respondida com sucesso!');
|
||||||
solicitacaoSelecionada = null;
|
solicitacaoSelecionada = null;
|
||||||
resposta = '';
|
resposta = '';
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(error.message || 'Erro ao responder solicitação');
|
const message = error instanceof Error ? error.message : 'Erro ao responder solicitação';
|
||||||
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
carregando = false;
|
carregando = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export const verificarConsentimento = query({
|
|||||||
v.object({
|
v.object({
|
||||||
aceito: v.boolean(),
|
aceito: v.boolean(),
|
||||||
versao: v.string(),
|
versao: v.string(),
|
||||||
aceitoEm: v.number()
|
aceitoEm: v.number(),
|
||||||
|
termoObrigatorio: v.boolean(),
|
||||||
|
versaoTermoAtual: v.string()
|
||||||
}),
|
}),
|
||||||
v.null()
|
v.null()
|
||||||
),
|
),
|
||||||
@@ -35,6 +37,15 @@ export const verificarConsentimento = query({
|
|||||||
|
|
||||||
const tipo = args.tipo || 'termo_uso';
|
const tipo = args.tipo || 'termo_uso';
|
||||||
|
|
||||||
|
// Buscar configuração para verificar se termo é obrigatório
|
||||||
|
const config = await ctx.db
|
||||||
|
.query('configuracaoLGPD')
|
||||||
|
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const termoObrigatorio = config?.termoObrigatorio ?? false;
|
||||||
|
const versaoTermoAtual = config?.versaoTermoAtual ?? '1.0';
|
||||||
|
|
||||||
const consentimento = await ctx.db
|
const consentimento = await ctx.db
|
||||||
.query('consentimentos')
|
.query('consentimentos')
|
||||||
.withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', tipo))
|
.withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', tipo))
|
||||||
@@ -42,13 +53,21 @@ export const verificarConsentimento = query({
|
|||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!consentimento || !consentimento.aceito || consentimento.revogadoEm) {
|
if (!consentimento || !consentimento.aceito || consentimento.revogadoEm) {
|
||||||
return null;
|
return {
|
||||||
|
aceito: false,
|
||||||
|
versao: '',
|
||||||
|
aceitoEm: 0,
|
||||||
|
termoObrigatorio,
|
||||||
|
versaoTermoAtual
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
aceito: consentimento.aceito,
|
aceito: consentimento.aceito,
|
||||||
versao: consentimento.versao,
|
versao: consentimento.versao,
|
||||||
aceitoEm: consentimento.aceitoEm
|
aceitoEm: consentimento.aceitoEm,
|
||||||
|
termoObrigatorio,
|
||||||
|
versaoTermoAtual
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -476,15 +495,43 @@ export const exportarDadosUsuario = query({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Buscar todos os dados do usuário
|
// Buscar todos os dados do usuário
|
||||||
const dadosUsuario: any = {
|
type DadosUsuario = {
|
||||||
|
usuario: {
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
setor?: string;
|
||||||
|
};
|
||||||
|
consentimentos: Array<{
|
||||||
|
tipo: string;
|
||||||
|
aceito: boolean;
|
||||||
|
versao: string;
|
||||||
|
aceitoEm: number;
|
||||||
|
revogadoEm?: number;
|
||||||
|
}>;
|
||||||
|
solicitacoes: Array<{
|
||||||
|
tipo: string;
|
||||||
|
status: string;
|
||||||
|
criadoEm: number;
|
||||||
|
respondidoEm?: number;
|
||||||
|
}>;
|
||||||
|
funcionario?: {
|
||||||
|
nome: string;
|
||||||
|
matricula?: string;
|
||||||
|
cpf: string;
|
||||||
|
email: string;
|
||||||
|
telefone: string;
|
||||||
|
descricaoCargo?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const dadosUsuario: DadosUsuario = {
|
||||||
usuario: {
|
usuario: {
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email,
|
email: usuario.email,
|
||||||
setor: usuario.setor
|
setor: usuario.setor
|
||||||
},
|
},
|
||||||
consentimentos: [],
|
consentimentos: [],
|
||||||
solicitacoes: [],
|
solicitacoes: []
|
||||||
atividades: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Consentimentos
|
// Consentimentos
|
||||||
@@ -522,8 +569,7 @@ export const exportarDadosUsuario = query({
|
|||||||
cpf: funcionario.cpf,
|
cpf: funcionario.cpf,
|
||||||
email: funcionario.email,
|
email: funcionario.email,
|
||||||
telefone: funcionario.telefone,
|
telefone: funcionario.telefone,
|
||||||
cargo: funcionario.cargo,
|
descricaoCargo: funcionario.descricaoCargo
|
||||||
setor: funcionario.setor
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,8 +701,11 @@ export const obterConfiguracaoLGPD = query({
|
|||||||
encarregadoNome: v.union(v.string(), v.null()),
|
encarregadoNome: v.union(v.string(), v.null()),
|
||||||
encarregadoEmail: v.union(v.string(), v.null()),
|
encarregadoEmail: v.union(v.string(), v.null()),
|
||||||
encarregadoTelefone: v.union(v.string(), v.null()),
|
encarregadoTelefone: v.union(v.string(), v.null()),
|
||||||
|
encarregadoHorarioAtendimento: v.union(v.string(), v.null()),
|
||||||
prazoRespostaPadrao: v.number(),
|
prazoRespostaPadrao: v.number(),
|
||||||
diasAlertaVencimento: v.number()
|
diasAlertaVencimento: v.number(),
|
||||||
|
termoObrigatorio: v.boolean(),
|
||||||
|
versaoTermoAtual: v.string()
|
||||||
}),
|
}),
|
||||||
v.null()
|
v.null()
|
||||||
),
|
),
|
||||||
@@ -672,8 +721,11 @@ export const obterConfiguracaoLGPD = query({
|
|||||||
encarregadoNome: null,
|
encarregadoNome: null,
|
||||||
encarregadoEmail: null,
|
encarregadoEmail: null,
|
||||||
encarregadoTelefone: null,
|
encarregadoTelefone: null,
|
||||||
|
encarregadoHorarioAtendimento: null,
|
||||||
prazoRespostaPadrao: 15,
|
prazoRespostaPadrao: 15,
|
||||||
diasAlertaVencimento: 3
|
diasAlertaVencimento: 3,
|
||||||
|
termoObrigatorio: false,
|
||||||
|
versaoTermoAtual: '1.0'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,8 +733,11 @@ export const obterConfiguracaoLGPD = query({
|
|||||||
encarregadoNome: config.encarregadoNome ?? null,
|
encarregadoNome: config.encarregadoNome ?? null,
|
||||||
encarregadoEmail: config.encarregadoEmail ?? null,
|
encarregadoEmail: config.encarregadoEmail ?? null,
|
||||||
encarregadoTelefone: config.encarregadoTelefone ?? null,
|
encarregadoTelefone: config.encarregadoTelefone ?? null,
|
||||||
|
encarregadoHorarioAtendimento: config.encarregadoHorarioAtendimento ?? null,
|
||||||
prazoRespostaPadrao: config.prazoRespostaPadrao,
|
prazoRespostaPadrao: config.prazoRespostaPadrao,
|
||||||
diasAlertaVencimento: config.diasAlertaVencimento
|
diasAlertaVencimento: config.diasAlertaVencimento,
|
||||||
|
termoObrigatorio: config.termoObrigatorio,
|
||||||
|
versaoTermoAtual: config.versaoTermoAtual
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -695,8 +750,11 @@ export const atualizarConfiguracaoLGPD = mutation({
|
|||||||
encarregadoNome: v.optional(v.string()),
|
encarregadoNome: v.optional(v.string()),
|
||||||
encarregadoEmail: v.optional(v.string()),
|
encarregadoEmail: v.optional(v.string()),
|
||||||
encarregadoTelefone: v.optional(v.string()),
|
encarregadoTelefone: v.optional(v.string()),
|
||||||
|
encarregadoHorarioAtendimento: v.optional(v.string()),
|
||||||
prazoRespostaPadrao: v.optional(v.number()),
|
prazoRespostaPadrao: v.optional(v.number()),
|
||||||
diasAlertaVencimento: v.optional(v.number())
|
diasAlertaVencimento: v.optional(v.number()),
|
||||||
|
termoObrigatorio: v.optional(v.boolean()),
|
||||||
|
versaoTermoAtual: v.optional(v.string())
|
||||||
},
|
},
|
||||||
returns: v.object({ sucesso: v.boolean() }),
|
returns: v.object({ sucesso: v.boolean() }),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
@@ -716,13 +774,29 @@ export const atualizarConfiguracaoLGPD = mutation({
|
|||||||
await ctx.db.patch(config._id, { ativo: false });
|
await ctx.db.patch(config._id, { ativo: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buscar valores atuais para manter os que não foram atualizados
|
||||||
|
const valoresAtuais = config || {
|
||||||
|
encarregadoNome: undefined,
|
||||||
|
encarregadoEmail: undefined,
|
||||||
|
encarregadoTelefone: undefined,
|
||||||
|
encarregadoHorarioAtendimento: undefined,
|
||||||
|
prazoRespostaPadrao: 15,
|
||||||
|
diasAlertaVencimento: 3,
|
||||||
|
termoObrigatorio: false,
|
||||||
|
versaoTermoAtual: '1.0'
|
||||||
|
};
|
||||||
|
|
||||||
// Criar nova configuração
|
// Criar nova configuração
|
||||||
await ctx.db.insert('configuracaoLGPD', {
|
await ctx.db.insert('configuracaoLGPD', {
|
||||||
encarregadoNome: args.encarregadoNome,
|
encarregadoNome: args.encarregadoNome ?? valoresAtuais.encarregadoNome ?? undefined,
|
||||||
encarregadoEmail: args.encarregadoEmail,
|
encarregadoEmail: args.encarregadoEmail ?? valoresAtuais.encarregadoEmail ?? undefined,
|
||||||
encarregadoTelefone: args.encarregadoTelefone,
|
encarregadoTelefone: args.encarregadoTelefone ?? valoresAtuais.encarregadoTelefone ?? undefined,
|
||||||
prazoRespostaPadrao: args.prazoRespostaPadrao ?? 15,
|
encarregadoHorarioAtendimento:
|
||||||
diasAlertaVencimento: args.diasAlertaVencimento ?? 3,
|
args.encarregadoHorarioAtendimento ?? valoresAtuais.encarregadoHorarioAtendimento ?? undefined,
|
||||||
|
prazoRespostaPadrao: args.prazoRespostaPadrao ?? valoresAtuais.prazoRespostaPadrao,
|
||||||
|
diasAlertaVencimento: args.diasAlertaVencimento ?? valoresAtuais.diasAlertaVencimento,
|
||||||
|
termoObrigatorio: args.termoObrigatorio ?? valoresAtuais.termoObrigatorio ?? false,
|
||||||
|
versaoTermoAtual: args.versaoTermoAtual ?? valoresAtuais.versaoTermoAtual ?? '1.0',
|
||||||
ativo: true,
|
ativo: true,
|
||||||
atualizadoPor: usuario._id,
|
atualizadoPor: usuario._id,
|
||||||
atualizadoEm: Date.now()
|
atualizadoEm: Date.now()
|
||||||
|
|||||||
@@ -1956,8 +1956,11 @@ export default defineSchema({
|
|||||||
encarregadoNome: v.optional(v.string()),
|
encarregadoNome: v.optional(v.string()),
|
||||||
encarregadoEmail: v.optional(v.string()),
|
encarregadoEmail: v.optional(v.string()),
|
||||||
encarregadoTelefone: v.optional(v.string()),
|
encarregadoTelefone: v.optional(v.string()),
|
||||||
|
encarregadoHorarioAtendimento: v.optional(v.string()), // Ex: "Segunda a Sexta, das 8h às 17h"
|
||||||
prazoRespostaPadrao: v.number(), // em dias (padrão: 15)
|
prazoRespostaPadrao: v.number(), // em dias (padrão: 15)
|
||||||
diasAlertaVencimento: v.number(), // dias antes do prazo para alertar (padrão: 3)
|
diasAlertaVencimento: v.number(), // dias antes do prazo para alertar (padrão: 3)
|
||||||
|
termoObrigatorio: v.boolean(), // Se o termo de consentimento é obrigatório
|
||||||
|
versaoTermoAtual: v.string(), // Versão atual do termo (ex: "1.0")
|
||||||
politicaRetencao: v.optional(v.string()), // JSON com política de retenção por tipo de dado
|
politicaRetencao: v.optional(v.string()), // JSON com política de retenção por tipo de dado
|
||||||
ativo: v.boolean(),
|
ativo: v.boolean(),
|
||||||
atualizadoPor: v.id("usuarios"),
|
atualizadoPor: v.id("usuarios"),
|
||||||
|
|||||||
Reference in New Issue
Block a user