refactor: improve data handling and UI feedback in LGPD-related components; enhance error handling and consent term display
This commit is contained in:
@@ -30,7 +30,17 @@ let observacoes = $state('');
|
||||
let carregando = $state(false);
|
||||
|
||||
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 tiposSolicitacao: Array<{ valor: TipoSolicitacao; label: string; descricao: string }> = [
|
||||
@@ -241,16 +251,26 @@ let carregando = $state(false);
|
||||
<!-- 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>
|
||||
<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">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if minhasSolicitacoes.length === 0}
|
||||
{:else if !Array.isArray(minhasSolicitacoes) || 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>
|
||||
<p class="text-xs text-base-content/40 mt-2">
|
||||
Suas solicitações aparecerão aqui após serem criadas
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
|
||||
@@ -32,11 +32,15 @@
|
||||
let termoBusca = $state('');
|
||||
|
||||
const client = useConvexClient();
|
||||
const solicitacoes = useQuery(api.lgpd.listarSolicitacoes, {
|
||||
|
||||
// Query reativa que atualiza quando os filtros mudam
|
||||
const solicitacoesQuery = $derived({
|
||||
status: statusFiltro || undefined,
|
||||
tipo: tipoFiltro || undefined
|
||||
});
|
||||
|
||||
const solicitacoes = useQuery(api.lgpd.listarSolicitacoes, solicitacoesQuery);
|
||||
|
||||
let solicitacaoSelecionada = $state<string | null>(null);
|
||||
let resposta = $state('');
|
||||
let statusResposta = $state<'concluida' | 'rejeitada' | 'em_analise'>('concluida');
|
||||
@@ -85,15 +89,21 @@
|
||||
}
|
||||
|
||||
function filtrarSolicitacoes() {
|
||||
if (!solicitacoes) return [];
|
||||
if (!termoBusca) return solicitacoes;
|
||||
// Verificar se solicitacoes existe e é um array
|
||||
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(
|
||||
(s) =>
|
||||
s.usuarioNome.toLowerCase().includes(busca) ||
|
||||
s.usuarioEmail.toLowerCase().includes(busca) ||
|
||||
(s.usuarioMatricula?.toLowerCase().includes(busca) ?? false)
|
||||
(s.usuarioNome?.toLowerCase().includes(busca) ?? false) ||
|
||||
(s.usuarioEmail?.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 -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<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">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if solicitacoesFiltradas.length === 0}
|
||||
{:else if !Array.isArray(solicitacoes) || solicitacoes.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>
|
||||
<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>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
@@ -230,6 +276,23 @@
|
||||
<div>
|
||||
<span class="font-semibold">E-mail:</span> {solicitacao.usuarioEmail}
|
||||
</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>
|
||||
<span class="font-semibold">Criada em:</span>{' '}
|
||||
{format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||
|
||||
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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_flows from "../tables/flows.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_pedidos from "../tables/pedidos.js";
|
||||
import type * as tables_ponto from "../tables/ponto.js";
|
||||
@@ -155,6 +156,7 @@ declare const fullApi: ApiFromModules<{
|
||||
"tables/ferias": typeof tables_ferias;
|
||||
"tables/flows": typeof tables_flows;
|
||||
"tables/funcionarios": typeof tables_funcionarios;
|
||||
"tables/lgpdTables": typeof tables_lgpdTables;
|
||||
"tables/licencas": typeof tables_licencas;
|
||||
"tables/pedidos": typeof tables_pedidos;
|
||||
"tables/ponto": typeof tables_ponto;
|
||||
|
||||
@@ -311,17 +311,21 @@ export const listarMinhasSolicitacoes = query({
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
let solicitacoes = await ctx.db
|
||||
.query('solicitacoesLGPD')
|
||||
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id))
|
||||
.order('desc')
|
||||
.collect();
|
||||
|
||||
// Filtrar por status se especificado
|
||||
if (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,
|
||||
tipo: s.tipo,
|
||||
status: s.status,
|
||||
@@ -331,6 +335,13 @@ export const listarMinhasSolicitacoes = query({
|
||||
resposta: s.resposta ?? 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(),
|
||||
prazoResposta: v.number(),
|
||||
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) => {
|
||||
@@ -382,23 +402,53 @@ export const listarSolicitacoes = query({
|
||||
// Verificar se é TI (simplificado - pode melhorar com verificação de role)
|
||||
// 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) {
|
||||
solicitacoes = solicitacoes.filter((s) => s.status === args.status);
|
||||
}
|
||||
|
||||
// Filtrar por tipo
|
||||
if (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) {
|
||||
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
|
||||
const resultado = await Promise.all(
|
||||
solicitacoes.map(async (s) => {
|
||||
// Usar Promise.allSettled para garantir que todas as solicitações sejam processadas,
|
||||
// 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);
|
||||
let matricula: string | null = null;
|
||||
|
||||
@@ -413,6 +463,38 @@ export const listarSolicitacoes = query({
|
||||
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 {
|
||||
_id: s._id,
|
||||
tipo: s.tipo,
|
||||
@@ -423,11 +505,34 @@ export const listarSolicitacoes = query({
|
||||
criadoEm: s.criadoEm,
|
||||
prazoResposta: s.prazoResposta,
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user