import { v } from 'convex/values'; import type { Doc } from './_generated/dataModel'; import { mutation, query } from './_generated/server'; import { registrarAtividade } from './logsAtividades'; import { textToHTML, wrapEmailHTML } 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; } }); /** * Obter template por ID */ export const obterTemplatePorId = query({ args: { templateId: v.id('templatesMensagens') }, handler: async (ctx, args) => { const template = await ctx.db.get(args.templateId); 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; } export type VariaveisTemplate = Record; 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('')) { 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: "" + "
" + "

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: '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 {{funcionarioNome}} solicitou uma ausência:\n\n
  • Período: {{dataInicio}} até {{dataFim}}
  • Motivo: {{motivo}}
\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
  • Período: {{dataInicio}} até {{dataFim}}
  • Motivo: {{motivo}}
', 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
  • Período: {{dataInicio}} até {{dataFim}}
  • Motivo: {{motivo}}
  • Motivo da Reprovação: {{motivoReprovacao}}
', 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 }; } });