diff --git a/call_center_pro.php b/call_center_pro.php index 3d88053d..3da06047 100644 --- a/call_center_pro.php +++ b/call_center_pro.php @@ -73,6 +73,10 @@ function cc_test_display_value(?string $value, string $fallback = 'No registrado function cc_test_order_label(array $order): string { + if (!empty($order['is_agregado'])) { + return 'AGREGADOS'; + } + $codigo = trim((string) ($order['codigo'] ?? '')); if ($codigo !== '') { return '#' . ltrim($codigo, '#'); @@ -98,16 +102,40 @@ function cc_test_badge_class(string $estado): string 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) { + foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado'] as $field) { $date = cc_test_parse_datetime($order[$field] ?? null); if ($date) { return $date->getTimestamp(); } } - return 0; + // Fallback recency for Drive imports where import_id is not a datetime (e.g. "#35898"). + return cc_test_import_time($order); } +function cc_test_import_time(array $order): int +{ + if (!empty($order['is_agregado'])) { + return (int) ($order['id'] ?? 0); + } + + $importRaw = trim((string) ($order['import_id'] ?? '')); + if ($importRaw !== '') { + $date = cc_test_parse_datetime($importRaw); + if ($date) { + return $date->getTimestamp(); + } + + $digits = preg_replace('/\D+/', '', $importRaw); + if ($digits !== '') { + return (int) $digits; + } + } + + $codigoRaw = trim((string) ($order['codigo'] ?? '')); + $digits = preg_replace('/\D+/', '', $codigoRaw); + return $digits !== '' ? (int) $digits : 0; +} function cc_test_followup_semaforo(array $order): ?array { if (cc_test_normalize_state((string) ($order['estado'] ?? '')) !== 'SE ENVIO NUMERO DE CUENTA') { @@ -146,6 +174,8 @@ $orders = []; $visibleOrders = []; $modalsHtml = []; $totalRows = 0; +$agregadosCount = 0; +$tuaniLastProcessedRow = null; $loadLimit = $storeKey === 'otra_tienda' ? 0 : 100; $startRow = (int) ($storeConfig['startRow'] ?? 5552); $catalogoProductos = []; @@ -172,9 +202,31 @@ try { $catalogoProductos = []; } - $preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey); - $totalRows = (int) ($preview['total_rows'] ?? 0); - $orders = $preview['orders'] ?? []; + if ($storeKey === 'otra_tienda') { + $syncInfo = drive_test_sync_orders_incremental($pdo, $storeKey, 0); + $totalRows = (int) ($syncInfo['drive_rows_total'] ?? 0); + $tuaniLastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0); + $startRow = (int) ($syncInfo['next_start_row'] ?? $startRow); + + $orders = drive_test_fetch_orders_from_db($pdo, $storeKey); + + $agregadosCount = 0; + foreach ($orders as $o) { + if (!empty($o['is_agregado'])) { + $agregadosCount++; + } + } + } else { + $preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey); + $totalRows = (int) ($preview['total_rows'] ?? 0); + $orders = $preview['orders'] ?? []; + + $agregadosOrders = drive_test_fetch_orders_from_db($pdo, $storeKey, true); + $agregadosCount = count($agregadosOrders); + if (!empty($agregadosOrders)) { + $orders = array_merge($orders, $agregadosOrders); + } + } $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); $orders = drive_test_merge_tracking($orders, $tracking); @@ -289,9 +341,22 @@ try { })); 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); + $aAgregado = !empty($a['is_agregado']); + $bAgregado = !empty($b['is_agregado']); + if ($aAgregado !== $bAgregado) { + return $aAgregado ? 1 : -1; + } + + $aImport = cc_test_import_time($a); + $bImport = cc_test_import_time($b); + + if ($aImport !== $bImport) { + return $bImport <=> $aImport; + } + + 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; } @@ -334,7 +399,8 @@ require_once 'layout_header.php';
Drive detectado: filas - Extracción: desde fila + Agregados: + TUANI: último procesado · próxima desde Extracción: desde fila Auto actualización: cada 10 min Ver vista previa Drive
diff --git a/db/migrations/088_add_store_key_and_drive_test_checkpoints.sql b/db/migrations/088_add_store_key_and_drive_test_checkpoints.sql new file mode 100644 index 00000000..b833198a --- /dev/null +++ b/db/migrations/088_add_store_key_and_drive_test_checkpoints.sql @@ -0,0 +1,16 @@ +-- 088_add_store_key_and_drive_test_checkpoints.sql +-- Adds store_key to callcenter_test_orders (so we can filter per Drive store) +-- and creates a checkpoint table to support incremental Drive imports for TUANI. + +ALTER TABLE callcenter_test_orders + ADD COLUMN IF NOT EXISTS store_key VARCHAR(50) NULL; + +CREATE TABLE IF NOT EXISTS drive_test_import_checkpoints ( + store_key VARCHAR(50) NOT NULL, + last_processed_row INT NOT NULL DEFAULT 0, + last_sync_at DATETIME NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (store_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE INDEX idx_callcenter_test_orders_store_key ON callcenter_test_orders (store_key); \ No newline at end of file diff --git a/db/migrations/089_expand_callcenter_test_orders_dni_drive.sql b/db/migrations/089_expand_callcenter_test_orders_dni_drive.sql new file mode 100644 index 00000000..7d0ec647 --- /dev/null +++ b/db/migrations/089_expand_callcenter_test_orders_dni_drive.sql @@ -0,0 +1,3 @@ +-- 089_expand_callcenter_test_orders_dni_drive.sql +ALTER TABLE callcenter_test_orders + MODIFY COLUMN dni_drive TEXT NULL; diff --git a/db/migrations/090_expand_callcenter_test_orders_dni_drive_longtext.sql b/db/migrations/090_expand_callcenter_test_orders_dni_drive_longtext.sql new file mode 100644 index 00000000..20e9ad1d --- /dev/null +++ b/db/migrations/090_expand_callcenter_test_orders_dni_drive_longtext.sql @@ -0,0 +1,4 @@ +-- 090_expand_callcenter_test_orders_dni_drive_longtext.sql +-- Ensures callcenter_test_orders.dni_drive can store very long values coming from Google Sheets. +ALTER TABLE callcenter_test_orders + MODIFY COLUMN dni_drive LONGTEXT NULL; diff --git a/download_agregados_pedidos_template.php b/download_agregados_pedidos_template.php new file mode 100644 index 00000000..5844d506 --- /dev/null +++ b/download_agregados_pedidos_template.php @@ -0,0 +1,54 @@ +downloadAs($filename); diff --git a/download_report.php b/download_report.php index a48c0295..ec8166af 100644 --- a/download_report.php +++ b/download_report.php @@ -20,7 +20,7 @@ try { p.producto, p.cantidad FROM pedidos p - WHERE p.estado = 'ROTULADO 📦' + WHERE (p.estado = 'ROTULADO 📦' OR p.estado = 'PENDIENTE') ORDER BY p.id DESC "; $stmt = $pdo->prepare($query); diff --git a/download_shalom.php b/download_shalom.php index f69d1fbc..5b28fe75 100644 --- a/download_shalom.php +++ b/download_shalom.php @@ -13,11 +13,11 @@ try { $pdo = db(); $type = $_GET['type'] ?? 'all'; - // Filtramos solo los pedidos que están en estado 'ROTULADO 📦' y son de la agencia 'SHALOM' + // Filtramos los pedidos ROTULADO 📦 y PENDIENTE (listados como 'Pedidos Rotulados') y de la agencia 'SHALOM' $query = " SELECT p.dni_cliente, p.celular, p.sede_envio, p.agencia, p.producto, p.cantidad FROM pedidos p - WHERE p.estado = 'ROTULADO 📦' + WHERE (p.estado = 'ROTULADO 📦' OR p.estado = 'PENDIENTE') AND p.agencia LIKE '%SHALOM%' "; diff --git a/gestiones_callcenter.php b/gestiones_callcenter.php index 0f835212..51e11ae3 100644 --- a/gestiones_callcenter.php +++ b/gestiones_callcenter.php @@ -3,6 +3,7 @@ session_start(); require_once 'db/config.php'; require_once 'includes/callcenter_test_helpers.php'; require_once 'includes/drive_test_orders.php'; +require_once 'includes/agregados_excel_import.php'; require_once 'includes/contraentrega_cobertura.php'; $provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento(); @@ -73,6 +74,10 @@ function cc_test_display_value(?string $value, string $fallback = 'No registrado function cc_test_order_label(array $order): string { + if (!empty($order['is_agregado'])) { + return 'AGREGADOS'; + } + $codigo = trim((string) ($order['codigo'] ?? '')); if ($codigo !== '') { return '#' . ltrim($codigo, '#'); @@ -98,16 +103,40 @@ function cc_test_badge_class(string $estado): string 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) { + foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado'] as $field) { $date = cc_test_parse_datetime($order[$field] ?? null); if ($date) { return $date->getTimestamp(); } } - return 0; + // Fallback recency for Drive imports where import_id is not a datetime (e.g. "#35898"). + return cc_test_import_time($order); } +function cc_test_import_time(array $order): int +{ + if (!empty($order['is_agregado'])) { + return (int) ($order['id'] ?? 0); + } + + $importRaw = trim((string) ($order['import_id'] ?? '')); + if ($importRaw !== '') { + $date = cc_test_parse_datetime($importRaw); + if ($date) { + return $date->getTimestamp(); + } + + $digits = preg_replace('/\D+/', '', $importRaw); + if ($digits !== '') { + return (int) $digits; + } + } + + $codigoRaw = trim((string) ($order['codigo'] ?? '')); + $digits = preg_replace('/\D+/', '', $codigoRaw); + return $digits !== '' ? (int) $digits : 0; +} function cc_test_followup_semaforo(array $order): ?array { if (cc_test_normalize_state((string) ($order['estado'] ?? '')) !== 'SE ENVIO NUMERO DE CUENTA') { @@ -141,11 +170,14 @@ $storeConfig = $availableStores[$storeKey]; $errorMessage = null; $noticeMessage = null; +$uploadErrorMessage = null; $assessors = []; $orders = []; $visibleOrders = []; $modalsHtml = []; $totalRows = 0; +$agregadosCount = 0; +$tuaniLastProcessedRow = null; $loadLimit = $storeKey === 'otra_tienda' ? 0 : 100; $cupoObjetivoPorAsesora = 10; @@ -177,9 +209,52 @@ try { $catalogoProductos = []; } - $preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey); - $totalRows = (int) ($preview['total_rows'] ?? 0); - $orders = $preview['orders'] ?? []; + if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'upload_agregados_excel') { + try { + $file = $_FILES['agregados_excel'] ?? null; + if (!is_array($file)) { + throw new RuntimeException('No se encontró el archivo de Excel.'); + } + $maxSize = 10 * 1024 * 1024; // 10 MB + if ((int) ($file['size'] ?? 0) > $maxSize) { + throw new RuntimeException('El archivo es demasiado grande. Máximo 10 MB.'); + } + + $res = cc_agregados_import_from_uploaded_file($pdo, $storeKey, $file); + $inserted = (int) ($res['inserted'] ?? 0); + $skipped = (int) ($res['skipped'] ?? 0); + $noticeMessage = 'Importación completada: ' . $inserted . ' pedidos agregados.' . ($skipped > 0 ? ' Filas ignoradas: ' . $skipped . '.' : ''); + $uploadErrorMessage = null; + } catch (Throwable $e) { + $uploadErrorMessage = $e->getMessage(); + } + } + + if ($storeKey === 'otra_tienda') { + $syncInfo = drive_test_sync_orders_incremental($pdo, $storeKey, 0); + $totalRows = (int) ($syncInfo['drive_rows_total'] ?? 0); + $tuaniLastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0); + $startRow = (int) ($syncInfo['next_start_row'] ?? $startRow); + + $orders = drive_test_fetch_orders_from_db($pdo, $storeKey); + + $agregadosCount = 0; + foreach ($orders as $o) { + if (!empty($o['is_agregado'])) { + $agregadosCount++; + } + } + } else { + $preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey); + $totalRows = (int) ($preview['total_rows'] ?? 0); + $orders = $preview['orders'] ?? []; + + $agregadosOrders = drive_test_fetch_orders_from_db($pdo, $storeKey, true); + $agregadosCount = count($agregadosOrders); + if (!empty($agregadosOrders)) { + $orders = array_merge($orders, $agregadosOrders); + } + } $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); $orders = drive_test_merge_tracking($orders, $tracking); @@ -465,9 +540,22 @@ try { })); 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); + $aAgregado = !empty($a['is_agregado']); + $bAgregado = !empty($b['is_agregado']); + if ($aAgregado !== $bAgregado) { + return $aAgregado ? 1 : -1; + } + + $aImport = cc_test_import_time($a); + $bImport = cc_test_import_time($b); + + if ($aImport !== $bImport) { + return $bImport <=> $aImport; + } + + 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; } @@ -508,7 +596,8 @@ require_once 'layout_header.php';
Drive detectado: filas - Extracción: desde fila + Agregados: + TUANI: último procesado · próxima desde Extracción: desde fila Auto actualización: cada 10 min Ver vista previa Drive
@@ -526,6 +615,47 @@ require_once 'layout_header.php'; +
+
+
+
+
+

+ Agregar pedidos por Excel +

+
+ Para: . + Los pedidos cargados se muestran como AGREGADOS y no afectan la secuencia de Drive. +
+ +
+ +
+
+ + + +
+
+
+ + + + +
+
+
+
diff --git a/imprimir_rotulos_grandes.php b/imprimir_rotulos_grandes.php index 6a666277..ef93e707 100644 --- a/imprimir_rotulos_grandes.php +++ b/imprimir_rotulos_grandes.php @@ -25,7 +25,7 @@ $is_today = isset($_GET['today']); $sql = "SELECT p.* FROM pedidos p - WHERE p.estado = 'ROTULADO 📦'"; + WHERE (p.estado = 'ROTULADO 📦' OR p.estado = 'PENDIENTE')"; $params = []; if ($user_role === 'Asesor') { @@ -197,7 +197,7 @@ $label_generated_date = date('d/m');
-

No hay pedidos rotulados para mostrar.

+

No hay pedidos rotulados o pendientes para mostrar (ROTULADO 📦 / PENDIENTE).

diff --git a/includes/agregados_excel_import.php b/includes/agregados_excel_import.php new file mode 100644 index 00000000..fd290d4c --- /dev/null +++ b/includes/agregados_excel_import.php @@ -0,0 +1,488 @@ + 'Z') { + continue; + } + $index = $index * 26 + (ord($ch) - ord('A') + 1); + } + + return max(0, $index - 1); +} + +function cc_agregados_normalize_header(string $label): string +{ + $label = trim($label); + if ($label === '') { + return ''; + } + + $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT', $label); + if ($ascii !== false && $ascii !== '') { + $label = $ascii; + } + + $label = mb_strtoupper($label); + $label = preg_replace('/[^A-Z0-9]+/', '', $label); + + return (string) ($label ?? ''); +} + +function cc_agregados_guess_field_from_header(string $normalizedHeader): ?string +{ + if ($normalizedHeader === '') { + return null; + } + + $h = $normalizedHeader; + + if (str_contains($h, 'NOMBRE') || str_contains($h, 'CLIENTE')) { + return 'nombre'; + } + if (str_contains($h, 'CELULAR') || str_contains($h, 'TELEF')) { + return 'celular'; + } + if (str_contains($h, 'DNI')) { + return 'dni'; + } + if (str_contains($h, 'DIREC')) { + return 'direccion'; + } + if (str_contains($h, 'REFER')) { + return 'referencia'; + } + if (str_contains($h, 'SEDE')) { + return 'sede'; + } + if (str_contains($h, 'CIUDAD')) { + return 'ciudad'; + } + if (str_contains($h, 'DISTRIT')) { + return 'distrito'; + } + if (str_contains($h, 'PRODUCT')) { + return 'producto'; + } + if (str_contains($h, 'CANT')) { + return 'cantidad'; + } + if (str_contains($h, 'PRECIO')) { + return 'precio'; + } + if (str_contains($h, 'PAIS')) { + return 'pais'; + } + if (str_contains($h, 'COORD')) { + return 'coordenadas'; + } + if (str_contains($h, 'METODO')) { + return 'metodo'; + } + if (str_contains($h, 'OBSERV')) { + return 'observaciones'; + } + + return null; +} + +function cc_agregados_read_csv_rows(string $filePath): array +{ + $handle = fopen($filePath, 'rb'); + if (!$handle) { + throw new RuntimeException('No se pudo leer el archivo CSV.'); + } + + $firstLine = fgets($handle); + if ($firstLine === false) { + fclose($handle); + return []; + } + + $delimiter = ';'; + $commaCount = substr_count($firstLine, ','); + $semiCount = substr_count($firstLine, ';'); + $delimiter = $semiCount >= $commaCount ? ';' : ','; + + rewind($handle); + + $rows = []; + $rowNum = 1; + while (($line = fgetcsv($handle, 0, $delimiter)) !== false) { + // fgetcsv puede devolver nulls. + $clean = []; + foreach ($line as $cell) { + $clean[] = $cell === null ? '' : (string) $cell; + } + $rows[$rowNum] = $clean; + $rowNum++; + } + + fclose($handle); + + return $rows; +} + +function cc_agregados_read_xlsx_rows(string $filePath): array +{ + $zip = new ZipArchive(); + $openResult = $zip->open($filePath); + if ($openResult !== true) { + throw new RuntimeException('No se pudo abrir el archivo XLSX.'); + } + + $sharedStrings = []; + $sharedName = 'xl/sharedStrings.xml'; + if ($zip->locateName($sharedName) !== false) { + $sharedXml = $zip->getFromName($sharedName); + if ($sharedXml) { + $doc = new DOMDocument(); + $doc->loadXML($sharedXml); + $xpath = new DOMXPath($doc); + + foreach ($xpath->query("//*[local-name()='si']") as $siNode) { + $parts = []; + foreach ($xpath->query(".//*[local-name()='t']", $siNode) as $tNode) { + $parts[] = (string) $tNode->textContent; + } + $sharedStrings[] = implode('', $parts); + } + } + } + + // Elegimos la primera hoja. + $sheetName = null; + if ($zip->locateName('xl/worksheets/sheet1.xml') !== false) { + $sheetName = 'xl/worksheets/sheet1.xml'; + } else { + $candidates = []; + for ($i = 0; $i < $zip->numFiles; $i++) { + $name = $zip->getNameIndex($i); + if (preg_match('#^xl/worksheets/sheet\d+\.xml$#', $name)) { + $candidates[] = $name; + } + } + sort($candidates); + $sheetName = $candidates[0] ?? null; + } + + if (!$sheetName) { + throw new RuntimeException('No se encontró una hoja en el archivo XLSX.'); + } + + $sheetXml = $zip->getFromName($sheetName); + if (!$sheetXml) { + throw new RuntimeException('No se pudo leer la hoja del archivo XLSX.'); + } + + $doc = new DOMDocument(); + $doc->loadXML($sheetXml); + $xpath = new DOMXPath($doc); + + $rows = []; + $rowNodes = $xpath->query("//*[local-name()='sheetData']/*[local-name()='row']"); + foreach ($rowNodes as $rowNode) { + $rowNumRaw = $rowNode->getAttribute('r'); + $rowNum = (int) $rowNumRaw; + if ($rowNum <= 0) { + continue; + } + + foreach ($xpath->query("*[local-name()='c']", $rowNode) as $cNode) { + /** @var DOMElement $cNode */ + $cellRef = $cNode->getAttribute('r'); + if ($cellRef === '') { + continue; + } + + $colLetters = preg_replace('/\d+/', '', $cellRef); + $colIndex = cc_agregados_col_letters_to_index($colLetters); + if ($colIndex < 0) { + continue; + } + + $cellType = $cNode->getAttribute('t'); + $value = ''; + + if ($cellType === 's') { + $v = (string) $xpath->evaluate("string(./*[local-name()='v'])", $cNode); + $idx = (int) $v; + $value = $sharedStrings[$idx] ?? ''; + } elseif ($cellType === 'inlineStr') { + $parts = []; + foreach ($xpath->query("./*[local-name()='is']/*[local-name()='t']", $cNode) as $tNode) { + $parts[] = (string) $tNode->textContent; + } + $value = implode('', $parts); + } else { + $value = (string) $xpath->evaluate("string(./*[local-name()='v'])", $cNode); + } + + $rows[$rowNum][$colIndex] = $value; + } + } + + return $rows; +} + +function cc_agregados_normalize_phone(string $value): string +{ + $value = trim($value); + if ($value === '') { + return ''; + } + + return preg_replace('/\D+/', '', $value); +} + +function cc_agregados_import_from_uploaded_file(PDO $pdo, string $storeKey, array $uploadedFile): array +{ + if (!isset($uploadedFile['error']) || (int) $uploadedFile['error'] !== UPLOAD_ERR_OK) { + $err = (int) ($uploadedFile['error'] ?? -1); + throw new RuntimeException('Error al subir el archivo. Código: ' . $err); + } + + $tmpPath = (string) ($uploadedFile['tmp_name'] ?? ''); + if ($tmpPath === '' || !file_exists($tmpPath)) { + throw new RuntimeException('El archivo temporal no está disponible.'); + } + + $originalName = (string) ($uploadedFile['name'] ?? ''); + $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); + + // Ensure schema. + drive_test_ensure_orders_table($pdo); + + $rowsByIndex = []; + if ($ext === 'xlsx') { + $rowsByIndex = cc_agregados_read_xlsx_rows($tmpPath); + } elseif ($ext === 'csv') { + $rowsByIndex = cc_agregados_read_csv_rows($tmpPath); + } else { + throw new RuntimeException('Formato no soportado. Solo .xlsx o .csv'); + } + + if (empty($rowsByIndex)) { + throw new RuntimeException('El archivo está vacío o no se pudo leer.'); + } + + // Cabecera: intentamos fila 1. + $rowNumbers = array_keys($rowsByIndex); + sort($rowNumbers); + $headerRowNum = $rowNumbers[0] ?? 1; + if (isset($rowsByIndex[1])) { + $headerRowNum = 1; + } + + $headerRow = $rowsByIndex[$headerRowNum] ?? []; + if (!is_array($headerRow) || empty($headerRow)) { + throw new RuntimeException('No se encontró la fila de encabezados.'); + } + + // Column index => field key + $colToField = []; + foreach ($headerRow as $colIndex => $headerValue) { + $headerValue = trim((string) $headerValue); + if ($headerValue === '') { + continue; + } + $normalized = cc_agregados_normalize_header($headerValue); + $field = cc_agregados_guess_field_from_header($normalized); + if ($field !== null) { + $colToField[(int) $colIndex] = $field; + } + } + + if (empty($colToField)) { + throw new RuntimeException('No se reconocieron encabezados. Revisa la plantilla de Excel.'); + } + + $maxRowNum = max($rowNumbers); + + $now = date('Y-m-d H:i:s'); + + $stmt = $pdo->prepare( + "INSERT INTO callcenter_test_orders ( + store_key, + source_key, + codigo, + import_id, + drive_imported_at, + is_agregado, + nombre, + direccion_drive, + referencia_drive, + sede_drive, + ciudad_drive, + distrito_drive, + dni_drive, + observaciones_drive, + celular, + producto, + cantidad, + precio, + pais, + coordenadas, + metodo + ) VALUES ( + :store_key, + :source_key, + :codigo, + :import_id, + :drive_imported_at, + :is_agregado, + :nombre, + :direccion_drive, + :referencia_drive, + :sede_drive, + :ciudad_drive, + :distrito_drive, + :dni_drive, + :observaciones_drive, + :celular, + :producto, + :cantidad, + :precio, + :pais, + :coordenadas, + :metodo + ) ON DUPLICATE KEY UPDATE + store_key = VALUES(store_key), + codigo = VALUES(codigo), + import_id = VALUES(import_id), + drive_imported_at = VALUES(drive_imported_at), + is_agregado = VALUES(is_agregado), + nombre = VALUES(nombre), + direccion_drive = VALUES(direccion_drive), + referencia_drive = VALUES(referencia_drive), + sede_drive = VALUES(sede_drive), + ciudad_drive = VALUES(ciudad_drive), + distrito_drive = VALUES(distrito_drive), + dni_drive = VALUES(dni_drive), + observaciones_drive = VALUES(observaciones_drive), + celular = VALUES(celular), + producto = VALUES(producto), + cantidad = VALUES(cantidad), + precio = VALUES(precio), + pais = VALUES(pais), + coordenadas = VALUES(coordenadas), + metodo = VALUES(metodo), + last_seen_at = CURRENT_TIMESTAMP" + ); + + $inserted = 0; + $skipped = 0; + $rowProcessed = 0; + + for ($rowNum = $headerRowNum + 1; $rowNum <= $maxRowNum; $rowNum++) { + if (!isset($rowsByIndex[$rowNum])) { + continue; + } + + $rowProcessed++; + + $data = []; + foreach ($colToField as $colIndex => $field) { + $val = $rowsByIndex[$rowNum][$colIndex] ?? ''; + $val = trim((string) $val); + $data[$field] = $val; + } + + $nombre = $data['nombre'] ?? ''; + $celularRaw = $data['celular'] ?? ''; + $celular = cc_agregados_normalize_phone($celularRaw); + $producto = $data['producto'] ?? ''; + + $isEmptyRow = ($nombre === '' && $celular === '' && $producto === ''); + if ($isEmptyRow) { + $skipped++; + continue; + } + + $dni = cc_agregados_null_if_empty($data['dni'] ?? ''); + $direccion = cc_agregados_null_if_empty($data['direccion'] ?? ''); + $referencia = cc_agregados_null_if_empty($data['referencia'] ?? ''); + $sede = cc_agregados_null_if_empty($data['sede'] ?? ''); + $ciudad = cc_agregados_null_if_empty($data['ciudad'] ?? ''); + $distrito = cc_agregados_null_if_empty($data['distrito'] ?? ''); + $observaciones = cc_agregados_null_if_empty($data['observaciones'] ?? ''); + $cantidad = cc_agregados_null_if_empty($data['cantidad'] ?? ''); + $precio = cc_agregados_null_if_empty($data['precio'] ?? ''); + $pais = cc_agregados_null_if_empty($data['pais'] ?? ''); + $coordenadas = cc_agregados_null_if_empty($data['coordenadas'] ?? ''); + $metodo = cc_agregados_null_if_empty($data['metodo'] ?? ''); + + $sourceKeyBase = implode('|', [ + $storeKey, + (string) $nombre, + (string) $celular, + (string) ($dni ?? ''), + (string) $producto, + (string) ($cantidad ?? ''), + (string) ($precio ?? ''), + (string) ($direccion ?? ''), + (string) ($referencia ?? ''), + (string) ($sede ?? ''), + (string) ($ciudad ?? ''), + (string) ($distrito ?? ''), + (string) ($observaciones ?? ''), + (string) ($metodo ?? ''), + ]); + + $sourceKey = sha1('manual|' . $sourceKeyBase); + + $stmt->execute([ + ':store_key' => $storeKey, + ':source_key' => $sourceKey, + ':codigo' => null, + ':import_id' => null, + ':drive_imported_at' => $now, + ':is_agregado' => 1, + ':nombre' => cc_agregados_null_if_empty($nombre), + ':direccion_drive' => $direccion, + ':referencia_drive' => $referencia, + ':sede_drive' => $sede, + ':ciudad_drive' => $ciudad, + ':distrito_drive' => $distrito, + ':dni_drive' => $dni, + ':observaciones_drive' => $observaciones, + ':celular' => $celular !== '' ? $celular : null, + ':producto' => cc_agregados_null_if_empty($producto), + ':cantidad' => $cantidad, + ':precio' => $precio, + ':pais' => $pais, + ':coordenadas' => $coordenadas, + ':metodo' => $metodo, + ]); + + $inserted++; + } + + return [ + 'inserted' => $inserted, + 'skipped' => $skipped, + 'rows_processed' => $rowProcessed, + ]; +} diff --git a/includes/callcenter_test_management.php b/includes/callcenter_test_management.php index 80858ae8..af584ecc 100644 --- a/includes/callcenter_test_management.php +++ b/includes/callcenter_test_management.php @@ -169,7 +169,7 @@ function cc_test_ensure_schema(PDO $pdo): void `sede_drive` VARCHAR(120) NULL, `ciudad_drive` VARCHAR(120) NULL, `distrito_drive` VARCHAR(120) NULL, - `dni_drive` VARCHAR(40) NULL, + `dni_drive` LONGTEXT NULL, `observaciones_drive` TEXT NULL, `celular` VARCHAR(40) DEFAULT NULL, `producto` TEXT NULL, @@ -185,6 +185,26 @@ function cc_test_ensure_schema(PDO $pdo): void KEY `idx_callcenter_test_orders_last_seen_at` (`last_seen_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); + // Asegurar que `dni_drive` soporte valores grandes (evita SQLSTATE[22001] por truncamiento). + try { + $colStmt = $pdo->prepare(" + SELECT DATA_TYPE + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'callcenter_test_orders' + AND COLUMN_NAME = 'dni_drive' + LIMIT 1 + "); + $colStmt->execute(); + $dataType = strtolower((string) $colStmt->fetchColumn()); + + if ($dataType !== 'longtext') { + $pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `dni_drive` LONGTEXT NULL"); + } + } catch (Throwable $exception) { + error_log('cc_test_ensure_schema: ' . $exception->getMessage()); + } + $pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_tracking` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `source_key` CHAR(40) NOT NULL, diff --git a/includes/drive_test_orders.php b/includes/drive_test_orders.php index 88f299a0..3b8725f9 100644 --- a/includes/drive_test_orders.php +++ b/includes/drive_test_orders.php @@ -141,7 +141,11 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $ $dataRows = array_slice($values, 1); $startIndex = max(0, $startRow - 2); - if ($startIndex > 0 && $startIndex < count($dataRows)) { + $totalDataRows = count($dataRows); + + if ($startIndex >= $totalDataRows) { + $dataRows = []; + } elseif ($startIndex > 0) { $dataRows = array_slice($dataRows, $startIndex); } @@ -263,3 +267,340 @@ function drive_test_merge_tracking(array $orders, array $tracking): array return $orders; } + + +function drive_test_parse_datetime_mysql(?string $value): ?string +{ + $value = trim((string) $value); + if ($value === "") { + return null; + } + + $formats = [ + "Y-m-d\\TH:i:sP", + "Y-m-d\\TH:i:s", + "Y-m-d\\TH:i", + "Y-m-d H:i:s", + "Y-m-d H:i", + DateTimeInterface::ATOM, + "Y-m-d", + ]; + + foreach ($formats as $format) { + try { + $date = DateTimeImmutable::createFromFormat($format, $value); + if ($date instanceof DateTimeImmutable) { + return $date->format("Y-m-d H:i:s"); + } + } catch (Throwable $exception) { + // ignore and try next format + } + } + + try { + return (new DateTimeImmutable($value))->format("Y-m-d H:i:s"); + } catch (Throwable $exception) { + return null; + } +} + +function drive_test_ensure_orders_table(PDO $pdo): void +{ + static $checked = false; + if ($checked) { + return; + } + + $pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_orders` ( + `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `store_key` VARCHAR(50) NULL, + `source_key` CHAR(40) NOT NULL, + `codigo` VARCHAR(80) DEFAULT NULL, + `import_id` VARCHAR(120) DEFAULT NULL, + `is_agregado` TINYINT(1) NOT NULL DEFAULT 0, + `drive_imported_at` DATETIME NULL, + `nombre` VARCHAR(255) DEFAULT NULL, + `direccion_drive` TEXT NULL, + `referencia_drive` TEXT NULL, + `sede_drive` VARCHAR(120) NULL, + `ciudad_drive` VARCHAR(120) NULL, + `distrito_drive` VARCHAR(120) NULL, + `dni_drive` LONGTEXT NULL, + `observaciones_drive` TEXT NULL, + `celular` VARCHAR(40) DEFAULT NULL, + `producto` TEXT NULL, + `cantidad` VARCHAR(60) NULL, + `precio` VARCHAR(60) NULL, + `pais` VARCHAR(80) NULL, + `coordenadas` VARCHAR(255) NULL, + `metodo` VARCHAR(120) NULL, + `first_seen_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_seen_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `uniq_callcenter_test_orders_source_key` (`source_key`), + KEY `idx_callcenter_test_orders_drive_imported_at` (`drive_imported_at`), + KEY `idx_callcenter_test_orders_last_seen_at` (`last_seen_at`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); + + // In case the existing table was created before we introduced store_key. + cc_test_ensure_column($pdo, "callcenter_test_orders", "store_key", "VARCHAR(50) NULL"); + + // Asegura columna para pedidos cargados manualmente (Agregados) + cc_test_ensure_column($pdo, "callcenter_test_orders", "is_agregado", "TINYINT(1) NOT NULL DEFAULT 0"); + + // Asegurar que `dni_drive` soporte valores grandes desde Google Sheets. + try { + $colStmt = $pdo->prepare(" + SELECT DATA_TYPE + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'callcenter_test_orders' + AND COLUMN_NAME = 'dni_drive' + LIMIT 1 + "); + $colStmt->execute(); + $dataType = strtolower((string) $colStmt->fetchColumn()); + + if ($dataType !== 'longtext') { + $pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `dni_drive` LONGTEXT NULL"); + } + } catch (Throwable $exception) { + // No bloqueamos el panel si por alguna razón no se puede ajustar el tipo. + error_log('drive_test_ensure_orders_table: ' . $exception->getMessage()); + } + + $checked = true; +} + +function drive_test_ensure_import_checkpoints_table(PDO $pdo): void +{ + static $checked = false; + if ($checked) { + return; + } + + $pdo->exec("CREATE TABLE IF NOT EXISTS `drive_test_import_checkpoints` ( + `store_key` VARCHAR(50) NOT NULL, + `last_processed_row` INT NOT NULL DEFAULT 0, + `last_sync_at` DATETIME NULL, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`store_key`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); + + $checked = true; +} + +function drive_test_sync_orders_incremental(PDO $pdo, string $storeKey, int $overlapRows = 50): array +{ + $storeKey = mb_strtolower(trim($storeKey)); + $storeConfig = drive_test_get_store_config($storeKey); + $baselineStartRow = (int) ($storeConfig["startRow"] ?? 5552); + + drive_test_ensure_orders_table($pdo); + drive_test_ensure_import_checkpoints_table($pdo); + + $stmtCheckpoint = $pdo->prepare("SELECT last_processed_row FROM drive_test_import_checkpoints WHERE store_key = ? LIMIT 1"); + $stmtCheckpoint->execute([$storeKey]); + $checkpointRow = (int) ($stmtCheckpoint->fetchColumn() ?? 0); + + $overlapRows = max(0, $overlapRows); + $effectiveStartRow = $checkpointRow > 0 + ? max($baselineStartRow, $checkpointRow - $overlapRows + 1) + : $baselineStartRow; + + $preview = drive_test_fetch_orders(0, $effectiveStartRow, $storeKey); + $driveOrders = $preview["orders"] ?? []; + $driveTotalRows = (int) ($preview["total_rows"] ?? 0); + + $sheetLastRow = ($effectiveStartRow - 1) + $driveTotalRows; + $newCheckpointRow = max($checkpointRow, (int) $sheetLastRow); + + if (!empty($driveOrders)) { + $stmtUpsert = $pdo->prepare( + "INSERT INTO callcenter_test_orders ( + store_key, + source_key, + codigo, + import_id, + drive_imported_at, + nombre, + direccion_drive, + referencia_drive, + sede_drive, + ciudad_drive, + distrito_drive, + dni_drive, + observaciones_drive, + celular, + producto, + cantidad, + precio, + pais, + coordenadas, + metodo + ) VALUES ( + :store_key, + :source_key, + :codigo, + :import_id, + :drive_imported_at, + :nombre, + :direccion_drive, + :referencia_drive, + :sede_drive, + :ciudad_drive, + :distrito_drive, + :dni_drive, + :observaciones_drive, + :celular, + :producto, + :cantidad, + :precio, + :pais, + :coordenadas, + :metodo + ) ON DUPLICATE KEY UPDATE + store_key = VALUES(store_key), + codigo = VALUES(codigo), + import_id = VALUES(import_id), + is_agregado = 0, + drive_imported_at = VALUES(drive_imported_at), + nombre = VALUES(nombre), + direccion_drive = VALUES(direccion_drive), + referencia_drive = VALUES(referencia_drive), + sede_drive = VALUES(sede_drive), + ciudad_drive = VALUES(ciudad_drive), + distrito_drive = VALUES(distrito_drive), + dni_drive = VALUES(dni_drive), + observaciones_drive = VALUES(observaciones_drive), + celular = VALUES(celular), + producto = VALUES(producto), + cantidad = VALUES(cantidad), + precio = VALUES(precio), + pais = VALUES(pais), + coordenadas = VALUES(coordenadas), + metodo = VALUES(metodo), + last_seen_at = CURRENT_TIMESTAMP" + ); + + foreach ($driveOrders as $order) { + $driveImportedAt = drive_test_parse_datetime_mysql($order["import_id"] ?? null); + + $stmtUpsert->execute([ + ":store_key" => $storeKey, + ":source_key" => $order["source_key"] ?? "", + ":codigo" => ($order["codigo"] ?? "") !== "" ? ($order["codigo"] ?? null) : null, + ":import_id" => ($order["import_id"] ?? "") !== "" ? ($order["import_id"] ?? null) : null, + ":drive_imported_at" => $driveImportedAt, + ":nombre" => ($order["nombre"] ?? "") !== "" ? ($order["nombre"] ?? null) : null, + ":direccion_drive" => ($order["direccion"] ?? "") !== "" ? ($order["direccion"] ?? null) : null, + ":referencia_drive" => ($order["referencia"] ?? "") !== "" ? ($order["referencia"] ?? null) : null, + ":sede_drive" => ($order["sede"] ?? "") !== "" ? ($order["sede"] ?? null) : null, + ":ciudad_drive" => ($order["ciudad"] ?? "") !== "" ? ($order["ciudad"] ?? null) : null, + ":distrito_drive" => ($order["distrito"] ?? "") !== "" ? ($order["distrito"] ?? null) : null, + ":dni_drive" => ($order["dni"] ?? "") !== "" ? ($order["dni"] ?? null) : null, + ":observaciones_drive" => ($order["observaciones"] ?? "") !== "" ? ($order["observaciones"] ?? null) : null, + ":celular" => ($order["celular"] ?? "") !== "" ? ($order["celular"] ?? null) : null, + ":producto" => ($order["producto"] ?? "") !== "" ? ($order["producto"] ?? null) : null, + ":cantidad" => ($order["cantidad"] ?? "") !== "" ? ($order["cantidad"] ?? null) : null, + ":precio" => ($order["precio"] ?? "") !== "" ? ($order["precio"] ?? null) : null, + ":pais" => ($order["pais"] ?? "") !== "" ? ($order["pais"] ?? null) : null, + ":coordenadas" => ($order["coordenadas"] ?? "") !== "" ? ($order["coordenadas"] ?? null) : null, + ":metodo" => ($order["metodo"] ?? "") !== "" ? ($order["metodo"] ?? null) : null, + ]); + } + } + + $stmtCheckpointUpsert = $pdo->prepare( + "INSERT INTO drive_test_import_checkpoints (store_key, last_processed_row, last_sync_at) + VALUES (:store_key, :last_processed_row, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE + last_processed_row = VALUES(last_processed_row), + last_sync_at = CURRENT_TIMESTAMP" + ); + $stmtCheckpointUpsert->execute([ + ":store_key" => $storeKey, + ":last_processed_row" => $newCheckpointRow, + ]); + + return [ + "checkpoint_was" => $checkpointRow, + "last_processed_row" => $newCheckpointRow, + "next_start_row" => $newCheckpointRow + 1, + "effective_start_row" => $effectiveStartRow, + "drive_rows_total" => $driveTotalRows, + "orders_synced_count" => count($driveOrders), + ]; +} + +function drive_test_fetch_orders_from_db(PDO $pdo, string $storeKey, ?bool $onlyAgregados = null): array +{ + $storeKey = mb_strtolower(trim($storeKey)); + drive_test_ensure_orders_table($pdo); + + $where = "WHERE store_key = ?"; + $params = [$storeKey]; + + if ($onlyAgregados === true) { + $where .= " AND is_agregado = 1"; + } elseif ($onlyAgregados === false) { + $where .= " AND (is_agregado = 0 OR is_agregado IS NULL)"; + } + + $stmt = $pdo->prepare( + "SELECT + id, + is_agregado, + source_key, + codigo, + import_id, + nombre, + celular, + producto, + cantidad, + precio, + pais, + coordenadas, + metodo, + direccion_drive, + referencia_drive, + sede_drive, + ciudad_drive, + distrito_drive, + dni_drive, + observaciones_drive + FROM callcenter_test_orders + $where + ORDER BY id DESC" + ); + $stmt->execute($params); + + $orders = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $orders[] = [ + "id" => (int) ($row["id"] ?? 0), + "is_agregado" => (int) ($row["is_agregado"] ?? 0), + "source_key" => (string) ($row["source_key"] ?? ""), + "codigo" => (string) ($row["codigo"] ?? ""), + "import_id" => (string) ($row["import_id"] ?? ""), + "nombre" => (string) ($row["nombre"] ?? ""), + "direccion" => (string) ($row["direccion_drive"] ?? ""), + "referencia" => (string) ($row["referencia_drive"] ?? ""), + "agencia" => "", + "sede_agencia" => "", + "distrito" => (string) ($row["distrito_drive"] ?? ""), + "celular" => (string) ($row["celular"] ?? ""), + "producto" => (string) ($row["producto"] ?? ""), + "cantidad" => (string) ($row["cantidad"] ?? ""), + "precio" => (string) ($row["precio"] ?? ""), + "pais" => (string) ($row["pais"] ?? ""), + "coordenadas" => (string) ($row["coordenadas"] ?? ""), + "ciudad" => (string) ($row["ciudad_drive"] ?? ""), + "metodo" => (string) ($row["metodo"] ?? ""), + "sede" => (string) ($row["sede_drive"] ?? ""), + "dni" => (string) ($row["dni_drive"] ?? ""), + "observaciones" => (string) ($row["observaciones_drive"] ?? ""), + ]; + } + + return $orders; +}