diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 7a30849..7c38553 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -451,10 +451,10 @@ href={resolve('/abrir-chamado')} class="link link-hover hover:text-primary transition-colors">Suporte - + Privacidade + >Privacidade
diff --git a/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte b/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte index 101545f..63a3de1 100644 --- a/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte +++ b/apps/web/src/routes/(dashboard)/privacidade/meus-dados/+page.svelte @@ -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

+
+

Minhas Solicitações

+ {#if minhasSolicitacoes && Array.isArray(minhasSolicitacoes)} +
+ {minhasSolicitacoes.length} solicitação{minhasSolicitacoes.length !== 1 ? 'ões' : ''} +
+ {/if} +
- {#if minhasSolicitacoes === undefined} + {#if minhasSolicitacoes === undefined || minhasSolicitacoes === null}
- {:else if minhasSolicitacoes.length === 0} + {:else if !Array.isArray(minhasSolicitacoes) || minhasSolicitacoes.length === 0}

Nenhuma solicitação encontrada

+

+ Suas solicitações aparecerão aqui após serem criadas +

{:else}
diff --git a/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte index ea02ef0..98f4f4b 100644 --- a/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/lgpd/solicitacoes/+page.svelte @@ -32,10 +32,14 @@ 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(null); let resposta = $state(''); @@ -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 []; + + // Se não há termo de busca, retorna todas as solicitações + if (!termoBusca || termoBusca.trim() === '') return solicitacoes; - const busca = termoBusca.toLowerCase(); + // 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 @@
-

Solicitações

+
+

Solicitações

+ {#if solicitacoes && Array.isArray(solicitacoes)} +
+ Total: {solicitacoes.length} | Exibindo: {solicitacoesFiltradas.length} +
+ {/if} +
- {#if solicitacoes === undefined} + {#if solicitacoes === undefined || solicitacoes === null}
- {:else if solicitacoesFiltradas.length === 0} + {:else if !Array.isArray(solicitacoes) || solicitacoes.length === 0}
-

Nenhuma solicitação encontrada

+

+ {#if statusFiltro || tipoFiltro} + Nenhuma solicitação encontrada com os filtros aplicados + {:else} + Nenhuma solicitação encontrada + {/if} +

+ {#if statusFiltro || tipoFiltro} + + {/if} +
+ {:else if solicitacoesFiltradas.length === 0 && termoBusca} +
+ +

Nenhuma solicitação encontrada com o termo "{termoBusca}"

+
{:else}
@@ -230,6 +276,23 @@
E-mail: {solicitacao.usuarioEmail}
+ {#if solicitacao.consentimentoTermo} +
+ Termo de Consentimento: + Aceito + + (v.{solicitacao.consentimentoTermo.versao} em{' '} + {format(new Date(solicitacao.consentimentoTermo.aceitoEm), 'dd/MM/yyyy', { + locale: ptBR + })}) + +
+ {:else} +
+ Termo de Consentimento: + Não aceito +
+ {/if}
Criada em:{' '} {format(new Date(solicitacao.criadoEm), "dd/MM/yyyy 'às' HH:mm", { diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 799c897..beb3c67 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -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; diff --git a/packages/backend/convex/lgpd.ts b/packages/backend/convex/lgpd.ts index 1740bff..26f2e09 100644 --- a/packages/backend/convex/lgpd.ts +++ b/packages/backend/convex/lgpd.ts @@ -311,26 +311,37 @@ export const listarMinhasSolicitacoes = query({ return []; } - let solicitacoes = await ctx.db - .query('solicitacoesLGPD') - .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) - .order('desc') - .collect(); + try { + let solicitacoes = await ctx.db + .query('solicitacoesLGPD') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) + .collect(); - if (args.status) { - solicitacoes = solicitacoes.filter((s) => s.status === args.status); + // Filtrar por status se especificado + if (args.status) { + solicitacoes = solicitacoes.filter((s) => s.status === args.status); + } + + // 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, + criadoEm: s.criadoEm, + prazoResposta: s.prazoResposta, + respondidoEm: s.respondidoEm ?? null, + 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 []; } - - return solicitacoes.map((s) => ({ - _id: s._id, - tipo: s.tipo, - status: s.status, - criadoEm: s.criadoEm, - prazoResposta: s.prazoResposta, - respondidoEm: s.respondidoEm ?? null, - resposta: s.resposta ?? null, - arquivoResposta: s.arquivoResposta ? s.arquivoResposta.toString() : null - })); } }); @@ -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,52 +402,137 @@ 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) => { - const usuarioSolicitante = await ctx.db.get(s.usuarioId); - let matricula: string | null = null; + // 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 => { + try { + const usuarioSolicitante = await ctx.db.get(s.usuarioId); + let matricula: string | null = null; - if (usuarioSolicitante?.funcionarioId) { - const funcionario = await ctx.db.get(usuarioSolicitante.funcionarioId); - matricula = funcionario?.matricula ?? null; + if (usuarioSolicitante?.funcionarioId) { + const funcionario = await ctx.db.get(usuarioSolicitante.funcionarioId); + matricula = funcionario?.matricula ?? null; + } + + let respondidoPorNome: string | null = null; + if (s.respondidoPor) { + const respondente = await ctx.db.get(s.respondidoPor); + 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, + status: s.status, + usuarioNome: usuarioSolicitante?.nome ?? 'Usuário Desconhecido', + usuarioEmail: usuarioSolicitante?.email ?? '', + usuarioMatricula: matricula, + criadoEm: s.criadoEm, + prazoResposta: s.prazoResposta, + respondidoEm: s.respondidoEm ?? null, + 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 + }; } - - let respondidoPorNome: string | null = null; - if (s.respondidoPor) { - const respondente = await ctx.db.get(s.respondidoPor); - respondidoPorNome = respondente?.nome ?? null; - } - - return { - _id: s._id, - tipo: s.tipo, - status: s.status, - usuarioNome: usuarioSolicitante?.nome ?? 'Usuário Desconhecido', - usuarioEmail: usuarioSolicitante?.email ?? '', - usuarioMatricula: matricula, - criadoEm: s.criadoEm, - prazoResposta: s.prazoResposta, - respondidoEm: s.respondidoEm ?? null, - respondidoPorNome - }; }) ); + // Filtrar apenas resultados bem-sucedidos e converter para o tipo correto + const resultado = resultados + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map((r) => r.value); + return resultado; } });