34849-vm/pedidos_en_transito.php
2026-07-21 17:29:06 +00:00

997 lines
52 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
require_once 'db/config.php';
function getStatusStyle($status) {
$style = 'color: white;'; // Default text color
$bgColor = '#0dcaf0'; // Default info blue
switch (strtoupper(trim($status))) {
case 'ROTULADO':
$bgColor = '#ffc107'; // yellow
$style = 'color: black;';
break;
case 'EN TRANSITO':
$bgColor = '#90EE90'; // light green
$style = 'color: black;';
break;
case 'EN DESTINO':
$bgColor = '#800080'; // purple
break;
case 'COMPLETADO':
case 'COMPLETADO ✅':
$bgColor = '#198754'; // dark green
break;
case 'GESTION':
$bgColor = '#6c757d'; // secondary grey
break;
}
return "background-color: {$bgColor} !important; {$style}";
}
function getShalomStatusBadgeClass($status) {
$normalized = mb_strtoupper(trim((string) $status), 'UTF-8');
if ($normalized === '') {
return 'bg-warning text-dark';
}
if (str_contains($normalized, 'ENTREGADO') || str_contains($normalized, 'COMPLETADO') || str_contains($normalized, 'DESTINO')) {
return 'bg-success';
}
if (str_contains($normalized, 'TRANSITO') || str_contains($normalized, 'TRÁNSITO')) {
return 'bg-primary';
}
if (str_contains($normalized, 'REGISTRADO') || str_contains($normalized, 'ORIGEN')) {
return 'bg-warning text-dark';
}
return 'bg-secondary';
}
$pdo = db();
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
// Fetch years for the filter
$years_query = "SELECT DISTINCT YEAR(created_at) as year FROM pedidos";
if ($user_role === 'Asesor') {
$years_query .= " WHERE asesor_id = ?";
$years_stmt = $pdo->prepare($years_query);
$years_stmt->execute([$user_id]);
} else {
$years_stmt = $pdo->query($years_query);
}
$years = $years_stmt->fetchAll(PDO::FETCH_COLUMN);
// Filter logic
$selected_month = $_GET['mes'] ?? '';
$selected_year = $_GET['año'] ?? '';
$search_query = $_GET['q'] ?? '';
$sql = "SELECT p.*, u.nombre_asesor as asesor_nombre FROM pedidos p LEFT JOIN users u ON p.asesor_id = u.id WHERE p.estado = 'EN TRANSITO 🚛'";
$params = [];
if ($user_role === 'Asesor') {
$sql .= " AND p.asesor_id = ?";
$params[] = $user_id;
}
if (!empty($search_query)) {
$sql .= " AND (p.nombre_completo LIKE ? OR p.dni_cliente LIKE ? OR p.celular LIKE ?)";
$params[] = "%$search_query%";
$params[] = "%$search_query%";
$params[] = "%$search_query%";
}
if (!empty($selected_month)) {
$sql .= " AND MONTH(p.created_at) = ?";
$params[] = $selected_month;
}
if (!empty($selected_year)) {
$sql .= " AND YEAR(p.created_at) = ?";
$params[] = $selected_year;
}
$sql .= " ORDER BY p.created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$pedidos = $stmt->fetchAll();
$months = [
1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril', 5 => 'Mayo', 6 => 'Junio',
7 => 'Julio', 8 => 'Agosto', 9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre'
];
// Cargar plantilla de WhatsApp desde la configuración
$stmt_config = $pdo->query("SELECT valor FROM configuracion WHERE clave = 'whatsapp_template_notificacion'");
$whatsapp_template_db = $stmt_config->fetchColumn();
?>
<?php
$pageTitle = "Pedidos en Tránsito";
$pageDescription = "Consulta y actualiza el estado Shalom de los pedidos en tránsito.";
include 'layout_header.php';
?>
<!-- Modal for tracking status -->
<div class="modal fade" id="trackingModal" tabindex="-1" aria-labelledby="trackingModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" style="max-width: 1100px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="trackingModalLabel">Estado del Envío - Shalom</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="padding: 0;">
<div class="p-3">
<p class="mb-1"><strong>Nº De Orden:</strong> <span id="modal-order-number"></span></p>
<p class="mb-0"><strong>Código De Orden:</strong> <span id="modal-order-code"></span></p>
</div>
<hr class="my-0">
<div id="modal-tracking-status">
<p class="text-center p-5">Consultando estado en Shalom...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<?php if (isset($_SESSION['flash_message'])):
$flash = $_SESSION['flash_message'];
unset($_SESSION['flash_message']);
?>
<div class="alert alert-<?php echo htmlspecialchars($flash['type']); ?> alert-dismissible fade show" role="alert">
<?php echo htmlspecialchars($flash['message']); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card mb-4">
<div class="card-body">
<form method="GET" action="pedidos_en_transito.php" class="row g-3 align-items-center">
<div class="col-auto">
<label for="q" class="form-label">Buscar</label>
<input type="text" name="q" id="q" class="form-control" value="<?php echo htmlspecialchars($search_query); ?>" placeholder="Nombre, DNI o Celular">
</div>
<div class="col-auto">
<label for="mes" class="form-label">Mes</label>
<select name="mes" id="mes" class="form-select">
<option value="">Todos</option>
<?php foreach ($months as $num => $name): ?>
<option value="<?php echo $num; ?>" <?php echo $selected_month == $num ? 'selected' : ''; ?>><?php echo $name; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<label for="año" class="form-label">Año</label>
<select name="año" id="año" class="form-select">
<option value="">Todos</option>
<?php foreach ($years as $year): ?>
<option value="<?php echo $year; ?>" <?php echo $selected_year == $year ? 'selected' : ''; ?>><?php echo $year; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto mt-4">
<button type="submit" class="btn btn-info">Filtrar</button>
<a href="pedidos_en_transito.php" class="btn btn-secondary">Limpiar</a>
<button type="button" id="verify-statuses-btn" class="btn btn-primary">Actualizar Estados Shalom</button>
</div>
</form>
<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>
<div class="card">
<div class="card-body p-0">
<div class="excel-container cc-pedidos-rotulados-container">
<table id="pedidos-table" class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Cliente</th>
<th>DNI</th>
<th>Celular</th>
<th>Agencia</th>
<th>Producto</th>
<th>Sede de Envío</th>
<th>Cantidad</th>
<th>Monto Total</th>
<th>Monto Debe</th>
<th>Nº De Orden</th>
<th>Codigo De Orden</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; ?>
<th>Fecha Creación</th>
<th>Voucher Restante</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($pedidos as $pedido): ?>
<tr id="pedido-<?php echo $pedido['id']; ?>" data-order-number="<?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? ''); ?>" data-order-code="<?php echo htmlspecialchars($pedido['codigo_tracking'] ?? ''); ?>">
<td><?php echo htmlspecialchars($pedido['id']); ?></td>
<td><?php echo htmlspecialchars($pedido['nombre_completo']); ?></td>
<td><?php echo htmlspecialchars($pedido['dni_cliente'] ?? 'N/A'); ?></td>
<td>
<?php
echo htmlspecialchars($pedido['celular']);
// Prepare WhatsApp message
$nombre_cliente = $pedido['nombre_completo'];
$producto = $pedido['producto'];
$sede_envio = $pedido['sede_envio'] ?? 'su agencia de destino';
$monto_total = (float)($pedido['monto_total'] ?? 0);
$monto_debe = (float)($pedido['monto_debe'] ?? 0);
$adelanto = $monto_total - $monto_debe;
$template = $whatsapp_template_db ?: "Estimado(a) {NOMBRE_CLIENTE} 👋\n"
. "Le informamos que su pedido de {PRODUCTO} ya se encuentra disponible en su ciudad 📦\n\n"
. "📍 Lugar de recojo: Shalom {SEDE_ENVIO}\n\n"
. "💰 Detalle de pago:\n\n"
. "• Monto total del pedido: S/ {MONTO_TOTAL}\n"
. "• Adelanto realizado: S/ {ADELANTO} ✅\n"
. "• Saldo pendiente: S/ {SALDO_PENDIENTE}\n\n"
. "Para continuar, le solicitamos enviar la captura de su pago restante por este medio 📄\n\n"
. "Una vez confirmado su pago ✅, le enviaremos su clave de recojo 🔐, para que pueda retirar su pedido en la agencia sin inconvenientes 📦\n\n"
. "Quedamos atentos a su confirmación.\n"
. "Área de Atención al Cliente";
$replacements = [
'{NOMBRE_CLIENTE}' => $nombre_cliente,
'{PRODUCTO}' => $producto,
'{SEDE_ENVIO}' => $sede_envio,
'{MONTO_TOTAL}' => number_format($monto_total, 2),
'{ADELANTO}' => number_format($adelanto, 2),
'{SALDO_PENDIENTE}' => number_format($monto_debe, 2)
];
$whatsappMessageRaw = str_replace(array_keys($replacements), array_values($replacements), $template);
$whatsappMessageEncoded = urlencode($whatsappMessageRaw);
$celular = preg_replace('/[^0-9]/', '', $pedido['celular']);
if (strlen($celular) == 9) {
$celular = '51' . $celular;
}
$whatsappUrl = "https://api.whatsapp.com/send?phone={$celular}&text={$whatsappMessageEncoded}";
$trackingUrl = (!empty($pedido['codigo_rastreo']) && !empty($pedido['codigo_tracking']))
? 'https://shalom.com.pe/rastrea/' . rawurlencode((string) $pedido['codigo_rastreo']) . '/' . rawurlencode((string) $pedido['codigo_tracking'])
: 'https://shalom.com.pe/rastrea';
?>
<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 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>
</td>
<td><?php echo htmlspecialchars($pedido['agencia'] ?? 'SHALOM'); ?></td>
<td><?php echo htmlspecialchars($pedido['producto']); ?></td>
<td><?php echo htmlspecialchars($pedido['sede_envio'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pedido['cantidad'] ?: 1); ?></td>
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
<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 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>
<?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>
<?php if ($estadoShalomActualizadoAt !== ''): ?>
<small class="text-muted">Act. <?php echo htmlspecialchars($estadoShalomActualizadoAt); ?></small>
<?php endif; ?>
</div>
<?php else: ?>
<span class="badge bg-warning text-dark">VERIFICAR</span>
<?php endif; ?>
</td>
<?php if ($user_role !== 'Asesor'): ?><td><?php echo htmlspecialchars($pedido['asesor_nombre'] ?? 'N/A'); ?></td><?php endif; ?>
<td><?php echo htmlspecialchars($pedido['created_at']); ?></td>
<td>
<?php
$voucherRestantePath = $pedido['voucher_restante_path'] ?? '';
$voucherRestanteExists = $voucherRestantePath !== '' && file_exists(__DIR__ . '/' . $voucherRestantePath);
?>
<?php if ($voucherRestantePath && $voucherRestanteExists): ?>
<a href="<?php echo htmlspecialchars($voucherRestantePath); ?>" target="_blank" rel="noopener noreferrer">Ver</a>
<?php elseif (!empty($voucherRestantePath)): ?>
<span class="text-muted">No disponible</span>
<?php else: ?>
N/A
<?php endif; ?>
</td>
<td>
<a href="pedido_form.php?id=<?php echo $pedido['id']; ?>" class="btn btn-sm btn-warning">Editar</a>
<?php if ($user_role === 'Administrador'): ?>
<a href="delete_pedido.php?id=<?php echo $pedido['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('¿Estás seguro de que quieres eliminar este pedido?');">Eliminar</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (empty($pedidos)): ?>
<p class="text-center">No hay pedidos que coincidan con el filtro.</p>
<?php endif; ?>
</div>
</div>
</div>
<?php include 'layout_footer.php'; ?>
<script>
$(document).ready(function() {
$("#pedidos-table").DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.10.25/i18n/Spanish.json"
},
"order": [[ 0, "desc" ]],
"paging": false,
"lengthChange": false,
"info": false
});
});
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 SHALOM_AUTO_SKIP_RECENT_MINUTES = 3;
const SHALOM_REQUEST_GAP_MS = 250;
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';
let whatsappClass = 'btn-secondary';
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')) {
badgeClass = 'bg-primary';
} else if (upper.includes('REGISTRADO') || upper.includes('EN ORIGEN') || upper.includes('ORIGEN')) {
badgeClass = 'bg-warning text-dark';
}
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';
const span = document.createElement('span');
span.className = `badge ${badgeClass}`;
span.textContent = text;
if (title) {
span.title = title;
}
wrapper.appendChild(span);
if (updatedAtText) {
const small = document.createElement('small');
small.className = 'text-muted';
small.textContent = updatedAtText;
wrapper.appendChild(small);
}
cell.appendChild(wrapper);
};
const formatDateTimeLabel = (rawValue) => {
if (!rawValue) return '';
const parsed = new Date(rawValue);
if (!Number.isNaN(parsed.getTime())) {
return 'Act. ' + parsed.toLocaleString('es-PE', {
day: '2-digit',
month: '2-digit',
year: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
const cleanText = String(rawValue).replace(/^Desde el\s*/i, '').trim();
return cleanText ? `Act. ${cleanText}` : '';
};
const buildErrorBadgeText = (errorMessage, detailsText = '') => {
const errText = (errorMessage || '').toString();
const codeMatch = errText.match(/c[oó]digo\s*(\d{3,5})/i);
const codeMatch2 = !codeMatch ? errText.match(/Shalom\s*(\d{3,5})/i) : null;
if (codeMatch) return `Shalom ${codeMatch[1]}`;
if (codeMatch2) return `Shalom ${codeMatch2[1]}`;
const isTimeout = /timed out|timeout/i.test(errText) || /timed out|timeout/i.test(detailsText);
const httpMatch = errText.match(/HTTP\s*(\d{3})/i);
if (isTimeout) return 'Shalom timeout';
if (httpMatch) return `Shalom HTTP ${httpMatch[1]}`;
if (/comunicaci[oó]n/i.test(errText)) return 'Error API Shalom';
return errText.length > 22 ? errText.slice(0, 22) + '…' : (errText || 'Error Shalom');
};
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();
} catch (error) {
rawText = '';
}
let data = null;
if (rawText) {
try {
data = JSON.parse(rawText);
} catch (error) {
data = null;
}
}
if (!response.ok) {
const msg =
(data && data.error) ? data.error :
rawText ? (rawText.length > 140 ? rawText.slice(0, 140) + '…' : rawText) :
`HTTP ${response.status}`;
let detailsText = '';
if (data && Object.prototype.hasOwnProperty.call(data, 'details') && data.details !== undefined && data.details !== null && data.details !== '') {
detailsText = typeof data.details === 'string' ? data.details : JSON.stringify(data.details);
} else if (rawText) {
detailsText = rawText;
}
return {
error: msg,
details: detailsText,
http_code: response.status
};
}
return data;
};
const persistShalomStatus = async (pedidoId, statusMessage) => {
if (!pedidoId || !statusMessage) return null;
try {
const response = await fetch('update_shalom_status.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: pedidoId,
status: statusMessage
})
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload.success) {
return null;
}
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 null;
}
};
const applyStatusToRow = async (pedidoId, statusMessage, data, whatsappIcon) => {
const statusCell = document.getElementById(`live-status-${pedidoId}`);
if (!statusCell) return;
const { badgeClass, whatsappClass } = getStatusMeta(statusMessage);
const updatedFromProvider =
formatDateTimeLabel(data?.statuses?.data?.registrado?.fecha) ||
formatDateTimeLabel(data?.statuses?.data?.registrado?.fecha_text);
setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider || 'Actualizando...');
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);
}
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);
}
};
if (table) {
table.addEventListener('click', function(e) {
if (e.target && e.target.classList.contains('editable')) {
// ... (la lógica de edición en línea se mantiene igual)
}
});
}
var trackingModal = document.getElementById('trackingModal');
trackingModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var orderNumber = button.getAttribute('data-order-number');
var orderCode = button.getAttribute('data-order-code');
var whatsappMessage = button.getAttribute('data-whatsapp-message');
var pedidoId = button.getAttribute('data-pedido-id');
var modalOrderNumberSpan = trackingModal.querySelector('#modal-order-number');
var modalOrderCodeSpan = trackingModal.querySelector('#modal-order-code');
var modalStatusDiv = trackingModal.querySelector('#modal-tracking-status');
modalOrderNumberSpan.textContent = orderNumber;
modalOrderCodeSpan.textContent = orderCode;
modalStatusDiv.innerHTML = '<p class="text-center">Consultando estado en Shalom...</p>';
if (!SHALOM_API_ENABLED) {
modalStatusDiv.innerHTML = '<p class="text-warning text-center"><strong>VERIFICAR</strong>: Estado Shalom requiere confirmación manual por el momento.</p>';
return;
}
if (orderNumber === 'N/A' || orderCode === 'N/A' || !orderNumber || !orderCode) {
modalStatusDiv.innerHTML = '<p class="text-danger text-center">No hay suficientes datos (Nº de Orden o Código de Orden) para consultar.</p>';
return;
}
fetchShalomData(orderNumber, orderCode, { includeDetails: true })
.then(async data => {
if (data && data.error) {
const title = data.details ? `${data.error} - ${data.details}` : data.error;
modalStatusDiv.innerHTML = `<p class="text-danger text-center" title="${title.replace(/"/g, '&quot;')}"><strong>Error:</strong> ${data.error}</p>`;
return;
}
if (!(data && data.statuses && data.statuses.message)) {
modalStatusDiv.innerHTML = '<p class="text-warning text-center">No se pudo encontrar la guía.</p>';
return;
}
const searchData = (data.search && data.search.data) ? data.search.data : {};
const statusData = (data.statuses && data.statuses.data) ? data.statuses.data : {};
const statusMessage = data.statuses.message || 'No disponible';
const upperStatus = statusMessage.toUpperCase();
let progressWidth = 0;
let activeStep = 0;
let descriptionText = searchData.detail_text || 'Su pedido está siendo procesado.';
if (upperStatus.includes('ENTREGADO')) {
progressWidth = 100;
activeStep = 4;
if (!searchData.detail_text) descriptionText = 'El pedido ha sido entregado satisfactoriamente.';
} else if (upperStatus.includes('DESTINO') || upperStatus.includes('REPARTO')) {
progressWidth = 66;
activeStep = 3;
if (!searchData.detail_text) descriptionText = 'Está listo para su recojo.';
} else if (upperStatus.includes('TRANSITO') || upperStatus.includes('TRÁNSITO')) {
progressWidth = 33;
activeStep = 2;
if (!searchData.detail_text) descriptionText = 'Su pedido se encuentra en camino a la ciudad de destino.';
} else {
progressWidth = 0;
activeStep = 1;
if (!searchData.detail_text) descriptionText = 'El pedido ha sido recibido en nuestra agencia de origen.';
}
let formattedDate = new Date().toLocaleString('es-PE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
if (statusData.registrado && statusData.registrado.fecha) {
const parsedDate = new Date(statusData.registrado.fecha);
if (!Number.isNaN(parsedDate.getTime())) {
formattedDate = parsedDate.toLocaleString('es-PE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
}
} else if (statusData.registrado && statusData.registrado.fecha_text) {
formattedDate = String(statusData.registrado.fecha_text).replace(/^Desde el\s*/i, '').trim();
}
const steps = ['En origen', 'En tránsito', 'En destino', 'Entregado'];
let stepsHtml = '';
steps.forEach((step, index) => {
const stepNum = index + 1;
const isActive = stepNum <= activeStep;
stepsHtml += `
<div class="text-center" style="width: 120px; position: relative; z-index: 2;">
<div style="width: 36px; height: 36px; background-color: ${isActive ? '#e30613' : 'white'}; border: 2px solid ${isActive ? '#e30613' : '#fce4e4'}; border-radius: 50%; margin: 0 auto 10px; display: flex; align-items: center; justify-content: center;">
${isActive ? '<i class="fas fa-check" style="color: white;"></i>' : '<span style="color: #e30613; font-weight: bold;">' + stepNum + '</span>'}
</div>
<div style="font-size: 0.95rem; color: ${isActive ? '#1a1a6e' : '#999'}; font-weight: ${isActive ? 'bold' : 'normal'};">${step}</div>
</div>
`;
});
const internalOrderNumber = searchData.internal_order_number || orderNumber;
const externalTrackingUrl = searchData.tracking_url || `https://shalom.com.pe/rastrea/${encodeURIComponent(orderNumber)}/${encodeURIComponent(orderCode)}`;
const loginNotice = searchData.requires_login
? '<div class="alert alert-light border mt-4 mb-0">Para ver más detalles del remitente, destinatario o comprobantes, Shalom pide iniciar sesión en su plataforma.</div>'
: '';
const html = `
<div class="p-4 p-md-5" style="background-color: #ffffff; color: #1a1a6e; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
<div class="d-flex flex-column gap-4">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-4">
<div class="d-flex align-items-center gap-4">
<div class="d-none d-md-block">
<svg width="149" height="76" viewBox="0 0 149 76" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<g clip-path="url(#clip0_2090_6295)">
<path d="M5.18789 63.2602C2.70391 64.3057 -0.0905687 65.6739 -6.91088e-06 68.0618C0.0517427 69.4558 1.16436 70.6821 2.6651 71.4049C5.35608 72.7085 8.49986 72.5149 11.5013 72.36C46.1218 70.553 80.393 76.2194 114.999 74.4123C125.276 73.8805 138.12 71.279 148.162 69.7873C148.834 69.6842 149.656 69.5167 149.907 68.9526C150.179 68.3614 149.691 67.7355 149.166 67.2826C145.429 64.0579 139.416 63.9578 133.927 63.9027C106.948 63.6333 79.9726 63.3638 52.9937 63.0944C37.0619 62.9347 21.0684 62.6781 5.18789 63.2602Z" fill="#F2F2F2"></path>
<path d="M91.3254 57.0124L60.8542 48.3572L57.8184 59.5408L88.2895 68.196L91.3254 57.0124Z" fill="#163560"></path>
<path d="M124.784 60.1128H91.1367V17.1699H124.784V60.1128Z" fill="#163560"></path>
<path d="M91.1367 60.1128H62.4088C57.0991 60.1128 52.793 55.8071 52.793 50.497V26.7857C52.793 21.4756 57.0991 17.1699 62.4088 17.1699H91.1367V60.1128Z" fill="#EE2A2F"></path>
<path d="M121.742 20.8018H93.8867V56.4735H121.742V20.8018Z" fill="white"></path>
<path d="M118.944 24.3525H96.6777V52.9224H118.944V24.3525Z" fill="#163560"></path>
<path d="M75.2105 26.5192L82.5716 30.3197L83.821 38.5193L77.9399 44.5176L69.8667 43.2392L65.0059 35.6452L67.0191 27.7568L75.2105 26.5192Z" fill="white"></path>
</g>
<defs>
<clipPath id="clip0_2090_6295">
<rect width="149" height="76" fill="white"></rect>
</clipPath>
</defs>
</svg>
</div>
<div>
<div class="d-flex align-items-center flex-wrap gap-3 mb-2">
<h1 style="color: #e30613 !important; font-weight: 900; margin: 0; text-transform: uppercase; font-size: 2.4rem;">${statusMessage.toUpperCase()}</h1>
<span style="border: 2px solid #e30613; color: #e30613; border-radius: 999px; padding: 6px 16px; font-size: 1rem; font-weight: 700;">GRT</span>
</div>
<p class="mb-1" style="color: #666; font-size: 1.1rem;">${descriptionText}</p>
<p class="mb-0" style="color: #e30613; font-weight: bold; font-size: 1rem;">N° interno Shalom: ${internalOrderNumber}</p>
</div>
</div>
<div class="text-lg-end">
<p class="mb-1" style="font-weight: 700; font-size: 1.05rem; color: #1a1a6e;">Consulta externa: ${orderNumber} / ${orderCode}</p>
<p class="text-muted mb-2" style="font-size: 1rem;">Desde el ${formattedDate}</p>
<a href="${externalTrackingUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm">Abrir rastreo Shalom</a>
</div>
</div>
<div class="position-relative mt-2" style="padding: 0 40px;">
<div class="progress" style="height: 12px; background-color: #fce4e4; border-radius: 6px;">
<div class="progress-bar" role="progressbar" style="width: ${progressWidth}%; background-color: #e30613;"></div>
</div>
<div class="d-flex justify-content-between position-absolute top-50 start-0 translate-middle-y w-100" style="padding: 0 16px;">
${stepsHtml}
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<div class="border rounded-4 p-3 h-100 bg-light-subtle">
<div class="small text-muted text-uppercase mb-1">Origen</div>
<div class="fw-semibold">${searchData.origen?.nombre || 'N/A'}</div>
</div>
</div>
<div class="col-md-6">
<div class="border rounded-4 p-3 h-100 bg-light-subtle">
<div class="small text-muted text-uppercase mb-1">Destino</div>
<div class="fw-semibold">${searchData.destino?.nombre || 'N/A'}</div>
<div class="text-muted small">${searchData.destino?.direccion || ''}</div>
</div>
</div>
</div>
${loginNotice}
<div class="d-grid gap-2 mt-2">
<button class="btn btn-success btn-lg" id="btn-copy-summary">
<i class="fab fa-whatsapp"></i> Copiar Resumen para Cliente
</button>
</div>
</div>
</div>
`;
modalStatusDiv.innerHTML = html;
document.getElementById('btn-copy-summary').addEventListener('click', function() {
copyStatusToClipboard(whatsappMessage);
});
const whatsappIcon = document.getElementById(`whatsapp-icon-${pedidoId}`);
await applyStatusToRow(pedidoId, statusMessage, data, whatsappIcon);
})
.catch(error => {
console.error('Error fetching tracking status:', error);
modalStatusDiv.innerHTML = `<p class="text-danger text-center"><strong>Error al consultar:</strong> ${error.message}</p>`;
});
});
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];
const statusCell = document.getElementById(`live-status-${pedidoId}`);
if (!statusCell) return;
setStatusCell(statusCell, 'bg-warning text-dark', 'VERIFICAR');
});
updateVerificationNotice('La consulta automática de Shalom no está disponible ahora mismo.', 'warning');
return;
}
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;
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;
const orderCode = row.dataset.orderCode;
const pedidoId = row.id.split('-')[1];
const statusCell = document.getElementById(`live-status-${pedidoId}`);
const whatsappIcon = document.getElementById(`whatsapp-icon-${pedidoId}`);
if (!statusCell) {
processed++;
continue;
}
if (orderCode === '26') {
setStatusCell(statusCell, 'bg-info', 'OLVA COURIER');
processed++;
updateVerificationNotice(buildVerificationProgressLabel(), 'info');
continue;
}
if (!orderNumber || !orderCode || orderNumber === 'N/A' || orderCode === 'N/A') {
setStatusCell(statusCell, 'bg-light text-dark', 'Sin datos');
processed++;
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;
}
setStatusCell(statusCell, 'bg-info text-dark', 'Verificando...');
try {
const data = await fetchShalomData(orderNumber, orderCode);
if (data && data.error) {
const details = data.details ? (typeof data.details === 'string' ? data.details : JSON.stringify(data.details)) : '';
const title = details ? `${data.error} - ${details}` : data.error;
const badgeText = buildErrorBadgeText(data.error, details);
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');
}
} catch (error) {
console.error('Error fetching status:', error);
const fullMessage = error && error.message ? error.message : 'Error al consultar';
const shortErr = fullMessage.length > 22 ? fullMessage.slice(0, 22) + '…' : fullMessage;
setStatusCell(statusCell, 'bg-danger', shortErr, fullMessage);
}
processed++;
updateVerificationNotice(buildVerificationProgressLabel(), 'info');
if (processed < rows.length) {
await sleep(SHALOM_REQUEST_GAP_MS);
}
}
const finishedAt = new Date().toLocaleTimeString('es-PE', {
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}.${skippedLockedSummary}${skippedRecentSummary}`
: `Actualización manual de Estados Shalom completada a las ${finishedAt}.${skippedLockedSummary}${skippedRecentSummary}`,
'success'
);
})();
try {
await shalomVerificationPromise;
} finally {
shalomVerificationPromise = null;
if (verifyButton) {
verifyButton.disabled = false;
verifyButton.innerHTML = 'Actualizar Estados Shalom';
}
}
}
const verifyButton = document.getElementById('verify-statuses-btn');
if (verifyButton) {
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) {
const btn = e.target && e.target.closest ? e.target.closest('.copy-summary-icon') : null;
if (!btn) return;
const message = btn.getAttribute('data-whatsapp-message') || '';
if (!message) {
alert('No hay resumen disponible para copiar.');
return;
}
copyStatusToClipboard(message);
});
});
function copyStatusToClipboard(message) {
navigator.clipboard.writeText(message).then(() => {
alert('Resumen copiado al portapapeles. Ya puedes pegarlo en WhatsApp.');
}).catch(err => {
console.error('Error al copiar:', err);
});
}
</script>