[ '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' => 6710, ], ]; } 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) { $normalized = drive_test_normalize_header($alias); if (array_key_exists($normalized, $indexes)) { return trim((string) ($row[$indexes[$normalized]] ?? $default)); } } return $default; } 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 = (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.'); } $client = new Google\Client(); $client->setAuthConfig($credentialsPath); $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(); if (empty($values)) { return [ 'headers' => [], 'orders' => [], 'total_rows' => 0, ]; } $headers = $values[0] ?? []; $headerIndexes = []; foreach ($headers as $index => $header) { $normalized = drive_test_normalize_header((string) $header); if ($normalized === '') { $normalized = $index === 0 ? 'CODIGO' : 'COL_' . $index; } $headerIndexes[$normalized] = $index; } $dataRows = array_slice($values, 1); $startIndex = max(0, $startRow - 2); $totalDataRows = count($dataRows); if ($startIndex >= $totalDataRows) { $dataRows = []; } elseif ($startIndex > 0) { $dataRows = array_slice($dataRows, $startIndex); } $previewRows = $limit > 0 ? array_reverse(array_slice($dataRows, -$limit)) : array_reverse($dataRows); $orders = []; foreach ($previewRows as $row) { $codigo = trim((string) ($row[0] ?? '')); $importId = drive_test_get_cell($row, $headerIndexes, ['ID']); $nombre = drive_test_get_cell($row, $headerIndexes, ['NOMBRE']); $celular = preg_replace('/\D+/', '', drive_test_get_cell($row, $headerIndexes, ['CELULAR'])); $producto = drive_test_get_cell($row, $headerIndexes, ['PRODUCTO']); $cantidad = drive_test_get_cell($row, $headerIndexes, ['CANTIDAD']); $precio = drive_test_get_cell($row, $headerIndexes, ['PRECIO']); $pais = drive_test_get_cell($row, $headerIndexes, ['PAIS']); $coordenadas = drive_test_get_cell($row, $headerIndexes, ['COORDENADAS']); $ciudad = drive_test_get_cell($row, $headerIndexes, ['CIUDAD']); $metodo = drive_test_get_cell($row, $headerIndexes, ['METODO']); $sede = drive_test_get_cell($row, $headerIndexes, ['SEDE / ID', 'SEDE/ID']); $dni = drive_test_get_cell($row, $headerIndexes, ['N° DNI', 'N° DNI ', 'NRO DNI', 'DNI']); $observaciones = drive_test_get_cell($row, $headerIndexes, ['OBSERVACIONES', 'OBSERVACIONES ']); $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']); // 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, 'codigo' => $codigo, 'import_id' => $importId, 'nombre' => $nombre, 'direccion' => $direccion, 'referencia' => $referencia, 'agencia' => '', 'sede_agencia' => '', 'distrito' => $distrito, 'celular' => $celular, 'producto' => $producto, 'cantidad' => $cantidad, 'precio' => $precio, 'pais' => $pais, 'coordenadas' => $coordenadas, 'ciudad' => $ciudad, 'metodo' => $metodo, 'sede' => $sede, 'dni' => $dni, 'observaciones' => $observaciones, ]; } return [ 'headers' => $headers, 'orders' => $orders, 'total_rows' => count($dataRows), ]; } function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array { if (empty($sourceKeys)) { return []; } cc_test_ensure_tracking_table($pdo); $placeholders = implode(',', array_fill(0, count($sourceKeys), '?')); $stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, user_id, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, observaciones, coordenadas, producto, cantidad, precio, 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, ruta_contraentrega_pedido_id, ruta_contraentrega_subido_at, ruta_contraentrega_subido_por, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)"); $stmt->execute($sourceKeys); $tracking = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $tracking[$row['source_key']] = $row; } return $tracking; } function drive_test_merge_tracking(array $orders, array $tracking): array { $editableFields = ['direccion', 'referencia', 'agencia', 'sede_agencia', 'sede', 'ciudad', 'distrito', 'dni', 'observaciones', 'coordenadas', 'producto', 'cantidad', 'precio', 'confirmacion_producto', 'confirmacion_cantidad', 'confirmacion_precio', 'confirmacion_producto_extra', 'confirmacion_cantidad_extra', 'confirmacion_precio_extra']; foreach ($orders as &$order) { $current = $tracking[$order['source_key']] ?? null; $order['estado'] = $current['estado'] ?? 'POR LLAMAR'; $order['nota_seguimiento'] = $current['nota_seguimiento'] ?? ''; $userId = array_key_exists('user_id', (array) $current) ? $current['user_id'] : null; $order['user_id'] = $userId !== null ? ((int) $userId > 0 ? (int) $userId : 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; $order['numero_cuenta_enviado_at'] = $current['numero_cuenta_enviado_at'] ?? null; $order['ruta_contraentrega_pedido_id'] = isset($current['ruta_contraentrega_pedido_id']) && (int) $current['ruta_contraentrega_pedido_id'] > 0 ? (int) $current['ruta_contraentrega_pedido_id'] : null; $order['ruta_contraentrega_subido_at'] = $current['ruta_contraentrega_subido_at'] ?? null; $order['ruta_contraentrega_subido_por'] = isset($current['ruta_contraentrega_subido_por']) && (int) $current['ruta_contraentrega_subido_por'] > 0 ? (int) $current['ruta_contraentrega_subido_por'] : null; $order['ultima_gestion_at'] = $current['ultima_gestion_at'] ?? ($current['updated_at'] ?? null); foreach ($editableFields as $field) { $order[$field . '_drive'] = $order[$field] ?? ''; $trackedValue = $current[$field] ?? null; $order[$field . '_editado'] = $trackedValue; if ($trackedValue !== null && trim((string) $trackedValue) !== '') { $order[$field] = $trackedValue; } } $digits = preg_replace('/\D+/', '', $order['celular'] ?? ''); $order['whatsapp_url'] = $digits !== '' ? 'https://wa.me/' . $digits : ''; $order['telefono_url'] = $digits !== '' ? 'tel:' . $digits : ''; } unset($order); 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; }