Merge branch 'feat-central-chamados' into feat-cibersecurity
This commit is contained in:
@@ -100,14 +100,13 @@ function montarTimeline(base: number, prazos: ReturnType<typeof calcularPrazos>)
|
||||
return timeline;
|
||||
}
|
||||
|
||||
async function selecionarSlaConfig(ctx: Parameters<typeof getCurrentUserFunction>[0], slaConfigId?: Id<"slaConfigs">) {
|
||||
if (slaConfigId) {
|
||||
return await ctx.db.get(slaConfigId);
|
||||
}
|
||||
|
||||
async function selecionarSlaConfig(
|
||||
ctx: Parameters<typeof getCurrentUserFunction>[0],
|
||||
prioridade: "baixa" | "media" | "alta" | "critica"
|
||||
): Promise<Doc<"slaConfigs"> | null> {
|
||||
return await ctx.db
|
||||
.query("slaConfigs")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.withIndex("by_prioridade", (q) => q.eq("prioridade", prioridade).eq("ativo", true))
|
||||
.first();
|
||||
}
|
||||
|
||||
@@ -122,6 +121,7 @@ async function registrarNotificacoes(
|
||||
) {
|
||||
const { ticket, titulo, mensagem, usuarioEvento } = params;
|
||||
|
||||
// Notificar solicitante
|
||||
if (ticket.solicitanteEmail) {
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: ticket.solicitanteEmail,
|
||||
@@ -142,6 +142,31 @@ async function registrarNotificacoes(
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
});
|
||||
|
||||
// Notificar responsável (se houver)
|
||||
if (ticket.responsavelId && ticket.responsavelId !== ticket.solicitanteId) {
|
||||
const responsavel = await ctx.db.get(ticket.responsavelId);
|
||||
if (responsavel?.email) {
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: responsavel.email,
|
||||
destinatarioId: ticket.responsavelId,
|
||||
assunto: `${titulo} - Chamado ${ticket.numero}`,
|
||||
corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE`,
|
||||
enviadoPor: usuarioEvento,
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert("notificacoes", {
|
||||
usuarioId: ticket.responsavelId,
|
||||
tipo: "nova_mensagem",
|
||||
...(ticket.conversaId ? { conversaId: ticket.conversaId } : {}),
|
||||
remetenteId: usuarioEvento,
|
||||
titulo,
|
||||
descricao: mensagem.length > 120 ? `${mensagem.slice(0, 117)}...` : mensagem,
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function registrarInteracao(
|
||||
@@ -185,7 +210,6 @@ export const abrirChamado = mutation({
|
||||
categoria: v.optional(v.string()),
|
||||
prioridade: prioridadeValidator,
|
||||
anexos: v.optional(v.array(arquivoValidator)),
|
||||
slaConfigId: v.optional(v.id("slaConfigs")),
|
||||
canalOrigem: v.optional(v.string()),
|
||||
},
|
||||
returns: v.object({
|
||||
@@ -195,7 +219,7 @@ export const abrirChamado = mutation({
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await assertAuth(ctx);
|
||||
const agora = Date.now();
|
||||
const sla = await selecionarSlaConfig(ctx, args.slaConfigId);
|
||||
const sla = await selecionarSlaConfig(ctx, args.prioridade);
|
||||
const prazos = calcularPrazos(agora, sla);
|
||||
const timeline = montarTimeline(agora, prazos);
|
||||
|
||||
@@ -304,8 +328,23 @@ export const listarChamadosTI = query({
|
||||
return true;
|
||||
});
|
||||
|
||||
filtrados.sort((a, b) => b.atualizadoEm - a.atualizadoEm);
|
||||
return args.limite ? filtrados.slice(0, args.limite) : filtrados;
|
||||
// Enriquecer tickets com nome do responsável
|
||||
const ticketsEnriquecidos = await Promise.all(
|
||||
filtrados.map(async (ticket) => {
|
||||
let responsavelNome: string | undefined = undefined;
|
||||
if (ticket.responsavelId) {
|
||||
const responsavel = await ctx.db.get(ticket.responsavelId);
|
||||
responsavelNome = responsavel?.nome;
|
||||
}
|
||||
return {
|
||||
...ticket,
|
||||
responsavelNome,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
ticketsEnriquecidos.sort((a, b) => b.atualizadoEm - a.atualizadoEm);
|
||||
return args.limite ? ticketsEnriquecidos.slice(0, args.limite) : ticketsEnriquecidos;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -481,12 +520,108 @@ export const listarSlaConfigs = query({
|
||||
},
|
||||
});
|
||||
|
||||
export const obterEstatisticasChamados = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
await assertAuth(ctx);
|
||||
const todosTickets = await ctx.db.query("tickets").collect();
|
||||
|
||||
const total = todosTickets.length;
|
||||
const abertos = todosTickets.filter((t) => t.status === "aberto").length;
|
||||
const emAndamento = todosTickets.filter((t) => t.status === "em_andamento").length;
|
||||
const vencidos = todosTickets.filter(
|
||||
(t) => (t.prazoConclusao && t.prazoConclusao < Date.now()) || t.status === "cancelado"
|
||||
).length;
|
||||
|
||||
return { total, abertos, emAndamento, vencidos };
|
||||
},
|
||||
});
|
||||
|
||||
export const obterDadosSlaGrafico = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
await assertAuth(ctx);
|
||||
const agora = Date.now();
|
||||
const todosTickets = await ctx.db.query("tickets").collect();
|
||||
const slaConfigs = await ctx.db.query("slaConfigs").collect();
|
||||
|
||||
// Agrupar SLAs por prioridade
|
||||
const slaPorPrioridade = new Map<string, Doc<"slaConfigs">>();
|
||||
slaConfigs.filter(s => s.ativo).forEach(sla => {
|
||||
if (sla.prioridade) {
|
||||
slaPorPrioridade.set(sla.prioridade, sla);
|
||||
}
|
||||
});
|
||||
|
||||
// Calcular status de SLA para cada ticket
|
||||
const statusSla = {
|
||||
dentroPrazo: 0,
|
||||
proximoVencimento: 0,
|
||||
vencido: 0,
|
||||
semPrazo: 0,
|
||||
};
|
||||
|
||||
const porPrioridade = {
|
||||
baixa: { dentroPrazo: 0, proximoVencimento: 0, vencido: 0, total: 0 },
|
||||
media: { dentroPrazo: 0, proximoVencimento: 0, vencido: 0, total: 0 },
|
||||
alta: { dentroPrazo: 0, proximoVencimento: 0, vencido: 0, total: 0 },
|
||||
critica: { dentroPrazo: 0, proximoVencimento: 0, vencido: 0, total: 0 },
|
||||
};
|
||||
|
||||
todosTickets.forEach(ticket => {
|
||||
if (!ticket.prazoConclusao) {
|
||||
statusSla.semPrazo++;
|
||||
return;
|
||||
}
|
||||
|
||||
const prazoConclusao = ticket.prazoConclusao;
|
||||
const horasRestantes = (prazoConclusao - agora) / (1000 * 60 * 60);
|
||||
const sla = slaPorPrioridade.get(ticket.prioridade);
|
||||
const alertaHoras = sla?.alertaAntecedenciaHoras ?? 2;
|
||||
|
||||
if (prazoConclusao < agora) {
|
||||
statusSla.vencido++;
|
||||
if (ticket.prioridade && porPrioridade[ticket.prioridade as keyof typeof porPrioridade]) {
|
||||
porPrioridade[ticket.prioridade as keyof typeof porPrioridade].vencido++;
|
||||
porPrioridade[ticket.prioridade as keyof typeof porPrioridade].total++;
|
||||
}
|
||||
} else if (horasRestantes <= alertaHoras) {
|
||||
statusSla.proximoVencimento++;
|
||||
if (ticket.prioridade && porPrioridade[ticket.prioridade as keyof typeof porPrioridade]) {
|
||||
porPrioridade[ticket.prioridade as keyof typeof porPrioridade].proximoVencimento++;
|
||||
porPrioridade[ticket.prioridade as keyof typeof porPrioridade].total++;
|
||||
}
|
||||
} else {
|
||||
statusSla.dentroPrazo++;
|
||||
if (ticket.prioridade && porPrioridade[ticket.prioridade as keyof typeof porPrioridade]) {
|
||||
porPrioridade[ticket.prioridade as keyof typeof porPrioridade].dentroPrazo++;
|
||||
porPrioridade[ticket.prioridade as keyof typeof porPrioridade].total++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calcular taxa de cumprimento
|
||||
const totalComPrazo = statusSla.dentroPrazo + statusSla.proximoVencimento + statusSla.vencido;
|
||||
const taxaCumprimento = totalComPrazo > 0
|
||||
? Math.round((statusSla.dentroPrazo / totalComPrazo) * 100)
|
||||
: 100;
|
||||
|
||||
return {
|
||||
statusSla,
|
||||
porPrioridade,
|
||||
taxaCumprimento,
|
||||
totalComPrazo,
|
||||
atualizadoEm: agora,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const salvarSlaConfig = mutation({
|
||||
args: {
|
||||
slaId: v.optional(v.id("slaConfigs")),
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
setores: v.optional(v.array(v.string())),
|
||||
prioridade: prioridadeValidator,
|
||||
tempoRespostaHoras: v.number(),
|
||||
tempoConclusaoHoras: v.number(),
|
||||
tempoEncerramentoHoras: v.optional(v.number()),
|
||||
@@ -501,7 +636,7 @@ export const salvarSlaConfig = mutation({
|
||||
await ctx.db.patch(args.slaId, {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
setores: args.setores,
|
||||
prioridade: args.prioridade,
|
||||
tempoRespostaHoras: args.tempoRespostaHoras,
|
||||
tempoConclusaoHoras: args.tempoConclusaoHoras,
|
||||
tempoEncerramentoHoras: args.tempoEncerramentoHoras,
|
||||
@@ -516,7 +651,7 @@ export const salvarSlaConfig = mutation({
|
||||
return await ctx.db.insert("slaConfigs", {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
setores: args.setores,
|
||||
prioridade: args.prioridade,
|
||||
tempoRespostaHoras: args.tempoRespostaHoras,
|
||||
tempoConclusaoHoras: args.tempoConclusaoHoras,
|
||||
tempoEncerramentoHoras: args.tempoEncerramentoHoras,
|
||||
@@ -530,6 +665,102 @@ export const salvarSlaConfig = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const excluirSlaConfig = mutation({
|
||||
args: {
|
||||
slaId: v.id("slaConfigs"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await assertAuth(ctx);
|
||||
const sla = await ctx.db.get(args.slaId);
|
||||
if (!sla) {
|
||||
throw new Error("Configuração de SLA não encontrada");
|
||||
}
|
||||
await ctx.db.delete(args.slaId);
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const prorrogarChamado = mutation({
|
||||
args: {
|
||||
ticketId: v.id("tickets"),
|
||||
horasAdicionais: v.number(),
|
||||
prazo: v.union(v.literal("resposta"), v.literal("conclusao")),
|
||||
motivo: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await assertAuth(ctx);
|
||||
const ticket = await ctx.db.get(args.ticketId);
|
||||
if (!ticket) {
|
||||
throw new Error("Chamado não encontrado");
|
||||
}
|
||||
|
||||
const agora = Date.now();
|
||||
const horasMs = args.horasAdicionais * 60 * 60 * 1000;
|
||||
let novoPrazoResposta = ticket.prazoResposta;
|
||||
let novoPrazoConclusao = ticket.prazoConclusao;
|
||||
let prazoExtendido: number;
|
||||
|
||||
if (args.prazo === "resposta") {
|
||||
prazoExtendido = (ticket.prazoResposta || agora) + horasMs;
|
||||
novoPrazoResposta = prazoExtendido;
|
||||
// Se o prazo de conclusão é antes do novo prazo de resposta, ajuste-o também
|
||||
if (ticket.prazoConclusao && ticket.prazoConclusao < prazoExtendido) {
|
||||
novoPrazoConclusao = prazoExtendido + (ticket.prazoConclusao - (ticket.prazoResposta || agora));
|
||||
}
|
||||
} else {
|
||||
prazoExtendido = (ticket.prazoConclusao || agora) + horasMs;
|
||||
novoPrazoConclusao = prazoExtendido;
|
||||
}
|
||||
|
||||
// Atualizar timeline
|
||||
const timelineAtualizada = ticket.timeline?.map((etapa) => {
|
||||
if (args.prazo === "resposta" && etapa.etapa === "resposta_inicial") {
|
||||
return {
|
||||
...etapa,
|
||||
prazo: prazoExtendido,
|
||||
status: prazoExtendido > agora ? "pendente" : etapa.status,
|
||||
};
|
||||
}
|
||||
if (args.prazo === "conclusao" && etapa.etapa === "conclusao") {
|
||||
return {
|
||||
...etapa,
|
||||
prazo: prazoExtendido,
|
||||
status: prazoExtendido > agora ? "pendente" : etapa.status,
|
||||
};
|
||||
}
|
||||
return etapa;
|
||||
}) || ticket.timeline;
|
||||
|
||||
await ctx.db.patch(ticket._id, {
|
||||
prazoResposta: novoPrazoResposta,
|
||||
prazoConclusao: novoPrazoConclusao,
|
||||
timeline: timelineAtualizada,
|
||||
atualizadoEm: agora,
|
||||
ultimaInteracaoEm: agora,
|
||||
});
|
||||
|
||||
await registrarInteracao(ctx, {
|
||||
ticketId: ticket._id,
|
||||
autorId: usuario._id,
|
||||
origem: "ti",
|
||||
tipo: "status",
|
||||
conteudo: `Prazo ${args.prazo === "resposta" ? "de resposta" : "de conclusão"} prorrogado em ${args.horasAdicionais}h. Motivo: ${args.motivo}`,
|
||||
});
|
||||
|
||||
const ticketAtualizado = await ctx.db.get(ticket._id);
|
||||
if (ticketAtualizado) {
|
||||
await registrarNotificacoes(ctx, {
|
||||
ticket: ticketAtualizado,
|
||||
titulo: `Prazo prorrogado - Chamado ${ticketAtualizado.numero}`,
|
||||
mensagem: `O prazo ${args.prazo === "resposta" ? "de resposta" : "de conclusão"} foi prorrogado em ${args.horasAdicionais} horas. Motivo: ${args.motivo}`,
|
||||
usuarioEvento: usuario._id,
|
||||
});
|
||||
}
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const emitirAlertaPrazo = mutation({
|
||||
args: {
|
||||
ticketId: v.id("tickets"),
|
||||
|
||||
@@ -1027,7 +1027,14 @@ export default defineSchema({
|
||||
slaConfigs: defineTable({
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
setores: v.optional(v.array(v.string())),
|
||||
prioridade: v.optional(
|
||||
v.union(
|
||||
v.literal("baixa"),
|
||||
v.literal("media"),
|
||||
v.literal("alta"),
|
||||
v.literal("critica")
|
||||
)
|
||||
),
|
||||
tempoRespostaHoras: v.number(),
|
||||
tempoConclusaoHoras: v.number(),
|
||||
tempoEncerramentoHoras: v.optional(v.number()),
|
||||
@@ -1039,6 +1046,7 @@ export default defineSchema({
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_ativo", ["ativo"])
|
||||
.index("by_prioridade", ["prioridade", "ativo"])
|
||||
.index("by_nome", ["nome"]),
|
||||
|
||||
ticketAssignments: defineTable({
|
||||
|
||||
@@ -287,6 +287,115 @@ export const criarTemplatesPadrao = mutation({
|
||||
+ "</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) {
|
||||
|
||||
Reference in New Issue
Block a user