diff --git a/app/Http/Controllers/PostProxyController.php b/app/Http/Controllers/PostProxyController.php index 8a87872..e666828 100644 --- a/app/Http/Controllers/PostProxyController.php +++ b/app/Http/Controllers/PostProxyController.php @@ -388,9 +388,18 @@ public function operationState($area): JsonResponse $data = $response->json(); return response()->json($data); } - public function operationQueueState($area): JsonResponse + public function operationPromState($area): JsonResponse { - $response = Http::get("http://10.57.251.181:3014/operacion/{$area}"); + $response = Http::get("http://10.57.251.181:3015/tiempo-respuesta/{$area}"); + if (!$response->successful()) { + return response()->json(['error' => 'No se pudo obtener los datos'], 500); + } + $data = $response->json(); + return response()->json($data); + } + public function operationAgentSatus($area): JsonResponse + { + $response = Http::get("http://10.57.251.181:3016/operacion/{$area}"); if (!$response->successful()) { return response()->json(['error' => 'No se pudo obtener los datos'], 500); } diff --git a/resources/js/components/queuesAgentsModal.jsx b/resources/js/components/queuesAgentsModal.jsx new file mode 100644 index 0000000..12f3b88 --- /dev/null +++ b/resources/js/components/queuesAgentsModal.jsx @@ -0,0 +1,53 @@ +// ModalColasAgent.jsx +import React from 'react'; +import { + Glasses, +} from 'lucide-react'; +function ModalColasAgent({ titulo = "Agentes", usuarios = [], tipo }) { + const estadoColorMap = { + disponibles: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300', + ocupados: 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300', + en_pausa: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300', + }; + + const badgeClass = `text-xs font-semibold px-3 py-1 rounded-full ${estadoColorMap[tipo] ?? ''}`; + return ( +
+

+ {titulo} ({usuarios.length}) +

+ + {usuarios.length === 0 ? ( +

+ No hay agentes en este estado. +

+ ) : ( + + )} +
+ ); +} + +export default ModalColasAgent; diff --git a/resources/js/components/utils/formatTime.jsx b/resources/js/components/utils/formatTime.jsx new file mode 100644 index 0000000..2b81e3f --- /dev/null +++ b/resources/js/components/utils/formatTime.jsx @@ -0,0 +1,32 @@ +export default function TiempoFormateado({ tiempo }) { + if (tiempo === undefined || tiempo === null || tiempo === '') { + return ; + } + + let horas = 0; + let minutos = 0; + let segundos = 0; + + if (typeof tiempo === 'string' && tiempo.includes(':')) { + const partes = tiempo.split(':'); + horas = parseInt(partes[0], 10); + minutos = parseInt(partes[1], 10); + segundos = parseFloat(partes[2]); + } else if (!isNaN(tiempo)) { + const totalSegundos = parseFloat(tiempo); + horas = Math.floor(totalSegundos / 3600); + minutos = Math.floor((totalSegundos % 3600) / 60); + segundos = Math.floor(totalSegundos % 60); + } else { + return ; + } + + let resultado = ''; + if (horas > 0) { + resultado = `${horas}:${String(minutos).padStart(2, '0')}:${String(segundos).padStart(2, '0')}`; + } else { + resultado = `${minutos}:${String(segundos).padStart(2, '0')}`; + } + + return {resultado}; +} diff --git a/resources/js/components/welcome/agentRankingWidget.jsx b/resources/js/components/welcome/agentRankingWidget.jsx index a825846..f1a86e6 100644 --- a/resources/js/components/welcome/agentRankingWidget.jsx +++ b/resources/js/components/welcome/agentRankingWidget.jsx @@ -4,7 +4,7 @@ import DiscordLoader from '@/components/discordloader'; import { useLoadStatus } from "../context/loadContext"; import { Link, usePage } from "@inertiajs/react"; import { themeByProject } from '../utils/theme'; - +import TiempoFormateado from '@/components/utils/formatTime'; const getMedal = (rank) => { switch (rank) { @@ -34,6 +34,8 @@ export default function AgentRankingWidget() { .map((agente, index) => ({ ...agente, rank: index + 1 })); setAgentes(ordenados); + console.log(ordenados); + } else { console.warn("La respuesta no contiene un array de datos:", res.data); } @@ -49,7 +51,7 @@ export default function AgentRankingWidget() { }, []); return ( -
+
{loading || !allLoaded ? ( // ⛳ doble condición: hasta que TODOS estén listos ) : ( @@ -72,7 +74,7 @@ export default function AgentRankingWidget() {

{agente.agente}

- Directas: {agente.directas}, Transferidas: {agente.transferidas} + promedio: ,
Total en llamadas:

diff --git a/resources/js/components/welcome/callPerOperationChart.jsx b/resources/js/components/welcome/callPerOperationChart.jsx index 0837cc0..c6eb26e 100644 --- a/resources/js/components/welcome/callPerOperationChart.jsx +++ b/resources/js/components/welcome/callPerOperationChart.jsx @@ -13,7 +13,7 @@ import { useEffect, useState } from 'react'; import DiscordLoader from '@/components/discordloader'; import axios from "axios"; import { useLoadStatus } from "../context/loadContext"; -import { themeByProject } from "../utils/theme"; +import { themeByProject, getChartColors } from '../utils/theme'; ChartJS.register( CategoryScale, LinearScale, @@ -63,6 +63,8 @@ export default function CallsPerOperationChart() { const theme = themeByProject[proyecto]; const [loading, setLoading] = useState(true); const { allLoaded, markLoaded } = useLoadStatus(); + const chartColors = getChartColors(proyecto); + const [callData, setCallData] = useState({ Soporte: 0, Tramites: 0, @@ -111,8 +113,8 @@ export default function CallsPerOperationChart() { callData.Movil, callData.Pruebas ], - backgroundColor: 'rgba(168, 85, 247, 0.6)', - borderColor: 'rgba(147, 51, 234, 1)', + backgroundColor: chartColors.fill, + borderColor: chartColors.border, borderWidth: 1, borderRadius: 10, }, @@ -120,7 +122,7 @@ export default function CallsPerOperationChart() { }; return ( -
+
{!allLoaded ? ( ) : ( diff --git a/resources/js/hooks/useOperationModal.jsx b/resources/js/hooks/useOperationModal.jsx new file mode 100644 index 0000000..fcb4e48 --- /dev/null +++ b/resources/js/hooks/useOperationModal.jsx @@ -0,0 +1,15 @@ +import { useState } from 'react'; + +export default function useOperationModal() { + const [modal, setModal] = useState({ show: false, tipo: null }); + + const showModal = (tipo) => { + setModal({ show: true, tipo }); + }; + + const hideModal = () => { + setModal({ show: false, tipo: null }); + }; + + return { modal, showModal, hideModal }; +} diff --git a/resources/js/pages/agentState.jsx b/resources/js/pages/agentState.jsx index 9445270..aed7779 100644 --- a/resources/js/pages/agentState.jsx +++ b/resources/js/pages/agentState.jsx @@ -37,31 +37,33 @@ export default function TableAgents() {

Tabla de Ranking

- {/* + - */} -
-
-
-

Resumen del Día

-
    -
  • - 124 llamadas atendidas - Excelente desempeño general. -
  • -
  • - 32 llamadas en espera - Revisar horarios de mayor demanda. -
  • -
  • - 18 llamadas perdidas - Oportunidad de mejora en atención. -
  • -
-
- Última actualización: hace 5 minutos. +
+
+

Estadísticas Curiosas

+ +
    +
  • + 🏆 luisa611 fue el agente más rápido + Promedio de duración: 2:31 +
  • +
  • + 🧱 baguirre39 trabajó sin pausas por 3h 10m + Resistencia comprobada 💪 +
  • +
  • + 🎯 sramoss1 recibió 12 transferencias + ¡Más apoyo recibido! +
  • +
+ +
+ Última actualización: hace 5 minutos. +
+
diff --git a/resources/js/pages/operationState.jsx b/resources/js/pages/operationState.jsx index b9b2d10..7a3803f 100644 --- a/resources/js/pages/operationState.jsx +++ b/resources/js/pages/operationState.jsx @@ -8,6 +8,10 @@ import axios from 'axios'; import { useLoadStatus } from '@/components/context/loadContext'; import { themeByProject } from '@/components/utils/theme'; import { usePage } from "@inertiajs/react"; +import TiempoFormateado from '@/components/utils/formatTime'; +import AgentModalWrapper from '@/components/agentsModalWrapper'; +import useOperationModal from '@/hooks/useOperationModal'; +import ModalColasAgent from '@/components/queuesAgentsModal'; import { Hourglass, @@ -28,9 +32,12 @@ export default function CallsWaitingByOperation() { const [operation, setOperation] = useState(null); const [pollingInterval, setPollingInterval] = useState(null); const [stats, setStats] = useState([]); + const [promedio, setPromedio] = useState([]); + const [estadoAgentes, setEstadoAgentes] = useState([]); const { props } = usePage(); const proyecto = props?.auth?.user?.proyecto || 'AZZU'; const theme = themeByProject[proyecto]; + const { modal, showModal, hideModal } = useOperationModal(); @@ -46,29 +53,42 @@ export default function CallsWaitingByOperation() { .catch(() => console.error('Error fetching stats')); } - const fetchStastQueueOperation = (operation) => { + const fetchStastPromOperation = (operation) => { if (!operation) return; - fetch(`/api/operationQueueState/${operation}`) + fetch(`/api/operationPromState/${operation}`) .then(res => res.json()) .then(data => { - console.log('Datos crudos de la API:', data); - setStats(data); + // console.log('Datos crudos del promedio:', data); + setPromedio(data); }) .catch(() => console.error('Error fetching stats')); + } - + const fetchStatusAgentsOperation = (operation) => { + if (!operation) return; + + fetch(`/api/operationStatusAgentOperation/${operation}`) + .then(res => res.json()) + .then(data => { + // console.log('Datos crudos del estado del agente:', data); + setEstadoAgentes(data); + }) + .catch(() => console.error('Error fetching stats')); } + const startPolling = (operation) => { // Limpia cualquier intervalo existente antes de iniciar uno nuevo if (pollingInterval) { clearInterval(pollingInterval); } - fetchStastOperation(operation); // Realiza la primera solicitud inmediatamente + fetchStastOperation(operation); + (operation) // Realiza la primera solicitud inmediatamente const intervalId = setInterval(() => { fetchStastOperation(operation); + fetchStatusAgentsOperation(operation); }, 8000); @@ -90,7 +110,7 @@ export default function CallsWaitingByOperation() { .then(res => setUserOps(res.data.operations)) .catch(err => console.error('Error cargando operaciones', err)); }, []); - console.log('Operaciones asignadas:',userOps); + // console.log('Operaciones asignadas:',userOps); const customTheme = { root: { @@ -115,17 +135,17 @@ export default function CallsWaitingByOperation() {

-
+
-
+
{userOps.map((op) => ( - { startPolling(op); setOperation(op); }}> + { startPolling(op); setOperation(op); fetchStastPromOperation(op) }}> {op} ))} @@ -156,19 +176,40 @@ export default function CallsWaitingByOperation() {
- {stats.detalle_colas && stats.detalle_colas.map((cola) => ( -
-
-

Cola {cola.cola}

- {cola.llamadas} llamadas -
-
-

Total: {cola.agentes_totales}

-

Ocupados: {cola.agentes_ocupados}

-

Disponibles: {cola.agentes_disponibles}

-
-
- ))} + {stats.detalle_colas && + [...stats.detalle_colas] + .sort((a, b) => b.llamadas - a.llamadas) // ← ordena de mayor a menor + .map((cola) => { + let containerClass = + "p-3 rounded-lg border mb-4 transition-colors "; + + if (cola.llamadas > 5) { + containerClass += "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-300"; + } else if (cola.llamadas >= 1) { + containerClass += "border-blue-500 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/10 text-blue-800 dark:text-blue-300"; + } else { + containerClass += "border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-white"; + } + + return ( +
+
+

+ + Cola {cola.cola} +

+ + {cola.llamadas} llamadas + +
+
+

Total: {cola.agentes_totales}

+

Ocupados: {cola.agentes_ocupados}

+

Disponibles: {cola.agentes_disponibles}

+
+
+ ); + })}
@@ -192,7 +233,7 @@ export default function CallsWaitingByOperation() {
{!operation ? ( -
+

Análisis detallado por operación

Aún no has seleccionado una operación.

@@ -204,13 +245,14 @@ export default function CallsWaitingByOperation() { {/* Columna 1: Tiempo promedio de espera */}
-

01:38

+

Promedio actual en la operación {operation}

- Máximo histórico hoy: 04:15
- Objetivo ideal: ≤ 02:00 + Máximo histórico hoy:
+ Tiempo maximo de respuesta hoy:
+ Objetivo ideal: ≤ 01:00
@@ -218,26 +260,39 @@ export default function CallsWaitingByOperation() { {/* Columna 2: Agentes conectados */}
-

9 agentes conectados

-
    -
  • 🟢 4 disponibles
  • -
  • 🔴 3 ocupados
  • -
  • 🟡 2 en pausa
  • -
-
-

Top 3 agentes activos

-
    -
  • Karol.Ospina — 18 llamadas
  • -
  • David.Mendez — 14 llamadas
  • -
  • Andrea.Sierra — 13 llamadas
  • -
-
-
+

9 agentes conectados

+
    +
  • showModal('disponibles')}> + 🟢 {estadoAgentes?.total?.disponibles ?? 0} disponibles +
  • +
  • showModal('ocupados')}> + 🔴 {estadoAgentes?.total?.ocupados ?? 0} ocupados +
  • +
  • showModal('en_pausa')}> + 🟡 {estadoAgentes?.total?.en_pausa ?? 0} en pausa +
  • +
+
+

Has click en el status que desees para obtener mas informacion

+
    +
+
+
{/* */}
)} + {modal.show && ( + + + + )} +
diff --git a/routes/api.php b/routes/api.php index 1579b6c..590092e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,6 +19,7 @@ Route::post('/transfer-call', [PostProxyController::class, 'channelTransfer']); Route::get('/getCallsPerOperation', [PostProxyController::class, 'getCallsPerOperation']); Route::get('/operationState/{area}', [PostProxyController::class, 'operationState']); -Route::get('/operationQueueState/{area}', [PostProxyController::class, 'operationQueueState']); +Route::get('/operationPromState/{area}', [PostProxyController::class, 'operationPromState']); +Route::get('/operationStatusAgentOperation/{area}', [PostProxyController::class, 'operationAgentSatus']); // routes/api.php Route::post('/spy', [SpyController::class, 'start']);