From c4432f5693859141fdbf36e8620cc3746ed8fcbc Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 7 Jul 2026 08:34:42 +0000 Subject: [PATCH] Autosave: 20260707-083540 --- call_center_pro.php | 50 +++-- gestiones_callcenter.php | 304 ++++++++++++++++++++++++++-- includes/drive_test_orders.php | 108 +++++++++- index.php | 2 +- layout_header.php | 8 +- save_llamada.php | 17 ++ simular_asignacion.php | 2 +- test_importar_drive.php | 28 ++- update_callcenter_test_tracking.php | 20 +- 9 files changed, 480 insertions(+), 59 deletions(-) diff --git a/call_center_pro.php b/call_center_pro.php index dd6dc46c..8f8113e9 100644 --- a/call_center_pro.php +++ b/call_center_pro.php @@ -9,8 +9,8 @@ $provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento $distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia(); $departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega); -$pageTitle = 'Call Center Pro | Bandeja de la asesora'; -$pageDescription = 'Replica de la bandeja de Call Center Pro para que cada asesora trabaje desde Call Center Pro con la misma interfaz, estados, historial y acciones sobre sus pedidos asignados.'; +$pageTitle = 'Pedidos Asignados | Bandeja de la asesora'; +$pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con la misma interfaz, estados, historial y acciones.'; if (!cc_test_current_user_can_access_module(db())) { http_response_code(403); @@ -129,6 +129,14 @@ if (!isset($allowedViews[$view])) { $view = 'pendientes_hoy'; } +$storeKey = trim((string) ($_GET['store'] ?? 'flower')); +$storeKey = mb_strtolower($storeKey); +$availableStores = drive_test_available_stores(); +if (!isset($availableStores[$storeKey])) { + $storeKey = 'flower'; +} +$storeConfig = $availableStores[$storeKey]; + $errorMessage = null; $noticeMessage = null; $assessors = []; @@ -136,8 +144,8 @@ $orders = []; $visibleOrders = []; $modalsHtml = []; $totalRows = 0; -$loadLimit = 10; -$startRow = 5552; +$loadLimit = 20; +$startRow = (int) ($storeConfig['startRow'] ?? 5552); $catalogoProductos = []; $stats = [ 'total' => 0, @@ -162,7 +170,7 @@ try { $catalogoProductos = []; } - $preview = drive_test_fetch_orders($loadLimit, $startRow); + $preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey); $totalRows = (int) ($preview['total_rows'] ?? 0); $orders = $preview['orders'] ?? []; @@ -287,17 +295,27 @@ require_once 'layout_header.php';
-

Call Center Pro

+

Pedidos Asignados

+
Drive detectado: filas Extracción: desde fila Auto actualización: cada 10 min - Ver vista previa Drive + Ver vista previa Drive
@@ -315,7 +333,7 @@ require_once 'layout_header.php';
- +
- +
- +
- +
- +
- +
- +
Todos
diff --git a/gestiones_callcenter.php b/gestiones_callcenter.php index 6ecd8e35..79f28b84 100644 --- a/gestiones_callcenter.php +++ b/gestiones_callcenter.php @@ -9,10 +9,11 @@ $provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento $distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia(); $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.'; +$pageTitle = 'Base de Datos Pedidos | Asignación'; +$pageDescription = 'Bandejas de asignación y gestión de estados para el Call Center, cargadas desde Drive.'; -if (!cc_test_current_user_can_access_module(db())) { +$role = (string) ($_SESSION['user_role'] ?? ''); +if (!in_array($role, ['Administrador', 'admin'], true)) { http_response_code(403); require_once 'layout_header.php'; echo "
Acceso denegado.
"; @@ -128,6 +129,14 @@ if (!isset($allowedViews[$view])) { $view = 'pendientes_hoy'; } +$storeKey = trim((string) ($_GET['store'] ?? 'flower')); +$storeKey = mb_strtolower($storeKey); +$availableStores = drive_test_available_stores(); +if (!isset($availableStores[$storeKey])) { + $storeKey = 'flower'; +} +$storeConfig = $availableStores[$storeKey]; + $errorMessage = null; $noticeMessage = null; $assessors = []; @@ -135,8 +144,13 @@ $orders = []; $visibleOrders = []; $modalsHtml = []; $totalRows = 0; -$loadLimit = 10; -$startRow = 5552; +$loadLimit = 20; + +$cupoObjetivoPorAsesora = 10; +$advisorOpenCounts = []; +$advisorTotalCounts = []; +$recommendedAssessorKey = null; +$startRow = (int) ($storeConfig['startRow'] ?? 5552); $catalogoProductos = []; $stats = [ 'total' => 0, @@ -161,7 +175,7 @@ try { $catalogoProductos = []; } - $preview = drive_test_fetch_orders($loadLimit, $startRow); + $preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey); $totalRows = (int) ($preview['total_rows'] ?? 0); $orders = $preview['orders'] ?? []; @@ -191,6 +205,145 @@ try { $orders = drive_test_merge_tracking($orders, $tracking); } + if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'auto_assign_cupo') { + $openStates = cc_test_open_states(); + + if (empty($assessors)) { + throw new RuntimeException('No hay asesoras configuradas en el sistema.'); + } + + $pendingByAssessorId = []; + foreach ($assessors as $assessor) { + $id = (int) ($assessor['id'] ?? 0); + if ($id > 0) { + $pendingByAssessorId[$id] = 0; + } + } + + // Contamos cuántos pedidos siguen activos (POR LLAMAR / DEVOLVER LLAMADA / OBSERVADO). + foreach ($orders as $o) { + if ($o['user_id'] === null) { + continue; + } + + $uid = (int) ($o['user_id'] ?? 0); + if (!isset($pendingByAssessorId[$uid])) { + continue; + } + + $estadoNorm = cc_test_normalize_state((string) ($o['estado'] ?? '')); + if (in_array($estadoNorm, $openStates, true)) { + $pendingByAssessorId[$uid]++; + } + } + + $missingByAssessorId = []; + foreach ($pendingByAssessorId as $uid => $pending) { + $missingByAssessorId[$uid] = max(0, (int) $cupoObjetivoPorAsesora - (int) $pending); + } + + $missingTotal = array_sum($missingByAssessorId); + + // Pedidos sin asignar y que todavía requieren llamada. + $eligibleUnassigned = []; + foreach ($orders as $o) { + if ($o['user_id'] !== null) { + continue; + } + + $estadoNorm = cc_test_normalize_state((string) ($o['estado'] ?? '')); + if (!in_array($estadoNorm, $openStates, true)) { + continue; + } + + $eligibleUnassigned[] = $o; + } + + if ($missingTotal <= 0) { + $noticeMessage = 'Cupos completos: ninguna asesora necesita pedidos por el momento.'; + } elseif (empty($eligibleUnassigned)) { + $noticeMessage = 'No hay pedidos sin asignar (pendientes) para rellenar los cupos.'; + } else { + usort($eligibleUnassigned, static function (array $a, array $b): int { + $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($a) <=> cc_test_order_time($b); + }); + + $orderedAssessorIds = []; + foreach (['KARINA', 'ESTEFANYA'] as $assessorKey) { + if (isset($assessors[$assessorKey])) { + $orderedAssessorIds[] = (int) ($assessors[$assessorKey]['id'] ?? 0); + } + } + if (empty($orderedAssessorIds)) { + $orderedAssessorIds = array_map('intval', array_keys($pendingByAssessorId)); + } + + $assignedCount = 0; + + foreach ($eligibleUnassigned as $order) { + if ($missingTotal <= 0) { + break; + } + + $chosenId = null; + $minPending = PHP_INT_MAX; + + foreach ($orderedAssessorIds as $uid) { + $uid = (int) $uid; + if ($uid <= 0) { + continue; + } + + $missing = $missingByAssessorId[$uid] ?? 0; + if ($missing <= 0) { + continue; + } + + $pending = $pendingByAssessorId[$uid] ?? 0; + if ($pending < $minPending) { + $minPending = $pending; + $chosenId = $uid; + } + } + + if ($chosenId === null) { + break; + } + + cc_test_upsert_assignee($pdo, (string) ($order['source_key'] ?? ''), $chosenId); + + $pendingByAssessorId[$chosenId] = ((int) ($pendingByAssessorId[$chosenId] ?? 0)) + 1; + $missingByAssessorId[$chosenId] = max(0, (int) ($missingByAssessorId[$chosenId] ?? 0) - 1); + $missingTotal--; + + $assignedCount++; + } + + if ($assignedCount > 0) { + $noticeMessage = 'Auto-asignación lista: se asignaron ' . $assignedCount . ' pedidos para completar cupos.'; + } else { + $noticeMessage = 'No se asignaron pedidos (puede que ya no existan cupos o pedidos elegibles).'; + } + + $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)) { @@ -205,6 +358,16 @@ try { $openStates = cc_test_open_states(); $closedStates = cc_test_closed_states(); + $advisorOpenCounts = []; + $advisorTotalCounts = []; + foreach ($assessors as $assessor) { + $id = (int) ($assessor['id'] ?? 0); + if ($id > 0) { + $advisorOpenCounts[$id] = 0; + $advisorTotalCounts[$id] = 0; + } + } + foreach ($orders as &$order) { $order['estado'] = cc_test_normalize_state((string) ($order['estado'] ?? '')); $order['total_llamadas'] = (int) ($callCounts[$order['source_key']] ?? 0); @@ -216,6 +379,16 @@ try { && ($proximaDate === null || $proximaDate <= $todayEnd); $order['es_cerrado'] = in_array($order['estado'], $closedStates, true); + if ($order['user_id'] !== null) { + $userId = (int) $order['user_id']; + if (isset($advisorTotalCounts[$userId])) { + $advisorTotalCounts[$userId]++; + if (in_array($order['estado'], $openStates, true)) { + $advisorOpenCounts[$userId]++; + } + } + } + $stats['total']++; if ($order['es_nuevo_hoy']) { $stats['nuevos_hoy']++; @@ -238,6 +411,26 @@ try { } unset($order); + $orderedAssessorKeys = ['KARINA', 'ESTEFANYA']; + $orderedAssessorKeys = array_values(array_filter($orderedAssessorKeys, static function (string $key) use ($assessors): bool { + return isset($assessors[$key]); + })); + + if (empty($orderedAssessorKeys)) { + $orderedAssessorKeys = array_keys($assessors); + } + + $recommendedAssessorKey = null; + $minPending = PHP_INT_MAX; + foreach ($orderedAssessorKeys as $assessorKey) { + $id = (int) ($assessors[$assessorKey]['id'] ?? 0); + $pending = (int) ($advisorOpenCounts[$id] ?? 0); + if ($pending < $minPending) { + $minPending = $pending; + $recommendedAssessorKey = $assessorKey; + } + } + $visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view): bool { return match ($view) { 'pendientes_hoy' => (bool) ($order['es_pendiente_hoy'] ?? false), @@ -278,17 +471,25 @@ require_once 'layout_header.php';
@@ -306,7 +507,7 @@ require_once 'layout_header.php';
+
+
+
+ 0 ? (int) ($advisorOpenCounts[$karinaId] ?? 0) : 0; + $estefPendientes = $estefId > 0 ? (int) ($advisorOpenCounts[$estefId] ?? 0) : 0; + $karinaCupoRestante = max(0, (int) $cupoObjetivoPorAsesora - $karinaPendientes); + $estefCupoRestante = max(0, (int) $cupoObjetivoPorAsesora - $estefPendientes); + $tieneCupos = ($karinaCupoRestante > 0 || $estefCupoRestante > 0); + ?> + +
+
+

+ Rendimiento por asesora +

+
Pendientes = POR LLAMAR / DEVOLVER LLAMADA / OBSERVADO
+
+ +
+
+
+
KARINA
+ Pendientes: +
+
+
Cupo restante ()
+
+
+
+ +
+
+
ESTEFANYA
+ Pendientes: +
+
+
Cupo restante ()
+
+
+
+ +
+ + +
Asigna pedidos sin asignar hasta completar el cupo.
+
+
+
+
+
+
+
@@ -471,10 +729,16 @@ require_once 'layout_header.php';
Asignar a asesora
+ diff --git a/includes/drive_test_orders.php b/includes/drive_test_orders.php index d2b9ec1b..61b8cec7 100644 --- a/includes/drive_test_orders.php +++ b/includes/drive_test_orders.php @@ -10,6 +10,76 @@ function drive_test_normalize_header(string $header): string return $header; } +function drive_test_available_stores(): array +{ + return [ + // Hoja original que ya venía funcionando. + 'flower' => [ + 'label' => 'Flower', + 'spreadsheet_id' => '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c', + 'sheet_gid' => null, + 'startRow' => 5552, + ], + // Segunda tienda (nuevo link que pasaste). + 'otra_tienda' => [ + 'label' => 'TUANI', + 'spreadsheet_id' => '1QYKeBJIIqYm6yW6Ka-5gKdxhUHwXOAc0No7RjnimxTw', + 'sheet_gid' => 1523126328, + 'startRow' => 5552, + ], + ]; +} + +function drive_test_get_store_config(string $storeKey): array +{ + $storeKey = trim(mb_strtolower($storeKey)); + $stores = drive_test_available_stores(); + + if (!isset($stores[$storeKey])) { + return $stores['flower']; + } + + return $stores[$storeKey] + ['key' => $storeKey]; +} + +function drive_test_resolve_sheet_a1_range($service, string $spreadsheetId, ?int $sheetGid, string $rangeA1): string +{ + if ($sheetGid === null) { + return $rangeA1; + } + + static $cache = []; + $cacheKey = $spreadsheetId . '|' . (string) $sheetGid . '|' . $rangeA1; + if (isset($cache[$cacheKey])) { + return $cache[$cacheKey]; + } + + try { + $meta = $service->spreadsheets->get($spreadsheetId, ['fields' => 'sheets(properties(sheetId,title))']); + foreach (($meta->getSheets() ?? []) as $sheet) { + $props = $sheet->getProperties(); + if (!$props) { + continue; + } + + if ((int) $props->getSheetId() === (int) $sheetGid) { + $title = trim((string) ($props->getTitle() ?? '')); + if ($title !== '') { + // Sheet names with spaces/special chars must be quoted. + $escaped = str_replace("'", "''", $title); + $cache[$cacheKey] = "'{$escaped}'!{$rangeA1}"; + return $cache[$cacheKey]; + } + } + } + } catch (Throwable $exception) { + // Fallback to default range. + } + + $cache[$cacheKey] = $rangeA1; + return $rangeA1; +} + function drive_test_get_cell(array $row, array $indexes, array $aliases, string $default = ''): string { foreach ($aliases as $alias) { @@ -22,11 +92,18 @@ function drive_test_get_cell(array $row, array $indexes, array $aliases, string return $default; } -function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array +function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $storeKey = 'flower'): array { + $storeConfig = drive_test_get_store_config($storeKey); + $credentialsPath = __DIR__ . '/../google_credentials.json'; - $spreadsheetId = '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c'; - $range = 'A:Z'; + $spreadsheetId = (string) ($storeConfig['spreadsheet_id'] ?? ''); + $sheetGid = array_key_exists('sheet_gid', $storeConfig) ? ($storeConfig['sheet_gid'] === null ? null : (int) $storeConfig['sheet_gid']) : null; + $rangeA1 = 'A:Z'; + + if ($spreadsheetId === '') { + throw new RuntimeException('Configuración de Drive para la tienda no válida.'); + } if (!file_exists($credentialsPath)) { throw new RuntimeException('No se encontró el archivo de credenciales de Google.'); @@ -37,6 +114,9 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array $client->addScope(Google\Service\Sheets::SPREADSHEETS_READONLY); $service = new Google\Service\Sheets($client); + + $range = drive_test_resolve_sheet_a1_range($service, $spreadsheetId, $sheetGid, $rangeA1); + $response = $service->spreadsheets_values->get($spreadsheetId, $range); $values = $response->getValues(); @@ -59,16 +139,16 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array } $dataRows = array_slice($values, 1); + $startIndex = max(0, $startRow - 2); - if ($startIndex > 0) { + if ($startIndex > 0 && $startIndex < count($dataRows)) { $dataRows = array_slice($dataRows, $startIndex); } - if ($limit > 0) { - $previewRows = array_reverse(array_slice($dataRows, -$limit)); - } else { - $previewRows = array_reverse($dataRows); - } + $previewRows = $limit > 0 + ? array_reverse(array_slice($dataRows, -$limit)) + : array_reverse($dataRows); + $orders = []; foreach ($previewRows as $row) { @@ -89,7 +169,15 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array $direccion = drive_test_get_cell($row, $headerIndexes, ['DIRECION', 'DIRECCION']); $referencia = drive_test_get_cell($row, $headerIndexes, ['REFERENCIA']); $distrito = drive_test_get_cell($row, $headerIndexes, ['DISTRITO']); - $sourceKey = sha1(implode('|', [$codigo, $importId, $nombre, $celular, $producto])); + + // Importante: para 'flower' mantenemos EXACTAMENTE el algoritmo anterior + // para no romper el historial/tracking ya existente. + $sourceKeyBase = implode('|', [$codigo, $importId, $nombre, $celular, $producto]); + if (($storeConfig['key'] ?? 'flower') === 'flower') { + $sourceKey = sha1($sourceKeyBase); + } else { + $sourceKey = sha1(($storeConfig['key'] ?? $storeKey) . '|' . $sourceKeyBase); + } $orders[] = [ 'source_key' => $sourceKey, diff --git a/index.php b/index.php index d693afda..3372832b 100644 --- a/index.php +++ b/index.php @@ -104,7 +104,7 @@ $pageDescription = 'Entrada rápida al sistema CRM y panel de administración.';
- Abrir Call Center de prueba + Abrir Base de Datos Pedidos
diff --git a/layout_header.php b/layout_header.php index 6ef4b3cd..ad7194e2 100644 --- a/layout_header.php +++ b/layout_header.php @@ -348,13 +348,13 @@ $navItems = [ 'call_center' => [ 'url' => 'gestiones_callcenter.php', 'icon' => 'fa-headset', - 'text' => 'Call Center (Mis Gestiones)', - 'roles' => ['Administrador', 'admin', 'Asesor'] + 'text' => 'Base de Datos Pedidos', + 'roles' => ['Administrador', 'admin'] ], 'call_center_pro' => [ 'url' => 'call_center_pro.php', 'icon' => 'fa-headset', - 'text' => 'Call Center Pro', + 'text' => 'Pedidos Asignados', 'roles' => ['Administrador', 'admin', 'Asesor'] ], 'simular_asignacion' => [ @@ -447,7 +447,7 @@ $navItems = [
- - visibles - +
+ Tienda: + + visibles + +
diff --git a/update_callcenter_test_tracking.php b/update_callcenter_test_tracking.php index 824173a3..8fad7a65 100644 --- a/update_callcenter_test_tracking.php +++ b/update_callcenter_test_tracking.php @@ -71,10 +71,26 @@ try { $pdo = db(); cc_test_ensure_tracking_table($pdo); - $stmtCurrent = $pdo->prepare('SELECT estado, numero_cuenta_enviado_at FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1'); + $stmtCurrent = $pdo->prepare('SELECT estado, numero_cuenta_enviado_at, user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1'); $stmtCurrent->execute([$sourceKey]); $currentTracking = $stmtCurrent->fetch(PDO::FETCH_ASSOC) ?: null; + $role = (string) ($_SESSION['user_role'] ?? ''); + $isAdmin = in_array($role, ['Administrador', 'admin'], true); + if (!$isAdmin) { + $currentUserId = (int) ($_SESSION['user_id'] ?? 0); + $trackingUserId = $currentTracking ? (int) ($currentTracking['user_id'] ?? 0) : null; + if (!$currentTracking || $trackingUserId !== $currentUserId) { + http_response_code(403); + echo json_encode(['success' => false, 'message' => 'No autorizado para gestionar este pedido.']); + exit; + } + } + + $userIdToSet = $isAdmin + ? ($currentTracking ? ($currentTracking['user_id'] ?? null) : null) + : (int) $_SESSION['user_id']; + $proximaRaw = trim((string) ($_POST['proxima_llamada_at'] ?? '')); $proximaLlamada = null; if ($proximaRaw !== '') { @@ -204,7 +220,7 @@ try { ':source_key' => $sourceKey, ':estado' => $estado, ':nota' => $nota, - ':user_id' => (int) $_SESSION['user_id'], + ':user_id' => $userIdToSet, ':direccion' => $direccion, ':referencia' => $referencia, ':agencia' => $agencia,