From 2825bd0e6e9327b22a39ed139049a1180c4be2b8 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Tue, 2 Dec 2025 05:54:37 -0300 Subject: [PATCH] feat: enhance LGPD compliance features by adding configurable data protection officer details, consent term settings, and improved error handling across various components --- .../(dashboard)/privacidade/+page.svelte | 26 ++++- .../privacidade/meus-dados/+page.svelte | 5 +- .../termo-consentimento/+page.svelte | 50 +++++--- .../routes/(dashboard)/ti/lgpd/+page.svelte | 42 ++++--- .../ti/lgpd/configuracoes/+page.svelte | 85 +++++++++++++- .../ti/lgpd/registros-tratamento/+page.svelte | 5 +- .../ti/lgpd/solicitacoes/+page.svelte | 8 +- packages/backend/convex/lgpd.ts | 108 +++++++++++++++--- packages/backend/convex/schema.ts | 3 + 9 files changed, 270 insertions(+), 62 deletions(-) diff --git a/apps/web/src/routes/(dashboard)/privacidade/+page.svelte b/apps/web/src/routes/(dashboard)/privacidade/+page.svelte index 843a5d9..4e06757 100644 --- a/apps/web/src/routes/(dashboard)/privacidade/+page.svelte +++ b/apps/web/src/routes/(dashboard)/privacidade/+page.svelte @@ -1,6 +1,15 @@
@@ -360,31 +369,40 @@ entre em contato com nosso Encarregado de Proteção de Dados:

+ {#if encarregadoNome && encarregadoNome !== 'Encarregado de Proteção de Dados'} +
+ +
+

Nome

+

{encarregadoNome}

+
+
+ {/if}

E-mail

-

lgpd@esportes.pe.gov.br

+

{encarregadoEmail}

Telefone

-

(81) 3184-XXXX

+

{encarregadoTelefone}

Horário de Atendimento

-

Segunda a Sexta, das 8h às 17h

+

{encarregadoHorario}

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

diff --git a/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte b/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte index a0fbc57..9d9dddb 100644 --- a/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte +++ b/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte @@ -115,8 +115,9 @@ tipoSelecionado = null; dadosSolicitados = ''; observacoes = ''; - } catch (error: any) { - toast.error(error.message || 'Erro ao criar solicitação'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Erro ao criar solicitação'; + toast.error(message); } finally { carregando = false; } diff --git a/apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte b/apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte index dc1b68a..79a4c38 100644 --- a/apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte +++ b/apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte @@ -14,15 +14,25 @@ const registrarConsentimento = useMutation(api.lgpd.registrarConsentimento); 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() { - if (!aceito) { + if (termoObrigatorio && !aceito) { erro = 'Você precisa aceitar o termo para continuar'; return; } + if (!aceito) { + // Se não é obrigatório e não aceitou, apenas redireciona + window.location.href = resolve('/'); + return; + } + carregando = true; erro = null; @@ -30,11 +40,11 @@ await registrarConsentimento({ tipo: 'termo_uso', aceito: true, - versao: '1.0' + versao: versaoTermoAtual }); sucesso = true; - } catch (e: any) { - erro = e.message || 'Erro ao registrar consentimento'; + } catch (e: unknown) { + erro = e instanceof Error ? e.message : 'Erro ao registrar consentimento'; } finally { carregando = false; } @@ -68,7 +78,7 @@

Termo Já Aceito

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

@@ -208,14 +218,24 @@
-
- -

- Atenção: O aceite deste termo é obrigatório para utilização do - sistema. Ao aceitar, você confirma que leu, compreendeu e concorda com todos os - termos e condições estabelecidos. -

-
+ {#if termoObrigatorio} +
+ +

+ Atenção: O aceite deste termo é obrigatório para utilização do + sistema. Ao aceitar, você confirma que leu, compreendeu e concorda com todos os + termos e condições estabelecidos. +

+
+ {:else} +
+ +

+ Informação: O aceite deste termo é opcional. Você pode aceitar + voluntariamente ou continuar sem aceitar. +

+
+ {/if}
@@ -253,7 +273,7 @@
- {#if estatisticas} + {#if estatisticas?.data}

Solicitações por Tipo

- {#each Object.entries(estatisticas.solicitacoesPorTipo) as [tipo, quantidade]} + {#each Object.entries(estatisticas.data.solicitacoesPorTipo) as [tipo, quantidade]}
{tipo} {quantidade} @@ -111,19 +123,19 @@
Total de ROTs: - {estatisticas.totalROTs} + {estatisticas.data.totalROTs}
ROTs Ativos: - {estatisticas.rotsAtivos} + {estatisticas.data.rotsAtivos}
Total de Consentimentos: - {estatisticas.totalConsentimentos} + {estatisticas.data.totalConsentimentos}
Consentimentos Ativos: - {estatisticas.consentimentosAtivos} + {estatisticas.data.consentimentosAtivos}
diff --git a/apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte index 5189eb7..e663040 100644 --- a/apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte @@ -2,7 +2,7 @@ import { resolve } from '$app/paths'; import { useQuery, useMutation } from 'convex-svelte'; 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'; const config = useQuery(api.lgpd.obterConfiguracaoLGPD, {}); @@ -11,8 +11,11 @@ let encarregadoNome = $state(''); let encarregadoEmail = $state(''); let encarregadoTelefone = $state(''); + let encarregadoHorarioAtendimento = $state('Segunda a Sexta, das 8h às 17h'); let prazoRespostaPadrao = $state(15); let diasAlertaVencimento = $state(3); + let termoObrigatorio = $state(false); + let versaoTermoAtual = $state('1.0'); let carregando = $state(false); // Sincronizar com query @@ -21,8 +24,11 @@ encarregadoNome = config.data.encarregadoNome || ''; encarregadoEmail = config.data.encarregadoEmail || ''; encarregadoTelefone = config.data.encarregadoTelefone || ''; + encarregadoHorarioAtendimento = config.data.encarregadoHorarioAtendimento || 'Segunda a Sexta, das 8h às 17h'; prazoRespostaPadrao = config.data.prazoRespostaPadrao; diasAlertaVencimento = config.data.diasAlertaVencimento; + termoObrigatorio = config.data.termoObrigatorio; + versaoTermoAtual = config.data.versaoTermoAtual; } }); @@ -34,13 +40,17 @@ encarregadoNome: encarregadoNome || undefined, encarregadoEmail: encarregadoEmail || undefined, encarregadoTelefone: encarregadoTelefone || undefined, + encarregadoHorarioAtendimento: encarregadoHorarioAtendimento || undefined, prazoRespostaPadrao, - diasAlertaVencimento + diasAlertaVencimento, + termoObrigatorio, + versaoTermoAtual: versaoTermoAtual || '1.0' }); toast.success('Configurações salvas com sucesso!'); - } catch (error: any) { - toast.error(error.message || 'Erro ao salvar configurações'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Erro ao salvar configurações'; + toast.error(message); } finally { carregando = false; } @@ -120,6 +130,73 @@ />
+ +
+ +
+ + +
+ +
+
+
+ + + +
+
+

Termo de Consentimento

+

+ Configure se o termo de consentimento é obrigatório para acesso ao sistema. +

+ +
+
+ +
+ +
+ + + +
diff --git a/apps/web/src/routes/(dashboard)/ti/lgpd/registros-tratamento/+page.svelte b/apps/web/src/routes/(dashboard)/ti/lgpd/registros-tratamento/+page.svelte index 8f0ce77..5d17efc 100644 --- a/apps/web/src/routes/(dashboard)/ti/lgpd/registros-tratamento/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/registros-tratamento/+page.svelte @@ -87,8 +87,9 @@ compartilhamentoTerceiros = false; terceiros = []; descricao = ''; - } catch (error: any) { - toast.error(error.message || 'Erro ao criar registro'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Erro ao criar registro'; + toast.error(message); } finally { carregando = false; } diff --git a/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte index 57027d6..eb52064 100644 --- a/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte @@ -2,6 +2,7 @@ import { resolve } from '$app/paths'; import { useQuery, useMutation } from 'convex-svelte'; import { api } from '@sgse-app/backend/convex/_generated/api'; + import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { Shield, FileText, @@ -106,7 +107,7 @@ try { await responderSolicitacao({ - solicitacaoId: solicitacaoSelecionada as any, + solicitacaoId: solicitacaoSelecionada as Id<'solicitacoesLGPD'>, resposta: resposta.trim(), status: statusResposta }); @@ -114,8 +115,9 @@ toast.success('Solicitação respondida com sucesso!'); solicitacaoSelecionada = null; resposta = ''; - } catch (error: any) { - toast.error(error.message || 'Erro ao responder solicitação'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Erro ao responder solicitação'; + toast.error(message); } finally { carregando = false; } diff --git a/packages/backend/convex/lgpd.ts b/packages/backend/convex/lgpd.ts index b3598c0..1740bff 100644 --- a/packages/backend/convex/lgpd.ts +++ b/packages/backend/convex/lgpd.ts @@ -23,7 +23,9 @@ export const verificarConsentimento = query({ v.object({ aceito: v.boolean(), versao: v.string(), - aceitoEm: v.number() + aceitoEm: v.number(), + termoObrigatorio: v.boolean(), + versaoTermoAtual: v.string() }), v.null() ), @@ -35,6 +37,15 @@ export const verificarConsentimento = query({ 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 .query('consentimentos') .withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', tipo)) @@ -42,13 +53,21 @@ export const verificarConsentimento = query({ .first(); if (!consentimento || !consentimento.aceito || consentimento.revogadoEm) { - return null; + return { + aceito: false, + versao: '', + aceitoEm: 0, + termoObrigatorio, + versaoTermoAtual + }; } return { aceito: consentimento.aceito, 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 - 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: { nome: usuario.nome, email: usuario.email, setor: usuario.setor }, consentimentos: [], - solicitacoes: [], - atividades: [] + solicitacoes: [] }; // Consentimentos @@ -522,8 +569,7 @@ export const exportarDadosUsuario = query({ cpf: funcionario.cpf, email: funcionario.email, telefone: funcionario.telefone, - cargo: funcionario.cargo, - setor: funcionario.setor + descricaoCargo: funcionario.descricaoCargo }; } } @@ -655,8 +701,11 @@ export const obterConfiguracaoLGPD = query({ encarregadoNome: v.union(v.string(), v.null()), encarregadoEmail: v.union(v.string(), v.null()), encarregadoTelefone: v.union(v.string(), v.null()), + encarregadoHorarioAtendimento: v.union(v.string(), v.null()), prazoRespostaPadrao: v.number(), - diasAlertaVencimento: v.number() + diasAlertaVencimento: v.number(), + termoObrigatorio: v.boolean(), + versaoTermoAtual: v.string() }), v.null() ), @@ -672,8 +721,11 @@ export const obterConfiguracaoLGPD = query({ encarregadoNome: null, encarregadoEmail: null, encarregadoTelefone: null, + encarregadoHorarioAtendimento: null, prazoRespostaPadrao: 15, - diasAlertaVencimento: 3 + diasAlertaVencimento: 3, + termoObrigatorio: false, + versaoTermoAtual: '1.0' }; } @@ -681,8 +733,11 @@ export const obterConfiguracaoLGPD = query({ encarregadoNome: config.encarregadoNome ?? null, encarregadoEmail: config.encarregadoEmail ?? null, encarregadoTelefone: config.encarregadoTelefone ?? null, + encarregadoHorarioAtendimento: config.encarregadoHorarioAtendimento ?? null, 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()), encarregadoEmail: v.optional(v.string()), encarregadoTelefone: v.optional(v.string()), + encarregadoHorarioAtendimento: v.optional(v.string()), 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() }), handler: async (ctx, args) => { @@ -716,13 +774,29 @@ export const atualizarConfiguracaoLGPD = mutation({ 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 await ctx.db.insert('configuracaoLGPD', { - encarregadoNome: args.encarregadoNome, - encarregadoEmail: args.encarregadoEmail, - encarregadoTelefone: args.encarregadoTelefone, - prazoRespostaPadrao: args.prazoRespostaPadrao ?? 15, - diasAlertaVencimento: args.diasAlertaVencimento ?? 3, + encarregadoNome: args.encarregadoNome ?? valoresAtuais.encarregadoNome ?? undefined, + encarregadoEmail: args.encarregadoEmail ?? valoresAtuais.encarregadoEmail ?? undefined, + encarregadoTelefone: args.encarregadoTelefone ?? valoresAtuais.encarregadoTelefone ?? undefined, + encarregadoHorarioAtendimento: + 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, atualizadoPor: usuario._id, atualizadoEm: Date.now() diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 7cf8095..796db0a 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -1956,8 +1956,11 @@ export default defineSchema({ encarregadoNome: v.optional(v.string()), encarregadoEmail: 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) 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 ativo: v.boolean(), atualizadoPor: v.id("usuarios"),