Files
sgse-app/packages/backend/convex/templatesMensagens.ts

943 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
});
/**
* 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<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 };
}
});