12f688ff7c
Expansão do dashboard de 3 para 8 páginas com dados reais do stack: - MCPs: monitorização de 33 MCPs no gateway com ping e estado online/offline - n8n: 14 workflows com último run, duração e falhas 24h - Paperclip: 16 agentes operacionais, routines e issues (PostgreSQL) - IA/Claude: visão das 3 camadas (189 skills, 72 agents, 39 MCPs, CARL) - Operações: tickets Desk CRM por departamento + cobertura PROCs 16 ficheiros novos (3042 linhas), 3 existentes editados. Nova dependência: pg (PostgreSQL client para Paperclip). Audit: 0 vulnerabilidades (npm audit fix aplicado). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
/**
|
|
* Operations Dashboard Service — Tickets e Cobertura de PROCs
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
import db from '../db.js'
|
|
import type { RowDataPacket } from 'mysql2'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tipos
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface OperationsDashboard {
|
|
tickets: {
|
|
open: number
|
|
high_priority: number
|
|
avg_response_hours: number
|
|
by_department: { dept: string; count: number }[]
|
|
}
|
|
procedures: {
|
|
total: number
|
|
departments: number
|
|
coverage: { dept: string; procs: number; total_expected: number; pct: number }[]
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dados estáticos — cobertura de PROCs por departamento
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const PROC_COVERAGE = [
|
|
{ dept: 'D1 — Comercial', procs: 5, total_expected: 8 },
|
|
{ dept: 'D2 — Suporte', procs: 3, total_expected: 6 },
|
|
{ dept: 'D3 — Contabilidade', procs: 3, total_expected: 5 },
|
|
{ dept: 'D4 — RH', procs: 1, total_expected: 4 },
|
|
{ dept: 'D5 — Marketing', procs: 5, total_expected: 8 },
|
|
{ dept: 'D6 — Gestão', procs: 8, total_expected: 10 },
|
|
{ dept: 'D7 — Tecnologia', procs: 18, total_expected: 20 },
|
|
{ dept: 'Cross-Departamental', procs: 5, total_expected: 6 },
|
|
]
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Query principal
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function getOperationsDashboard(): Promise<OperationsDashboard> {
|
|
// Queries paralelas para melhor performance
|
|
const [
|
|
ticketsAbertosResult,
|
|
ticketsAltaPrioridadeResult,
|
|
ticketsPorDepartamentoResult,
|
|
tempoMedioRespostaResult,
|
|
] = await Promise.all([
|
|
// a) Total de tickets abertos (status 1=Open, 2=In Progress, 3=Answered)
|
|
db.query<RowDataPacket[]>(
|
|
`SELECT COUNT(*) as count
|
|
FROM tbltickets
|
|
WHERE status IN ('1','2','3')`
|
|
),
|
|
|
|
// b) Tickets de alta prioridade ainda abertos (priority 2=High, 3=Urgent)
|
|
db.query<RowDataPacket[]>(
|
|
`SELECT COUNT(*) as count
|
|
FROM tbltickets
|
|
WHERE priority IN (2,3)
|
|
AND status IN ('1','2','3')`
|
|
),
|
|
|
|
// c) Tickets abertos agrupados por departamento
|
|
db.query<RowDataPacket[]>(
|
|
`SELECT d.name as dept, COUNT(t.ticketid) as count
|
|
FROM tbltickets t
|
|
LEFT JOIN tbldepartments d ON t.department = d.departmentid
|
|
WHERE t.status IN ('1','2','3')
|
|
GROUP BY d.departmentid, d.name
|
|
ORDER BY count DESC`
|
|
),
|
|
|
|
// d) Tempo médio de resposta em horas (últimos 90 dias)
|
|
db.query<RowDataPacket[]>(
|
|
`SELECT ROUND(AVG(TIMESTAMPDIFF(HOUR, t.date, t.lastreply)), 1) as avg_hours
|
|
FROM tbltickets t
|
|
WHERE t.lastreply IS NOT NULL
|
|
AND t.lastreply != '0000-00-00 00:00:00'
|
|
AND t.date > DATE_SUB(CURDATE(), INTERVAL 90 DAY)`
|
|
),
|
|
])
|
|
|
|
// Extrair valores das queries
|
|
const ticketsAbertos = (ticketsAbertosResult[0][0] as RowDataPacket)?.count ?? 0
|
|
const ticketsAltaPrioridade = (ticketsAltaPrioridadeResult[0][0] as RowDataPacket)?.count ?? 0
|
|
const avgResponseHours = (tempoMedioRespostaResult[0][0] as RowDataPacket)?.avg_hours ?? 0
|
|
|
|
const byDepartment = (ticketsPorDepartamentoResult[0] as RowDataPacket[]).map(row => ({
|
|
dept: (row.dept as string) || 'Sem departamento',
|
|
count: Number(row.count) || 0,
|
|
}))
|
|
|
|
// Calcular cobertura em percentagem
|
|
const coverage = PROC_COVERAGE.map(item => ({
|
|
dept: item.dept,
|
|
procs: item.procs,
|
|
total_expected: item.total_expected,
|
|
pct: Math.round((item.procs / item.total_expected) * 100),
|
|
}))
|
|
|
|
const totalProcs = PROC_COVERAGE.reduce((sum, d) => sum + d.procs, 0)
|
|
const totalDepartments = PROC_COVERAGE.filter(d => d.dept.startsWith('D')).length
|
|
|
|
return {
|
|
tickets: {
|
|
open: Number(ticketsAbertos),
|
|
high_priority: Number(ticketsAltaPrioridade),
|
|
avg_response_hours: Number(avgResponseHours),
|
|
by_department: byDepartment,
|
|
},
|
|
procedures: {
|
|
total: totalProcs,
|
|
departments: totalDepartments,
|
|
coverage,
|
|
},
|
|
}
|
|
}
|