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:
@@ -451,10 +451,10 @@
|
||||
href={resolve('/abrir-chamado')}
|
||||
class="link link-hover hover:text-primary transition-colors">Suporte</a
|
||||
>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
|
||||
>Privacidade</a
|
||||
>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href={resolve('/privacidade')} class="link link-hover hover:text-primary transition-colors"
|
||||
>Privacidade</a
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3">
|
||||
<div class="avatar">
|
||||
|
||||
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',
|
||||
icon: 'shieldCheck'
|
||||
},
|
||||
{
|
||||
title: 'LGPD - Proteção de Dados',
|
||||
description:
|
||||
'Gerenciar solicitações LGPD, consentimentos, registros de tratamento e conformidade com a Lei Geral de Proteção de Dados.',
|
||||
ctaLabel: 'Acessar LGPD',
|
||||
href: '/(dashboard)/ti/lgpd',
|
||||
palette: 'info',
|
||||
icon: 'shieldCheck',
|
||||
highlightBadges: [
|
||||
{ label: 'Conformidade', variant: 'solid' },
|
||||
{ label: 'Direitos', variant: 'outline' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Configuração de Email',
|
||||
description:
|
||||
|
||||
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 { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
||||
import { BarChart3, Users, CheckCircle2, Ban, Clock, Plus, Layers, FileText, Info } from "lucide-svelte";
|
||||
import { BarChart3, Users, CheckCircle2, Ban, Clock, Plus, Layers, FileText, Info, Shield, AlertTriangle } from "lucide-svelte";
|
||||
|
||||
import { resolve } from "$app/paths";
|
||||
const client = useConvexClient();
|
||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||
const estatisticasLGPD = useQuery(api.lgpd.obterEstatisticasLGPD, {});
|
||||
|
||||
// Verificar se está carregando
|
||||
const carregando = $derived(usuariosQuery === undefined);
|
||||
@@ -96,6 +97,54 @@
|
||||
</div>
|
||||
{/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 -->
|
||||
<div class="card bg-base-100 shadow-xl mb-8">
|
||||
<div class="card-body">
|
||||
@@ -115,6 +164,11 @@
|
||||
<FileText class="h-5 w-5" strokeWidth={2} />
|
||||
Ver Logs
|
||||
</a>
|
||||
|
||||
<a href={resolve("/ti/lgpd")} class="btn btn-info">
|
||||
<Shield class="h-5 w-5" strokeWidth={2} />
|
||||
LGPD
|
||||
</a>
|
||||
</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 healthCheck from "../healthCheck.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as lgpd from "../lgpd.js";
|
||||
import type * as logsAcesso from "../logsAcesso.js";
|
||||
import type * as logsAtividades from "../logsAtividades.js";
|
||||
import type * as logsLogin from "../logsLogin.js";
|
||||
@@ -101,6 +102,7 @@ declare const fullApi: ApiFromModules<{
|
||||
funcionarios: typeof funcionarios;
|
||||
healthCheck: typeof healthCheck;
|
||||
http: typeof http;
|
||||
lgpd: typeof lgpd;
|
||||
logsAcesso: typeof logsAcesso;
|
||||
logsAtividades: typeof logsAtividades;
|
||||
logsLogin: typeof logsLogin;
|
||||
|
||||
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_data_inicio", ["dataInicio"])
|
||||
.index("by_data_fim", ["dataFim"]),
|
||||
|
||||
// ========== LGPD - Lei Geral de Proteção de Dados ==========
|
||||
|
||||
// Solicitações de direitos LGPD
|
||||
solicitacoesLGPD: defineTable({
|
||||
tipo: v.union(
|
||||
v.literal("acesso"),
|
||||
v.literal("correcao"),
|
||||
v.literal("exclusao"),
|
||||
v.literal("portabilidade"),
|
||||
v.literal("revogacao_consentimento"),
|
||||
v.literal("informacao_compartilhamento")
|
||||
),
|
||||
usuarioId: v.id("usuarios"),
|
||||
funcionarioId: v.optional(v.id("funcionarios")),
|
||||
status: v.union(
|
||||
v.literal("pendente"),
|
||||
v.literal("em_analise"),
|
||||
v.literal("concluida"),
|
||||
v.literal("rejeitada")
|
||||
),
|
||||
dadosSolicitados: v.optional(v.string()), // JSON com detalhes da solicitação
|
||||
resposta: v.optional(v.string()), // Resposta da solicitação
|
||||
arquivoResposta: v.optional(v.id("_storage")), // Arquivo gerado (ex: exportação de dados)
|
||||
respondidoPor: v.optional(v.id("usuarios")),
|
||||
respondidoEm: v.optional(v.number()),
|
||||
criadoEm: v.number(),
|
||||
prazoResposta: v.number(), // Prazo legal (15 dias) - timestamp
|
||||
observacoes: v.optional(v.string()),
|
||||
})
|
||||
.index("by_usuario", ["usuarioId"])
|
||||
.index("by_status", ["status"])
|
||||
.index("by_tipo", ["tipo"])
|
||||
.index("by_prazo", ["prazoResposta"])
|
||||
.index("by_funcionario", ["funcionarioId"]),
|
||||
|
||||
// Consentimentos dos usuários
|
||||
consentimentos: defineTable({
|
||||
usuarioId: v.id("usuarios"),
|
||||
tipo: v.union(
|
||||
v.literal("termo_uso"),
|
||||
v.literal("politica_privacidade"),
|
||||
v.literal("comunicacoes"),
|
||||
v.literal("compartilhamento_dados")
|
||||
),
|
||||
aceito: v.boolean(),
|
||||
versao: v.string(), // Versão do documento aceito (ex: "1.0")
|
||||
ipAddress: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
aceitoEm: v.number(),
|
||||
revogadoEm: v.optional(v.number()),
|
||||
revogadoPor: v.optional(v.id("usuarios")), // Se revogado pelo próprio usuário ou por TI
|
||||
})
|
||||
.index("by_usuario", ["usuarioId"])
|
||||
.index("by_tipo", ["tipo"])
|
||||
.index("by_usuario_tipo", ["usuarioId", "tipo"])
|
||||
.index("by_versao", ["versao"]),
|
||||
|
||||
// Registro de Operações de Tratamento (ROT)
|
||||
registrosTratamento: defineTable({
|
||||
finalidade: v.string(), // Finalidade do tratamento
|
||||
baseLegal: v.string(), // Base legal (ex: "Art. 7º, II - Execução de políticas públicas")
|
||||
categoriasDados: v.array(v.string()), // ["dados_identificacao", "dados_contato", "dados_profissionais"]
|
||||
categoriasTitulares: v.array(v.string()), // ["funcionarios", "servidores", "colaboradores"]
|
||||
medidasSeguranca: v.array(v.string()), // ["criptografia", "controle_acesso", "logs_auditoria"]
|
||||
prazoRetencao: v.number(), // em dias
|
||||
compartilhamentoTerceiros: v.boolean(),
|
||||
terceiros: v.optional(v.array(v.string())), // Lista de terceiros com quem compartilha
|
||||
responsavel: v.id("usuarios"), // Responsável pelo tratamento
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
ativo: v.boolean(),
|
||||
descricao: v.optional(v.string()), // Descrição detalhada
|
||||
})
|
||||
.index("by_finalidade", ["finalidade"])
|
||||
.index("by_ativo", ["ativo"])
|
||||
.index("by_responsavel", ["responsavel"]),
|
||||
|
||||
// Configurações LGPD
|
||||
configuracaoLGPD: defineTable({
|
||||
encarregadoNome: v.optional(v.string()),
|
||||
encarregadoEmail: v.optional(v.string()),
|
||||
encarregadoTelefone: v.optional(v.string()),
|
||||
prazoRespostaPadrao: v.number(), // em dias (padrão: 15)
|
||||
diasAlertaVencimento: v.number(), // dias antes do prazo para alertar (padrão: 3)
|
||||
politicaRetencao: v.optional(v.string()), // JSON com política de retenção por tipo de dado
|
||||
ativo: v.boolean(),
|
||||
atualizadoPor: v.id("usuarios"),
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_ativo", ["ativo"]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user