/** * Página Operações — Tickets e Cobertura de PROCs * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { useState, useEffect, useCallback } from 'react' import { motion } from 'framer-motion' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts' import { Ticket, AlertTriangle, Clock, FileText, Building2, RefreshCw, } from 'lucide-react' // --------------------------------------------------------------------------- // Tipos // --------------------------------------------------------------------------- interface TicketsByDept { dept: string count: number } interface CoverageItem { dept: string procs: number total_expected: number pct: number } interface OperationsData { tickets: { open: number high_priority: number avg_response_hours: number by_department: TicketsByDept[] } procedures: { total: number departments: number coverage: CoverageItem[] } } // --------------------------------------------------------------------------- // Animações // --------------------------------------------------------------------------- const containerVariants = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.05 } }, } const itemVariants = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 }, } // --------------------------------------------------------------------------- // Sub-componentes // --------------------------------------------------------------------------- const StatCard = ({ label, value, icon: Icon, color, sub, }: { label: string value: string icon: React.ElementType color: string sub?: string }) => (
{label}
{value}
{sub &&
{sub}
}
) const CustomTooltip = ({ active, payload, label }: { active?: boolean payload?: { color: string; name: string; value: number }[] label?: string }) => { if (!active || !payload?.length) return null return (

{label}

{payload.map((p, i) => (

{p.name}: {p.value}

))}
) } // Barra de progresso para cobertura de PROCs const ProgressBar = ({ pct }: { pct: number }) => { const color = pct >= 80 ? 'bg-emerald-500' : pct >= 50 ? 'bg-amber-500' : 'bg-red-500' return (
) } // --------------------------------------------------------------------------- // Página principal // --------------------------------------------------------------------------- export default function Operations() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const fetchData = useCallback(async () => { setRefreshing(true) try { const response = await fetch('/api/operations') if (!response.ok) throw new Error('Resposta inválida') const json: OperationsData = await response.json() setData(json) } catch (err) { console.error('[Operations] Erro ao carregar dados:', err) } finally { setLoading(false) setRefreshing(false) } }, []) useEffect(() => { fetchData() }, [fetchData]) // Estado de carregamento if (loading) { return (

A carregar dados de operações...

) } if (!data) return null // Cor do card de alta prioridade: vermelho se existirem tickets críticos const priorityColor = data.tickets.high_priority > 0 ? 'bg-red-500/20' : 'bg-zinc-700/40' return (
{/* Cabeçalho */}

Operações

Tickets de Suporte e Cobertura de Procedimentos

{/* Cards de resumo */}
0 ? 'Requerem atenção imediata' : 'Sem tickets críticos'} />
{/* Gráfico de barras horizontal + Resumo de PROCs */}
{/* Tickets por departamento (barra horizontal) */}

Tickets por Departamento

} />
{/* Resumo de PROCs */}

Procedimentos

Total de PROCs {data.procedures.total}
Departamentos cobertos {data.procedures.departments}
Cobertura global {Math.round( (data.procedures.coverage.reduce((s, d) => s + d.procs, 0) / data.procedures.coverage.reduce((s, d) => s + d.total_expected, 0)) * 100 )}%
{/* Tabela de cobertura por departamento */}

Cobertura de PROCs por Departamento

{data.procedures.coverage.map((item, i) => ( ))}
Departamento PROCs Esperados Cobertura %
{item.dept} {item.procs} {item.total_expected} = 80 ? '#34d399' : item.pct >= 50 ? '#fbbf24' : '#f87171', }} > {item.pct}%
) }