refactor: improve data handling and UI feedback in LGPD-related components; enhance error handling and consent term display

This commit is contained in:
2025-12-02 14:03:52 -03:00
parent e81054874f
commit ffa4dc5fb2
5 changed files with 254 additions and 64 deletions

View File

@@ -30,7 +30,17 @@ let observacoes = $state('');
let carregando = $state(false); let carregando = $state(false);
const client = useConvexClient(); const client = useConvexClient();
const minhasSolicitacoes = useQuery(api.lgpd.listarMinhasSolicitacoes, {}); const minhasSolicitacoesQuery = useQuery(api.lgpd.listarMinhasSolicitacoes, {});
// Garantir que sempre seja um array ou undefined
const minhasSolicitacoes = $derived(
minhasSolicitacoesQuery === undefined || minhasSolicitacoesQuery === null
? undefined
: Array.isArray(minhasSolicitacoesQuery)
? minhasSolicitacoesQuery
: []
);
const exportarDados = useQuery(api.lgpd.exportarDadosUsuario, {}); const exportarDados = useQuery(api.lgpd.exportarDadosUsuario, {});
const tiposSolicitacao: Array<{ valor: TipoSolicitacao; label: string; descricao: string }> = [ const tiposSolicitacao: Array<{ valor: TipoSolicitacao; label: string; descricao: string }> = [
@@ -241,16 +251,26 @@ let carregando = $state(false);
<!-- Minhas Solicitações --> <!-- Minhas Solicitações -->
<div class="card bg-base-100 shadow-xl"> <div class="card bg-base-100 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-2xl mb-4">Minhas Solicitações</h2> <div class="flex items-center justify-between mb-4">
<h2 class="card-title text-2xl">Minhas Solicitações</h2>
{#if minhasSolicitacoes && Array.isArray(minhasSolicitacoes)}
<div class="badge badge-outline">
{minhasSolicitacoes.length} solicitação{minhasSolicitacoes.length !== 1 ? 'ões' : ''}
</div>
{/if}
</div>
{#if minhasSolicitacoes === undefined} {#if minhasSolicitacoes === undefined || minhasSolicitacoes === null}
<div class="flex justify-center items-center py-10"> <div class="flex justify-center items-center py-10">
<span class="loading loading-spinner loading-lg text-primary"></span> <span class="loading loading-spinner loading-lg text-primary"></span>
</div> </div>
{:else if minhasSolicitacoes.length === 0} {:else if !Array.isArray(minhasSolicitacoes) || minhasSolicitacoes.length === 0}
<div class="text-center py-10"> <div class="text-center py-10">
<FileText class="h-16 w-16 text-base-content/30 mx-auto mb-4" /> <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> <p class="text-base-content/60">Nenhuma solicitação encontrada</p>
<p class="text-xs text-base-content/40 mt-2">
Suas solicitações aparecerão aqui após serem criadas
</p>
</div> </div>
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">

View File

@@ -32,11 +32,15 @@
let termoBusca = $state(''); let termoBusca = $state('');
const client = useConvexClient(); const client = useConvexClient();
const solicitacoes = useQuery(api.lgpd.listarSolicitacoes, {
// Query reativa que atualiza quando os filtros mudam
const solicitacoesQuery = $derived({
status: statusFiltro || undefined, status: statusFiltro || undefined,
tipo: tipoFiltro || undefined tipo: tipoFiltro || undefined
}); });
const solicitacoes = useQuery(api.lgpd.listarSolicitacoes, solicitacoesQuery);
let solicitacaoSelecionada = $state<string | null>(null); let solicitacaoSelecionada = $state<string | null>(null);
let resposta = $state(''); let resposta = $state('');
let statusResposta = $state<'concluida' | 'rejeitada' | 'em_analise'>('concluida'); let statusResposta = $state<'concluida' | 'rejeitada' | 'em_analise'>('concluida');
@@ -85,15 +89,21 @@
} }
function filtrarSolicitacoes() { function filtrarSolicitacoes() {
if (!solicitacoes) return []; // Verificar se solicitacoes existe e é um array
if (!termoBusca) return solicitacoes; if (!solicitacoes || !Array.isArray(solicitacoes)) return [];
const busca = termoBusca.toLowerCase(); // Se não há termo de busca, retorna todas as solicitações
if (!termoBusca || termoBusca.trim() === '') return solicitacoes;
// Filtrar por termo de busca
const busca = termoBusca.toLowerCase().trim();
return solicitacoes.filter( return solicitacoes.filter(
(s) => (s) =>
s.usuarioNome.toLowerCase().includes(busca) || (s.usuarioNome?.toLowerCase().includes(busca) ?? false) ||
s.usuarioEmail.toLowerCase().includes(busca) || (s.usuarioEmail?.toLowerCase().includes(busca) ?? false) ||
(s.usuarioMatricula?.toLowerCase().includes(busca) ?? false) (s.usuarioMatricula?.toLowerCase().includes(busca) ?? false) ||
(getTipoLabel(s.tipo).toLowerCase().includes(busca)) ||
(getStatusBadge(s.status).label.toLowerCase().includes(busca))
); );
} }
@@ -193,16 +203,52 @@
<!-- Lista de Solicitações --> <!-- Lista de Solicitações -->
<div class="card bg-base-100 shadow-xl"> <div class="card bg-base-100 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-2xl mb-4">Solicitações</h2> <div class="flex items-center justify-between mb-4">
<h2 class="card-title text-2xl">Solicitações</h2>
{#if solicitacoes && Array.isArray(solicitacoes)}
<div class="badge badge-outline">
Total: {solicitacoes.length} | Exibindo: {solicitacoesFiltradas.length}
</div>
{/if}
</div>
{#if solicitacoes === undefined} {#if solicitacoes === undefined || solicitacoes === null}
<div class="flex justify-center items-center py-20"> <div class="flex justify-center items-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span> <span class="loading loading-spinner loading-lg text-primary"></span>
</div> </div>
{:else if solicitacoesFiltradas.length === 0} {:else if !Array.isArray(solicitacoes) || solicitacoes.length === 0}
<div class="text-center py-10"> <div class="text-center py-10">
<FileText class="h-16 w-16 text-base-content/30 mx-auto mb-4" /> <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> <p class="text-base-content/60">
{#if statusFiltro || tipoFiltro}
Nenhuma solicitação encontrada com os filtros aplicados
{:else}
Nenhuma solicitação encontrada
{/if}
</p>
{#if statusFiltro || tipoFiltro}
<button
onclick={() => {
statusFiltro = null;
tipoFiltro = null;
termoBusca = '';
}}
class="btn btn-sm btn-outline mt-4"
>
Limpar Filtros
</button>
{/if}
</div>
{:else if solicitacoesFiltradas.length === 0 && termoBusca}
<div class="text-center py-10">
<Search class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
<p class="text-base-content/60">Nenhuma solicitação encontrada com o termo "{termoBusca}"</p>
<button
onclick={() => (termoBusca = '')}
class="btn btn-sm btn-outline mt-4"
>
Limpar Busca
</button>
</div> </div>
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">
@@ -230,6 +276,23 @@
<div> <div>
<span class="font-semibold">E-mail:</span> {solicitacao.usuarioEmail} <span class="font-semibold">E-mail:</span> {solicitacao.usuarioEmail}
</div> </div>
{#if solicitacao.consentimentoTermo}
<div class="flex items-center gap-2">
<span class="font-semibold">Termo de Consentimento:</span>
<span class="badge badge-success badge-sm">Aceito</span>
<span class="text-xs text-base-content/60">
(v.{solicitacao.consentimentoTermo.versao} em{' '}
{format(new Date(solicitacao.consentimentoTermo.aceitoEm), 'dd/MM/yyyy', {
locale: ptBR
})})
</span>
</div>
{:else}
<div class="flex items-center gap-2">
<span class="font-semibold">Termo de Consentimento:</span>
<span class="badge badge-warning badge-sm">Não aceito</span>
</div>
{/if}
<div> <div>
<span class="font-semibold">Criada em:</span>{' '} <span class="font-semibold">Criada em:</span>{' '}
{format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", { {format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", {

View File

@@ -69,6 +69,7 @@ import type * as tables_enderecos from "../tables/enderecos.js";
import type * as tables_ferias from "../tables/ferias.js"; import type * as tables_ferias from "../tables/ferias.js";
import type * as tables_flows from "../tables/flows.js"; import type * as tables_flows from "../tables/flows.js";
import type * as tables_funcionarios from "../tables/funcionarios.js"; import type * as tables_funcionarios from "../tables/funcionarios.js";
import type * as tables_lgpdTables from "../tables/lgpdTables.js";
import type * as tables_licencas from "../tables/licencas.js"; import type * as tables_licencas from "../tables/licencas.js";
import type * as tables_pedidos from "../tables/pedidos.js"; import type * as tables_pedidos from "../tables/pedidos.js";
import type * as tables_ponto from "../tables/ponto.js"; import type * as tables_ponto from "../tables/ponto.js";
@@ -155,6 +156,7 @@ declare const fullApi: ApiFromModules<{
"tables/ferias": typeof tables_ferias; "tables/ferias": typeof tables_ferias;
"tables/flows": typeof tables_flows; "tables/flows": typeof tables_flows;
"tables/funcionarios": typeof tables_funcionarios; "tables/funcionarios": typeof tables_funcionarios;
"tables/lgpdTables": typeof tables_lgpdTables;
"tables/licencas": typeof tables_licencas; "tables/licencas": typeof tables_licencas;
"tables/pedidos": typeof tables_pedidos; "tables/pedidos": typeof tables_pedidos;
"tables/ponto": typeof tables_ponto; "tables/ponto": typeof tables_ponto;

View File

@@ -311,17 +311,21 @@ export const listarMinhasSolicitacoes = query({
return []; return [];
} }
try {
let solicitacoes = await ctx.db let solicitacoes = await ctx.db
.query('solicitacoesLGPD') .query('solicitacoesLGPD')
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id))
.order('desc')
.collect(); .collect();
// Filtrar por status se especificado
if (args.status) { if (args.status) {
solicitacoes = solicitacoes.filter((s) => s.status === args.status); solicitacoes = solicitacoes.filter((s) => s.status === args.status);
} }
return solicitacoes.map((s) => ({ // Ordenar por data de criação (mais recentes primeiro)
solicitacoes.sort((a, b) => b.criadoEm - a.criadoEm);
const resultado = solicitacoes.map((s) => ({
_id: s._id, _id: s._id,
tipo: s.tipo, tipo: s.tipo,
status: s.status, status: s.status,
@@ -331,6 +335,13 @@ export const listarMinhasSolicitacoes = query({
resposta: s.resposta ?? null, resposta: s.resposta ?? null,
arquivoResposta: s.arquivoResposta ? s.arquivoResposta.toString() : null arquivoResposta: s.arquivoResposta ? s.arquivoResposta.toString() : null
})); }));
console.log(`[listarMinhasSolicitacoes] Usuário: ${usuario._id}, Solicitações encontradas: ${resultado.length}`);
return resultado;
} catch (error) {
console.error('[listarMinhasSolicitacoes] Erro ao listar minhas solicitações:', error);
return [];
}
} }
}); });
@@ -370,7 +381,16 @@ export const listarSolicitacoes = query({
criadoEm: v.number(), criadoEm: v.number(),
prazoResposta: v.number(), prazoResposta: v.number(),
respondidoEm: v.union(v.number(), v.null()), respondidoEm: v.union(v.number(), v.null()),
respondidoPorNome: v.union(v.string(), v.null()) respondidoPorNome: v.union(v.string(), v.null()),
consentimentoTermo: v.union(
v.object({
aceito: v.boolean(),
versao: v.string(),
aceitoEm: v.number(),
revogadoEm: v.union(v.number(), v.null())
}),
v.null()
)
}) })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -382,23 +402,53 @@ export const listarSolicitacoes = query({
// Verificar se é TI (simplificado - pode melhorar com verificação de role) // Verificar se é TI (simplificado - pode melhorar com verificação de role)
// Por enquanto, qualquer usuário autenticado pode ver (será melhorado) // Por enquanto, qualquer usuário autenticado pode ver (será melhorado)
let solicitacoes = await ctx.db.query('solicitacoesLGPD').order('desc').collect(); // Buscar TODAS as solicitações sem filtros iniciais
let solicitacoes = await ctx.db.query('solicitacoesLGPD').collect();
// Filtrar por status
if (args.status) { if (args.status) {
solicitacoes = solicitacoes.filter((s) => s.status === args.status); solicitacoes = solicitacoes.filter((s) => s.status === args.status);
} }
// Filtrar por tipo
if (args.tipo) { if (args.tipo) {
solicitacoes = solicitacoes.filter((s) => s.tipo === args.tipo); solicitacoes = solicitacoes.filter((s) => s.tipo === args.tipo);
} }
// Ordenar por data de criação (mais recentes primeiro)
solicitacoes.sort((a, b) => b.criadoEm - a.criadoEm);
// Aplicar limite se especificado
if (args.limite) { if (args.limite) {
solicitacoes = solicitacoes.slice(0, args.limite); solicitacoes = solicitacoes.slice(0, args.limite);
} }
// Tipo do resultado enriquecido
type SolicitacaoEnriquecida = {
_id: Id<'solicitacoesLGPD'>;
tipo: string;
status: string;
usuarioNome: string;
usuarioEmail: string;
usuarioMatricula: string | null;
criadoEm: number;
prazoResposta: number;
respondidoEm: number | null;
respondidoPorNome: string | null;
consentimentoTermo: {
aceito: boolean;
versao: string;
aceitoEm: number;
revogadoEm: number | null;
} | null;
};
// Enriquecer com dados do usuário // Enriquecer com dados do usuário
const resultado = await Promise.all( // Usar Promise.allSettled para garantir que todas as solicitações sejam processadas,
solicitacoes.map(async (s) => { // mesmo se houver erro ao buscar dados de algum usuário
const resultados = await Promise.allSettled(
solicitacoes.map(async (s): Promise<SolicitacaoEnriquecida> => {
try {
const usuarioSolicitante = await ctx.db.get(s.usuarioId); const usuarioSolicitante = await ctx.db.get(s.usuarioId);
let matricula: string | null = null; let matricula: string | null = null;
@@ -413,6 +463,38 @@ export const listarSolicitacoes = query({
respondidoPorNome = respondente?.nome ?? null; respondidoPorNome = respondente?.nome ?? null;
} }
// Buscar consentimento do termo de uso
let consentimentoTermo: {
aceito: boolean;
versao: string;
aceitoEm: number;
revogadoEm: number | null;
} | null = null;
if (usuarioSolicitante) {
try {
const consentimento = await ctx.db
.query('consentimentos')
.withIndex('by_usuario_tipo', (q) =>
q.eq('usuarioId', usuarioSolicitante._id).eq('tipo', 'termo_uso')
)
.order('desc')
.first();
if (consentimento && consentimento.aceito && !consentimento.revogadoEm) {
consentimentoTermo = {
aceito: consentimento.aceito,
versao: consentimento.versao,
aceitoEm: consentimento.aceitoEm,
revogadoEm: consentimento.revogadoEm ?? null
};
}
} catch (error) {
// Se houver erro ao buscar consentimento, continua sem ele
console.error('Erro ao buscar consentimento:', error);
}
}
return { return {
_id: s._id, _id: s._id,
tipo: s.tipo, tipo: s.tipo,
@@ -423,11 +505,34 @@ export const listarSolicitacoes = query({
criadoEm: s.criadoEm, criadoEm: s.criadoEm,
prazoResposta: s.prazoResposta, prazoResposta: s.prazoResposta,
respondidoEm: s.respondidoEm ?? null, respondidoEm: s.respondidoEm ?? null,
respondidoPorNome respondidoPorNome,
consentimentoTermo
}; };
} catch (error) {
// Se houver erro ao processar uma solicitação, retorna com dados mínimos
console.error('Erro ao processar solicitação:', s._id, error);
return {
_id: s._id,
tipo: s.tipo,
status: s.status,
usuarioNome: 'Erro ao carregar',
usuarioEmail: '',
usuarioMatricula: null,
criadoEm: s.criadoEm,
prazoResposta: s.prazoResposta,
respondidoEm: s.respondidoEm ?? null,
respondidoPorNome: null,
consentimentoTermo: null
};
}
}) })
); );
// Filtrar apenas resultados bem-sucedidos e converter para o tipo correto
const resultado = resultados
.filter((r): r is PromiseFulfilledResult<SolicitacaoEnriquecida> => r.status === 'fulfilled')
.map((r) => r.value);
return resultado; return resultado;
} }
}); });