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:
2025-12-02 05:54:37 -03:00
parent fec5f5c33d
commit 2825bd0e6e
9 changed files with 270 additions and 62 deletions

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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()

View File

@@ -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"),