diff --git a/.gitignore b/.gitignore index e427ff3c..a5867014 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ */node_modules/ */build/ + +assets/uploads/ diff --git a/call_center_pro.php b/call_center_pro.php new file mode 100644 index 00000000..dd6dc46c --- /dev/null +++ b/call_center_pro.php @@ -0,0 +1,1349 @@ +
Acceso denegado.
"; + require_once 'layout_footer.php'; + exit(); +} + + +function cc_test_parse_datetime(?string $value): ?DateTimeImmutable +{ + $value = trim((string) $value); + if ($value === '') { + return null; + } + + try { + return new DateTimeImmutable($value); + } catch (Throwable $exception) { + return null; + } +} + +function cc_test_format_datetime(?string $value, string $fallback = 'Sin programar'): string +{ + $date = cc_test_parse_datetime($value); + return $date ? $date->format('d/m/Y h:i A') : $fallback; +} + +function cc_test_format_datetime_input(?string $value): string +{ + $date = cc_test_parse_datetime($value); + return $date ? $date->format('Y-m-d\TH:i') : ''; +} + +function cc_test_format_date(?string $value, string $fallback = 'Sin fecha'): string +{ + $date = cc_test_parse_datetime($value); + return $date ? $date->format('d/m/Y') : $fallback; +} + +function cc_test_format_date_input(?string $value): string +{ + $date = cc_test_parse_datetime($value); + return $date ? $date->format('Y-m-d') : ''; +} + +function cc_test_format_price(?string $value): string +{ + $value = trim((string) $value); + return $value !== '' ? 'S/ ' . $value : 'No registrado'; +} + +function cc_test_display_value(?string $value, string $fallback = 'No registrado'): string +{ + $value = trim((string) $value); + return $value !== '' ? $value : $fallback; +} + +function cc_test_order_label(array $order): string +{ + $codigo = trim((string) ($order['codigo'] ?? '')); + if ($codigo !== '') { + return '#' . ltrim($codigo, '#'); + } + + return 'Sin número'; +} + +function cc_test_badge_class(string $estado): string +{ + return match (cc_test_normalize_state($estado)) { + 'CONFIRMADO CONTRAENTREGA', 'CONFIRMADO ENVIO' => 'bg-success-subtle text-success-emphasis', + 'DEVOLVER LLAMADA' => 'bg-info-subtle text-info-emphasis', + 'OBSERVADO' => 'bg-warning-subtle text-warning-emphasis', + 'CANCELADO' => 'bg-danger-subtle text-danger-emphasis', + 'REPETIDO' => 'bg-secondary-subtle text-secondary-emphasis', + 'SE ENVIO NUMERO DE CUENTA' => 'bg-primary-subtle text-primary-emphasis', + default => 'bg-dark-subtle text-dark-emphasis', + }; +} + +function cc_test_order_time(array $order): int +{ + foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado', 'import_id'] as $field) { + $date = cc_test_parse_datetime($order[$field] ?? null); + if ($date) { + return $date->getTimestamp(); + } + } + + return 0; +} + +function cc_test_followup_semaforo(array $order): ?array +{ + if (cc_test_normalize_state((string) ($order['estado'] ?? '')) !== 'SE ENVIO NUMERO DE CUENTA') { + return null; + } + + return cc_test_account_followup_semaforo($order['numero_cuenta_enviado_at'] ?? null); +} + +$view = $_GET['view'] ?? 'pendientes_hoy'; +$allowedViews = [ + 'pendientes_hoy' => 'Pendientes de hoy', + 'nuevos_hoy' => 'Nuevos de hoy', + 'confirmados' => 'Confirmados', + 'seguimiento' => 'Seguimiento', + 'observados' => 'Observados', + 'cerrados' => 'Cerrados / descartados', + 'todos' => 'Todos los pedidos cargados', +]; +if (!isset($allowedViews[$view])) { + $view = 'pendientes_hoy'; +} + +$errorMessage = null; +$noticeMessage = null; +$assessors = []; +$orders = []; +$visibleOrders = []; +$modalsHtml = []; +$totalRows = 0; +$loadLimit = 10; +$startRow = 5552; +$catalogoProductos = []; +$stats = [ + 'total' => 0, + 'pendientes_hoy' => 0, + 'nuevos_hoy' => 0, + 'confirmados' => 0, + 'seguimiento' => 0, + 'observados' => 0, + 'cerrados' => 0, +]; + +try { + $pdo = db(); + cc_test_ensure_tracking_table($pdo); + cc_test_ensure_historial_llamadas_table($pdo); + $assessors = cc_test_fetch_assessors($pdo); + + try { + $stmtProductos = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC"); + $catalogoProductos = $stmtProductos->fetchAll(PDO::FETCH_ASSOC); + } catch (Throwable $exception) { + $catalogoProductos = []; + } + + $preview = drive_test_fetch_orders($loadLimit, $startRow); + $totalRows = (int) ($preview['total_rows'] ?? 0); + $orders = $preview['orders'] ?? []; + + $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); + $orders = drive_test_merge_tracking($orders, $tracking); + + if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'assign_assessor') { + $sourceKey = trim((string) ($_POST['source_key'] ?? '')); + $targetValue = trim((string) ($_POST['target_assessor'] ?? '')); + if ($sourceKey === '' || !preg_match('/^[a-f0-9]{40}$/', $sourceKey)) { + throw new RuntimeException('Pedido de prueba inválido para asignar.'); + } + + $targetKey = trim(mb_strtoupper($targetValue)); + $targetKey = preg_replace('/\s+/', ' ', $targetKey) ?? $targetKey; + $selectedAssessor = $targetKey !== '' ? ($assessors[$targetKey] ?? null) : null; + if ($targetKey !== '' && !$selectedAssessor) { + throw new RuntimeException('No se encontró la asesora seleccionada.'); + } + + cc_test_upsert_assignee($pdo, $sourceKey, $selectedAssessor ? (int) $selectedAssessor['id'] : null); + $noticeMessage = $selectedAssessor + ? 'Pedido asignado a ' . $selectedAssessor['label'] . '.' + : 'Pedido dejado sin asignar.'; + + $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); + $orders = drive_test_merge_tracking($orders, $tracking); + } + + $sourceKeys = array_column($orders, 'source_key'); + $callCounts = []; + if (!empty($sourceKeys)) { + $placeholders = implode(',', array_fill(0, count($sourceKeys), '?')); + $stmtCalls = $pdo->prepare("SELECT pedido_id, COUNT(*) as total FROM historial_llamadas WHERE pedido_id IN ($placeholders) GROUP BY pedido_id"); + $stmtCalls->execute($sourceKeys); + $callCounts = $stmtCalls->fetchAll(PDO::FETCH_KEY_PAIR); + } + + $role = (string) ($_SESSION['user_role'] ?? ''); + $currentUserId = (int) ($_SESSION['user_id'] ?? 0); + if (!in_array($role, ['Administrador', 'admin'], true)) { + $orders = array_values(array_filter($orders, static function (array $order) use ($currentUserId): bool { + return (int) ($order['user_id'] ?? 0) === $currentUserId; + })); + } + + $todayStart = new DateTimeImmutable('today'); + $todayEnd = $todayStart->setTime(23, 59, 59); + $openStates = cc_test_open_states(); + $closedStates = cc_test_closed_states(); + + foreach ($orders as &$order) { + $order['estado'] = cc_test_normalize_state((string) ($order['estado'] ?? '')); + $order['total_llamadas'] = (int) ($callCounts[$order['source_key']] ?? 0); + $importDate = cc_test_parse_datetime($order['import_id'] ?? null); + $proximaDate = cc_test_parse_datetime($order['proxima_llamada_at'] ?? null); + + $order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false; + $order['es_pendiente_hoy'] = in_array($order['estado'], $openStates, true) + && ($proximaDate === null || $proximaDate <= $todayEnd); + $order['es_cerrado'] = in_array($order['estado'], $closedStates, true); + + $stats['total']++; + if ($order['es_nuevo_hoy']) { + $stats['nuevos_hoy']++; + } + if ($order['es_pendiente_hoy']) { + $stats['pendientes_hoy']++; + } + if (in_array($order['estado'], cc_test_confirmed_states(), true)) { + $stats['confirmados']++; + } + if ($order['estado'] === 'SE ENVIO NUMERO DE CUENTA') { + $stats['seguimiento']++; + } + if ($order['estado'] === 'OBSERVADO') { + $stats['observados']++; + } + if ($order['es_cerrado']) { + $stats['cerrados']++; + } + } + unset($order); + + $visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view): bool { + return match ($view) { + 'pendientes_hoy' => (bool) ($order['es_pendiente_hoy'] ?? false), + 'nuevos_hoy' => (bool) ($order['es_nuevo_hoy'] ?? false), + 'confirmados' => in_array(($order['estado'] ?? ''), cc_test_confirmed_states(), true), + 'seguimiento' => ($order['estado'] ?? '') === 'SE ENVIO NUMERO DE CUENTA', + 'observados' => ($order['estado'] ?? '') === 'OBSERVADO', + 'cerrados' => (bool) ($order['es_cerrado'] ?? false), + default => true, + }; + })); + + usort($visibleOrders, static function (array $a, array $b) use ($view): int { + if ($view === 'pendientes_hoy') { + $aDue = cc_test_parse_datetime($a['proxima_llamada_at'] ?? null); + $bDue = cc_test_parse_datetime($b['proxima_llamada_at'] ?? null); + if ($aDue && $bDue && $aDue != $bDue) { + return $aDue <=> $bDue; + } + if ($aDue && !$bDue) { + return 1; + } + if (!$aDue && $bDue) { + return -1; + } + } + + return cc_test_order_time($b) <=> cc_test_order_time($a); + }); +} catch (Throwable $exception) { + $errorMessage = $exception->getMessage(); +} + +require_once 'layout_header.php'; +?> + +
+
+
+
+

Call Center Pro

+ +
+
+ Drive detectado: filas + Extracción: desde fila + Auto actualización: cada 10 min + Ver vista previa Drive +
+
+
+ + + + + + + +
+ + + + + + + +
+ +
+
+
+

+

Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega, Confirmado envío, Cancelado y Repetido.

+
+ pedidos en esta bandeja +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
N° PedidoClienteUbicación editablePedidoGestiónAcciones
+ + No hay pedidos en esta bandeja por ahora. +
+
+
ID Drive:
+
+
+
Celular:
+
DNI:
+
+
Dirección:
+
Referencia:
+
Departamento:
+
Provincia:
+
Distrito:
+ +
DISTRITO 1:
+ +
+
+
Cantidad: ·
+
Ingreso Drive:
+
Observación Drive:
+
+
+ + llamadas + + Seguimiento + +
+ +
Número de cuenta enviado hace día ·
+ +
Próxima llamada:
+ +
Entrega programada:
+ +
Última gestión:
+
Nota:
+
+
+ +
+ + +
Asignar a asesora
+ + +
+ + + + + + + + + WhatsApp + + + +
+
+
+
+
+ + + + + +
+
+
+
+
Llamada preparada para AirDroid
+

Número listo para copiar

+
+ +
+
+
+
Pedido
+
-
+
Cliente
+
-
+
Número
+
-
+
+
+ +
+
+ +
+ + + + diff --git a/gestiones_callcenter.php b/gestiones_callcenter.php index 088a9afe..6ecd8e35 100644 --- a/gestiones_callcenter.php +++ b/gestiones_callcenter.php @@ -12,6 +12,14 @@ $departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega $pageTitle = 'Call Center de Prueba | Bandejas de gestión'; $pageDescription = 'Bandejas diarias de Call Center con estados reales, próxima llamada, fecha de entrega, historial y edición de datos del pedido importado desde Drive.'; +if (!cc_test_current_user_can_access_module(db())) { + http_response_code(403); + require_once 'layout_header.php'; + echo "
Acceso denegado.
"; + require_once 'layout_footer.php'; + exit(); +} + function cc_test_parse_datetime(?string $value): ?DateTimeImmutable { $value = trim((string) $value); @@ -121,11 +129,14 @@ if (!isset($allowedViews[$view])) { } $errorMessage = null; +$noticeMessage = null; +$assessors = []; $orders = []; $visibleOrders = []; $modalsHtml = []; $totalRows = 0; $loadLimit = 10; +$startRow = 5552; $catalogoProductos = []; $stats = [ 'total' => 0, @@ -141,6 +152,7 @@ try { $pdo = db(); cc_test_ensure_tracking_table($pdo); cc_test_ensure_historial_llamadas_table($pdo); + $assessors = cc_test_fetch_assessors($pdo); try { $stmtProductos = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC"); @@ -149,13 +161,36 @@ try { $catalogoProductos = []; } - $preview = drive_test_fetch_orders($loadLimit); + $preview = drive_test_fetch_orders($loadLimit, $startRow); $totalRows = (int) ($preview['total_rows'] ?? 0); $orders = $preview['orders'] ?? []; $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); $orders = drive_test_merge_tracking($orders, $tracking); + if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'assign_assessor') { + $sourceKey = trim((string) ($_POST['source_key'] ?? '')); + $targetValue = trim((string) ($_POST['target_assessor'] ?? '')); + if ($sourceKey === '' || !preg_match('/^[a-f0-9]{40}$/', $sourceKey)) { + throw new RuntimeException('Pedido de prueba inválido para asignar.'); + } + + $targetKey = trim(mb_strtoupper($targetValue)); + $targetKey = preg_replace('/\s+/', ' ', $targetKey) ?? $targetKey; + $selectedAssessor = $targetKey !== '' ? ($assessors[$targetKey] ?? null) : null; + if ($targetKey !== '' && !$selectedAssessor) { + throw new RuntimeException('No se encontró la asesora seleccionada.'); + } + + cc_test_upsert_assignee($pdo, $sourceKey, $selectedAssessor ? (int) $selectedAssessor['id'] : null); + $noticeMessage = $selectedAssessor + ? 'Pedido asignado a ' . $selectedAssessor['label'] . '.' + : 'Pedido dejado sin asignar.'; + + $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); + $orders = drive_test_merge_tracking($orders, $tracking); + } + $sourceKeys = array_column($orders, 'source_key'); $callCounts = []; if (!empty($sourceKeys)) { @@ -244,35 +279,31 @@ require_once 'layout_header.php';

Call Center de prueba

-

Ahora el panel trabaja por bandejas: Nuevos de hoy, Pendientes de hoy, Seguimiento, Confirmados, Observados y Cerrados. Así no se mezcla todo cuando entran pedidos diarios.

+
Drive detectado: filas - Carga operativa: últimos registros + Extracción: desde fila + Auto actualización: cada 10 min Ver vista previa Drive
+ + + -
-
-
-

Flujo recomendado ya listo para prueba

-

Los pedidos nuevos entran desde Drive. Luego tú gestionas cada cliente con estado, próxima llamada y observaciones internas. Los casos abiertos siguen apareciendo en Pendientes de hoy, Seguimiento o Observados hasta que los cierres o confirmes.

-
-
-
Campos editables del módulo
-
Dirección, referencia, departamento, provincia, distrito, DNI y observaciones
-
-
-
-
@@ -351,7 +382,6 @@ require_once 'layout_header.php';

Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega, Confirmado envío, Cancelado y Repetido.

-

El botón Llamar / AirDroid registra el intento, copia el número y muestra la ayuda visual para pegar en tu app de AirDroid.

pedidos en esta bandeja
@@ -436,6 +466,20 @@ require_once 'layout_header.php';
+ +
+ + +
Asignar a asesora
+ + +
+
-
-
+
+
-
Producto seleccionado:
@@ -543,6 +586,34 @@ require_once 'layout_header.php';
+
+ +
+
+
+
Producto adicional distinto
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -970,17 +1041,56 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('select[id^="confirmacion_producto-"]').forEach(select => { const sourceKey = select.id.replace('confirmacion_producto-', ''); - const resumen = document.getElementById('confirmacion_producto_resumen-' + sourceKey); - const syncResumen = () => { + const syncTitle = () => { const selectedOption = select.selectedOptions && select.selectedOptions[0] ? select.selectedOptions[0] : null; const label = selectedOption && selectedOption.textContent ? selectedOption.textContent.trim() : (select.value || 'Sin producto seleccionado'); - if (resumen) { - resumen.textContent = 'Producto seleccionado: ' + label; - } select.title = label; }; - syncResumen(); - select.addEventListener('change', syncResumen); + syncTitle(); + select.addEventListener('change', syncTitle); + }); + + document.querySelectorAll('.js-toggle-confirmacion-extra').forEach(button => { + button.addEventListener('click', () => { + const sourceKey = button.dataset.sourceKey || ''; + const block = document.getElementById('confirmacion_extra_block-' + sourceKey); + if (!block) return; + block.classList.remove('d-none'); + button.classList.add('d-none'); + }); + }); + + document.querySelectorAll('.js-remove-confirmacion-extra').forEach(button => { + button.addEventListener('click', () => { + const sourceKey = button.dataset.sourceKey || ''; + const block = document.getElementById('confirmacion_extra_block-' + sourceKey); + const toggle = document.querySelector('.js-toggle-confirmacion-extra[data-source-key="' + sourceKey + '"]'); + if (block) { + block.classList.add('d-none'); + const productoExtra = document.getElementById('confirmacion_producto_extra-' + sourceKey); + const cantidadExtra = document.getElementById('confirmacion_cantidad_extra-' + sourceKey); + const precioExtra = document.getElementById('confirmacion_precio_extra-' + sourceKey); + if (productoExtra) productoExtra.value = ''; + if (cantidadExtra) cantidadExtra.value = ''; + if (precioExtra) precioExtra.value = ''; + } + if (toggle) { + toggle.classList.remove('d-none'); + } + }); + }); + + document.querySelectorAll('.js-confirmacion-extra-block').forEach(block => { + const sourceKey = block.id.replace('confirmacion_extra_block-', ''); + const productoExtra = document.getElementById('confirmacion_producto_extra-' + sourceKey); + const cantidadExtra = document.getElementById('confirmacion_cantidad_extra-' + sourceKey); + const precioExtra = document.getElementById('confirmacion_precio_extra-' + sourceKey); + const toggle = document.querySelector('.js-toggle-confirmacion-extra[data-source-key="' + sourceKey + '"]'); + const hasValue = [productoExtra, cantidadExtra, precioExtra].some(input => (input?.value || '').trim() !== ''); + if (hasValue) { + block.classList.remove('d-none'); + if (toggle) toggle.classList.add('d-none'); + } }); }); @@ -1005,7 +1115,10 @@ function guardarGestion(sourceKey, trigger) { precio: document.getElementById('precio-' + sourceKey)?.value || '', confirmacion_producto: document.getElementById('confirmacion_producto-' + sourceKey)?.value || '', confirmacion_cantidad: document.getElementById('confirmacion_cantidad-' + sourceKey)?.value || '', - confirmacion_precio: document.getElementById('confirmacion_precio-' + sourceKey)?.value || '' + confirmacion_precio: document.getElementById('confirmacion_precio-' + sourceKey)?.value || '', + confirmacion_producto_extra: document.getElementById('confirmacion_producto_extra-' + sourceKey)?.value || '', + confirmacion_cantidad_extra: document.getElementById('confirmacion_cantidad_extra-' + sourceKey)?.value || '', + confirmacion_precio_extra: document.getElementById('confirmacion_precio_extra-' + sourceKey)?.value || '' }); if (trigger) { @@ -1218,6 +1331,10 @@ if (airDroidCopyButton) { }); }); } + +window.setInterval(function () { + window.location.reload(); +}, 600000); diff --git a/includes/callcenter_test_helpers.php b/includes/callcenter_test_helpers.php index a05c343d..61835d1a 100644 --- a/includes/callcenter_test_helpers.php +++ b/includes/callcenter_test_helpers.php @@ -45,6 +45,9 @@ function cc_test_ensure_tracking_table(PDO $pdo): void `confirmacion_producto` VARCHAR(255) NULL, `confirmacion_cantidad` VARCHAR(50) NULL, `confirmacion_precio` VARCHAR(80) NULL, + `confirmacion_producto_extra` VARCHAR(255) NULL, + `confirmacion_cantidad_extra` VARCHAR(50) NULL, + `confirmacion_precio_extra` VARCHAR(80) NULL, `proxima_llamada_at` DATETIME NULL, `fecha_entrega_programada` DATE NULL, `numero_cuenta_enviado_at` DATETIME NULL, @@ -72,6 +75,9 @@ function cc_test_ensure_tracking_table(PDO $pdo): void cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_producto', 'VARCHAR(255) NULL AFTER `precio`'); cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_cantidad', 'VARCHAR(50) NULL AFTER `confirmacion_producto`'); cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_precio', 'VARCHAR(80) NULL AFTER `confirmacion_cantidad`'); + cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_producto_extra', 'VARCHAR(255) NULL AFTER `confirmacion_precio`'); + cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_cantidad_extra', 'VARCHAR(50) NULL AFTER `confirmacion_producto_extra`'); + cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_precio_extra', 'VARCHAR(80) NULL AFTER `confirmacion_cantidad_extra`'); cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'proxima_llamada_at', 'DATETIME NULL AFTER `confirmacion_precio`'); cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'fecha_entrega_programada', 'DATE NULL AFTER `proxima_llamada_at`'); cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'numero_cuenta_enviado_at', 'DATETIME NULL AFTER `fecha_entrega_programada`'); @@ -80,6 +86,107 @@ function cc_test_ensure_tracking_table(PDO $pdo): void $checked = true; } +function cc_test_fetch_assessors(PDO $pdo, array $allowedNames = ['KARINA', 'ESTEFANYA']): array +{ + $stmt = $pdo->prepare("SELECT id, username, nombre_asesor FROM users WHERE role = 'Asesor'"); + $stmt->execute(); + + $allowedLookup = array_fill_keys(array_map('strtoupper', $allowedNames), true); + $byKey = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $key = trim(mb_strtoupper((string) ($row['nombre_asesor'] ?: $row['username'] ?: ''))); + $key = preg_replace('/\s+/', ' ', $key) ?? $key; + if ($key === '' || (!empty($allowedLookup) && !isset($allowedLookup[$key]))) { + continue; + } + + $byKey[$key] = [ + 'id' => (int) $row['id'], + 'label' => trim((string) ($row['nombre_asesor'] ?: $row['username'] ?: ('Asesor #' . (int) $row['id']))), + ]; + } + + return $byKey; +} + +function cc_test_normalize_user_key(?string $value): string +{ + $value = trim((string) $value); + if ($value === '') { + return ''; + } + + $value = preg_replace('/\s+/', ' ', $value) ?? $value; + + return mb_strtoupper($value); +} + +function cc_test_allowed_module_user_keys(): array +{ + return ['KARINA', 'ESTEFANYA']; +} + +function cc_test_is_allowed_module_user(?string $role = null, ?string $username = null, ?string $nombreAsesor = null): bool +{ + $role = trim((string) $role); + if (in_array($role, ['Administrador', 'admin'], true)) { + return true; + } + + $allowedLookup = array_fill_keys(cc_test_allowed_module_user_keys(), true); + foreach ([$username, $nombreAsesor] as $candidate) { + $key = cc_test_normalize_user_key($candidate); + if ($key !== '' && isset($allowedLookup[$key])) { + return true; + } + } + + return false; +} + +function cc_test_current_user_can_access_module(?PDO $pdo = null): bool +{ + $role = $_SESSION['user_role'] ?? ''; + $username = $_SESSION['username'] ?? ''; + $nombreAsesor = null; + + if ($pdo instanceof PDO && !empty($_SESSION['user_id']) && !in_array($role, ['Administrador', 'admin'], true)) { + try { + $stmt = $pdo->prepare('SELECT username, nombre_asesor FROM users WHERE id = ? LIMIT 1'); + $stmt->execute([$_SESSION['user_id']]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $username = $row['username'] ?? $username; + $nombreAsesor = $row['nombre_asesor'] ?? null; + } + } catch (Throwable $exception) { + // Fall back to the session values if the lookup fails. + } + } + + return cc_test_is_allowed_module_user($role, $username, $nombreAsesor); +} + +function cc_test_upsert_assignee(PDO $pdo, string $sourceKey, ?int $userId): void +{ + cc_test_ensure_tracking_table($pdo); + + $stmt = $pdo->prepare("INSERT INTO callcenter_test_tracking (source_key, user_id, ultima_gestion_at) + VALUES (:source_key, :user_id, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE + user_id = VALUES(user_id), + ultima_gestion_at = VALUES(ultima_gestion_at), + updated_at = CURRENT_TIMESTAMP"); + + $stmt->bindValue(':source_key', $sourceKey, PDO::PARAM_STR); + if ($userId === null) { + $stmt->bindValue(':user_id', null, PDO::PARAM_NULL); + } else { + $stmt->bindValue(':user_id', $userId, PDO::PARAM_INT); + } + + $stmt->execute(); +} + function cc_test_ensure_historial_llamadas_table(PDO $pdo): void { static $checked = false; @@ -127,9 +234,11 @@ function cc_test_valid_states(): array ]; } -function cc_test_open_states(): array -{ - return ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO']; +if (!function_exists('cc_test_open_states')) { + function cc_test_open_states(): array + { + return ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO']; + } } function cc_test_confirmed_states(): array @@ -137,9 +246,11 @@ function cc_test_confirmed_states(): array return ['CONFIRMADO CONTRAENTREGA', 'CONFIRMADO ENVIO']; } -function cc_test_closed_states(): array -{ - return ['CANCELADO', 'REPETIDO']; +if (!function_exists('cc_test_closed_states')) { + function cc_test_closed_states(): array + { + return ['CANCELADO', 'REPETIDO']; + } } function cc_test_requires_delivery_date(string $estado): bool @@ -200,17 +311,19 @@ function cc_test_state_label(string $estado): string }; } -function cc_test_table_exists(PDO $pdo, string $tableName): bool -{ - static $cache = []; - if (array_key_exists($tableName, $cache)) { +if (!function_exists('cc_test_table_exists')) { + function cc_test_table_exists(PDO $pdo, string $tableName): bool + { + static $cache = []; + if (array_key_exists($tableName, $cache)) { + return $cache[$tableName]; + } + + $stmt = $pdo->prepare('SHOW TABLES LIKE ?'); + $stmt->execute([$tableName]); + $cache[$tableName] = (bool) $stmt->fetchColumn(); return $cache[$tableName]; } - - $stmt = $pdo->prepare('SHOW TABLES LIKE ?'); - $stmt->execute([$tableName]); - $cache[$tableName] = (bool) $stmt->fetchColumn(); - return $cache[$tableName]; } function cc_test_fetch_historial(PDO $pdo, string $pedidoId): array diff --git a/includes/callcenter_test_management.php b/includes/callcenter_test_management.php index ef7a75cc..80858ae8 100644 --- a/includes/callcenter_test_management.php +++ b/includes/callcenter_test_management.php @@ -1,4 +1,5 @@ prepare('SHOW TABLES LIKE ?'); + $stmt->execute([$tableName]); + $cache[$tableName] = (bool) $stmt->fetchColumn(); - if (array_key_exists($tableName, $cache)) { return $cache[$tableName]; } - - $stmt = $pdo->prepare('SHOW TABLES LIKE ?'); - $stmt->execute([$tableName]); - $cache[$tableName] = (bool) $stmt->fetchColumn(); - - return $cache[$tableName]; } function cc_test_column_exists(PDO $pdo, string $tableName, string $columnName): bool diff --git a/includes/drive_test_orders.php b/includes/drive_test_orders.php index 0e28222a..d2b9ec1b 100644 --- a/includes/drive_test_orders.php +++ b/includes/drive_test_orders.php @@ -22,7 +22,7 @@ function drive_test_get_cell(array $row, array $indexes, array $aliases, string return $default; } -function drive_test_fetch_orders(int $limit = 10): array +function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array { $credentialsPath = __DIR__ . '/../google_credentials.json'; $spreadsheetId = '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c'; @@ -59,7 +59,16 @@ function drive_test_fetch_orders(int $limit = 10): array } $dataRows = array_slice($values, 1); - $previewRows = array_reverse(array_slice($dataRows, -$limit)); + $startIndex = max(0, $startRow - 2); + if ($startIndex > 0) { + $dataRows = array_slice($dataRows, $startIndex); + } + + if ($limit > 0) { + $previewRows = array_reverse(array_slice($dataRows, -$limit)); + } else { + $previewRows = array_reverse($dataRows); + } $orders = []; foreach ($previewRows as $row) { @@ -122,7 +131,7 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array cc_test_ensure_tracking_table($pdo); $placeholders = implode(',', array_fill(0, count($sourceKeys), '?')); - $stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, observaciones, producto, cantidad, precio, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)"); + $stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, user_id, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, observaciones, producto, cantidad, precio, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)"); $stmt->execute($sourceKeys); $tracking = []; @@ -141,6 +150,7 @@ function drive_test_merge_tracking(array $orders, array $tracking): array $current = $tracking[$order['source_key']] ?? null; $order['estado'] = $current['estado'] ?? 'POR LLAMAR'; $order['nota_seguimiento'] = $current['nota_seguimiento'] ?? ''; + $order['user_id'] = array_key_exists('user_id', (array) $current) ? ($current['user_id'] !== null ? (int) $current['user_id'] : null) : null; $order['seguimiento_actualizado'] = $current['updated_at'] ?? null; $order['proxima_llamada_at'] = $current['proxima_llamada_at'] ?? null; $order['fecha_entrega_programada'] = $current['fecha_entrega_programada'] ?? null; diff --git a/layout_header.php b/layout_header.php index d5a4b9a3..6d6ac51a 100644 --- a/layout_header.php +++ b/layout_header.php @@ -10,7 +10,29 @@ if (!isset($_SESSION['user_id'])) { exit(); } +require_once __DIR__ . '/db/config.php'; +require_once __DIR__ . '/includes/callcenter_test_helpers.php'; + $userRole = $_SESSION['user_role'] ?? ''; +$currentUserId = (int) ($_SESSION['user_id'] ?? 0); +$currentUserUsername = $_SESSION['username'] ?? ''; +$currentUserNombreAsesor = null; + +if ($currentUserId > 0 && !in_array($userRole, ['Administrador', 'admin'], true)) { + try { + $pdo = db(); + $stmt = $pdo->prepare('SELECT username, nombre_asesor FROM users WHERE id = ? LIMIT 1'); + $stmt->execute([$currentUserId]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $currentUserUsername = $row['username'] ?? $currentUserUsername; + $currentUserNombreAsesor = $row['nombre_asesor'] ?? null; + } + } catch (Throwable $exception) { + // Use the session values as a fallback. + } +} + +$canAccessTestModule = cc_test_is_allowed_module_user($userRole, $currentUserUsername, $currentUserNombreAsesor); // Define navigation items for each role $navItems = [ @@ -281,6 +303,12 @@ $navItems = [ 'text' => 'Call Center (Mis Gestiones)', 'roles' => ['Administrador', 'admin', 'Asesor'] ], + 'call_center_pro' => [ + 'url' => 'call_center_pro.php', + 'icon' => 'fa-headset', + 'text' => 'Call Center Pro', + 'roles' => ['Administrador', 'admin', 'Asesor'] + ], 'simular_asignacion' => [ 'url' => 'simular_asignacion.php', 'icon' => 'fa-magic', @@ -352,7 +380,12 @@ $navItems = [ $currentPage = basename($_SERVER['PHP_SELF']); foreach ($navItems as $key => $item): - if (in_array($userRole, $item['roles'])): + $isTestModule = ($key === 'modulo_pruebas_group'); + if ($isTestModule && !$canAccessTestModule) { + continue; + } + + if ($isTestModule || in_array($userRole, $item['roles'])): if (isset($item['submenu'])): $isSubmenuActive = false; foreach ($item['submenu'] as $sub_item) { @@ -366,7 +399,7 @@ $navItems = [
"; +if (!cc_test_current_user_can_access_module(db())) { + http_response_code(403); + require_once __DIR__ . '/layout_header.php'; + echo "
Acceso denegado.
"; require_once __DIR__ . '/layout_footer.php'; exit(); } +require_once __DIR__ . '/layout_header.php'; require_once __DIR__ . '/vendor/autoload.php'; $credentialsPath = __DIR__ . '/google_credentials.json'; diff --git a/update_callcenter_test_tracking.php b/update_callcenter_test_tracking.php index 7dbe1855..824173a3 100644 --- a/update_callcenter_test_tracking.php +++ b/update_callcenter_test_tracking.php @@ -5,7 +5,7 @@ require_once 'includes/callcenter_test_helpers.php'; header('Content-Type: application/json; charset=utf-8'); -if (!isset($_SESSION['user_id'])) { +if (!isset($_SESSION['user_id']) || !cc_test_current_user_can_access_module(db())) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'No autorizado']); exit; @@ -64,6 +64,9 @@ try { $confirmacionProducto = cc_test_normalize_nullable_text('confirmacion_producto', 255); $confirmacionCantidad = cc_test_normalize_nullable_text('confirmacion_cantidad', 50); $confirmacionPrecio = cc_test_normalize_nullable_text('confirmacion_precio', 80); + $confirmacionProductoExtra = cc_test_normalize_nullable_text('confirmacion_producto_extra', 255); + $confirmacionCantidadExtra = cc_test_normalize_nullable_text('confirmacion_cantidad_extra', 50); + $confirmacionPrecioExtra = cc_test_normalize_nullable_text('confirmacion_precio_extra', 80); $pdo = db(); cc_test_ensure_tracking_table($pdo); @@ -133,6 +136,9 @@ try { confirmacion_producto, confirmacion_cantidad, confirmacion_precio, + confirmacion_producto_extra, + confirmacion_cantidad_extra, + confirmacion_precio_extra, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, @@ -157,6 +163,9 @@ try { :confirmacion_producto, :confirmacion_cantidad, :confirmacion_precio, + :confirmacion_producto_extra, + :confirmacion_cantidad_extra, + :confirmacion_precio_extra, :proxima_llamada_at, :fecha_entrega_programada, :numero_cuenta_enviado_at, @@ -181,6 +190,9 @@ try { confirmacion_producto = VALUES(confirmacion_producto), confirmacion_cantidad = VALUES(confirmacion_cantidad), confirmacion_precio = VALUES(confirmacion_precio), + confirmacion_producto_extra = VALUES(confirmacion_producto_extra), + confirmacion_cantidad_extra = VALUES(confirmacion_cantidad_extra), + confirmacion_precio_extra = VALUES(confirmacion_precio_extra), proxima_llamada_at = VALUES(proxima_llamada_at), fecha_entrega_programada = VALUES(fecha_entrega_programada), numero_cuenta_enviado_at = VALUES(numero_cuenta_enviado_at), @@ -208,6 +220,9 @@ try { ':confirmacion_producto' => $confirmacionProducto, ':confirmacion_cantidad' => $confirmacionCantidad, ':confirmacion_precio' => $confirmacionPrecio, + ':confirmacion_producto_extra' => $confirmacionProductoExtra, + ':confirmacion_cantidad_extra' => $confirmacionCantidadExtra, + ':confirmacion_precio_extra' => $confirmacionPrecioExtra, ':proxima_llamada_at' => $proximaLlamada, ':fecha_entrega_programada' => $fechaEntrega, ':numero_cuenta_enviado_at' => $numeroCuentaEnviadoAt,