From 730f9cb10ba99f1776654e7082238290368b7ce7 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 18 Jul 2026 19:42:46 +0000 Subject: [PATCH] Autosave: 20260718-194402 --- assets/css/style.css | 10 + completados.php | 38 +-- duplicados.php | 14 +- gestiones.php | 12 +- includes/drive_test_orders.php | 56 +++-- includes/shalom_api_lookup.js | 289 ++++++++++++++++++++++ listos_para_recojo.php | 18 +- pedido_form.php | 49 +++- pedidos.php | 12 +- pedidos_en_transito.php | 135 +++++++++-- save_pedido.php | 34 ++- shalom_api.php | 426 ++++++++++++++++++++++++++++++++- update_clave.php | 7 + update_estado.php | 45 +++- update_pedido_field.php | 8 + 15 files changed, 1041 insertions(+), 112 deletions(-) create mode 100644 includes/shalom_api_lookup.js diff --git a/assets/css/style.css b/assets/css/style.css index 7c2cd377..fed82d28 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -274,6 +274,16 @@ h1, .h1 { border-spacing: 0; table-layout: fixed; /* Ayuda a controlar mejor los anchos de columna */ } +.cc-pedidos-listos-container .table, +.cc-pedidos-completados-container .table { + min-width: 1400px; +} +.cc-pedidos-listos-container, +.cc-pedidos-completados-container { + overflow-x: auto !important; + overflow-y: scroll !important; + scrollbar-gutter: stable both-edges; +} .excel-container th { position: sticky; top: 0; diff --git a/completados.php b/completados.php index 997f9025..fe6c72cd 100644 --- a/completados.php +++ b/completados.php @@ -130,9 +130,9 @@ include 'layout_header.php';
-
-
- +
+
+
@@ -174,12 +174,11 @@ include 'layout_header.php'; - - + @@ -203,13 +203,11 @@ include 'layout_header.php'; - - + @@ -255,7 +253,7 @@ $(document).ready(function() { "language": { "url": "//cdn.datatables.net/plug-ins/1.10.25/i18n/Spanish.json" }, - "order": [[ , "desc" ]], + "order": [[ , "desc" ]], "paging": false, "lengthChange": false, "info": false diff --git a/gestiones.php b/gestiones.php index 2ba75167..0d9c315d 100644 --- a/gestiones.php +++ b/gestiones.php @@ -175,7 +175,7 @@ include 'layout_header.php'; - + @@ -199,13 +199,11 @@ include 'layout_header.php'; - - + diff --git a/includes/drive_test_orders.php b/includes/drive_test_orders.php index 348d9a8b..f7d7fbc3 100644 --- a/includes/drive_test_orders.php +++ b/includes/drive_test_orders.php @@ -97,6 +97,17 @@ function drive_test_get_cell(array $row, array $indexes, array $aliases, string return $default; } +function drive_test_compact_text(?string $value): string +{ + $value = trim((string) $value); + if ($value === '') { + return ''; + } + + $normalized = preg_replace('/\s+/u', ' ', $value); + return trim((string) ($normalized ?? $value)); +} + function drive_test_extract_codigo_number(?string $codigo): ?int { $digits = preg_replace('/\D+/', '', trim((string) $codigo)); @@ -182,14 +193,14 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $ $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']); + $ciudad = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['CIUDAD'])); + $metodo = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['METODO'])); + $sede = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['SEDE / ID', 'SEDE/ID'])); + $dni = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['N° DNI', 'N° DNI ', 'NRO DNI', 'DNI'])); + $observaciones = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['OBSERVACIONES', 'OBSERVACIONES '])); + $direccion = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['DIRECION', 'DIRECCION'])); + $referencia = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['REFERENCIA'])); + $distrito = drive_test_compact_text(drive_test_get_cell($row, $headerIndexes, ['DISTRITO'])); $sourceRow = (int) $rowIndex + 2; $codigoNumber = drive_test_extract_codigo_number($codigo); @@ -364,9 +375,9 @@ function drive_test_ensure_orders_table(PDO $pdo): void `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, + `sede_drive` TEXT NULL, + `ciudad_drive` TEXT NULL, + `distrito_drive` TEXT NULL, `dni_drive` LONGTEXT NULL, `observaciones_drive` TEXT NULL, `celular` VARCHAR(40) DEFAULT NULL, @@ -390,20 +401,31 @@ function drive_test_ensure_orders_table(PDO $pdo): void // 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. + // Asegurar que los campos de ubicación y DNI soporten valores largos desde Google Sheets. try { $colStmt = $pdo->prepare(" - SELECT DATA_TYPE + SELECT COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'callcenter_test_orders' - AND COLUMN_NAME = 'dni_drive' - LIMIT 1 + AND COLUMN_NAME IN ('sede_drive', 'ciudad_drive', 'distrito_drive', 'dni_drive') "); $colStmt->execute(); - $dataType = strtolower((string) $colStmt->fetchColumn()); - if ($dataType !== 'longtext') { + $dataTypes = []; + foreach ($colStmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $columnName = strtolower((string) ($row['COLUMN_NAME'] ?? '')); + $dataTypes[$columnName] = strtolower((string) ($row['DATA_TYPE'] ?? '')); + } + + foreach (['sede_drive', 'ciudad_drive', 'distrito_drive'] as $columnName) { + $dataType = $dataTypes[$columnName] ?? ''; + if (!in_array($dataType, ['text', 'mediumtext', 'longtext'], true)) { + $pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `{$columnName}` TEXT NULL"); + } + } + + if (($dataTypes['dni_drive'] ?? '') !== 'longtext') { $pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `dni_drive` LONGTEXT NULL"); } } catch (Throwable $exception) { diff --git a/includes/shalom_api_lookup.js b/includes/shalom_api_lookup.js new file mode 100644 index 00000000..88c48b74 --- /dev/null +++ b/includes/shalom_api_lookup.js @@ -0,0 +1,289 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const puppeteer = require('puppeteer'); + +const SHALOM_SECRET = '.Ov3rsku112024l4r43l.'; +const SHALOM_ENCRYPTION_KEY_B64 = 'uQn/bQ94PXBEfId70zjN+VE1hSU7kh9VBXTOUd68Ssc='; +const SHALOM_TRACKING_HOME = 'https://shalom.com.pe/rastrea'; +const SHALOM_SEARCH_URL = 'https://serviceswebapi.shalomcontrol.com/api/v1/web/rastrea/buscar'; +const SHALOM_STATUS_URL = 'https://serviceswebapi.shalomcontrol.com/api/v1/web/rastrea/estados'; + +function detectChromeExecutable() { + const candidates = []; + const cacheRoot = '/home/ubuntu/.cache/puppeteer/chrome'; + + try { + if (fs.existsSync(cacheRoot)) { + const versions = fs.readdirSync(cacheRoot).sort().reverse(); + for (const version of versions) { + candidates.push(path.join(cacheRoot, version, 'chrome-linux64', 'chrome')); + } + } + } catch (error) { + // Ignoramos y seguimos con rutas fijas. + } + + if (process.env.PUPPETEER_EXECUTABLE_PATH) { + candidates.unshift(process.env.PUPPETEER_EXECUTABLE_PATH); + } + + candidates.push( + '/usr/bin/google-chrome-stable', + '/usr/bin/google-chrome', + '/usr/bin/chromium', + '/usr/bin/chromium-browser' + ); + + return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || null; +} + +function uuidv4() { + const bytes = crypto.randomBytes(16); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = bytes.toString('hex'); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +function buildAuthorizationHeader() { + const token = `web-${uuidv4()}`; + const expiresAt = Math.floor(Date.now() / 1000) + 300; + const payload = `${token}@${expiresAt}`; + const signature = crypto.createHmac('sha256', SHALOM_SECRET).update(payload).digest('hex'); + return `Bearer ${payload}@${signature}`; +} + +function decryptShalomPayload(payload) { + if (!payload || payload.encrypted !== true || !payload.data) { + return payload; + } + + const key = Buffer.from(SHALOM_ENCRYPTION_KEY_B64, 'base64'); + if (key.length !== 32) { + throw new Error('No se pudo preparar la clave de descifrado.'); + } + + const cipherBuffer = Buffer.from(String(payload.data), 'base64'); + if (cipherBuffer.length <= 16) { + throw new Error('La respuesta cifrada de Shalom es inválida.'); + } + + const iv = cipherBuffer.subarray(0, 16); + const ciphertext = cipherBuffer.subarray(16); + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + const plainText = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8'); + + try { + return JSON.parse(plainText); + } catch (error) { + return plainText; + } +} + +async function obtainRecaptchaToken(page) { + try { + await page.goto(SHALOM_TRACKING_HOME, { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + } catch (error) { + // La página puede reintentar/cargar de nuevo por el limpiador de service workers. + } + + await page.waitForFunction(() => window.RECAPTCHA_SITE_KEY && window.grecaptcha, { + timeout: 60000, + }); + + return page.evaluate(async () => { + await new Promise((resolve) => window.grecaptcha.ready(resolve)); + return window.grecaptcha.execute(window.RECAPTCHA_SITE_KEY, { action: 'rastrea_buscar' }); + }); +} + +async function postShalomApi(url, fields) { + const form = new FormData(); + for (const [key, value] of Object.entries(fields)) { + if (value === undefined || value === null) continue; + form.append(key, String(value)); + } + + const response = await fetch(url, { + method: 'POST', + body: form, + headers: { + Accept: 'application/json', + Authorization: buildAuthorizationHeader(), + Origin: 'https://shalom.com.pe', + Referer: 'https://shalom.com.pe/', + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + const rawText = await response.text(); + let wrappedPayload = null; + try { + wrappedPayload = JSON.parse(rawText); + } catch (error) { + return { + success: false, + error: `Respuesta no JSON de Shalom (${response.status}).`, + details: rawText.trim() ? rawText.slice(0, 1200) : 'Sin contenido útil.', + }; + } + + let plainPayload = wrappedPayload; + try { + plainPayload = decryptShalomPayload(wrappedPayload); + } catch (error) { + return { + success: false, + error: `No se pudo descifrar la respuesta de Shalom (${response.status}).`, + details: error.message, + }; + } + + if (response.status >= 400) { + const message = plainPayload && typeof plainPayload === 'object' + ? String(plainPayload.message || plainPayload.error || '').trim() + : ''; + + return { + success: false, + error: message || `Shalom respondió HTTP ${response.status}.`, + details: plainPayload, + }; + } + + if (!plainPayload || typeof plainPayload !== 'object') { + return { + success: false, + error: 'Shalom devolvió una respuesta vacía o inválida.', + details: plainPayload, + }; + } + + if (plainPayload.success === false) { + return { + success: false, + error: String(plainPayload.message || plainPayload.error || 'Shalom rechazó la solicitud.'), + details: plainPayload, + }; + } + + return { + success: true, + data: plainPayload, + }; +} + +function printJson(payload) { + console.log(JSON.stringify(payload)); +} + +async function main() { + const orderNumber = String(process.argv[2] || '').trim(); + const orderCode = String(process.argv[3] || '').trim(); + + if (!orderNumber || !orderCode) { + printJson({ + success: false, + error: 'Número de orden y código de orden son requeridos.', + }); + process.exit(1); + } + + let browser = null; + + try { + const executablePath = detectChromeExecutable(); + if (!executablePath) { + printJson({ + success: false, + error: 'No se encontró un navegador Chromium compatible para consultar Shalom.', + }); + process.exit(2); + } + + browser = await puppeteer.launch({ + headless: 'new', + executablePath, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-features=SSLCommonNameMismatchHandling', + ], + }); + + const page = await browser.newPage(); + await page.setViewport({ width: 1280, height: 900 }); + await page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'); + + const recaptchaToken = await obtainRecaptchaToken(page); + + const searchResult = await postShalomApi(SHALOM_SEARCH_URL, { + numero: orderNumber, + codigo: orderCode, + ose_id: '', + recaptcha_token: recaptchaToken, + }); + + if (!searchResult.success) { + printJson(searchResult); + process.exit(3); + } + + const searchPayload = searchResult.data; + const oseId = searchPayload && searchPayload.data ? searchPayload.data.ose_id : null; + if (!oseId) { + printJson({ + success: false, + error: 'La búsqueda de Shalom no devolvió un N° interno válido.', + details: searchPayload, + }); + process.exit(4); + } + + const statusResult = await postShalomApi(SHALOM_STATUS_URL, { + ose_id: String(oseId), + }); + + if (!statusResult.success) { + printJson(statusResult); + process.exit(5); + } + + const statusPayload = statusResult.data; + const trackingUrl = `${SHALOM_TRACKING_HOME}/${encodeURIComponent(String(oseId))}/${encodeURIComponent(orderCode)}`; + + printJson({ + success: true, + source: 'shalom_search_api', + search: searchPayload, + status: statusPayload, + resolved: { + ose_id: oseId, + tracking_url: trackingUrl, + }, + }); + } catch (error) { + printJson({ + success: false, + error: error && error.message ? error.message : 'Error al consultar Shalom.', + details: error && error.stack ? error.stack.split('\n').slice(0, 4).join(' | ') : undefined, + }); + process.exit(6); + } finally { + if (browser) { + try { + await browser.close(); + } catch (error) { + // Ignoramos errores al cerrar el navegador. + } + } + } +} + +main(); diff --git a/listos_para_recojo.php b/listos_para_recojo.php index 1f293339..cb7aff61 100644 --- a/listos_para_recojo.php +++ b/listos_para_recojo.php @@ -176,8 +176,8 @@ include 'layout_header.php';
-
-
ID - Oculto'; ?> + @@ -272,16 +271,10 @@ function updatePedidoField(pedidoId, field, value) { if (data.success) { console.log(`${field} actualizado con éxito.`); - // Si el usuario es Asesor, verificar si ahora puede ver la clave - const userRole = ""; - if (userRole === 'Asesor') { - const pedido = data.pedido; - const claveCell = document.getElementById(`clave-${pedidoId}`); - if (pedido.numero_operacion && pedido.banco) { - claveCell.innerHTML = pedido.clave || 'N/A'; - } else { - claveCell.innerHTML = ' Oculto'; - } + const pedido = data.pedido || {}; + const claveCell = document.getElementById(`clave-${pedidoId}`); + if (claveCell) { + claveCell.innerHTML = pedido.clave || 'N/A'; } } else if (data.error) { alert('Error: ' + data.error); @@ -461,15 +454,10 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { if (data.success) { - // Si es numero_operacion y es Asesor, verificar visibilidad de clave - if (field === 'numero_operacion' && userRole === 'Asesor') { - const pedido = data.pedido; - const claveCell = document.getElementById(`clave-${pedidoId}`); - if (pedido.numero_operacion && pedido.banco) { - claveCell.innerHTML = pedido.clave || 'N/A'; - } else { - claveCell.innerHTML = ' Oculto'; - } + const pedido = data.pedido || {}; + const claveCell = document.getElementById(`clave-${pedidoId}`); + if (claveCell) { + claveCell.innerHTML = pedido.clave || 'N/A'; } } else if (data.error) { console.error('Error al guardar:', data.error); diff --git a/duplicados.php b/duplicados.php index 17ffbbc9..53850c14 100644 --- a/duplicados.php +++ b/duplicados.php @@ -179,7 +179,7 @@ include 'layout_header.php'; Monto Debe Nº De Orden Codigo De OrdenCLAVECLAVE DESCARGO PENDIENTES Estado - Oculto'; ?> + + + Monto Debe Nº De Orden Codigo De OrdenCLAVECLAVE DESCARGO PENDIENTES Estado - Oculto'; ?> + + +
+
+
@@ -191,7 +191,7 @@ include 'layout_header.php'; - + @@ -203,7 +203,7 @@ include 'layout_header.php'; - + @@ -219,13 +219,11 @@ include 'layout_header.php'; - - +
IDMonto Total Monto Debe Nro. OperaciónClaveClave Recojo Cliente (Día y Hora) Estado Asesor
No hay pedidos listos para recoger.No hay pedidos listos para recoger.
- Oculto'; ?> + + + diff --git a/pedido_form.php b/pedido_form.php index 519309c1..4a5930a7 100644 --- a/pedido_form.php +++ b/pedido_form.php @@ -37,6 +37,7 @@ $pedido = [ 'costo_envio_real' => 0, 'gasto_publicidad_unitario' => 0, 'voucher_adelanto_path' => '', + 'voucher_restante_path' => '', ]; $page_title = 'Crear Pedido'; @@ -340,6 +341,7 @@ include 'layout_header.php';
+
Este archivo es obligatorio para pasar el pedido a COMPLETADO ✅. Si ya hay un voucher cargado, no necesitas subirlo otra vez.
>GESTIONES ⚙️ +
Para mover el pedido a COMPLETADO ✅ debes registrar número de operación, banco y voucher del pago restante.
@@ -398,6 +401,7 @@ include 'layout_header.php';