From fb784d6f7ebb189e1831a7cd55d4fd388afed8f7 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sun, 16 Nov 2025 15:41:16 -0300 Subject: [PATCH] refactor: simplify ticket form and SLA configuration handling - Removed SLA configuration selection from the TicketForm component to streamline the ticket creation process. - Updated the abrir-chamado route to eliminate unnecessary SLA loading logic and directly pass loading state to the TicketForm. - Enhanced the central-chamados route to support SLA configurations by priority, allowing for better management of SLA settings. - Introduced new mutations for SLA configuration management, including creation, updating, and deletion of SLA settings. - Improved email templates for ticket notifications, ensuring better communication with users regarding ticket status and updates. --- .../lib/components/chamados/TicketForm.svelte | 29 +- .../(dashboard)/abrir-chamado/+page.svelte | 63 +- .../ti/central-chamados/+page.svelte | 565 ++++++++++++++++-- packages/backend/convex/chamados.ts | 142 ++++- packages/backend/convex/schema.ts | 8 +- packages/backend/convex/templatesMensagens.ts | 109 ++++ 6 files changed, 755 insertions(+), 161 deletions(-) diff --git a/apps/web/src/lib/components/chamados/TicketForm.svelte b/apps/web/src/lib/components/chamados/TicketForm.svelte index ef2779b..132bcab 100644 --- a/apps/web/src/lib/components/chamados/TicketForm.svelte +++ b/apps/web/src/lib/components/chamados/TicketForm.svelte @@ -1,28 +1,23 @@
@@ -313,21 +499,94 @@
{#if assignFeedback} -

{assignFeedback}

+
+ {assignFeedback} +
{/if} +
+ + +
+

Prorrogar prazo

+

Recurso exclusivo para a equipe de TI

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {#if prorrogacaoFeedback} +
+ {prorrogacaoFeedback} +
+ {/if} +
@@ -336,56 +595,242 @@
-

Configuração de SLA

-

Defina tempos de resposta, conclusão e alertas.

+

Configuração de SLA por Prioridade

+

Configure SLAs separados para cada nível de prioridade

- {#if slaConfigsQuery?.data} - {#each slaConfigsQuery.data as sla (sla._id)} - +
+
+ + +
+ {#each ["baixa", "media", "alta", "critica"] as prioridade} + {@const slaAtual = slaConfigsPorPrioridade[prioridade]} +
+
+

{prioridade}

+ {#if slaAtual} + Configurado + {:else} + Não configurado + {/if} +
+ {#if slaAtual} +
+
+ Resposta: + {slaAtual.tempoRespostaHoras}h +
+
+ Conclusão: + {slaAtual.tempoConclusaoHoras}h +
+ {#if slaAtual.tempoEncerramentoHoras} +
+ Auto-encerramento: + {slaAtual.tempoEncerramentoHoras}h +
+ {/if} +
+
+ + +
+ {:else} + - {/each} + {/if} +
+ {/each} +
+ + +
+

+ {slaForm.slaId ? "Editar" : "Novo"} SLA - Prioridade {slaForm.prioridade.charAt(0).toUpperCase() + slaForm.prioridade.slice(1)} +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ {#if slaFeedback} +
+

{slaFeedback}

+
+ {/if} +
+ + {#if slaForm.slaId} + {/if}
+
-
-
- - + + {#if slaParaExcluir} + - {#if slaFeedback} -

{slaFeedback}

- {/if} - + {/if} + + +
+
+

Templates de Email - Chamados

+

Templates automáticos usados nas notificações de chamados.

+
+ +
+ {#if carregandoTemplates} +
+ +

Carregando templates...

+
+ {:else if templatesChamados.length === 0} +
+

Nenhum template encontrado

+

+ Os templates de chamados serão criados automaticamente quando o sistema for inicializado. + Clique no botão abaixo para criar os templates padrão agora. +

+ {#if templatesFeedback} +
+ {templatesFeedback} +
+ {/if} + +
+ {:else} + {#each templatesChamados as template (template._id)} +
+
+

{template.nome}

+ Sistema +
+

{template.titulo}

+ {#if template.variaveis && template.variaveis.length > 0} +
+

Variáveis:

+
+ {#each template.variaveis as variavel} + {{variavel}} + {/each} +
+
+ {/if} +
+

Código: {template.codigo || "N/A"}

+
+
+ {/each} + {:else} +
+

Carregando templates...

+
+ {/if} +
diff --git a/packages/backend/convex/chamados.ts b/packages/backend/convex/chamados.ts index 0d2a3cf..c98497e 100644 --- a/packages/backend/convex/chamados.ts +++ b/packages/backend/convex/chamados.ts @@ -100,14 +100,13 @@ function montarTimeline(base: number, prazos: ReturnType) return timeline; } -async function selecionarSlaConfig(ctx: Parameters[0], slaConfigId?: Id<"slaConfigs">) { - if (slaConfigId) { - return await ctx.db.get(slaConfigId); - } - +async function selecionarSlaConfig( + ctx: Parameters[0], + prioridade: "baixa" | "media" | "alta" | "critica" +): Promise | 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); @@ -486,7 +510,7 @@ export const salvarSlaConfig = mutation({ 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 +525,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 +540,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 +554,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"), diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 2b5291f..e08aeac 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -913,7 +913,12 @@ export default defineSchema({ slaConfigs: defineTable({ nome: v.string(), descricao: v.optional(v.string()), - setores: v.optional(v.array(v.string())), + prioridade: 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()), @@ -925,6 +930,7 @@ export default defineSchema({ atualizadoEm: v.number(), }) .index("by_ativo", ["ativo"]) + .index("by_prioridade", ["prioridade", "ativo"]) .index("by_nome", ["nome"]), ticketAssignments: defineTable({ diff --git a/packages/backend/convex/templatesMensagens.ts b/packages/backend/convex/templatesMensagens.ts index d76bc11..c7b027a 100644 --- a/packages/backend/convex/templatesMensagens.ts +++ b/packages/backend/convex/templatesMensagens.ts @@ -287,6 +287,115 @@ export const criarTemplatesPadrao = mutation({ + "", variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"], }, + { + codigo: "chamado_registrado", + nome: "Chamado Registrado", + titulo: "Chamado {{numeroTicket}} registrado", + corpo: "" + + "
" + + "

Chamado registrado com sucesso!

" + + "

Olá {{solicitante}},

" + + "

Recebemos sua solicitação e iniciaremos o atendimento em breve.

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Prioridade: {{prioridade}}

" + + "

Categoria: {{categoria}}

" + + "
" + + "

" + + "" + + "Acompanhar chamado" + + "" + + "

" + + "

" + + "Central de Chamados SGSE" + + "

" + + "
", + variaveis: ["solicitante", "numeroTicket", "prioridade", "categoria", "urlSistema"], + }, + { + codigo: "chamado_atualizado", + nome: "Atualização no Chamado", + titulo: "Atualização no chamado {{numeroTicket}}", + corpo: "" + + "
" + + "

Nova atualização no seu chamado

" + + "

Olá {{solicitante}},

" + + "

Há uma nova atualização no seu chamado:

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Mensagem:

" + + "

{{mensagem}}

" + + "
" + + "

" + + "" + + "Ver detalhes" + + "" + + "

" + + "

" + + "Central de Chamados SGSE" + + "

" + + "
", + variaveis: ["solicitante", "numeroTicket", "mensagem", "urlSistema"], + }, + { + codigo: "chamado_atribuido", + nome: "Chamado Atribuído", + titulo: "Chamado {{numeroTicket}} atribuído", + corpo: "" + + "
" + + "

Chamado atribuído

" + + "

Olá {{responsavel}},

" + + "

Um novo chamado foi atribuído para você:

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Solicitante: {{solicitante}}

" + + "

Prioridade: {{prioridade}}

" + + "

Descrição: {{descricao}}

" + + "
" + + "

" + + "" + + "Acessar chamado" + + "" + + "

" + + "

" + + "Central de Chamados SGSE" + + "

" + + "
", + variaveis: ["responsavel", "numeroTicket", "solicitante", "prioridade", "descricao", "urlSistema"], + }, + { + codigo: "chamado_alerta_prazo", + nome: "Alerta de Prazo do Chamado", + titulo: "⚠️ Alerta de prazo - Chamado {{numeroTicket}}", + corpo: "" + + "
" + + "

⚠️ Alerta de prazo

" + + "

Olá {{destinatario}},

" + + "

O chamado abaixo está próximo do prazo de {{tipoPrazo}}:

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Prazo de {{tipoPrazo}}: {{prazo}}

" + + "

Status: {{status}}

" + + "
" + + "

" + + "" + + "Ver chamado" + + "" + + "

" + + "

" + + "Central de Chamados SGSE" + + "

" + + "
", + variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"], + }, ]; for (const template of templatesPadrao) {