379 lines
11 KiB
Svelte
379 lines
11 KiB
Svelte
<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: 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>
|
|
|