feat: adicionar 5 novos painéis ao dashboard (MCPs, n8n, Paperclip, IA, Operações)
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>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user