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> = {}; 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 { let resultado = template; for (const [chave, valor] of Object.entries(variaveis)) { const placeholder = `{{${chave}}}`; resultado = resultado.replace(new RegExp(placeholder, "g"), valor); } return resultado; } /** * 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: "" + "
" + "

Nova mensagem no chat

" + "

{{remetente}} enviou uma nova mensagem:

" + "
" + "

{{mensagem}}

" + "
" + "

" + "" + "Ver conversa" + "" + "

" + "

" + "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." + "

" + "
", variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"], }, { codigo: "chat_mencao", nome: "Menção no Chat", titulo: "{{remetente}} mencionou você", corpo: "" + "
" + "

Você foi mencionado!

" + "

{{remetente}} mencionou você em uma mensagem:

" + "
" + "

{{mensagem}}

" + "
" + "

" + "" + "Ver mensagem" + "" + "

" + "

" + "Você está recebendo este email porque foi mencionado em uma conversa. " + "Você pode desativar essas notificações nas configurações da conversa." + "

" + "
", variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"], }, { codigo: "chamado_registrado", nome: "Chamado Registrado", titulo: "Chamado {{numeroTicket}} registrado", corpo: "" + "
" + "

Chamado registrado com sucesso!

" + "

Olá {{solicitante}},

" + "

Recebemos sua solicitação e iniciaremos o atendimento em breve.

" + "
" + "

Ticket: {{numeroTicket}}

" + "

Prioridade: {{prioridade}}

" + "

Categoria: {{categoria}}

" + "
" + "

" + "" + "Acompanhar chamado" + "" + "

" + "

" + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + "

" + "
", variaveis: ["solicitante", "numeroTicket", "prioridade", "categoria", "urlSistema"], }, { codigo: "chamado_atualizado", nome: "Atualização no Chamado", titulo: "Atualização no chamado {{numeroTicket}}", corpo: "" + "
" + "

Nova atualização no seu chamado

" + "

Olá {{solicitante}},

" + "

Há uma nova atualização no seu chamado:

" + "
" + "

Ticket: {{numeroTicket}}

" + "

Mensagem:

" + "

{{mensagem}}

" + "
" + "

" + "" + "Ver detalhes" + "" + "

" + "

" + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + "

" + "
", variaveis: ["solicitante", "numeroTicket", "mensagem", "urlSistema"], }, { codigo: "chamado_atribuido", nome: "Chamado Atribuído", titulo: "Chamado {{numeroTicket}} atribuído", corpo: "" + "
" + "

Chamado atribuído

" + "

Olá {{responsavel}},

" + "

Um novo chamado foi atribuído para você:

" + "
" + "

Ticket: {{numeroTicket}}

" + "

Solicitante: {{solicitante}}

" + "

Prioridade: {{prioridade}}

" + "

Descrição: {{descricao}}

" + "
" + "

" + "" + "Acessar chamado" + "" + "

" + "

" + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + "

" + "
", variaveis: ["responsavel", "numeroTicket", "solicitante", "prioridade", "descricao", "urlSistema"], }, { codigo: "chamado_alerta_prazo", nome: "Alerta de Prazo do Chamado", titulo: "⚠️ Alerta de prazo - Chamado {{numeroTicket}}", corpo: "" + "
" + "

⚠️ Alerta de prazo

" + "

Olá {{destinatario}},

" + "

O chamado abaixo está próximo do prazo de {{tipoPrazo}}:

" + "
" + "

Ticket: {{numeroTicket}}

" + "

Prazo de {{tipoPrazo}}: {{prazo}}

" + "

Status: {{status}}

" + "
" + "

" + "" + "Ver chamado" + "" + "

" + "

" + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + "

" + "
", variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"], }, { codigo: "ausencia_solicitada", nome: "Ausência Solicitada", titulo: "Nova Solicitação de Ausência - {{funcionarioNome}}", corpo: "Olá {{gestorNome}},\n\nO funcionário {{funcionarioNome}} solicitou uma ausência:\n\n\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 aprovada pelo gestor {{gestorNome}}:\n\n", 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 reprovada pelo gestor {{gestorNome}}:\n\n", 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 = { 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 }; }, });