From fec5f5c33d73f6e3c41292b51c19a2f232002eb4 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 1 Dec 2025 22:37:43 -0300 Subject: [PATCH] feat: implement LGPD compliance features including data request management, consent tracking, and statistics display in the dashboard for enhanced data protection compliance --- apps/web/src/lib/components/Sidebar.svelte | 8 +- .../perfil/privacidade/+page.svelte | 194 +++++ .../(dashboard)/privacidade/+page.svelte | 419 +++++++++ .../privacidade/meus-dados/+page.svelte | 377 +++++++++ .../termo-consentimento/+page.svelte | 276 ++++++ .../src/routes/(dashboard)/ti/+page.svelte | 13 + .../routes/(dashboard)/ti/lgpd/+page.svelte | 134 +++ .../ti/lgpd/configuracoes/+page.svelte | 196 +++++ .../ti/lgpd/registros-tratamento/+page.svelte | 341 ++++++++ .../ti/lgpd/solicitacoes/+page.svelte | 333 ++++++++ .../ti/painel-administrativo/+page.svelte | 56 +- packages/backend/convex/_generated/api.d.ts | 2 + packages/backend/convex/lgpd.ts | 796 ++++++++++++++++++ packages/backend/convex/schema.ts | 91 ++ 14 files changed, 3231 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/routes/(dashboard)/perfil/privacidade/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/privacidade/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/ti/lgpd/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/ti/lgpd/registros-tratamento/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte create mode 100644 packages/backend/convex/lgpd.ts diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index c290ba7..7a30849 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -451,10 +451,10 @@ href={resolve('/abrir-chamado')} class="link link-hover hover:text-primary transition-colors">Suporte - - Privacidade + + Privacidade
diff --git a/apps/web/src/routes/(dashboard)/perfil/privacidade/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/privacidade/+page.svelte new file mode 100644 index 0000000..2e3bf7c --- /dev/null +++ b/apps/web/src/routes/(dashboard)/perfil/privacidade/+page.svelte @@ -0,0 +1,194 @@ + + +
+ +
+
+
+ +
+
+

Preferências de Privacidade

+

+ Gerencie seus consentimentos e preferências de privacidade +

+
+
+
+ + +
+
+

Meus Consentimentos

+ + {#if consentimentos === undefined} +
+ +
+ {:else if consentimentos.length === 0} +
+ +

Nenhum consentimento registrado

+
+ {:else} +
+ {#each consentimentos as consentimento} +
+
+
+
+ {#if consentimento.aceito && !consentimento.revogadoEm} + + {:else} + + {/if} +

+ {getTipoLabel(consentimento.tipo)} +

+ {#if consentimento.aceito && !consentimento.revogadoEm} + Ativo + {:else} + Revogado + {/if} +
+ +
+
+ + + Aceito em:{' '} + {format(new Date(consentimento.aceitoEm), 'dd/MM/yyyy às HH:mm', { + locale: ptBR + })} + +
+
+ Versão: {consentimento.versao} +
+ {#if consentimento.revogadoEm} +
+ + + Revogado em:{' '} + {format( + new Date(consentimento.revogadoEm), + 'dd/MM/yyyy às HH:mm', + { locale: ptBR } + )} + +
+ {/if} +
+
+ + {#if consentimento.aceito && !consentimento.revogadoEm} + + {/if} +
+
+ {/each} +
+ {/if} +
+
+ + +
+
+

Informações Importantes

+ +
+ +
+

Atenção ao Revogar Consentimentos

+

+ A revogação de alguns consentimentos pode impedir o acesso a funcionalidades do + sistema que dependem do tratamento de dados pessoais. +

+
+
+ +
+

+ Termo de Uso: Aceite obrigatório para utilização do sistema. A + revogação pode impedir o acesso. +

+

+ Política de Privacidade: Informa como seus dados são tratados. A + revogação não impede o tratamento, mas você pode solicitar exclusão. +

+

+ Comunicações: Permite envio de notificações e comunicações do + sistema. A revogação pode limitar informações importantes. +

+

+ Compartilhamento de Dados: Permite compartilhamento com terceiros + quando necessário. A revogação pode afetar serviços terceirizados. +

+
+
+
+ + + +
+ diff --git a/apps/web/src/routes/(dashboard)/privacidade/+page.svelte b/apps/web/src/routes/(dashboard)/privacidade/+page.svelte new file mode 100644 index 0000000..843a5d9 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/privacidade/+page.svelte @@ -0,0 +1,419 @@ + + +
+ +
+
+
+ +
+
+

Política de Privacidade

+

+ Lei Geral de Proteção de Dados Pessoais (LGPD) - Lei nº 13.709/2018 +

+
+
+
+ + Última atualização: {new Date().toLocaleDateString('pt-BR')} +
+
+ + +
+ +
+
+

1. Introdução

+

+ A Secretaria de Esportes do Estado de Pernambuco, no exercício de suas atribuições + constitucionais e legais, está comprometida com a proteção dos dados pessoais de seus + servidores, colaboradores e cidadãos, em conformidade com a Lei Geral de Proteção de + Dados Pessoais (LGPD) - Lei nº 13.709/2018. +

+

+ Esta Política de Privacidade descreve como coletamos, utilizamos, armazenamos e + protegemos seus dados pessoais no Sistema de Gestão da Secretaria de Esportes (SGSE). +

+
+
+ + +
+
+

2. Dados Pessoais Coletados

+

+ O SGSE coleta e processa os seguintes tipos de dados pessoais: +

+
+
+ +
+

Dados de Identificação

+

+ Nome completo, CPF, RG, data de nascimento, naturalidade, nacionalidade, + estado civil, filiação (nome do pai e mãe) +

+
+
+
+ +
+

Dados de Contato

+

+ E-mail, telefone, endereço residencial e endereços de marcação de ponto +

+
+
+
+ +
+

Dados Profissionais

+

+ Matrícula, cargo, função, setor, data de admissão, regime de trabalho, + documentos profissionais (CTPS, título eleitor, reservista, PIS) +

+
+
+
+ +
+

Dados de Saúde

+

+ Atestados médicos, licenças de saúde, grupo sanguíneo, fator RH (quando + necessário para atividades específicas) +

+
+
+
+ +
+

Dados de Acesso

+

+ Credenciais de acesso, logs de acesso, endereço IP, histórico de atividades + no sistema +

+
+
+
+
+
+ + +
+
+

3. Finalidade do Tratamento

+

+ Os dados pessoais são tratados para as seguintes finalidades: +

+
    +
  • Gestão de recursos humanos e folha de pagamento
  • +
  • Controle de ponto e registro de jornada de trabalho
  • +
  • Gestão de férias, ausências e licenças
  • +
  • Processamento de atestados médicos e licenças de saúde
  • +
  • Gestão de chamados e suporte técnico
  • +
  • Comunicação interna e notificações
  • +
  • Cumprimento de obrigações legais e regulatórias
  • +
  • Execução de políticas públicas
  • +
  • Segurança e prevenção de fraudes
  • +
  • Auditoria e controle interno
  • +
+
+
+ + +
+
+

4. Base Legal do Tratamento

+

+ O tratamento de dados pessoais no SGSE fundamenta-se nas seguintes bases legais, + conforme previsto no Art. 7º da LGPD: +

+
+
+

I. Execução de Políticas Públicas

+

+ Art. 7º, II - Para a execução de políticas públicas previstas em leis ou + regulamentos +

+
+
+

II. Cumprimento de Obrigação Legal

+

+ Art. 7º, I - Para cumprimento de obrigação legal ou regulatória pelo controlador +

+
+
+

III. Execução de Contrato

+

+ Art. 7º, V - Para a execução de contrato ou de procedimentos preliminares + relacionados a contrato do qual seja parte o titular +

+
+
+

IV. Proteção da Vida e Saúde

+

+ Art. 7º, VI e VII - Para a proteção da vida ou da incolumidade física do titular + ou de terceiro, e para a tutela da saúde +

+
+
+
+
+ + +
+
+

5. Compartilhamento de Dados

+

+ Os dados pessoais podem ser compartilhados com: +

+
    +
  • + Órgãos Públicos: Quando necessário para cumprimento de obrigações + legais ou execução de políticas públicas +
  • +
  • + Fornecedores de Serviços: Empresas contratadas para prestação de + serviços técnicos, sempre com garantias de proteção de dados +
  • +
  • + Autoridades Competentes: Quando exigido por determinação judicial ou + legal +
  • +
+
+

+ Todos os compartilhamentos são realizados com base legal e com garantias de + proteção dos dados pessoais. +

+
+
+
+ + +
+
+

6. Medidas de Segurança

+

+ Adotamos medidas técnicas e administrativas para proteger seus dados pessoais: +

+
+
+

Medidas Técnicas

+
    +
  • • Criptografia de dados sensíveis
  • +
  • • Controle de acesso por permissões
  • +
  • • Logs de auditoria
  • +
  • • Backup regular
  • +
  • • Monitoramento de segurança
  • +
+
+
+

Medidas Administrativas

+
    +
  • • Treinamento de equipe
  • +
  • • Políticas de acesso
  • +
  • • Procedimentos de segurança
  • +
  • • Gestão de incidentes
  • +
  • • Revisão periódica
  • +
+
+
+
+
+ + +
+
+

7. Prazo de Retenção

+

+ Os dados pessoais são mantidos pelo prazo necessário para: +

+
    +
  • + Dados de Funcionários Ativos: Durante todo o período de vínculo + empregatício/estatutário +
  • +
  • + Dados de Funcionários Inativos: Conforme prazo legal aplicável (em + geral, 5 anos após desligamento) +
  • +
  • + Logs de Acesso: 2 anos, conforme recomendação da ANPD +
  • +
  • + Documentos Trabalhistas: Conforme legislação trabalhista aplicável +
  • +
+

+ Após o término do prazo de retenção, os dados são eliminados de forma segura, exceto + quando houver obrigação legal de manutenção. +

+
+
+ + +
+
+

8. Direitos do Titular dos Dados

+

+ Conforme previsto no Art. 18 da LGPD, você possui os seguintes direitos: +

+
+
+
1
+
+

Confirmação da Existência de Tratamento

+

+ Confirmar se tratamos seus dados pessoais +

+
+
+
+
2
+
+

Acesso aos Dados

+

+ Acessar seus dados pessoais tratados por nós +

+
+
+
+
3
+
+

Correção de Dados

+

+ Solicitar correção de dados incompletos, inexatos ou desatualizados +

+
+
+
+
4
+
+

Anonimização, Bloqueio ou Eliminação

+

+ Solicitar anonimização, bloqueio ou eliminação de dados desnecessários ou + excessivos +

+
+
+
+
5
+
+

Portabilidade dos Dados

+

+ Solicitar a portabilidade dos dados a outro fornecedor de serviço ou produto +

+
+
+
+
6
+
+

Eliminação de Dados

+

+ Solicitar a eliminação dos dados pessoais tratados com base em consentimento +

+
+
+
+
7
+
+

Informação sobre Compartilhamento

+

+ Obter informações sobre compartilhamento de dados com terceiros +

+
+
+
+
8
+
+

Revogação de Consentimento

+

+ Revogar seu consentimento, quando aplicável +

+
+
+
+ +
+
+ + +
+
+

9. Encarregado de Proteção de Dados (DPO)

+

+ Para exercer seus direitos ou esclarecer dúvidas sobre o tratamento de dados pessoais, + entre em contato com nosso Encarregado de Proteção de Dados: +

+
+
+ +
+

E-mail

+

lgpd@esportes.pe.gov.br

+
+
+
+ +
+

Telefone

+

(81) 3184-XXXX

+
+
+
+ +
+

Horário de Atendimento

+

Segunda a Sexta, das 8h às 17h

+
+
+
+
+

+ As solicitações serão respondidas em até 15 (quinze) dias, conforme previsto na + LGPD. +

+
+
+
+ + +
+
+

10. Alterações nesta Política

+

+ Esta Política de Privacidade pode ser atualizada periodicamente. Recomendamos que + você revise esta página regularmente para estar ciente de quaisquer alterações. A data + da última atualização está indicada no topo desta página. +

+
+
+ + + +
+
+ diff --git a/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte b/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte new file mode 100644 index 0000000..a0fbc57 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte @@ -0,0 +1,377 @@ + + +
+ +
+
+
+ +
+
+

Meus Direitos LGPD

+

+ Solicite o exercício dos seus direitos conforme a Lei Geral de Proteção de Dados +

+
+
+
+ +
+ +
+ +
+
+

Nova Solicitação

+ +
+ + + {#if tipoSelecionado} + + {/if} +
+ + {#if tipoSelecionado === 'correcao'} +
+ + +
+ {/if} + +
+ + +
+ + + +
+ +

+ Sua solicitação será analisada e respondida em até 15 dias úteis, conforme + previsto na LGPD. +

+
+
+
+ + +
+
+

Minhas Solicitações

+ + {#if minhasSolicitacoes === undefined} +
+ +
+ {:else if minhasSolicitacoes.length === 0} +
+ +

Nenhuma solicitação encontrada

+
+ {:else} +
+ {#each minhasSolicitacoes as solicitacao} + {@const statusInfo = getStatusBadge(solicitacao.status)} + {@const StatusIcon = getStatusIcon(solicitacao.status)} +
+
+
+ +
+

+ {tiposSolicitacao.find((t) => t.valor === solicitacao.tipo) + ?.label || solicitacao.tipo} +

+

+ Criada em{' '} + {format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", { + locale: ptBR + })} +

+
+
+ {statusInfo.label} +
+ + {#if solicitacao.resposta} +
+

Resposta:

+

{solicitacao.resposta}

+
+ {/if} + + {#if solicitacao.arquivoResposta} + + {/if} + + {#if solicitacao.status === 'pendente' || solicitacao.status === 'em_analise'} +
+ Prazo para resposta:{' '} + {format(new Date(solicitacao.prazoResposta), 'dd/MM/yyyy', { + locale: ptBR + })} +
+ {/if} +
+ {/each} +
+ {/if} +
+
+
+ + +
+ +
+
+

Exportar Meus Dados

+

+ Baixe uma cópia completa dos seus dados pessoais em formato JSON. +

+ +
+
+ + +
+
+

Seus Direitos

+
    +
  • • Confirmar existência de tratamento
  • +
  • • Acessar seus dados
  • +
  • • Corrigir dados incorretos
  • +
  • • Solicitar exclusão
  • +
  • • Portabilidade dos dados
  • +
  • • Revogar consentimento
  • +
+
+
+ + + +
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte b/apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte new file mode 100644 index 0000000..dc1b68a --- /dev/null +++ b/apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte @@ -0,0 +1,276 @@ + + +
+ +
+
+
+ +
+
+

Termo de Consentimento

+

+ Termo de Uso e Consentimento para Tratamento de Dados Pessoais +

+
+
+
+ + {#if consentimentoQuery === undefined} +
+ +
+ {:else if jaAceitou} + +
+
+ +

Termo Já Aceito

+

+ Você já aceitou este termo de consentimento. Se desejar revogar seu consentimento ou + gerenciar suas preferências de privacidade, acesse a página de privacidade. +

+ +
+
+ {:else if sucesso} + +
+
+ +

Termo Aceito com Sucesso!

+

+ Seu consentimento foi registrado. Você pode acessar o sistema normalmente. +

+ + Continuar para o Sistema + +
+
+ {:else} + +
+
+
+

Termo de Uso e Consentimento

+ +
+

1. Aceitação dos Termos

+

+ Ao utilizar o Sistema de Gestão da Secretaria de Esportes (SGSE), você concorda + com os termos e condições estabelecidos neste documento, bem como com a + Política de Privacidade do sistema. +

+
+ +
+

2. Tratamento de Dados Pessoais

+

+ Você consente que a Secretaria de Esportes do Estado de Pernambuco trate seus + dados pessoais para as finalidades descritas na Política de Privacidade, + incluindo: +

+
    +
  • Gestão de recursos humanos e folha de pagamento
  • +
  • Controle de ponto e registro de jornada de trabalho
  • +
  • Gestão de férias, ausências e licenças
  • +
  • Processamento de atestados médicos e licenças de saúde
  • +
  • Comunicação interna e notificações
  • +
  • Cumprimento de obrigações legais e regulatórias
  • +
+
+ +
+

3. Base Legal

+

+ O tratamento de seus dados pessoais fundamenta-se nas seguintes bases legais, + conforme previsto na Lei Geral de Proteção de Dados (LGPD): +

+
    +
  • + Execução de Políticas Públicas: Para a execução de políticas + públicas previstas em leis ou regulamentos +
  • +
  • + Cumprimento de Obrigação Legal: Para cumprimento de + obrigação legal ou regulatória +
  • +
  • + Execução de Contrato: Para a execução de contrato ou de + procedimentos preliminares relacionados a contrato +
  • +
+
+ +
+

4. Direitos do Titular

+

+ Você possui os seguintes direitos em relação aos seus dados pessoais: +

+
    +
  • Confirmar a existência de tratamento de dados
  • +
  • Acessar seus dados pessoais
  • +
  • Corrigir dados incompletos, inexatos ou desatualizados
  • +
  • Solicitar anonimização, bloqueio ou eliminação de dados
  • +
  • Solicitar portabilidade dos dados
  • +
  • Revogar seu consentimento
  • +
+

+ Para exercer seus direitos, acesse a página{' '} + + Solicitar Meus Direitos + + . +

+
+ +
+

5. Segurança dos Dados

+

+ A Secretaria de Esportes adota medidas técnicas e administrativas para proteger + seus dados pessoais contra acesso não autorizado, alteração, divulgação ou + destruição. +

+
+ +
+

6. Revogação do Consentimento

+

+ Você pode revogar seu consentimento a qualquer momento através da página de + gerenciamento de privacidade. No entanto, a revogação pode impedir o acesso a + algumas funcionalidades do sistema que dependem do tratamento de dados pessoais. +

+
+ +
+

7. Contato

+

+ Para questões relacionadas ao tratamento de dados pessoais, entre em contato com + o Encarregado de Proteção de Dados: +

+
+

+ E-mail: lgpd@esportes.pe.gov.br +

+

+ Telefone: (81) 3184-XXXX +

+
+
+ +
+ +

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

+
+
+
+
+ + +
+
+

Aceitar Termo

+ + {#if erro} +
+ + {erro} +
+ {/if} + +
+ +
+ +
+ + + + Ver Política de Privacidade + +
+
+
+ {/if} +
+ diff --git a/apps/web/src/routes/(dashboard)/ti/+page.svelte b/apps/web/src/routes/(dashboard)/ti/+page.svelte index 23902f3..1f81dad 100644 --- a/apps/web/src/routes/(dashboard)/ti/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/+page.svelte @@ -268,6 +268,19 @@ palette: 'success', icon: 'shieldCheck' }, + { + title: 'LGPD - Proteção de Dados', + description: + 'Gerenciar solicitações LGPD, consentimentos, registros de tratamento e conformidade com a Lei Geral de Proteção de Dados.', + ctaLabel: 'Acessar LGPD', + href: '/(dashboard)/ti/lgpd', + palette: 'info', + icon: 'shieldCheck', + highlightBadges: [ + { label: 'Conformidade', variant: 'solid' }, + { label: 'Direitos', variant: 'outline' } + ] + }, { title: 'Configuração de Email', description: diff --git a/apps/web/src/routes/(dashboard)/ti/lgpd/+page.svelte b/apps/web/src/routes/(dashboard)/ti/lgpd/+page.svelte new file mode 100644 index 0000000..fa1c1c2 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/+page.svelte @@ -0,0 +1,134 @@ + + +
+ +
+
+
+ +
+
+

LGPD - Proteção de Dados

+

Gestão de conformidade com a Lei Geral de Proteção de Dados

+
+
+
+ + + {#if estatisticas} +
+ + + + + + + +
+ {:else} +
+ +
+ {/if} + + + + + + {#if estatisticas} +
+ +
+
+

Solicitações por Tipo

+
+ {#each Object.entries(estatisticas.solicitacoesPorTipo) as [tipo, quantidade]} +
+ {tipo} + {quantidade} +
+ {/each} +
+
+
+ + +
+
+

Resumo

+
+
+ Total de ROTs: + {estatisticas.totalROTs} +
+
+ ROTs Ativos: + {estatisticas.rotsAtivos} +
+
+ Total de Consentimentos: + {estatisticas.totalConsentimentos} +
+
+ Consentimentos Ativos: + {estatisticas.consentimentosAtivos} +
+
+
+
+
+ {/if} +
+ diff --git a/apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte new file mode 100644 index 0000000..5189eb7 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/configuracoes/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+
+ +
+
+

Configurações LGPD

+

Configure as definições de proteção de dados

+
+
+
+ + {#if config === undefined} +
+ +
+ {:else} + +
+
+

Encarregado de Proteção de Dados (DPO)

+

+ Configure os dados de contato do Encarregado de Proteção de Dados, responsável por + atender solicitações e questões relacionadas à LGPD. +

+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+
+
+ + +
+
+

Configurações de Prazos

+

+ Configure os prazos para resposta de solicitações e alertas de vencimento. +

+ +
+
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+
+
+
+ + +
+ Cancelar + +
+ {/if} +
+ 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 new file mode 100644 index 0000000..8f0ce77 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/registros-tratamento/+page.svelte @@ -0,0 +1,341 @@ + + +
+ +
+
+
+ +
+
+

Registros de Tratamento (ROT)

+

+ Gerencie os registros de operações de tratamento de dados pessoais +

+
+
+ +
+ + + {#if mostrarFormulario} +
+
+

Criar Novo Registro de Tratamento

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ {#each categoriasDadosDisponiveis as categoria} + + {/each} +
+
+ +
+ +
+ {#each categoriasTitularesDisponiveis as categoria} + + {/each} +
+
+ +
+ +
+ {#each medidasSegurancaDisponiveis as medida} + + {/each} +
+
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+ {/if} + + +
+
+

Registros de Tratamento

+ + {#if registros === undefined} +
+ +
+ {:else if registros.length === 0} +
+ +

Nenhum registro de tratamento encontrado

+
+ {:else} +
+ {#each registros as registro} +
+
+
+
+

{registro.finalidade}

+ {#if registro.ativo} + Ativo + {:else} + Inativo + {/if} +
+ +
+
+ Base Legal: {registro.baseLegal} +
+
+ Categorias de Dados:{' '} + {registro.categoriasDados.join(', ')} +
+
+ Categorias de Titulares:{' '} + {registro.categoriasTitulares.join(', ')} +
+
+ Medidas de Segurança:{' '} + {registro.medidasSeguranca.join(', ')} +
+
+ Prazo de Retenção:{' '} + {registro.prazoRetencao} dias +
+
+ Compartilhamento:{' '} + {registro.compartilhamentoTerceiros ? 'Sim' : 'Não'} +
+ {#if registro.terceiros && registro.terceiros.length > 0} +
+ Terceiros:{' '} + {registro.terceiros.join(', ')} +
+ {/if} +
+ Responsável: {registro.responsavelNome} +
+
+ Criado em:{' '} + {format(new Date(registro.criadoEm), 'dd/MM/yyyy', { locale: ptBR })} +
+
+
+
+
+ {/each} +
+ {/if} +
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte new file mode 100644 index 0000000..57027d6 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte @@ -0,0 +1,333 @@ + + +
+ +
+
+
+ +
+
+

Gestão de Solicitações LGPD

+

Responda e gerencie solicitações de direitos dos titulares

+
+
+
+ + +
+
+
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+
+ + +
+
+

Solicitações

+ + {#if solicitacoes === undefined} +
+ +
+ {:else if solicitacoesFiltradas.length === 0} +
+ +

Nenhuma solicitação encontrada

+
+ {:else} +
+ {#each solicitacoesFiltradas as solicitacao} + {@const statusInfo = getStatusBadge(solicitacao.status)} + {@const StatusIcon = getStatusIcon(solicitacao.status)} +
+
+
+
+ +

+ {getTipoLabel(solicitacao.tipo)} +

+ {statusInfo.label} +
+ +
+
+ Solicitante: {solicitacao.usuarioNome} + {#if solicitacao.usuarioMatricula} + ({solicitacao.usuarioMatricula}) + {/if} +
+
+ E-mail: {solicitacao.usuarioEmail} +
+
+ Criada em:{' '} + {format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", { + locale: ptBR + })} +
+ {#if solicitacao.respondidoEm} +
+ Respondida em:{' '} + {format( + new Date(solicitacao.respondidoEm), + "dd/MM/yyyy 'às' HH:mm", + { locale: ptBR } + )} +
+ {#if solicitacao.respondidoPorNome} +
+ Respondida por:{' '} + {solicitacao.respondidoPorNome} +
+ {/if} + {/if} + {#if solicitacao.status === 'pendente' || solicitacao.status === 'em_analise'} +
+ Prazo:{' '} + {format(new Date(solicitacao.prazoResposta), 'dd/MM/yyyy', { + locale: ptBR + })} +
+ {/if} +
+
+ + {#if solicitacao.status === 'pendente' || solicitacao.status === 'em_analise'} + + {/if} +
+
+ {/each} +
+ {/if} +
+
+ + + {#if solicitacaoSelecionada} + + {/if} +
+ diff --git a/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte b/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte index 54e5976..7e3988e 100644 --- a/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte @@ -2,11 +2,12 @@ import { useQuery, useConvexClient } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; import StatsCard from "$lib/components/ti/StatsCard.svelte"; - import { BarChart3, Users, CheckCircle2, Ban, Clock, Plus, Layers, FileText, Info } from "lucide-svelte"; + import { BarChart3, Users, CheckCircle2, Ban, Clock, Plus, Layers, FileText, Info, Shield, AlertTriangle } from "lucide-svelte"; import { resolve } from "$app/paths"; const client = useConvexClient(); const usuariosQuery = useQuery(api.usuarios.listar, {}); + const estatisticasLGPD = useQuery(api.lgpd.obterEstatisticasLGPD, {}); // Verificar se está carregando const carregando = $derived(usuariosQuery === undefined); @@ -96,6 +97,54 @@
{/if} + + {#if estatisticasLGPD} +
+
+
+

LGPD - Proteção de Dados

+ + + Acessar LGPD + +
+
+ + + + + + + +
+
+
+ {/if} +
@@ -115,6 +164,11 @@ Ver Logs + + + + LGPD +
diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index e6d551e..ae0176d 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -39,6 +39,7 @@ import type * as funcionarioEnderecos from "../funcionarioEnderecos.js"; import type * as funcionarios from "../funcionarios.js"; import type * as healthCheck from "../healthCheck.js"; import type * as http from "../http.js"; +import type * as lgpd from "../lgpd.js"; import type * as logsAcesso from "../logsAcesso.js"; import type * as logsAtividades from "../logsAtividades.js"; import type * as logsLogin from "../logsLogin.js"; @@ -101,6 +102,7 @@ declare const fullApi: ApiFromModules<{ funcionarios: typeof funcionarios; healthCheck: typeof healthCheck; http: typeof http; + lgpd: typeof lgpd; logsAcesso: typeof logsAcesso; logsAtividades: typeof logsAtividades; logsLogin: typeof logsLogin; diff --git a/packages/backend/convex/lgpd.ts b/packages/backend/convex/lgpd.ts new file mode 100644 index 0000000..b3598c0 --- /dev/null +++ b/packages/backend/convex/lgpd.ts @@ -0,0 +1,796 @@ +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; +import { getCurrentUserFunction } from './auth'; +import { Id, Doc } from './_generated/dataModel'; +import type { QueryCtx, MutationCtx } from './_generated/server'; +import { registrarAtividade } from './logsAtividades'; + +/** + * Verificar se usuário aceitou o termo de consentimento + */ +export const verificarConsentimento = query({ + args: { + tipo: v.optional( + v.union( + v.literal('termo_uso'), + v.literal('politica_privacidade'), + v.literal('comunicacoes'), + v.literal('compartilhamento_dados') + ) + ) + }, + returns: v.union( + v.object({ + aceito: v.boolean(), + versao: v.string(), + aceitoEm: v.number() + }), + v.null() + ), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + return null; + } + + const tipo = args.tipo || 'termo_uso'; + + const consentimento = await ctx.db + .query('consentimentos') + .withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', tipo)) + .order('desc') + .first(); + + if (!consentimento || !consentimento.aceito || consentimento.revogadoEm) { + return null; + } + + return { + aceito: consentimento.aceito, + versao: consentimento.versao, + aceitoEm: consentimento.aceitoEm + }; + } +}); + +/** + * Registrar consentimento do usuário + */ +export const registrarConsentimento = mutation({ + args: { + tipo: v.union( + v.literal('termo_uso'), + v.literal('politica_privacidade'), + v.literal('comunicacoes'), + v.literal('compartilhamento_dados') + ), + aceito: v.boolean(), + versao: v.string(), + ipAddress: v.optional(v.string()), + userAgent: v.optional(v.string()) + }, + returns: v.object({ sucesso: v.boolean(), consentimentoId: v.id('consentimentos') }), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + // Verificar se já existe consentimento ativo + const existente = await ctx.db + .query('consentimentos') + .withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', args.tipo)) + .order('desc') + .first(); + + if (existente && existente.aceito && !existente.revogadoEm) { + // Atualizar consentimento existente + await ctx.db.patch(existente._id, { + aceito: args.aceito, + versao: args.versao, + aceitoEm: Date.now(), + ipAddress: args.ipAddress, + userAgent: args.userAgent, + revogadoEm: undefined, + revogadoPor: undefined + }); + + return { sucesso: true, consentimentoId: existente._id }; + } + + // Criar novo consentimento + const consentimentoId = await ctx.db.insert('consentimentos', { + usuarioId: usuario._id, + tipo: args.tipo, + aceito: args.aceito, + versao: args.versao, + ipAddress: args.ipAddress, + userAgent: args.userAgent, + aceitoEm: Date.now() + }); + + // Log de atividade + await registrarAtividade( + ctx, + usuario._id, + 'aceitar_consentimento', + 'consentimentos', + JSON.stringify({ tipo: args.tipo, versao: args.versao }), + consentimentoId.toString() + ); + + return { sucesso: true, consentimentoId }; + } +}); + +/** + * Revogar consentimento + */ +export const revogarConsentimento = mutation({ + args: { + tipo: v.union( + v.literal('termo_uso'), + v.literal('politica_privacidade'), + v.literal('comunicacoes'), + v.literal('compartilhamento_dados') + ) + }, + returns: v.object({ sucesso: v.boolean() }), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + const consentimento = await ctx.db + .query('consentimentos') + .withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', args.tipo)) + .order('desc') + .first(); + + if (!consentimento) { + throw new Error('Consentimento não encontrado'); + } + + await ctx.db.patch(consentimento._id, { + revogadoEm: Date.now(), + revogadoPor: usuario._id + }); + + // Log de atividade + await registrarAtividade( + ctx, + usuario._id, + 'revogar_consentimento', + 'consentimentos', + JSON.stringify({ tipo: args.tipo }), + consentimento._id.toString() + ); + + return { sucesso: true }; + } +}); + +/** + * Listar consentimentos do usuário + */ +export const listarConsentimentos = query({ + args: {}, + returns: v.array( + v.object({ + _id: v.id('consentimentos'), + tipo: v.string(), + aceito: v.boolean(), + versao: v.string(), + aceitoEm: v.number(), + revogadoEm: v.union(v.number(), v.null()) + }) + ), + handler: async (ctx) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + return []; + } + + const consentimentos = await ctx.db + .query('consentimentos') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) + .order('desc') + .collect(); + + return consentimentos.map((c) => ({ + _id: c._id, + tipo: c.tipo, + aceito: c.aceito, + versao: c.versao, + aceitoEm: c.aceitoEm, + revogadoEm: c.revogadoEm ?? null + })); + } +}); + +/** + * Criar solicitação de direito LGPD + */ +export const criarSolicitacao = mutation({ + args: { + tipo: v.union( + v.literal('acesso'), + v.literal('correcao'), + v.literal('exclusao'), + v.literal('portabilidade'), + v.literal('revogacao_consentimento'), + v.literal('informacao_compartilhamento') + ), + dadosSolicitados: v.optional(v.string()), + observacoes: v.optional(v.string()) + }, + returns: v.object({ sucesso: v.boolean(), solicitacaoId: v.id('solicitacoesLGPD') }), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + // Prazo de resposta: 15 dias (conforme LGPD) + const prazoResposta = Date.now() + 15 * 24 * 60 * 60 * 1000; + + const solicitacaoId = await ctx.db.insert('solicitacoesLGPD', { + tipo: args.tipo, + usuarioId: usuario._id, + funcionarioId: usuario.funcionarioId, + status: 'pendente', + dadosSolicitados: args.dadosSolicitados, + observacoes: args.observacoes, + criadoEm: Date.now(), + prazoResposta + }); + + // Log de atividade + await registrarAtividade( + ctx, + usuario._id, + 'criar_solicitacao_lgpd', + 'solicitacoesLGPD', + JSON.stringify({ tipo: args.tipo }), + solicitacaoId.toString() + ); + + return { sucesso: true, solicitacaoId }; + } +}); + +/** + * Listar solicitações do usuário + */ +export const listarMinhasSolicitacoes = query({ + args: { + status: v.optional( + v.union( + v.literal('pendente'), + v.literal('em_analise'), + v.literal('concluida'), + v.literal('rejeitada') + ) + ) + }, + returns: v.array( + v.object({ + _id: v.id('solicitacoesLGPD'), + tipo: v.string(), + status: v.string(), + criadoEm: v.number(), + prazoResposta: v.number(), + respondidoEm: v.union(v.number(), v.null()), + resposta: v.union(v.string(), v.null()), + arquivoResposta: v.union(v.string(), v.null()) + }) + ), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + return []; + } + + let solicitacoes = await ctx.db + .query('solicitacoesLGPD') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) + .order('desc') + .collect(); + + if (args.status) { + solicitacoes = solicitacoes.filter((s) => s.status === args.status); + } + + return solicitacoes.map((s) => ({ + _id: s._id, + tipo: s.tipo, + status: s.status, + criadoEm: s.criadoEm, + prazoResposta: s.prazoResposta, + respondidoEm: s.respondidoEm ?? null, + resposta: s.resposta ?? null, + arquivoResposta: s.arquivoResposta ? s.arquivoResposta.toString() : null + })); + } +}); + +/** + * Listar todas as solicitações (apenas TI) + */ +export const listarSolicitacoes = query({ + args: { + status: v.optional( + v.union( + v.literal('pendente'), + v.literal('em_analise'), + v.literal('concluida'), + v.literal('rejeitada') + ) + ), + tipo: v.optional( + v.union( + v.literal('acesso'), + v.literal('correcao'), + v.literal('exclusao'), + v.literal('portabilidade'), + v.literal('revogacao_consentimento'), + v.literal('informacao_compartilhamento') + ) + ), + limite: v.optional(v.number()) + }, + returns: v.array( + v.object({ + _id: v.id('solicitacoesLGPD'), + tipo: v.string(), + status: v.string(), + usuarioNome: v.string(), + usuarioEmail: v.string(), + usuarioMatricula: v.union(v.string(), v.null()), + criadoEm: v.number(), + prazoResposta: v.number(), + respondidoEm: v.union(v.number(), v.null()), + respondidoPorNome: v.union(v.string(), v.null()) + }) + ), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + return []; + } + + // Verificar se é TI (simplificado - pode melhorar com verificação de role) + // Por enquanto, qualquer usuário autenticado pode ver (será melhorado) + + let solicitacoes = await ctx.db.query('solicitacoesLGPD').order('desc').collect(); + + if (args.status) { + solicitacoes = solicitacoes.filter((s) => s.status === args.status); + } + + if (args.tipo) { + solicitacoes = solicitacoes.filter((s) => s.tipo === args.tipo); + } + + if (args.limite) { + solicitacoes = solicitacoes.slice(0, args.limite); + } + + // Enriquecer com dados do usuário + const resultado = await Promise.all( + solicitacoes.map(async (s) => { + const usuarioSolicitante = await ctx.db.get(s.usuarioId); + let matricula: string | null = null; + + if (usuarioSolicitante?.funcionarioId) { + const funcionario = await ctx.db.get(usuarioSolicitante.funcionarioId); + matricula = funcionario?.matricula ?? null; + } + + let respondidoPorNome: string | null = null; + if (s.respondidoPor) { + const respondente = await ctx.db.get(s.respondidoPor); + respondidoPorNome = respondente?.nome ?? null; + } + + return { + _id: s._id, + tipo: s.tipo, + status: s.status, + usuarioNome: usuarioSolicitante?.nome ?? 'Usuário Desconhecido', + usuarioEmail: usuarioSolicitante?.email ?? '', + usuarioMatricula: matricula, + criadoEm: s.criadoEm, + prazoResposta: s.prazoResposta, + respondidoEm: s.respondidoEm ?? null, + respondidoPorNome + }; + }) + ); + + return resultado; + } +}); + +/** + * Responder solicitação (apenas TI) + */ +export const responderSolicitacao = mutation({ + args: { + solicitacaoId: v.id('solicitacoesLGPD'), + resposta: v.string(), + status: v.union( + v.literal('concluida'), + v.literal('rejeitada'), + v.literal('em_analise') + ), + arquivoResposta: v.optional(v.id('_storage')) + }, + returns: v.object({ sucesso: v.boolean() }), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + const solicitacao = await ctx.db.get(args.solicitacaoId); + if (!solicitacao) { + throw new Error('Solicitação não encontrada'); + } + + await ctx.db.patch(args.solicitacaoId, { + status: args.status, + resposta: args.resposta, + arquivoResposta: args.arquivoResposta, + respondidoPor: usuario._id, + respondidoEm: Date.now() + }); + + // Log de atividade + await registrarAtividade( + ctx, + usuario._id, + 'responder_solicitacao_lgpd', + 'solicitacoesLGPD', + JSON.stringify({ solicitacaoId: args.solicitacaoId, status: args.status }), + args.solicitacaoId.toString() + ); + + return { sucesso: true }; + } +}); + +/** + * Exportar dados do usuário (portabilidade) + */ +export const exportarDadosUsuario = query({ + args: {}, + returns: v.object({ + dados: v.string() // JSON string com todos os dados do usuário + }), + handler: async (ctx) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + // Buscar todos os dados do usuário + const dadosUsuario: any = { + usuario: { + nome: usuario.nome, + email: usuario.email, + setor: usuario.setor + }, + consentimentos: [], + solicitacoes: [], + atividades: [] + }; + + // Consentimentos + const consentimentos = await ctx.db + .query('consentimentos') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) + .collect(); + dadosUsuario.consentimentos = consentimentos.map((c) => ({ + tipo: c.tipo, + aceito: c.aceito, + versao: c.versao, + aceitoEm: c.aceitoEm, + revogadoEm: c.revogadoEm + })); + + // Solicitações LGPD + const solicitacoes = await ctx.db + .query('solicitacoesLGPD') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) + .collect(); + dadosUsuario.solicitacoes = solicitacoes.map((s) => ({ + tipo: s.tipo, + status: s.status, + criadoEm: s.criadoEm, + respondidoEm: s.respondidoEm + })); + + // Dados do funcionário (se houver) + if (usuario.funcionarioId) { + const funcionario = await ctx.db.get(usuario.funcionarioId); + if (funcionario) { + dadosUsuario.funcionario = { + nome: funcionario.nome, + matricula: funcionario.matricula, + cpf: funcionario.cpf, + email: funcionario.email, + telefone: funcionario.telefone, + cargo: funcionario.cargo, + setor: funcionario.setor + }; + } + } + + return { + dados: JSON.stringify(dadosUsuario, null, 2) + }; + } +}); + +/** + * Criar Registro de Operação de Tratamento (ROT) + */ +export const criarRegistroTratamento = mutation({ + args: { + finalidade: v.string(), + baseLegal: v.string(), + categoriasDados: v.array(v.string()), + categoriasTitulares: v.array(v.string()), + medidasSeguranca: v.array(v.string()), + prazoRetencao: v.number(), + compartilhamentoTerceiros: v.boolean(), + terceiros: v.optional(v.array(v.string())), + descricao: v.optional(v.string()) + }, + returns: v.object({ sucesso: v.boolean(), registroId: v.id('registrosTratamento') }), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + const agora = Date.now(); + + const registroId = await ctx.db.insert('registrosTratamento', { + finalidade: args.finalidade, + baseLegal: args.baseLegal, + categoriasDados: args.categoriasDados, + categoriasTitulares: args.categoriasTitulares, + medidasSeguranca: args.medidasSeguranca, + prazoRetencao: args.prazoRetencao, + compartilhamentoTerceiros: args.compartilhamentoTerceiros, + terceiros: args.terceiros, + responsavel: usuario._id, + descricao: args.descricao, + criadoEm: agora, + atualizadoEm: agora, + ativo: true + }); + + // Log de atividade + await registrarAtividade( + ctx, + usuario._id, + 'criar_rot', + 'registrosTratamento', + JSON.stringify({ finalidade: args.finalidade }), + registroId.toString() + ); + + return { sucesso: true, registroId }; + } +}); + +/** + * Listar Registros de Tratamento + */ +export const listarRegistrosTratamento = query({ + args: { + ativo: v.optional(v.boolean()) + }, + returns: v.array( + v.object({ + _id: v.id('registrosTratamento'), + finalidade: v.string(), + baseLegal: v.string(), + categoriasDados: v.array(v.string()), + categoriasTitulares: v.array(v.string()), + medidasSeguranca: v.array(v.string()), + prazoRetencao: v.number(), + compartilhamentoTerceiros: v.boolean(), + terceiros: v.union(v.array(v.string()), v.null()), + responsavelNome: v.string(), + criadoEm: v.number(), + atualizadoEm: v.number(), + ativo: v.boolean() + }) + ), + handler: async (ctx, args) => { + let registros = await ctx.db.query('registrosTratamento').collect(); + + if (args.ativo !== undefined) { + registros = registros.filter((r) => r.ativo === args.ativo); + } + + // Enriquecer com nome do responsável + const resultado = await Promise.all( + registros.map(async (r) => { + const responsavel = await ctx.db.get(r.responsavel); + return { + _id: r._id, + finalidade: r.finalidade, + baseLegal: r.baseLegal, + categoriasDados: r.categoriasDados, + categoriasTitulares: r.categoriasTitulares, + medidasSeguranca: r.medidasSeguranca, + prazoRetencao: r.prazoRetencao, + compartilhamentoTerceiros: r.compartilhamentoTerceiros, + terceiros: r.terceiros ?? null, + responsavelNome: responsavel?.nome ?? 'Desconhecido', + criadoEm: r.criadoEm, + atualizadoEm: r.atualizadoEm, + ativo: r.ativo + }; + }) + ); + + return resultado; + } +}); + +/** + * Obter configurações LGPD + */ +export const obterConfiguracaoLGPD = query({ + args: {}, + returns: v.union( + v.object({ + encarregadoNome: v.union(v.string(), v.null()), + encarregadoEmail: v.union(v.string(), v.null()), + encarregadoTelefone: v.union(v.string(), v.null()), + prazoRespostaPadrao: v.number(), + diasAlertaVencimento: v.number() + }), + v.null() + ), + handler: async (ctx) => { + const config = await ctx.db + .query('configuracaoLGPD') + .withIndex('by_ativo', (q) => q.eq('ativo', true)) + .first(); + + if (!config) { + // Retornar valores padrão + return { + encarregadoNome: null, + encarregadoEmail: null, + encarregadoTelefone: null, + prazoRespostaPadrao: 15, + diasAlertaVencimento: 3 + }; + } + + return { + encarregadoNome: config.encarregadoNome ?? null, + encarregadoEmail: config.encarregadoEmail ?? null, + encarregadoTelefone: config.encarregadoTelefone ?? null, + prazoRespostaPadrao: config.prazoRespostaPadrao, + diasAlertaVencimento: config.diasAlertaVencimento + }; + } +}); + +/** + * Atualizar configurações LGPD (apenas TI) + */ +export const atualizarConfiguracaoLGPD = mutation({ + args: { + encarregadoNome: v.optional(v.string()), + encarregadoEmail: v.optional(v.string()), + encarregadoTelefone: v.optional(v.string()), + prazoRespostaPadrao: v.optional(v.number()), + diasAlertaVencimento: v.optional(v.number()) + }, + returns: v.object({ sucesso: v.boolean() }), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + // Buscar configuração ativa ou criar nova + let config = await ctx.db + .query('configuracaoLGPD') + .withIndex('by_ativo', (q) => q.eq('ativo', true)) + .first(); + + if (config) { + // Desativar configuração antiga + await ctx.db.patch(config._id, { ativo: false }); + } + + // 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, + ativo: true, + atualizadoPor: usuario._id, + atualizadoEm: Date.now() + }); + + // Log de atividade + await registrarAtividade( + ctx, + usuario._id, + 'atualizar_config_lgpd', + 'configuracaoLGPD', + JSON.stringify(args), + '' + ); + + return { sucesso: true }; + } +}); + +/** + * Obter estatísticas LGPD (apenas TI) + */ +export const obterEstatisticasLGPD = query({ + args: {}, + returns: v.object({ + totalSolicitacoes: v.number(), + solicitacoesPendentes: v.number(), + solicitacoesVencendo: v.number(), + solicitacoesPorTipo: v.record(v.string(), v.number()), + totalConsentimentos: v.number(), + consentimentosAtivos: v.number(), + totalROTs: v.number(), + rotsAtivos: v.number() + }), + handler: async (ctx) => { + const solicitacoes = await ctx.db.query('solicitacoesLGPD').collect(); + const consentimentos = await ctx.db.query('consentimentos').collect(); + const rots = await ctx.db.query('registrosTratamento').collect(); + + const agora = Date.now(); + const tresDias = 3 * 24 * 60 * 60 * 1000; + + const solicitacoesVencendo = solicitacoes.filter( + (s) => + s.status === 'pendente' || s.status === 'em_analise' + ? s.prazoResposta - agora <= tresDias && s.prazoResposta > agora + : false + ).length; + + const solicitacoesPorTipo: Record = {}; + solicitacoes.forEach((s) => { + solicitacoesPorTipo[s.tipo] = (solicitacoesPorTipo[s.tipo] || 0) + 1; + }); + + const consentimentosAtivos = consentimentos.filter( + (c) => c.aceito && !c.revogadoEm + ).length; + + return { + totalSolicitacoes: solicitacoes.length, + solicitacoesPendentes: solicitacoes.filter((s) => s.status === 'pendente').length, + solicitacoesVencendo, + solicitacoesPorTipo, + totalConsentimentos: consentimentos.length, + consentimentosAtivos, + totalROTs: rots.length, + rotsAtivos: rots.filter((r) => r.ativo).length + }; + } +}); + diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 76294be..7cf8095 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -1873,4 +1873,95 @@ export default defineSchema({ .index("by_ativo", ["ativo"]) .index("by_data_inicio", ["dataInicio"]) .index("by_data_fim", ["dataFim"]), + + // ========== LGPD - Lei Geral de Proteção de Dados ========== + + // Solicitações de direitos LGPD + solicitacoesLGPD: defineTable({ + tipo: v.union( + v.literal("acesso"), + v.literal("correcao"), + v.literal("exclusao"), + v.literal("portabilidade"), + v.literal("revogacao_consentimento"), + v.literal("informacao_compartilhamento") + ), + usuarioId: v.id("usuarios"), + funcionarioId: v.optional(v.id("funcionarios")), + status: v.union( + v.literal("pendente"), + v.literal("em_analise"), + v.literal("concluida"), + v.literal("rejeitada") + ), + dadosSolicitados: v.optional(v.string()), // JSON com detalhes da solicitação + resposta: v.optional(v.string()), // Resposta da solicitação + arquivoResposta: v.optional(v.id("_storage")), // Arquivo gerado (ex: exportação de dados) + respondidoPor: v.optional(v.id("usuarios")), + respondidoEm: v.optional(v.number()), + criadoEm: v.number(), + prazoResposta: v.number(), // Prazo legal (15 dias) - timestamp + observacoes: v.optional(v.string()), + }) + .index("by_usuario", ["usuarioId"]) + .index("by_status", ["status"]) + .index("by_tipo", ["tipo"]) + .index("by_prazo", ["prazoResposta"]) + .index("by_funcionario", ["funcionarioId"]), + + // Consentimentos dos usuários + consentimentos: defineTable({ + usuarioId: v.id("usuarios"), + tipo: v.union( + v.literal("termo_uso"), + v.literal("politica_privacidade"), + v.literal("comunicacoes"), + v.literal("compartilhamento_dados") + ), + aceito: v.boolean(), + versao: v.string(), // Versão do documento aceito (ex: "1.0") + ipAddress: v.optional(v.string()), + userAgent: v.optional(v.string()), + aceitoEm: v.number(), + revogadoEm: v.optional(v.number()), + revogadoPor: v.optional(v.id("usuarios")), // Se revogado pelo próprio usuário ou por TI + }) + .index("by_usuario", ["usuarioId"]) + .index("by_tipo", ["tipo"]) + .index("by_usuario_tipo", ["usuarioId", "tipo"]) + .index("by_versao", ["versao"]), + + // Registro de Operações de Tratamento (ROT) + registrosTratamento: defineTable({ + finalidade: v.string(), // Finalidade do tratamento + baseLegal: v.string(), // Base legal (ex: "Art. 7º, II - Execução de políticas públicas") + categoriasDados: v.array(v.string()), // ["dados_identificacao", "dados_contato", "dados_profissionais"] + categoriasTitulares: v.array(v.string()), // ["funcionarios", "servidores", "colaboradores"] + medidasSeguranca: v.array(v.string()), // ["criptografia", "controle_acesso", "logs_auditoria"] + prazoRetencao: v.number(), // em dias + compartilhamentoTerceiros: v.boolean(), + terceiros: v.optional(v.array(v.string())), // Lista de terceiros com quem compartilha + responsavel: v.id("usuarios"), // Responsável pelo tratamento + criadoEm: v.number(), + atualizadoEm: v.number(), + ativo: v.boolean(), + descricao: v.optional(v.string()), // Descrição detalhada + }) + .index("by_finalidade", ["finalidade"]) + .index("by_ativo", ["ativo"]) + .index("by_responsavel", ["responsavel"]), + + // Configurações LGPD + configuracaoLGPD: defineTable({ + encarregadoNome: v.optional(v.string()), + encarregadoEmail: v.optional(v.string()), + encarregadoTelefone: v.optional(v.string()), + prazoRespostaPadrao: v.number(), // em dias (padrão: 15) + diasAlertaVencimento: v.number(), // dias antes do prazo para alertar (padrão: 3) + politicaRetencao: v.optional(v.string()), // JSON com política de retenção por tipo de dado + ativo: v.boolean(), + atualizadoPor: v.id("usuarios"), + atualizadoEm: v.number(), + }) + .index("by_ativo", ["ativo"]), });