diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte
index 498b105..39cef46 100644
--- a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte
+++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte
@@ -134,6 +134,9 @@
let processando = $state(false);
let criandoTemplates = $state(false);
let progressoEnvio = $state({ total: 0, enviados: 0, falhas: 0 });
+
+ // Aba ativa
+ let abaAtiva = $state<'enviar' | 'templates' | 'agendamentos'>('enviar');
// Estrutura de dados para logs de envio
type StatusLog = 'sucesso' | 'erro' | 'fila' | 'info' | 'enviando';
@@ -1173,6 +1176,36 @@
{/if}
+
+
diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/+page.svelte
new file mode 100644
index 0000000..eaca790
--- /dev/null
+++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/+page.svelte
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
+
Gerenciar Templates
+
Criar, editar e excluir templates de emails e mensagens
+
+
+
+
+
+
+ Voltar
+
+
+
+
+ {#if mensagem}
+
+ {mensagem.texto}
+ (mensagem = null)}>
+ ✕
+
+
+ {/if}
+
+
+
+
+
+
+
+ Buscar
+
+
+
+
+
+ Categoria
+
+
+ Todas
+ Email
+ Chat
+ Ambos
+
+
+
+
+
+
+
+
+
+
+
+ {#if templatesFiltrados.length === 0}
+
+
Nenhum template encontrado.
+
+ {:else}
+
+
+
+
+ Código
+ Nome
+ Tipo
+ Categoria
+ Variáveis
+ Ações
+
+
+
+ {#each templatesFiltrados as template (template._id)}
+
+
+ {template.codigo}
+
+
+ {template.nome}
+ {template.titulo}
+
+
+ {#if template.tipo === 'sistema'}
+ Sistema
+ {:else}
+ Customizado
+ {/if}
+
+
+ {#if template.categoria}
+ {template.categoria}
+ {:else}
+ -
+ {/if}
+
+
+ {#if template.variaveis && template.variaveis.length > 0}
+
+ {#each template.variaveis.slice(0, 3) as var}
+ {{var}}
+ {/each}
+ {#if template.variaveis.length > 3}
+ +{template.variaveis.length - 3}
+ {/if}
+
+ {:else}
+ -
+ {/if}
+
+
+
+
+
+
+
+
+ {#if template.tipo === 'customizado'}
+
excluirTemplate(template._id)}
+ disabled={processando}
+ title="Excluir"
+ >
+
+
+
+
+ {/if}
+
+
+
+ {/each}
+
+
+
+ {/if}
+
+
+
+
diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts
index 770c2b4..e6d551e 100644
--- a/packages/backend/convex/_generated/api.d.ts
+++ b/packages/backend/convex/_generated/api.d.ts
@@ -57,7 +57,10 @@ import type * as templatesMensagens from "../templatesMensagens.js";
import type * as times from "../times.js";
import type * as todos from "../todos.js";
import type * as usuarios from "../usuarios.js";
+import type * as utils_chatTemplateWrapper from "../utils/chatTemplateWrapper.js";
+import type * as utils_emailTemplateWrapper from "../utils/emailTemplateWrapper.js";
import type * as utils_getClientIP from "../utils/getClientIP.js";
+import type * as utils_scanEmailSenders from "../utils/scanEmailSenders.js";
import type * as verificarMatriculas from "../verificarMatriculas.js";
import type {
@@ -116,7 +119,10 @@ declare const fullApi: ApiFromModules<{
times: typeof times;
todos: typeof todos;
usuarios: typeof usuarios;
+ "utils/chatTemplateWrapper": typeof utils_chatTemplateWrapper;
+ "utils/emailTemplateWrapper": typeof utils_emailTemplateWrapper;
"utils/getClientIP": typeof utils_getClientIP;
+ "utils/scanEmailSenders": typeof utils_scanEmailSenders;
verificarMatriculas: typeof verificarMatriculas;
}>;
diff --git a/packages/backend/convex/ausencias.ts b/packages/backend/convex/ausencias.ts
index c48a5d0..d3981fd 100644
--- a/packages/backend/convex/ausencias.ts
+++ b/packages/backend/convex/ausencias.ts
@@ -358,20 +358,45 @@ export const criarSolicitacao = mutation({
.first();
if (gestorUsuario && funcionarioUsuario) {
- // Enviar email ao gestor
- await ctx.runMutation(api.email.enfileirarEmail, {
- destinatario: gestorUsuario.email,
- destinatarioId: gestorId,
- assunto: `Nova Solicitação de Ausência - ${funcionario.nome}`,
- corpo: `
Olá ${gestorUsuario.nome},
-
O funcionário ${funcionario.nome} solicitou uma ausência:
-
- Período: ${new Date(args.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(args.dataFim).toLocaleDateString("pt-BR")}
- Motivo: ${args.motivo}
-
-
Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.
`,
- enviadoPor: funcionarioUsuario._id,
- });
+ // Obter URL do sistema
+ let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173";
+ if (!urlSistema.match(/^https?:\/\//i)) {
+ urlSistema = `http://${urlSistema}`;
+ }
+
+ // Enviar email ao gestor usando template
+ try {
+ await ctx.runAction(api.email.enviarEmailComTemplate, {
+ destinatario: gestorUsuario.email,
+ destinatarioId: gestorId,
+ templateCodigo: "ausencia_solicitada",
+ variaveis: {
+ gestorNome: gestorUsuario.nome,
+ funcionarioNome: funcionario.nome,
+ dataInicio: new Date(args.dataInicio).toLocaleDateString("pt-BR"),
+ dataFim: new Date(args.dataFim).toLocaleDateString("pt-BR"),
+ motivo: args.motivo,
+ urlSistema,
+ },
+ enviadoPor: funcionarioUsuario._id,
+ });
+ } catch (error) {
+ // Fallback para envio direto se template não existir
+ console.warn("Template ausencia_solicitada não encontrado, usando envio direto:", error);
+ await ctx.runMutation(api.email.enfileirarEmail, {
+ destinatario: gestorUsuario.email,
+ destinatarioId: gestorId,
+ assunto: `Nova Solicitação de Ausência - ${funcionario.nome}`,
+ corpo: `
Olá ${gestorUsuario.nome},
+
O funcionário ${funcionario.nome} solicitou uma ausência:
+
+ Período: ${new Date(args.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(args.dataFim).toLocaleDateString("pt-BR")}
+ Motivo: ${args.motivo}
+
+
Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.
`,
+ enviadoPor: funcionarioUsuario._id,
+ });
+ }
// Criar ou obter conversa entre gestor e funcionário
const conversasExistentes = await ctx.db
@@ -475,19 +500,44 @@ export const aprovar = mutation({
const gestorUsuario = await ctx.db.get(args.gestorId);
if (gestorUsuario) {
- // Enviar email ao funcionário
- await ctx.runMutation(api.email.enfileirarEmail, {
- destinatario: funcionarioUsuario.email,
- destinatarioId: funcionarioUsuario._id,
- assunto: "Solicitação de Ausência Aprovada",
- corpo: `
Olá ${funcionarioUsuario.nome},
-
Sua solicitação de ausência foi aprovada pelo gestor ${gestorUsuario.nome}:
-
- Período: ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}
- Motivo: ${solicitacao.motivo}
- `,
- enviadoPor: args.gestorId,
- });
+ // Obter URL do sistema
+ let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173";
+ if (!urlSistema.match(/^https?:\/\//i)) {
+ urlSistema = `http://${urlSistema}`;
+ }
+
+ // Enviar email ao funcionário usando template
+ try {
+ await ctx.runAction(api.email.enviarEmailComTemplate, {
+ destinatario: funcionarioUsuario.email,
+ destinatarioId: funcionarioUsuario._id,
+ templateCodigo: "ausencia_aprovada",
+ variaveis: {
+ funcionarioNome: funcionarioUsuario.nome,
+ gestorNome: gestorUsuario.nome,
+ dataInicio: new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR"),
+ dataFim: new Date(solicitacao.dataFim).toLocaleDateString("pt-BR"),
+ motivo: solicitacao.motivo,
+ urlSistema,
+ },
+ enviadoPor: args.gestorId,
+ });
+ } catch (error) {
+ // Fallback para envio direto se template não existir
+ console.warn("Template ausencia_aprovada não encontrado, usando envio direto:", error);
+ await ctx.runMutation(api.email.enfileirarEmail, {
+ destinatario: funcionarioUsuario.email,
+ destinatarioId: funcionarioUsuario._id,
+ assunto: "Solicitação de Ausência Aprovada",
+ corpo: `
Olá ${funcionarioUsuario.nome},
+
Sua solicitação de ausência foi aprovada pelo gestor ${gestorUsuario.nome}:
+
+ Período: ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}
+ Motivo: ${solicitacao.motivo}
+ `,
+ enviadoPor: args.gestorId,
+ });
+ }
// Criar ou obter conversa
const conversasExistentes = await ctx.db
@@ -593,20 +643,46 @@ export const reprovar = mutation({
const gestorUsuario = await ctx.db.get(args.gestorId);
if (gestorUsuario) {
- // Enviar email ao funcionário
- await ctx.runMutation(api.email.enfileirarEmail, {
- destinatario: funcionarioUsuario.email,
- destinatarioId: funcionarioUsuario._id,
- assunto: "Solicitação de Ausência Reprovada",
- corpo: `
Olá ${funcionarioUsuario.nome},
-
Sua solicitação de ausência foi reprovada pelo gestor ${gestorUsuario.nome}:
-
- Período: ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}
- Motivo: ${solicitacao.motivo}
- Motivo da Reprovação: ${args.motivoReprovacao}
- `,
- enviadoPor: args.gestorId,
- });
+ // Obter URL do sistema
+ let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173";
+ if (!urlSistema.match(/^https?:\/\//i)) {
+ urlSistema = `http://${urlSistema}`;
+ }
+
+ // Enviar email ao funcionário usando template
+ try {
+ await ctx.runAction(api.email.enviarEmailComTemplate, {
+ destinatario: funcionarioUsuario.email,
+ destinatarioId: funcionarioUsuario._id,
+ templateCodigo: "ausencia_reprovada",
+ variaveis: {
+ funcionarioNome: funcionarioUsuario.nome,
+ gestorNome: gestorUsuario.nome,
+ dataInicio: new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR"),
+ dataFim: new Date(solicitacao.dataFim).toLocaleDateString("pt-BR"),
+ motivo: solicitacao.motivo,
+ motivoReprovacao: args.motivoReprovacao,
+ urlSistema,
+ },
+ enviadoPor: args.gestorId,
+ });
+ } catch (error) {
+ // Fallback para envio direto se template não existir
+ console.warn("Template ausencia_reprovada não encontrado, usando envio direto:", error);
+ await ctx.runMutation(api.email.enfileirarEmail, {
+ destinatario: funcionarioUsuario.email,
+ destinatarioId: funcionarioUsuario._id,
+ assunto: "Solicitação de Ausência Reprovada",
+ corpo: `
Olá ${funcionarioUsuario.nome},
+
Sua solicitação de ausência foi reprovada pelo gestor ${gestorUsuario.nome}:
+
+ Período: ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}
+ Motivo: ${solicitacao.motivo}
+ Motivo da Reprovação: ${args.motivoReprovacao}
+ `,
+ enviadoPor: args.gestorId,
+ });
+ }
// Criar ou obter conversa
const conversasExistentes = await ctx.db
diff --git a/packages/backend/convex/chamados.ts b/packages/backend/convex/chamados.ts
index f25df21..f0a9776 100644
--- a/packages/backend/convex/chamados.ts
+++ b/packages/backend/convex/chamados.ts
@@ -121,15 +121,38 @@ async function registrarNotificacoes(
) {
const { ticket, titulo, mensagem, usuarioEvento } = params;
+ // Obter URL do sistema
+ let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173";
+ if (!urlSistema.match(/^https?:\/\//i)) {
+ urlSistema = `http://${urlSistema}`;
+ }
+
// Notificar solicitante
if (ticket.solicitanteEmail) {
- await ctx.runMutation(api.email.enfileirarEmail, {
- destinatario: ticket.solicitanteEmail,
- destinatarioId: ticket.solicitanteId,
- assunto: `${titulo} - Chamado ${ticket.numero}`,
- corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`,
- enviadoPor: usuarioEvento,
- });
+ // Tentar usar template, senão usar envio direto
+ try {
+ await ctx.runAction(api.email.enviarEmailComTemplate, {
+ destinatario: ticket.solicitanteEmail,
+ destinatarioId: ticket.solicitanteId,
+ templateCodigo: "chamado_atualizado",
+ variaveis: {
+ solicitante: ticket.solicitanteNome || "Usuário",
+ numeroTicket: ticket.numero,
+ mensagem: mensagem,
+ urlSistema,
+ },
+ enviadoPor: usuarioEvento,
+ });
+ } catch (error) {
+ // Fallback para envio direto
+ await ctx.runMutation(api.email.enfileirarEmail, {
+ destinatario: ticket.solicitanteEmail,
+ destinatarioId: ticket.solicitanteId,
+ assunto: `${titulo} - Chamado ${ticket.numero}`,
+ corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`,
+ enviadoPor: usuarioEvento,
+ });
+ }
}
await ctx.db.insert("notificacoes", {
@@ -147,13 +170,30 @@ async function registrarNotificacoes(
if (ticket.responsavelId && ticket.responsavelId !== ticket.solicitanteId) {
const responsavel = await ctx.db.get(ticket.responsavelId);
if (responsavel?.email) {
- await ctx.runMutation(api.email.enfileirarEmail, {
- destinatario: responsavel.email,
- destinatarioId: ticket.responsavelId,
- assunto: `${titulo} - Chamado ${ticket.numero}`,
- corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`,
- enviadoPor: usuarioEvento,
- });
+ // Tentar usar template, senão usar envio direto
+ try {
+ await ctx.runAction(api.email.enviarEmailComTemplate, {
+ destinatario: responsavel.email,
+ destinatarioId: ticket.responsavelId,
+ templateCodigo: "chamado_atualizado",
+ variaveis: {
+ solicitante: ticket.solicitanteNome || "Usuário",
+ numeroTicket: ticket.numero,
+ mensagem: mensagem,
+ urlSistema,
+ },
+ enviadoPor: usuarioEvento,
+ });
+ } catch (error) {
+ // Fallback para envio direto
+ await ctx.runMutation(api.email.enfileirarEmail, {
+ destinatario: responsavel.email,
+ destinatarioId: ticket.responsavelId,
+ assunto: `${titulo} - Chamado ${ticket.numero}`,
+ corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`,
+ enviadoPor: usuarioEvento,
+ });
+ }
}
await ctx.db.insert("notificacoes", {
diff --git a/packages/backend/convex/email.ts b/packages/backend/convex/email.ts
index c136376..b178228 100644
--- a/packages/backend/convex/email.ts
+++ b/packages/backend/convex/email.ts
@@ -2,6 +2,7 @@ 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 type { Doc, Id } from "./_generated/dataModel";
// ========== INTERNAL QUERIES ==========
@@ -221,12 +222,27 @@ export const enviarEmailComTemplate = action({
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);
+ }
+ }
+
// Enfileirar email via mutation
const emailId: Id<"notificacoesEmail"> = await ctx.runMutation(api.email.enfileirarEmail, {
destinatario: args.destinatario,
destinatarioId: args.destinatarioId,
assunto: tituloRenderizado,
- corpo: corpoRenderizado,
+ corpo: corpoHTML, // Usar HTML completo
templateId: template._id, // template._id sempre existe se template não é null
enviadoPor: args.enviadoPor,
agendadaPara: args.agendadaPara,
diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts
index 022cbcf..76294be 100644
--- a/packages/backend/convex/schema.ts
+++ b/packages/backend/convex/schema.ts
@@ -851,13 +851,19 @@ export default defineSchema({
),
titulo: v.string(),
corpo: v.string(), // pode ter variáveis {{variavel}}
+ htmlCorpo: v.optional(v.string()), // versão HTML do corpo (com wrapper)
variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.]
+ categoria: v.optional(
+ v.union(v.literal("email"), v.literal("chat"), v.literal("ambos"))
+ ), // categoria do template
+ tags: v.optional(v.array(v.string())), // tags para organização
criadoPor: v.optional(v.id("usuarios")),
criadoEm: v.number(),
})
.index("by_codigo", ["codigo"])
.index("by_tipo", ["tipo"])
- .index("by_criado_por", ["criadoPor"]),
+ .index("by_criado_por", ["criadoPor"])
+ .index("by_categoria", ["categoria"]),
// Configuração de Email/SMTP
configuracaoEmail: defineTable({
diff --git a/packages/backend/convex/templatesMensagens.ts b/packages/backend/convex/templatesMensagens.ts
index 1b92ecf..a7fec7a 100644
--- a/packages/backend/convex/templatesMensagens.ts
+++ b/packages/backend/convex/templatesMensagens.ts
@@ -2,6 +2,7 @@ import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { registrarAtividade } from "./logsAtividades";
import { Doc } from "./_generated/dataModel";
+import { wrapEmailHTML, textToHTML } from "./utils/emailTemplateWrapper";
/**
* Listar todos os templates
@@ -40,7 +41,10 @@ export const criarTemplate = mutation({
nome: v.string(),
titulo: v.string(),
corpo: v.string(),
+ htmlCorpo: v.optional(v.string()),
variaveis: v.optional(v.array(v.string())),
+ categoria: v.optional(v.union(v.literal("email"), v.literal("chat"), v.literal("ambos"))),
+ tags: v.optional(v.array(v.string())),
criadoPorId: v.id("usuarios"),
},
returns: v.union(
@@ -58,6 +62,18 @@ export const criarTemplate = mutation({
return { sucesso: false as const, erro: "Código de template já existe" };
}
+ // Gerar HTML se não fornecido
+ let htmlCorpo = args.htmlCorpo;
+ if (!htmlCorpo) {
+ // Se o corpo já for HTML, usar diretamente, senão converter
+ if (args.corpo.includes("<") && args.corpo.includes(">")) {
+ htmlCorpo = wrapEmailHTML(args.corpo, args.titulo);
+ } else {
+ const corpoHTML = textToHTML(args.corpo);
+ htmlCorpo = wrapEmailHTML(corpoHTML, args.titulo);
+ }
+ }
+
// Criar template
const templateId = await ctx.db.insert("templatesMensagens", {
codigo: args.codigo,
@@ -65,7 +81,10 @@ export const criarTemplate = mutation({
tipo: "customizado",
titulo: args.titulo,
corpo: args.corpo,
+ htmlCorpo,
variaveis: args.variaveis,
+ categoria: args.categoria || "email",
+ tags: args.tags,
criadoPor: args.criadoPorId,
criadoEm: Date.now(),
});
@@ -93,7 +112,10 @@ export const editarTemplate = mutation({
nome: v.optional(v.string()),
titulo: v.optional(v.string()),
corpo: v.optional(v.string()),
+ htmlCorpo: v.optional(v.string()),
variaveis: v.optional(v.array(v.string())),
+ categoria: v.optional(v.union(v.literal("email"), v.literal("chat"), v.literal("ambos"))),
+ tags: v.optional(v.array(v.string())),
editadoPorId: v.id("usuarios"),
},
returns: v.union(
@@ -116,7 +138,21 @@ export const editarTemplate = mutation({
if (args.nome !== undefined) updates.nome = args.nome;
if (args.titulo !== undefined) updates.titulo = args.titulo;
if (args.corpo !== undefined) updates.corpo = args.corpo;
+ if (args.htmlCorpo !== undefined) {
+ updates.htmlCorpo = args.htmlCorpo;
+ } else if (args.corpo !== undefined) {
+ // Se corpo foi atualizado mas htmlCorpo não, regenerar HTML
+ const titulo = args.titulo || template.titulo;
+ if (args.corpo.includes("<") && args.corpo.includes(">")) {
+ updates.htmlCorpo = wrapEmailHTML(args.corpo, titulo);
+ } else {
+ const corpoHTML = textToHTML(args.corpo);
+ updates.htmlCorpo = wrapEmailHTML(corpoHTML, titulo);
+ }
+ }
if (args.variaveis !== undefined) updates.variaveis = args.variaveis;
+ if (args.categoria !== undefined) updates.categoria = args.categoria;
+ if (args.tags !== undefined) updates.tags = args.tags;
await ctx.db.patch(args.templateId, updates);
@@ -396,6 +432,33 @@ export const criarTemplatesPadrao = mutation({
+ "