/** * 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 { // 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( `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( `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( `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( `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, }, } }