feat: enhance LGPD request handling with email notifications and response templates; update frontend filters for improved user experience

This commit is contained in:
2025-12-04 05:13:43 -03:00
parent 4a662c08a0
commit a3d9e782af
4 changed files with 333 additions and 46 deletions

View File

@@ -4,6 +4,7 @@ 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
@@ -275,6 +276,43 @@ export const criarSolicitacao = mutation({
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<string, string> = {
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 };
}
});
@@ -586,6 +624,7 @@ export const responderSolicitacao = mutation({
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,
@@ -594,6 +633,48 @@ export const responderSolicitacao = mutation({
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,
@@ -604,6 +685,107 @@ export const responderSolicitacao = mutation({
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<string, string> = {
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<string, string> = {
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<string, string> = {
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 };
}
});

View File

@@ -549,6 +549,107 @@ export const criarTemplatesPadrao = mutation({
categoria: 'email' as const,
tags: ['monitoramento', 'alerta', 'sistema', 'ti']
},
// ===================== LGPD =====================
{
codigo: 'lgpd_solicitacao_criada',
nome: 'LGPD - Solicitação Criada',
titulo: 'Recebemos sua solicitação LGPD ({{tipoSolicitacaoLabel}})',
corpo:
'Olá {{nomeTitular}},\n\n' +
'Recebemos sua solicitação LGPD do tipo "{{tipoSolicitacaoLabel}}".\n\n' +
'Prazo estimado para resposta: até {{prazoResposta}}.\n\n' +
'Você pode acompanhar o andamento acessando: {{urlPortalLGPD}}.\n\n' +
'Equipe de Proteção de Dados / TI.',
variaveis: ['nomeTitular', 'tipoSolicitacaoLabel', 'prazoResposta', 'urlPortalLGPD'],
categoria: 'email' as const,
tags: ['lgpd', 'solicitacao', 'dados_pessoais']
},
{
codigo: 'lgpd_resposta_acesso',
nome: 'LGPD - Resposta Acesso',
titulo: 'Resposta à sua solicitação LGPD - Acesso aos Dados',
corpo:
'Olá {{nomeTitular}},\n\n' +
'Sua solicitação LGPD de Acesso aos Dados foi marcada como {{statusLabel}}.\n\n' +
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
'Equipe de Proteção de Dados / TI.',
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
categoria: 'email' as const,
tags: ['lgpd', 'acesso', 'dados_pessoais']
},
{
codigo: 'lgpd_resposta_correcao',
nome: 'LGPD - Resposta Correção',
titulo: 'Resposta à sua solicitação LGPD - Correção de Dados',
corpo:
'Olá {{nomeTitular}},\n\n' +
'Sua solicitação LGPD de Correção de Dados foi marcada como {{statusLabel}}.\n\n' +
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
'Equipe de Proteção de Dados / TI.',
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
categoria: 'email' as const,
tags: ['lgpd', 'correcao', 'dados_pessoais']
},
{
codigo: 'lgpd_resposta_exclusao',
nome: 'LGPD - Resposta Exclusão',
titulo: 'Resposta à sua solicitação LGPD - Exclusão de Dados',
corpo:
'Olá {{nomeTitular}},\n\n' +
'Sua solicitação LGPD de Exclusão de Dados foi marcada como {{statusLabel}}.\n\n' +
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
'Equipe de Proteção de Dados / TI.',
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
categoria: 'email' as const,
tags: ['lgpd', 'exclusao', 'dados_pessoais']
},
{
codigo: 'lgpd_resposta_portabilidade',
nome: 'LGPD - Resposta Portabilidade',
titulo: 'Resposta à sua solicitação LGPD - Portabilidade dos Dados',
corpo:
'Olá {{nomeTitular}},\n\n' +
'Sua solicitação LGPD de Portabilidade dos Dados foi marcada como {{statusLabel}}.\n\n' +
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
'Caso tenha recebido um arquivo anexo, ele contém os dados em formato portável.\n\n' +
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
'Equipe de Proteção de Dados / TI.',
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
categoria: 'email' as const,
tags: ['lgpd', 'portabilidade', 'dados_pessoais']
},
{
codigo: 'lgpd_resposta_revogacao_consentimento',
nome: 'LGPD - Resposta Revogação de Consentimento',
titulo: 'Confirmação de Revogação de Consentimento',
corpo:
'Olá {{nomeTitular}},\n\n' +
'Sua solicitação LGPD de Revogação de Consentimento foi marcada como {{statusLabel}}.\n\n' +
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
'Todos os consentimentos ativos associados à sua conta foram marcados como revogados a partir desta data.\n\n' +
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
'Equipe de Proteção de Dados / TI.',
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
categoria: 'email' as const,
tags: ['lgpd', 'revogacao_consentimento', 'dados_pessoais']
},
{
codigo: 'lgpd_resposta_informacao_compartilhamento',
nome: 'LGPD - Resposta Informação sobre Compartilhamento',
titulo: 'Resposta à sua solicitação LGPD - Informação sobre Compartilhamento',
corpo:
'Olá {{nomeTitular}},\n\n' +
'Sua solicitação LGPD de Informação sobre Compartilhamento foi marcada como {{statusLabel}}.\n\n' +
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
'Equipe de Proteção de Dados / TI.',
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
categoria: 'email' as const,
tags: ['lgpd', 'informacao_compartilhamento', 'dados_pessoais']
},
{
codigo: 'ausencia_solicitada',
nome: 'Ausência Solicitada',