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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user