Autosave: 20260718-194402
This commit is contained in:
parent
3e16c3adac
commit
730f9cb10b
@ -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;
|
||||
|
||||
@ -130,9 +130,9 @@ include 'layout_header.php';
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<div class="card-body p-0">
|
||||
<div class="excel-container cc-pedidos-rotulados-container cc-pedidos-completados-container">
|
||||
<table id="pedidos-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@ -174,12 +174,11 @@ include 'layout_header.php';
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="numero_operacion">
|
||||
<?php echo htmlspecialchars($pedido['numero_operacion'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
<?php
|
||||
$canSeeClave = ($user_role !== 'Asesor' || (!empty($pedido['numero_operacion']) && !empty($pedido['banco'])));
|
||||
<?php
|
||||
$isEditableClave = ($user_role !== 'Asesor') ? 'editable' : '';
|
||||
?>
|
||||
<td class="<?php echo $isEditableClave; ?> clave-cell" data-id="<?php echo $pedido['id']; ?>" data-field="clave" id="clave-<?php echo $pedido['id']; ?>">
|
||||
<?php echo $canSeeClave ? htmlspecialchars($pedido['clave'] ?? 'N/A') : '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>'; ?>
|
||||
<?php echo htmlspecialchars($pedido['clave'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
<td class="editable-select" data-id="<?php echo $pedido['id']; ?>" data-field="banco">
|
||||
<?php echo !empty($pedido['banco']) ? htmlspecialchars($pedido['banco']) : 'N/A'; ?>
|
||||
@ -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 = "<?php echo $user_role; ?>";
|
||||
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 = '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>';
|
||||
}
|
||||
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 = '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>';
|
||||
}
|
||||
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);
|
||||
|
||||
@ -179,7 +179,7 @@ include 'layout_header.php';
|
||||
<th>Monto Debe</th>
|
||||
<th>Nº De Orden</th>
|
||||
<th>Codigo De Orden</th>
|
||||
<th>CLAVE</th>
|
||||
<?php if ($user_role !== 'Asesor'): ?><th>CLAVE</th><?php endif; ?>
|
||||
<th>DESCARGO</th>
|
||||
<th>PENDIENTES</th>
|
||||
<th>Estado</th>
|
||||
@ -203,13 +203,11 @@ include 'layout_header.php';
|
||||
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_rastreo"><?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?></td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_tracking"><?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?></td>
|
||||
<?php
|
||||
$canSeeClave = ($user_role !== 'Asesor' || (!empty($pedido['numero_operacion']) && !empty($pedido['banco'])));
|
||||
$isEditableClave = ($user_role !== 'Asesor') ? 'editable' : '';
|
||||
?>
|
||||
<td class="<?php echo $isEditableClave; ?>" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo $canSeeClave ? htmlspecialchars($pedido['clave'] ?? 'N/A') : '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>'; ?>
|
||||
<?php if ($user_role !== 'Asesor'): ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo htmlspecialchars($pedido['clave'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="descargo">
|
||||
<?php echo htmlspecialchars($pedido['descargo'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
@ -255,7 +253,7 @@ $(document).ready(function() {
|
||||
"language": {
|
||||
"url": "//cdn.datatables.net/plug-ins/1.10.25/i18n/Spanish.json"
|
||||
},
|
||||
"order": [[ <?php echo ($user_role !== 'Asesor') ? 16 : 15; ?>, "desc" ]],
|
||||
"order": [[ <?php echo ($user_role !== 'Asesor') ? 16 : 14; ?>, "desc" ]],
|
||||
"paging": false,
|
||||
"lengthChange": false,
|
||||
"info": false
|
||||
|
||||
@ -175,7 +175,7 @@ include 'layout_header.php';
|
||||
<th>Monto Debe</th>
|
||||
<th>Nº De Orden</th>
|
||||
<th>Codigo De Orden</th>
|
||||
<th>CLAVE</th>
|
||||
<?php if ($user_role !== 'Asesor'): ?><th>CLAVE</th><?php endif; ?>
|
||||
<th>DESCARGO</th>
|
||||
<th>PENDIENTES</th>
|
||||
<th>Estado</th>
|
||||
@ -199,13 +199,11 @@ include 'layout_header.php';
|
||||
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_rastreo"><?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?></td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_tracking"><?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?></td>
|
||||
<?php
|
||||
$canSeeClave = ($user_role !== 'Asesor' || (!empty($pedido['numero_operacion']) && !empty($pedido['banco'])));
|
||||
$isEditableClave = ($user_role !== 'Asesor') ? 'editable' : '';
|
||||
?>
|
||||
<td class="<?php echo $isEditableClave; ?>" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo $canSeeClave ? htmlspecialchars($pedido['clave'] ?? 'N/A') : '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>'; ?>
|
||||
<?php if ($user_role !== 'Asesor'): ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo htmlspecialchars($pedido['clave'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="descargo">
|
||||
<?php echo htmlspecialchars($pedido['descargo'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
|
||||
@ -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) {
|
||||
|
||||
289
includes/shalom_api_lookup.js
Normal file
289
includes/shalom_api_lookup.js
Normal file
@ -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();
|
||||
@ -176,8 +176,8 @@ include 'layout_header.php';
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
|
||||
<div class="excel-container cc-pedidos-rotulados-container">
|
||||
<table class="table table-striped">
|
||||
<div class="excel-container cc-pedidos-rotulados-container cc-pedidos-listos-container">
|
||||
<table id="pedidos-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@ -191,7 +191,7 @@ include 'layout_header.php';
|
||||
<th>Monto Total</th>
|
||||
<th>Monto Debe</th>
|
||||
<th>Nro. Operación</th>
|
||||
<th>Clave</th>
|
||||
<?php if ($user_role !== 'Asesor'): ?><th>Clave</th><?php endif; ?>
|
||||
<th style="background-color: #d4edda; font-weight: bold;">Recojo Cliente (Día y Hora)</th>
|
||||
<th>Estado</th>
|
||||
<th>Asesor</th>
|
||||
@ -203,7 +203,7 @@ include 'layout_header.php';
|
||||
<tbody>
|
||||
<?php if (empty($pedidos)): ?>
|
||||
<tr>
|
||||
<td colspan="16" class="text-center">No hay pedidos listos para recoger.</td>
|
||||
<td colspan="<?php echo ($user_role === 'Asesor') ? '17' : '18'; ?>" class="text-center">No hay pedidos listos para recoger.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($pedidos as $pedido): ?>
|
||||
@ -219,13 +219,11 @@ include 'layout_header.php';
|
||||
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['numero_operacion'] ?? 'N/A'); ?></td>
|
||||
<?php
|
||||
$canSeeClave = ($user_role !== 'Asesor' || (!empty($pedido['numero_operacion']) && !empty($pedido['banco'])));
|
||||
$isEditableClave = ($user_role !== 'Asesor') ? 'editable' : '';
|
||||
?>
|
||||
<td class="<?php echo $isEditableClave; ?>" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo $canSeeClave ? htmlspecialchars($pedido['clave'] ?? 'N/A') : '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>'; ?>
|
||||
<?php if ($user_role !== 'Asesor'): ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo htmlspecialchars($pedido['clave'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td class="editable-recojo" data-id="<?php echo $pedido['id']; ?>" style="background-color: #d4edda; font-weight: bold;">
|
||||
<span class="text" title="Doble clic para editar"><?php echo !empty($pedido['fecha_recojo']) ? htmlspecialchars($pedido['fecha_recojo']) : 'N/A'; ?></span>
|
||||
<input type="text" class="form-control edit-input" style="display: none;" value="<?php echo htmlspecialchars($pedido['fecha_recojo'] ?? ''); ?>">
|
||||
|
||||
@ -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';
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="voucher_restante" class="form-label">Voucher de Pago Restante</label>
|
||||
<input type="file" class="form-control" id="voucher_restante" name="voucher_restante">
|
||||
<div class="form-text">Este archivo es obligatorio para pasar el pedido a COMPLETADO ✅. Si ya hay un voucher cargado, no necesitas subirlo otra vez.</div>
|
||||
<?php
|
||||
$voucherRestantePath = $pedido['voucher_restante_path'] ?? '';
|
||||
$voucherRestanteExists = $voucherRestantePath !== '' && file_exists(__DIR__ . '/' . $voucherRestantePath);
|
||||
@ -367,6 +369,7 @@ include 'layout_header.php';
|
||||
<option value="Gestion" <?php echo ($pedido['estado'] == 'Gestion') ? 'selected' : ''; ?>>GESTIONES ⚙️</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<div id="completion-requirements" class="form-text text-danger d-none">Para mover el pedido a COMPLETADO ✅ debes registrar número de operación, banco y voucher del pago restante.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -398,6 +401,7 @@ include 'layout_header.php';
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const pedidoForm = document.querySelector('form[action="save_pedido.php"]');
|
||||
const numeroOperacionInput = document.getElementById('numero_operacion');
|
||||
const bancoInput = document.getElementById('banco');
|
||||
const operacionFeedback = document.getElementById('operacion-feedback');
|
||||
@ -407,6 +411,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const submitBtn = document.querySelector('button[type="submit"]');
|
||||
const agenciaSelect = document.getElementById('agencia');
|
||||
const sedeEnvioInput = document.getElementById('sede_envio');
|
||||
const estadoInput = document.getElementById('estado');
|
||||
const voucherRestanteInput = document.getElementById('voucher_restante');
|
||||
const completionRequirements = document.getElementById('completion-requirements');
|
||||
const hasExistingVoucherRestante = <?php echo $voucherRestanteExists ? 'true' : 'false'; ?>;
|
||||
|
||||
function updateSedeList() {
|
||||
if (agenciaSelect.value === 'SHALOM') {
|
||||
@ -416,8 +424,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
|
||||
function isCompletedStateSelected() {
|
||||
return estadoInput.value === 'COMPLETADO ✅' || estadoInput.value === 'Completado';
|
||||
}
|
||||
|
||||
function updateCompletionRequirements() {
|
||||
const completionSelected = isCompletedStateSelected();
|
||||
const needsVoucherUpload = completionSelected && !hasExistingVoucherRestante;
|
||||
|
||||
numeroOperacionInput.required = completionSelected;
|
||||
bancoInput.required = completionSelected;
|
||||
voucherRestanteInput.required = needsVoucherUpload;
|
||||
|
||||
if (completionRequirements) {
|
||||
completionRequirements.classList.toggle('d-none', !completionSelected);
|
||||
}
|
||||
}
|
||||
|
||||
agenciaSelect.addEventListener('change', updateSedeList);
|
||||
updateSedeList(); // Run on load
|
||||
updateSedeList();
|
||||
|
||||
function validateOperacion() {
|
||||
const value = numeroOperacionInput.value.trim();
|
||||
@ -428,6 +453,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
numeroOperacionInput.classList.remove('is-invalid', 'is-valid');
|
||||
toggleClaveBtn.disabled = true;
|
||||
claveInput.type = 'password';
|
||||
submitBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -456,7 +482,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
numeroOperacionInput.classList.remove('is-invalid');
|
||||
numeroOperacionInput.classList.add('is-valid');
|
||||
|
||||
// Solo habilitar si también hay banco seleccionado
|
||||
if (bancoValue !== '') {
|
||||
toggleClaveBtn.disabled = false;
|
||||
} else {
|
||||
@ -468,17 +493,33 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking duplicate:', error);
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
numeroOperacionInput.addEventListener('input', validateOperacion);
|
||||
bancoInput.addEventListener('change', validateOperacion);
|
||||
|
||||
// Run validation on load if there's a value
|
||||
estadoInput.addEventListener('change', function() {
|
||||
updateCompletionRequirements();
|
||||
validateOperacion();
|
||||
});
|
||||
voucherRestanteInput.addEventListener('change', updateCompletionRequirements);
|
||||
|
||||
updateCompletionRequirements();
|
||||
if (numeroOperacionInput.value.trim() !== '') {
|
||||
validateOperacion();
|
||||
}
|
||||
|
||||
if (pedidoForm) {
|
||||
pedidoForm.addEventListener('submit', function(event) {
|
||||
updateCompletionRequirements();
|
||||
if (!pedidoForm.checkValidity()) {
|
||||
event.preventDefault();
|
||||
pedidoForm.reportValidity();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleClaveBtn.addEventListener('click', function() {
|
||||
if (claveInput.type === 'password') {
|
||||
claveInput.type = 'text';
|
||||
|
||||
12
pedidos.php
12
pedidos.php
@ -224,7 +224,7 @@ include 'layout_header.php';
|
||||
<?php endif; ?>
|
||||
<th>Nº De Orden</th>
|
||||
<th>Codigo De Orden</th>
|
||||
<th>CLAVE</th>
|
||||
<?php if ($user_role !== 'Asesor'): ?><th>CLAVE</th><?php endif; ?>
|
||||
<th>NOTA ADICIONAL</th>
|
||||
<th>Estado</th>
|
||||
<?php if ($user_role !== 'Asesor'): ?><th>Asesor</th><?php endif; ?>
|
||||
@ -250,13 +250,11 @@ include 'layout_header.php';
|
||||
<?php endif; ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_rastreo"><?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?></td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_tracking"><?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?></td>
|
||||
<?php
|
||||
$canSeeClave = ($user_role !== 'Asesor' || (!empty($pedido['numero_operacion']) && !empty($pedido['banco'])));
|
||||
$isEditableClave = ($user_role !== 'Asesor') ? 'editable' : '';
|
||||
?>
|
||||
<td class="<?php echo $isEditableClave; ?>" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo $canSeeClave ? htmlspecialchars($pedido['clave'] ?? 'N/A') : '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>'; ?>
|
||||
<?php if ($user_role !== 'Asesor'): ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo htmlspecialchars($pedido['clave'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td class="editable-dblclick" data-id="<?php echo $pedido['id']; ?>" data-field="nota_adicional">
|
||||
<?php echo htmlspecialchars($pedido['nota_adicional'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
<?php
|
||||
session_start();
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
@ -191,7 +194,7 @@ include 'layout_header.php';
|
||||
<button type="button" id="verify-statuses-btn" class="btn btn-primary">Actualizar Estados Shalom</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-muted small mb-0 mt-3">El sistema guarda el último estado consultado de Shalom y puedes refrescar la columna con este botón cuando lo necesites.</p>
|
||||
<p id="shalom-auto-status" class="text-muted small mb-0 mt-3" aria-live="polite">La columna Estado Shalom se actualiza automáticamente al abrir esta pantalla y también puedes refrescarla manualmente con el botón.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -213,7 +216,7 @@ include 'layout_header.php';
|
||||
<th>Monto Debe</th>
|
||||
<th>Nº De Orden</th>
|
||||
<th>Codigo De Orden</th>
|
||||
<th>CLAVE</th>
|
||||
<?php if ($user_role !== 'Asesor'): ?><th>CLAVE</th><?php endif; ?>
|
||||
<th>Estado</th>
|
||||
<th>Estado Shalom</th>
|
||||
<?php if ($user_role !== 'Asesor'): ?><th>Asesor</th><?php endif; ?>
|
||||
@ -276,7 +279,7 @@ include 'layout_header.php';
|
||||
?>
|
||||
<div class="mt-1">
|
||||
<button type="button" class="btn btn-sm btn-info" title="Consultar Estado" data-bs-toggle="modal" data-bs-target="#trackingModal" data-pedido-id="<?php echo htmlspecialchars((string) $pedido['id']); ?>" data-order-number="<?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?>" data-order-code="<?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?>" data-whatsapp-message="<?php echo htmlspecialchars($whatsappMessageRaw); ?>">🔍</button>
|
||||
<a href="<?php echo htmlspecialchars($trackingUrl); ?>" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-primary" title="Rastreo Shalom">🚚</a>
|
||||
<a id="tracking-link-<?php echo htmlspecialchars((string) $pedido['id']); ?>" href="<?php echo htmlspecialchars($trackingUrl); ?>" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-primary" title="Rastreo Shalom">🚚</a>
|
||||
<button type='button' class='btn btn-sm btn-success copy-summary-icon' title='Copiar Resumen para Cliente' data-whatsapp-message="<?php echo htmlspecialchars($whatsappMessageRaw); ?>">📋</button>
|
||||
<a href="<?php echo $whatsappUrl; ?>" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-secondary whatsapp-icon" id="whatsapp-icon-<?php echo $pedido['id']; ?>" title="Enviar WhatsApp">💬</a>
|
||||
</div>
|
||||
@ -289,13 +292,11 @@ include 'layout_header.php';
|
||||
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?></td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_tracking"><?php echo htmlspecialchars($pedido['codigo_tracking'] ?? 'N/A'); ?></td>
|
||||
<?php
|
||||
$canSeeClave = ($user_role !== 'Asesor' || (!empty($pedido['numero_operacion']) && !empty($pedido['banco'])));
|
||||
$isEditableClave = ($user_role !== 'Asesor') ? 'editable' : '';
|
||||
?>
|
||||
<td class="<?php echo $isEditableClave; ?>" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo $canSeeClave ? htmlspecialchars($pedido['clave'] ?? 'N/A') : '<i class="fas fa-eye-slash text-muted" title="Suba el número de operación y seleccione el banco para ver la clave"></i> <span class="text-muted" style="font-size: 0.8rem;">Oculto</span>'; ?>
|
||||
<?php if ($user_role !== 'Asesor'): ?>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="clave">
|
||||
<?php echo htmlspecialchars($pedido['clave'] ?? 'N/A'); ?>
|
||||
</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
|
||||
@ -365,9 +366,17 @@ $(document).ready(function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const table = document.querySelector('.table');
|
||||
const SHALOM_API_ENABLED = true;
|
||||
const verificationNotice = document.getElementById('shalom-auto-status');
|
||||
let shalomVerificationPromise = null;
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const updateVerificationNotice = (message, tone = 'muted') => {
|
||||
if (!verificationNotice) return;
|
||||
verificationNotice.className = `small mb-0 mt-3 text-${tone}`;
|
||||
verificationNotice.textContent = message;
|
||||
};
|
||||
|
||||
const getStatusMeta = (statusMessage) => {
|
||||
const upper = (statusMessage || '').toUpperCase();
|
||||
let badgeClass = 'bg-secondary';
|
||||
@ -443,8 +452,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return errText.length > 22 ? errText.slice(0, 22) + '…' : (errText || 'Error Shalom');
|
||||
};
|
||||
|
||||
const fetchShalomData = async (orderNumber, orderCode) => {
|
||||
const response = await fetch(`shalom_api.php?orderNumber=${encodeURIComponent(orderNumber)}&orderCode=${encodeURIComponent(orderCode)}`);
|
||||
const fetchShalomData = async (orderNumber, orderCode, options = {}) => {
|
||||
const { includeDetails = false } = options;
|
||||
const query = new URLSearchParams({
|
||||
orderNumber,
|
||||
orderCode,
|
||||
_ts: Date.now().toString(),
|
||||
});
|
||||
|
||||
if (includeDetails) {
|
||||
query.set('includeDetails', '1');
|
||||
}
|
||||
|
||||
const response = await fetch(`shalom_api.php?${query.toString()}`, {
|
||||
cache: 'no-store',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
let rawText = '';
|
||||
try {
|
||||
rawText = await response.text();
|
||||
@ -529,6 +552,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider);
|
||||
}
|
||||
|
||||
const trackingLink = document.getElementById(`tracking-link-${pedidoId}`);
|
||||
const resolvedTrackingUrl = data?.search?.data?.tracking_url || data?.search?.tracking_url || '';
|
||||
if (trackingLink && resolvedTrackingUrl) {
|
||||
trackingLink.href = resolvedTrackingUrl;
|
||||
}
|
||||
|
||||
if (whatsappIcon) {
|
||||
whatsappIcon.classList.remove('btn-secondary', 'btn-success');
|
||||
whatsappIcon.classList.add(whatsappClass);
|
||||
@ -569,7 +598,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchShalomData(orderNumber, orderCode)
|
||||
fetchShalomData(orderNumber, orderCode, { includeDetails: true })
|
||||
.then(async data => {
|
||||
if (data && data.error) {
|
||||
const title = data.details ? `${data.error} - ${data.details}` : data.error;
|
||||
@ -730,10 +759,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
async function verificarEstados() {
|
||||
const rows = document.querySelectorAll('tr[data-order-number][data-order-code]');
|
||||
async function verificarEstados(options = {}) {
|
||||
const { automatic = false } = options;
|
||||
const rows = Array.from(document.querySelectorAll('tr[data-order-number][data-order-code]'));
|
||||
const verifyButton = document.getElementById('verify-statuses-btn');
|
||||
|
||||
if (shalomVerificationPromise) {
|
||||
if (!automatic) {
|
||||
updateVerificationNotice('Ya hay una actualización automática de Shalom en curso. Espera un momento.', 'warning');
|
||||
}
|
||||
return shalomVerificationPromise;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
updateVerificationNotice('No hay pedidos en tránsito para consultar en Shalom.', 'muted');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SHALOM_API_ENABLED) {
|
||||
rows.forEach((row) => {
|
||||
const pedidoId = row.id.split('-')[1];
|
||||
@ -741,15 +783,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!statusCell) return;
|
||||
setStatusCell(statusCell, 'bg-warning text-dark', 'VERIFICAR');
|
||||
});
|
||||
updateVerificationNotice('La consulta automática de Shalom no está disponible ahora mismo.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (verifyButton) {
|
||||
verifyButton.disabled = true;
|
||||
verifyButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Actualizando...';
|
||||
}
|
||||
shalomVerificationPromise = (async () => {
|
||||
if (verifyButton) {
|
||||
verifyButton.disabled = true;
|
||||
verifyButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Actualizando...';
|
||||
}
|
||||
|
||||
updateVerificationNotice(
|
||||
automatic
|
||||
? 'Actualizando estados Shalom automáticamente al abrir la pantalla...'
|
||||
: 'Actualizando estados Shalom manualmente...',
|
||||
'info'
|
||||
);
|
||||
|
||||
let processed = 0;
|
||||
let updated = 0;
|
||||
|
||||
try {
|
||||
for (const row of rows) {
|
||||
const orderNumber = row.dataset.orderNumber;
|
||||
const orderCode = row.dataset.orderCode;
|
||||
@ -758,16 +811,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const whatsappIcon = document.getElementById(`whatsapp-icon-${pedidoId}`);
|
||||
|
||||
if (!statusCell) {
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (orderCode === '26') {
|
||||
setStatusCell(statusCell, 'bg-info', 'OLVA COURIER');
|
||||
processed++;
|
||||
updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados.`, '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');
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -783,6 +841,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setStatusCell(statusCell, 'bg-danger', badgeText, title);
|
||||
} else if (data && data.statuses && data.statuses.message) {
|
||||
await applyStatusToRow(pedidoId, data.statuses.message, data, whatsappIcon);
|
||||
updated++;
|
||||
} else {
|
||||
setStatusCell(statusCell, 'bg-warning text-dark', 'Inválido');
|
||||
}
|
||||
@ -793,9 +852,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setStatusCell(statusCell, 'bg-danger', shortErr, fullMessage);
|
||||
}
|
||||
|
||||
await sleep(700);
|
||||
processed++;
|
||||
updateVerificationNotice(`Actualizando Estado Shalom: ${processed}/${rows.length} pedidos revisados${updated ? ` · ${updated} actualizados` : ''}.`, 'info');
|
||||
|
||||
if (processed < rows.length) {
|
||||
await sleep(700);
|
||||
}
|
||||
}
|
||||
|
||||
const finishedAt = new Date().toLocaleTimeString('es-PE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
updateVerificationNotice(
|
||||
automatic
|
||||
? `Estados Shalom actualizados automáticamente a las ${finishedAt}.`
|
||||
: `Actualización manual de Estados Shalom completada a las ${finishedAt}.`,
|
||||
'success'
|
||||
);
|
||||
})();
|
||||
|
||||
try {
|
||||
await shalomVerificationPromise;
|
||||
} finally {
|
||||
shalomVerificationPromise = null;
|
||||
if (verifyButton) {
|
||||
verifyButton.disabled = false;
|
||||
verifyButton.innerHTML = 'Actualizar Estados Shalom';
|
||||
@ -805,7 +886,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const verifyButton = document.getElementById('verify-statuses-btn');
|
||||
if (verifyButton) {
|
||||
verifyButton.addEventListener('click', verificarEstados);
|
||||
verifyButton.addEventListener('click', function () {
|
||||
verificarEstados({ automatic: false });
|
||||
});
|
||||
}
|
||||
|
||||
const autoRefreshRows = document.querySelectorAll('tr[data-order-number][data-order-code]');
|
||||
if (autoRefreshRows.length) {
|
||||
updateVerificationNotice('La columna Estado Shalom empezará a actualizarse sola en unos segundos...', 'muted');
|
||||
window.setTimeout(function () {
|
||||
verificarEstados({ automatic: true });
|
||||
}, 900);
|
||||
} else {
|
||||
updateVerificationNotice('No hay pedidos en tránsito para consultar en Shalom.', 'muted');
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
|
||||
@ -114,8 +114,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($productos_detalle)) {
|
||||
$notas .= "\n\n" . $notas_adicionales;
|
||||
}
|
||||
|
||||
$completed_states = ['Completado', 'COMPLETADO ✅'];
|
||||
$is_completed_state = in_array($estado, $completed_states, true);
|
||||
$voucher_restante_exists = !empty($voucher_restante_path) && file_exists(__DIR__ . '/' . $voucher_restante_path);
|
||||
|
||||
if ($estado === 'COMPLETADO ✅' && empty($numero_operacion)) {
|
||||
if ($is_completed_state && empty($numero_operacion)) {
|
||||
$error_message = urlencode("El número de operación es obligatorio cuando el estado es 'COMPLETADO ✅'.");
|
||||
$redirect_url = 'pedido_form.php?error=' . $error_message;
|
||||
if ($id) {
|
||||
@ -125,6 +129,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($is_completed_state && empty($banco)) {
|
||||
$error_message = urlencode("El banco es obligatorio cuando el estado es 'COMPLETADO ✅'.");
|
||||
$redirect_url = 'pedido_form.php?error=' . $error_message;
|
||||
if ($id) {
|
||||
$redirect_url .= '&id=' . $id;
|
||||
}
|
||||
header('Location: ' . $redirect_url);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($is_completed_state && !$voucher_restante_exists) {
|
||||
$error_message = urlencode("Debe subir el Voucher de Pago Restante antes de mover el pedido a 'COMPLETADO ✅'.");
|
||||
$redirect_url = 'pedido_form.php?error=' . $error_message;
|
||||
if ($id) {
|
||||
$redirect_url .= '&id=' . $id;
|
||||
}
|
||||
header('Location: ' . $redirect_url);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check for minimum length of operation number
|
||||
if (!empty($numero_operacion) && strlen($numero_operacion) < 6) {
|
||||
$error_message = urlencode("El número de operación debe tener al menos 6 dígitos.");
|
||||
@ -237,8 +261,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
// Conditionally add fecha_completado
|
||||
$completed_states = ['Completado', 'COMPLETADO ✅'];
|
||||
if (in_array($estado, $completed_states)) {
|
||||
if ($is_completed_state) {
|
||||
// Only set fecha_completado if it hasn't been set before.
|
||||
$stmt_check = $pdo->prepare("SELECT fecha_completado FROM pedidos WHERE id = ?");
|
||||
$stmt_check->execute([$id]);
|
||||
@ -287,8 +310,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$columns_sql = "dni_cliente, nombre_completo, celular, agencia, sede_envio, codigo_rastreo, codigo_tracking, clave, pendientes, producto, cantidad, monto_total, monto_adelantado, numero_operacion, banco, monto_debe, estado, asesor_id, notas, nota_adicional, observacion, descargo, comision_vendedora, costo_envio_real, gasto_publicidad_unitario, voucher_adelanto_path, voucher_restante_path";
|
||||
$values_sql = ":dni_cliente, :nombre_completo, :celular, :agencia, :sede_envio, :codigo_rastreo, :codigo_tracking, :clave, :pendientes, :producto, :cantidad, :monto_total, :monto_adelantado, :numero_operacion, :banco, :monto_debe, :estado, :asesor_id, :notas, :nota_adicional, :observacion, :descargo, :comision_vendedora, :costo_envio_real, :gasto_publicidad_unitario, :voucher_adelanto_path, :voucher_restante_path";
|
||||
|
||||
$completed_states = ['Completado', 'COMPLETADO ✅'];
|
||||
if (in_array($estado, $completed_states)) {
|
||||
if ($is_completed_state) {
|
||||
$columns_sql .= ", fecha_completado";
|
||||
$values_sql .= ", :fecha_completado";
|
||||
$params['fecha_completado'] = date('Y-m-d H:i:s');
|
||||
@ -300,7 +322,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$new_id = $pdo->lastInsertId();
|
||||
|
||||
// --- NEW: Insert into operaciones_provincia if completed ---
|
||||
if (in_array($estado, $completed_states)) {
|
||||
if ($is_completed_state) {
|
||||
$stmt_u = $pdo->prepare("SELECT nombre_asesor FROM users WHERE id = ?");
|
||||
$stmt_u->execute([$_SESSION['user_id']]);
|
||||
$nombre_asesor = $stmt_u->fetchColumn();
|
||||
|
||||
426
shalom_api.php
426
shalom_api.php
@ -1,5 +1,8 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
|
||||
// Make sure we ALWAYS return valid JSON from this endpoint, even when
|
||||
// upstream error bodies include invalid UTF-8 bytes.
|
||||
@ -44,6 +47,65 @@ function parse_shalom_tracking_date(?string $rawText): array {
|
||||
return [null, $cleanText];
|
||||
}
|
||||
|
||||
function normalize_shalom_status_label(string $statusMessage): string {
|
||||
$normalized = trim(preg_replace('/\s+/u', ' ', $statusMessage));
|
||||
if ($normalized === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$upper = mb_strtoupper($normalized, 'UTF-8');
|
||||
|
||||
if (str_contains($upper, 'ENTREGA EXITOSA') || str_contains($upper, 'ENTREGAD') || str_contains($upper, 'COMPLET')) {
|
||||
return 'Entregado';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'REPARTO')) {
|
||||
return 'En reparto';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'DESTINO')) {
|
||||
return 'En destino';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'TRANSITO') || str_contains($upper, 'TRÁNSITO')) {
|
||||
return 'En tránsito';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'ORIGEN') || str_contains($upper, 'REGISTRADO')) {
|
||||
return 'En origen';
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
function infer_status_message_from_payload(array $statusPayload): string {
|
||||
$statusData = isset($statusPayload['data']) && is_array($statusPayload['data'])
|
||||
? $statusPayload['data']
|
||||
: [];
|
||||
|
||||
$priority = [
|
||||
['key' => 'entregado', 'label' => 'Entregado'],
|
||||
['key' => 'reparto', 'label' => 'En reparto'],
|
||||
['key' => 'destino', 'label' => 'En destino'],
|
||||
['key' => 'transito', 'label' => 'En tránsito'],
|
||||
['key' => 'origen', 'label' => 'En origen'],
|
||||
['key' => 'registrado', 'label' => 'En origen'],
|
||||
['key' => 'demora', 'label' => 'En demora'],
|
||||
];
|
||||
|
||||
foreach ($priority as $candidate) {
|
||||
$key = $candidate['key'];
|
||||
if (!empty($statusData[$key])) {
|
||||
return $candidate['label'];
|
||||
}
|
||||
}
|
||||
|
||||
$fallbackMessage = trim((string) ($statusPayload['message'] ?? $statusPayload['error'] ?? ''));
|
||||
$normalizedFallback = normalize_shalom_status_label($fallbackMessage);
|
||||
|
||||
return $normalizedFallback !== '' ? $normalizedFallback : 'No disponible';
|
||||
}
|
||||
|
||||
// Load env from executor/.env (helps when Apache/PHP doesn't export .env vars)
|
||||
function load_dotenv_if_needed(array $keys): void {
|
||||
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
||||
@ -87,6 +149,248 @@ function resolve_binary(array $candidates, string $fallback): string {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
function generate_uuid_v4(): string {
|
||||
$bytes = random_bytes(16);
|
||||
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||
$hex = bin2hex($bytes);
|
||||
|
||||
return sprintf(
|
||||
'%s-%s-%s-%s-%s',
|
||||
substr($hex, 0, 8),
|
||||
substr($hex, 8, 4),
|
||||
substr($hex, 12, 4),
|
||||
substr($hex, 16, 4),
|
||||
substr($hex, 20, 12)
|
||||
);
|
||||
}
|
||||
|
||||
function build_shalom_web_authorization(): string {
|
||||
$token = 'web-' . generate_uuid_v4();
|
||||
$expiresAt = time() + 300;
|
||||
$payload = $token . '@' . $expiresAt;
|
||||
$signature = hash_hmac('sha256', $payload, '.Ov3rsku112024l4r43l.');
|
||||
|
||||
return 'Bearer ' . $payload . '@' . $signature;
|
||||
}
|
||||
|
||||
function decrypt_shalom_public_payload(array $decoded): array {
|
||||
if (empty($decoded['encrypted'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $decoded,
|
||||
];
|
||||
}
|
||||
|
||||
$encodedPayload = trim((string) ($decoded['data'] ?? ''));
|
||||
if ($encodedPayload === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Shalom devolvió una respuesta cifrada vacía.',
|
||||
];
|
||||
}
|
||||
|
||||
$key = base64_decode('uQn/bQ94PXBEfId70zjN+VE1hSU7kh9VBXTOUd68Ssc=', true);
|
||||
if ($key === false || strlen($key) !== 32) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'No se pudo preparar la clave de descifrado de Shalom.',
|
||||
];
|
||||
}
|
||||
|
||||
$binaryPayload = base64_decode($encodedPayload, true);
|
||||
if ($binaryPayload === false || strlen($binaryPayload) <= 16) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'La respuesta cifrada de Shalom es inválida.',
|
||||
];
|
||||
}
|
||||
|
||||
$iv = substr($binaryPayload, 0, 16);
|
||||
$ciphertext = substr($binaryPayload, 16);
|
||||
$plainText = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
if (!is_string($plainText) || $plainText === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'No se pudo descifrar la respuesta del estado público de Shalom.',
|
||||
];
|
||||
}
|
||||
|
||||
$decodedPlain = json_decode($plainText, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decodedPlain)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Shalom devolvió un estado público imposible de interpretar.',
|
||||
'details' => $plainText,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $decodedPlain,
|
||||
];
|
||||
}
|
||||
|
||||
function call_public_shalom_status_api(string $orderNumber): array {
|
||||
$authorization = build_shalom_web_authorization();
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => 'https://serviceswebapi.shalomcontrol.com/api/v1/web/rastrea/estados',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => [
|
||||
'ose_id' => $orderNumber,
|
||||
],
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
'Authorization: ' . $authorization,
|
||||
'Origin: https://shalom.com.pe',
|
||||
'Referer: https://shalom.com.pe/',
|
||||
'X-Requested-With: XMLHttpRequest',
|
||||
],
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Error consultando el estado público de Shalom.',
|
||||
'details' => $curlError,
|
||||
];
|
||||
}
|
||||
|
||||
if ($response === false || $response === null || $response === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Shalom no devolvió respuesta para el estado público.',
|
||||
'details' => 'HTTP ' . ($httpCode ?: 'desconocido'),
|
||||
];
|
||||
}
|
||||
|
||||
$decodedTransport = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decodedTransport)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'La respuesta del estado público de Shalom no es JSON válido.',
|
||||
'details' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
$decrypted = decrypt_shalom_public_payload($decodedTransport);
|
||||
if (empty($decrypted['success'])) {
|
||||
return $decrypted;
|
||||
}
|
||||
|
||||
$payload = $decrypted['data'];
|
||||
$success = !empty($payload['success']);
|
||||
$message = is_array($payload) ? trim((string) ($payload['message'] ?? $payload['error'] ?? '')) : '';
|
||||
|
||||
if ($httpCode >= 400 || !$success) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $message !== '' ? 'Estado público Shalom: ' . $message : 'Shalom no pudo devolver el estado real del envío.',
|
||||
'details' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
function infer_status_detail_text(string $statusMessage): string {
|
||||
$upper = mb_strtoupper(trim($statusMessage), 'UTF-8');
|
||||
|
||||
if ($upper === '') {
|
||||
return 'Estado actualizado en Shalom.';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'ENTREGADO') || str_contains($upper, 'COMPLETADO')) {
|
||||
return 'El pedido ha sido entregado satisfactoriamente.';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'REPARTO')) {
|
||||
return 'El pedido está en reparto final.';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'DESTINO')) {
|
||||
return 'El pedido llegó a la ciudad de destino y está listo para el siguiente paso.';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'TRANSITO') || str_contains($upper, 'TRÁNSITO')) {
|
||||
return 'Rumbo a su destino.';
|
||||
}
|
||||
|
||||
if (str_contains($upper, 'ORIGEN') || str_contains($upper, 'REGISTRADO')) {
|
||||
return 'El pedido fue registrado en la agencia de origen.';
|
||||
}
|
||||
|
||||
return 'Estado actualizado en Shalom.';
|
||||
}
|
||||
|
||||
function build_status_api_response(string $orderNumber, string $orderCode, array $statusPayload, ?array $scraped = null): array {
|
||||
$statusMessage = infer_status_message_from_payload($statusPayload);
|
||||
|
||||
$statusData = isset($statusPayload['data']) && is_array($statusPayload['data'])
|
||||
? $statusPayload['data']
|
||||
: [];
|
||||
|
||||
if ($scraped !== null) {
|
||||
[$dateIso, $dateText] = parse_shalom_tracking_date($scraped['date_text'] ?? '');
|
||||
if (!isset($statusData['registrado']) || !is_array($statusData['registrado'])) {
|
||||
$statusData['registrado'] = [
|
||||
'fecha' => $dateIso,
|
||||
'fecha_text' => $dateText,
|
||||
];
|
||||
} else {
|
||||
if ($dateIso !== null && empty($statusData['registrado']['fecha'])) {
|
||||
$statusData['registrado']['fecha'] = $dateIso;
|
||||
}
|
||||
if ($dateText !== null && empty($statusData['registrado']['fecha_text'])) {
|
||||
$statusData['registrado']['fecha_text'] = $dateText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$detailText = $scraped['description'] ?? '';
|
||||
if (trim((string) $detailText) === '') {
|
||||
$detailText = infer_status_detail_text($statusMessage);
|
||||
}
|
||||
|
||||
return [
|
||||
'source' => $scraped !== null ? 'shalom_public_status+web' : 'shalom_public_status',
|
||||
'search' => [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'origen' => ['nombre' => 'N/A'],
|
||||
'destino' => ['nombre' => 'N/A', 'direccion' => ''],
|
||||
'tracking_url' => $scraped['tracking_url'] ?? build_public_tracking_url($orderNumber, $orderCode),
|
||||
'internal_order_number' => $scraped['internal_order_number'] ?? null,
|
||||
'external_order_number' => $orderNumber,
|
||||
'order_code' => $orderCode,
|
||||
'detail_text' => $detailText,
|
||||
'requires_login' => !empty($scraped['requires_login']),
|
||||
],
|
||||
],
|
||||
'statuses' => [
|
||||
'message' => $statusMessage,
|
||||
'raw_message' => isset($statusPayload['message']) ? trim((string) $statusPayload['message']) : null,
|
||||
'data' => $statusData,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function run_shalom_web_scraper(string $orderNumber, string $orderCode): array {
|
||||
if (!function_exists('exec')) {
|
||||
return [
|
||||
@ -148,6 +452,67 @@ function run_shalom_web_scraper(string $orderNumber, string $orderCode): array {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
function run_shalom_search_api_lookup(string $orderNumber, string $orderCode): array {
|
||||
if (!function_exists('exec')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'La función exec está deshabilitada en el servidor.',
|
||||
];
|
||||
}
|
||||
|
||||
$scriptPath = __DIR__ . '/includes/shalom_api_lookup.js';
|
||||
if (!is_readable($scriptPath)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'No se encontró el helper local de búsqueda de Shalom.',
|
||||
];
|
||||
}
|
||||
|
||||
$nodeBinary = resolve_binary(['/usr/bin/node', '/usr/local/bin/node'], 'node');
|
||||
$timeoutBinary = resolve_binary(['/usr/bin/timeout', '/bin/timeout'], 'timeout');
|
||||
|
||||
$commandParts = [
|
||||
escapeshellarg($timeoutBinary),
|
||||
'80s',
|
||||
escapeshellarg($nodeBinary),
|
||||
escapeshellarg($scriptPath),
|
||||
escapeshellarg($orderNumber),
|
||||
escapeshellarg($orderCode),
|
||||
];
|
||||
|
||||
$command = implode(' ', $commandParts) . ' 2>&1';
|
||||
$output = [];
|
||||
$exitCode = 0;
|
||||
exec($command, $output, $exitCode);
|
||||
|
||||
$rawOutput = trim(implode("\n", $output));
|
||||
if ($rawOutput === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'La búsqueda oficial de Shalom no devolvió respuesta.',
|
||||
'details' => 'Proceso sin salida útil.',
|
||||
];
|
||||
}
|
||||
|
||||
$decoded = json_decode($rawOutput, true);
|
||||
if (!is_array($decoded)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Respuesta inválida del helper oficial de Shalom.',
|
||||
'details' => $rawOutput,
|
||||
];
|
||||
}
|
||||
|
||||
if ($exitCode !== 0 && empty($decoded['success'])) {
|
||||
if (empty($decoded['details'])) {
|
||||
$decoded['details'] = $rawOutput;
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
function build_scraper_response(string $orderNumber, string $orderCode, array $scraped): array {
|
||||
[$dateIso, $dateText] = parse_shalom_tracking_date($scraped['date_text'] ?? '');
|
||||
|
||||
@ -167,7 +532,7 @@ function build_scraper_response(string $orderNumber, string $orderCode, array $s
|
||||
],
|
||||
],
|
||||
'statuses' => [
|
||||
'message' => $scraped['status'] ?? 'No disponible',
|
||||
'message' => normalize_shalom_status_label((string) ($scraped['status'] ?? 'No disponible')),
|
||||
'data' => [
|
||||
'registrado' => [
|
||||
'fecha' => $dateIso,
|
||||
@ -269,6 +634,8 @@ function call_official_shalom_api(string $orderNumber, string $orderCode, ?strin
|
||||
// Inputs
|
||||
$orderNumber = trim($_GET['orderNumber'] ?? '');
|
||||
$orderCode = trim($_GET['orderCode'] ?? '');
|
||||
$includeDetailsRaw = trim((string) ($_GET['includeDetails'] ?? ''));
|
||||
$includeDetails = in_array(strtolower($includeDetailsRaw), ['1', 'true', 'yes', 'si', 'sí'], true);
|
||||
|
||||
if ($orderNumber === '' || $orderCode === '') {
|
||||
respond_json(400, ['error' => 'Número de orden y código de orden son requeridos.'], $jsonOptions);
|
||||
@ -293,8 +660,58 @@ if (!$apiKey) {
|
||||
|
||||
$attemptErrors = [];
|
||||
|
||||
// Prioridad: rastreo web público primero.
|
||||
// En esta instancia es la vía más estable para mostrar el estado rápido en la columna.
|
||||
// Fallback 0: resolver el pedido con la búsqueda oficial de Shalom y luego pedir su estado real.
|
||||
$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;
|
||||
$resolvedOrderNumber = is_scalar($resolvedOrderNumber) ? (string) $resolvedOrderNumber : null;
|
||||
$trackingUrl = $searchApiAttempt['resolved']['tracking_url'] ?? build_public_tracking_url(
|
||||
$resolvedOrderNumber !== null ? $resolvedOrderNumber : $orderNumber,
|
||||
$orderCode
|
||||
);
|
||||
|
||||
$apiResponse = build_status_api_response($orderNumber, $orderCode, $searchApiAttempt['status'], [
|
||||
'internal_order_number' => $resolvedOrderNumber,
|
||||
'tracking_url' => $trackingUrl,
|
||||
'description' => '',
|
||||
'requires_login' => false,
|
||||
]);
|
||||
$apiResponse['source'] = 'shalom_search_api';
|
||||
|
||||
respond_json(200, $apiResponse, $jsonOptions);
|
||||
}
|
||||
if (!empty($searchApiAttempt['error'])) {
|
||||
$attemptErrors[] = 'Búsqueda API Shalom: ' . $searchApiAttempt['error'];
|
||||
}
|
||||
if (!empty($searchApiAttempt['details'])) {
|
||||
$details = is_string($searchApiAttempt['details']) ? $searchApiAttempt['details'] : json_encode($searchApiAttempt['details'], $jsonOptions);
|
||||
if ($details) {
|
||||
$attemptErrors[] = 'Detalle búsqueda API: ' . $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).
|
||||
$scraperAttempt = run_shalom_web_scraper($orderNumber, $orderCode);
|
||||
if (!empty($scraperAttempt['success'])) {
|
||||
respond_json(200, build_scraper_response($orderNumber, $orderCode, $scraperAttempt), $jsonOptions);
|
||||
@ -309,6 +726,7 @@ if (!empty($scraperAttempt['details'])) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -325,6 +743,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 de la API ni del rastreo web.',
|
||||
'details' => !empty($attemptErrors) ? implode(' | ', $attemptErrors) : 'No hubo respuesta útil del estado público, del rastreo web ni de la API.',
|
||||
'tracking_url' => build_public_tracking_url($orderNumber, $orderCode),
|
||||
], $jsonOptions);
|
||||
|
||||
@ -8,6 +8,13 @@ if (!isset($_SESSION['user_id'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_role = $_SESSION['user_role'] ?? 'Asesor';
|
||||
if ($user_role === 'Asesor') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'No autorizado para actualizar la clave.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$data || !isset($data['id']) || !isset($data['value'])) {
|
||||
|
||||
@ -22,12 +22,51 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$pdo = db();
|
||||
|
||||
$valid_states = ['RUTA_CONTRAENTREGA', 'PENDIENTE', 'NO CONTESTO, VOLVER A LLAMAR', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA', 'RETORNADO', 'NO CONTESTO, DEVOLVER LLAMADA', 'COMPLETADO ✅', 'Gestion'];
|
||||
if (!in_array($estado, $valid_states)) {
|
||||
if (!in_array($estado, $valid_states, true)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Estado inválido: ' . $estado]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE pedidos SET estado = ? WHERE id = ?");
|
||||
|
||||
if ($estado === 'COMPLETADO ✅') {
|
||||
$stmt_pedido = $pdo->prepare('SELECT numero_operacion, banco, voucher_restante_path FROM pedidos WHERE id = ?');
|
||||
$stmt_pedido->execute([$pedido_id]);
|
||||
$pedido = $stmt_pedido->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$pedido) {
|
||||
echo json_encode(['success' => false, 'message' => 'Pedido no encontrado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$numero_operacion = trim((string)($pedido['numero_operacion'] ?? ''));
|
||||
$banco = trim((string)($pedido['banco'] ?? ''));
|
||||
$voucher_restante_path = trim((string)($pedido['voucher_restante_path'] ?? ''));
|
||||
$voucher_restante_exists = $voucher_restante_path !== '' && file_exists(__DIR__ . '/' . $voucher_restante_path);
|
||||
|
||||
if ($numero_operacion === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Antes de completar el pedido debes registrar el número de operación.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (strlen($numero_operacion) < 6) {
|
||||
echo json_encode(['success' => false, 'message' => 'El número de operación debe tener al menos 6 dígitos antes de completar el pedido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($banco === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Antes de completar el pedido debes seleccionar el banco.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$voucher_restante_exists) {
|
||||
echo json_encode(['success' => false, 'message' => 'Antes de completar el pedido debes subir el voucher del pago restante.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE pedidos SET estado = ?, fecha_completado = COALESCE(fecha_completado, NOW()) WHERE id = ?');
|
||||
} else {
|
||||
$stmt = $pdo->prepare('UPDATE pedidos SET estado = ? WHERE id = ?');
|
||||
}
|
||||
|
||||
$result = $stmt->execute([$estado, $pedido_id]);
|
||||
|
||||
if ($result) {
|
||||
|
||||
@ -8,6 +8,8 @@ if (!isset($_SESSION['user_id'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_role = $_SESSION['user_role'] ?? 'Asesor';
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$data || !isset($data['id']) || !isset($data['field']) || !isset($data['value'])) {
|
||||
@ -20,6 +22,12 @@ $pedido_id = $data['id'];
|
||||
$field = $data['field'];
|
||||
$value = $data['value'];
|
||||
|
||||
if ($field === 'clave' && $user_role === 'Asesor') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'No autorizado para actualizar la clave.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Whitelist allowed fields for security
|
||||
$allowed_fields = ['numero_operacion', 'banco', 'clave', 'fecha_recojo', 'observacion', 'descargo', 'notas', 'nota_adicional'];
|
||||
if (!in_array($field, $allowed_fields)) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user