1348 lines
51 KiB
TypeScript
1348 lines
51 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;
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 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:
|
||
"<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;'>Bem-vindo ao SGSE</h2>" +
|
||
'<p>Olá <strong>{{nome}}</strong>,</p>' +
|
||
'<p>Seja bem-vindo ao <strong>SGSE - Sistema de Gerenciamento de Secretaria</strong>!</p>' +
|
||
'<p>Seu cadastro foi realizado com sucesso.</p>' +
|
||
"<div style='background-color: #F3F4F6; border-left: 4px solid #2563EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0 0 10px 0;'><strong>Suas credenciais de acesso:</strong></p>" +
|
||
"<ul style='margin: 0; padding-left: 20px;'>" +
|
||
'<li><strong>E-mail:</strong> {{email}}</li>' +
|
||
'{{credenciaisAdicionais}}' +
|
||
'<li><strong>Senha temporária:</strong> {{senha}}</li>' +
|
||
'</ul>' +
|
||
'</div>' +
|
||
'<p><strong>⚠️ Importante:</strong> Por favor, altere sua senha no primeiro acesso ao sistema.</p>' +
|
||
"<p>Acesse o sistema através do link: <a href='{{urlSistema}}' style='color: #2563EB;'>{{urlSistema}}</a></p>" +
|
||
"<p style='margin-top: 30px; color: #6B7280; font-size: 14px;'>Equipe de TI - Secretaria de Esportes</p>" +
|
||
'</div></body></html>',
|
||
variaveis: ['nome', 'email', 'credenciaisAdicionais', 'senha', 'urlSistema'],
|
||
categoria: 'email' as const,
|
||
tags: ['boas_vindas', 'cadastro', 'credenciais']
|
||
},
|
||
{
|
||
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.',
|
||
htmlCorpo:
|
||
'<div style="max-width: 600px; margin: 0 auto; padding: 20px;">' +
|
||
'<div style="background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%); border-radius: 8px; padding: 20px; margin-bottom: 25px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">' +
|
||
'<h2 style="color: #FFFFFF; margin: 0 0 10px 0; font-size: 24px; font-weight: bold;">⚠️ Alerta de Sistema</h2>' +
|
||
'<p style="color: #FFFFFF; margin: 0; font-size: 16px; font-weight: 500;">Métrica: <strong>{{metricName}}</strong></p>' +
|
||
'</div>' +
|
||
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||
'<div style="background-color: #FFF3CD; border-left: 4px solid #FFC107; padding: 15px; border-radius: 4px; margin: 20px 0;">' +
|
||
'<p style="margin: 0 0 10px 0; color: #856404; font-weight: bold; font-size: 14px;">📊 Detalhes do Alerta:</p>' +
|
||
'<ul style="margin: 0; padding-left: 20px; color: #856404; font-size: 14px; line-height: 1.8;">' +
|
||
'<li><strong>Métrica:</strong> {{metricName}}</li>' +
|
||
'<li><strong>Valor Atual:</strong> <span style="color: #DC3545; font-weight: bold;">{{metricValue}}</span></li>' +
|
||
'<li><strong>Limite Configurado:</strong> {{threshold}}</li>' +
|
||
'</ul>' +
|
||
'</div>' +
|
||
'<p style="color: #333333; font-size: 14px; line-height: 1.6; margin: 20px 0;">' +
|
||
'Recomenda-se verificar o <strong>painel de monitoramento do SGSE</strong> para detalhes adicionais e, se necessário, executar ações corretivas.' +
|
||
'</p>' +
|
||
'<div style="background-color: #E7F3FF; border-left: 4px solid #0052A5; padding: 15px; border-radius: 4px; margin: 20px 0;">' +
|
||
'<p style="margin: 0; color: #004085; font-size: 14px; line-height: 1.6;">' +
|
||
'<strong>💡 Dica:</strong> Acesse o painel de monitoramento para visualizar gráficos e histórico detalhado desta métrica.' +
|
||
'</p>' +
|
||
'</div>' +
|
||
'<p style="color: #666666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #E0E0E0;">' +
|
||
'Esta é uma notificação automática do sistema de monitoramento SGSE.' +
|
||
'</p>' +
|
||
'</div>',
|
||
variaveis: ['destinatarioNome', 'metricName', 'metricValue', 'threshold'],
|
||
categoria: 'email' as const,
|
||
tags: ['monitoramento', 'alerta', 'sistema', 'ti']
|
||
},
|
||
// ===================== LGPD =====================
|
||
{
|
||
codigo: 'lgpd_solicitacao_criada',
|
||
nome: 'LGPD - Solicitação Criada',
|
||
titulo: 'Recebemos sua solicitação LGPD ({{tipoSolicitacaoLabel}})',
|
||
corpo:
|
||
'Olá {{nomeTitular}},\n\n' +
|
||
'Recebemos sua solicitação LGPD do tipo "{{tipoSolicitacaoLabel}}".\n\n' +
|
||
'Prazo estimado para resposta: até {{prazoResposta}}.\n\n' +
|
||
'Você pode acompanhar o andamento acessando: {{urlPortalLGPD}}.\n\n' +
|
||
'Equipe de Proteção de Dados / TI.',
|
||
variaveis: ['nomeTitular', 'tipoSolicitacaoLabel', 'prazoResposta', 'urlPortalLGPD'],
|
||
categoria: 'email' as const,
|
||
tags: ['lgpd', 'solicitacao', 'dados_pessoais']
|
||
},
|
||
{
|
||
codigo: 'lgpd_resposta_acesso',
|
||
nome: 'LGPD - Resposta Acesso',
|
||
titulo: 'Resposta à sua solicitação LGPD - Acesso aos Dados',
|
||
corpo:
|
||
'Olá {{nomeTitular}},\n\n' +
|
||
'Sua solicitação LGPD de Acesso aos Dados foi marcada como {{statusLabel}}.\n\n' +
|
||
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
|
||
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
|
||
'Equipe de Proteção de Dados / TI.',
|
||
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
|
||
categoria: 'email' as const,
|
||
tags: ['lgpd', 'acesso', 'dados_pessoais']
|
||
},
|
||
{
|
||
codigo: 'lgpd_resposta_correcao',
|
||
nome: 'LGPD - Resposta Correção',
|
||
titulo: 'Resposta à sua solicitação LGPD - Correção de Dados',
|
||
corpo:
|
||
'Olá {{nomeTitular}},\n\n' +
|
||
'Sua solicitação LGPD de Correção de Dados foi marcada como {{statusLabel}}.\n\n' +
|
||
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
|
||
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
|
||
'Equipe de Proteção de Dados / TI.',
|
||
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
|
||
categoria: 'email' as const,
|
||
tags: ['lgpd', 'correcao', 'dados_pessoais']
|
||
},
|
||
{
|
||
codigo: 'lgpd_resposta_exclusao',
|
||
nome: 'LGPD - Resposta Exclusão',
|
||
titulo: 'Resposta à sua solicitação LGPD - Exclusão de Dados',
|
||
corpo:
|
||
'Olá {{nomeTitular}},\n\n' +
|
||
'Sua solicitação LGPD de Exclusão de Dados foi marcada como {{statusLabel}}.\n\n' +
|
||
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
|
||
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
|
||
'Equipe de Proteção de Dados / TI.',
|
||
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
|
||
categoria: 'email' as const,
|
||
tags: ['lgpd', 'exclusao', 'dados_pessoais']
|
||
},
|
||
{
|
||
codigo: 'lgpd_resposta_portabilidade',
|
||
nome: 'LGPD - Resposta Portabilidade',
|
||
titulo: 'Resposta à sua solicitação LGPD - Portabilidade dos Dados',
|
||
corpo:
|
||
'Olá {{nomeTitular}},\n\n' +
|
||
'Sua solicitação LGPD de Portabilidade dos Dados foi marcada como {{statusLabel}}.\n\n' +
|
||
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
|
||
'Caso tenha recebido um arquivo anexo, ele contém os dados em formato portável.\n\n' +
|
||
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
|
||
'Equipe de Proteção de Dados / TI.',
|
||
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
|
||
categoria: 'email' as const,
|
||
tags: ['lgpd', 'portabilidade', 'dados_pessoais']
|
||
},
|
||
{
|
||
codigo: 'lgpd_resposta_revogacao_consentimento',
|
||
nome: 'LGPD - Resposta Revogação de Consentimento',
|
||
titulo: 'Confirmação de Revogação de Consentimento',
|
||
corpo:
|
||
'Olá {{nomeTitular}},\n\n' +
|
||
'Sua solicitação LGPD de Revogação de Consentimento foi marcada como {{statusLabel}}.\n\n' +
|
||
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
|
||
'Todos os consentimentos ativos associados à sua conta foram marcados como revogados a partir desta data.\n\n' +
|
||
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
|
||
'Equipe de Proteção de Dados / TI.',
|
||
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
|
||
categoria: 'email' as const,
|
||
tags: ['lgpd', 'revogacao_consentimento', 'dados_pessoais']
|
||
},
|
||
{
|
||
codigo: 'lgpd_resposta_informacao_compartilhamento',
|
||
nome: 'LGPD - Resposta Informação sobre Compartilhamento',
|
||
titulo: 'Resposta à sua solicitação LGPD - Informação sobre Compartilhamento',
|
||
corpo:
|
||
'Olá {{nomeTitular}},\n\n' +
|
||
'Sua solicitação LGPD de Informação sobre Compartilhamento foi marcada como {{statusLabel}}.\n\n' +
|
||
'Resumo da resposta:\n{{resumoResposta}}\n\n' +
|
||
'Para mais detalhes, acesse: {{urlPortalLGPD}}.\n\n' +
|
||
'Equipe de Proteção de Dados / TI.',
|
||
variaveis: ['nomeTitular', 'statusLabel', 'resumoResposta', 'urlPortalLGPD'],
|
||
categoria: 'email' as const,
|
||
tags: ['lgpd', 'informacao_compartilhamento', 'dados_pessoais']
|
||
},
|
||
{
|
||
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']
|
||
},
|
||
{
|
||
codigo: 'ferias_aprovada',
|
||
nome: 'Férias Aprovada',
|
||
titulo: 'Solicitação de Férias Aprovada',
|
||
corpo:
|
||
'Olá {{funcionarioNome}},\n\nSua solicitação de férias foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Dias:</strong> {{diasFerias}} dias</li></ul>',
|
||
variaveis: [
|
||
'funcionarioNome',
|
||
'gestorNome',
|
||
'dataInicio',
|
||
'dataFim',
|
||
'diasFerias',
|
||
'urlSistema'
|
||
],
|
||
categoria: 'email' as const,
|
||
tags: ['ferias', 'aprovacao', 'gestao']
|
||
},
|
||
{
|
||
codigo: 'ferias_cancelada_rh',
|
||
nome: 'Férias Cancelada pelo RH',
|
||
titulo: 'Solicitação de Férias Cancelada',
|
||
corpo:
|
||
'Olá {{funcionarioNome}},\n\nSua solicitação de férias foi <strong>cancelada</strong> pelo setor de Recursos Humanos:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Dias:</strong> {{diasFerias}} dias</li></ul>\n\nPara mais informações, entre em contato com o setor de Recursos Humanos.',
|
||
variaveis: ['funcionarioNome', 'dataInicio', 'dataFim', 'diasFerias', 'urlSistema'],
|
||
categoria: 'email' as const,
|
||
tags: ['ferias', 'cancelamento', 'recursos_humanos']
|
||
},
|
||
// ===================== ALERTAS DE SEGURANÇA CIBERNÉTICA =====================
|
||
{
|
||
codigo: 'incidente_critico',
|
||
nome: 'Incidente Crítico - Ação Imediata',
|
||
titulo: '🚨 ALERTA CRÍTICO: {{tipoAtaque}}',
|
||
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 CRÍTICO DE SEGURANÇA</h2>" +
|
||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||
'<p>Um <strong>incidente crítico de segurança</strong> foi detectado no sistema:</p>' +
|
||
"<div style='background-color: #FEF2F2; border-left: 4px solid #DC2626; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0;'><strong>Tipo de Ataque:</strong> {{tipoAtaque}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Severidade:</strong> <span style='color: #DC2626; font-weight: bold;'>{{severidade}}</span></p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Descrição:</strong> {{descricao}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>IP de Origem:</strong> {{origemIp}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{dataHora}}</p>" +
|
||
'</div>' +
|
||
"<p style='color: #DC2626; font-weight: bold;'>⚠️ AÇÃO IMEDIATA NECESSÁRIA</p>" +
|
||
"<p style='margin-top: 30px;'>" +
|
||
"<a href='{{urlSistema}}/ti/cybersecurity' " +
|
||
"style='background-color: #DC2626; color: white; padding: 12px 24px; " +
|
||
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
|
||
'Ver Detalhes do Incidente' +
|
||
'</a>' +
|
||
'</p>' +
|
||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
|
||
'</p>' +
|
||
'</div></body></html>',
|
||
variaveis: [
|
||
'destinatarioNome',
|
||
'tipoAtaque',
|
||
'severidade',
|
||
'descricao',
|
||
'origemIp',
|
||
'dataHora',
|
||
'urlSistema'
|
||
],
|
||
categoria: 'email' as const,
|
||
tags: ['seguranca', 'alerta', 'critico', 'cybersecurity']
|
||
},
|
||
{
|
||
codigo: 'bloqueio_automatico',
|
||
nome: 'Bloqueio Automático',
|
||
titulo: '🔒 Bloqueio Automático: {{tipoAtaque}}',
|
||
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: #F59E0B;'>🔒 Bloqueio Automático Aplicado</h2>" +
|
||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||
'<p>O sistema aplicou um <strong>bloqueio automático</strong> devido a uma tentativa de ataque detectada:</p>' +
|
||
"<div style='background-color: #FFFBEB; border-left: 4px solid #F59E0B; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0;'><strong>Tipo de Ataque:</strong> {{tipoAtaque}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>IP Bloqueado:</strong> {{origemIp}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Descrição:</strong> {{descricao}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{dataHora}}</p>" +
|
||
'</div>' +
|
||
"<p style='margin-top: 30px;'>" +
|
||
"<a href='{{urlSistema}}/ti/cybersecurity' " +
|
||
"style='background-color: #F59E0B; color: white; padding: 12px 24px; " +
|
||
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
|
||
'Ver Detalhes do Bloqueio' +
|
||
'</a>' +
|
||
'</p>' +
|
||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
|
||
'</p>' +
|
||
'</div></body></html>',
|
||
variaveis: [
|
||
'destinatarioNome',
|
||
'tipoAtaque',
|
||
'origemIp',
|
||
'descricao',
|
||
'dataHora',
|
||
'urlSistema'
|
||
],
|
||
categoria: 'email' as const,
|
||
tags: ['seguranca', 'bloqueio', 'automatico', 'cybersecurity']
|
||
},
|
||
{
|
||
codigo: 'sumario_30min',
|
||
nome: 'Sumário 30 Min',
|
||
titulo: '📊 Sumário de Segurança - Últimos 30 minutos',
|
||
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;'>📊 Sumário de Segurança</h2>" +
|
||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||
'<p>Resumo dos eventos de segurança dos últimos 30 minutos:</p>' +
|
||
"<div style='background-color: #EFF6FF; border-left: 4px solid #2563EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0;'><strong>Total de Eventos:</strong> {{totalEventos}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Eventos Críticos:</strong> {{eventosCriticos}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Eventos Altos:</strong> {{eventosAltos}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>IPs Bloqueados:</strong> {{ipsBloqueados}}</p>" +
|
||
'</div>' +
|
||
"<p style='margin-top: 30px;'>" +
|
||
"<a href='{{urlSistema}}/ti/cybersecurity' " +
|
||
"style='background-color: #2563EB; color: white; padding: 12px 24px; " +
|
||
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
|
||
'Ver Relatório Completo' +
|
||
'</a>' +
|
||
'</p>' +
|
||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
|
||
'</p>' +
|
||
'</div></body></html>',
|
||
variaveis: [
|
||
'destinatarioNome',
|
||
'totalEventos',
|
||
'eventosCriticos',
|
||
'eventosAltos',
|
||
'ipsBloqueados',
|
||
'urlSistema'
|
||
],
|
||
categoria: 'email' as const,
|
||
tags: ['seguranca', 'sumario', 'relatorio', 'cybersecurity']
|
||
},
|
||
{
|
||
codigo: 'anormalidade',
|
||
nome: 'Anomalia Detectada',
|
||
titulo: '⚠️ Anomalia Detectada: {{tipoAtaque}}',
|
||
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: #F59E0B;'>⚠️ Anomalia Detectada</h2>" +
|
||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||
'<p>O sistema detectou uma <strong>anomalia de segurança</strong> que requer atenção:</p>' +
|
||
"<div style='background-color: #FFFBEB; border-left: 4px solid #F59E0B; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0;'><strong>Tipo de Ataque:</strong> {{tipoAtaque}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Severidade:</strong> {{severidade}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Descrição:</strong> {{descricao}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>IP de Origem:</strong> {{origemIp}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{dataHora}}</p>" +
|
||
'</div>' +
|
||
"<p style='margin-top: 30px;'>" +
|
||
"<a href='{{urlSistema}}/ti/cybersecurity' " +
|
||
"style='background-color: #F59E0B; color: white; padding: 12px 24px; " +
|
||
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
|
||
'Ver Detalhes da Anomalia' +
|
||
'</a>' +
|
||
'</p>' +
|
||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
|
||
'</p>' +
|
||
'</div></body></html>',
|
||
variaveis: [
|
||
'destinatarioNome',
|
||
'tipoAtaque',
|
||
'severidade',
|
||
'descricao',
|
||
'origemIp',
|
||
'dataHora',
|
||
'urlSistema'
|
||
],
|
||
categoria: 'email' as const,
|
||
tags: ['seguranca', 'anomalia', 'alerta', 'cybersecurity']
|
||
},
|
||
// ===================== NOTIFICAÇÕES DE ERROS DO SERVIDOR =====================
|
||
{
|
||
codigo: 'ERRO_SERVIDOR_404',
|
||
nome: 'Erro 404 - Página Não Encontrada',
|
||
titulo: '⚠️ Erro 404 - Página não encontrada',
|
||
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: #F59E0B;'>⚠️ Erro 404 - Página Não Encontrada</h2>" +
|
||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||
'<p>O sistema detectou uma tentativa de acesso a uma página que não existe:</p>' +
|
||
"<div style='background-color: #FFFBEB; border-left: 4px solid #F59E0B; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0;'><strong>URL:</strong> {{url}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Método HTTP:</strong> {{method}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Mensagem:</strong> {{mensagem}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{timestamp}}</p>" +
|
||
'</div>' +
|
||
"<p style='color: #6B7280; font-size: 14px; margin-top: 20px;'>" +
|
||
'<strong>Possíveis causas:</strong><br>' +
|
||
'• Link quebrado ou desatualizado<br>' +
|
||
'• URL digitada incorretamente<br>' +
|
||
'• Página movida ou removida<br>' +
|
||
'• Tentativa de acesso a recurso inexistente' +
|
||
'</p>' +
|
||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de TI' +
|
||
'</p>' +
|
||
'</div></body></html>',
|
||
variaveis: ['destinatarioNome', 'url', 'method', 'mensagem', 'timestamp'],
|
||
categoria: 'email' as const,
|
||
tags: ['erro', '404', 'servidor', 'notificacao', 'ti']
|
||
},
|
||
{
|
||
codigo: 'ERRO_SERVIDOR_500',
|
||
nome: 'Erro 500 - Erro Interno do Servidor',
|
||
titulo: '🚨 Erro 500 - Erro Interno do Servidor',
|
||
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;'>🚨 Erro 500 - Erro Interno do Servidor</h2>" +
|
||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||
'<p>O sistema detectou um <strong>erro interno do servidor</strong> que requer atenção imediata:</p>' +
|
||
"<div style='background-color: #FEF2F2; border-left: 4px solid #DC2626; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0;'><strong>Código HTTP:</strong> {{statusCode}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>URL:</strong> {{url}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Método HTTP:</strong> {{method}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Mensagem:</strong> {{mensagem}}</p>" +
|
||
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{timestamp}}</p>" +
|
||
'</div>' +
|
||
"<div style='background-color: #F9FAFB; border: 1px solid #E5E7EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||
"<p style='margin: 0; font-size: 12px; color: #6B7280;'><strong>Stack Trace:</strong></p>" +
|
||
"<pre style='margin: 10px 0 0 0; padding: 10px; background-color: #FFFFFF; border: 1px solid #E5E7EB; border-radius: 4px; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; overflow-x: auto;'>{{stack}}</pre>" +
|
||
'</div>' +
|
||
"<p style='color: #DC2626; font-weight: bold; margin-top: 20px;'>" +
|
||
'⚠️ AÇÃO IMEDIATA NECESSÁRIA' +
|
||
'</p>' +
|
||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de TI<br>' +
|
||
'Este é um alerta automático do sistema de monitoramento de erros.' +
|
||
'</p>' +
|
||
'</div></body></html>',
|
||
variaveis: [
|
||
'destinatarioNome',
|
||
'statusCode',
|
||
'url',
|
||
'method',
|
||
'mensagem',
|
||
'stack',
|
||
'timestamp'
|
||
],
|
||
categoria: 'email' as const,
|
||
tags: ['erro', '500', 'servidor', 'critico', 'notificacao', 'ti']
|
||
}
|
||
];
|
||
|
||
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.SITE_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 };
|
||
}
|
||
});
|