Merge remote-tracking branch 'origin' into feat-licitacoes-contratos

This commit is contained in:
2025-11-18 11:18:26 -03:00
29 changed files with 4470 additions and 3550 deletions

View File

@@ -1,233 +1,233 @@
"use node";
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
import { decryptSMTPPasswordNode } from "./utils/nodeCrypto";
import nodemailer from "nodemailer";
export const enviar = action({
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
"use node";
let email;
try {
// Buscar email da fila
email = await ctx.runQuery(internal.email.getEmailById, {
emailId: args.emailId,
});
if (!email) {
return { sucesso: false, erro: "Email não encontrado" };
}
// Buscar configuração SMTP ativa
const configRaw = await ctx.runQuery(
internal.email.getActiveEmailConfig,
{}
);
if (!configRaw) {
console.error(
"❌ Configuração SMTP não encontrada ou inativa para email:",
email.destinatario
);
return {
sucesso: false,
erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.",
};
}
console.log("📧 Tentando enviar email:", {
para: email.destinatario,
assunto: email.assunto,
servidor: configRaw.servidor,
porta: configRaw.porta,
});
// Descriptografar senha usando função compatível com Node.js
let senhaDescriptografada: string;
try {
senhaDescriptografada = await decryptSMTPPasswordNode(
configRaw.senhaHash
);
} catch (decryptError) {
const decryptErrorMessage =
decryptError instanceof Error
? decryptError.message
: String(decryptError);
console.error(
"Erro ao descriptografar senha SMTP:",
decryptErrorMessage
);
return {
sucesso: false,
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`,
};
}
const config = {
...configRaw,
senha: senhaDescriptografada,
};
// Config já foi validado acima
// Avisar mas não bloquear se não foi testado
if (!config.testadoEm) {
console.warn(
"⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim..."
);
}
// Marcar como enviando
await ctx.runMutation(internal.email.markEmailEnviando, {
emailId: args.emailId,
});
// Criar transporter do nodemailer com configuração melhorada
const transporterOptions: {
host: string;
port: number;
secure: boolean;
requireTLS?: boolean;
auth: {
user: string;
pass: string;
};
tls?: {
rejectUnauthorized: boolean;
ciphers?: string;
};
connectionTimeout: number;
greetingTimeout: number;
socketTimeout: number;
pool?: boolean;
maxConnections?: number;
maxMessages?: number;
} = {
host: config.servidor,
port: config.porta,
secure: config.usarSSL,
auth: {
user: config.usuario,
pass: config.senha, // Senha já descriptografada
},
connectionTimeout: 15000, // 15 segundos
greetingTimeout: 15000,
socketTimeout: 15000,
pool: true, // Usar pool de conexões
maxConnections: 5,
maxMessages: 100,
};
// Adicionar TLS apenas se necessário
if (config.usarTLS) {
transporterOptions.requireTLS = true;
transporterOptions.tls = {
rejectUnauthorized: false, // Permitir certificados autoassinados
};
} else if (config.usarSSL) {
transporterOptions.tls = {
rejectUnauthorized: false,
};
}
const transporter = nodemailer.createTransport(transporterOptions);
// Verificar conexão antes de enviar
try {
await transporter.verify();
console.log("✅ Conexão SMTP verificada com sucesso");
} catch (verifyError) {
const verifyErrorMessage =
verifyError instanceof Error
? verifyError.message
: String(verifyError);
console.warn(
"⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:",
verifyErrorMessage
);
// Não bloquear envio por falha na verificação, apenas avisar
}
// Validar email destinatário antes de enviar
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.destinatario)) {
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
}
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
const textoPlano = email.corpo
.replace(/<[^>]*>/g, "") // Remover tags HTML
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.trim();
// Enviar email
const info = await transporter.sendMail({
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
headers: {
"X-Mailer": "SGSE-Sistema",
"X-Priority": "3",
},
});
interface MessageInfo {
messageId?: string;
response?: string;
}
const messageInfo = info as MessageInfo;
console.log("✅ Email enviado com sucesso!", {
para: email.destinatario,
assunto: email.assunto,
messageId: messageInfo.messageId,
response: messageInfo.response,
});
// Marcar como enviado
await ctx.runMutation(internal.email.markEmailEnviado, {
emailId: args.emailId,
});
return { sucesso: true };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
console.error("❌ Erro ao enviar email:", {
emailId: args.emailId,
destinatario: email?.destinatario,
erro: errorMessage,
stack: errorStack,
});
// Marcar como falha com detalhes completos
const erroCompleto = errorStack
? `${errorMessage}\n\nStack: ${errorStack}`
: errorMessage;
await ctx.runMutation(internal.email.markEmailFalha, {
emailId: args.emailId,
erro: erroCompleto.substring(0, 2000), // Limitar tamanho do erro
});
return { sucesso: false, erro: errorMessage };
}
},
});
"use node";
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
import { decryptSMTPPasswordNode } from "./utils/nodeCrypto";
import nodemailer from "nodemailer";
export const enviar = action({
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
"use node";
let email;
try {
// Buscar email da fila
email = await ctx.runQuery(internal.email.getEmailById, {
emailId: args.emailId,
});
if (!email) {
return { sucesso: false, erro: "Email não encontrado" };
}
// Buscar configuração SMTP ativa
const configRaw = await ctx.runQuery(
internal.email.getActiveEmailConfig,
{}
);
if (!configRaw) {
console.error(
"❌ Configuração SMTP não encontrada ou inativa para email:",
email.destinatario
);
return {
sucesso: false,
erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.",
};
}
console.log("📧 Tentando enviar email:", {
para: email.destinatario,
assunto: email.assunto,
servidor: configRaw.servidor,
porta: configRaw.porta,
});
// Descriptografar senha usando função compatível com Node.js
let senhaDescriptografada: string;
try {
senhaDescriptografada = await decryptSMTPPasswordNode(
configRaw.senhaHash
);
} catch (decryptError) {
const decryptErrorMessage =
decryptError instanceof Error
? decryptError.message
: String(decryptError);
console.error(
"Erro ao descriptografar senha SMTP:",
decryptErrorMessage
);
return {
sucesso: false,
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`,
};
}
const config = {
...configRaw,
senha: senhaDescriptografada,
};
// Config já foi validado acima
// Avisar mas não bloquear se não foi testado
if (!config.testadoEm) {
console.warn(
"⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim..."
);
}
// Marcar como enviando
await ctx.runMutation(internal.email.markEmailEnviando, {
emailId: args.emailId,
});
// Criar transporter do nodemailer com configuração melhorada
const transporterOptions: {
host: string;
port: number;
secure: boolean;
requireTLS?: boolean;
auth: {
user: string;
pass: string;
};
tls?: {
rejectUnauthorized: boolean;
ciphers?: string;
};
connectionTimeout: number;
greetingTimeout: number;
socketTimeout: number;
pool?: boolean;
maxConnections?: number;
maxMessages?: number;
} = {
host: config.servidor,
port: config.porta,
secure: config.usarSSL,
auth: {
user: config.usuario,
pass: config.senha, // Senha já descriptografada
},
connectionTimeout: 15000, // 15 segundos
greetingTimeout: 15000,
socketTimeout: 15000,
pool: true, // Usar pool de conexões
maxConnections: 5,
maxMessages: 100,
};
// Adicionar TLS apenas se necessário
if (config.usarTLS) {
transporterOptions.requireTLS = true;
transporterOptions.tls = {
rejectUnauthorized: false, // Permitir certificados autoassinados
};
} else if (config.usarSSL) {
transporterOptions.tls = {
rejectUnauthorized: false,
};
}
const transporter = nodemailer.createTransport(transporterOptions);
// Verificar conexão antes de enviar
try {
await transporter.verify();
console.log("✅ Conexão SMTP verificada com sucesso");
} catch (verifyError) {
const verifyErrorMessage =
verifyError instanceof Error
? verifyError.message
: String(verifyError);
console.warn(
"⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:",
verifyErrorMessage
);
// Não bloquear envio por falha na verificação, apenas avisar
}
// Validar email destinatário antes de enviar
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.destinatario)) {
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
}
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
const textoPlano = email.corpo
.replace(/<[^>]*>/g, "") // Remover tags HTML
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.trim();
// Enviar email
const info = await transporter.sendMail({
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
headers: {
"X-Mailer": "SGSE-Sistema-de-Gerenciamento-de-Secretaria",
"X-Priority": "3",
},
});
interface MessageInfo {
messageId?: string;
response?: string;
}
const messageInfo = info as MessageInfo;
console.log("✅ Email enviado com sucesso!", {
para: email.destinatario,
assunto: email.assunto,
messageId: messageInfo.messageId,
response: messageInfo.response,
});
// Marcar como enviado
await ctx.runMutation(internal.email.markEmailEnviado, {
emailId: args.emailId,
});
return { sucesso: true };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
console.error("❌ Erro ao enviar email:", {
emailId: args.emailId,
destinatario: email?.destinatario,
erro: errorMessage,
stack: errorStack,
});
// Marcar como falha com detalhes completos
const erroCompleto = errorStack
? `${errorMessage}\n\nStack: ${errorStack}`
: errorMessage;
await ctx.runMutation(internal.email.markEmailFalha, {
emailId: args.emailId,
erro: erroCompleto.substring(0, 2000), // Limitar tamanho do erro
});
return { sucesso: false, erro: errorMessage };
}
},
});

View File

@@ -0,0 +1,72 @@
import { mutation } from './_generated/server';
import { v } from 'convex/values';
import { updatePassword } from './auth';
import { authComponent } from './auth';
/**
* Alterar senha do usuário autenticado
*/
export const alterarSenha = mutation({
args: {
token: v.string(), // Token não é usado, mas mantido para compatibilidade
senhaAtual: v.string(),
novaSenha: v.string()
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
try {
// Verificar se o usuário está autenticado
const authUser = await authComponent.safeGetAuthUser(ctx);
if (!authUser) {
return {
sucesso: false as const,
erro: 'Usuário não autenticado'
};
}
// Validar que a nova senha não está vazia
if (!args.novaSenha || args.novaSenha.trim().length === 0) {
return {
sucesso: false as const,
erro: 'A nova senha não pode estar vazia'
};
}
// Chamar a função de atualização de senha
await updatePassword(ctx, {
currentPassword: args.senhaAtual,
newPassword: args.novaSenha
});
return {
sucesso: true as const
};
} catch (error: any) {
// Capturar erros específicos do Better Auth
let mensagemErro = 'Erro ao alterar senha';
if (error?.message) {
mensagemErro = error.message;
} else if (typeof error === 'string') {
mensagemErro = error;
}
// Mensagens de erro mais amigáveis
if (mensagemErro.toLowerCase().includes('password') ||
mensagemErro.toLowerCase().includes('senha') ||
mensagemErro.toLowerCase().includes('incorrect') ||
mensagemErro.toLowerCase().includes('incorreta')) {
mensagemErro = 'Senha atual incorreta';
}
return {
sucesso: false as const,
erro: mensagemErro
};
}
}
});

View File

@@ -127,7 +127,7 @@ async function registrarNotificacoes(
destinatario: ticket.solicitanteEmail,
destinatarioId: ticket.solicitanteId,
assunto: `${titulo} - Chamado ${ticket.numero}`,
corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE`,
corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`,
enviadoPor: usuarioEvento,
});
}
@@ -151,7 +151,7 @@ async function registrarNotificacoes(
destinatario: responsavel.email,
destinatarioId: ticket.responsavelId,
assunto: `${titulo} - Chamado ${ticket.numero}`,
corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE`,
corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`,
enviadoPor: usuarioEvento,
});
}

View File

@@ -588,7 +588,8 @@ export const atualizarStatus = mutation({
v.literal("aguardando_aprovacao"),
v.literal("aprovado"),
v.literal("reprovado"),
v.literal("data_ajustada_aprovada")
v.literal("data_ajustada_aprovada"),
v.literal("Cancelado_RH")
),
usuarioId: v.id("usuarios"),
},

View File

@@ -636,7 +636,7 @@ export const getStatusSistema = query({
/**
* Atividade do banco no último minuto (agregada em buckets)
* Usa mensagensPorMinuto como proxy de atividade quando disponível.
* Usa logsAtividades e systemMetrics para calcular atividade real.
*/
export const getAtividadeBancoDados = query({
args: {},
@@ -652,6 +652,14 @@ export const getAtividadeBancoDados = query({
const agora = Date.now();
const haUmMinuto = agora - 60 * 1000;
// Buscar atividades reais do sistema
const atividadesRecentes = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
.order('asc')
.collect();
// Buscar métricas também (para mensagens se houver)
const metricasRecentes = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
@@ -666,15 +674,30 @@ export const getAtividadeBancoDados = query({
for (let i = 0; i < numBuckets; i++) {
const inicio = haUmMinuto + i * bucketSizeMs;
const fim = inicio + bucketSizeMs;
// Contar atividades de criação/inserção (entradas)
const atividadesBucket = atividadesRecentes.filter(
(a) => a.timestamp >= inicio && a.timestamp < fim
);
const entradasAtividades = atividadesBucket.filter(
a => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
).length;
// Contar atividades de exclusão/remoção (saídas)
const saidasAtividades = atividadesBucket.filter(
a => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
).length;
// Usar mensagensPorMinuto como adicional se disponível
const bucketMetricas = metricasRecentes.filter(
(m) => m.timestamp >= inicio && m.timestamp < fim
);
// Usar mensagensPorMinuto como proxy de "entradas"; "saídas" como fração
const somaMensagens =
bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
const entradas = Math.max(0, Math.round(somaMensagens));
const saidas = Math.max(0, Math.round(entradas * 0.6));
// Combinar atividades reais com métricas de mensagens
const entradas = Math.max(0, Math.round(entradasAtividades + somaMensagens * 0.3));
const saidas = Math.max(0, Math.round(saidasAtividades + somaMensagens * 0.2));
historico.push({ entradas, saidas });
}
@@ -684,7 +707,7 @@ export const getAtividadeBancoDados = query({
});
/**
* Distribuição de operações (estimada a partir das métricas)
* Distribuição de operações (calculada a partir de logsAtividades e métricas)
*/
export const getDistribuicaoRequisicoes = query({
args: {},
@@ -696,21 +719,43 @@ export const getDistribuicaoRequisicoes = query({
}),
handler: async (ctx) => {
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
// Buscar atividades reais do sistema
const atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.collect();
// Buscar métricas também
const metricas = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.order('desc')
.take(100);
const totalOps = Math.max(
// Contar operações de leitura (consultas, visualizações)
const leituras = atividades.filter(
a => a.acao === 'consultar' || a.acao === 'visualizar' || a.acao === 'listar' || a.acao === 'buscar'
).length;
// Contar operações de escrita (criar, editar, excluir)
const escritas = atividades.filter(
a => a.acao === 'criar' || a.acao === 'editar' || a.acao === 'excluir' ||
a.acao === 'inserir' || a.acao === 'atualizar' || a.acao === 'deletar' ||
a.acao === 'cadastrar' || a.acao === 'remover'
).length;
// Adicionar estimativa baseada em mensagens se disponível
const totalMensagens = Math.max(
0,
Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
);
const queries = Math.round(totalOps * 0.7);
const mutations = Math.max(0, totalOps - queries);
const leituras = queries;
const escritas = mutations;
// Queries são leituras + parte das mensagens (como consultas de chat)
const queries = leituras + Math.round(totalMensagens * 0.5);
// Mutations são escritas + parte das mensagens (como envio de mensagens)
const mutations = escritas + Math.round(totalMensagens * 0.3);
return { queries, mutations, leituras, escritas };
}

View File

@@ -366,7 +366,8 @@ export default defineSchema({
v.literal("aprovado"),
v.literal("reprovado"),
v.literal("data_ajustada_aprovada"),
v.literal("EmFérias")
v.literal("EmFérias"),
v.literal("Cancelado_RH")
),
gestorId: v.optional(v.id("usuarios")),
observacao: v.optional(v.string()),

View File

@@ -1,421 +1,421 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { registrarAtividade } from "./logsAtividades";
import { Doc } from "./_generated/dataModel";
/**
* Listar todos os templates
*/
export const listarTemplates = query({
args: {},
handler: async (ctx) => {
const templates = await ctx.db.query("templatesMensagens").collect();
return templates;
},
});
/**
* Obter template por código
*/
export const obterTemplatePorCodigo = query({
args: {
codigo: v.string(),
},
handler: async (ctx, args) => {
const template = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
.first();
return template;
},
});
/**
* Criar template customizado (apenas TI_MASTER)
*/
export const criarTemplate = mutation({
args: {
codigo: v.string(),
nome: v.string(),
titulo: v.string(),
corpo: v.string(),
variaveis: 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" };
}
// Criar template
const templateId = await ctx.db.insert("templatesMensagens", {
codigo: args.codigo,
nome: args.nome,
tipo: "customizado",
titulo: args.titulo,
corpo: args.corpo,
variaveis: args.variaveis,
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()),
variaveis: 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.variaveis !== undefined) updates.variaveis = args.variaveis;
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;
}
/**
* 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 Sistema de Gestão da Secretaria de Esportes!\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"
+ "</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"
+ "</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"
+ "</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"
+ "</p>"
+ "</div></body></html>",
variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"],
},
];
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 };
},
});
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { registrarAtividade } from "./logsAtividades";
import { Doc } from "./_generated/dataModel";
/**
* Listar todos os templates
*/
export const listarTemplates = query({
args: {},
handler: async (ctx) => {
const templates = await ctx.db.query("templatesMensagens").collect();
return templates;
},
});
/**
* Obter template por código
*/
export const obterTemplatePorCodigo = query({
args: {
codigo: v.string(),
},
handler: async (ctx, args) => {
const template = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
.first();
return template;
},
});
/**
* Criar template customizado (apenas TI_MASTER)
*/
export const criarTemplate = mutation({
args: {
codigo: v.string(),
nome: v.string(),
titulo: v.string(),
corpo: v.string(),
variaveis: 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" };
}
// Criar template
const templateId = await ctx.db.insert("templatesMensagens", {
codigo: args.codigo,
nome: args.nome,
tipo: "customizado",
titulo: args.titulo,
corpo: args.corpo,
variaveis: args.variaveis,
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()),
variaveis: 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.variaveis !== undefined) updates.variaveis = args.variaveis;
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;
}
/**
* 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"],
},
];
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 };
},
});