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:
@@ -95,6 +95,34 @@
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function templateDisponivelParaCanal(
|
||||||
|
template: Doc<'templatesMensagens'>,
|
||||||
|
canalAtual: 'chat' | 'email' | 'ambos'
|
||||||
|
): boolean {
|
||||||
|
const categoria = template.categoria as 'email' | 'chat' | 'ambos' | undefined;
|
||||||
|
|
||||||
|
// Se não tiver categoria definida, considerar disponível para qualquer canal
|
||||||
|
if (!categoria) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canalAtual === 'ambos') {
|
||||||
|
// No modo "ambos", aceitar templates marcados para qualquer canal ou ambos
|
||||||
|
return categoria === 'chat' || categoria === 'email' || categoria === 'ambos';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para canal específico, aceitar templates daquele canal ou "ambos"
|
||||||
|
if (categoria === 'ambos') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoria === canalAtual;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatesParaCanal = $derived.by(() =>
|
||||||
|
templates.filter((t) => templateDisponivelParaCanal(t as Doc<'templatesMensagens'>, canal))
|
||||||
|
);
|
||||||
|
|
||||||
// Estados de carregamento e erro
|
// Estados de carregamento e erro
|
||||||
const carregandoTemplates = $derived(templatesQuery === undefined || templatesQuery === null);
|
const carregandoTemplates = $derived(templatesQuery === undefined || templatesQuery === null);
|
||||||
const carregandoUsuarios = $derived(usuariosQuery === undefined || usuariosQuery === null);
|
const carregandoUsuarios = $derived(usuariosQuery === undefined || usuariosQuery === null);
|
||||||
@@ -211,6 +239,17 @@
|
|||||||
return resultado;
|
return resultado;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Versão específica para CHAT: garante texto puro (sem HTML)
|
||||||
|
function renderizarTemplateChatLocal(
|
||||||
|
template: string,
|
||||||
|
variaveis: Record<string, string>
|
||||||
|
): string {
|
||||||
|
const textoComVariaveis = renderizarTemplate(template, variaveis);
|
||||||
|
// Remove quaisquer tags HTML que possam ter sido inseridas por engano
|
||||||
|
const textoPuro = textoComVariaveis.replace(/<[^>]*>/g, '');
|
||||||
|
return textoPuro;
|
||||||
|
}
|
||||||
|
|
||||||
// Função para mostrar mensagens
|
// Função para mostrar mensagens
|
||||||
function mostrarMensagem(tipo: 'success' | 'error' | 'info', texto: string) {
|
function mostrarMensagem(tipo: 'success' | 'error' | 'info', texto: string) {
|
||||||
mensagem = { tipo, texto };
|
mensagem = { tipo, texto };
|
||||||
@@ -885,10 +924,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (conversaId) {
|
if (conversaId) {
|
||||||
// Renderizar template com variáveis do destinatário
|
// Renderizar template com variáveis do destinatário (chat sempre em TEXTO PURO)
|
||||||
const mensagem =
|
const mensagem =
|
||||||
usarTemplate && templateSelecionado
|
usarTemplate && templateSelecionado
|
||||||
? renderizarTemplate(templateSelecionado.corpo, {
|
? renderizarTemplateChatLocal(templateSelecionado.corpo, {
|
||||||
nome: destinatario.nome,
|
nome: destinatario.nome,
|
||||||
matricula: destinatario.matricula || ''
|
matricula: destinatario.matricula || ''
|
||||||
})
|
})
|
||||||
@@ -1314,8 +1353,8 @@
|
|||||||
<option value="">Selecione um template</option>
|
<option value="">Selecione um template</option>
|
||||||
{#if carregandoTemplates}
|
{#if carregandoTemplates}
|
||||||
<option disabled>Carregando templates...</option>
|
<option disabled>Carregando templates...</option>
|
||||||
{:else if templates.length > 0}
|
{:else if templatesParaCanal.length > 0}
|
||||||
{#each templates as template (template._id)}
|
{#each templatesParaCanal as template (template._id)}
|
||||||
<option value={template._id}>
|
<option value={template._id}>
|
||||||
{template.nome}
|
{template.nome}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -203,6 +203,13 @@
|
|||||||
<option value="chat">Chat</option>
|
<option value="chat">Chat</option>
|
||||||
<option value="ambos">Ambos</option>
|
<option value="ambos">Ambos</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">
|
||||||
|
<span class="font-semibold">Chat:</span> usa o texto puro do corpo. <span
|
||||||
|
class="font-semibold">Email:</span
|
||||||
|
> usa uma versão HTML profissional gerada automaticamente com cabeçalho e assinatura SGSE.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -213,8 +220,16 @@
|
|||||||
id="corpo"
|
id="corpo"
|
||||||
bind:value={corpo}
|
bind:value={corpo}
|
||||||
class="textarea textarea-bordered h-40"
|
class="textarea textarea-bordered h-40"
|
||||||
placeholder="Digite o conteúdo da mensagem. Você pode usar {{variavel}} para valores dinâmicos."
|
placeholder="Digite o conteúdo em TEXTO. Você pode usar {{variavel}} para valores dinâmicos."
|
||||||
></textarea>
|
></textarea>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">
|
||||||
|
Este texto será usado diretamente nas mensagens de
|
||||||
|
<span class="font-semibold">chat</span>. Para
|
||||||
|
<span class="font-semibold">email</span>, o sistema gera automaticamente um layout HTML
|
||||||
|
padronizado com logo e assinatura.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
|||||||
@@ -180,6 +180,13 @@
|
|||||||
<option value="chat">Chat</option>
|
<option value="chat">Chat</option>
|
||||||
<option value="ambos">Ambos</option>
|
<option value="ambos">Ambos</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">
|
||||||
|
<span class="font-semibold">Chat:</span> usa o texto puro do corpo. <span class="font-semibold"
|
||||||
|
>Email:</span
|
||||||
|
> usa uma versão HTML profissional gerada automaticamente com cabeçalho e assinatura SGSE.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -190,8 +197,15 @@
|
|||||||
id="corpo"
|
id="corpo"
|
||||||
bind:value={corpo}
|
bind:value={corpo}
|
||||||
class="textarea textarea-bordered h-40"
|
class="textarea textarea-bordered h-40"
|
||||||
placeholder="Digite o conteúdo da mensagem. Você pode usar {{variavel}} para valores dinâmicos."
|
placeholder="Digite o conteúdo em TEXTO. Você pode usar {{variavel}} para valores dinâmicos."
|
||||||
></textarea>
|
></textarea>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">
|
||||||
|
Este texto será usado diretamente nas mensagens de <span class="font-semibold">chat</span>.
|
||||||
|
Para <span class="font-semibold">email</span>, o sistema gera automaticamente um layout HTML
|
||||||
|
padronizado com logo e assinatura.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
|||||||
@@ -170,6 +170,20 @@ async function registrarNotificacoes(
|
|||||||
criadaEm: Date.now(),
|
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)
|
// Notificar responsável (se houver)
|
||||||
if (ticket.responsavelId && ticket.responsavelId !== ticket.solicitanteId) {
|
if (ticket.responsavelId && ticket.responsavelId !== ticket.solicitanteId) {
|
||||||
const responsavel = await ctx.db.get(ticket.responsavelId);
|
const responsavel = await ctx.db.get(ticket.responsavelId);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { mutation, query, internalMutation, internalQuery, action } from "./_generated/server";
|
import { mutation, query, internalMutation, internalQuery, action } from "./_generated/server";
|
||||||
import { internal, api } from "./_generated/api";
|
import { internal, api } from "./_generated/api";
|
||||||
import { renderizarTemplate } from "./templatesMensagens";
|
import {
|
||||||
import { wrapEmailHTML, textToHTML } from "./utils/emailTemplateWrapper";
|
renderizarTemplateEmailFromDoc,
|
||||||
|
type VariaveisTemplate,
|
||||||
|
} from "./templatesMensagens";
|
||||||
import type { Doc, Id } from "./_generated/dataModel";
|
import type { Doc, Id } from "./_generated/dataModel";
|
||||||
|
|
||||||
// ========== INTERNAL QUERIES ==========
|
// ========== INTERNAL QUERIES ==========
|
||||||
@@ -212,37 +214,24 @@ export const enviarEmailComTemplate = action({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Renderizar template com variáveis
|
// Renderizar template com variáveis
|
||||||
const variaveisTemplate = args.variaveis || {};
|
const variaveisTemplate: VariaveisTemplate = args.variaveis ?? {};
|
||||||
|
|
||||||
// Garantir que urlSistema sempre tenha protocolo se presente
|
// 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}`;
|
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
|
const emailRenderizado = renderizarTemplateEmailFromDoc(template, variaveisTemplate);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enfileirar email via mutation
|
// Enfileirar email via mutation
|
||||||
const emailId: Id<"notificacoesEmail"> = await ctx.runMutation(api.email.enfileirarEmail, {
|
const emailId: Id<"notificacoesEmail"> = await ctx.runMutation(api.email.enfileirarEmail, {
|
||||||
destinatario: args.destinatario,
|
destinatario: args.destinatario,
|
||||||
destinatarioId: args.destinatarioId,
|
destinatarioId: args.destinatarioId,
|
||||||
assunto: tituloRenderizado,
|
assunto: emailRenderizado.titulo,
|
||||||
corpo: corpoHTML, // Usar HTML completo
|
corpo: emailRenderizado.html, // HTML completo com wrapper
|
||||||
templateId: template._id, // template._id sempre existe se template não é null
|
templateId: template._id, // template._id sempre existe se template não é null
|
||||||
enviadoPor: args.enviadoPor,
|
enviadoPor: args.enviadoPor,
|
||||||
agendadaPara: args.agendadaPara,
|
agendadaPara: args.agendadaPara,
|
||||||
|
|||||||
@@ -363,10 +363,39 @@ export const verificarAlertasInternal = internalMutation({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Enviar email se configurado (integração com sistema de email)
|
// Enviar email se configurado (usar template HTML padronizado)
|
||||||
// if (alerta.notifyByEmail) {
|
if (alerta.notifyByEmail) {
|
||||||
// await enviarEmailAlerta(alerta, metricValue);
|
// 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,71 @@ export function renderizarTemplate(template: string, variaveis: Record<string, s
|
|||||||
return resultado;
|
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)
|
* Criar templates padrão do sistema (chamado no seed)
|
||||||
*/
|
*/
|
||||||
@@ -432,6 +497,20 @@ export const criarTemplatesPadrao = mutation({
|
|||||||
+ "</div></body></html>",
|
+ "</div></body></html>",
|
||||||
variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"],
|
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",
|
codigo: "ausencia_solicitada",
|
||||||
nome: "Ausência Solicitada",
|
nome: "Ausência Solicitada",
|
||||||
|
|||||||
Reference in New Issue
Block a user