1097 lines
29 KiB
TypeScript
1097 lines
29 KiB
TypeScript
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';
|
|
|
|
/**
|
|
* 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()
|
|
);
|
|
|
|
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<SolicitacaoEnriquecida> => {
|
|
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<SolicitacaoEnriquecida> => 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');
|
|
}
|
|
|
|
await ctx.db.patch(args.solicitacaoId, {
|
|
status: args.status,
|
|
resposta: args.resposta,
|
|
arquivoResposta: args.arquivoResposta,
|
|
respondidoPor: usuario._id,
|
|
respondidoEm: Date.now()
|
|
});
|
|
|
|
// Log de atividade
|
|
await registrarAtividade(
|
|
ctx,
|
|
usuario._id,
|
|
'responder_solicitacao_lgpd',
|
|
'solicitacoesLGPD',
|
|
JSON.stringify({ solicitacaoId: args.solicitacaoId, status: args.status }),
|
|
args.solicitacaoId.toString()
|
|
);
|
|
|
|
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<string, number> = {};
|
|
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
|
|
};
|
|
}
|
|
});
|