feat: implement LGPD compliance features including data request management, consent tracking, and statistics display in the dashboard for enhanced data protection compliance
This commit is contained in:
@@ -452,7 +452,7 @@
|
|||||||
class="link link-hover hover:text-primary transition-colors">Suporte</a
|
class="link link-hover hover:text-primary transition-colors">Suporte</a
|
||||||
>
|
>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
|
<a href={resolve('/privacidade')} class="link link-hover hover:text-primary transition-colors"
|
||||||
>Privacidade</a
|
>Privacidade</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
194
apps/web/src/routes/(dashboard)/perfil/privacidade/+page.svelte
Normal file
194
apps/web/src/routes/(dashboard)/perfil/privacidade/+page.svelte
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { useQuery, useMutation } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import { Shield, CheckCircle, XCircle, Calendar, AlertCircle } from 'lucide-svelte';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ptBR } from 'date-fns/locale';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const consentimentos = useQuery(api.lgpd.listarConsentimentos, {});
|
||||||
|
const revogarConsentimento = useMutation(api.lgpd.revogarConsentimento);
|
||||||
|
|
||||||
|
let revogando = $state<string | null>(null);
|
||||||
|
|
||||||
|
function getTipoLabel(tipo: string) {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
termo_uso: 'Termo de Uso',
|
||||||
|
politica_privacidade: 'Política de Privacidade',
|
||||||
|
comunicacoes: 'Comunicações',
|
||||||
|
compartilhamento_dados: 'Compartilhamento de Dados'
|
||||||
|
};
|
||||||
|
return labels[tipo] || tipo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revogar(tipo: string) {
|
||||||
|
if (!confirm('Tem certeza que deseja revogar este consentimento?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
revogando = tipo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await revogarConsentimento({ tipo: tipo as any });
|
||||||
|
toast.success('Consentimento revogado com sucesso');
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Erro ao revogar consentimento');
|
||||||
|
} finally {
|
||||||
|
revogando = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">Preferências de Privacidade</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">
|
||||||
|
Gerencie seus consentimentos e preferências de privacidade
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Consentimentos -->
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Meus Consentimentos</h2>
|
||||||
|
|
||||||
|
{#if consentimentos === undefined}
|
||||||
|
<div class="flex justify-center items-center py-10">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if consentimentos.length === 0}
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<Shield class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
|
||||||
|
<p class="text-base-content/60">Nenhum consentimento registrado</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each consentimentos as consentimento}
|
||||||
|
<div class="border border-base-300 rounded-lg p-4">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
{#if consentimento.aceito && !consentimento.revogadoEm}
|
||||||
|
<CheckCircle class="h-5 w-5 text-success" />
|
||||||
|
{:else}
|
||||||
|
<XCircle class="h-5 w-5 text-error" />
|
||||||
|
{/if}
|
||||||
|
<h3 class="font-semibold text-lg">
|
||||||
|
{getTipoLabel(consentimento.tipo)}
|
||||||
|
</h3>
|
||||||
|
{#if consentimento.aceito && !consentimento.revogadoEm}
|
||||||
|
<span class="badge badge-success">Ativo</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-error">Revogado</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1 text-sm text-base-content/70">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Calendar class="h-4 w-4" />
|
||||||
|
<span>
|
||||||
|
Aceito em:{' '}
|
||||||
|
{format(new Date(consentimento.aceitoEm), 'dd/MM/yyyy às HH:mm', {
|
||||||
|
locale: ptBR
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Versão:</span> {consentimento.versao}
|
||||||
|
</div>
|
||||||
|
{#if consentimento.revogadoEm}
|
||||||
|
<div class="flex items-center gap-2 text-error">
|
||||||
|
<XCircle class="h-4 w-4" />
|
||||||
|
<span>
|
||||||
|
Revogado em:{' '}
|
||||||
|
{format(
|
||||||
|
new Date(consentimento.revogadoEm),
|
||||||
|
'dd/MM/yyyy às HH:mm',
|
||||||
|
{ locale: ptBR }
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if consentimento.aceito && !consentimento.revogadoEm}
|
||||||
|
<button
|
||||||
|
onclick={() => revogar(consentimento.tipo)}
|
||||||
|
disabled={revogando === consentimento.tipo}
|
||||||
|
class="btn btn-sm btn-error"
|
||||||
|
>
|
||||||
|
{#if revogando === consentimento.tipo}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
Revogar
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informações -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Informações Importantes</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-warning mb-4">
|
||||||
|
<AlertCircle class="h-5 w-5" />
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold">Atenção ao Revogar Consentimentos</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
A revogação de alguns consentimentos pode impedir o acesso a funcionalidades do
|
||||||
|
sistema que dependem do tratamento de dados pessoais.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-sm text-base-content/80">
|
||||||
|
<p>
|
||||||
|
<strong>Termo de Uso:</strong> Aceite obrigatório para utilização do sistema. A
|
||||||
|
revogação pode impedir o acesso.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Política de Privacidade:</strong> Informa como seus dados são tratados. A
|
||||||
|
revogação não impede o tratamento, mas você pode solicitar exclusão.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Comunicações:</strong> Permite envio de notificações e comunicações do
|
||||||
|
sistema. A revogação pode limitar informações importantes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Compartilhamento de Dados:</strong> Permite compartilhamento com terceiros
|
||||||
|
quando necessário. A revogação pode afetar serviços terceirizados.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 mt-6">
|
||||||
|
<a href={resolve('/privacidade')} class="btn btn-outline btn-lg flex-1">
|
||||||
|
<Shield class="h-5 w-5" />
|
||||||
|
Ver Política de Privacidade
|
||||||
|
</a>
|
||||||
|
<a href={resolve('/privacidade/meus-dados')} class="btn btn-primary btn-lg flex-1">
|
||||||
|
<Shield class="h-5 w-5" />
|
||||||
|
Solicitar Meus Direitos
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
419
apps/web/src/routes/(dashboard)/privacidade/+page.svelte
Normal file
419
apps/web/src/routes/(dashboard)/privacidade/+page.svelte
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { Shield, FileText, Mail, Phone, Calendar } from 'lucide-svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">Política de Privacidade</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">
|
||||||
|
Lei Geral de Proteção de Dados Pessoais (LGPD) - Lei nº 13.709/2018
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-base-content/60">
|
||||||
|
<Calendar class="h-4 w-4" />
|
||||||
|
<span>Última atualização: {new Date().toLocaleDateString('pt-BR')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conteúdo -->
|
||||||
|
<div class="prose prose-lg max-w-none">
|
||||||
|
<!-- Introdução -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">1. Introdução</h2>
|
||||||
|
<p class="text-base-content/80">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p class="text-base-content/80 mt-4">
|
||||||
|
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).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Dados Coletados -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">2. Dados Pessoais Coletados</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
O SGSE coleta e processa os seguintes tipos de dados pessoais:
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<FileText class="h-5 w-5 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Dados de Identificação</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Nome completo, CPF, RG, data de nascimento, naturalidade, nacionalidade,
|
||||||
|
estado civil, filiação (nome do pai e mãe)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<FileText class="h-5 w-5 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Dados de Contato</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
E-mail, telefone, endereço residencial e endereços de marcação de ponto
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<FileText class="h-5 w-5 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Dados Profissionais</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Matrícula, cargo, função, setor, data de admissão, regime de trabalho,
|
||||||
|
documentos profissionais (CTPS, título eleitor, reservista, PIS)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<FileText class="h-5 w-5 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Dados de Saúde</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Atestados médicos, licenças de saúde, grupo sanguíneo, fator RH (quando
|
||||||
|
necessário para atividades específicas)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<FileText class="h-5 w-5 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Dados de Acesso</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Credenciais de acesso, logs de acesso, endereço IP, histórico de atividades
|
||||||
|
no sistema
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Finalidade -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">3. Finalidade do Tratamento</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
Os dados pessoais são tratados para as seguintes finalidades:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside space-y-2 text-base-content/80">
|
||||||
|
<li>Gestão de recursos humanos e folha de pagamento</li>
|
||||||
|
<li>Controle de ponto e registro de jornada de trabalho</li>
|
||||||
|
<li>Gestão de férias, ausências e licenças</li>
|
||||||
|
<li>Processamento de atestados médicos e licenças de saúde</li>
|
||||||
|
<li>Gestão de chamados e suporte técnico</li>
|
||||||
|
<li>Comunicação interna e notificações</li>
|
||||||
|
<li>Cumprimento de obrigações legais e regulatórias</li>
|
||||||
|
<li>Execução de políticas públicas</li>
|
||||||
|
<li>Segurança e prevenção de fraudes</li>
|
||||||
|
<li>Auditoria e controle interno</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Base Legal -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">4. Base Legal do Tratamento</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
O tratamento de dados pessoais no SGSE fundamenta-se nas seguintes bases legais,
|
||||||
|
conforme previsto no Art. 7º da LGPD:
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="bg-primary/5 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-primary mb-2">I. Execução de Políticas Públicas</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Art. 7º, II - Para a execução de políticas públicas previstas em leis ou
|
||||||
|
regulamentos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-primary/5 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-primary mb-2">II. Cumprimento de Obrigação Legal</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Art. 7º, I - Para cumprimento de obrigação legal ou regulatória pelo controlador
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-primary/5 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-primary mb-2">III. Execução de Contrato</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Art. 7º, V - Para a execução de contrato ou de procedimentos preliminares
|
||||||
|
relacionados a contrato do qual seja parte o titular
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-primary/5 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-primary mb-2">IV. Proteção da Vida e Saúde</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
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
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Compartilhamento -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">5. Compartilhamento de Dados</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
Os dados pessoais podem ser compartilhados com:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside space-y-2 text-base-content/80">
|
||||||
|
<li>
|
||||||
|
<strong>Órgãos Públicos:</strong> Quando necessário para cumprimento de obrigações
|
||||||
|
legais ou execução de políticas públicas
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Fornecedores de Serviços:</strong> Empresas contratadas para prestação de
|
||||||
|
serviços técnicos, sempre com garantias de proteção de dados
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Autoridades Competentes:</strong> Quando exigido por determinação judicial ou
|
||||||
|
legal
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="alert alert-info mt-4">
|
||||||
|
<p class="text-sm">
|
||||||
|
Todos os compartilhamentos são realizados com base legal e com garantias de
|
||||||
|
proteção dos dados pessoais.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Segurança -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">6. Medidas de Segurança</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
Adotamos medidas técnicas e administrativas para proteger seus dados pessoais:
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold mb-2">Medidas Técnicas</h3>
|
||||||
|
<ul class="text-sm text-base-content/70 space-y-1">
|
||||||
|
<li>• Criptografia de dados sensíveis</li>
|
||||||
|
<li>• Controle de acesso por permissões</li>
|
||||||
|
<li>• Logs de auditoria</li>
|
||||||
|
<li>• Backup regular</li>
|
||||||
|
<li>• Monitoramento de segurança</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold mb-2">Medidas Administrativas</h3>
|
||||||
|
<ul class="text-sm text-base-content/70 space-y-1">
|
||||||
|
<li>• Treinamento de equipe</li>
|
||||||
|
<li>• Políticas de acesso</li>
|
||||||
|
<li>• Procedimentos de segurança</li>
|
||||||
|
<li>• Gestão de incidentes</li>
|
||||||
|
<li>• Revisão periódica</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Retenção -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">7. Prazo de Retenção</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
Os dados pessoais são mantidos pelo prazo necessário para:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside space-y-2 text-base-content/80">
|
||||||
|
<li>
|
||||||
|
<strong>Dados de Funcionários Ativos:</strong> Durante todo o período de vínculo
|
||||||
|
empregatício/estatutário
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Dados de Funcionários Inativos:</strong> Conforme prazo legal aplicável (em
|
||||||
|
geral, 5 anos após desligamento)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Logs de Acesso:</strong> 2 anos, conforme recomendação da ANPD
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Documentos Trabalhistas:</strong> Conforme legislação trabalhista aplicável
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-base-content/80 mt-4">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Direitos do Titular -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">8. Direitos do Titular dos Dados</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
Conforme previsto no Art. 18 da LGPD, você possui os seguintes direitos:
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">1</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Confirmação da Existência de Tratamento</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Confirmar se tratamos seus dados pessoais
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">2</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Acesso aos Dados</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Acessar seus dados pessoais tratados por nós
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">3</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Correção de Dados</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Solicitar correção de dados incompletos, inexatos ou desatualizados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">4</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Anonimização, Bloqueio ou Eliminação</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Solicitar anonimização, bloqueio ou eliminação de dados desnecessários ou
|
||||||
|
excessivos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">5</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Portabilidade dos Dados</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Solicitar a portabilidade dos dados a outro fornecedor de serviço ou produto
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">6</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Eliminação de Dados</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Solicitar a eliminação dos dados pessoais tratados com base em consentimento
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">7</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Informação sobre Compartilhamento</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Obter informações sobre compartilhamento de dados com terceiros
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="badge badge-primary badge-lg">8</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">Revogação de Consentimento</h3>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Revogar seu consentimento, quando aplicável
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<a
|
||||||
|
href={resolve('/privacidade/meus-dados')}
|
||||||
|
class="btn btn-primary btn-lg w-full md:w-auto"
|
||||||
|
>
|
||||||
|
<FileText class="h-5 w-5" />
|
||||||
|
Solicitar Meus Direitos
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Encarregado -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">9. Encarregado de Proteção de Dados (DPO)</h2>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
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:
|
||||||
|
</p>
|
||||||
|
<div class="bg-primary/5 p-6 rounded-lg space-y-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Mail class="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold">E-mail</p>
|
||||||
|
<p class="text-base-content/70">lgpd@esportes.pe.gov.br</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Phone class="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold">Telefone</p>
|
||||||
|
<p class="text-base-content/70">(81) 3184-XXXX</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<FileText class="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-info mt-4">
|
||||||
|
<p class="text-sm">
|
||||||
|
As solicitações serão respondidas em até 15 (quinze) dias, conforme previsto na
|
||||||
|
LGPD.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Alterações -->
|
||||||
|
<section class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">10. Alterações nesta Política</h2>
|
||||||
|
<p class="text-base-content/80">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ações -->
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 mt-8">
|
||||||
|
<a href={resolve('/privacidade/meus-dados')} class="btn btn-primary btn-lg flex-1">
|
||||||
|
<FileText class="h-5 w-5" />
|
||||||
|
Solicitar Meus Direitos LGPD
|
||||||
|
</a>
|
||||||
|
<a href={resolve('/termo-consentimento')} class="btn btn-outline btn-lg flex-1">
|
||||||
|
<Shield class="h-5 w-5" />
|
||||||
|
Ver Termo de Consentimento
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { useQuery, useMutation } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
FileText,
|
||||||
|
Download,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Send
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ptBR } from 'date-fns/locale';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
type TipoSolicitacao =
|
||||||
|
| 'acesso'
|
||||||
|
| 'correcao'
|
||||||
|
| 'exclusao'
|
||||||
|
| 'portabilidade'
|
||||||
|
| 'revogacao_consentimento'
|
||||||
|
| 'informacao_compartilhamento';
|
||||||
|
|
||||||
|
const tipoSelecionado = $state<TipoSolicitacao | null>(null);
|
||||||
|
const dadosSolicitados = $state('');
|
||||||
|
const observacoes = $state('');
|
||||||
|
const carregando = $state(false);
|
||||||
|
|
||||||
|
const minhasSolicitacoes = useQuery(api.lgpd.listarMinhasSolicitacoes, {});
|
||||||
|
const criarSolicitacao = useMutation(api.lgpd.criarSolicitacao);
|
||||||
|
const exportarDados = useQuery(api.lgpd.exportarDadosUsuario, {});
|
||||||
|
|
||||||
|
const tiposSolicitacao: Array<{ valor: TipoSolicitacao; label: string; descricao: string }> = [
|
||||||
|
{
|
||||||
|
valor: 'acesso',
|
||||||
|
label: 'Acesso aos Dados',
|
||||||
|
descricao: 'Solicitar acesso aos meus dados pessoais tratados pelo sistema'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valor: 'correcao',
|
||||||
|
label: 'Correção de Dados',
|
||||||
|
descricao: 'Solicitar correção de dados incompletos, inexatos ou desatualizados'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valor: 'exclusao',
|
||||||
|
label: 'Exclusão de Dados',
|
||||||
|
descricao: 'Solicitar exclusão de dados desnecessários ou excessivos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valor: 'portabilidade',
|
||||||
|
label: 'Portabilidade dos Dados',
|
||||||
|
descricao: 'Solicitar exportação dos meus dados em formato portável'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valor: 'revogacao_consentimento',
|
||||||
|
label: 'Revogar Consentimento',
|
||||||
|
descricao: 'Revogar consentimento para tratamento de dados pessoais'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valor: 'informacao_compartilhamento',
|
||||||
|
label: 'Informação sobre Compartilhamento',
|
||||||
|
descricao: 'Obter informações sobre compartilhamento de dados com terceiros'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function getStatusBadge(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'pendente':
|
||||||
|
return { label: 'Pendente', class: 'badge-warning' };
|
||||||
|
case 'em_analise':
|
||||||
|
return { label: 'Em Análise', class: 'badge-info' };
|
||||||
|
case 'concluida':
|
||||||
|
return { label: 'Concluída', class: 'badge-success' };
|
||||||
|
case 'rejeitada':
|
||||||
|
return { label: 'Rejeitada', class: 'badge-error' };
|
||||||
|
default:
|
||||||
|
return { label: status, class: 'badge-neutral' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusIcon(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'pendente':
|
||||||
|
return Clock;
|
||||||
|
case 'em_analise':
|
||||||
|
return AlertCircle;
|
||||||
|
case 'concluida':
|
||||||
|
return CheckCircle;
|
||||||
|
case 'rejeitada':
|
||||||
|
return XCircle;
|
||||||
|
default:
|
||||||
|
return FileText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enviarSolicitacao() {
|
||||||
|
if (!tipoSelecionado) {
|
||||||
|
toast.error('Selecione o tipo de solicitação');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
carregando = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await criarSolicitacao({
|
||||||
|
tipo: tipoSelecionado,
|
||||||
|
dadosSolicitados: dadosSolicitados || undefined,
|
||||||
|
observacoes: observacoes || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('Solicitação criada com sucesso!');
|
||||||
|
tipoSelecionado = null;
|
||||||
|
dadosSolicitados = '';
|
||||||
|
observacoes = '';
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Erro ao criar solicitação');
|
||||||
|
} finally {
|
||||||
|
carregando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadDados() {
|
||||||
|
if (!exportarDados?.data?.dados) {
|
||||||
|
toast.error('Dados não disponíveis');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([exportarDados.data.dados], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `meus-dados-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
toast.success('Download iniciado!');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-6xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">Meus Direitos LGPD</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">
|
||||||
|
Solicite o exercício dos seus direitos conforme a Lei Geral de Proteção de Dados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Formulário de Solicitação -->
|
||||||
|
<div class="lg:col-span-2 space-y-6">
|
||||||
|
<!-- Nova Solicitação -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Nova Solicitação</h2>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Tipo de Solicitação *</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
bind:value={tipoSelecionado}
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
>
|
||||||
|
<option value={null}>Selecione o tipo de solicitação</option>
|
||||||
|
{#each tiposSolicitacao as tipo}
|
||||||
|
<option value={tipo.valor}>{tipo.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if tipoSelecionado}
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-base-content/60">
|
||||||
|
{tiposSolicitacao.find((t) => t.valor === tipoSelecionado)?.descricao}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if tipoSelecionado === 'correcao'}
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Dados a Corrigir</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
bind:value={dadosSolicitados}
|
||||||
|
class="textarea textarea-bordered"
|
||||||
|
placeholder="Descreva quais dados precisam ser corrigidos e os valores corretos..."
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Observações</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
bind:value={observacoes}
|
||||||
|
class="textarea textarea-bordered"
|
||||||
|
placeholder="Informações adicionais sobre sua solicitação (opcional)..."
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={enviarSolicitacao}
|
||||||
|
disabled={!tipoSelecionado || carregando}
|
||||||
|
class="btn btn-primary btn-lg w-full"
|
||||||
|
>
|
||||||
|
{#if carregando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Enviando...
|
||||||
|
{:else}
|
||||||
|
<Send class="h-5 w-5" />
|
||||||
|
Enviar Solicitação
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="alert alert-info mt-4">
|
||||||
|
<AlertCircle class="h-5 w-5" />
|
||||||
|
<p class="text-sm">
|
||||||
|
Sua solicitação será analisada e respondida em até 15 dias úteis, conforme
|
||||||
|
previsto na LGPD.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Minhas Solicitações -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Minhas Solicitações</h2>
|
||||||
|
|
||||||
|
{#if minhasSolicitacoes === undefined}
|
||||||
|
<div class="flex justify-center items-center py-10">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if minhasSolicitacoes.length === 0}
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<FileText class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
|
||||||
|
<p class="text-base-content/60">Nenhuma solicitação encontrada</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each minhasSolicitacoes as solicitacao}
|
||||||
|
{@const statusInfo = getStatusBadge(solicitacao.status)}
|
||||||
|
{@const StatusIcon = getStatusIcon(solicitacao.status)}
|
||||||
|
<div class="border border-base-300 rounded-lg p-4">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<StatusIcon class="h-5 w-5 text-base-content/60" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{tiposSolicitacao.find((t) => t.valor === solicitacao.tipo)
|
||||||
|
?.label || solicitacao.tipo}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
Criada em{' '}
|
||||||
|
{format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||||
|
locale: ptBR
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge {statusInfo.class}">{statusInfo.label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if solicitacao.resposta}
|
||||||
|
<div class="mt-3 p-3 bg-base-200 rounded-lg">
|
||||||
|
<p class="text-sm font-semibold mb-1">Resposta:</p>
|
||||||
|
<p class="text-sm text-base-content/80">{solicitacao.resposta}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if solicitacao.arquivoResposta}
|
||||||
|
<div class="mt-3">
|
||||||
|
<a
|
||||||
|
href={solicitacao.arquivoResposta}
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-sm btn-outline"
|
||||||
|
>
|
||||||
|
<Download class="h-4 w-4" />
|
||||||
|
Baixar Arquivo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if solicitacao.status === 'pendente' || solicitacao.status === 'em_analise'}
|
||||||
|
<div class="mt-3 text-xs text-base-content/60">
|
||||||
|
Prazo para resposta:{' '}
|
||||||
|
{format(new Date(solicitacao.prazoResposta), 'dd/MM/yyyy', {
|
||||||
|
locale: ptBR
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Exportar Dados -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg mb-4">Exportar Meus Dados</h3>
|
||||||
|
<p class="text-sm text-base-content/70 mb-4">
|
||||||
|
Baixe uma cópia completa dos seus dados pessoais em formato JSON.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onclick={downloadDados}
|
||||||
|
disabled={exportarDados === undefined || !exportarDados?.data?.dados}
|
||||||
|
class="btn btn-outline btn-primary w-full"
|
||||||
|
>
|
||||||
|
{#if exportarDados === undefined}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Carregando...
|
||||||
|
{:else}
|
||||||
|
<Download class="h-5 w-5" />
|
||||||
|
Baixar Dados
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informações -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg mb-4">Seus Direitos</h3>
|
||||||
|
<ul class="space-y-2 text-sm text-base-content/80">
|
||||||
|
<li>• Confirmar existência de tratamento</li>
|
||||||
|
<li>• Acessar seus dados</li>
|
||||||
|
<li>• Corrigir dados incorretos</li>
|
||||||
|
<li>• Solicitar exclusão</li>
|
||||||
|
<li>• Portabilidade dos dados</li>
|
||||||
|
<li>• Revogar consentimento</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg mb-4">Links Úteis</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<a href={resolve('/privacidade')} class="btn btn-sm btn-ghost w-full justify-start">
|
||||||
|
<FileText class="h-4 w-4" />
|
||||||
|
Política de Privacidade
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={resolve('/perfil/privacidade')}
|
||||||
|
class="btn btn-sm btn-ghost w-full justify-start"
|
||||||
|
>
|
||||||
|
<Shield class="h-4 w-4" />
|
||||||
|
Preferências de Privacidade
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
276
apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte
Normal file
276
apps/web/src/routes/(dashboard)/termo-consentimento/+page.svelte
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { Shield, CheckCircle, AlertCircle, FileText } from 'lucide-svelte';
|
||||||
|
import { useQuery, useMutation } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
|
||||||
|
let aceito = $state(false);
|
||||||
|
let carregando = $state(false);
|
||||||
|
let erro = $state<string | null>(null);
|
||||||
|
let sucesso = $state(false);
|
||||||
|
|
||||||
|
// Verificar se já aceitou o termo
|
||||||
|
const consentimentoQuery = useQuery(api.lgpd.verificarConsentimento, { tipo: 'termo_uso' });
|
||||||
|
const registrarConsentimento = useMutation(api.lgpd.registrarConsentimento);
|
||||||
|
|
||||||
|
const jaAceitou = $derived(
|
||||||
|
consentimentoQuery?.data?.aceito === true && consentimentoQuery?.data?.versao === '1.0'
|
||||||
|
);
|
||||||
|
|
||||||
|
async function aceitarTermo() {
|
||||||
|
if (!aceito) {
|
||||||
|
erro = 'Você precisa aceitar o termo para continuar';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
carregando = true;
|
||||||
|
erro = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await registrarConsentimento({
|
||||||
|
tipo: 'termo_uso',
|
||||||
|
aceito: true,
|
||||||
|
versao: '1.0'
|
||||||
|
});
|
||||||
|
sucesso = true;
|
||||||
|
} catch (e: any) {
|
||||||
|
erro = e.message || 'Erro ao registrar consentimento';
|
||||||
|
} finally {
|
||||||
|
carregando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">Termo de Consentimento</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">
|
||||||
|
Termo de Uso e Consentimento para Tratamento de Dados Pessoais
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if consentimentoQuery === undefined}
|
||||||
|
<div class="flex justify-center items-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if jaAceitou}
|
||||||
|
<!-- Já aceitou -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<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>
|
||||||
|
<p class="text-base-content/80 mb-6">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href={resolve('/privacidade')} class="btn btn-primary">
|
||||||
|
<FileText class="h-5 w-5" />
|
||||||
|
Ver Política de Privacidade
|
||||||
|
</a>
|
||||||
|
<a href={resolve('/perfil/privacidade')} class="btn btn-outline">
|
||||||
|
<Shield class="h-5 w-5" />
|
||||||
|
Gerenciar Privacidade
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if sucesso}
|
||||||
|
<!-- Sucesso -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<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 Aceito com Sucesso!</h2>
|
||||||
|
<p class="text-base-content/80 mb-6">
|
||||||
|
Seu consentimento foi registrado. Você pode acessar o sistema normalmente.
|
||||||
|
</p>
|
||||||
|
<a href={resolve('/')} class="btn btn-primary btn-lg">
|
||||||
|
Continuar para o Sistema
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Termo -->
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="prose max-w-none">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">Termo de Uso e Consentimento</h2>
|
||||||
|
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">1. Aceitação dos Termos</h3>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">2. Tratamento de Dados Pessoais</h3>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
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:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside space-y-2 text-base-content/80 mb-4">
|
||||||
|
<li>Gestão de recursos humanos e folha de pagamento</li>
|
||||||
|
<li>Controle de ponto e registro de jornada de trabalho</li>
|
||||||
|
<li>Gestão de férias, ausências e licenças</li>
|
||||||
|
<li>Processamento de atestados médicos e licenças de saúde</li>
|
||||||
|
<li>Comunicação interna e notificações</li>
|
||||||
|
<li>Cumprimento de obrigações legais e regulatórias</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">3. Base Legal</h3>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
O tratamento de seus dados pessoais fundamenta-se nas seguintes bases legais,
|
||||||
|
conforme previsto na Lei Geral de Proteção de Dados (LGPD):
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside space-y-2 text-base-content/80">
|
||||||
|
<li>
|
||||||
|
<strong>Execução de Políticas Públicas:</strong> Para a execução de políticas
|
||||||
|
públicas previstas em leis ou regulamentos
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cumprimento de Obrigação Legal:</strong> Para cumprimento de
|
||||||
|
obrigação legal ou regulatória
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Execução de Contrato:</strong> Para a execução de contrato ou de
|
||||||
|
procedimentos preliminares relacionados a contrato
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">4. Direitos do Titular</h3>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
Você possui os seguintes direitos em relação aos seus dados pessoais:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside space-y-2 text-base-content/80">
|
||||||
|
<li>Confirmar a existência de tratamento de dados</li>
|
||||||
|
<li>Acessar seus dados pessoais</li>
|
||||||
|
<li>Corrigir dados incompletos, inexatos ou desatualizados</li>
|
||||||
|
<li>Solicitar anonimização, bloqueio ou eliminação de dados</li>
|
||||||
|
<li>Solicitar portabilidade dos dados</li>
|
||||||
|
<li>Revogar seu consentimento</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-base-content/80 mt-4">
|
||||||
|
Para exercer seus direitos, acesse a página{' '}
|
||||||
|
<a href={resolve('/privacidade/meus-dados')} class="link link-primary">
|
||||||
|
Solicitar Meus Direitos
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">5. Segurança dos Dados</h3>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">6. Revogação do Consentimento</h3>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">7. Contato</h3>
|
||||||
|
<p class="text-base-content/80 mb-4">
|
||||||
|
Para questões relacionadas ao tratamento de dados pessoais, entre em contato com
|
||||||
|
o Encarregado de Proteção de Dados:
|
||||||
|
</p>
|
||||||
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
|
<p class="text-sm">
|
||||||
|
<strong>E-mail:</strong> lgpd@esportes.pe.gov.br
|
||||||
|
</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
<strong>Telefone:</strong> (81) 3184-XXXX
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="alert alert-warning mb-6">
|
||||||
|
<AlertCircle class="h-5 w-5" />
|
||||||
|
<p class="text-sm">
|
||||||
|
<strong>Atenção:</strong> 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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulário de Aceite -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-xl mb-4">Aceitar Termo</h2>
|
||||||
|
|
||||||
|
{#if erro}
|
||||||
|
<div class="alert alert-error mb-4">
|
||||||
|
<AlertCircle class="h-5 w-5" />
|
||||||
|
<span>{erro}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="form-control mb-6">
|
||||||
|
<label class="label cursor-pointer justify-start gap-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={aceito}
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
/>
|
||||||
|
<span class="label-text text-base-content/80">
|
||||||
|
Declaro que li, compreendi e aceito os termos e condições estabelecidos neste
|
||||||
|
Termo de Consentimento e na{' '}
|
||||||
|
<a href={resolve('/privacidade')} class="link link-primary" target="_blank">
|
||||||
|
Política de Privacidade
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
|
<button
|
||||||
|
onclick={aceitarTermo}
|
||||||
|
disabled={!aceito || carregando}
|
||||||
|
class="btn btn-primary btn-lg flex-1"
|
||||||
|
>
|
||||||
|
{#if carregando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Processando...
|
||||||
|
{:else}
|
||||||
|
<CheckCircle class="h-5 w-5" />
|
||||||
|
Aceitar e Continuar
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<a href={resolve('/privacidade')} class="btn btn-outline btn-lg flex-1">
|
||||||
|
<FileText class="h-5 w-5" />
|
||||||
|
Ver Política de Privacidade
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -268,6 +268,19 @@
|
|||||||
palette: 'success',
|
palette: 'success',
|
||||||
icon: 'shieldCheck'
|
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',
|
title: 'Configuração de Email',
|
||||||
description:
|
description:
|
||||||
|
|||||||
134
apps/web/src/routes/(dashboard)/ti/lgpd/+page.svelte
Normal file
134
apps/web/src/routes/(dashboard)/ti/lgpd/+page.svelte
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { useQuery } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import { Shield, FileText, AlertTriangle, CheckCircle, Settings, Users } from 'lucide-svelte';
|
||||||
|
import StatsCard from '$lib/components/ti/StatsCard.svelte';
|
||||||
|
|
||||||
|
const estatisticas = useQuery(api.lgpd.obterEstatisticasLGPD, {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">LGPD - Proteção de Dados</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">Gestão de conformidade com a Lei Geral de Proteção de Dados</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
{#if estatisticas}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<StatsCard
|
||||||
|
title="Solicitações Pendentes"
|
||||||
|
value={estatisticas.solicitacoesPendentes}
|
||||||
|
description="Aguardando resposta"
|
||||||
|
Icon={AlertTriangle}
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="Solicitações Vencendo"
|
||||||
|
value={estatisticas.solicitacoesVencendo}
|
||||||
|
description="Prazo próximo"
|
||||||
|
Icon={AlertTriangle}
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="Total de Solicitações"
|
||||||
|
value={estatisticas.totalSolicitacoes}
|
||||||
|
description="Todas as solicitações"
|
||||||
|
Icon={FileText}
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="Consentimentos Ativos"
|
||||||
|
value={estatisticas.consentimentosAtivos}
|
||||||
|
description="Consentimentos válidos"
|
||||||
|
Icon={CheckCircle}
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex justify-center items-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Ações Rápidas -->
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-8">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Ações Rápidas</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<a href={resolve('/ti/lgpd/solicitacoes')} class="btn btn-primary btn-lg">
|
||||||
|
<FileText class="h-5 w-5" strokeWidth={2} />
|
||||||
|
Gerenciar Solicitações
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href={resolve('/ti/lgpd/registros-tratamento')} class="btn btn-secondary btn-lg">
|
||||||
|
<FileText class="h-5 w-5" strokeWidth={2} />
|
||||||
|
Registros de Tratamento
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href={resolve('/ti/lgpd/configuracoes')} class="btn btn-accent btn-lg">
|
||||||
|
<Settings class="h-5 w-5" strokeWidth={2} />
|
||||||
|
Configurações LGPD
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informações -->
|
||||||
|
{#if estatisticas}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Solicitações por Tipo -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-xl mb-4">Solicitações por Tipo</h2>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each Object.entries(estatisticas.solicitacoesPorTipo) as [tipo, quantidade]}
|
||||||
|
<div class="flex justify-between items-center p-2 bg-base-200 rounded">
|
||||||
|
<span class="text-sm font-medium">{tipo}</span>
|
||||||
|
<span class="badge badge-primary">{quantidade}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resumo -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-xl mb-4">Resumo</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Total de ROTs:</span>
|
||||||
|
<span class="font-semibold">{estatisticas.totalROTs}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>ROTs Ativos:</span>
|
||||||
|
<span class="font-semibold text-success">{estatisticas.rotsAtivos}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Total de Consentimentos:</span>
|
||||||
|
<span class="font-semibold">{estatisticas.totalConsentimentos}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Consentimentos Ativos:</span>
|
||||||
|
<span class="font-semibold text-success">{estatisticas.consentimentosAtivos}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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 { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const config = useQuery(api.lgpd.obterConfiguracaoLGPD, {});
|
||||||
|
const atualizarConfig = useMutation(api.lgpd.atualizarConfiguracaoLGPD);
|
||||||
|
|
||||||
|
let encarregadoNome = $state('');
|
||||||
|
let encarregadoEmail = $state('');
|
||||||
|
let encarregadoTelefone = $state('');
|
||||||
|
let prazoRespostaPadrao = $state(15);
|
||||||
|
let diasAlertaVencimento = $state(3);
|
||||||
|
let carregando = $state(false);
|
||||||
|
|
||||||
|
// Sincronizar com query
|
||||||
|
$effect(() => {
|
||||||
|
if (config?.data) {
|
||||||
|
encarregadoNome = config.data.encarregadoNome || '';
|
||||||
|
encarregadoEmail = config.data.encarregadoEmail || '';
|
||||||
|
encarregadoTelefone = config.data.encarregadoTelefone || '';
|
||||||
|
prazoRespostaPadrao = config.data.prazoRespostaPadrao;
|
||||||
|
diasAlertaVencimento = config.data.diasAlertaVencimento;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function salvar() {
|
||||||
|
carregando = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await atualizarConfig({
|
||||||
|
encarregadoNome: encarregadoNome || undefined,
|
||||||
|
encarregadoEmail: encarregadoEmail || undefined,
|
||||||
|
encarregadoTelefone: encarregadoTelefone || undefined,
|
||||||
|
prazoRespostaPadrao,
|
||||||
|
diasAlertaVencimento
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('Configurações salvas com sucesso!');
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Erro ao salvar configurações');
|
||||||
|
} finally {
|
||||||
|
carregando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-6 max-w-4xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">Configurações LGPD</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">Configure as definições de proteção de dados</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if config === undefined}
|
||||||
|
<div class="flex justify-center items-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Encarregado de Dados -->
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Encarregado de Proteção de Dados (DPO)</h2>
|
||||||
|
<p class="text-base-content/60 mb-6">
|
||||||
|
Configure os dados de contato do Encarregado de Proteção de Dados, responsável por
|
||||||
|
atender solicitações e questões relacionadas à LGPD.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Nome do Encarregado</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<User 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={encarregadoNome}
|
||||||
|
placeholder="Nome completo do Encarregado"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">E-mail</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<Mail class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
bind:value={encarregadoEmail}
|
||||||
|
placeholder="lgpd@esportes.pe.gov.br"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Telefone</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<Phone 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={encarregadoTelefone}
|
||||||
|
placeholder="(81) 3184-XXXX"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configurações de Prazos -->
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Configurações de Prazos</h2>
|
||||||
|
<p class="text-base-content/60 mb-6">
|
||||||
|
Configure os prazos para resposta de solicitações e alertas de vencimento.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Prazo Padrão para Resposta (dias)</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="number"
|
||||||
|
bind:value={prazoRespostaPadrao}
|
||||||
|
min="1"
|
||||||
|
max="30"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-base-content/60">
|
||||||
|
Prazo legal conforme LGPD: 15 dias (recomendado)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Dias para Alerta de Vencimento</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="number"
|
||||||
|
bind:value={diasAlertaVencimento}
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-base-content/60">
|
||||||
|
Dias antes do prazo para enviar alerta (padrão: 3 dias)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botão Salvar -->
|
||||||
|
<div class="flex justify-end gap-4">
|
||||||
|
<a href={resolve('/ti/lgpd')} class="btn btn-ghost btn-lg">Cancelar</a>
|
||||||
|
<button onclick={salvar} disabled={carregando} class="btn btn-primary btn-lg">
|
||||||
|
{#if carregando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Salvando...
|
||||||
|
{:else}
|
||||||
|
<Save class="h-5 w-5" />
|
||||||
|
Salvar Configurações
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { useQuery, useMutation } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import { Shield, FileText, Plus, CheckCircle, XCircle } from 'lucide-svelte';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ptBR } from 'date-fns/locale';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const registros = useQuery(api.lgpd.listarRegistrosTratamento, { ativo: undefined });
|
||||||
|
|
||||||
|
let mostrarFormulario = $state(false);
|
||||||
|
let finalidade = $state('');
|
||||||
|
let baseLegal = $state('');
|
||||||
|
let categoriasDados = $state<string[]>([]);
|
||||||
|
let categoriasTitulares = $state<string[]>([]);
|
||||||
|
let medidasSeguranca = $state<string[]>([]);
|
||||||
|
let prazoRetencao = $state(365);
|
||||||
|
let compartilhamentoTerceiros = $state(false);
|
||||||
|
let terceiros = $state<string[]>([]);
|
||||||
|
let descricao = $state('');
|
||||||
|
let carregando = $state(false);
|
||||||
|
|
||||||
|
const criarROT = useMutation(api.lgpd.criarRegistroTratamento);
|
||||||
|
|
||||||
|
const categoriasDadosDisponiveis = [
|
||||||
|
'dados_identificacao',
|
||||||
|
'dados_contato',
|
||||||
|
'dados_profissionais',
|
||||||
|
'dados_saude',
|
||||||
|
'dados_acesso'
|
||||||
|
];
|
||||||
|
|
||||||
|
const categoriasTitularesDisponiveis = [
|
||||||
|
'funcionarios',
|
||||||
|
'servidores',
|
||||||
|
'colaboradores',
|
||||||
|
'terceiros'
|
||||||
|
];
|
||||||
|
|
||||||
|
const medidasSegurancaDisponiveis = [
|
||||||
|
'criptografia',
|
||||||
|
'controle_acesso',
|
||||||
|
'logs_auditoria',
|
||||||
|
'backup',
|
||||||
|
'monitoramento'
|
||||||
|
];
|
||||||
|
|
||||||
|
function toggleArrayItem(array: string[], item: string) {
|
||||||
|
if (array.includes(item)) {
|
||||||
|
return array.filter((i) => i !== item);
|
||||||
|
} else {
|
||||||
|
return [...array, item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function salvar() {
|
||||||
|
if (!finalidade.trim() || !baseLegal.trim()) {
|
||||||
|
toast.error('Preencha todos os campos obrigatórios');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
carregando = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await criarROT({
|
||||||
|
finalidade: finalidade.trim(),
|
||||||
|
baseLegal: baseLegal.trim(),
|
||||||
|
categoriasDados,
|
||||||
|
categoriasTitulares,
|
||||||
|
medidasSeguranca,
|
||||||
|
prazoRetencao,
|
||||||
|
compartilhamentoTerceiros,
|
||||||
|
terceiros: compartilhamentoTerceiros && terceiros.length > 0 ? terceiros : undefined,
|
||||||
|
descricao: descricao.trim() || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('Registro de Tratamento criado com sucesso!');
|
||||||
|
mostrarFormulario = false;
|
||||||
|
// Reset form
|
||||||
|
finalidade = '';
|
||||||
|
baseLegal = '';
|
||||||
|
categoriasDados = [];
|
||||||
|
categoriasTitulares = [];
|
||||||
|
medidasSeguranca = [];
|
||||||
|
prazoRetencao = 365;
|
||||||
|
compartilhamentoTerceiros = false;
|
||||||
|
terceiros = [];
|
||||||
|
descricao = '';
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Erro ao criar registro');
|
||||||
|
} finally {
|
||||||
|
carregando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">Registros de Tratamento (ROT)</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">
|
||||||
|
Gerencie os registros de operações de tratamento de dados pessoais
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick={() => (mostrarFormulario = !mostrarFormulario)} class="btn btn-primary">
|
||||||
|
<Plus class="h-5 w-5" />
|
||||||
|
Novo ROT
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulário -->
|
||||||
|
{#if mostrarFormulario}
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Criar Novo Registro de Tratamento</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Finalidade *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={finalidade}
|
||||||
|
placeholder="Ex: Gestão de recursos humanos e folha de pagamento"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Base Legal *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={baseLegal}
|
||||||
|
placeholder="Ex: Art. 7º, II - Execução de políticas públicas"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Categorias de Dados</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each categoriasDadosDisponiveis as categoria}
|
||||||
|
<label class="cursor-pointer label gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={categoriasDados.includes(categoria)}
|
||||||
|
onchange={() => (categoriasDados = toggleArrayItem(categoriasDados, categoria))}
|
||||||
|
class="checkbox checkbox-primary checkbox-sm"
|
||||||
|
/>
|
||||||
|
<span class="label-text text-sm">{categoria}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Categorias de Titulares</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each categoriasTitularesDisponiveis as categoria}
|
||||||
|
<label class="cursor-pointer label gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={categoriasTitulares.includes(categoria)}
|
||||||
|
onchange={() =>
|
||||||
|
(categoriasTitulares = toggleArrayItem(categoriasTitulares, categoria))}
|
||||||
|
class="checkbox checkbox-primary checkbox-sm"
|
||||||
|
/>
|
||||||
|
<span class="label-text text-sm">{categoria}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Medidas de Segurança</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each medidasSegurancaDisponiveis as medida}
|
||||||
|
<label class="cursor-pointer label gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={medidasSeguranca.includes(medida)}
|
||||||
|
onchange={() =>
|
||||||
|
(medidasSeguranca = toggleArrayItem(medidasSeguranca, medida))}
|
||||||
|
class="checkbox checkbox-primary checkbox-sm"
|
||||||
|
/>
|
||||||
|
<span class="label-text text-sm">{medida}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Prazo de Retenção (dias)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
bind:value={prazoRetencao}
|
||||||
|
min="1"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="cursor-pointer label justify-start gap-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={compartilhamentoTerceiros}
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
/>
|
||||||
|
<span class="label-text font-semibold">Compartilhamento com Terceiros</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Descrição</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
bind:value={descricao}
|
||||||
|
class="textarea textarea-bordered"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Descrição detalhada do tratamento..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-4">
|
||||||
|
<button
|
||||||
|
onclick={() => (mostrarFormulario = false)}
|
||||||
|
class="btn btn-ghost"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button onclick={salvar} disabled={carregando} class="btn btn-primary">
|
||||||
|
{#if carregando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Salvando...
|
||||||
|
{:else}
|
||||||
|
<Plus class="h-5 w-5" />
|
||||||
|
Criar ROT
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Lista de ROTs -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Registros de Tratamento</h2>
|
||||||
|
|
||||||
|
{#if registros === undefined}
|
||||||
|
<div class="flex justify-center items-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if registros.length === 0}
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<FileText class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
|
||||||
|
<p class="text-base-content/60">Nenhum registro de tratamento encontrado</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each registros as registro}
|
||||||
|
<div class="border border-base-300 rounded-lg p-4">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<h3 class="font-semibold text-lg">{registro.finalidade}</h3>
|
||||||
|
{#if registro.ativo}
|
||||||
|
<span class="badge badge-success">Ativo</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-error">Inativo</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 text-sm text-base-content/70">
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Base Legal:</span> {registro.baseLegal}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Categorias de Dados:</span>{' '}
|
||||||
|
{registro.categoriasDados.join(', ')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Categorias de Titulares:</span>{' '}
|
||||||
|
{registro.categoriasTitulares.join(', ')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Medidas de Segurança:</span>{' '}
|
||||||
|
{registro.medidasSeguranca.join(', ')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Prazo de Retenção:</span>{' '}
|
||||||
|
{registro.prazoRetencao} dias
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Compartilhamento:</span>{' '}
|
||||||
|
{registro.compartilhamentoTerceiros ? 'Sim' : 'Não'}
|
||||||
|
</div>
|
||||||
|
{#if registro.terceiros && registro.terceiros.length > 0}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Terceiros:</span>{' '}
|
||||||
|
{registro.terceiros.join(', ')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Responsável:</span> {registro.responsavelNome}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Criado em:</span>{' '}
|
||||||
|
{format(new Date(registro.criadoEm), 'dd/MM/yyyy', { locale: ptBR })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { useQuery, useMutation } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
FileText,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Search,
|
||||||
|
Filter
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ptBR } from 'date-fns/locale';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
type StatusFiltro = 'pendente' | 'em_analise' | 'concluida' | 'rejeitada' | null;
|
||||||
|
type TipoFiltro =
|
||||||
|
| 'acesso'
|
||||||
|
| 'correcao'
|
||||||
|
| 'exclusao'
|
||||||
|
| 'portabilidade'
|
||||||
|
| 'revogacao_consentimento'
|
||||||
|
| 'informacao_compartilhamento'
|
||||||
|
| null;
|
||||||
|
|
||||||
|
let statusFiltro = $state<StatusFiltro>(null);
|
||||||
|
let tipoFiltro = $state<TipoFiltro>(null);
|
||||||
|
let termoBusca = $state('');
|
||||||
|
|
||||||
|
const solicitacoes = useQuery(api.lgpd.listarSolicitacoes, {
|
||||||
|
status: statusFiltro || undefined,
|
||||||
|
tipo: tipoFiltro || undefined
|
||||||
|
});
|
||||||
|
const responderSolicitacao = useMutation(api.lgpd.responderSolicitacao);
|
||||||
|
|
||||||
|
let solicitacaoSelecionada = $state<string | null>(null);
|
||||||
|
let resposta = $state('');
|
||||||
|
let statusResposta = $state<'concluida' | 'rejeitada' | 'em_analise'>('concluida');
|
||||||
|
let carregando = $state(false);
|
||||||
|
|
||||||
|
function getStatusBadge(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'pendente':
|
||||||
|
return { label: 'Pendente', class: 'badge-warning' };
|
||||||
|
case 'em_analise':
|
||||||
|
return { label: 'Em Análise', class: 'badge-info' };
|
||||||
|
case 'concluida':
|
||||||
|
return { label: 'Concluída', class: 'badge-success' };
|
||||||
|
case 'rejeitada':
|
||||||
|
return { label: 'Rejeitada', class: 'badge-error' };
|
||||||
|
default:
|
||||||
|
return { label: status, class: 'badge-neutral' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTipoLabel(tipo: string) {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
acesso: 'Acesso aos Dados',
|
||||||
|
correcao: 'Correção de Dados',
|
||||||
|
exclusao: 'Exclusão de Dados',
|
||||||
|
portabilidade: 'Portabilidade dos Dados',
|
||||||
|
revogacao_consentimento: 'Revogar Consentimento',
|
||||||
|
informacao_compartilhamento: 'Informação sobre Compartilhamento'
|
||||||
|
};
|
||||||
|
return labels[tipo] || tipo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusIcon(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'pendente':
|
||||||
|
return Clock;
|
||||||
|
case 'em_analise':
|
||||||
|
return AlertCircle;
|
||||||
|
case 'concluida':
|
||||||
|
return CheckCircle;
|
||||||
|
case 'rejeitada':
|
||||||
|
return XCircle;
|
||||||
|
default:
|
||||||
|
return FileText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filtrarSolicitacoes() {
|
||||||
|
if (!solicitacoes) return [];
|
||||||
|
if (!termoBusca) return solicitacoes;
|
||||||
|
|
||||||
|
const busca = termoBusca.toLowerCase();
|
||||||
|
return solicitacoes.filter(
|
||||||
|
(s) =>
|
||||||
|
s.usuarioNome.toLowerCase().includes(busca) ||
|
||||||
|
s.usuarioEmail.toLowerCase().includes(busca) ||
|
||||||
|
(s.usuarioMatricula?.toLowerCase().includes(busca) ?? false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function responder() {
|
||||||
|
if (!solicitacaoSelecionada || !resposta.trim()) {
|
||||||
|
toast.error('Preencha a resposta');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
carregando = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await responderSolicitacao({
|
||||||
|
solicitacaoId: solicitacaoSelecionada as any,
|
||||||
|
resposta: resposta.trim(),
|
||||||
|
status: statusResposta
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('Solicitação respondida com sucesso!');
|
||||||
|
solicitacaoSelecionada = null;
|
||||||
|
resposta = '';
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Erro ao responder solicitação');
|
||||||
|
} finally {
|
||||||
|
carregando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const solicitacoesFiltradas = $derived(filtrarSolicitacoes());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-base-content">Gestão de Solicitações LGPD</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">Responda e gerencie solicitações de direitos dos titulares</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros -->
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Status</span>
|
||||||
|
</label>
|
||||||
|
<select bind:value={statusFiltro} class="select select-bordered">
|
||||||
|
<option value={null}>Todos</option>
|
||||||
|
<option value="pendente">Pendente</option>
|
||||||
|
<option value="em_analise">Em Análise</option>
|
||||||
|
<option value="concluida">Concluída</option>
|
||||||
|
<option value="rejeitada">Rejeitada</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Tipo</span>
|
||||||
|
</label>
|
||||||
|
<select bind:value={tipoFiltro} class="select select-bordered">
|
||||||
|
<option value={null}>Todos</option>
|
||||||
|
<option value="acesso">Acesso</option>
|
||||||
|
<option value="correcao">Correção</option>
|
||||||
|
<option value="exclusao">Exclusão</option>
|
||||||
|
<option value="portabilidade">Portabilidade</option>
|
||||||
|
<option value="revogacao_consentimento">Revogar Consentimento</option>
|
||||||
|
<option value="informacao_compartilhamento">Informação Compartilhamento</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control md:col-span-2">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Buscar</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<Search 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={termoBusca}
|
||||||
|
placeholder="Buscar por nome, email ou matrícula..."
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lista de Solicitações -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Solicitações</h2>
|
||||||
|
|
||||||
|
{#if solicitacoes === undefined}
|
||||||
|
<div class="flex justify-center items-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if solicitacoesFiltradas.length === 0}
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<FileText class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
|
||||||
|
<p class="text-base-content/60">Nenhuma solicitação encontrada</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each solicitacoesFiltradas as solicitacao}
|
||||||
|
{@const statusInfo = getStatusBadge(solicitacao.status)}
|
||||||
|
{@const StatusIcon = getStatusIcon(solicitacao.status)}
|
||||||
|
<div class="border border-base-300 rounded-lg p-4">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<StatusIcon class="h-5 w-5 text-base-content/60" />
|
||||||
|
<h3 class="font-semibold text-lg">
|
||||||
|
{getTipoLabel(solicitacao.tipo)}
|
||||||
|
</h3>
|
||||||
|
<span class="badge {statusInfo.class}">{statusInfo.label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1 text-sm text-base-content/70">
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Solicitante:</span> {solicitacao.usuarioNome}
|
||||||
|
{#if solicitacao.usuarioMatricula}
|
||||||
|
({solicitacao.usuarioMatricula})
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">E-mail:</span> {solicitacao.usuarioEmail}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Criada em:</span>{' '}
|
||||||
|
{format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||||
|
locale: ptBR
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{#if solicitacao.respondidoEm}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Respondida em:</span>{' '}
|
||||||
|
{format(
|
||||||
|
new Date(solicitacao.respondidoEm),
|
||||||
|
"dd/MM/yyyy 'às' HH:mm",
|
||||||
|
{ locale: ptBR }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{#if solicitacao.respondidoPorNome}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Respondida por:</span>{' '}
|
||||||
|
{solicitacao.respondidoPorNome}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if solicitacao.status === 'pendente' || solicitacao.status === 'em_analise'}
|
||||||
|
<div class="text-warning">
|
||||||
|
<span class="font-semibold">Prazo:</span>{' '}
|
||||||
|
{format(new Date(solicitacao.prazoResposta), 'dd/MM/yyyy', {
|
||||||
|
locale: ptBR
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if solicitacao.status === 'pendente' || solicitacao.status === 'em_analise'}
|
||||||
|
<button
|
||||||
|
onclick={() => (solicitacaoSelecionada = solicitacao._id)}
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
>
|
||||||
|
Responder
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Resposta -->
|
||||||
|
{#if solicitacaoSelecionada}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="font-bold text-lg mb-4">Responder Solicitação</h3>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Status</span>
|
||||||
|
</label>
|
||||||
|
<select bind:value={statusResposta} class="select select-bordered">
|
||||||
|
<option value="concluida">Concluída</option>
|
||||||
|
<option value="rejeitada">Rejeitada</option>
|
||||||
|
<option value="em_analise">Em Análise</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Resposta *</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
bind:value={resposta}
|
||||||
|
class="textarea textarea-bordered"
|
||||||
|
placeholder="Digite a resposta para o solicitante..."
|
||||||
|
rows="6"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
solicitacaoSelecionada = null;
|
||||||
|
resposta = '';
|
||||||
|
}}
|
||||||
|
class="btn btn-ghost"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button onclick={responder} disabled={!resposta.trim() || carregando} class="btn btn-primary">
|
||||||
|
{#if carregando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Enviando...
|
||||||
|
{:else}
|
||||||
|
Enviar Resposta
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop" onclick={() => (solicitacaoSelecionada = null)}></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -2,11 +2,12 @@
|
|||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from "convex-svelte";
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
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";
|
import { resolve } from "$app/paths";
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||||
|
const estatisticasLGPD = useQuery(api.lgpd.obterEstatisticasLGPD, {});
|
||||||
|
|
||||||
// Verificar se está carregando
|
// Verificar se está carregando
|
||||||
const carregando = $derived(usuariosQuery === undefined);
|
const carregando = $derived(usuariosQuery === undefined);
|
||||||
@@ -96,6 +97,54 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- LGPD Stats Cards -->
|
||||||
|
{#if estatisticasLGPD}
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-8">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="card-title text-2xl">LGPD - Proteção de Dados</h2>
|
||||||
|
<a href={resolve("/ti/lgpd")} class="btn btn-sm btn-primary">
|
||||||
|
<Shield class="h-4 w-4" />
|
||||||
|
Acessar LGPD
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<StatsCard
|
||||||
|
title="Solicitações Pendentes"
|
||||||
|
value={estatisticasLGPD.solicitacoesPendentes}
|
||||||
|
description="Aguardando resposta"
|
||||||
|
Icon={AlertTriangle}
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="Solicitações Vencendo"
|
||||||
|
value={estatisticasLGPD.solicitacoesVencendo}
|
||||||
|
description="Prazo próximo"
|
||||||
|
Icon={AlertTriangle}
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="Total de Solicitações"
|
||||||
|
value={estatisticasLGPD.totalSolicitacoes}
|
||||||
|
description="Todas as solicitações"
|
||||||
|
Icon={FileText}
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="Consentimentos Ativos"
|
||||||
|
value={estatisticasLGPD.consentimentosAtivos}
|
||||||
|
description="Consentimentos válidos"
|
||||||
|
Icon={CheckCircle2}
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Ações Rápidas -->
|
<!-- Ações Rápidas -->
|
||||||
<div class="card bg-base-100 shadow-xl mb-8">
|
<div class="card bg-base-100 shadow-xl mb-8">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -115,6 +164,11 @@
|
|||||||
<FileText class="h-5 w-5" strokeWidth={2} />
|
<FileText class="h-5 w-5" strokeWidth={2} />
|
||||||
Ver Logs
|
Ver Logs
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href={resolve("/ti/lgpd")} class="btn btn-info">
|
||||||
|
<Shield class="h-5 w-5" strokeWidth={2} />
|
||||||
|
LGPD
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -39,6 +39,7 @@ import type * as funcionarioEnderecos from "../funcionarioEnderecos.js";
|
|||||||
import type * as funcionarios from "../funcionarios.js";
|
import type * as funcionarios from "../funcionarios.js";
|
||||||
import type * as healthCheck from "../healthCheck.js";
|
import type * as healthCheck from "../healthCheck.js";
|
||||||
import type * as http from "../http.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 logsAcesso from "../logsAcesso.js";
|
||||||
import type * as logsAtividades from "../logsAtividades.js";
|
import type * as logsAtividades from "../logsAtividades.js";
|
||||||
import type * as logsLogin from "../logsLogin.js";
|
import type * as logsLogin from "../logsLogin.js";
|
||||||
@@ -101,6 +102,7 @@ declare const fullApi: ApiFromModules<{
|
|||||||
funcionarios: typeof funcionarios;
|
funcionarios: typeof funcionarios;
|
||||||
healthCheck: typeof healthCheck;
|
healthCheck: typeof healthCheck;
|
||||||
http: typeof http;
|
http: typeof http;
|
||||||
|
lgpd: typeof lgpd;
|
||||||
logsAcesso: typeof logsAcesso;
|
logsAcesso: typeof logsAcesso;
|
||||||
logsAtividades: typeof logsAtividades;
|
logsAtividades: typeof logsAtividades;
|
||||||
logsLogin: typeof logsLogin;
|
logsLogin: typeof logsLogin;
|
||||||
|
|||||||
796
packages/backend/convex/lgpd.ts
Normal file
796
packages/backend/convex/lgpd.ts
Normal file
@@ -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<string, number> = {};
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1873,4 +1873,95 @@ export default defineSchema({
|
|||||||
.index("by_ativo", ["ativo"])
|
.index("by_ativo", ["ativo"])
|
||||||
.index("by_data_inicio", ["dataInicio"])
|
.index("by_data_inicio", ["dataInicio"])
|
||||||
.index("by_data_fim", ["dataFim"]),
|
.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"]),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user