feat: update system branding and improve user interface consistency
- Changed all instances of "Sistema de Gerenciamento da Secretaria de Esportes" to "Sistema de Gerenciamento de Secretaria" for a more concise branding. - Enhanced the PrintModal component with a user-friendly interface for selecting sections to include in PDF generation. - Improved error handling and user feedback during PDF generation processes. - Updated various components and routes to reflect the new branding, ensuring consistency across the application.
This commit is contained in:
@@ -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(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/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(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/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 };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user