881 lines
32 KiB
TypeScript
881 lines
32 KiB
TypeScript
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
|
||
*/
|
||
export const listarTemplates = query({
|
||
args: {},
|
||
handler: async (ctx) => {
|
||
const templates = await ctx.db.query("templatesMensagens").collect();
|
||
return templates;
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Obter template por código
|
||
*/
|
||
export const obterTemplatePorCodigo = query({
|
||
args: {
|
||
codigo: v.string(),
|
||
},
|
||
handler: async (ctx, args) => {
|
||
const template = await ctx.db
|
||
.query("templatesMensagens")
|
||
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
|
||
.first();
|
||
|
||
return template;
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Criar template customizado (apenas TI_MASTER)
|
||
*/
|
||
export const criarTemplate = mutation({
|
||
args: {
|
||
codigo: v.string(),
|
||
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(
|
||
v.object({ sucesso: v.literal(true), templateId: v.id("templatesMensagens") }),
|
||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||
),
|
||
handler: async (ctx, args) => {
|
||
// Verificar se código já existe
|
||
const existente = await ctx.db
|
||
.query("templatesMensagens")
|
||
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
|
||
.first();
|
||
|
||
if (existente) {
|
||
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,
|
||
nome: args.nome,
|
||
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(),
|
||
});
|
||
|
||
// Log de atividade
|
||
await registrarAtividade(
|
||
ctx,
|
||
args.criadoPorId,
|
||
"criar",
|
||
"templates",
|
||
JSON.stringify({ templateId, codigo: args.codigo, nome: args.nome }),
|
||
templateId
|
||
);
|
||
|
||
return { sucesso: true as const, templateId };
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Editar template customizado (apenas TI_MASTER, não edita templates do sistema)
|
||
*/
|
||
export const editarTemplate = mutation({
|
||
args: {
|
||
templateId: v.id("templatesMensagens"),
|
||
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(
|
||
v.object({ sucesso: v.literal(true) }),
|
||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||
),
|
||
handler: async (ctx, args) => {
|
||
const template = await ctx.db.get(args.templateId);
|
||
if (!template) {
|
||
return { sucesso: false as const, erro: "Template não encontrado" };
|
||
}
|
||
|
||
// Não permite editar templates do sistema
|
||
if (template.tipo === "sistema") {
|
||
return { sucesso: false as const, erro: "Templates do sistema não podem ser editados" };
|
||
}
|
||
|
||
// Atualizar template
|
||
const updates: Partial<Doc<"templatesMensagens">> = {};
|
||
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);
|
||
|
||
// Log de atividade
|
||
await registrarAtividade(
|
||
ctx,
|
||
args.editadoPorId,
|
||
"editar",
|
||
"templates",
|
||
JSON.stringify(updates),
|
||
args.templateId
|
||
);
|
||
|
||
return { sucesso: true as const };
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Excluir template customizado (apenas TI_MASTER, não exclui templates do sistema)
|
||
*/
|
||
export const excluirTemplate = mutation({
|
||
args: {
|
||
templateId: v.id("templatesMensagens"),
|
||
excluidoPorId: v.id("usuarios"),
|
||
},
|
||
returns: v.union(
|
||
v.object({ sucesso: v.literal(true) }),
|
||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||
),
|
||
handler: async (ctx, args) => {
|
||
const template = await ctx.db.get(args.templateId);
|
||
if (!template) {
|
||
return { sucesso: false as const, erro: "Template não encontrado" };
|
||
}
|
||
|
||
// Não permite excluir templates do sistema
|
||
if (template.tipo === "sistema") {
|
||
return { sucesso: false as const, erro: "Templates do sistema não podem ser excluídos" };
|
||
}
|
||
|
||
// Excluir template
|
||
await ctx.db.delete(args.templateId);
|
||
|
||
// Log de atividade
|
||
await registrarAtividade(
|
||
ctx,
|
||
args.excluidoPorId,
|
||
"excluir",
|
||
"templates",
|
||
JSON.stringify({ templateId: args.templateId, codigo: template.codigo }),
|
||
args.templateId
|
||
);
|
||
|
||
return { sucesso: true as const };
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Renderizar template com variáveis
|
||
*/
|
||
export function renderizarTemplate(template: string, variaveis: Record<string, string>): string {
|
||
let resultado = template;
|
||
|
||
for (const [chave, valor] of Object.entries(variaveis)) {
|
||
const placeholder = `{{${chave}}}`;
|
||
resultado = resultado.replace(new RegExp(placeholder, "g"), valor);
|
||
}
|
||
|
||
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)
|
||
*/
|
||
export const criarTemplatesPadrao = mutation({
|
||
args: {},
|
||
handler: async (ctx) => {
|
||
const templatesPadrao = [
|
||
{
|
||
codigo: "USUARIO_BLOQUEADO",
|
||
nome: "Usuário Bloqueado",
|
||
titulo: "Sua conta foi bloqueada",
|
||
corpo: "Sua conta no SGSE foi bloqueada.\n\nMotivo: {{motivo}}\n\nPara mais informações, entre em contato com a TI.",
|
||
variaveis: ["motivo"],
|
||
},
|
||
{
|
||
codigo: "USUARIO_DESBLOQUEADO",
|
||
nome: "Usuário Desbloqueado",
|
||
titulo: "Sua conta foi desbloqueada",
|
||
corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
|
||
variaveis: [],
|
||
},
|
||
{
|
||
codigo: "SENHA_RESETADA",
|
||
nome: "Senha Resetada",
|
||
titulo: "Sua senha foi resetada",
|
||
corpo: "Sua senha foi resetada pela equipe de TI.\n\nNova senha temporária: {{senha}}\n\nPor favor, altere sua senha no próximo login.",
|
||
variaveis: ["senha"],
|
||
},
|
||
{
|
||
codigo: "PERMISSAO_ALTERADA",
|
||
nome: "Permissão Alterada",
|
||
titulo: "Suas permissões foram atualizadas",
|
||
corpo: "Suas permissões de acesso ao sistema foram atualizadas.\n\nPara verificar suas novas permissões, acesse o menu de perfil.",
|
||
variaveis: [],
|
||
},
|
||
{
|
||
codigo: "AVISO_GERAL",
|
||
nome: "Aviso Geral",
|
||
titulo: "{{titulo}}",
|
||
corpo: "{{mensagem}}",
|
||
variaveis: ["titulo", "mensagem"],
|
||
},
|
||
{
|
||
codigo: "BEM_VINDO",
|
||
nome: "Boas-vindas",
|
||
titulo: "Bem-vindo ao SGSE",
|
||
corpo: "Olá {{nome}},\n\nSeja bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria!\n\nSuas credenciais de acesso:\nMatrícula: {{matricula}}\nSenha temporária: {{senha}}\n\nPor favor, altere sua senha no primeiro acesso.\n\nEquipe de TI",
|
||
variaveis: ["nome", "matricula", "senha"],
|
||
},
|
||
{
|
||
codigo: "chat_mensagem",
|
||
nome: "Nova Mensagem no Chat",
|
||
titulo: "Nova mensagem de {{remetente}}",
|
||
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
||
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
||
+ "<h2 style='color: #4F46E5;'>Nova mensagem no chat</h2>"
|
||
+ "<p><strong>{{remetente}}</strong> enviou uma nova mensagem:</p>"
|
||
+ "<div style='background-color: #F3F4F6; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
|
||
+ "<p style='margin: 0;'>{{mensagem}}</p>"
|
||
+ "</div>"
|
||
+ "<p style='margin-top: 30px;'>"
|
||
+ "<a href='{{urlSistema}}/chat?conversa={{conversaId}}' "
|
||
+ "style='background-color: #4F46E5; color: white; padding: 12px 24px; "
|
||
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
|
||
+ "Ver conversa"
|
||
+ "</a>"
|
||
+ "</p>"
|
||
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
|
||
+ "Você está recebendo este email porque não estava online quando a mensagem foi enviada. "
|
||
+ "Você pode desativar essas notificações nas configurações da conversa."
|
||
+ "</p>"
|
||
+ "</div></body></html>",
|
||
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
|
||
},
|
||
{
|
||
codigo: "chat_mencao",
|
||
nome: "Menção no Chat",
|
||
titulo: "{{remetente}} mencionou você",
|
||
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
||
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
||
+ "<h2 style='color: #DC2626;'>Você foi mencionado!</h2>"
|
||
+ "<p><strong>{{remetente}}</strong> mencionou você em uma mensagem:</p>"
|
||
+ "<div style='background-color: #FEF2F2; border-left: 4px solid #DC2626; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
|
||
+ "<p style='margin: 0;'>{{mensagem}}</p>"
|
||
+ "</div>"
|
||
+ "<p style='margin-top: 30px;'>"
|
||
+ "<a href='{{urlSistema}}/chat?conversa={{conversaId}}' "
|
||
+ "style='background-color: #DC2626; color: white; padding: 12px 24px; "
|
||
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
|
||
+ "Ver mensagem"
|
||
+ "</a>"
|
||
+ "</p>"
|
||
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
|
||
+ "Você está recebendo este email porque foi mencionado em uma conversa. "
|
||
+ "Você pode desativar essas notificações nas configurações da conversa."
|
||
+ "</p>"
|
||
+ "</div></body></html>",
|
||
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
|
||
},
|
||
{
|
||
codigo: "chamado_registrado",
|
||
nome: "Chamado Registrado",
|
||
titulo: "Chamado {{numeroTicket}} registrado",
|
||
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
||
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
||
+ "<h2 style='color: #2563EB;'>Chamado registrado com sucesso!</h2>"
|
||
+ "<p>Olá <strong>{{solicitante}}</strong>,</p>"
|
||
+ "<p>Recebemos sua solicitação e iniciaremos o atendimento em breve.</p>"
|
||
+ "<div style='background-color: #EFF6FF; border-left: 4px solid #2563EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
|
||
+ "<p style='margin: 0;'><strong>Ticket:</strong> {{numeroTicket}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Prioridade:</strong> {{prioridade}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Categoria:</strong> {{categoria}}</p>"
|
||
+ "</div>"
|
||
+ "<p style='margin-top: 30px;'>"
|
||
+ "<a href='{{urlSistema}}/perfil/chamados' "
|
||
+ "style='background-color: #2563EB; color: white; padding: 12px 24px; "
|
||
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
|
||
+ "Acompanhar chamado"
|
||
+ "</a>"
|
||
+ "</p>"
|
||
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
|
||
+ "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria"
|
||
+ "</p>"
|
||
+ "</div></body></html>",
|
||
variaveis: ["solicitante", "numeroTicket", "prioridade", "categoria", "urlSistema"],
|
||
},
|
||
{
|
||
codigo: "chamado_atualizado",
|
||
nome: "Atualização no Chamado",
|
||
titulo: "Atualização no chamado {{numeroTicket}}",
|
||
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
||
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
||
+ "<h2 style='color: #2563EB;'>Nova atualização no seu chamado</h2>"
|
||
+ "<p>Olá <strong>{{solicitante}}</strong>,</p>"
|
||
+ "<p>Há uma nova atualização no seu chamado:</p>"
|
||
+ "<div style='background-color: #EFF6FF; border-left: 4px solid #2563EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
|
||
+ "<p style='margin: 0;'><strong>Ticket:</strong> {{numeroTicket}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Mensagem:</strong></p>"
|
||
+ "<p style='margin: 10px 0 0 0;'>{{mensagem}}</p>"
|
||
+ "</div>"
|
||
+ "<p style='margin-top: 30px;'>"
|
||
+ "<a href='{{urlSistema}}/perfil/chamados' "
|
||
+ "style='background-color: #2563EB; color: white; padding: 12px 24px; "
|
||
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
|
||
+ "Ver detalhes"
|
||
+ "</a>"
|
||
+ "</p>"
|
||
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
|
||
+ "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria"
|
||
+ "</p>"
|
||
+ "</div></body></html>",
|
||
variaveis: ["solicitante", "numeroTicket", "mensagem", "urlSistema"],
|
||
},
|
||
{
|
||
codigo: "chamado_atribuido",
|
||
nome: "Chamado Atribuído",
|
||
titulo: "Chamado {{numeroTicket}} atribuído",
|
||
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
||
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
||
+ "<h2 style='color: #059669;'>Chamado atribuído</h2>"
|
||
+ "<p>Olá <strong>{{responsavel}}</strong>,</p>"
|
||
+ "<p>Um novo chamado foi atribuído para você:</p>"
|
||
+ "<div style='background-color: #ECFDF5; border-left: 4px solid #059669; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
|
||
+ "<p style='margin: 0;'><strong>Ticket:</strong> {{numeroTicket}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Solicitante:</strong> {{solicitante}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Prioridade:</strong> {{prioridade}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Descrição:</strong> {{descricao}}</p>"
|
||
+ "</div>"
|
||
+ "<p style='margin-top: 30px;'>"
|
||
+ "<a href='{{urlSistema}}/ti/central-chamados' "
|
||
+ "style='background-color: #059669; color: white; padding: 12px 24px; "
|
||
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
|
||
+ "Acessar chamado"
|
||
+ "</a>"
|
||
+ "</p>"
|
||
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
|
||
+ "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria"
|
||
+ "</p>"
|
||
+ "</div></body></html>",
|
||
variaveis: ["responsavel", "numeroTicket", "solicitante", "prioridade", "descricao", "urlSistema"],
|
||
},
|
||
{
|
||
codigo: "chamado_alerta_prazo",
|
||
nome: "Alerta de Prazo do Chamado",
|
||
titulo: "⚠️ Alerta de prazo - Chamado {{numeroTicket}}",
|
||
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
||
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
||
+ "<h2 style='color: #DC2626;'>⚠️ Alerta de prazo</h2>"
|
||
+ "<p>Olá <strong>{{destinatario}}</strong>,</p>"
|
||
+ "<p>O chamado abaixo está próximo do prazo de {{tipoPrazo}}:</p>"
|
||
+ "<div style='background-color: #FEF2F2; border-left: 4px solid #DC2626; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
|
||
+ "<p style='margin: 0;'><strong>Ticket:</strong> {{numeroTicket}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Prazo de {{tipoPrazo}}:</strong> {{prazo}}</p>"
|
||
+ "<p style='margin: 5px 0 0 0;'><strong>Status:</strong> {{status}}</p>"
|
||
+ "</div>"
|
||
+ "<p style='margin-top: 30px;'>"
|
||
+ "<a href='{{urlSistema}}{{rotaAcesso}}' "
|
||
+ "style='background-color: #DC2626; color: white; padding: 12px 24px; "
|
||
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
|
||
+ "Ver chamado"
|
||
+ "</a>"
|
||
+ "</p>"
|
||
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
|
||
+ "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria"
|
||
+ "</p>"
|
||
+ "</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",
|
||
titulo: "Nova Solicitação de Ausência - {{funcionarioNome}}",
|
||
corpo: "Olá {{gestorNome}},\n\nO funcionário <strong>{{funcionarioNome}}</strong> solicitou uma ausência:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>\n\nPor favor, acesse o sistema para aprovar ou reprovar esta solicitação.",
|
||
variaveis: ["gestorNome", "funcionarioNome", "dataInicio", "dataFim", "motivo", "urlSistema"],
|
||
categoria: "email" as const,
|
||
tags: ["ausencia", "solicitacao", "gestao"],
|
||
},
|
||
{
|
||
codigo: "ausencia_aprovada",
|
||
nome: "Ausência Aprovada",
|
||
titulo: "Solicitação de Ausência Aprovada",
|
||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>",
|
||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "urlSistema"],
|
||
categoria: "email" as const,
|
||
tags: ["ausencia", "aprovacao", "gestao"],
|
||
},
|
||
{
|
||
codigo: "ausencia_reprovada",
|
||
nome: "Ausência Reprovada",
|
||
titulo: "Solicitação de Ausência Reprovada",
|
||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>reprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li><li><strong>Motivo da Reprovação:</strong> {{motivoReprovacao}}</li></ul>",
|
||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "motivoReprovacao", "urlSistema"],
|
||
categoria: "email" as const,
|
||
tags: ["ausencia", "reprovacao", "gestao"],
|
||
},
|
||
];
|
||
|
||
for (const template of templatesPadrao) {
|
||
// Verificar se já existe
|
||
const existente = await ctx.db
|
||
.query("templatesMensagens")
|
||
.withIndex("by_codigo", (q) => q.eq("codigo", template.codigo))
|
||
.first();
|
||
|
||
if (!existente) {
|
||
await ctx.db.insert("templatesMensagens", {
|
||
...template,
|
||
tipo: "sistema",
|
||
criadoEm: Date.now(),
|
||
});
|
||
}
|
||
}
|
||
|
||
return { sucesso: true };
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Atualizar HTML de um template
|
||
*/
|
||
export const atualizarTemplateHTML = mutation({
|
||
args: {
|
||
templateId: v.id("templatesMensagens"),
|
||
htmlCorpo: v.string(),
|
||
editadoPorId: v.id("usuarios"),
|
||
},
|
||
returns: v.union(
|
||
v.object({ sucesso: v.literal(true) }),
|
||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||
),
|
||
handler: async (ctx, args) => {
|
||
const template = await ctx.db.get(args.templateId);
|
||
if (!template) {
|
||
return { sucesso: false as const, erro: "Template não encontrado" };
|
||
}
|
||
|
||
// Não permite editar templates do sistema
|
||
if (template.tipo === "sistema") {
|
||
return { sucesso: false as const, erro: "Templates do sistema não podem ser editados" };
|
||
}
|
||
|
||
await ctx.db.patch(args.templateId, {
|
||
htmlCorpo: args.htmlCorpo,
|
||
});
|
||
|
||
await registrarAtividade(
|
||
ctx,
|
||
args.editadoPorId,
|
||
"editar",
|
||
"templates",
|
||
JSON.stringify({ templateId: args.templateId, campo: "htmlCorpo" }),
|
||
args.templateId
|
||
);
|
||
|
||
return { sucesso: true as const };
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Preview de template renderizado com variáveis de teste
|
||
*/
|
||
export const previewTemplate = query({
|
||
args: {
|
||
templateId: v.id("templatesMensagens"),
|
||
variaveisTeste: v.optional(v.record(v.string(), v.string())),
|
||
},
|
||
handler: async (ctx, args) => {
|
||
const template = await ctx.db.get(args.templateId);
|
||
if (!template) {
|
||
return null;
|
||
}
|
||
|
||
// Variáveis padrão para teste
|
||
const variaveisPadrao: Record<string, string> = {
|
||
nome: "João Silva",
|
||
matricula: "12345",
|
||
senha: "Senha123!",
|
||
motivo: "Exemplo de motivo",
|
||
remetente: "Maria Santos",
|
||
mensagem: "Esta é uma mensagem de exemplo para preview do template.",
|
||
conversaId: "abc123",
|
||
urlSistema: getBaseUrl(),
|
||
solicitante: "João Silva",
|
||
numeroTicket: "TKT-2024-001",
|
||
prioridade: "Alta",
|
||
categoria: "Suporte Técnico",
|
||
responsavel: "Maria Santos",
|
||
descricao: "Exemplo de descrição de chamado",
|
||
destinario: "João Silva",
|
||
tipoPrazo: "resolução",
|
||
prazo: "24 horas",
|
||
status: "Em andamento",
|
||
rotaAcesso: "/ti/central-chamados",
|
||
titulo: "Título de Exemplo",
|
||
};
|
||
|
||
const variaveis = { ...variaveisPadrao, ...(args.variaveisTeste || {}) };
|
||
|
||
// Renderizar título e corpo
|
||
const tituloRenderizado = renderizarTemplate(template.titulo, variaveis);
|
||
const corpoRenderizado = renderizarTemplate(template.corpo, variaveis);
|
||
|
||
// Se tiver htmlCorpo, usar ele, senão gerar do corpo
|
||
let htmlFinal = template.htmlCorpo;
|
||
if (!htmlFinal) {
|
||
if (corpoRenderizado.includes("<") && corpoRenderizado.includes(">")) {
|
||
htmlFinal = wrapEmailHTML(corpoRenderizado, tituloRenderizado);
|
||
} else {
|
||
const corpoHTML = textToHTML(corpoRenderizado);
|
||
htmlFinal = wrapEmailHTML(corpoHTML, tituloRenderizado);
|
||
}
|
||
} else {
|
||
htmlFinal = renderizarTemplate(htmlFinal, variaveis);
|
||
}
|
||
|
||
return {
|
||
titulo: tituloRenderizado,
|
||
corpo: corpoRenderizado,
|
||
html: htmlFinal,
|
||
variaveisUsadas: template.variaveis || [],
|
||
};
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Função auxiliar para obter URL base
|
||
*/
|
||
function getBaseUrl(): string {
|
||
const url = process.env.FRONTEND_URL || "http://localhost:5173";
|
||
if (!url.match(/^https?:\/\//i)) {
|
||
return `http://${url}`;
|
||
}
|
||
return url;
|
||
}
|
||
|
||
/**
|
||
* Exportar templates (JSON)
|
||
*/
|
||
export const exportarTemplates = query({
|
||
args: {
|
||
templateIds: v.optional(v.array(v.id("templatesMensagens"))),
|
||
},
|
||
handler: async (ctx, args) => {
|
||
let templates;
|
||
|
||
if (args.templateIds && args.templateIds.length > 0) {
|
||
templates = await Promise.all(
|
||
args.templateIds.map((id) => ctx.db.get(id))
|
||
);
|
||
templates = templates.filter((t): t is Doc<"templatesMensagens"> => t !== null);
|
||
} else {
|
||
templates = await ctx.db.query("templatesMensagens").collect();
|
||
}
|
||
|
||
// Remover campos internos e retornar apenas dados exportáveis
|
||
return templates.map((t) => ({
|
||
codigo: t.codigo,
|
||
nome: t.nome,
|
||
tipo: t.tipo,
|
||
titulo: t.titulo,
|
||
corpo: t.corpo,
|
||
htmlCorpo: t.htmlCorpo,
|
||
variaveis: t.variaveis,
|
||
categoria: t.categoria,
|
||
tags: t.tags,
|
||
}));
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Importar templates (JSON)
|
||
*/
|
||
export const importarTemplates = mutation({
|
||
args: {
|
||
templates: v.array(
|
||
v.object({
|
||
codigo: v.string(),
|
||
nome: v.string(),
|
||
tipo: v.optional(v.union(v.literal("sistema"), v.literal("customizado"))),
|
||
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())),
|
||
})
|
||
),
|
||
importadoPorId: v.id("usuarios"),
|
||
sobrescrever: v.optional(v.boolean()),
|
||
},
|
||
returns: v.object({
|
||
sucesso: v.boolean(),
|
||
importados: v.number(),
|
||
atualizados: v.number(),
|
||
erros: v.array(v.string()),
|
||
}),
|
||
handler: async (ctx, args) => {
|
||
let importados = 0;
|
||
let atualizados = 0;
|
||
const erros: string[] = [];
|
||
|
||
for (const templateData of args.templates) {
|
||
try {
|
||
const existente = await ctx.db
|
||
.query("templatesMensagens")
|
||
.withIndex("by_codigo", (q) => q.eq("codigo", templateData.codigo))
|
||
.first();
|
||
|
||
if (existente) {
|
||
if (args.sobrescrever && existente.tipo === "customizado") {
|
||
// Atualizar template existente
|
||
await ctx.db.patch(existente._id, {
|
||
nome: templateData.nome,
|
||
titulo: templateData.titulo,
|
||
corpo: templateData.corpo,
|
||
htmlCorpo: templateData.htmlCorpo,
|
||
variaveis: templateData.variaveis,
|
||
categoria: templateData.categoria,
|
||
tags: templateData.tags,
|
||
});
|
||
atualizados++;
|
||
} else {
|
||
erros.push(`Template ${templateData.codigo} já existe e sobrescrever está desabilitado`);
|
||
}
|
||
} else {
|
||
// Criar novo template
|
||
const tipo = templateData.tipo || "customizado";
|
||
|
||
// Gerar HTML se não fornecido
|
||
let htmlCorpo = templateData.htmlCorpo;
|
||
if (!htmlCorpo) {
|
||
if (templateData.corpo.includes("<") && templateData.corpo.includes(">")) {
|
||
htmlCorpo = wrapEmailHTML(templateData.corpo, templateData.titulo);
|
||
} else {
|
||
const corpoHTML = textToHTML(templateData.corpo);
|
||
htmlCorpo = wrapEmailHTML(corpoHTML, templateData.titulo);
|
||
}
|
||
}
|
||
|
||
await ctx.db.insert("templatesMensagens", {
|
||
codigo: templateData.codigo,
|
||
nome: templateData.nome,
|
||
tipo,
|
||
titulo: templateData.titulo,
|
||
corpo: templateData.corpo,
|
||
htmlCorpo,
|
||
variaveis: templateData.variaveis,
|
||
categoria: templateData.categoria || "email",
|
||
tags: templateData.tags,
|
||
criadoPor: args.importadoPorId,
|
||
criadoEm: Date.now(),
|
||
});
|
||
importados++;
|
||
}
|
||
} catch (error) {
|
||
const erroMsg = error instanceof Error ? error.message : String(error);
|
||
erros.push(`Erro ao importar ${templateData.codigo}: ${erroMsg}`);
|
||
}
|
||
}
|
||
|
||
await registrarAtividade(
|
||
ctx,
|
||
args.importadoPorId,
|
||
"importar",
|
||
"templates",
|
||
JSON.stringify({ importados, atualizados, erros: erros.length }),
|
||
undefined
|
||
);
|
||
|
||
return {
|
||
sucesso: erros.length === 0,
|
||
importados,
|
||
atualizados,
|
||
erros,
|
||
};
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Duplicar template
|
||
*/
|
||
export const duplicarTemplate = mutation({
|
||
args: {
|
||
templateId: v.id("templatesMensagens"),
|
||
novoCodigo: v.string(),
|
||
novoNome: v.optional(v.string()),
|
||
criadoPorId: v.id("usuarios"),
|
||
},
|
||
returns: v.union(
|
||
v.object({ sucesso: v.literal(true), templateId: v.id("templatesMensagens") }),
|
||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||
),
|
||
handler: async (ctx, args) => {
|
||
const template = await ctx.db.get(args.templateId);
|
||
if (!template) {
|
||
return { sucesso: false as const, erro: "Template não encontrado" };
|
||
}
|
||
|
||
// Verificar se novo código já existe
|
||
const existente = await ctx.db
|
||
.query("templatesMensagens")
|
||
.withIndex("by_codigo", (q) => q.eq("codigo", args.novoCodigo))
|
||
.first();
|
||
|
||
if (existente) {
|
||
return { sucesso: false as const, erro: "Código de template já existe" };
|
||
}
|
||
|
||
const templateId = await ctx.db.insert("templatesMensagens", {
|
||
codigo: args.novoCodigo,
|
||
nome: args.novoNome || `${template.nome} (Cópia)`,
|
||
tipo: "customizado",
|
||
titulo: template.titulo,
|
||
corpo: template.corpo,
|
||
htmlCorpo: template.htmlCorpo,
|
||
variaveis: template.variaveis,
|
||
categoria: template.categoria,
|
||
tags: template.tags,
|
||
criadoPor: args.criadoPorId,
|
||
criadoEm: Date.now(),
|
||
});
|
||
|
||
await registrarAtividade(
|
||
ctx,
|
||
args.criadoPorId,
|
||
"duplicar",
|
||
"templates",
|
||
JSON.stringify({ templateId, codigo: args.novoCodigo, originalId: args.templateId }),
|
||
templateId
|
||
);
|
||
|
||
return { sucesso: true as const, templateId };
|
||
},
|
||
});
|
||
|