@@ -691,7 +753,7 @@ require_once 'layout_header.php';
Drive detectado: filas
Agregados:
: distribución desde fila · último procesado Extracción: desde fila
-
Auto actualización: cada 10 min
+
Auto actualización: activa
Ver vista previa Drive
@@ -2184,6 +2246,61 @@ if (airDroidCopyButton) {
});
}
+const ccPanelLiveEnabled = ;
+if (ccPanelLiveEnabled) {
+ const ccPanelRoot = document.querySelector('[data-cc-panel-root="1"]');
+ if (ccPanelRoot) {
+ const ccPanelPollUrl = ccPanelRoot.dataset.panelPollUrl || '';
+ const ccPanelInterval = parseInt(ccPanelRoot.dataset.panelPollInterval || '6000', 10) || 6000;
+ let ccPanelSignature = ccPanelRoot.dataset.panelSignature || '';
+ let ccPanelPolling = false;
+
+ const ccPanelCheck = async () => {
+ if (ccPanelPolling || !ccPanelPollUrl) {
+ return;
+ }
+
+ ccPanelPolling = true;
+ try {
+ const url = new URL(ccPanelPollUrl, window.location.href);
+ url.searchParams.set('_ts', Date.now().toString());
+
+ const response = await fetch(url.toString(), {
+ cache: 'no-store',
+ credentials: 'same-origin'
+ });
+
+ if (!response.ok) {
+ return;
+ }
+
+ const data = await response.json();
+ if (!data || !data.success) {
+ return;
+ }
+
+ const nextSignature = String(data.signature || '');
+ if (nextSignature && nextSignature !== ccPanelSignature) {
+ ccPanelSignature = nextSignature;
+ window.location.reload();
+ }
+ } catch (error) {
+ console.warn('Panel update check failed:', error);
+ } finally {
+ ccPanelPolling = false;
+ }
+ };
+
+ window.setTimeout(ccPanelCheck, 2500);
+ window.setInterval(ccPanelCheck, ccPanelInterval);
+ document.addEventListener('visibilitychange', function () {
+ if (!document.hidden) {
+ ccPanelCheck();
+ }
+ });
+ }
+}
+
window.setInterval(function () {
window.location.reload();
}, 600000);
diff --git a/pedidos_en_transito.php b/pedidos_en_transito.php
index 56f47899..3de7bf27 100644
--- a/pedidos_en_transito.php
+++ b/pedidos_en_transito.php
@@ -298,11 +298,15 @@ include 'layout_header.php';
|
-
+ |
@@ -368,6 +372,8 @@ document.addEventListener('DOMContentLoaded', function() {
const SHALOM_API_ENABLED = true;
const verificationNotice = document.getElementById('shalom-auto-status');
let shalomVerificationPromise = null;
+ const SHALOM_AUTO_SKIP_RECENT_MINUTES = 3;
+ const SHALOM_REQUEST_GAP_MS = 250;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -382,7 +388,9 @@ document.addEventListener('DOMContentLoaded', function() {
let badgeClass = 'bg-secondary';
let whatsappClass = 'btn-secondary';
- if (upper.includes('ENTREGADO') || upper.includes('COMPLETADO') || upper.includes('EN DESTINO') || upper.includes('DESTINO')) {
+ if (upper.includes('REVISAR') || upper.includes('VERIFICAR')) {
+ badgeClass = 'bg-warning text-dark';
+ } else if (upper.includes('ENTREGADO') || upper.includes('COMPLETADO') || upper.includes('EN DESTINO') || upper.includes('DESTINO')) {
badgeClass = 'bg-success';
whatsappClass = 'btn-success';
} else if (upper.includes('EN TRANSITO') || upper.includes('EN TRÁNSITO') || upper.includes('TRANSITO') || upper.includes('TRÁNSITO')) {
@@ -394,7 +402,33 @@ document.addEventListener('DOMContentLoaded', function() {
return { badgeClass, whatsappClass };
};
+ const getStatusTextFromCell = (cell) => {
+ const badge = cell ? cell.querySelector('.badge') : null;
+ return badge ? (badge.textContent || '').trim() : '';
+ };
+
+ const isLockedShalomStatus = (statusMessage) => {
+ const upper = (statusMessage || '').toUpperCase();
+ return upper.includes('ENTREGADO') || upper.includes('COMPLETADO') || upper.includes('EN DESTINO') || upper.includes('DESTINO');
+ };
+
+ const parseShalomUpdatedAt = (rawValue) => {
+ if (!rawValue) return null;
+ const normalized = String(rawValue).trim().replace(' ', 'T');
+ const parsed = new Date(normalized);
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
+ };
+
+ const wasShalomRecentlyChecked = (cell) => {
+ const parsed = parseShalomUpdatedAt(cell?.dataset?.shalomUpdatedAt || '');
+ if (!parsed) return false;
+
+ const ageMs = Date.now() - parsed.getTime();
+ return ageMs >= 0 && ageMs < (SHALOM_AUTO_SKIP_RECENT_MINUTES * 60 * 1000);
+ };
+
const setStatusCell = (cell, badgeClass, text, title = '', updatedAtText = '') => {
+ cell.dataset.shalomStatus = text || '';
cell.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'd-flex flex-column align-items-start gap-1';
@@ -508,7 +542,7 @@ document.addEventListener('DOMContentLoaded', function() {
};
const persistShalomStatus = async (pedidoId, statusMessage) => {
- if (!pedidoId || !statusMessage) return '';
+ if (!pedidoId || !statusMessage) return null;
try {
const response = await fetch('update_shalom_status.php', {
@@ -524,13 +558,16 @@ document.addEventListener('DOMContentLoaded', function() {
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload.success) {
- return '';
+ return null;
}
- return formatDateTimeLabel(payload.checked_at);
+ return {
+ checkedAtRaw: payload.checked_at || '',
+ checkedAtLabel: formatDateTimeLabel(payload.checked_at)
+ };
} catch (error) {
console.warn('No se pudo guardar el estado Shalom en caché.', error);
- return '';
+ return null;
}
};
@@ -543,11 +580,12 @@ document.addEventListener('DOMContentLoaded', function() {
formatDateTimeLabel(data?.statuses?.data?.registrado?.fecha) ||
formatDateTimeLabel(data?.statuses?.data?.registrado?.fecha_text);
- setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider || 'Actualizando caché...');
+ setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider || 'Actualizando...');
- const cachedUpdatedAt = await persistShalomStatus(pedidoId, statusMessage);
- if (cachedUpdatedAt) {
- setStatusCell(statusCell, badgeClass, statusMessage, '', cachedUpdatedAt);
+ const cachedUpdate = await persistShalomStatus(pedidoId, statusMessage);
+ if (cachedUpdate && cachedUpdate.checkedAtRaw) {
+ statusCell.dataset.shalomUpdatedAt = cachedUpdate.checkedAtRaw;
+ setStatusCell(statusCell, badgeClass, statusMessage, '', cachedUpdate.checkedAtLabel || updatedFromProvider);
} else if (updatedFromProvider) {
setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider);
}
@@ -802,6 +840,22 @@ document.addEventListener('DOMContentLoaded', function() {
let processed = 0;
let updated = 0;
+ let skippedLocked = 0;
+ let skippedRecent = 0;
+
+ const buildVerificationProgressLabel = () => {
+ let label = `Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados`;
+ if (updated) {
+ label += ` · ${updated} actualizados`;
+ }
+ if (skippedLocked) {
+ label += ` · ${skippedLocked} ya en destino omitidos`;
+ }
+ if (skippedRecent) {
+ label += ` · ${skippedRecent} recientes reutilizados`;
+ }
+ return `${label}.`;
+ };
for (const row of rows) {
const orderNumber = row.dataset.orderNumber;
@@ -818,14 +872,29 @@ document.addEventListener('DOMContentLoaded', function() {
if (orderCode === '26') {
setStatusCell(statusCell, 'bg-info', 'OLVA COURIER');
processed++;
- updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados.`, 'info');
+ updateVerificationNotice(buildVerificationProgressLabel(), 'info');
continue;
}
if (!orderNumber || !orderCode || orderNumber === 'N/A' || orderCode === 'N/A') {
setStatusCell(statusCell, 'bg-light text-dark', 'Sin datos');
processed++;
- updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados.`, 'info');
+ updateVerificationNotice(buildVerificationProgressLabel(), 'info');
+ continue;
+ }
+
+ const currentStatusText = getStatusTextFromCell(statusCell);
+ if (automatic && isLockedShalomStatus(currentStatusText)) {
+ skippedLocked++;
+ processed++;
+ updateVerificationNotice(buildVerificationProgressLabel(), 'info');
+ continue;
+ }
+
+ if (automatic && currentStatusText && currentStatusText !== 'VERIFICAR' && wasShalomRecentlyChecked(statusCell)) {
+ skippedRecent++;
+ processed++;
+ updateVerificationNotice(buildVerificationProgressLabel(), 'info');
continue;
}
@@ -853,10 +922,10 @@ document.addEventListener('DOMContentLoaded', function() {
}
processed++;
- updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados${updated ? ` · ${updated} actualizados` : ''}.`, 'info');
+ updateVerificationNotice(buildVerificationProgressLabel(), 'info');
if (processed < rows.length) {
- await sleep(700);
+ await sleep(SHALOM_REQUEST_GAP_MS);
}
}
@@ -864,11 +933,13 @@ document.addEventListener('DOMContentLoaded', function() {
hour: '2-digit',
minute: '2-digit'
});
+ const skippedLockedSummary = skippedLocked ? ` Se omitieron ${skippedLocked} pedido(s) que ya estaban en destino.` : '';
+ const skippedRecentSummary = skippedRecent ? ` Se reutilizaron ${skippedRecent} pedido(s) consultados hace menos de ${SHALOM_AUTO_SKIP_RECENT_MINUTES} minuto(s).` : '';
updateVerificationNotice(
automatic
- ? `Estados Shalom actualizados automáticamente a las ${finishedAt}.`
- : `Actualización manual de Estados Shalom completada a las ${finishedAt}.`,
+ ? `Estados Shalom actualizados automáticamente a las ${finishedAt}.${skippedLockedSummary}${skippedRecentSummary}`
+ : `Actualización manual de Estados Shalom completada a las ${finishedAt}.${skippedLockedSummary}${skippedRecentSummary}`,
'success'
);
})();
diff --git a/shalom_api.php b/shalom_api.php
index df48726a..e37e51d6 100644
--- a/shalom_api.php
+++ b/shalom_api.php
@@ -47,12 +47,120 @@ function parse_shalom_tracking_date(?string $rawText): array {
return [null, $cleanText];
}
+function normalize_shalom_lookup_text(string $text): string {
+ $normalized = trim((string) preg_replace('/\s+/u', ' ', $text));
+ if ($normalized === '') {
+ return '';
+ }
+
+ $upper = mb_strtoupper($normalized, 'UTF-8');
+
+ return strtr($upper, [
+ 'Á' => 'A',
+ 'É' => 'E',
+ 'Í' => 'I',
+ 'Ó' => 'O',
+ 'Ú' => 'U',
+ 'Ü' => 'U',
+ ]);
+}
+
+function is_shalom_missing_order_message(?string $text): bool {
+ $normalized = normalize_shalom_lookup_text((string) $text);
+ if ($normalized === '') {
+ return false;
+ }
+
+ if (
+ str_contains($normalized, 'NO SE ENCONTRO LA ORDEN')
+ || str_contains($normalized, 'NO SE ENCONTRO EL PEDIDO')
+ || str_contains($normalized, 'NO SE ENCONTRO LA GUIA')
+ || str_contains($normalized, 'NO SE PUDO ENCONTRAR LA GUIA')
+ || str_contains($normalized, 'ORDEN NO ENCONTRADA')
+ || str_contains($normalized, 'GUIA NO ENCONTRADA')
+ || str_contains($normalized, 'NO EXISTE LA ORDEN')
+ || str_contains($normalized, 'NO EXISTE LA GUIA')
+ || (str_contains($normalized, 'NO DEVOLVIO UN N') && str_contains($normalized, 'INTERNO VALIDO'))
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+function detect_shalom_missing_order_reason(array $payload): ?string {
+ $candidates = [];
+
+ foreach (['error', 'message'] as $key) {
+ if (isset($payload[$key]) && is_scalar($payload[$key])) {
+ $candidate = trim((string) $payload[$key]);
+ if ($candidate !== '') {
+ $candidates[] = $candidate;
+ }
+ }
+ }
+
+ if (isset($payload['details'])) {
+ if (is_scalar($payload['details'])) {
+ $candidate = trim((string) $payload['details']);
+ if ($candidate !== '') {
+ $candidates[] = $candidate;
+ }
+ } elseif (is_array($payload['details'])) {
+ foreach (['error', 'message', 'details'] as $key) {
+ if (isset($payload['details'][$key]) && is_scalar($payload['details'][$key])) {
+ $candidate = trim((string) $payload['details'][$key]);
+ if ($candidate !== '') {
+ $candidates[] = $candidate;
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($candidates as $candidate) {
+ if (is_shalom_missing_order_message($candidate)) {
+ return $candidate;
+ }
+ }
+
+ return null;
+}
+
+function build_invalid_guide_response(string $orderNumber, string $orderCode, string $rawMessage = ''): array {
+ return [
+ 'source' => 'shalom_invalid_guide',
+ 'search' => [
+ 'success' => false,
+ 'data' => [
+ 'origen' => ['nombre' => 'N/A'],
+ 'destino' => ['nombre' => 'N/A', 'direccion' => ''],
+ 'tracking_url' => build_public_tracking_url($orderNumber, $orderCode),
+ 'internal_order_number' => null,
+ 'external_order_number' => $orderNumber,
+ 'order_code' => $orderCode,
+ 'detail_text' => 'Shalom no encontró la orden. Revisa el N° de orden y el código de orden para corregir la guía.',
+ 'requires_login' => false,
+ ],
+ ],
+ 'statuses' => [
+ 'message' => 'REVISAR GUIA',
+ 'raw_message' => trim($rawMessage),
+ 'data' => [],
+ ],
+ ];
+}
+
function normalize_shalom_status_label(string $statusMessage): string {
$normalized = trim(preg_replace('/\s+/u', ' ', $statusMessage));
if ($normalized === '') {
return '';
}
+ if (is_shalom_missing_order_message($normalized)) {
+ return 'REVISAR GUIA';
+ }
+
$upper = mb_strtoupper($normalized, 'UTF-8');
if (str_contains($upper, 'ENTREGA EXITOSA') || str_contains($upper, 'ENTREGAD') || str_contains($upper, 'COMPLET')) {
@@ -316,6 +424,10 @@ function infer_status_detail_text(string $statusMessage): string {
return 'Estado actualizado en Shalom.';
}
+ if (str_contains($upper, 'REVISAR GUIA') || str_contains($upper, 'VERIFICAR')) {
+ return 'Shalom no encontró la orden. Revisa el N° de orden y el código de orden.';
+ }
+
if (str_contains($upper, 'ENTREGADO') || str_contains($upper, 'COMPLETADO')) {
return 'El pedido ha sido entregado satisfactoriamente.';
}
@@ -641,26 +753,12 @@ if ($orderNumber === '' || $orderCode === '') {
respond_json(400, ['error' => 'Número de orden y código de orden son requeridos.'], $jsonOptions);
}
-load_dotenv_if_needed(['SHALOM_API_KEY']);
-
-// API key fallback from DB (so the app can work even if you can't edit .env)
-$apiKey = getenv('SHALOM_API_KEY');
-if (!$apiKey) {
- try {
- require_once __DIR__ . '/db/config.php';
- $pdo = db();
- $stmt = $pdo->prepare('SELECT valor FROM configuracion WHERE clave = ? LIMIT 1');
- $stmt->execute(['SHALOM_API_KEY']);
- $apiKey = $stmt->fetchColumn();
- $apiKey = is_string($apiKey) ? trim($apiKey) : null;
- } catch (Throwable $e) {
- $apiKey = null;
- }
-}
-
$attemptErrors = [];
-// Fallback 0: resolver el pedido con la búsqueda oficial de Shalom y luego pedir su estado real.
+// Primero resolvemos el N° interno real con la búsqueda oficial de Shalom.
+// El endpoint público de estados usa ese identificador interno (ose_id);
+// si se le pasa el número externo del pedido puede devolver otra guía y
+// marcar "Entregado" por error.
$searchApiAttempt = run_shalom_search_api_lookup($orderNumber, $orderCode);
if (!empty($searchApiAttempt['success']) && !empty($searchApiAttempt['search']) && !empty($searchApiAttempt['status']) && is_array($searchApiAttempt['search']) && is_array($searchApiAttempt['status'])) {
$resolvedOrderNumber = $searchApiAttempt['search']['data']['ose_id'] ?? null;
@@ -680,6 +778,11 @@ if (!empty($searchApiAttempt['success']) && !empty($searchApiAttempt['search'])
respond_json(200, $apiResponse, $jsonOptions);
}
+$missingGuideReason = detect_shalom_missing_order_reason($searchApiAttempt);
+if ($missingGuideReason !== null) {
+ respond_json(200, build_invalid_guide_response($orderNumber, $orderCode, $missingGuideReason), $jsonOptions);
+}
+
if (!empty($searchApiAttempt['error'])) {
$attemptErrors[] = 'Búsqueda API Shalom: ' . $searchApiAttempt['error'];
}
@@ -690,32 +793,16 @@ if (!empty($searchApiAttempt['details'])) {
}
}
-$statusAttempt = call_public_shalom_status_api($orderNumber);
-if (!empty($statusAttempt['success']) && !empty($statusAttempt['data']) && is_array($statusAttempt['data'])) {
- if ($includeDetails) {
- $scraperAttempt = run_shalom_web_scraper($orderNumber, $orderCode);
- if (!empty($scraperAttempt['success'])) {
- respond_json(200, build_status_api_response($orderNumber, $orderCode, $statusAttempt['data'], $scraperAttempt), $jsonOptions);
- }
- }
-
- respond_json(200, build_status_api_response($orderNumber, $orderCode, $statusAttempt['data']), $jsonOptions);
-}
-if (!empty($statusAttempt['error'])) {
- $attemptErrors[] = 'Estado público: ' . $statusAttempt['error'];
-}
-if (!empty($statusAttempt['details'])) {
- $details = is_string($statusAttempt['details']) ? $statusAttempt['details'] : json_encode($statusAttempt['details'], $jsonOptions);
- if ($details) {
- $attemptErrors[] = 'Detalle estado público: ' . $details;
- }
-}
-
-// Fallback 1: rastreo web visual (más pesado, pero útil si el estado público falla).
+// Fallback 1: rastreo web visual (más pesado, pero usa el número + código visibles al usuario).
$scraperAttempt = run_shalom_web_scraper($orderNumber, $orderCode);
if (!empty($scraperAttempt['success'])) {
respond_json(200, build_scraper_response($orderNumber, $orderCode, $scraperAttempt), $jsonOptions);
}
+$missingGuideReason = detect_shalom_missing_order_reason($scraperAttempt);
+if ($missingGuideReason !== null) {
+ respond_json(200, build_invalid_guide_response($orderNumber, $orderCode, $missingGuideReason), $jsonOptions);
+}
+
if (!empty($scraperAttempt['error'])) {
$attemptErrors[] = 'Rastreo web: ' . $scraperAttempt['error'];
}
@@ -726,11 +813,34 @@ if (!empty($scraperAttempt['details'])) {
}
}
+load_dotenv_if_needed(['SHALOM_API_KEY']);
+
+// API key fallback from DB (so the app can work even if you can't edit .env)
+$apiKey = getenv('SHALOM_API_KEY');
+if (!$apiKey) {
+ try {
+ require_once __DIR__ . '/db/config.php';
+ $pdo = db();
+ $stmt = $pdo->prepare('SELECT valor FROM configuracion WHERE clave = ? LIMIT 1');
+ $stmt->execute(['SHALOM_API_KEY']);
+ $apiKey = $stmt->fetchColumn();
+ $apiKey = is_string($apiKey) ? trim($apiKey) : null;
+ } catch (Throwable $e) {
+ $apiKey = null;
+ }
+}
+
// Fallback 2: API oficial antigua.
$officialAttempt = call_official_shalom_api($orderNumber, $orderCode, $apiKey ?: null);
if (!empty($officialAttempt['success']) && !empty($officialAttempt['data']) && is_array($officialAttempt['data'])) {
respond_json(200, $officialAttempt['data'], $jsonOptions);
}
+
+$missingGuideReason = detect_shalom_missing_order_reason($officialAttempt);
+if ($missingGuideReason !== null) {
+ respond_json(200, build_invalid_guide_response($orderNumber, $orderCode, $missingGuideReason), $jsonOptions);
+}
+
if (!empty($officialAttempt['error'])) {
$attemptErrors[] = 'API oficial: ' . $officialAttempt['error'];
}
@@ -743,6 +853,6 @@ if (!empty($officialAttempt['details'])) {
respond_json(502, [
'error' => 'No se pudo consultar el estado en Shalom.',
- 'details' => !empty($attemptErrors) ? implode(' | ', $attemptErrors) : 'No hubo respuesta útil del estado público, del rastreo web ni de la API.',
+ 'details' => !empty($attemptErrors) ? implode(' | ', $attemptErrors) : 'No hubo respuesta útil de la búsqueda oficial, del rastreo web ni de la API.',
'tracking_url' => build_public_tracking_url($orderNumber, $orderCode),
], $jsonOptions);
|