diff --git a/app/Http/Controllers/PostProxyController.php b/app/Http/Controllers/PostProxyController.php index a9be421..b754b2c 100644 --- a/app/Http/Controllers/PostProxyController.php +++ b/app/Http/Controllers/PostProxyController.php @@ -60,45 +60,54 @@ public function usersTable($area): JsonResponse return response()->json($data); } - public function userData($extension): JsonResponse - { - $response = Http::get("http://10.57.251.181:3005/extension/info?ext={$extension}"); - if (!$response->successful()) { - return response()->json(['error' => 'No se pudo obtener los datos'], 500); - } +public function userData($extension): JsonResponse +{ + $response = Http::get("http://10.57.251.181:3005/extension/info?ext={$extension}"); + if (!$response->successful()) { + return response()->json(['error' => 'No se pudo obtener los datos'], 500); + } - $data = $response->json(); + $data = $response->json(); - $texto = $data['member'] ?? $data['member2'] ?? null; + $texto = $data['member'] ?? $data['member2'] ?? null; - if ($texto) { - // Extraer nombre - $nombre = explode(' ', $texto)[0]; + if ($texto) { + $nombre = explode(' ', $texto)[0]; + preg_match_all('/\((.*?)\)/', $texto, $matches); + $estado = null; + $pausa = null; - // Extraer estado (Busy, In call, etc.) - preg_match_all('/\((.*?)\)/', $texto, $matches); - $estado = null; - $pausa = null; + foreach ($matches[1] as $match) { + if (str_contains($match, 'paused')) { + $pausa = $match; + } + if (in_array($match, ['Busy', 'On Hold', 'In call', 'Ringing', 'Not in use'])) { + $estado = $match; + } + } - foreach ($matches[1] as $match) { + // Si no encontramos la pausa en el primer texto, revisamos member2 + if (!$pausa && isset($data['member2'])) { + preg_match_all('/\((.*?)\)/', $data['member2'], $matches2); + foreach ($matches2[1] as $match) { if (str_contains($match, 'paused')) { - $pausa = $match; // ej: paused:ACW was 2108 secs ago - } - if (in_array($match, ['Busy', 'On Hold', 'In call', 'Ringing', 'Not in use'])) { - $estado = $match; + $pausa = $match; + break; } } - - $data['member'] = [ - 'nombre' => $nombre, - 'estado' => $estado, - 'pausa' => $pausa, - ]; } - return response()->json($data); + $data['member'] = [ + 'nombre' => $nombre, + 'estado' => $estado, + 'pausa' => $pausa, + ]; } + return response()->json($data); +} + + @@ -159,19 +168,87 @@ public function chanelHangup(Request $request) return response()->json(['error' => 'No se pudo colgar el canal'], 500); } - public function pausedExtension(Request $request) + public function pauseExtension(Request $request): JsonResponse { $extension = $request->input('extension'); - $response = Http::post('http://10.57.251.181:3000/channel/hangup', [ - 'channel' => $extension + if (!$extension) { + return response()->json(['error' => 'Extensión no proporcionada'], 400); + } + + $interface = "SIP/{$extension}"; + $queues = []; + for ($i = 1; $i <= 120; $i++) { + $queues[] = 'Q' . str_pad($i, 3, '0', STR_PAD_LEFT); + } + $paused = 1; + $reason = 'ACW'; + + $response = Http::post('http://10.57.251.181:3000/queue/pause', [ + 'queues' => $queues, + 'interface' => $interface, + 'paused' => $paused, + 'reason' => $reason, ]); if ($response->successful()) { - return response()->json(['message' => 'Extension pausada correctamente']); + return response()->json(['message' => 'Agente pausado correctamente']); } - return response()->json(['error' => 'No se pudo colgar el canal'], 500); + return response()->json(['error' => 'No se pudo pausar al agente'], 500); + } + + + public function unpauseExtension(Request $request): JsonResponse + { + $extension = $request->input('extension'); + + if (!$extension) { + return response()->json(['error' => 'Extensión no proporcionada'], 400); + } + + $interface = "SIP/{$extension}"; + $queues = []; + for ($i = 1; $i <= 120; $i++) { + $queues[] = 'Q' . str_pad($i, 3, '0', STR_PAD_LEFT); + } + $paused = 0; + $reason = 'ACW'; + + $response = Http::post('http://10.57.251.181:3000/queue/pause', [ + 'queues' => $queues, + 'interface' => $interface, + 'paused' => $paused, + 'reason' => $reason, + ]); + + if ($response->successful()) { + return response()->json(['message' => 'Agente despausado correctamente']); + } + + return response()->json(['error' => 'No se pudo pausar al agente'], 500); + } + + public function channelTransfer(Request $request): JsonResponse + { + $channel = $request->input('channel'); + $destiny = $request->input('destiny'); + + if (!$channel || !$destiny) { + return response()->json(['error' => 'Extensión no proporcionada'], 400); + } + + + $response = Http::post('http://10.57.251.181:3006/transferir', [ + 'canal' => $channel, + 'destino' => $destiny, + ]); + + if ($response->successful()) { + return response()->json(['message' => 'Llamada transferida correctamente']); + } + + return response()->json(['error' => 'No se pudo transferir la llamada'], 500); } } diff --git a/resources/js/components/actionsAgent/pausedExtension.jsx b/resources/js/components/actionsAgent/pausedExtension.jsx index d302dea..94ebeb1 100644 --- a/resources/js/components/actionsAgent/pausedExtension.jsx +++ b/resources/js/components/actionsAgent/pausedExtension.jsx @@ -1,7 +1,7 @@ -export default async function pausedExtension(extension) { +export default async function PauseExtension(extension) { try { - const response = await fetch('/api/paused-extension', { + const response = await fetch('/api/pause-extension', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -18,7 +18,7 @@ export default async function pausedExtension(extension) { return { success: false, message: data.error || 'Error desconocido' }; } } catch (error) { - console.error('Error pausando extension:', error); + console.error('Error al pausar:', error); return { success: false, message: 'Error de red o servidor' }; } -} +} \ No newline at end of file diff --git a/resources/js/components/actionsAgent/unpauseExtension.jsx b/resources/js/components/actionsAgent/unpauseExtension.jsx new file mode 100644 index 0000000..6a58849 --- /dev/null +++ b/resources/js/components/actionsAgent/unpauseExtension.jsx @@ -0,0 +1,24 @@ + +export default async function UnpauseExtension(extension) { + try { + const response = await fetch('/api/unpause-extension', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ extension }), + }); + + const data = await response.json(); + + if (response.ok) { + return { success: true, message: data.message }; + } else { + return { success: false, message: data.error || 'Error desconocido' }; + } + } catch (error) { + console.error('Error al pausar:', error); + return { success: false, message: 'Error de red o servidor' }; + } +} \ No newline at end of file diff --git a/resources/js/components/adminActionButton.jsx b/resources/js/components/adminActionButton.jsx new file mode 100644 index 0000000..eda47a8 --- /dev/null +++ b/resources/js/components/adminActionButton.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export default function AdminActionButton({ icon, label, onClick, disabled, bg, border }) { + return ( + + ); +} diff --git a/resources/js/components/adminActions.jsx b/resources/js/components/adminActions.jsx index dd2962c..3baf654 100644 --- a/resources/js/components/adminActions.jsx +++ b/resources/js/components/adminActions.jsx @@ -1,93 +1,39 @@ -import { useState } from 'react'; -import React from 'react'; -import hangupChannel from '@/components/actionsAgent/deleteCall' -import { - HiPhoneXMark, - HiArrowRightCircle, - HiPause, - HiLockClosed, - HiArrowPath, - HiCheckCircle -} from 'react-icons/hi2'; -import { Toast } from 'flowbite-react'; -import { HiCheck, HiX } from 'react-icons/hi'; +//ESTE COMPONENTE ES MUY IMPORTANTE ENTENDER SU MODULARIZACION PARA PODER HACERLE MANTENIMIENTO, DENTRO DE ESTE COMPONENTE RECIBIMOS LA INFORMACION DEL AGENTE EN LA VARIABLE $data EL CUAL SE REPARTE EN 3 COMPONENTES MAS +// 1.0 ESTA SECCION DEL COMPONENTE LO QUE HACE ES VALIDAR UNA INFORMACION QUE NOS DA UN STATUS SOBRE EL AGENTE, ESTE NOS SIRVE PARA SABER SI TIENE UNA IP ACTIVA O ALMENOS ESTA EN USO, TODO ESTO PARA PREVENIR EL USO DE LOS BOTONES CON AGENTES QUE NO LO REQUIEREN. +// 1.1 ESTA SECCION HACE UN LLAMADO A DOS COSAS +// -EL VALIDADOR SI EXISTE EL AGENTE +// -EL COMPONENTE QUE MAPEA LOS BOTONES. ESTE BOTON LE PASAMOS LAS PROPIEDAS COMO COLORES O ACCIONES JUNTO CON SU IDENTIFICADOR. +// 1.3 ESTA SECCION VIENE DE LA MANO DEL ONCLICK DEL COMPONENTE DEL BOTON, LO QUE ESTA HACIENDO ES UNA VALIDACION SI EXISTE EN ESE MOMENTO UN TOAST, SI ES ASI MUESTRA LOS DATOS QUE DEVUELVE EL API +// 1.4 PARA QUE FUNCIONE O OBTENGAMOS LOS DATOS QUE QUEREMOS EN EL MAPEO DEL COMPONENTE DEL BOTON LO RECIBIMOS DE UN HOOK LLAMADO useAdminHandlers ESTOS GENERAN OTRAS ACCIONES QUE PODEMOS VER EN ESE COMPONENTE -export default function AdminActions({ data }) { - const [toast, setToast] = useState({ show: false, success: true, message: '' }); - const handleHangup = async () => { - const result = await hangupChannel(data?.canal); - setToast({ - show: true, - success: result.success, - message: result.message, - }); - setTimeout(() => setToast({ ...toast, show: false }), 4000); - }; +import useAdminHandlers from '@/hooks/useAdminHandlers'; +import useAdminModal from '@/hooks/useAdminModal'; +import useAdminButtons from '@/hooks/useAdminButtons'; +import { Toast } from 'flowbite-react'; +import { HiCheck, HiX } from 'react-icons/hi'; +import AdminActionButton from './adminActionButton'; +import AgentModalWrapper from '@/components/agentsModalWrapper'; +import TransferModal from './transferModal'; +import { useState } from 'react'; + +export default function AdminActions({ data }) { - const actions = [ - { - label: 'Finalizar llamada', - icon: , - bg: 'hover:bg-red-50 dark:hover:bg-red-400/60', - border: 'border-red-200 dark:border-black', - onClick: () => handleHangup(), - disabledIf: (conditions) => conditions.notCall, - }, - { - label: 'Transferir llamada', - icon: , - bg: 'hover:bg-blue-50 dark:hover:bg-blue-400/60', - border: 'border-blue-200 dark:border-black', - onClick: () => alert('Transferir llamada'), - disabledIf: (conditions) => conditions.notCall, - }, - { - label: 'Pausar agente', - icon: , - bg: 'hover:bg-yellow-50 dark:hover:bg-yellow-300/60', - border: 'border-yellow-200 dark:border-black', - onClick: () => alert('Pausar agente'), - disabledIf: (conditions) => conditions.paused, - }, - { - label: 'Desloguear agente', - icon: , - bg: 'hover:bg-gray-50 dark:hover:bg-gray-400/60', - border: 'border-gray-200 dark:border-black', - onClick: () => alert('Desloguear agente'), - disabledIf: () => false, - }, - { - label: 'Mover a cola', - icon: , - bg: 'hover:bg-indigo-50 dark:hover:bg-indigo-400/60', - border: 'border-indigo-200 dark:border-black', - onClick: () => alert('Mover a cola'), - disabledIf: () => false, - }, - { - label: 'Volver a disponible', - icon: , - bg: 'hover:bg-green-50 dark:hover:bg-green-400/60', - border: 'border-green-200 dark:border-black', - onClick: () => alert('Volver a disponible'), - disabledIf: (conditions) => conditions.notpaused, - }, - ]; const isIpInvalid = ['(Unspecified)', null].includes(data?.ip); const isMemberAndIpNull = data?.ip === null && data?.member === null; - const notpaused = data.member?.pausa === null; //Sirve para bloquear el volver a disponible porque ya lo esta - const paused = data.member?.pausa != null; //Sirve para bloquear a pausar agente porque ya esta en pausa. - const notCall = data?.duration === null; //Sirve para bloquear el finalizar llamada y transferir llamada porque no hay llamada activa - const isUnspecified = isIpInvalid || isMemberAndIpNull; + const { modal, showModal, hideModal } = useAdminModal(); + const handlers = useAdminHandlers(data); + // 1.4 llamado acciones del boton + const { actions, conditions } = useAdminButtons({ data, handlers }); + return (
+ {/* 1.0 VALIDACION AGENTE */} {isUnspecified && (
Agente no conectado o IP no disponible. @@ -95,45 +41,53 @@ export default function AdminActions({ data }) { )}
+ {/*1.1 MAPEO DE BOTONES */} {actions.map((action, index) => { - const conditions = { notpaused, paused, notCall }; - const individuallyDisabled = action.disabledIf(conditions); - const shouldDisable = isUnspecified || individuallyDisabled; - - return ( - - ); - })} - {toast.show && ( -
- -
- {toast.success ? : } -
-
{toast.message}
-
+ const disabled = isUnspecified || action.disabledIf(conditions); + return ( + showModal() : action.onClick} + disabled={disabled} + bg={action.bg} + border={action.border} + /> + ); + })} +
+ {/* 1.3 MOSTRAR TOATS */} + {handlers.toast.show && ( +
+ +
+ {handlers.toast.success ? : }
- )} +
{handlers.toast.message}
+
+
+ )} + + {modal.show && ( + + { + console.log('Transferir a:', queue); + hideModal(); // También puedes cerrarlo aquí + }} + /> + + )} + + -
); } + \ No newline at end of file diff --git a/resources/js/components/transferModal.jsx b/resources/js/components/transferModal.jsx new file mode 100644 index 0000000..3dba6ee --- /dev/null +++ b/resources/js/components/transferModal.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import { useState } from "react"; +import AgentModalWrapper from '@/components/agentsModalWrapper'; + +function TransferModal ({hideModal, handleTransfer}){ + const [selectedQueue, setSelectedQueue] = useState(null) + return( + <> +

Transferir llamada

+ +

+ ⚠️ Esta acción puede afectar la llamada en curso. El registro de la transferencia quedará guardado en el sistema. +

+ +
+ +
+ {['Tramites', 'Soporte', 'Movil', 'Retencion', 'Pruebas'].map((area) => ( + + ))} +
+
+ +
+ + +
+ + ); +} + +export default TransferModal; \ No newline at end of file diff --git a/resources/js/hooks/useAdminButtons.jsx b/resources/js/hooks/useAdminButtons.jsx new file mode 100644 index 0000000..db03291 --- /dev/null +++ b/resources/js/hooks/useAdminButtons.jsx @@ -0,0 +1,70 @@ + +import { + HiPhoneXMark, + HiArrowRightCircle, + HiPause, + HiLockClosed, + HiArrowPath, + HiCheckCircle, +} from 'react-icons/hi2'; + +export default function useAdminButtons({ data, handlers }) { + const notpaused = data?.member?.pausa === null; + const paused = data?.member?.pausa != null; + const notCall = data?.duration === null; + + const conditions = { notpaused, paused, notCall }; + + const actions = [ + { + label: 'Finalizar llamada', + icon: , + bg: 'hover:bg-red-50 dark:hover:bg-red-400/60', + border: 'border-red-200 dark:border-black', + onClick: handlers.handleHangup, + disabledIf: (c) => c.notCall, + }, + { + label: 'Transferir llamada', + icon: , + bg: 'hover:bg-blue-50 dark:hover:bg-blue-400/60', + border: 'border-blue-200 dark:border-black', + onClick: () => handlers.modal, + disabledIf: (c) => c.paused, + }, + { + label: 'Pausar agente', + icon: , + bg: 'hover:bg-yellow-50 dark:hover:bg-yellow-300/60', + border: 'border-yellow-200 dark:border-black', + onClick: handlers.handlePause, + disabledIf: (c) => c.paused, + }, + { + label: 'Desloguear agente', + icon: , + bg: 'hover:bg-gray-50 dark:hover:bg-gray-400/60', + border: 'border-gray-200 dark:border-black', + onClick: () => alert('Desloguear agente'), + disabledIf: () => false, + }, + { + label: 'Mover a cola', + icon: , + bg: 'hover:bg-indigo-50 dark:hover:bg-indigo-400/60', + border: 'border-indigo-200 dark:border-black', + onClick: () => alert('Mover a cola'), + disabledIf: () => false, + }, + { + label: 'Volver a disponible', + icon: , + bg: 'hover:bg-green-50 dark:hover:bg-green-400/60', + border: 'border-green-200 dark:border-black', + onClick: handlers.handleUnpause, + disabledIf: (c) => c.notpaused, + }, + ]; + + return { actions, conditions }; +} diff --git a/resources/js/hooks/useAdminHandlers.jsx b/resources/js/hooks/useAdminHandlers.jsx new file mode 100644 index 0000000..a679a2c --- /dev/null +++ b/resources/js/hooks/useAdminHandlers.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import hangupChannel from '@/components/actionsAgent/deleteCall'; +import PausedExtension from '@/components/actionsAgent/pausedExtension'; +import UnpauseExtension from '@/components/actionsAgent/unpauseExtension'; + +export default function useAdminHandlers(data) { + const [toast, setToast] = useState({ show: false, success: true, message: '' }); + + const showToast = (result) => { + setToast({ show: true, success: result.success, message: result.message }); + setTimeout(() => setToast({ show: false, success: true, message: '' }), 4000); + }; + + const handleHangup = async () => { + const result = await hangupChannel(data?.canal); + showToast(result); + }; + + const handlePause = async () => { + const result = await PausedExtension(data?.extension); + showToast(result); + }; + + const handleUnpause = async () => { + const result = await UnpauseExtension(data?.extension); + showToast(result); + }; + + const handleTransfer = async () => { + const result = await UnpauseExtension(data?.extension); + showToast(result); + }; + + + return { handleHangup, handlePause, handleUnpause, toast }; +} \ No newline at end of file diff --git a/resources/js/hooks/useAdminModal.jsx b/resources/js/hooks/useAdminModal.jsx new file mode 100644 index 0000000..1bc6ff4 --- /dev/null +++ b/resources/js/hooks/useAdminModal.jsx @@ -0,0 +1,15 @@ +import { useState } from 'react'; + +export default function useAdminModal() { + const [modal, setModal] = useState({ show: false, message: '' }); + + const showModal = (message) => { + setModal({ show: true, message }); + }; + + const hideModal = () => { + setModal({ show: false, message: '' }); + }; + + return { modal, showModal, hideModal }; +} diff --git a/routes/api.php b/routes/api.php index 3a9da11..8875e78 100644 --- a/routes/api.php +++ b/routes/api.php @@ -14,4 +14,5 @@ Route::get('/agent/{extension}', [PostProxyController::class, 'userData']); Route::get('/getOverview', [PostProxyController::class, 'getOverview']); Route::post('/hangup-channel', [PostProxyController::class, 'chanelHangup']); -Route::post('/paused-extension', [PostProxyController::class, 'pausedExtension']); \ No newline at end of file +Route::post('/pause-extension', [PostProxyController::class, 'pauseExtension']); +Route::post('/unpause-extension', [PostProxyController::class, 'unpauseExtension']); \ No newline at end of file