Files
sgse-app/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte

379 lines
11 KiB
Svelte

<script lang="ts">
import { resolve } from '$app/paths';
import { useQuery, useConvexClient } 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';
let tipoSelecionado = $state<TipoSolicitacao | null>(null);
let dadosSolicitados = $state('');
let observacoes = $state('');
let carregando = $state(false);
const client = useConvexClient();
const minhasSolicitacoes = useQuery(api.lgpd.listarMinhasSolicitacoes, {});
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 client.mutation(api.lgpd.criarSolicitacao, {
tipo: tipoSelecionado,
dadosSolicitados: dadosSolicitados || undefined,
observacoes: observacoes || undefined
});
toast.success('Solicitação criada com sucesso!');
tipoSelecionado = null;
dadosSolicitados = '';
observacoes = '';
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Erro ao criar solicitação';
toast.error(message);
} 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>