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:
2025-12-01 22:37:43 -03:00
parent 95c3b48ae6
commit fec5f5c33d
14 changed files with 3231 additions and 5 deletions

View File

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

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

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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