import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; import { getCurrentUserFunction } from './auth'; import { Id, Doc } from './_generated/dataModel'; import type { QueryCtx, MutationCtx } from './_generated/server'; import { registrarAtividade } from './logsAtividades'; import { api } from './_generated/api'; /** * Verificar se usuário aceitou o termo de consentimento */ export const verificarConsentimento = query({ args: { tipo: v.optional( v.union( v.literal('termo_uso'), v.literal('politica_privacidade'), v.literal('comunicacoes'), v.literal('compartilhamento_dados') ) ) }, returns: v.union( v.object({ aceito: v.boolean(), versao: v.string(), aceitoEm: v.number(), termoObrigatorio: v.boolean(), versaoTermoAtual: v.string() }), v.null() ), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return null; } const tipo = args.tipo || 'termo_uso'; // Buscar configuração para verificar se termo é obrigatório const config = await ctx.db .query('configuracaoLGPD') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); const termoObrigatorio = config?.termoObrigatorio ?? false; const versaoTermoAtual = config?.versaoTermoAtual ?? '1.0'; const consentimento = await ctx.db .query('consentimentos') .withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', tipo)) .order('desc') .first(); if (!consentimento || !consentimento.aceito || consentimento.revogadoEm) { return { aceito: false, versao: '', aceitoEm: 0, termoObrigatorio, versaoTermoAtual }; } return { aceito: consentimento.aceito, versao: consentimento.versao, aceitoEm: consentimento.aceitoEm, termoObrigatorio, versaoTermoAtual }; } }); /** * Registrar consentimento do usuário */ export const registrarConsentimento = mutation({ args: { tipo: v.union( v.literal('termo_uso'), v.literal('politica_privacidade'), v.literal('comunicacoes'), v.literal('compartilhamento_dados') ), aceito: v.boolean(), versao: v.string(), ipAddress: v.optional(v.string()), userAgent: v.optional(v.string()) }, returns: v.object({ sucesso: v.boolean(), consentimentoId: v.id('consentimentos') }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Verificar se já existe consentimento ativo const existente = await ctx.db .query('consentimentos') .withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', args.tipo)) .order('desc') .first(); if (existente && existente.aceito && !existente.revogadoEm) { // Atualizar consentimento existente await ctx.db.patch(existente._id, { aceito: args.aceito, versao: args.versao, aceitoEm: Date.now(), ipAddress: args.ipAddress, userAgent: args.userAgent, revogadoEm: undefined, revogadoPor: undefined }); return { sucesso: true, consentimentoId: existente._id }; } // Criar novo consentimento const consentimentoId = await ctx.db.insert('consentimentos', { usuarioId: usuario._id, tipo: args.tipo, aceito: args.aceito, versao: args.versao, ipAddress: args.ipAddress, userAgent: args.userAgent, aceitoEm: Date.now() }); // Log de atividade await registrarAtividade( ctx, usuario._id, 'aceitar_consentimento', 'consentimentos', JSON.stringify({ tipo: args.tipo, versao: args.versao }), consentimentoId.toString() ); return { sucesso: true, consentimentoId }; } }); /** * Revogar consentimento */ export const revogarConsentimento = mutation({ args: { tipo: v.union( v.literal('termo_uso'), v.literal('politica_privacidade'), v.literal('comunicacoes'), v.literal('compartilhamento_dados') ) }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const consentimento = await ctx.db .query('consentimentos') .withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', args.tipo)) .order('desc') .first(); if (!consentimento) { throw new Error('Consentimento não encontrado'); } await ctx.db.patch(consentimento._id, { revogadoEm: Date.now(), revogadoPor: usuario._id }); // Log de atividade await registrarAtividade( ctx, usuario._id, 'revogar_consentimento', 'consentimentos', JSON.stringify({ tipo: args.tipo }), consentimento._id.toString() ); return { sucesso: true }; } }); /** * Listar consentimentos do usuário */ export const listarConsentimentos = query({ args: {}, returns: v.array( v.object({ _id: v.id('consentimentos'), tipo: v.string(), aceito: v.boolean(), versao: v.string(), aceitoEm: v.number(), revogadoEm: v.union(v.number(), v.null()) }) ), handler: async (ctx) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return []; } const consentimentos = await ctx.db .query('consentimentos') .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) .order('desc') .collect(); return consentimentos.map((c) => ({ _id: c._id, tipo: c.tipo, aceito: c.aceito, versao: c.versao, aceitoEm: c.aceitoEm, revogadoEm: c.revogadoEm ?? null })); } }); /** * Criar solicitação de direito LGPD */ export const criarSolicitacao = mutation({ args: { tipo: v.union( v.literal('acesso'), v.literal('correcao'), v.literal('exclusao'), v.literal('portabilidade'), v.literal('revogacao_consentimento'), v.literal('informacao_compartilhamento') ), dadosSolicitados: v.optional(v.string()), observacoes: v.optional(v.string()) }, returns: v.object({ sucesso: v.boolean(), solicitacaoId: v.id('solicitacoesLGPD') }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Prazo de resposta: 15 dias (conforme LGPD) const prazoResposta = Date.now() + 15 * 24 * 60 * 60 * 1000; const solicitacaoId = await ctx.db.insert('solicitacoesLGPD', { tipo: args.tipo, usuarioId: usuario._id, funcionarioId: usuario.funcionarioId, status: 'pendente', dadosSolicitados: args.dadosSolicitados, observacoes: args.observacoes, criadoEm: Date.now(), prazoResposta }); // Log de atividade await registrarAtividade( ctx, usuario._id, 'criar_solicitacao_lgpd', 'solicitacoesLGPD', JSON.stringify({ tipo: args.tipo }), solicitacaoId.toString() ); // Notificações (email + opcional chat) para o titular if (usuario.email) { let urlSistema = process.env.FRONTEND_URL || 'http://localhost:5173'; if (!urlSistema.match(/^https?:\/\//i)) { urlSistema = `http://${urlSistema}`; } const tipoSolicitacaoLabelMap: Record = { acesso: 'Acesso aos Dados', correcao: 'Correção de Dados', exclusao: 'Exclusão de Dados', portabilidade: 'Portabilidade dos Dados', revogacao_consentimento: 'Revogação de Consentimento', informacao_compartilhamento: 'Informação sobre Compartilhamento' }; const tipoSolicitacaoLabel = tipoSolicitacaoLabelMap[args.tipo] ?? args.tipo; // Email usando template LGPD try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: usuario.email, destinatarioId: usuario._id, templateCodigo: 'lgpd_solicitacao_criada', variaveis: { nomeTitular: usuario.nome, tipoSolicitacaoLabel, prazoResposta: new Date(prazoResposta).toLocaleDateString('pt-BR'), urlPortalLGPD: `${urlSistema}/privacidade/meus-dados` }, enviadoPor: usuario._id }); } catch (error) { console.error('Erro ao agendar email lgpd_solicitacao_criada:', error); } } return { sucesso: true, solicitacaoId }; } }); /** * Listar solicitações do usuário */ export const listarMinhasSolicitacoes = query({ args: { status: v.optional( v.union( v.literal('pendente'), v.literal('em_analise'), v.literal('concluida'), v.literal('rejeitada'), v.literal('cancelada') ) ), _refresh: v.optional(v.number()) // Parâmetro para forçar atualização no frontend }, returns: v.array( v.object({ _id: v.id('solicitacoesLGPD'), tipo: v.string(), status: v.string(), criadoEm: v.number(), prazoResposta: v.number(), respondidoEm: v.union(v.number(), v.null()), resposta: v.union(v.string(), v.null()), arquivoResposta: v.union(v.string(), v.null()) }) ), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return []; } try { let solicitacoes = await ctx.db .query('solicitacoesLGPD') .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) .collect(); // 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 []; } } }); /** * Listar todas as solicitações (apenas TI) */ export const listarSolicitacoes = query({ args: { status: v.optional( v.union( v.literal('pendente'), v.literal('em_analise'), v.literal('concluida'), v.literal('rejeitada'), v.literal('cancelada') ) ), tipo: v.optional( v.union( v.literal('acesso'), v.literal('correcao'), v.literal('exclusao'), v.literal('portabilidade'), v.literal('revogacao_consentimento'), v.literal('informacao_compartilhamento') ) ), limite: v.optional(v.number()) }, returns: v.array( v.object({ _id: v.id('solicitacoesLGPD'), tipo: v.string(), status: v.string(), usuarioNome: v.string(), usuarioEmail: v.string(), usuarioMatricula: v.union(v.string(), v.null()), dadosSolicitados: v.union(v.string(), v.null()), observacoes: v.union(v.string(), v.null()), resposta: v.union(v.string(), v.null()), arquivoResposta: v.union(v.string(), v.null()), criadoEm: v.number(), prazoResposta: v.number(), respondidoEm: v.union(v.number(), 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) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return []; } // Verificar se é TI (simplificado - pode melhorar com verificação de role) // Por enquanto, qualquer usuário autenticado pode ver (será melhorado) // 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; dadosSolicitados: string | null; observacoes: string | null; resposta: string | null; arquivoResposta: 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 // 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; } 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); } } // Buscar URL do arquivo de resposta se existir let arquivoRespostaUrl: string | null = null; if (s.arquivoResposta) { arquivoRespostaUrl = await ctx.storage.getUrl(s.arquivoResposta); } return { _id: s._id, tipo: s.tipo, status: s.status, usuarioNome: usuarioSolicitante?.nome ?? 'Usuário Desconhecido', usuarioEmail: usuarioSolicitante?.email ?? '', usuarioMatricula: matricula, dadosSolicitados: s.dadosSolicitados ?? null, observacoes: s.observacoes ?? null, resposta: s.resposta ?? null, arquivoResposta: arquivoRespostaUrl, 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, dadosSolicitados: s.dadosSolicitados ?? null, observacoes: s.observacoes ?? null, resposta: s.resposta ?? null, arquivoResposta: 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 => r.status === 'fulfilled') .map((r) => r.value); return resultado; } }); /** * Responder solicitação (apenas TI) */ export const responderSolicitacao = mutation({ args: { solicitacaoId: v.id('solicitacoesLGPD'), resposta: v.string(), status: v.union(v.literal('concluida'), v.literal('rejeitada'), v.literal('em_analise')), arquivoResposta: v.optional(v.id('_storage')) }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const solicitacao = await ctx.db.get(args.solicitacaoId); if (!solicitacao) { throw new Error('Solicitação não encontrada'); } // Atualizar resposta da solicitação await ctx.db.patch(args.solicitacaoId, { status: args.status, resposta: args.resposta, arquivoResposta: args.arquivoResposta, respondidoPor: usuario._id, respondidoEm: Date.now() }); // Se for uma solicitação de "Revogar Consentimento" concluída, // revogar todos os consentimentos ativos do titular que fez a solicitação. if (solicitacao.tipo === 'revogacao_consentimento' && args.status === 'concluida') { // Garantir que temos o titular associado if (!solicitacao.usuarioId) { throw new Error( 'Solicitação de revogação de consentimento sem usuário associado. Verifique os dados.' ); } // Buscar consentimentos ativos do usuário const consentimentosAtivos = await ctx.db .query('consentimentos') .withIndex('by_usuario', (q) => q.eq('usuarioId', solicitacao.usuarioId)) .filter((q) => q.eq(q.field('aceito'), true)) .collect(); for (const consentimento of consentimentosAtivos) { // Pular consentimentos já revogados por segurança if (consentimento.revogadoEm) continue; await ctx.db.patch(consentimento._id, { revogadoEm: Date.now(), revogadoPor: usuario._id }); // Registrar atividade individual por consentimento revogado await registrarAtividade( ctx, usuario._id, 'revogar_consentimento_por_solicitacao', 'consentimentos', JSON.stringify({ tipo: consentimento.tipo, origem: 'solicitacao_lgpd', solicitacaoId: args.solicitacaoId }), consentimento._id.toString() ); } } // Log de atividade await registrarAtividade( ctx, usuario._id, 'responder_solicitacao_lgpd', 'solicitacoesLGPD', JSON.stringify({ solicitacaoId: args.solicitacaoId, status: args.status }), args.solicitacaoId.toString() ); // Notificações para o titular (email + chat) const usuarioTitular = await ctx.db.get(solicitacao.usuarioId); if (usuarioTitular) { let urlSistema = process.env.FRONTEND_URL || 'http://localhost:5173'; if (!urlSistema.match(/^https?:\/\//i)) { urlSistema = `http://${urlSistema}`; } const tipoSolicitacaoLabelMap: Record = { acesso: 'Acesso aos Dados', correcao: 'Correção de Dados', exclusao: 'Exclusão de Dados', portabilidade: 'Portabilidade dos Dados', revogacao_consentimento: 'Revogação de Consentimento', informacao_compartilhamento: 'Informação sobre Compartilhamento' }; const statusLabelMap: Record = { concluida: 'Concluída', rejeitada: 'Rejeitada', em_analise: 'Em Análise' }; const tipoSolicitacaoLabel = tipoSolicitacaoLabelMap[solicitacao.tipo] ?? solicitacao.tipo; const statusLabel = statusLabelMap[args.status] ?? args.status; const resumoResposta = args.resposta.length > 500 ? `${args.resposta.slice(0, 500)}...` : args.resposta; // Escolher template conforme o tipo const tipoToTemplate: Record = { acesso: 'lgpd_resposta_acesso', correcao: 'lgpd_resposta_correcao', exclusao: 'lgpd_resposta_exclusao', portabilidade: 'lgpd_resposta_portabilidade', revogacao_consentimento: 'lgpd_resposta_revogacao_consentimento', informacao_compartilhamento: 'lgpd_resposta_informacao_compartilhamento' }; const templateCodigo = tipoToTemplate[solicitacao.tipo] ?? 'lgpd_resposta_acesso'; // Email para o titular if (usuarioTitular.email) { try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: usuarioTitular.email, destinatarioId: usuarioTitular._id, templateCodigo, variaveis: { nomeTitular: usuarioTitular.nome, tipoSolicitacaoLabel, statusLabel, resumoResposta, urlPortalLGPD: `${urlSistema}/privacidade/meus-dados` }, enviadoPor: usuario._id }); } catch (error) { console.error(`Erro ao agendar email ${templateCodigo}:`, error); } } // Mensagem simples no chat entre TI (respondente) e o titular try { // Buscar conversa individual existente const conversas = await ctx.db .query('conversas') .filter((q) => q.eq(q.field('tipo'), 'individual')) .collect(); let conversaId: Id<'conversas'> | null = null; for (const conversa of conversas) { if ( conversa.participantes.length === 2 && conversa.participantes.includes(usuario._id) && conversa.participantes.includes(usuarioTitular._id) ) { conversaId = conversa._id; break; } } if (!conversaId) { conversaId = await ctx.db.insert('conversas', { tipo: 'individual', participantes: [usuario._id, usuarioTitular._id], criadoPor: usuario._id, criadoEm: Date.now() }); } await ctx.db.insert('mensagens', { conversaId, remetenteId: usuario._id, tipo: 'texto', conteudo: `Respondi sua solicitação LGPD (${tipoSolicitacaoLabel}) com status ${statusLabel}. Resumo: ${resumoResposta}`, enviadaEm: Date.now() }); } catch (error) { console.error('Erro ao criar mensagem de chat para resposta LGPD:', error); } } return { sucesso: true }; } }); /** * Cancelar solicitação LGPD (apenas pelo próprio usuário) */ export const cancelarSolicitacao = mutation({ args: { solicitacaoId: v.id('solicitacoesLGPD') }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const solicitacao = await ctx.db.get(args.solicitacaoId); if (!solicitacao) { throw new Error('Solicitação não encontrada'); } // Verificar se a solicitação pertence ao usuário if (solicitacao.usuarioId !== usuario._id) { throw new Error('Você não tem permissão para cancelar esta solicitação'); } // Só pode cancelar se estiver pendente ou em análise if (solicitacao.status !== 'pendente' && solicitacao.status !== 'em_analise') { throw new Error('Só é possível cancelar solicitações pendentes ou em análise'); } // Atualizar status para cancelada await ctx.db.patch(args.solicitacaoId, { status: 'cancelada' }); // Log de atividade await registrarAtividade( ctx, usuario._id, 'cancelar_solicitacao_lgpd', 'solicitacoesLGPD', JSON.stringify({ solicitacaoId: args.solicitacaoId }), args.solicitacaoId.toString() ); return { sucesso: true }; } }); /** * Excluir solicitação LGPD (apenas pelo próprio usuário e apenas se cancelada ou pendente) */ export const excluirSolicitacao = mutation({ args: { solicitacaoId: v.id('solicitacoesLGPD') }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const solicitacao = await ctx.db.get(args.solicitacaoId); if (!solicitacao) { throw new Error('Solicitação não encontrada'); } // Verificar se a solicitação pertence ao usuário if (solicitacao.usuarioId !== usuario._id) { throw new Error('Você não tem permissão para excluir esta solicitação'); } // Só pode excluir se estiver pendente ou cancelada if (solicitacao.status !== 'pendente' && solicitacao.status !== 'cancelada') { throw new Error('Só é possível excluir solicitações pendentes ou canceladas'); } // Excluir arquivo de resposta se existir if (solicitacao.arquivoResposta) { await ctx.storage.delete(solicitacao.arquivoResposta); } // Excluir a solicitação await ctx.db.delete(args.solicitacaoId); // Log de atividade await registrarAtividade( ctx, usuario._id, 'excluir_solicitacao_lgpd', 'solicitacoesLGPD', JSON.stringify({ solicitacaoId: args.solicitacaoId }), args.solicitacaoId.toString() ); return { sucesso: true }; } }); /** * Exportar dados do usuário (portabilidade) */ export const exportarDadosUsuario = query({ args: {}, returns: v.object({ dados: v.string() // JSON string com todos os dados do usuário }), handler: async (ctx) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Buscar todos os dados do usuário type DadosUsuario = { usuario: { nome: string; email: string; setor?: string; }; consentimentos: Array<{ tipo: string; aceito: boolean; versao: string; aceitoEm: number; revogadoEm?: number; }>; solicitacoes: Array<{ tipo: string; status: string; criadoEm: number; respondidoEm?: number; }>; funcionario?: { nome: string; matricula?: string; cpf: string; email: string; telefone: string; descricaoCargo?: string; }; }; const dadosUsuario: DadosUsuario = { usuario: { nome: usuario.nome, email: usuario.email, setor: usuario.setor }, consentimentos: [], solicitacoes: [] }; // Consentimentos const consentimentos = await ctx.db .query('consentimentos') .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) .collect(); dadosUsuario.consentimentos = consentimentos.map((c) => ({ tipo: c.tipo, aceito: c.aceito, versao: c.versao, aceitoEm: c.aceitoEm, revogadoEm: c.revogadoEm })); // Solicitações LGPD const solicitacoes = await ctx.db .query('solicitacoesLGPD') .withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id)) .collect(); dadosUsuario.solicitacoes = solicitacoes.map((s) => ({ tipo: s.tipo, status: s.status, criadoEm: s.criadoEm, respondidoEm: s.respondidoEm })); // Dados do funcionário (se houver) if (usuario.funcionarioId) { const funcionario = await ctx.db.get(usuario.funcionarioId); if (funcionario) { dadosUsuario.funcionario = { nome: funcionario.nome, matricula: funcionario.matricula, cpf: funcionario.cpf, email: funcionario.email, telefone: funcionario.telefone, descricaoCargo: funcionario.descricaoCargo }; } } return { dados: JSON.stringify(dadosUsuario, null, 2) }; } }); /** * Criar Registro de Operação de Tratamento (ROT) */ export const criarRegistroTratamento = mutation({ args: { finalidade: v.string(), baseLegal: v.string(), categoriasDados: v.array(v.string()), categoriasTitulares: v.array(v.string()), medidasSeguranca: v.array(v.string()), prazoRetencao: v.number(), compartilhamentoTerceiros: v.boolean(), terceiros: v.optional(v.array(v.string())), descricao: v.optional(v.string()) }, returns: v.object({ sucesso: v.boolean(), registroId: v.id('registrosTratamento') }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const agora = Date.now(); const registroId = await ctx.db.insert('registrosTratamento', { finalidade: args.finalidade, baseLegal: args.baseLegal, categoriasDados: args.categoriasDados, categoriasTitulares: args.categoriasTitulares, medidasSeguranca: args.medidasSeguranca, prazoRetencao: args.prazoRetencao, compartilhamentoTerceiros: args.compartilhamentoTerceiros, terceiros: args.terceiros, responsavel: usuario._id, descricao: args.descricao, criadoEm: agora, atualizadoEm: agora, ativo: true }); // Log de atividade await registrarAtividade( ctx, usuario._id, 'criar_rot', 'registrosTratamento', JSON.stringify({ finalidade: args.finalidade }), registroId.toString() ); return { sucesso: true, registroId }; } }); /** * Listar Registros de Tratamento */ export const listarRegistrosTratamento = query({ args: { ativo: v.optional(v.boolean()) }, returns: v.array( v.object({ _id: v.id('registrosTratamento'), finalidade: v.string(), baseLegal: v.string(), categoriasDados: v.array(v.string()), categoriasTitulares: v.array(v.string()), medidasSeguranca: v.array(v.string()), prazoRetencao: v.number(), compartilhamentoTerceiros: v.boolean(), terceiros: v.union(v.array(v.string()), v.null()), responsavelNome: v.string(), criadoEm: v.number(), atualizadoEm: v.number(), ativo: v.boolean() }) ), handler: async (ctx, args) => { let registros = await ctx.db.query('registrosTratamento').collect(); if (args.ativo !== undefined) { registros = registros.filter((r) => r.ativo === args.ativo); } // Enriquecer com nome do responsável const resultado = await Promise.all( registros.map(async (r) => { const responsavel = await ctx.db.get(r.responsavel); return { _id: r._id, finalidade: r.finalidade, baseLegal: r.baseLegal, categoriasDados: r.categoriasDados, categoriasTitulares: r.categoriasTitulares, medidasSeguranca: r.medidasSeguranca, prazoRetencao: r.prazoRetencao, compartilhamentoTerceiros: r.compartilhamentoTerceiros, terceiros: r.terceiros ?? null, responsavelNome: responsavel?.nome ?? 'Desconhecido', criadoEm: r.criadoEm, atualizadoEm: r.atualizadoEm, ativo: r.ativo }; }) ); return resultado; } }); /** * Obter configurações LGPD */ export const obterConfiguracaoLGPD = query({ args: {}, returns: v.union( v.object({ encarregadoNome: v.union(v.string(), v.null()), encarregadoEmail: v.union(v.string(), v.null()), encarregadoTelefone: v.union(v.string(), v.null()), encarregadoHorarioAtendimento: v.union(v.string(), v.null()), prazoRespostaPadrao: v.number(), diasAlertaVencimento: v.number(), termoObrigatorio: v.boolean(), versaoTermoAtual: v.string() }), v.null() ), handler: async (ctx) => { const config = await ctx.db .query('configuracaoLGPD') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); if (!config) { // Retornar valores padrão return { encarregadoNome: null, encarregadoEmail: null, encarregadoTelefone: null, encarregadoHorarioAtendimento: null, prazoRespostaPadrao: 15, diasAlertaVencimento: 3, termoObrigatorio: false, versaoTermoAtual: '1.0' }; } return { encarregadoNome: config.encarregadoNome ?? null, encarregadoEmail: config.encarregadoEmail ?? null, encarregadoTelefone: config.encarregadoTelefone ?? null, encarregadoHorarioAtendimento: config.encarregadoHorarioAtendimento ?? null, prazoRespostaPadrao: config.prazoRespostaPadrao, diasAlertaVencimento: config.diasAlertaVencimento, termoObrigatorio: config.termoObrigatorio, versaoTermoAtual: config.versaoTermoAtual }; } }); /** * Atualizar configurações LGPD (apenas TI) */ export const atualizarConfiguracaoLGPD = mutation({ args: { encarregadoNome: v.optional(v.string()), encarregadoEmail: v.optional(v.string()), encarregadoTelefone: v.optional(v.string()), encarregadoHorarioAtendimento: v.optional(v.string()), prazoRespostaPadrao: v.optional(v.number()), diasAlertaVencimento: v.optional(v.number()), termoObrigatorio: v.optional(v.boolean()), versaoTermoAtual: v.optional(v.string()) }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Buscar configuração ativa ou criar nova let config = await ctx.db .query('configuracaoLGPD') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); if (config) { // Desativar configuração antiga await ctx.db.patch(config._id, { ativo: false }); } // Buscar valores atuais para manter os que não foram atualizados const valoresAtuais = config || { encarregadoNome: undefined, encarregadoEmail: undefined, encarregadoTelefone: undefined, encarregadoHorarioAtendimento: undefined, prazoRespostaPadrao: 15, diasAlertaVencimento: 3, termoObrigatorio: false, versaoTermoAtual: '1.0' }; // Criar nova configuração await ctx.db.insert('configuracaoLGPD', { encarregadoNome: args.encarregadoNome ?? valoresAtuais.encarregadoNome ?? undefined, encarregadoEmail: args.encarregadoEmail ?? valoresAtuais.encarregadoEmail ?? undefined, encarregadoTelefone: args.encarregadoTelefone ?? valoresAtuais.encarregadoTelefone ?? undefined, encarregadoHorarioAtendimento: args.encarregadoHorarioAtendimento ?? valoresAtuais.encarregadoHorarioAtendimento ?? undefined, prazoRespostaPadrao: args.prazoRespostaPadrao ?? valoresAtuais.prazoRespostaPadrao, diasAlertaVencimento: args.diasAlertaVencimento ?? valoresAtuais.diasAlertaVencimento, termoObrigatorio: args.termoObrigatorio ?? valoresAtuais.termoObrigatorio ?? false, versaoTermoAtual: args.versaoTermoAtual ?? valoresAtuais.versaoTermoAtual ?? '1.0', ativo: true, atualizadoPor: usuario._id, atualizadoEm: Date.now() }); // Log de atividade await registrarAtividade( ctx, usuario._id, 'atualizar_config_lgpd', 'configuracaoLGPD', JSON.stringify(args), '' ); return { sucesso: true }; } }); /** * Obter estatísticas LGPD (apenas TI) */ export const obterEstatisticasLGPD = query({ args: {}, returns: v.object({ totalSolicitacoes: v.number(), solicitacoesPendentes: v.number(), solicitacoesVencendo: v.number(), solicitacoesPorTipo: v.record(v.string(), v.number()), totalConsentimentos: v.number(), consentimentosAtivos: v.number(), totalROTs: v.number(), rotsAtivos: v.number() }), handler: async (ctx) => { const solicitacoes = await ctx.db.query('solicitacoesLGPD').collect(); const consentimentos = await ctx.db.query('consentimentos').collect(); const rots = await ctx.db.query('registrosTratamento').collect(); const agora = Date.now(); const tresDias = 3 * 24 * 60 * 60 * 1000; const solicitacoesVencendo = solicitacoes.filter((s) => s.status === 'pendente' || s.status === 'em_analise' ? s.prazoResposta - agora <= tresDias && s.prazoResposta > agora : false ).length; const solicitacoesPorTipo: Record = {}; solicitacoes.forEach((s) => { solicitacoesPorTipo[s.tipo] = (solicitacoesPorTipo[s.tipo] || 0) + 1; }); const consentimentosAtivos = consentimentos.filter((c) => c.aceito && !c.revogadoEm).length; return { totalSolicitacoes: solicitacoes.length, solicitacoesPendentes: solicitacoes.filter((s) => s.status === 'pendente').length, solicitacoesVencendo, solicitacoesPorTipo, totalConsentimentos: consentimentos.length, consentimentosAtivos, totalROTs: rots.length, rotsAtivos: rots.filter((r) => r.ativo).length }; } });