Autosave: 20260721-173027
This commit is contained in:
parent
730f9cb10b
commit
33ea21ca77
@ -280,9 +280,7 @@ h1, .h1 {
|
||||
}
|
||||
.cc-pedidos-listos-container,
|
||||
.cc-pedidos-completados-container {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: scroll !important;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
overflow: auto !important;
|
||||
}
|
||||
.excel-container th {
|
||||
position: sticky;
|
||||
|
||||
@ -5,15 +5,15 @@ require_once 'includes/callcenter_test_helpers.php';
|
||||
require_once 'includes/drive_test_orders.php';
|
||||
require_once 'includes/contraentrega_cobertura.php';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
$provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento();
|
||||
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
|
||||
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
|
||||
$sedesShalom = cc_test_fetch_shalom_sedes(db());
|
||||
|
||||
$pageTitle = 'Pedidos Asignados | Bandeja de la asesora';
|
||||
$pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con la misma interfaz, estados, historial y acciones.';
|
||||
|
||||
if (!cc_test_current_user_can_access_module(db())) {
|
||||
if (!cc_test_current_user_can_access_module($pdo)) {
|
||||
http_response_code(403);
|
||||
require_once 'layout_header.php';
|
||||
echo "<div class='container py-5'><div class='alert alert-danger mb-0'>Acceso denegado.</div></div>";
|
||||
@ -21,6 +21,43 @@ if (!cc_test_current_user_can_access_module(db())) {
|
||||
exit();
|
||||
}
|
||||
|
||||
if (isset($_GET['ajax']) && $_GET['ajax'] === 'panel_status') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
|
||||
if ($currentUserId <= 0) {
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Sesión no válida.',
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
$stmtPanel = $pdo->prepare("\n SELECT source_key, user_id, estado, assigned_at, updated_at, eliminado_at\n FROM callcenter_test_tracking\n WHERE user_id = ? AND eliminado_at IS NULL\n ORDER BY source_key\n ");
|
||||
$stmtPanel->execute([$currentUserId]);
|
||||
$panelOrders = $stmtPanel->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'signature' => cc_test_panel_orders_signature($panelOrders),
|
||||
'total' => count($panelOrders),
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Panel status check failed: ' . $exception->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'No se pudo verificar el panel.',
|
||||
]);
|
||||
}
|
||||
|
||||
exit();
|
||||
}
|
||||
|
||||
$sedesShalom = cc_test_fetch_shalom_sedes($pdo);
|
||||
|
||||
function cc_test_parse_datetime(?string $value): ?DateTimeImmutable
|
||||
{
|
||||
@ -175,6 +212,28 @@ function cc_test_followup_semaforo(array $order): ?array
|
||||
return cc_test_account_followup_semaforo($order['numero_cuenta_enviado_at'] ?? null);
|
||||
}
|
||||
|
||||
function cc_test_panel_orders_signature(array $orders): string
|
||||
{
|
||||
$payload = [];
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$payload[] = [
|
||||
'source_key' => (string) ($order['source_key'] ?? ''),
|
||||
'user_id' => (int) ($order['user_id'] ?? 0),
|
||||
'estado' => cc_test_normalize_state((string) ($order['estado'] ?? '')),
|
||||
'assigned_at' => trim((string) ($order['assigned_at'] ?? '')),
|
||||
'updated_at' => trim((string) ($order['updated_at'] ?? ($order['seguimiento_actualizado'] ?? ''))),
|
||||
'eliminado_at' => trim((string) ($order['eliminado_at'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
usort($payload, static function (array $a, array $b): int {
|
||||
return strcmp($a['source_key'], $b['source_key']);
|
||||
});
|
||||
|
||||
return sha1(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
$view = $_GET['view'] ?? 'pendientes_hoy';
|
||||
$allowedViews = [
|
||||
'pendientes_hoy' => 'Bandeja principal',
|
||||
@ -225,8 +284,9 @@ $stats = [
|
||||
'cerrados' => 0,
|
||||
];
|
||||
|
||||
$panelSignature = '';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
cc_test_ensure_tracking_table($pdo);
|
||||
cc_test_ensure_historial_llamadas_table($pdo);
|
||||
$assessors = cc_test_fetch_assessors($pdo);
|
||||
@ -402,6 +462,8 @@ try {
|
||||
}
|
||||
unset($order);
|
||||
|
||||
$panelSignature = cc_test_panel_orders_signature($orders);
|
||||
|
||||
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view, $selectedAssessorFilter): bool {
|
||||
if ($selectedAssessorFilter !== '' && !($order['matches_assessor_filter'] ?? false)) {
|
||||
return false;
|
||||
@ -667,7 +729,7 @@ require_once 'layout_header.php';
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="container-fluid py-4">
|
||||
<main class="container-fluid py-4" data-cc-panel-root="1" data-panel-signature="<?php echo htmlspecialchars($panelSignature); ?>" data-panel-poll-url="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['ajax' => 'panel_status']))); ?>" data-panel-poll-interval="6000">
|
||||
<section class="mb-4">
|
||||
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3">
|
||||
<div>
|
||||
@ -691,7 +753,7 @@ require_once 'layout_header.php';
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">Agregados: <?php echo (int) $agregadosCount; ?></span>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($usesIncrementalSync && $lastProcessedRow !== null): ?><?php echo htmlspecialchars(mb_strtoupper($storeLabel)); ?>: distribución desde fila <?php echo (int) ($storeConfig['startRow'] ?? $startRow); ?> · último procesado <?php echo (int) $lastProcessedRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: activa</span>
|
||||
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -2184,6 +2246,61 @@ if (airDroidCopyButton) {
|
||||
});
|
||||
}
|
||||
|
||||
const ccPanelLiveEnabled = <?php echo $isAdmin ? 'false' : 'true'; ?>;
|
||||
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);
|
||||
|
||||
@ -298,11 +298,15 @@ include 'layout_header.php';
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td><span class="badge" style="<?php echo getStatusStyle($pedido['estado']); ?>"><?php echo ($pedido['estado'] == 'Gestion') ? 'GESTIONES ⚙️' : htmlspecialchars($pedido['estado']); ?></span></td>
|
||||
<td id="live-status-<?php echo $pedido['id']; ?>">
|
||||
<?php
|
||||
$estadoShalomCache = trim((string) ($pedido['estado_shalom'] ?? ''));
|
||||
$estadoShalomActualizadoAt = trim((string) ($pedido['estado_shalom_actualizado_at'] ?? ''));
|
||||
?>
|
||||
<td
|
||||
id="live-status-<?php echo $pedido['id']; ?>"
|
||||
data-shalom-status="<?php echo htmlspecialchars($estadoShalomCache); ?>"
|
||||
data-shalom-updated-at="<?php echo htmlspecialchars($estadoShalomActualizadoAt); ?>"
|
||||
>
|
||||
<?php if ($estadoShalomCache !== ''): ?>
|
||||
<div class="d-flex flex-column align-items-start gap-1">
|
||||
<span class="badge <?php echo htmlspecialchars(getShalomStatusBadgeClass($estadoShalomCache)); ?>"><?php echo htmlspecialchars($estadoShalomCache); ?></span>
|
||||
@ -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'
|
||||
);
|
||||
})();
|
||||
|
||||
192
shalom_api.php
192
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user