feat: implement template filtering for notifications based on channel type and enhance email rendering with HTML wrapper, ensuring chat messages are sent as plain text

This commit is contained in:
2025-12-01 09:50:53 -03:00
parent d9e78079c8
commit 4c2d12f443
7 changed files with 213 additions and 34 deletions

View File

@@ -170,6 +170,20 @@ async function registrarNotificacoes(
criadaEm: Date.now(),
});
// Se o ticket estiver associado a uma conversa, registrar também uma mensagem de chat
// Isso garante o "duplo canal": email + chat para notificações importantes.
if (ticket.conversaId) {
const conteudoChat = mensagem.length > 0 ? `${titulo}: ${mensagem}` : titulo;
await ctx.db.insert("mensagens", {
conversaId: ticket.conversaId,
remetenteId: usuarioEvento,
tipo: "texto",
conteudo: conteudoChat,
enviadaEm: Date.now(),
});
}
// Notificar responsável (se houver)
if (ticket.responsavelId && ticket.responsavelId !== ticket.solicitanteId) {
const responsavel = await ctx.db.get(ticket.responsavelId);

View File

@@ -1,8 +1,10 @@
import { v } from "convex/values";
import { mutation, query, internalMutation, internalQuery, action } from "./_generated/server";
import { internal, api } from "./_generated/api";
import { renderizarTemplate } from "./templatesMensagens";
import { wrapEmailHTML, textToHTML } from "./utils/emailTemplateWrapper";
import {
renderizarTemplateEmailFromDoc,
type VariaveisTemplate,
} from "./templatesMensagens";
import type { Doc, Id } from "./_generated/dataModel";
// ========== INTERNAL QUERIES ==========
@@ -212,37 +214,24 @@ export const enviarEmailComTemplate = action({
}
// Renderizar template com variáveis
const variaveisTemplate = args.variaveis || {};
const variaveisTemplate: VariaveisTemplate = args.variaveis ?? {};
// Garantir que urlSistema sempre tenha protocolo se presente
if (variaveisTemplate.urlSistema && !variaveisTemplate.urlSistema.match(/^https?:\/\//i)) {
if (
typeof variaveisTemplate.urlSistema === "string" &&
!variaveisTemplate.urlSistema.match(/^https?:\/\//i)
) {
variaveisTemplate.urlSistema = `http://${variaveisTemplate.urlSistema}`;
}
const tituloRenderizado = renderizarTemplate(template.titulo, variaveisTemplate);
const corpoRenderizado = renderizarTemplate(template.corpo, variaveisTemplate);
// Usar htmlCorpo se disponível, senão gerar do corpo
let corpoHTML = template.htmlCorpo;
if (corpoHTML) {
// Renderizar variáveis no HTML
corpoHTML = renderizarTemplate(corpoHTML, variaveisTemplate);
} else {
// Gerar HTML do corpo renderizado
if (corpoRenderizado.includes("<") && corpoRenderizado.includes(">")) {
corpoHTML = wrapEmailHTML(corpoRenderizado, tituloRenderizado);
} else {
const corpoHTMLFormatado = textToHTML(corpoRenderizado);
corpoHTML = wrapEmailHTML(corpoHTMLFormatado, tituloRenderizado);
}
}
const emailRenderizado = renderizarTemplateEmailFromDoc(template, variaveisTemplate);
// Enfileirar email via mutation
const emailId: Id<"notificacoesEmail"> = await ctx.runMutation(api.email.enfileirarEmail, {
destinatario: args.destinatario,
destinatarioId: args.destinatarioId,
assunto: tituloRenderizado,
corpo: corpoHTML, // Usar HTML completo
assunto: emailRenderizado.titulo,
corpo: emailRenderizado.html, // HTML completo com wrapper
templateId: template._id, // template._id sempre existe se template não é null
enviadoPor: args.enviadoPor,
agendadaPara: args.agendadaPara,

View File

@@ -363,10 +363,39 @@ export const verificarAlertasInternal = internalMutation({
}
}
// TODO: Enviar email se configurado (integração com sistema de email)
// if (alerta.notifyByEmail) {
// await enviarEmailAlerta(alerta, metricValue);
// }
// Enviar email se configurado (usar template HTML padronizado)
if (alerta.notifyByEmail) {
// Buscar usuários administradores/TI para receber o alerta por email
const rolesAdminOuTi = await ctx.db
.query('roles')
.filter((q) => q.lte(q.field('nivel'), 1))
.collect();
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
const usuarios = await ctx.db.query('usuarios').collect();
const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId) && !!u.email);
for (const usuario of usuariosTI) {
const email = usuario.email;
if (!email) continue;
// Montar variáveis para template de alerta de sistema
const variaveisEmail = {
destinatarioNome: usuario.nome,
metricName: alerta.metricName,
metricValue: metricValue.toFixed(2),
threshold: alerta.threshold.toString()
};
await ctx.scheduler.runAfter(0, internal.email.enviarEmailComTemplate, {
destinatario: email,
destinatarioId: usuario._id,
templateCodigo: 'monitoramento_alerta_sistema',
variaveis: variaveisEmail,
enviadoPor: usuario._id
});
}
}
}
}

View File

@@ -224,6 +224,71 @@ export function renderizarTemplate(template: string, variaveis: Record<string, s
return resultado;
}
export type VariaveisTemplate = Record<string, string>;
export interface EmailRenderizado {
titulo: string;
html: string;
}
/**
* Renderizar template para EMAIL (HTML padronizado)
* - Usa `htmlCorpo` se existir, senão gera HTML a partir de `corpo` (texto ou HTML simples)
* - Sempre aplica o wrapper visual de email
*/
export function renderizarTemplateEmailFromDoc(
template: Doc<"templatesMensagens">,
variaveis: VariaveisTemplate,
): EmailRenderizado {
const variaveisTemplate: VariaveisTemplate = { ...variaveis };
const tituloRenderizado = renderizarTemplate(template.titulo, variaveisTemplate);
// Base para o corpo: se existir htmlCorpo usamos ele, senão usamos corpo
const baseCorpo = template.htmlCorpo ?? template.corpo ?? "";
const corpoRenderizado = renderizarTemplate(baseCorpo, variaveisTemplate);
let htmlFinal: string;
if (template.htmlCorpo) {
// htmlCorpo já é HTML completo de email (com ou sem wrapper) apenas aplica variáveis
htmlFinal = corpoRenderizado.includes("<html")
? corpoRenderizado
: wrapEmailHTML(corpoRenderizado, tituloRenderizado);
} else {
// corpo pode ser texto puro ou HTML simples sempre gera HTML padronizado
if (corpoRenderizado.includes("<") && corpoRenderizado.includes(">")) {
htmlFinal = wrapEmailHTML(corpoRenderizado, tituloRenderizado);
} else {
const corpoHTML = textToHTML(corpoRenderizado);
htmlFinal = wrapEmailHTML(corpoHTML, tituloRenderizado);
}
}
return {
titulo: tituloRenderizado,
html: htmlFinal,
};
}
/**
* Renderizar template para CHAT (texto puro)
* - Usa sempre `corpo` como fonte
* - Remove quaisquer tags HTML residuais
*/
export function renderizarTemplateChatFromDoc(
template: Doc<"templatesMensagens">,
variaveis: VariaveisTemplate,
): string {
const corpoBase = template.corpo ?? "";
const textoComVariaveis = renderizarTemplate(corpoBase, variaveis);
// Garantir texto puro para o chat (sem tags HTML)
const textoPuro = textoComVariaveis.replace(/<[^>]*>/g, "");
return textoPuro;
}
/**
* Criar templates padrão do sistema (chamado no seed)
*/
@@ -432,6 +497,20 @@ export const criarTemplatesPadrao = mutation({
+ "</div></body></html>",
variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"],
},
{
codigo: "monitoramento_alerta_sistema",
nome: "Alerta de Sistema (Monitoramento)",
titulo: "⚠️ Alerta de Sistema: {{metricName}}",
corpo:
"Olá {{destinatarioNome}},\n\n" +
"A métrica {{metricName}} atingiu o valor {{metricValue}} (limite configurado: {{threshold}}).\n\n" +
"Recomenda-se verificar o painel de monitoramento do SGSE para detalhes adicionais e, se necessário, " +
"executar ações corretivas.\n\n" +
"Esta é uma notificação automática do sistema de monitoramento SGSE.",
variaveis: ["destinatarioNome", "metricName", "metricValue", "threshold"],
categoria: "email" as const,
tags: ["monitoramento", "alerta", "sistema", "ti"],
},
{
codigo: "ausencia_solicitada",
nome: "Ausência Solicitada",