src/Controller/WorkingHoursController.php line 390

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  4. use Symfony\Component\HttpFoundation\Request;
  5. use Symfony\Component\HttpFoundation\Response;
  6. use Symfony\Component\HttpFoundation\JsonResponse;
  7. use Symfony\Component\Routing\Annotation\Route;
  8. use Webkul\UVDesk\CoreFrameworkBundle\Services\UserService;
  9. class WorkingHoursController extends AbstractController
  10. {
  11.     private $userService;
  12.     public function __construct(UserService $userService)
  13.     {
  14.         $this->userService $userService;
  15.     }
  16.     public function workingHoursAction(): Response
  17.     {
  18.         return $this->render('reports/working-hours.html.twig');
  19.     }
  20.     public function getWorkingHoursStatusAction(): JsonResponse
  21.     {
  22.         $entityManager $this->getDoctrine()->getManager();
  23.         
  24.         // Forzar refresh - cerrar y reabrir la conexión para evitar cache
  25.         $entityManager->close();
  26.         $entityManager $this->getDoctrine()->getManager();
  27.         $connection $entityManager->getConnection();
  28.         
  29.         // Forzar que no use cache de transacciones
  30.         $connection->executeQuery('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED');
  31.         
  32.         // Obtener países y horarios desde la tabla uv_support_group y uv_working_hours
  33.         $sql "
  34.             SELECT 
  35.                 sg.id as supportGroupId,
  36.                 sg.name as supportGroupName,
  37.                 sg.description as supportGroupDescription,
  38.                 wh.timezone,
  39.                 wh.start_time,
  40.                 wh.end_time,
  41.                 wh.monday,
  42.                 wh.tuesday,
  43.                 wh.wednesday,
  44.                 wh.thursday,
  45.                 wh.friday,
  46.                 wh.saturday,
  47.                 wh.sunday,
  48.                 wh.is_active,
  49.                 wh.auto_assignment_enabled
  50.             FROM uv_support_group sg
  51.             LEFT JOIN uv_working_hours wh ON sg.id = wh.support_group_id
  52.             WHERE sg.is_active = 1
  53.             AND (sg.name LIKE 'OR %' OR sg.name = 'Casa Central')
  54.             ORDER BY 
  55.                 CASE 
  56.                     WHEN sg.name LIKE 'OR %' THEN 1
  57.                     WHEN sg.name = 'Casa Central' THEN 2
  58.                     ELSE 3
  59.                 END,
  60.                 sg.name
  61.         ";
  62.         
  63.         $stmt $connection->prepare($sql);
  64.         $stmt->execute();
  65.         $supportGroups $stmt->fetchAll(\PDO::FETCH_ASSOC);
  66.         
  67.         // Obtener solo agentes online
  68.         $onlineAgentsStatus $this->getOnlineAgentsStatus();
  69.         
  70.         // Si no hay agentes online, devolver grupos sin agentes
  71.         if (empty($onlineAgentsStatus)) {
  72.             $agentsByRegion = [];
  73.         } else {
  74.             // Obtener solo agentes online agrupados por región
  75.             $onlineAgentIds array_keys($onlineAgentsStatus);
  76.             $placeholders str_repeat('?,'count($onlineAgentIds) - 1) . '?';
  77.             
  78.             $sql "
  79.                 SELECT 
  80.                     a.id as agentId,
  81.                     a.first_name as firstName,
  82.                     a.last_name as lastName,
  83.                     a.email,
  84.                     a.is_enabled as isEnabled,
  85.                     a.idOR as idor,
  86.                     sg.name as supportGroupName,
  87.                     sg.description as supportGroupDescription,
  88.                     sr.code as supportRoleCode
  89.                 FROM uv_user a
  90.                 JOIN uv_user_instance ui ON a.id = ui.user_id
  91.                 JOIN uv_support_role sr ON ui.supportRole_id = sr.id
  92.                 LEFT JOIN uv_support_group sg ON a.idOR = sg.id
  93.                 WHERE a.is_enabled = 1
  94.                 AND sr.code = 'ROLE_AGENT'
  95.                 AND a.id IN ($placeholders)
  96.                 ORDER BY a.first_name, a.last_name
  97.             ";
  98.             
  99.             $stmt $connection->prepare($sql);
  100.             $stmt->execute($onlineAgentIds);
  101.             $agents $stmt->fetchAll(\PDO::FETCH_ASSOC);
  102.             
  103.             // Agrupar agentes por región
  104.             $agentsByRegion = [];
  105.             foreach ($agents as $agent) {
  106.                 $regionName $agent['supportGroupName'] ?: 'Sin región asignada';
  107.                 if (!isset($agentsByRegion[$regionName])) {
  108.                     $agentsByRegion[$regionName] = [];
  109.                 }
  110.                 
  111.                 $agentId $agent['agentId'];
  112.                 $connectionInfo $onlineAgentsStatus[$agentId];
  113.                 
  114.                 $agentsByRegion[$regionName][] = [
  115.                     'id' => $agent['agentId'],
  116.                     'name' => $agent['firstName'] . ' ' $agent['lastName'],
  117.                     'email' => $agent['email'],
  118.                     'isEnabled' => $agent['isEnabled'],
  119.                     'isOnline' => $connectionInfo['isOnline'],
  120.                     'lastActivity' => $connectionInfo['lastActivity'],
  121.                     'lastAuthType' => $connectionInfo['lastAuthType'],
  122.                     'idor' => $agent['idor'],
  123.                     'role' => $agent['supportRoleCode']
  124.                 ];
  125.             }
  126.         }
  127.         
  128.         $status = [];
  129.         foreach ($supportGroups as $group) {
  130.             // Extraer nombre del país del nombre del grupo
  131.             if ($group['supportGroupName'] === 'Casa Central') {
  132.                 $countryName 'Casa Central';
  133.             } else {
  134.                 // Para oficinas regionales: "OR Colombia" -> "Colombia"
  135.                 $countryName trim(str_replace('OR'''$group['supportGroupName']));
  136.             }
  137.             
  138.             // Si no hay configuración de horarios, usar valores por defecto
  139.             if (!$group['timezone']) {
  140.                 $group['timezone'] = 'UTC';
  141.                 $group['start_time'] = '09:00:00';
  142.                 $group['end_time'] = '18:00:00';
  143.                 $group['monday'] = 1;
  144.                 $group['tuesday'] = 1;
  145.                 $group['wednesday'] = 1;
  146.                 $group['thursday'] = 1;
  147.                 $group['friday'] = 1;
  148.                 $group['saturday'] = 0;
  149.                 $group['sunday'] = 0;
  150.             }
  151.             
  152.             $localTime = new \DateTime('now', new \DateTimeZone($group['timezone']));
  153.             $dayOfWeek = (int)$localTime->format('N'); // 1 = Monday, 7 = Sunday
  154.             $hour = (int)$localTime->format('G'); // 0-23 hour format
  155.             $minute = (int)$localTime->format('i');
  156.             $currentTime $hour 60 $minute// Convertir a minutos para comparación
  157.             
  158.             // Convertir horarios de inicio y fin a minutos
  159.             $startTimeParts explode(':'$group['start_time']);
  160.             $endTimeParts explode(':'$group['end_time']);
  161.             $startTimeMinutes = (int)$startTimeParts[0] * 60 + (int)$startTimeParts[1];
  162.             $endTimeMinutes = (int)$endTimeParts[0] * 60 + (int)$endTimeParts[1];
  163.             
  164.             // Verificar si es día laboral
  165.             $workingDays = [
  166.                 => $group['monday'],
  167.                 => $group['tuesday'],
  168.                 => $group['wednesday'],
  169.                 => $group['thursday'],
  170.                 => $group['friday'],
  171.                 => $group['saturday'],
  172.                 => $group['sunday']
  173.             ];
  174.             
  175.             $isWorkingDay $workingDays[$dayOfWeek] == 1;
  176.             
  177.             // Verificar si está dentro del horario laboral
  178.             $isWorkingHours $isWorkingDay && $currentTime >= $startTimeMinutes && $currentTime $endTimeMinutes;
  179.             
  180.             // Determinar estado
  181.             if ($isWorkingDay && $isWorkingHours) {
  182.                 $statusCode 'open';
  183.                 $statusText 'Abierto';
  184.                 $statusColor 'green';
  185.             } else {
  186.                 $statusCode 'closed';
  187.                 $statusText 'Cerrado';
  188.                 $statusColor 'red';
  189.             }
  190.             
  191.             // Buscar agentes para este país
  192.             $countryAgents = [];
  193.             foreach ($agentsByRegion as $regionName => $regionAgents) {
  194.                 if (stripos($regionName$countryName) !== false || 
  195.                     stripos($countryName$regionName) !== false ||
  196.                     $this->isCountryInRegion($countryName$regionName)) {
  197.                     $countryAgents array_merge($countryAgents$regionAgents);
  198.                 }
  199.             }
  200.             $status[$countryName] = [
  201.                 'country' => $countryName,
  202.                 'timezone' => $group['timezone'],
  203.                 'localTime' => $localTime->format('H:i:s'),
  204.                 'localDate' => $localTime->format('Y-m-d'),
  205.                 'dayOfWeek' => $this->getDayOfWeekInSpanish($localTime->format('N')),
  206.                 'isWorkingDay' => $isWorkingDay,
  207.                 'isWorkingHours' => $isWorkingHours,
  208.                 'status' => $statusCode,
  209.                 'statusText' => $statusText,
  210.                 'statusColor' => $statusColor,
  211.                 'workingHours' => $group['start_time'] . ' - ' $group['end_time'],
  212.                 'workingDays' => $this->getWorkingDaysArray($workingDays),
  213.                 'workingDaysText' => $this->getWorkingDaysText($workingDays),
  214.                 'supportGroupId' => $group['supportGroupId'],
  215.                 'autoAssignmentEnabled' => $group['auto_assignment_enabled'] ?? 1// Por defecto activado
  216.                 'agents' => $countryAgents
  217.             ];
  218.         }
  219.         return new JsonResponse($status);
  220.     }
  221.     
  222.     private function isCountryInRegion($country$regionName): bool
  223.     {
  224.         // Mapeo de países a posibles nombres de regiones
  225.         $countryRegionMap = [
  226.             'Casa Central' => ['casa central''argentina''buenos aires''córdoba''rosario'],
  227.             'Colombia' => ['colombia''bogotá''medellín''cali''barranquilla'],
  228.             'Mexico' => ['mexico''méxico''ciudad de mexico''cdmx''guadalajara''monterrey'],
  229.             'Brasil' => ['brasil''brazil''sao paulo''rio de janeiro''brasilia'],
  230.             'España' => ['españa''spain''madrid''barcelona''valencia''sevilla'],
  231.             'Peru' => ['peru''perú''lima''arequipa''trujillo'],
  232.             'Chile' => ['chile''santiago''valparaíso''concepción'],
  233.             'Venezuela' => ['venezuela''caracas''maracaibo''valencia'],
  234.             'Costa Rica' => ['costa rica''san josé''cartago''alajuela']
  235.         ];
  236.         
  237.         if (isset($countryRegionMap[$country])) {
  238.             foreach ($countryRegionMap[$country] as $regionKeyword) {
  239.                 if (stripos($regionName$regionKeyword) !== false) {
  240.                     return true;
  241.                 }
  242.             }
  243.         }
  244.         
  245.         return false;
  246.     }
  247.     
  248.     private function getWorkingDaysText($workingDays): string
  249.     {
  250.         $days = [];
  251.         $dayNames = [
  252.             => 'Lunes',
  253.             => 'Martes'
  254.             => 'Miércoles',
  255.             => 'Jueves',
  256.             => 'Viernes',
  257.             => 'Sábado',
  258.             => 'Domingo'
  259.         ];
  260.         
  261.         foreach ($workingDays as $dayNum => $isWorking) {
  262.             if ($isWorking) {
  263.                 $days[] = $dayNames[$dayNum];
  264.             }
  265.         }
  266.         
  267.         if (empty($days)) {
  268.             return 'No hay días laborales configurados';
  269.         }
  270.         
  271.         if (count($days) === 7) {
  272.             return 'Todos los días';
  273.         }
  274.         
  275.         if (count($days) === && $workingDays[1] && $workingDays[2] && $workingDays[3] && $workingDays[4] && $workingDays[5]) {
  276.             return 'Lunes a Viernes';
  277.         }
  278.         
  279.         return implode(', '$days);
  280.     }
  281.     private function getWorkingDaysArray($workingDays): array
  282.     {
  283.         $days = [];
  284.         $dayNames = [
  285.             => 'Lunes',
  286.             => 'Martes'
  287.             => 'Miércoles',
  288.             => 'Jueves',
  289.             => 'Viernes',
  290.             => 'Sábado',
  291.             => 'Domingo'
  292.         ];
  293.         
  294.         foreach ($workingDays as $dayNum => $isWorking) {
  295.             if ($isWorking) {
  296.                 $days[] = $dayNames[$dayNum];
  297.             }
  298.         }
  299.         
  300.         return $days;
  301.     }
  302.     public function getRecentTicketsAction(): JsonResponse
  303.     {
  304.         $entityManager $this->getDoctrine()->getManager();
  305.         
  306.         // Forzar refresh - cerrar y reabrir la conexión para evitar cache
  307.         $entityManager->close();
  308.         $entityManager $this->getDoctrine()->getManager();
  309.         $connection $entityManager->getConnection();
  310.         
  311.         // Forzar que no use cache de transacciones
  312.         $connection->executeQuery('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED');
  313.         
  314.         // Query nativo MySQL para obtener los últimos 5 tickets
  315.         $sql "
  316.             SELECT 
  317.                 t.id as ticketId,
  318.                 t.subject,
  319.                 s.code as status,
  320.                 p.code as priority,
  321.                 t.created_at as createdAt,
  322.                 t.updated_at as updatedAt,
  323.                 c.id as customerId,
  324.                 c.first_name as customerFirstName,
  325.                 c.last_name as customerLastName,
  326.                 c.email as customerEmail,
  327.                 c.idOR as idor,
  328.                 sg.name as supportGroupName,
  329.                 sg.description as supportGroupDescription,
  330.                 a.id as agentId,
  331.                 a.first_name as agentFirstName,
  332.                 a.last_name as agentLastName,
  333.                 a.email as agentEmail
  334.             FROM uv_ticket t
  335.             JOIN uv_user c ON t.customer_id = c.id
  336.             LEFT JOIN uv_ticket_status s ON t.status_id = s.id
  337.             LEFT JOIN uv_ticket_priority p ON t.priority_id = p.id
  338.             LEFT JOIN uv_support_group sg ON c.idOR = sg.id
  339.             LEFT JOIN uv_user a ON t.agent_id = a.id
  340.             WHERE t.is_trashed = 0
  341.             ORDER BY t.created_at DESC
  342.             LIMIT 5
  343.         ";
  344.         
  345.         $stmt $connection->prepare($sql);
  346.         $stmt->execute();
  347.         $tickets $stmt->fetchAll(\PDO::FETCH_ASSOC);
  348.         
  349.         // Formatear los datos para la respuesta
  350.         $formattedTickets = [];
  351.         foreach ($tickets as $ticket) {
  352.             $formattedTickets[] = [
  353.                 'ticketId' => $ticket['ticketId'],
  354.                 'subject' => $ticket['subject'],
  355.                 'status' => $ticket['status'],
  356.                 'priority' => $ticket['priority'],
  357.                 'createdAt' => $ticket['createdAt'],
  358.                 'updatedAt' => $ticket['updatedAt'],
  359.                 'customer' => [
  360.                     'id' => $ticket['customerId'],
  361.                     'name' => $ticket['customerFirstName'] . ' ' $ticket['customerLastName'],
  362.                     'email' => $ticket['customerEmail'],
  363.                     'idor' => $ticket['idor']
  364.                 ],
  365.                 'region' => [
  366.                     'name' => $ticket['supportGroupName'] ?: 'Sin región asignada',
  367.                     'description' => $ticket['supportGroupDescription'] ?: ''
  368.                 ],
  369.                 'agent' => $ticket['agentId'] ? [
  370.                     'id' => $ticket['agentId'],
  371.                     'name' => $ticket['agentFirstName'] . ' ' $ticket['agentLastName'],
  372.                     'email' => $ticket['agentEmail']
  373.                 ] : null
  374.             ];
  375.         }
  376.         
  377.         return new JsonResponse($formattedTickets);
  378.     }
  379.     public function getAgentsAction(): JsonResponse
  380.     {
  381.         $entityManager $this->getDoctrine()->getManager();
  382.         
  383.         // Forzar refresh - cerrar y reabrir la conexión para evitar cache
  384.         $entityManager->close();
  385.         $entityManager $this->getDoctrine()->getManager();
  386.         $connection $entityManager->getConnection();
  387.         
  388.         // Forzar que no use cache de transacciones
  389.         $connection->executeQuery('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED');
  390.         
  391.         // Obtener solo agentes online
  392.         $onlineAgentsStatus $this->getOnlineAgentsStatus();
  393.         
  394.         // Si no hay agentes online, devolver array vacío
  395.         if (empty($onlineAgentsStatus)) {
  396.             return new JsonResponse([]);
  397.         }
  398.         
  399.         // Obtener solo agentes online
  400.         $onlineAgentIds array_keys($onlineAgentsStatus);
  401.         $placeholders str_repeat('?,'count($onlineAgentIds) - 1) . '?';
  402.         
  403.         $sql "
  404.             SELECT 
  405.                 a.id as agentId,
  406.                 a.first_name as firstName,
  407.                 a.last_name as lastName,
  408.                 a.email,
  409.                 a.is_enabled as isEnabled,
  410.                 a.idOR as idor,
  411.                 sg.name as supportGroupName,
  412.                 sg.description as supportGroupDescription,
  413.                 sr.code as supportRoleCode
  414.             FROM uv_user a
  415.             JOIN uv_user_instance ui ON a.id = ui.user_id
  416.             JOIN uv_support_role sr ON ui.supportRole_id = sr.id
  417.             LEFT JOIN uv_support_group sg ON a.idOR = sg.id
  418.             WHERE a.is_enabled = 1
  419.             AND sr.code = 'ROLE_AGENT'
  420.             AND a.id IN ($placeholders)
  421.             ORDER BY a.first_name, a.last_name
  422.         ";
  423.         
  424.         $stmt $connection->prepare($sql);
  425.         $stmt->execute($onlineAgentIds);
  426.         $agents $stmt->fetchAll(\PDO::FETCH_ASSOC);
  427.         
  428.         $formattedAgents = [];
  429.         foreach ($agents as $agent) {
  430.             $agentId $agent['agentId'];
  431.             $connectionInfo $onlineAgentsStatus[$agentId];
  432.             
  433.             $formattedAgents[] = [
  434.                 'id' => $agent['agentId'],
  435.                 'name' => $agent['firstName'] . ' ' $agent['lastName'],
  436.                 'email' => $agent['email'],
  437.                 'isEnabled' => $agent['isEnabled'],
  438.                 'isOnline' => $connectionInfo['isOnline'],
  439.                 'lastActivity' => $connectionInfo['lastActivity'],
  440.                 'lastAuthType' => $connectionInfo['lastAuthType'],
  441.                 'idor' => $agent['idor'],
  442.                 'role' => $agent['supportRoleCode'],
  443.                 'region' => [
  444.                     'name' => $agent['supportGroupName'] ?: 'Sin región asignada',
  445.                     'description' => $agent['supportGroupDescription'] ?: 'No hay descripción disponible'
  446.                 ]
  447.             ];
  448.         }
  449.         
  450.         return new JsonResponse($formattedAgents);
  451.     }
  452.     
  453.     public function getActiveOfficeAgentsAction(): JsonResponse
  454.     {
  455.         $entityManager $this->getDoctrine()->getManager();
  456.         
  457.         // Forzar refresh - cerrar y reabrir la conexión para evitar cache
  458.         $entityManager->close();
  459.         $entityManager $this->getDoctrine()->getManager();
  460.         $connection $entityManager->getConnection();
  461.         
  462.         // Forzar que no use cache de transacciones
  463.         $connection->executeQuery('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED');
  464.         
  465.         // Primero obtener las oficinas que están abiertas actualmente
  466.         $sql "
  467.             SELECT 
  468.                 sg.id as supportGroupId,
  469.                 sg.name as supportGroupName,
  470.                 sg.description as supportGroupDescription,
  471.                 wh.timezone,
  472.                 wh.start_time,
  473.                 wh.end_time,
  474.                 wh.monday,
  475.                 wh.tuesday,
  476.                 wh.wednesday,
  477.                 wh.thursday,
  478.                 wh.friday,
  479.                 wh.saturday,
  480.                 wh.sunday,
  481.                 wh.is_active
  482.             FROM uv_support_group sg
  483.             LEFT JOIN uv_working_hours wh ON sg.id = wh.support_group_id
  484.             WHERE sg.is_active = 1
  485.             AND (sg.name LIKE 'OR %' OR sg.name = 'Casa Central')
  486.         ";
  487.         
  488.         $stmt $connection->prepare($sql);
  489.         $stmt->execute();
  490.         $supportGroups $stmt->fetchAll(\PDO::FETCH_ASSOC);
  491.         
  492.         // Determinar qué oficinas están abiertas
  493.         $openOffices = [];
  494.         foreach ($supportGroups as $group) {
  495.             // Si no hay configuración de horarios, usar valores por defecto
  496.             if (!$group['timezone']) {
  497.                 $group['timezone'] = 'UTC';
  498.                 $group['start_time'] = '09:00:00';
  499.                 $group['end_time'] = '18:00:00';
  500.                 $group['monday'] = 1;
  501.                 $group['tuesday'] = 1;
  502.                 $group['wednesday'] = 1;
  503.                 $group['thursday'] = 1;
  504.                 $group['friday'] = 1;
  505.                 $group['saturday'] = 0;
  506.                 $group['sunday'] = 0;
  507.             }
  508.             
  509.             $localTime = new \DateTime('now', new \DateTimeZone($group['timezone']));
  510.             $dayOfWeek = (int)$localTime->format('N'); // 1 = Monday, 7 = Sunday
  511.             $hour = (int)$localTime->format('G'); // 0-23 hour format
  512.             $minute = (int)$localTime->format('i');
  513.             $currentTime $hour 60 $minute// Convertir a minutos para comparación
  514.             
  515.             // Convertir horarios de inicio y fin a minutos
  516.             $startTimeParts explode(':'$group['start_time']);
  517.             $endTimeParts explode(':'$group['end_time']);
  518.             $startTimeMinutes = (int)$startTimeParts[0] * 60 + (int)$startTimeParts[1];
  519.             $endTimeMinutes = (int)$endTimeParts[0] * 60 + (int)$endTimeParts[1];
  520.             
  521.             // Verificar si es día laboral
  522.             $workingDays = [
  523.                 => $group['monday'],
  524.                 => $group['tuesday'],
  525.                 => $group['wednesday'],
  526.                 => $group['thursday'],
  527.                 => $group['friday'],
  528.                 => $group['saturday'],
  529.                 => $group['sunday']
  530.             ];
  531.             
  532.             $isWorkingDay $workingDays[$dayOfWeek] == 1;
  533.             
  534.             // Verificar si está dentro del horario laboral
  535.             $isWorkingHours $isWorkingDay && $currentTime >= $startTimeMinutes && $currentTime $endTimeMinutes;
  536.             
  537.             if ($isWorkingHours) {
  538.                 $openOffices[] = $group['supportGroupId'];
  539.             }
  540.         }
  541.         
  542.         // Si no hay oficinas abiertas, devolver array vacío
  543.         if (empty($openOffices)) {
  544.             return new JsonResponse([]);
  545.         }
  546.         
  547.         // Obtener solo agentes online
  548.         $onlineAgentsStatus $this->getOnlineAgentsStatus();
  549.         
  550.         // Si no hay agentes online, devolver array vacío
  551.         if (empty($onlineAgentsStatus)) {
  552.             return new JsonResponse([]);
  553.         }
  554.         
  555.         // Obtener solo agentes online de las oficinas abiertas
  556.         $onlineAgentIds array_keys($onlineAgentsStatus);
  557.         $placeholders str_repeat('?,'count($onlineAgentIds) - 1) . '?';
  558.         $officePlaceholders str_repeat('?,'count($openOffices) - 1) . '?';
  559.         
  560.         $sql "
  561.             SELECT 
  562.                 a.id as agentId,
  563.                 a.first_name as firstName,
  564.                 a.last_name as lastName,
  565.                 a.email,
  566.                 a.is_enabled as isEnabled,
  567.                 a.idOR as idor,
  568.                 sg.name as supportGroupName,
  569.                 sg.description as supportGroupDescription,
  570.                 sr.code as supportRoleCode
  571.             FROM uv_user a
  572.             JOIN uv_user_instance ui ON a.id = ui.user_id
  573.             JOIN uv_support_role sr ON ui.supportRole_id = sr.id
  574.             LEFT JOIN uv_support_group sg ON a.idOR = sg.id
  575.             WHERE a.is_enabled = 1
  576.             AND sr.code = 'ROLE_AGENT'
  577.             AND a.id IN ($placeholders)
  578.             AND a.idOR IN ($officePlaceholders)
  579.             ORDER BY a.first_name, a.last_name
  580.         ";
  581.         
  582.         $params array_merge($onlineAgentIds$openOffices);
  583.         $stmt $connection->prepare($sql);
  584.         $stmt->execute($params);
  585.         $agents $stmt->fetchAll(\PDO::FETCH_ASSOC);
  586.         
  587.         $formattedAgents = [];
  588.         foreach ($agents as $agent) {
  589.             $agentId $agent['agentId'];
  590.             $connectionInfo $onlineAgentsStatus[$agentId];
  591.             
  592.             $formattedAgents[] = [
  593.                 'id' => $agent['agentId'],
  594.                 'name' => $agent['firstName'] . ' ' $agent['lastName'],
  595.                 'email' => $agent['email'],
  596.                 'isEnabled' => $agent['isEnabled'],
  597.                 'isOnline' => $connectionInfo['isOnline'],
  598.                 'lastActivity' => $connectionInfo['lastActivity'],
  599.                 'lastAuthType' => $connectionInfo['lastAuthType'],
  600.                 'idor' => $agent['idor'],
  601.                 'role' => $agent['supportRoleCode'],
  602.                 'region' => [
  603.                     'name' => $agent['supportGroupName'] ?: 'Sin región asignada',
  604.                     'description' => $agent['supportGroupDescription'] ?: 'No hay descripción disponible'
  605.                 ]
  606.             ];
  607.         }
  608.         
  609.         return new JsonResponse($formattedAgents);
  610.     }
  611.     
  612.     public function assignAgentAction(Request $request): JsonResponse
  613.     {
  614.         try {
  615.             $data json_decode($request->getContent(), true);
  616.             
  617.             if (!isset($data['ticketId']) || !isset($data['agentId'])) {
  618.                 return new JsonResponse([
  619.                     'success' => false,
  620.                     'message' => 'Se requieren ticketId y agentId'
  621.                 ], 400);
  622.             }
  623.             
  624.             $ticketId = (int) $data['ticketId'];
  625.             $agentId = (int) $data['agentId'];
  626.             
  627.             $entityManager $this->getDoctrine()->getManager();
  628.             
  629.             // Forzar refresh - cerrar y reabrir la conexión para evitar cache
  630.             $entityManager->close();
  631.             $entityManager $this->getDoctrine()->getManager();
  632.             $connection $entityManager->getConnection();
  633.             
  634.             // Forzar que no use cache de transacciones
  635.             $connection->executeQuery('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED');
  636.             
  637.             // Verificar que el ticket existe usando query nativo
  638.             $sql "SELECT id FROM uv_ticket WHERE id = ?";
  639.             $stmt $connection->prepare($sql);
  640.             $stmt->execute([$ticketId]);
  641.             $ticket $stmt->fetch(\PDO::FETCH_ASSOC);
  642.             
  643.             if (!$ticket) {
  644.                 return new JsonResponse([
  645.                     'success' => false,
  646.                     'message' => 'El ticket especificado no existe'
  647.                 ], 404);
  648.             }
  649.             
  650.             // Verificar que el agente existe y está activo
  651.             $sql "SELECT id, first_name, last_name, is_enabled FROM uv_user WHERE id = ?";
  652.             $stmt $connection->prepare($sql);
  653.             $stmt->execute([$agentId]);
  654.             $agent $stmt->fetch(\PDO::FETCH_ASSOC);
  655.             
  656.             if (!$agent) {
  657.                 return new JsonResponse([
  658.                     'success' => false,
  659.                     'message' => 'El agente especificado no existe'
  660.                 ], 404);
  661.             }
  662.             
  663.             if (!$agent['is_enabled']) {
  664.                 return new JsonResponse([
  665.                     'success' => false,
  666.                     'message' => 'El agente especificado no está activo'
  667.                 ], 400);
  668.             }
  669.             
  670.             // Verificar que el usuario tiene rol de agente
  671.             $sql "
  672.                 SELECT sr.code 
  673.                 FROM uv_user_instance ui
  674.                 JOIN uv_support_role sr ON ui.supportRole_id = sr.id
  675.                 WHERE ui.user_id = ?
  676.             ";
  677.             $stmt $connection->prepare($sql);
  678.             $stmt->execute([$agentId]);
  679.             $role $stmt->fetch(\PDO::FETCH_ASSOC);
  680.             
  681.             if (!$role || $role['code'] !== 'ROLE_AGENT') {
  682.                 return new JsonResponse([
  683.                     'success' => false,
  684.                     'message' => 'El usuario especificado no tiene rol de agente'
  685.                 ], 400);
  686.             }
  687.             
  688.             // Asignar el agente al ticket usando query nativo
  689.             $sql "UPDATE uv_ticket SET agent_id = ? WHERE id = ?";
  690.             $stmt $connection->prepare($sql);
  691.             $stmt->execute([$agentId$ticketId]);
  692.             
  693.             return new JsonResponse([
  694.                 'success' => true,
  695.                 'message' => 'Ticket asignado exitosamente al agente',
  696.                 'data' => [
  697.                     'ticketId' => $ticketId,
  698.                     'agentId' => $agentId,
  699.                     'agentName' => $agent['first_name'] . ' ' $agent['last_name']
  700.                 ]
  701.             ]);
  702.             
  703.         } catch (\Exception $e) {
  704.             return new JsonResponse([
  705.                 'success' => false,
  706.                 'message' => 'Error interno del servidor: ' $e->getMessage()
  707.             ], 500);
  708.         }
  709.     }
  710.     
  711.     public function updateWorkingHoursAction(Request $request): JsonResponse
  712.     {
  713.         try {
  714.             $data json_decode($request->getContent(), true);
  715.             
  716.             if (!isset($data['supportGroupId']) || !isset($data['timezone']) || 
  717.                 !isset($data['startTime']) || !isset($data['endTime'])) {
  718.                 return new JsonResponse([
  719.                     'success' => false,
  720.                     'message' => 'Se requieren todos los campos obligatorios'
  721.                 ], 400);
  722.             }
  723.             
  724.             $supportGroupId = (int) $data['supportGroupId'];
  725.             $timezone $data['timezone'];
  726.             $startTime $data['startTime'];
  727.             $endTime $data['endTime'];
  728.             $monday = isset($data['monday']) ? (int) $data['monday'] : 1;
  729.             $tuesday = isset($data['tuesday']) ? (int) $data['tuesday'] : 1;
  730.             $wednesday = isset($data['wednesday']) ? (int) $data['wednesday'] : 1;
  731.             $thursday = isset($data['thursday']) ? (int) $data['thursday'] : 1;
  732.             $friday = isset($data['friday']) ? (int) $data['friday'] : 1;
  733.             $saturday = isset($data['saturday']) ? (int) $data['saturday'] : 0;
  734.             $sunday = isset($data['sunday']) ? (int) $data['sunday'] : 0;
  735.             
  736.             $entityManager $this->getDoctrine()->getManager();
  737.             
  738.             // Forzar refresh - cerrar y reabrir la conexión para evitar cache
  739.             $entityManager->close();
  740.             $entityManager $this->getDoctrine()->getManager();
  741.             $connection $entityManager->getConnection();
  742.             
  743.             // Forzar que no use cache de transacciones
  744.             $connection->executeQuery('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED');
  745.             
  746.             // Verificar que el grupo de soporte existe
  747.             $sql "SELECT id FROM uv_support_group WHERE id = ? AND is_active = 1";
  748.             $stmt $connection->prepare($sql);
  749.             $stmt->execute([$supportGroupId]);
  750.             $supportGroup $stmt->fetch(\PDO::FETCH_ASSOC);
  751.             
  752.             if (!$supportGroup) {
  753.                 return new JsonResponse([
  754.                     'success' => false,
  755.                     'message' => 'El grupo de soporte especificado no existe o no está activo'
  756.                 ], 404);
  757.             }
  758.             
  759.             // Verificar si ya existe configuración de horarios para este grupo
  760.             $sql "SELECT id FROM uv_working_hours WHERE support_group_id = ?";
  761.             $stmt $connection->prepare($sql);
  762.             $stmt->execute([$supportGroupId]);
  763.             $existingHours $stmt->fetch(\PDO::FETCH_ASSOC);
  764.             
  765.             if ($existingHours) {
  766.                 // Actualizar horarios existentes
  767.                 $sql "
  768.                     UPDATE uv_working_hours 
  769.                     SET timezone = ?, start_time = ?, end_time = ?, 
  770.                         monday = ?, tuesday = ?, wednesday = ?, thursday = ?, 
  771.                         friday = ?, saturday = ?, sunday = ?, updated_at = CURRENT_TIMESTAMP
  772.                     WHERE support_group_id = ?
  773.                 ";
  774.                 $stmt $connection->prepare($sql);
  775.                 $stmt->execute([
  776.                     $timezone$startTime$endTime,
  777.                     $monday$tuesday$wednesday$thursday$friday$saturday$sunday,
  778.                     $supportGroupId
  779.                 ]);
  780.             } else {
  781.                 // Crear nueva configuración de horarios
  782.                 $sql "
  783.                     INSERT INTO uv_working_hours 
  784.                     (support_group_id, timezone, start_time, end_time, 
  785.                      monday, tuesday, wednesday, thursday, friday, saturday, sunday)
  786.                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  787.                 ";
  788.                 $stmt $connection->prepare($sql);
  789.                 $stmt->execute([
  790.                     $supportGroupId$timezone$startTime$endTime,
  791.                     $monday$tuesday$wednesday$thursday$friday$saturday$sunday
  792.                 ]);
  793.             }
  794.             
  795.             return new JsonResponse([
  796.                 'success' => true,
  797.                 'message' => 'Horarios laborales actualizados exitosamente',
  798.                 'data' => [
  799.                     'supportGroupId' => $supportGroupId,
  800.                     'timezone' => $timezone,
  801.                     'startTime' => $startTime,
  802.                     'endTime' => $endTime,
  803.                     'workingDays' => [
  804.                         'monday' => $monday,
  805.                         'tuesday' => $tuesday,
  806.                         'wednesday' => $wednesday,
  807.                         'thursday' => $thursday,
  808.                         'friday' => $friday,
  809.                         'saturday' => $saturday,
  810.                         'sunday' => $sunday
  811.                     ]
  812.                 ]
  813.             ]);
  814.             
  815.         } catch (\Exception $e) {
  816.             return new JsonResponse([
  817.                 'success' => false,
  818.                 'message' => 'Error interno del servidor: ' $e->getMessage()
  819.             ], 500);
  820.         }
  821.     }
  822.     /**
  823.      * Obtiene solo los agentes que están en línea con su estado de conexión y última actividad
  824.      * @return array
  825.      */
  826.     private function getOnlineAgentsStatus(): array
  827.     {
  828.         $entityManager $this->getDoctrine()->getManager();
  829.         $connection $entityManager->getConnection();
  830.         
  831.         // Obtener agentes online usando el servicio
  832.         $onlineAgents $this->userService->getUsersOnline();
  833.         $onlineAgentIds array_column($onlineAgents'id');
  834.         
  835.         // Si no hay agentes online, devolver array vacío
  836.         if (empty($onlineAgentIds)) {
  837.             return [];
  838.         }
  839.         
  840.         // Obtener última actividad solo de agentes online desde AgentLogin
  841.         $placeholders str_repeat('?,'count($onlineAgentIds) - 1) . '?';
  842.         $sql "
  843.             SELECT 
  844.                 al.agent_id as agentId,
  845.                 MAX(al.date) as lastActivityDate,
  846.                 al.auth_type as lastAuthType
  847.             FROM agent_login al
  848.             WHERE al.agent_id IN ($placeholders)
  849.             GROUP BY al.agent_id
  850.         ";
  851.         
  852.         $stmt $connection->prepare($sql);
  853.         $stmt->execute($onlineAgentIds);
  854.         $agentActivities $stmt->fetchAll(\PDO::FETCH_ASSOC);
  855.         
  856.         // Crear array con estado de conexión y última actividad solo para agentes online
  857.         $agentStatus = [];
  858.         foreach ($agentActivities as $activity) {
  859.             $agentId $activity['agentId'];
  860.             
  861.             $agentStatus[$agentId] = [
  862.                 'isOnline' => true// Todos los agentes en este array están online
  863.                 'lastActivity' => $activity['lastActivityDate'],
  864.                 'lastAuthType' => $activity['lastAuthType']
  865.             ];
  866.         }
  867.         
  868.         return $agentStatus;
  869.     }
  870.     /**
  871.      * Convierte el número del día de la semana a nombre en español
  872.      * @param int $dayNumber 1 = Lunes, 7 = Domingo
  873.      * @return string
  874.      */
  875.     private function getDayOfWeekInSpanish($dayNumber)
  876.     {
  877.         $days = [
  878.             => 'Lunes',
  879.             => 'Martes'
  880.             => 'Miércoles',
  881.             => 'Jueves',
  882.             => 'Viernes',
  883.             => 'Sábado',
  884.             => 'Domingo'
  885.         ];
  886.         
  887.         return $days[$dayNumber] ?? 'Desconocido';
  888.     }
  889.     /**
  890.      * Método público para obtener un agente disponible de oficinas abiertas
  891.      * Versión optimizada que no cierra el EntityManager
  892.      * @return array|null Retorna el primer agente disponible o null si no hay agentes
  893.      */
  894.     public function getAvailableOfficeAgent(): ?array
  895.     {
  896.         $entityManager $this->getDoctrine()->getManager();
  897.         $connection $entityManager->getConnection();
  898.         
  899.         // Obtener solo agentes online
  900.         $onlineAgentsStatus $this->getOnlineAgentsStatus();
  901.         echo "DEBUG - onlineAgentsStatus: ";
  902.         var_dump($onlineAgentsStatus);
  903.         echo "<br>";
  904.         
  905.         // Si no hay agentes online, devolver null
  906.         if (empty($onlineAgentsStatus)) {
  907.             echo "DEBUG - No online agents found<br>";
  908.             return null;
  909.         }
  910.         
  911.         // Obtener oficinas abiertas
  912.         $openOffices $this->getOpenOffices();
  913.         echo "DEBUG - openOffices: ";
  914.         var_dump($openOffices);
  915.         echo "<br>";
  916.         
  917.         // Si no hay oficinas abiertas, devolver null
  918.         if (empty($openOffices)) {
  919.             echo "DEBUG - No open offices found<br>";
  920.             return null;
  921.         }
  922.         
  923.         // Obtener el primer agente online de oficinas abiertas
  924.         $onlineAgentIds array_keys($onlineAgentsStatus);
  925.         $placeholders str_repeat('?,'count($onlineAgentIds) - 1) . '?';
  926.         $officePlaceholders str_repeat('?,'count($openOffices) - 1) . '?';
  927.         
  928.         $sql "
  929.             SELECT 
  930.                 a.id as agentId,
  931.                 a.first_name as firstName,
  932.                 a.last_name as lastName,
  933.                 a.email,
  934.                 a.is_enabled as isEnabled,
  935.                 a.idOR as idor,
  936.                 sg.name as supportGroupName,
  937.                 sg.description as supportGroupDescription,
  938.                 sr.code as supportRoleCode
  939.             FROM uv_user a
  940.             JOIN uv_user_instance ui ON a.id = ui.user_id
  941.             JOIN uv_support_role sr ON ui.supportRole_id = sr.id
  942.             LEFT JOIN uv_support_group sg ON a.idOR = sg.id
  943.             WHERE a.is_enabled = 1
  944.             AND sr.code = 'ROLE_AGENT'
  945.             AND a.id IN ($placeholders)
  946.             AND a.idOR IN ($officePlaceholders)
  947.             ORDER BY a.first_name, a.last_name
  948.             LIMIT 1
  949.         ";
  950.         
  951.         $params array_merge($onlineAgentIds$openOffices);
  952.         echo "DEBUG - SQL params: ";
  953.         var_dump($params);
  954.         echo "<br>";
  955.         
  956.         $stmt $connection->prepare($sql);
  957.         $stmt->execute($params);
  958.         $agent $stmt->fetch(\PDO::FETCH_ASSOC);
  959.         
  960.         echo "DEBUG - Found agent: ";
  961.         var_dump($agent);
  962.         echo "<br>";
  963.         
  964.         return $agent ?: null;
  965.     }
  966.     /**
  967.      * Obtiene las oficinas que están actualmente abiertas
  968.      * @return array
  969.      */
  970.     private function getOpenOffices(): array
  971.     {
  972.         $entityManager $this->getDoctrine()->getManager();
  973.         $connection $entityManager->getConnection();
  974.         
  975.         // Obtener grupos de soporte con horarios y asignación automática habilitada
  976.         $sql "
  977.             SELECT 
  978.                 sg.id as supportGroupId,
  979.                 sg.name as supportGroupName,
  980.                 wh.timezone,
  981.                 wh.start_time,
  982.                 wh.end_time,
  983.                 wh.monday,
  984.                 wh.tuesday,
  985.                 wh.wednesday,
  986.                 wh.thursday,
  987.                 wh.friday,
  988.                 wh.saturday,
  989.                 wh.sunday,
  990.                 wh.auto_assignment_enabled
  991.             FROM uv_support_group sg
  992.             LEFT JOIN uv_working_hours wh ON sg.id = wh.support_group_id
  993.             WHERE sg.is_active = 1
  994.             AND (sg.name LIKE 'OR %' OR sg.name = 'Casa Central')
  995.             AND (wh.auto_assignment_enabled = 1 OR wh.auto_assignment_enabled IS NULL)
  996.         ";
  997.         
  998.         $stmt $connection->prepare($sql);
  999.         $stmt->execute();
  1000.         $supportGroups $stmt->fetchAll(\PDO::FETCH_ASSOC);
  1001.         
  1002.         // Determinar qué oficinas están abiertas
  1003.         $openOffices = [];
  1004.         foreach ($supportGroups as $group) {
  1005.             // Si no hay configuración de horarios, usar valores por defecto
  1006.             if (!$group['timezone']) {
  1007.                 $group['timezone'] = 'UTC';
  1008.                 $group['start_time'] = '09:00:00';
  1009.                 $group['end_time'] = '18:00:00';
  1010.                 $group['monday'] = 1;
  1011.                 $group['tuesday'] = 1;
  1012.                 $group['wednesday'] = 1;
  1013.                 $group['thursday'] = 1;
  1014.                 $group['friday'] = 1;
  1015.                 $group['saturday'] = 0;
  1016.                 $group['sunday'] = 0;
  1017.             }
  1018.             
  1019.             $localTime = new \DateTime('now', new \DateTimeZone($group['timezone']));
  1020.             $dayOfWeek = (int)$localTime->format('N');
  1021.             $hour = (int)$localTime->format('G');
  1022.             $minute = (int)$localTime->format('i');
  1023.             $currentTime $hour 60 $minute;
  1024.             
  1025.             // Convertir horarios a minutos
  1026.             $startTimeParts explode(':'$group['start_time']);
  1027.             $endTimeParts explode(':'$group['end_time']);
  1028.             $startTimeMinutes = (int)$startTimeParts[0] * 60 + (int)$startTimeParts[1];
  1029.             $endTimeMinutes = (int)$endTimeParts[0] * 60 + (int)$endTimeParts[1];
  1030.             
  1031.             // Verificar días laborales
  1032.             $workingDays = [
  1033.                 => $group['monday'],
  1034.                 => $group['tuesday'],
  1035.                 => $group['wednesday'],
  1036.                 => $group['thursday'],
  1037.                 => $group['friday'],
  1038.                 => $group['saturday'],
  1039.                 => $group['sunday']
  1040.             ];
  1041.             
  1042.             $isWorkingDay $workingDays[$dayOfWeek] == 1;
  1043.             $isWorkingHours $isWorkingDay && $currentTime >= $startTimeMinutes && $currentTime $endTimeMinutes;
  1044.             
  1045.             if ($isWorkingHours) {
  1046.                 $openOffices[] = $group['supportGroupId'];
  1047.             }
  1048.         }
  1049.         
  1050.         return $openOffices;
  1051.     }
  1052.     /**
  1053.      * Actualiza el estado de asignación automática para una oficina
  1054.      * @Route("/working-hours/toggle-auto-assignment", name="helpdesk_report_working_hours_toggle_auto_assignment", methods={"POST"})
  1055.      */
  1056.     public function toggleAutoAssignmentAction(Request $request): JsonResponse
  1057.     {
  1058.         try {
  1059.             $data json_decode($request->getContent(), true);
  1060.             
  1061.             if (!isset($data['supportGroupId']) || !isset($data['enabled'])) {
  1062.                 return new JsonResponse([
  1063.                     'success' => false,
  1064.                     'message' => 'Faltan parámetros requeridos'
  1065.                 ], 400);
  1066.             }
  1067.             
  1068.             $supportGroupId $data['supportGroupId'];
  1069.             $enabled = (bool)$data['enabled'];
  1070.             
  1071.             $entityManager $this->getDoctrine()->getManager();
  1072.             $connection $entityManager->getConnection();
  1073.             
  1074.             // Verificar si existe el registro en uv_working_hours
  1075.             $checkSql "SELECT id FROM uv_working_hours WHERE support_group_id = ?";
  1076.             $stmt $connection->prepare($checkSql);
  1077.             $stmt->execute([$supportGroupId]);
  1078.             $existingRecord $stmt->fetch(\PDO::FETCH_ASSOC);
  1079.             
  1080.             if ($existingRecord) {
  1081.                 // Actualizar registro existente
  1082.                 $updateSql "UPDATE uv_working_hours SET auto_assignment_enabled = ? WHERE support_group_id = ?";
  1083.                 $stmt $connection->prepare($updateSql);
  1084.                 $stmt->execute([$enabled 0$supportGroupId]);
  1085.             } else {
  1086.                 // Crear nuevo registro con valores por defecto
  1087.                 $insertSql "
  1088.                     INSERT INTO uv_working_hours 
  1089.                     (support_group_id, timezone, start_time, end_time, monday, tuesday, wednesday, thursday, friday, saturday, sunday, is_active, auto_assignment_enabled)
  1090.                     VALUES (?, 'UTC', '09:00:00', '18:00:00', 1, 1, 1, 1, 1, 0, 0, 1, ?)
  1091.                 ";
  1092.                 $stmt $connection->prepare($insertSql);
  1093.                 $stmt->execute([$supportGroupId$enabled 0]);
  1094.             }
  1095.             
  1096.             return new JsonResponse([
  1097.                 'success' => true,
  1098.                 'message' => 'Estado de asignación automática actualizado correctamente',
  1099.                 'enabled' => $enabled
  1100.             ]);
  1101.             
  1102.         } catch (\Exception $e) {
  1103.             return new JsonResponse([
  1104.                 'success' => false,
  1105.                 'message' => 'Error al actualizar el estado: ' $e->getMessage()
  1106.             ], 500);
  1107.         }
  1108.     }
  1109. }