feat: enhance LGPD request handling with email notifications and response templates; update frontend filters for improved user experience
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user