Files
DashDescomplicar/api/services/operations.ts
ealmeida 12f688ff7c 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>
2026-04-06 20:58:48 +01:00

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,
},
}
}