From 55847e2a770912266313537dc772a733fe15a55c Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 17 Nov 2025 09:33:33 -0300 Subject: [PATCH] feat: add SLA statistics and real-time monitoring to central-chamados - Introduced new queries to fetch SLA statistics and real-time SLA data for better ticket management insights. - Enhanced the central-chamados route to display SLA performance metrics, including compliance rates and ticket statuses by priority. - Implemented fallback logic for statistics calculation to ensure data availability even when queries return undefined. - Refactored the UI to include a dedicated section for SLA performance, improving user experience and data visibility. --- .../lib/components/chamados/SlaChart.svelte | 183 +++++++++ .../ti/central-chamados/+page.svelte | 367 ++++++++---------- packages/backend/convex/chamados.ts | 133 +++++-- 3 files changed, 447 insertions(+), 236 deletions(-) create mode 100644 apps/web/src/lib/components/chamados/SlaChart.svelte diff --git a/apps/web/src/lib/components/chamados/SlaChart.svelte b/apps/web/src/lib/components/chamados/SlaChart.svelte new file mode 100644 index 0000000..52d1b96 --- /dev/null +++ b/apps/web/src/lib/components/chamados/SlaChart.svelte @@ -0,0 +1,183 @@ + + +
+ +
+ diff --git a/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte b/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte index fcfef8b..a8b1ddf 100644 --- a/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte @@ -11,6 +11,7 @@ } from "$lib/utils/chamados"; import { useConvexWithAuth } from "$lib/hooks/useConvexWithAuth"; import { authStore } from "$lib/stores/auth.svelte"; + import SlaChart from "$lib/components/chamados/SlaChart.svelte"; type Ticket = Doc<"tickets">; type Usuario = Doc<"usuarios">; @@ -30,6 +31,8 @@ const usuariosQuery = useQuery(api.usuarios.listar, {}); const slaConfigsQuery = useQuery(api.chamados.listarSlaConfigs, {}); const templatesQuery = useQuery(api.templatesMensagens.listarTemplates, {}); + const estatisticasQuery = useQuery(api.chamados.obterEstatisticasChamados, {}); + const dadosSlaGraficoQuery = useQuery(api.chamados.obterDadosSlaGrafico, {}); // Extrair dados dos templates const templates = $derived.by(() => { @@ -102,8 +105,6 @@ let prorrogacaoFeedback = $state(null); let criandoTemplates = $state(false); let templatesFeedback = $state(null); - let migrandoSLAs = $state(false); - let migracaoFeedback = $state(null); let carregamentoToken = 0; @@ -237,7 +238,25 @@ return usuariosFiltrados; }); - const estatisticas = $derived(() => { + const estatisticas = $derived.by(() => { + // Usar query de estatísticas se disponível, senão calcular localmente + if (estatisticasQuery !== undefined && estatisticasQuery !== null) { + // useQuery retorna dados diretamente ou em propriedade data + let dadosEstatisticas: { total: number; abertos: number; emAndamento: number; vencidos: number } | null = null; + + if (typeof estatisticasQuery === 'object') { + if ('data' in estatisticasQuery && estatisticasQuery.data) { + dadosEstatisticas = estatisticasQuery.data as { total: number; abertos: number; emAndamento: number; vencidos: number }; + } else if ('total' in estatisticasQuery) { + dadosEstatisticas = estatisticasQuery as { total: number; abertos: number; emAndamento: number; vencidos: number }; + } + } + + if (dadosEstatisticas) { + return dadosEstatisticas; + } + } + // Fallback: calcular com base nos tickets carregados const total = tickets.length; const abertos = tickets.filter((t) => t.status === "aberto").length; const emAndamento = tickets.filter((t) => t.status === "em_andamento").length; @@ -438,23 +457,6 @@ } } - async function migrarSlaConfigs() { - try { - migrandoSLAs = true; - migracaoFeedback = null; - const resultado = await client.mutation(api.chamados.migrarSlaConfigs, {}); - migracaoFeedback = `Migração concluída: ${resultado?.migrados || 0} SLA(s) migrado(s) de ${resultado?.total || 0} total`; - // Recarregar SLAs após migração - await new Promise((resolve) => setTimeout(resolve, 1000)); - // Recarregar página para atualizar dados - window.location.reload(); - } catch (error) { - console.error("Erro ao migrar SLAs:", error); - migracaoFeedback = error instanceof Error ? error.message : "Erro ao migrar SLAs"; - } finally { - migrandoSLAs = false; - } - } // Debug: ver templates carregados (remover em produção) // $effect(() => { @@ -468,22 +470,96 @@

Total de chamados

-

{estatisticas.total}

+

{estatisticas.total ?? 0}

Abertos

-

{estatisticas.abertos}

+

{estatisticas.abertos ?? 0}

Em andamento

-

{estatisticas.emAndamento}

+

{estatisticas.emAndamento ?? 0}

Vencidos/Cancelados

-

{estatisticas.vencidos}

+

{estatisticas.vencidos ?? 0}

+ +
+
+
+

Performance de SLA

+

Monitoramento em tempo real do cumprimento de SLA por prioridade

+
+ {#if dadosSlaGraficoQuery !== undefined && dadosSlaGraficoQuery !== null} + {@const dadosSla = typeof dadosSlaGraficoQuery === 'object' && 'data' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery.data + : (typeof dadosSlaGraficoQuery === 'object' && 'taxaCumprimento' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery + : null)} + {#if dadosSla} +
+
+

Taxa de Cumprimento

+

+ {dadosSla.taxaCumprimento}% +

+
+
+

Última atualização

+

+ {new Date(dadosSla.atualizadoEm).toLocaleTimeString('pt-BR')} +

+
+
+ {/if} + {/if} +
+ + {#if dadosSlaGraficoQuery === undefined || dadosSlaGraficoQuery === null} +
+ +
+ {:else} + {@const dadosSla = typeof dadosSlaGraficoQuery === 'object' && 'data' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery.data + : (typeof dadosSlaGraficoQuery === 'object' && 'taxaCumprimento' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery + : null)} + {#if dadosSla} +
+
+

Dentro do Prazo

+

{dadosSla.statusSla.dentroPrazo}

+
+
+

Próximo Vencimento

+

{dadosSla.statusSla.proximoVencimento}

+
+
+

Vencidos

+

{dadosSla.statusSla.vencido}

+
+
+

Sem Prazo

+

{dadosSla.statusSla.semPrazo}

+
+
+ + {:else} +
+

Carregando dados de SLA...

+
+ {/if} + {/if} +
+
@@ -633,72 +709,73 @@ Atribuir responsável
-
-
-

Prorrogar prazo

-

Recurso exclusivo para a equipe de TI

-
-
- - -
-
- - -
-
- - -
-
- - -
- {#if prorrogacaoFeedback} -
- {prorrogacaoFeedback} + +
+

Prorrogar prazo

+

Recurso exclusivo para a equipe de TI

+
+
+ +
- {/if} - +
+ + +
+
+ + +
+
+ + +
+ {#if prorrogacaoFeedback} +
+ {prorrogacaoFeedback} +
+ {/if} + +
@@ -865,34 +942,9 @@
-
-
-

Configuração de SLA por Prioridade

-

Configure SLAs separados para cada nível de prioridade

-
-
- - -
- {#if migracaoFeedback} -
- {migracaoFeedback} -
- {/if} +
+

Configuração de SLA por Prioridade

+

Configure SLAs separados para cada nível de prioridade

@@ -1042,88 +1094,5 @@
{/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 499182d..7bfe140 100644 --- a/packages/backend/convex/chamados.ts +++ b/packages/backend/convex/chamados.ts @@ -520,6 +520,102 @@ 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>(); + 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")), @@ -718,40 +814,3 @@ export const generateUploadUrl = mutation({ }, }); -/** - * Migração: Adiciona o campo 'prioridade' aos SLAs antigos que não possuem - * Esta mutation corrige documentos criados antes da migração do schema - */ -export const migrarSlaConfigs = mutation({ - args: {}, - handler: async (ctx) => { - const usuario = await assertAuth(ctx); - - // Buscar todos os SLAs - const slaConfigs = await ctx.db.query("slaConfigs").collect(); - - let migrados = 0; - for (const sla of slaConfigs) { - // Verificar se o documento não tem o campo 'prioridade' - // Usando type assertion para acessar campos não tipados - const slaDoc = sla as any; - if (!slaDoc.prioridade) { - // Adicionar prioridade padrão "media" para SLAs antigos - await ctx.db.patch(sla._id, { - prioridade: "media" as "baixa" | "media" | "alta" | "critica", - atualizadoPor: usuario._id, - atualizadoEm: Date.now(), - }); - migrados++; - console.log(`✅ SLA migrado: ${sla.nome} (ID: ${sla._id})`); - } - } - - return { - sucesso: true, - migrados, - total: slaConfigs.length, - }; - }, -}); -