Autosave: 20260718-003740
This commit is contained in:
parent
82bca45582
commit
3e16c3adac
@ -72,6 +72,35 @@ function cc_test_display_value(?string $value, string $fallback = 'No registrado
|
||||
return $value !== '' ? $value : $fallback;
|
||||
}
|
||||
|
||||
function cc_test_build_url(string $path, array $params = []): string
|
||||
{
|
||||
foreach ($params as $key => $value) {
|
||||
if ($value === '' || $value === null) {
|
||||
unset($params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$query = http_build_query($params);
|
||||
|
||||
return $query !== '' ? $path . '?' . $query : $path;
|
||||
}
|
||||
|
||||
function cc_test_find_assessor_key_by_user_id(?int $userId, array $assessors): ?string
|
||||
{
|
||||
$userId = $userId !== null && $userId > 0 ? $userId : null;
|
||||
if ($userId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === $userId) {
|
||||
return (string) $assessorKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function cc_test_order_label(array $order): string
|
||||
{
|
||||
if (!empty($order['is_agregado'])) {
|
||||
@ -177,6 +206,9 @@ $assessors = [];
|
||||
$orders = [];
|
||||
$visibleOrders = [];
|
||||
$modalsHtml = [];
|
||||
$isAdmin = false;
|
||||
$selectedAssessorFilter = '';
|
||||
$selectedAssessorFilterLabel = '';
|
||||
$totalRows = 0;
|
||||
$agregadosCount = 0;
|
||||
$lastProcessedRow = null;
|
||||
@ -295,7 +327,16 @@ try {
|
||||
|
||||
$role = (string) ($_SESSION['user_role'] ?? '');
|
||||
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
|
||||
if (!in_array($role, ['Administrador', 'admin'], true)) {
|
||||
$isAdmin = in_array($role, ['Administrador', 'admin'], true);
|
||||
$selectedAssessorFilter = cc_test_normalize_user_key($_GET['assessor'] ?? '');
|
||||
if (!$isAdmin || ($selectedAssessorFilter !== '' && !isset($assessors[$selectedAssessorFilter]))) {
|
||||
$selectedAssessorFilter = '';
|
||||
}
|
||||
$selectedAssessorFilterLabel = $selectedAssessorFilter !== ''
|
||||
? (string) ($assessors[$selectedAssessorFilter]['label'] ?? $selectedAssessorFilter)
|
||||
: '';
|
||||
|
||||
if (!$isAdmin) {
|
||||
$orders = array_values(array_filter($orders, static function (array $order) use ($currentUserId): bool {
|
||||
return (int) ($order['user_id'] ?? 0) === $currentUserId;
|
||||
}));
|
||||
@ -326,6 +367,15 @@ try {
|
||||
$order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false;
|
||||
$order['es_pendiente_hoy'] = !$order['es_promo_final'] && (in_array($order['estado'], $openStates, true) || $order['pendiente_logistica']);
|
||||
$order['es_cerrado'] = in_array($order['estado'], $closedStates, true);
|
||||
$order['assessor_key'] = cc_test_find_assessor_key_by_user_id(isset($order['user_id']) ? (int) $order['user_id'] : null, $assessors);
|
||||
$order['assessor_label'] = $order['assessor_key'] !== null
|
||||
? (string) ($assessors[$order['assessor_key']]['label'] ?? $order['assessor_key'])
|
||||
: 'Sin asignar';
|
||||
$order['matches_assessor_filter'] = $selectedAssessorFilter === '' || (($order['assessor_key'] ?? '') === $selectedAssessorFilter);
|
||||
|
||||
if (!$order['matches_assessor_filter']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stats['total']++;
|
||||
if ($order['es_nuevo_hoy']) {
|
||||
@ -352,7 +402,11 @@ try {
|
||||
}
|
||||
unset($order);
|
||||
|
||||
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view): bool {
|
||||
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view, $selectedAssessorFilter): bool {
|
||||
if ($selectedAssessorFilter !== '' && !($order['matches_assessor_filter'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($view) {
|
||||
'pendientes_hoy' => (bool) ($order['es_pendiente_hoy'] ?? false),
|
||||
'nuevos_hoy' => (bool) ($order['es_nuevo_hoy'] ?? false),
|
||||
@ -442,6 +496,15 @@ try {
|
||||
$errorMessage = $exception->getMessage();
|
||||
}
|
||||
|
||||
$callCenterBaseParams = [
|
||||
'view' => $view,
|
||||
'store' => $storeKey,
|
||||
];
|
||||
$callCenterParams = $callCenterBaseParams;
|
||||
if ($selectedAssessorFilter !== '') {
|
||||
$callCenterParams['assessor'] = $selectedAssessorFilter;
|
||||
}
|
||||
|
||||
require_once 'layout_header.php';
|
||||
?>
|
||||
|
||||
@ -456,48 +519,140 @@ require_once 'layout_header.php';
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form {
|
||||
padding: .5rem !important;
|
||||
width: 100%;
|
||||
padding: .72rem !important;
|
||||
border: 1px solid rgba(0, 0, 0, .10) !important;
|
||||
border-left: .55rem solid #FFFFFF;
|
||||
background-color: #FFFFFF;
|
||||
color: #212529;
|
||||
box-shadow: 0 .65rem 1.4rem rgba(15, 23, 42, .06), inset 0 0 0 1px rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent {
|
||||
border-left: .45rem solid var(--cc-assessor-accent, #adb5bd);
|
||||
background: linear-gradient(135deg, rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .12) 0%, rgba(255, 255, 255, .98) 72%);
|
||||
border-color: rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .38) !important;
|
||||
border-left-color: var(--cc-assessor-accent, #adb5bd);
|
||||
background-color: var(--cc-assessor-accent, #adb5bd);
|
||||
color: var(--cc-assessor-accent-contrast, #212529);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.is-neutral {
|
||||
border-color: rgba(108, 117, 125, .22) !important;
|
||||
border-left-color: #ADB5BD;
|
||||
background-color: #F1F3F5;
|
||||
color: #495057;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form .text-muted {
|
||||
color: #6C757D !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent .text-muted {
|
||||
color: var(--cc-assessor-accent-contrast, #212529) !important;
|
||||
opacity: .88;
|
||||
}
|
||||
|
||||
.cc-callcenter-assessor-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
background-color: rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .12);
|
||||
color: #212529;
|
||||
border-color: rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .28) !important;
|
||||
background-color: #F8F9FA;
|
||||
color: #6C757D;
|
||||
border-color: rgba(0, 0, 0, .12) !important;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .04);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent .cc-callcenter-assessor-badge {
|
||||
background-color: rgba(255, 255, 255, .18);
|
||||
color: var(--cc-assessor-accent-contrast, #212529);
|
||||
border-color: rgba(255, 255, 255, .42) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-color-dot {
|
||||
width: .55rem;
|
||||
height: .55rem;
|
||||
border-radius: 999px;
|
||||
background: var(--cc-assessor-accent, #adb5bd);
|
||||
background: #ADB5BD;
|
||||
border: 1px solid rgba(0, 0, 0, .12);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
|
||||
box-shadow: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent .cc-callcenter-assessor-badge .cc-callcenter-color-dot {
|
||||
background: var(--cc-assessor-accent-contrast, #212529);
|
||||
border: 1px solid rgba(255, 255, 255, .35);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
padding: .5rem;
|
||||
border-radius: .95rem;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent .cc-callcenter-assign-row {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.is-neutral .cc-callcenter-assign-row {
|
||||
background-color: rgba(255, 255, 255, .72);
|
||||
box-shadow: inset 0 0 0 1px rgba(108, 117, 125, .12);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-row .form-select {
|
||||
min-width: 11rem;
|
||||
flex: 1 1 11rem;
|
||||
border-color: rgba(0, 0, 0, .16);
|
||||
background-color: #FFFFFF;
|
||||
color: #212529;
|
||||
font-weight: 700;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent .cc-callcenter-assign-row .form-select {
|
||||
border-color: rgba(255, 255, 255, .48);
|
||||
background-color: rgba(255, 255, 255, .18);
|
||||
color: var(--cc-assessor-accent-contrast, #212529);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-row .form-select:focus {
|
||||
border-color: rgba(0, 0, 0, .18);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent .cc-callcenter-assign-row .form-select:focus {
|
||||
border-color: rgba(255, 255, 255, .72);
|
||||
box-shadow: 0 0 0 .2rem rgba(255, 255, 255, .18);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-submit {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
border-color: rgba(0, 0, 0, .12) !important;
|
||||
background-color: #F1F3F5 !important;
|
||||
color: #495057 !important;
|
||||
font-weight: 700;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.has-accent .cc-callcenter-assign-submit {
|
||||
border-color: rgba(255, 255, 255, .42) !important;
|
||||
background-color: rgba(255, 255, 255, .18) !important;
|
||||
color: var(--cc-assessor-accent-contrast, #212529) !important;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-submit:hover:not(:disabled) {
|
||||
filter: brightness(.97);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cc-callcenter-actions {
|
||||
@ -518,17 +673,17 @@ require_once 'layout_header.php';
|
||||
<div>
|
||||
<h1 class="h2 fw-bold mb-1"><i class="bi bi-headset text-primary"></i> Pedidos Asignados</h1>
|
||||
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||
<?php if (in_array((string) ($_SESSION['user_role'] ?? ''), ['Administrador', 'admin'], true)): ?>
|
||||
<a href="gestiones_callcenter.php?view=<?php echo urlencode($view); ?>&store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-sm btn-outline-primary">Base de Datos Pedidos</a>
|
||||
<?php if ($isAdmin): ?>
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', $callCenterParams)); ?>" class="btn btn-sm btn-outline-primary">Base de Datos Pedidos</a>
|
||||
<?php endif; ?>
|
||||
<a href="call_center_pro.php?view=<?php echo urlencode($view); ?>&store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-sm btn-primary">Pedidos Asignados</a>
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', $callCenterParams)); ?>" class="btn btn-sm btn-primary">Pedidos Asignados</a>
|
||||
</div>
|
||||
<ul class="nav nav-pills mt-3" role="tablist" aria-label="Selector de tienda">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo $storeKey === 'flower' ? 'active' : ''; ?>" href="?view=<?php echo htmlspecialchars($view); ?>&store=flower">Flower</a>
|
||||
<a class="nav-link <?php echo $storeKey === 'flower' ? 'active' : ''; ?>" href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['store' => 'flower']))); ?>">Flower</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo $storeKey === 'otra_tienda' ? 'active' : ''; ?>" href="?view=<?php echo htmlspecialchars($view); ?>&store=otra_tienda">TUANI</a>
|
||||
<a class="nav-link <?php echo $storeKey === 'otra_tienda' ? 'active' : ''; ?>" href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['store' => 'otra_tienda']))); ?>">TUANI</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -555,7 +710,7 @@ require_once 'layout_header.php';
|
||||
<?php else: ?>
|
||||
<section class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=pendientes_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'pendientes_hoy']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'pendientes_hoy' ? 'bg-dark text-white' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase opacity-75 mb-2">Bandeja principal</div>
|
||||
@ -565,7 +720,7 @@ require_once 'layout_header.php';
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=nuevos_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'nuevos_hoy']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'nuevos_hoy' ? 'bg-primary-subtle border border-primary' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Nuevos de hoy</div>
|
||||
@ -575,7 +730,7 @@ require_once 'layout_header.php';
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=confirmados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'confirmados']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'confirmados' ? 'bg-success-subtle border border-success' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Confirmados</div>
|
||||
@ -585,7 +740,7 @@ require_once 'layout_header.php';
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=seguimiento&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'seguimiento']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'seguimiento' ? 'bg-primary-subtle border border-primary' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Seguimiento</div>
|
||||
@ -595,7 +750,7 @@ require_once 'layout_header.php';
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=promo_final&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'promo_final']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'promo_final' ? 'bg-danger-subtle border border-danger' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Promo Final</div>
|
||||
@ -605,7 +760,7 @@ require_once 'layout_header.php';
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=observados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'observados']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'observados' ? 'bg-warning-subtle border border-warning' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Observados</div>
|
||||
@ -615,7 +770,7 @@ require_once 'layout_header.php';
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=cerrados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'cerrados']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'cerrados' ? 'bg-secondary-subtle border border-secondary' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Cerrados</div>
|
||||
@ -625,7 +780,7 @@ require_once 'layout_header.php';
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=todos&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'todos']))); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'todos' ? 'bg-light border' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Todos</div>
|
||||
@ -636,13 +791,50 @@ require_once 'layout_header.php';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($isAdmin): ?>
|
||||
<section class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body py-3">
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="view" value="<?php echo htmlspecialchars($view); ?>">
|
||||
<input type="hidden" name="store" value="<?php echo htmlspecialchars($storeKey); ?>">
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<label for="assessorFilter" class="form-label small text-uppercase text-muted fw-semibold mb-1">Filtrar por asesora</label>
|
||||
<select name="assessor" id="assessorFilter" class="form-select">
|
||||
<option value="">Todas las asesoras</option>
|
||||
<?php foreach ($assessors as $assessorKey => $assessor): ?>
|
||||
<option value="<?php echo htmlspecialchars((string) $assessorKey); ?>"<?php echo $selectedAssessorFilter === $assessorKey ? ' selected' : ''; ?>><?php echo htmlspecialchars((string) ($assessor['label'] ?? $assessorKey)); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Aplicar filtro</button>
|
||||
<?php if ($selectedAssessorFilter !== ''): ?>
|
||||
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', $callCenterBaseParams)); ?>" class="btn btn-outline-secondary">Ver todas</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg">
|
||||
<div class="small text-muted">Usa este filtro para revisar solo los pedidos de una asesora y ver sus tarjetas por estado dentro de esta misma pantalla.</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
|
||||
<div>
|
||||
<h2 class="h5 fw-bold mb-1"><?php echo htmlspecialchars($allowedViews[$view]); ?></h2>
|
||||
<p class="text-muted small mb-1">Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega, Confirmado envío, Cancelado y Repetido. <strong>Promo Final</strong> se habilita desde el Día 4 y exige imagen de sustento para mover el pedido.</p>
|
||||
</div>
|
||||
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
<?php if ($selectedAssessorFilterLabel !== ''): ?>
|
||||
<span class="badge bg-primary-subtle text-primary-emphasis border border-primary-subtle">Asesora: <?php echo htmlspecialchars($selectedAssessorFilterLabel); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
@ -662,7 +854,11 @@ require_once 'layout_header.php';
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
|
||||
No hay pedidos en esta bandeja por ahora.
|
||||
<?php if ($selectedAssessorFilterLabel !== ''): ?>
|
||||
No hay pedidos en esta bandeja para <?php echo htmlspecialchars($selectedAssessorFilterLabel); ?>.
|
||||
<?php else: ?>
|
||||
No hay pedidos en esta bandeja por ahora.
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
@ -780,30 +976,22 @@ require_once 'layout_header.php';
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column gap-1 align-items-stretch cc-callcenter-actions">
|
||||
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
|
||||
<?php if ($isAdmin): ?>
|
||||
<?php
|
||||
$orderUserId = $order['user_id'] ?? null;
|
||||
$orderUserIdNorm = $orderUserId !== null ? ((int) $orderUserId > 0 ? (int) $orderUserId : null) : null;
|
||||
$unassigned = $orderUserIdNorm === null;
|
||||
$assignedAssessorKey = null;
|
||||
if (!$unassigned) {
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === (int) $orderUserIdNorm) {
|
||||
$assignedAssessorKey = (string) $assessorKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$assignedAssessorKey = $order['assessor_key'] ?? null;
|
||||
|
||||
$assignFormClass = 'border rounded-3 p-1 text-start shadow-sm cc-callcenter-assign-form';
|
||||
$assignFormClass = 'border rounded-3 p-1 text-start cc-callcenter-assign-form';
|
||||
$assignFormStyle = '';
|
||||
if ($unassigned) {
|
||||
$assignFormClass .= ' bg-light';
|
||||
$assignFormClass .= ' is-unassigned';
|
||||
} elseif ($assignedAssessorKey !== null) {
|
||||
$assignFormClass .= ' has-accent';
|
||||
$assignFormStyle = cc_test_assessor_css_vars($assignedAssessorKey, $assessors[$assignedAssessorKey]['color_hex'] ?? null);
|
||||
} else {
|
||||
$assignFormClass .= ' bg-secondary-subtle border-secondary';
|
||||
$assignFormClass .= ' is-neutral';
|
||||
}
|
||||
|
||||
$selectedAssessorKeyForSelect = $unassigned ? '' : ($assignedAssessorKey !== null ? (string) $assignedAssessorKey : '');
|
||||
|
||||
4
db/migrations/092_add_estado_shalom_cache_columns.sql
Normal file
4
db/migrations/092_add_estado_shalom_cache_columns.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- Cachea el último estado consultado de Shalom para la vista de Pedidos en Tránsito.
|
||||
ALTER TABLE `pedidos`
|
||||
ADD COLUMN IF NOT EXISTS `estado_shalom` VARCHAR(120) NULL AFTER `seguimiento`,
|
||||
ADD COLUMN IF NOT EXISTS `estado_shalom_actualizado_at` DATETIME NULL AFTER `estado_shalom`;
|
||||
@ -286,7 +286,7 @@ $is_admin = in_array($user_role, ['Administrador', 'admin'], true);
|
||||
// Exportar solo los pedidos con fecha de entrega "mañana"
|
||||
$date_condition = "DATE(p.fecha_entrega) = DATE_ADD(CURDATE(), INTERVAL 1 DAY)";
|
||||
|
||||
$sql = "SELECT p.* FROM pedidos p WHERE p.estado IN ('RUTA_CONTRAENTREGA', 'PENDIENTE', 'NO CONTESTO, VOLVER A LLAMAR', 'NO CONTESTO, DEVOLVER LLAMADA', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA', 'RETORNADO')";
|
||||
$sql = "SELECT p.* FROM pedidos p WHERE p.estado IN ('RUTA_CONTRAENTREGA', 'NO CONTESTO, VOLVER A LLAMAR', 'NO CONTESTO, DEVOLVER LLAMADA', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA', 'RETORNADO')";
|
||||
$params = [];
|
||||
|
||||
if ($user_role === 'Asesor') {
|
||||
|
||||
@ -870,10 +870,10 @@ require_once 'layout_header.php';
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form .cc-callcenter-assessor-badge {
|
||||
background-color: rgba(0, 0, 0, .16);
|
||||
background-color: rgba(255, 255, 255, .18);
|
||||
color: var(--cc-assessor-accent-contrast, #212529);
|
||||
border-color: rgba(0, 0, 0, .16) !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
|
||||
border-color: rgba(255, 255, 255, .42) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assessor-badge {
|
||||
@ -884,7 +884,7 @@ require_once 'layout_header.php';
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form .cc-callcenter-assessor-badge .cc-callcenter-color-dot {
|
||||
border: 1px solid rgba(0, 0, 0, .18);
|
||||
border: 1px solid rgba(255, 255, 255, .65);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@ -912,10 +912,11 @@ require_once 'layout_header.php';
|
||||
.cc-callcenter-assign-row .form-select {
|
||||
min-width: 10rem;
|
||||
flex: 1 1 10rem;
|
||||
border-color: rgba(0, 0, 0, .18);
|
||||
background-color: rgba(0, 0, 0, .12);
|
||||
border-color: rgba(255, 255, 255, .48);
|
||||
background-color: rgba(255, 255, 255, .18);
|
||||
color: var(--cc-assessor-accent-contrast, #212529);
|
||||
font-weight: 700;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assign-row .form-select {
|
||||
@ -925,18 +926,18 @@ require_once 'layout_header.php';
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-row .form-select:focus {
|
||||
border-color: rgba(0, 0, 0, .24);
|
||||
box-shadow: 0 0 0 .2rem rgba(0, 0, 0, .12);
|
||||
border-color: rgba(255, 255, 255, .72);
|
||||
box-shadow: 0 0 0 .2rem rgba(255, 255, 255, .18);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-submit {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
border-color: rgba(0, 0, 0, .18) !important;
|
||||
background-color: rgba(0, 0, 0, .16) !important;
|
||||
border-color: rgba(255, 255, 255, .42) !important;
|
||||
background-color: rgba(255, 255, 255, .18) !important;
|
||||
color: var(--cc-assessor-accent-contrast, #212529) !important;
|
||||
font-weight: 700;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-assessor-badge {
|
||||
@ -956,11 +957,11 @@ require_once 'layout_header.php';
|
||||
gap: .3rem;
|
||||
padding: .2rem .55rem;
|
||||
white-space: nowrap;
|
||||
border-color: rgba(0, 0, 0, .18) !important;
|
||||
background-color: rgba(0, 0, 0, .12);
|
||||
border-color: rgba(255, 255, 255, .42) !important;
|
||||
background-color: rgba(255, 255, 255, .18);
|
||||
color: var(--cc-assessor-accent-contrast, #212529);
|
||||
font-weight: 700;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cc-callcenter-color-trigger:hover:not(:disabled),
|
||||
@ -971,7 +972,7 @@ require_once 'layout_header.php';
|
||||
|
||||
.cc-callcenter-color-trigger:disabled {
|
||||
opacity: .72;
|
||||
background-color: rgba(0, 0, 0, .10);
|
||||
background-color: rgba(255, 255, 255, .12);
|
||||
}
|
||||
|
||||
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-color-trigger {
|
||||
@ -990,7 +991,7 @@ require_once 'layout_header.php';
|
||||
height: .72rem;
|
||||
border-radius: 999px;
|
||||
background: var(--cc-assessor-accent, #adb5bd);
|
||||
border: 1px solid rgba(0, 0, 0, .24);
|
||||
border: 1px solid rgba(255, 255, 255, .65);
|
||||
box-shadow: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
166
includes/shalom_scraper.js
Normal file
166
includes/shalom_scraper.js
Normal file
@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
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 sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const orderNumber = (process.argv[2] || '').trim();
|
||||
const orderCode = (process.argv[3] || '').trim();
|
||||
|
||||
if (!orderNumber || !orderCode) {
|
||||
console.log(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Número de orden y código de orden son requeridos.'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const trackingUrl = `https://shalom.com.pe/rastrea/${encodeURIComponent(orderNumber)}/${encodeURIComponent(orderCode)}`;
|
||||
let browser = null;
|
||||
|
||||
try {
|
||||
const executablePath = detectChromeExecutable();
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
executablePath: executablePath || undefined,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage'
|
||||
]
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
await page.goto(trackingUrl, {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
await sleep(4000);
|
||||
|
||||
const payload = await page.evaluate(() => {
|
||||
const textOf = (selectorList) => {
|
||||
for (const selector of selectorList) {
|
||||
const el = document.querySelector(selector);
|
||||
const text = el && el.textContent ? el.textContent.trim() : '';
|
||||
if (text) return text;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const allParagraphs = Array.from(document.querySelectorAll('p'))
|
||||
.map((p) => (p.textContent || '').trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const status = textOf([
|
||||
'.text-4xl.font-bold.text-red-color-sidebar',
|
||||
'.text-3xl.font-bold.text-red-color-sidebar'
|
||||
]);
|
||||
|
||||
let description = textOf([
|
||||
'p.text-md.font-normal.text-silver-title',
|
||||
'p.text-sm.font-normal.text-silver-title'
|
||||
]);
|
||||
|
||||
if (!description) {
|
||||
description = allParagraphs.find((text) => {
|
||||
return !/^N° DE ORDEN:/i.test(text)
|
||||
&& !/^Desde el/i.test(text)
|
||||
&& /destino|recojo|entregado|origen|procesado|tránsito|transito/i.test(text);
|
||||
}) || '';
|
||||
}
|
||||
|
||||
const orderLabel = allParagraphs.find((text) => /^N° DE ORDEN:/i.test(text)) || '';
|
||||
const dateLabel = allParagraphs.find((text) => /^Desde el/i.test(text)) || '';
|
||||
const internalOrderNumber = orderLabel.replace(/^N° DE ORDEN:\s*/i, '').trim();
|
||||
const bodyText = document.body && document.body.innerText ? document.body.innerText.trim() : '';
|
||||
|
||||
return {
|
||||
status,
|
||||
description,
|
||||
orderLabel,
|
||||
dateLabel,
|
||||
internalOrderNumber,
|
||||
requiresLogin: /Inicia sesión para acceder/i.test(bodyText),
|
||||
currentUrl: location.href,
|
||||
currentPath: location.pathname,
|
||||
};
|
||||
});
|
||||
|
||||
if (!payload.status || payload.currentPath === '/rastrea') {
|
||||
console.log(JSON.stringify({
|
||||
success: false,
|
||||
error: 'No se pudo encontrar la guía en el rastreo público de Shalom.',
|
||||
tracking_url: trackingUrl,
|
||||
}));
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
success: true,
|
||||
source: 'shalom_web',
|
||||
status: payload.status,
|
||||
description: payload.description,
|
||||
internal_order_number: payload.internalOrderNumber || null,
|
||||
external_order_number: orderNumber,
|
||||
order_code: orderCode,
|
||||
date_text: payload.dateLabel || null,
|
||||
tracking_url: trackingUrl,
|
||||
requires_login: !!payload.requiresLogin,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify({
|
||||
success: false,
|
||||
error: error && error.message ? error.message : 'Error al consultar Shalom.',
|
||||
tracking_url: trackingUrl,
|
||||
}));
|
||||
process.exit(3);
|
||||
} finally {
|
||||
if (browser) {
|
||||
try {
|
||||
await browser.close();
|
||||
} catch (error) {
|
||||
// Ignoramos errores al cerrar el navegador.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@ -34,6 +34,28 @@ function getStatusStyle($status) {
|
||||
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'];
|
||||
@ -97,6 +119,7 @@ $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';
|
||||
?>
|
||||
|
||||
@ -165,9 +188,10 @@ include 'layout_header.php';
|
||||
<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">Verificar Estados Ahora</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -246,12 +270,15 @@ include 'layout_header.php';
|
||||
}
|
||||
|
||||
$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-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="https://rastrea.shalom.pe/rastrea" target="_blank" class="btn btn-sm btn-primary" title="Rastreo Shalom">🚚</a>
|
||||
<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>
|
||||
<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" class="btn btn-sm btn-secondary whatsapp-icon" id="whatsapp-icon-<?php echo $pedido['id']; ?>" title="Enviar WhatsApp">💬</a>
|
||||
<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>
|
||||
@ -270,7 +297,22 @@ include 'layout_header.php';
|
||||
<?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>'; ?>
|
||||
</td>
|
||||
<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']; ?>"><span class="badge bg-warning text-dark">VERIFICAR</span></td>
|
||||
<td id="live-status-<?php echo $pedido['id']; ?>">
|
||||
<?php
|
||||
$estadoShalomCache = trim((string) ($pedido['estado_shalom'] ?? ''));
|
||||
$estadoShalomActualizadoAt = trim((string) ($pedido['estado_shalom_actualizado_at'] ?? ''));
|
||||
?>
|
||||
<?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>
|
||||
@ -308,7 +350,6 @@ include 'layout_header.php';
|
||||
<?php include 'layout_footer.php'; ?>
|
||||
|
||||
<script>
|
||||
// DataTable initialization and existing modal logic
|
||||
$(document).ready(function() {
|
||||
$("#pedidos-table").DataTable({
|
||||
"language": {
|
||||
@ -323,24 +364,192 @@ $(document).ready(function() {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const table = document.querySelector('.table');
|
||||
const SHALOM_API_ENABLED = true;
|
||||
|
||||
// Shalom API temporalmente desactivada: por ahora solo mostramos “VERIFICAR”
|
||||
const SHALOM_API_ENABLED = false;
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Logic for editable cells (existing logic)
|
||||
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)
|
||||
const getStatusMeta = (statusMessage) => {
|
||||
const upper = (statusMessage || '').toUpperCase();
|
||||
let badgeClass = 'bg-secondary';
|
||||
let whatsappClass = 'btn-secondary';
|
||||
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
// Logic for tracking modal (existing logic)
|
||||
return { badgeClass, whatsappClass };
|
||||
};
|
||||
|
||||
const setStatusCell = (cell, badgeClass, text, title = '', updatedAtText = '') => {
|
||||
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) => {
|
||||
const response = await fetch(`shalom_api.php?orderNumber=${encodeURIComponent(orderNumber)}&orderCode=${encodeURIComponent(orderCode)}`);
|
||||
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 '';
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
return formatDateTimeLabel(payload.checked_at);
|
||||
} catch (error) {
|
||||
console.warn('No se pudo guardar el estado Shalom en caché.', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
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 caché...');
|
||||
|
||||
const cachedUpdatedAt = await persistShalomStatus(pedidoId, statusMessage);
|
||||
if (cachedUpdatedAt) {
|
||||
setStatusCell(statusCell, badgeClass, statusMessage, '', cachedUpdatedAt);
|
||||
} else if (updatedFromProvider) {
|
||||
setStatusCell(statusCell, badgeClass, statusMessage, '', updatedFromProvider);
|
||||
}
|
||||
|
||||
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');
|
||||
@ -360,167 +569,160 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`shalom_api.php?orderNumber=${encodeURIComponent(orderNumber)}&orderCode=${encodeURIComponent(orderCode)}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(errorData => {
|
||||
throw new Error(errorData.error || `Error del servidor: ${response.status}`);
|
||||
});
|
||||
fetchShalomData(orderNumber, orderCode)
|
||||
.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, '"')}"><strong>Error:</strong> ${data.error}</p>`;
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
modalStatusDiv.innerHTML = `<p class="text-danger text-center"><strong>Error:</strong> ${data.error}</p>`;
|
||||
} else if (data.search && data.search.success) {
|
||||
const searchData = data.search.data;
|
||||
const statusData = data.statuses.data;
|
||||
const statusMessage = data.statuses.message || 'No disponible';
|
||||
const upperStatus = statusMessage.toUpperCase();
|
||||
|
||||
// Determine progress and description
|
||||
let progressWidth = 0;
|
||||
let activeStep = 0;
|
||||
let descriptionText = 'Su pedido está siendo procesado.';
|
||||
if (!(data && data.statuses && data.statuses.message)) {
|
||||
modalStatusDiv.innerHTML = '<p class="text-warning text-center">No se pudo encontrar la guía.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (upperStatus.includes('ENTREGADO')) {
|
||||
progressWidth = 100;
|
||||
activeStep = 4;
|
||||
descriptionText = 'El pedido ha sido entregado satisfactoriamente.';
|
||||
} else if (upperStatus.includes('DESTINO') || upperStatus.includes('REPARTO')) {
|
||||
progressWidth = 66;
|
||||
activeStep = 3;
|
||||
descriptionText = 'Está listo para su recojo.';
|
||||
} else if (upperStatus.includes('TRANSITO') || upperStatus.includes('TRÁNSITO')) {
|
||||
progressWidth = 33;
|
||||
activeStep = 2;
|
||||
descriptionText = 'Su pedido se encuentra en camino a la ciudad de destino.';
|
||||
} else {
|
||||
progressWidth = 0;
|
||||
activeStep = 1;
|
||||
descriptionText = 'El pedido ha sido recibido en nuestra agencia de origen.';
|
||||
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();
|
||||
}
|
||||
|
||||
// Format date
|
||||
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) {
|
||||
formattedDate = new Date(statusData.registrado.fecha).toLocaleString('es-PE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
const steps = ['En origen', 'En tránsito', 'En destino', 'Entregado'];
|
||||
let stepsHtml = '';
|
||||
steps.forEach((step, index) => {
|
||||
const stepNum = index + 1;
|
||||
const isActive = stepNum <= activeStep;
|
||||
|
||||
const steps = ['En origen', 'En tránsito', 'En destino', 'Entregado'];
|
||||
let stepsHtml = '';
|
||||
steps.forEach((step, index) => {
|
||||
const stepNum = index + 1;
|
||||
const isActive = stepNum <= activeStep;
|
||||
const isCurrent = 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; font-size: 18px;"></i>' : ''}
|
||||
</div>
|
||||
<span style="font-size: 1.1rem; font-weight: ${isCurrent ? 'bold' : 'normal'}; color: ${isActive ? '#1a1a6e' : '#999'}; display: block; white-space: nowrap;">${step}</span>
|
||||
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>
|
||||
`;
|
||||
});
|
||||
|
||||
let html = `
|
||||
<div style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #333; background: white; padding: 50px 60px; border-radius: 15px; border: 1px solid #eee; box-shadow: 0 4px 12px rgba(0,0,0,0.05);">
|
||||
<!-- Header Section -->
|
||||
<div class="d-flex justify-content-between align-items-start mb-3 flex-wrap">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="me-4">
|
||||
<!-- Illustration -->
|
||||
<svg width="180" height="180" viewBox="0 0 200 200">
|
||||
<!-- Dashed path -->
|
||||
<path d="M40 80C60 80 70 130 100 130C130 130 140 80 160 80" stroke="#94a3b8" stroke-width="3" stroke-dasharray="6 6" fill="none" />
|
||||
|
||||
<!-- Small pin in distance -->
|
||||
<g transform="translate(30, 65) scale(0.6)">
|
||||
<path d="M15 0C6.7 0 0 6.7 0 15C0 26 15 40 15 40C15 40 30 26 30 15C30 6.7 23.3 0 15 0Z" fill="#1a1a6e"/>
|
||||
<circle cx="15" cy="15" r="6" fill="white"/>
|
||||
</g>
|
||||
|
||||
<!-- The Box -->
|
||||
<g transform="translate(85, 95)">
|
||||
<!-- Front face -->
|
||||
<path d="M0 20 L55 38 V90 L0 72 Z" fill="#e30613"/>
|
||||
<!-- Side face -->
|
||||
<path d="M55 38 L95 22 V74 L55 90 Z" fill="#b90510"/>
|
||||
<!-- Top face -->
|
||||
<path d="M0 20 L40 0 L95 22 L55 38 Z" fill="#f14b55"/>
|
||||
|
||||
<!-- White stripe on top -->
|
||||
<path d="M18 9 L58 27 L78 36 L38 18 Z" fill="white"/>
|
||||
<!-- White stripe on side -->
|
||||
<path d="M75 30 L95 22 V35 L75 43 Z" fill="white"/>
|
||||
|
||||
<!-- SHALOM Text -->
|
||||
<text x="6" y="58" fill="white" font-family="Arial Black" font-weight="900" font-style="italic" font-size="11" transform="skewY(18)">SHALOM</text>
|
||||
</g>
|
||||
|
||||
<!-- Large pin on box -->
|
||||
<g transform="translate(108, 40)">
|
||||
<path d="M25 0C11.2 0 0 11.2 0 25C0 43.3 25 66.7 25 66.7C25 66.7 50 43.3 50 25C50 11.2 38.8 0 25 0Z" fill="#1a1a6e"/>
|
||||
<circle cx="25" cy="25" r="10" fill="white"/>
|
||||
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 mb-1">
|
||||
<h1 style="color: #e30613 !important; font-weight: 900; margin: 0; margin-right: 15px; text-transform: uppercase; font-size: 3.2rem;">${statusMessage.toUpperCase()}</h1>
|
||||
<span style="border: 3px solid #e30613; color: #e30613; border-radius: 25px; padding: 6px 20px; font-size: 1.2rem; font-weight: bold;">
|
||||
<i class="fas fa-download"></i> GRT
|
||||
</span>
|
||||
<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.3rem;">${descriptionText}</p>
|
||||
<p style="color: #e30613; font-weight: bold; font-size: 1.1rem; margin: 0;">
|
||||
<i class="fas fa-chevron-right"></i> Tiempo referencial de llegada 1 día.
|
||||
</p>
|
||||
<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-sm-end">
|
||||
<p class="mb-1" style="font-weight: bold; font-size: 1.2rem; color: #1a1a6e;">N° DE ORDEN: ${orderNumber}</p>
|
||||
<p class="text-muted" style="font-size: 1.1rem;">Desde el ${formattedDate}</p>
|
||||
<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>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="position-relative mb-5 mt-5" style="padding: 0 60px;">
|
||||
<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 30px;">
|
||||
<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>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<div class="row mt-5 pt-4 border-top">
|
||||
<div class="col-md-6 mb-2 mb-md-0">
|
||||
<p class="mb-1" style="font-weight: bold; color: #1a1a6e; font-size: 1.2rem;">Origen: <span style="color: #888; font-weight: normal;">${searchData.origen.nombre || 'N/A'}</span></p>
|
||||
<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">
|
||||
<p class="mb-1" style="font-weight: bold; color: #1a1a6e; font-size: 1.2rem;">Destino: <span style="color: #888; font-weight: normal;">${searchData.destino.nombre || 'N/A'}</span></p>
|
||||
<p class="text-muted mb-0" style="font-size: 1.1rem;">${searchData.destino.direccion || ''}</p>
|
||||
<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>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button class="btn btn-success btn-lg" id="btn-copy-summary">
|
||||
<i class="fab fa-whatsapp"></i> Copiar Resumen para Cliente
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalStatusDiv.innerHTML = html;
|
||||
|
||||
document.getElementById('btn-copy-summary').addEventListener('click', function() {
|
||||
copyStatusToClipboard(whatsappMessage);
|
||||
});
|
||||
} else {
|
||||
modalStatusDiv.innerHTML = `<p class="text-warning text-center">No se pudo encontrar la guía.</p>`;
|
||||
}
|
||||
${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);
|
||||
@ -528,8 +730,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// --- NEW ---
|
||||
function verificarEstados() {
|
||||
async function verificarEstados() {
|
||||
const rows = document.querySelectorAll('tr[data-order-number][data-order-code]');
|
||||
const verifyButton = document.getElementById('verify-statuses-btn');
|
||||
|
||||
@ -538,180 +739,75 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const pedidoId = row.id.split('-')[1];
|
||||
const statusCell = document.getElementById(`live-status-${pedidoId}`);
|
||||
if (!statusCell) return;
|
||||
statusCell.innerHTML = '<span class="badge bg-warning text-dark">VERIFICAR</span>';
|
||||
setStatusCell(statusCell, 'bg-warning text-dark', 'VERIFICAR');
|
||||
});
|
||||
|
||||
if (verifyButton) {
|
||||
verifyButton.disabled = false;
|
||||
verifyButton.innerHTML = 'Verificar Estados Ahora';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(verifyButton) {
|
||||
if (verifyButton) {
|
||||
verifyButton.disabled = true;
|
||||
verifyButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Verificando...';
|
||||
verifyButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Actualizando...';
|
||||
}
|
||||
|
||||
const setStatusCell = (cell, badgeClass, text, title = '') => {
|
||||
cell.innerHTML = '';
|
||||
const span = document.createElement('span');
|
||||
span.className = `badge ${badgeClass}`;
|
||||
span.textContent = text;
|
||||
if (title) span.title = title;
|
||||
cell.appendChild(span);
|
||||
};
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
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) return;
|
||||
|
||||
const reenableIfLast = () => {
|
||||
if (index === rows.length - 1 && verifyButton) {
|
||||
verifyButton.disabled = false;
|
||||
verifyButton.innerHTML = 'Verificar Estados Ahora';
|
||||
}
|
||||
};
|
||||
if (!statusCell) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (orderCode === '26') {
|
||||
setStatusCell(statusCell, 'bg-info', 'OLVA COURIER');
|
||||
reenableIfLast();
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!orderNumber || !orderCode || orderNumber === 'N/A' || orderCode === 'N/A') {
|
||||
setStatusCell(statusCell, 'bg-light text-dark', 'Sin datos');
|
||||
reenableIfLast();
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
setStatusCell(statusCell, 'bg-info text-dark', 'Verificando...');
|
||||
|
||||
fetch(`shalom_api.php?orderNumber=${encodeURIComponent(orderNumber)}&orderCode=${encodeURIComponent(orderCode)}`)
|
||||
.then(async response => {
|
||||
let rawText = '';
|
||||
try { rawText = await response.text(); } catch (e) { /* ignore */ }
|
||||
try {
|
||||
const data = await fetchShalomData(orderNumber, orderCode);
|
||||
|
||||
let data = null;
|
||||
if (rawText) {
|
||||
try { data = JSON.parse(rawText); } catch (e) { /* ignore */ }
|
||||
}
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
|
||||
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 !== '') {
|
||||
const details = data.details;
|
||||
detailsText = typeof details === 'string' ? details : JSON.stringify(details);
|
||||
} else if (rawText) {
|
||||
detailsText = rawText;
|
||||
}
|
||||
|
||||
// Devolvemos el error dentro de JSON para que el siguiente then lo pinte como Error
|
||||
return {
|
||||
error: msg,
|
||||
details: detailsText,
|
||||
http_code: response.status
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
.then(data => {
|
||||
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 errText = (data.error || '').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;
|
||||
|
||||
let badgeText = 'Error Shalom';
|
||||
if (codeMatch) {
|
||||
badgeText = `Shalom ${codeMatch[1]}`;
|
||||
} else if (codeMatch2) {
|
||||
badgeText = `Shalom ${codeMatch2[1]}`;
|
||||
} else {
|
||||
const isTimeout = /timed out|timeout/i.test(errText) || /timed out|timeout/i.test(details);
|
||||
const httpMatch = errText.match(/HTTP\s*(\d{3})/i);
|
||||
|
||||
if (isTimeout) {
|
||||
badgeText = 'Shalom timeout';
|
||||
} else if (httpMatch) {
|
||||
badgeText = `Shalom HTTP ${httpMatch[1]}`;
|
||||
} else if (/comunicaci[oó]n/i.test(errText)) {
|
||||
badgeText = 'Error API Shalom';
|
||||
} else {
|
||||
const shortErr = errText.length > 22 ? errText.slice(0, 22) + '…' : errText;
|
||||
badgeText = shortErr || 'Error Shalom';
|
||||
}
|
||||
}
|
||||
|
||||
setStatusCell(statusCell, 'bg-danger', badgeText, title);
|
||||
} else if (data && data.statuses && data.statuses.message) {
|
||||
const statusMessage = data.statuses.message;
|
||||
let badgeClass = 'bg-secondary';
|
||||
let whatsappClass = 'btn-secondary';
|
||||
|
||||
const upper = (statusMessage || '').toUpperCase();
|
||||
|
||||
if (upper.includes('EN DESTINO')) {
|
||||
badgeClass = 'bg-success';
|
||||
whatsappClass = 'btn-success';
|
||||
} else if (upper.includes('EN TRANSITO')) {
|
||||
badgeClass = 'bg-primary';
|
||||
} else if (upper.includes('REGISTRADO')) {
|
||||
badgeClass = 'bg-warning text-dark';
|
||||
}
|
||||
|
||||
setStatusCell(statusCell, badgeClass, statusMessage);
|
||||
|
||||
if (whatsappIcon) {
|
||||
whatsappIcon.classList.remove('btn-secondary', 'btn-success');
|
||||
whatsappIcon.classList.add(whatsappClass);
|
||||
}
|
||||
} 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);
|
||||
})
|
||||
.finally(() => {
|
||||
reenableIfLast();
|
||||
});
|
||||
}, index * 500); // Stagger requests to avoid overwhelming the server/API
|
||||
});
|
||||
await sleep(700);
|
||||
}
|
||||
} finally {
|
||||
if (verifyButton) {
|
||||
verifyButton.disabled = false;
|
||||
verifyButton.innerHTML = 'Actualizar Estados Shalom';
|
||||
}
|
||||
}
|
||||
}
|
||||
// Initial check on page load
|
||||
verificarEstados();
|
||||
|
||||
// Add event listener for the manual verification button
|
||||
const verifyButton = document.getElementById('verify-statuses-btn');
|
||||
if (verifyButton) {
|
||||
verifyButton.addEventListener('click', verificarEstados);
|
||||
}
|
||||
|
||||
// Copiar resumen desde el icono de la columna Celular
|
||||
document.addEventListener('click', function (e) {
|
||||
const btn = e.target && e.target.closest ? e.target.closest('.copy-summary-icon') : null;
|
||||
if (!btn) return;
|
||||
|
||||
@ -34,10 +34,6 @@ function getStatusStyle($status) {
|
||||
case 'RUTA_CONTRAENTREGA':
|
||||
$bgColor = '#007bff'; // primary blue
|
||||
break;
|
||||
case 'PENDIENTE':
|
||||
$bgColor = '#fff3cd'; // cream
|
||||
$style = 'color: #5f4700;';
|
||||
break;
|
||||
case 'ENTREGA EXITOSA':
|
||||
$bgColor = '#198754'; // dark green
|
||||
break;
|
||||
@ -136,7 +132,7 @@ if (!empty($period)) {
|
||||
}
|
||||
}
|
||||
|
||||
$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 IN ('RUTA_CONTRAENTREGA', 'PENDIENTE', 'NO CONTESTO, VOLVER A LLAMAR', 'NO CONTESTO, DEVOLVER LLAMADA', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA', 'RETORNADO')";
|
||||
$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 IN ('RUTA_CONTRAENTREGA', 'NO CONTESTO, VOLVER A LLAMAR', 'NO CONTESTO, DEVOLVER LLAMADA', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA', 'RETORNADO')";
|
||||
$params = [];
|
||||
|
||||
if ($user_role === 'Asesor') {
|
||||
@ -910,10 +906,6 @@ function getStatusStyleJS(status) {
|
||||
case 'RUTA_CONTRAENTREGA':
|
||||
bgColor = '#007bff';
|
||||
break;
|
||||
case 'PENDIENTE':
|
||||
bgColor = '#fff3cd';
|
||||
style = 'color: #5f4700;';
|
||||
break;
|
||||
case 'NO CONTESTO, VOLVER A LLAMAR':
|
||||
bgColor = '#fd7e14';
|
||||
break;
|
||||
@ -1042,7 +1034,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const options = [
|
||||
{val: 'RUTA_CONTRAENTREGA', text: 'RUTA_CONTRAENTREGA'},
|
||||
{val: 'PENDIENTE', text: 'PENDIENTE'},
|
||||
{val: 'NO CONTESTO, DEVOLVER LLAMADA', text: 'NO CONTESTO, DEVOLVER LLAMADA'},
|
||||
{val: 'CANCELADO', text: 'CANCELADO'},
|
||||
{val: 'REPROGRAMADO', text: 'REPROGRAMADO'},
|
||||
@ -1057,10 +1048,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
option.style.backgroundColor = '#8b1e2d';
|
||||
option.style.color = '#ffffff';
|
||||
}
|
||||
if (opt.val === 'PENDIENTE') {
|
||||
option.style.backgroundColor = '#fff3cd';
|
||||
option.style.color = '#5f4700';
|
||||
}
|
||||
if (opt.val === currentVal) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
346
shalom_api.php
346
shalom_api.php
@ -15,12 +15,33 @@ function respond_json(int $statusCode, array $payload, int $jsonOptions): void {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Inputs
|
||||
$orderNumber = trim($_GET['orderNumber'] ?? '');
|
||||
$orderCode = trim($_GET['orderCode'] ?? '');
|
||||
function build_public_tracking_url(string $orderNumber, string $orderCode): string {
|
||||
return 'https://shalom.com.pe/rastrea/' . rawurlencode($orderNumber) . '/' . rawurlencode($orderCode);
|
||||
}
|
||||
|
||||
if ($orderNumber === '' || $orderCode === '') {
|
||||
respond_json(400, ['error' => 'Número de orden y código de orden son requeridos.'], $jsonOptions);
|
||||
function parse_shalom_tracking_date(?string $rawText): array {
|
||||
$rawText = trim((string) $rawText);
|
||||
if ($rawText === '') {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$cleanText = preg_replace('/^Desde el\s*/iu', '', $rawText);
|
||||
$cleanText = is_string($cleanText) ? trim($cleanText) : $rawText;
|
||||
|
||||
$timezone = new DateTimeZone('America/Lima');
|
||||
$formats = [
|
||||
'd/m/y \a \l\a\s H:i',
|
||||
'd/m/Y \a \l\a\s H:i',
|
||||
];
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$dt = DateTimeImmutable::createFromFormat($format, $cleanText, $timezone);
|
||||
if ($dt instanceof DateTimeImmutable) {
|
||||
return [$dt->format(DATE_ATOM), $cleanText];
|
||||
}
|
||||
}
|
||||
|
||||
return [null, $cleanText];
|
||||
}
|
||||
|
||||
// Load env from executor/.env (helps when Apache/PHP doesn't export .env vars)
|
||||
@ -57,12 +78,206 @@ function load_dotenv_if_needed(array $keys): void {
|
||||
}
|
||||
}
|
||||
|
||||
function resolve_binary(array $candidates, string $fallback): string {
|
||||
foreach ($candidates as $candidate) {
|
||||
if (is_executable($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
function run_shalom_web_scraper(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_scraper.js';
|
||||
if (!is_readable($scriptPath)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'No se encontró el scraper local 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),
|
||||
'50s',
|
||||
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' => 'El rastreo público 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 rastreo público 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'] ?? '');
|
||||
|
||||
return [
|
||||
'source' => 'shalom_web',
|
||||
'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' => $scraped['description'] ?? '',
|
||||
'requires_login' => !empty($scraped['requires_login']),
|
||||
],
|
||||
],
|
||||
'statuses' => [
|
||||
'message' => $scraped['status'] ?? 'No disponible',
|
||||
'data' => [
|
||||
'registrado' => [
|
||||
'fecha' => $dateIso,
|
||||
'fecha_text' => $dateText,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function call_official_shalom_api(string $orderNumber, string $orderCode, ?string $apiKey): array {
|
||||
if (!$apiKey) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Falta SHALOM_API_KEY.',
|
||||
];
|
||||
}
|
||||
|
||||
$url = 'https://shalom-api.lat/api/track';
|
||||
$postData = [
|
||||
'orderNumber' => $orderNumber,
|
||||
'orderCode' => $orderCode,
|
||||
];
|
||||
|
||||
$jsonData = json_encode($postData, JSON_UNESCAPED_UNICODE);
|
||||
if ($jsonData === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Error serializando la solicitud a la API de Shalom.',
|
||||
];
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $jsonData,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
"Authorization: Bearer {$apiKey}",
|
||||
],
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 25,
|
||||
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 en la comunicación con la API de Shalom.',
|
||||
'details' => $curlError,
|
||||
];
|
||||
}
|
||||
|
||||
if ($response === false || $response === null || $response === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Shalom no devolvió respuesta por la API.',
|
||||
'details' => 'HTTP ' . ($httpCode ?: 'desconocido'),
|
||||
];
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
$message = null;
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$message = $decoded['message'] ?? $decoded['error'] ?? null;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $message ? 'Error API Shalom: ' . $message : 'Error API Shalom HTTP ' . $httpCode . '.',
|
||||
'details' => is_array($decoded) ? $decoded : $response,
|
||||
];
|
||||
}
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'La API de Shalom devolvió una respuesta inválida.',
|
||||
'details' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $decoded,
|
||||
];
|
||||
}
|
||||
|
||||
// Inputs
|
||||
$orderNumber = trim($_GET['orderNumber'] ?? '');
|
||||
$orderCode = trim($_GET['orderCode'] ?? '');
|
||||
|
||||
if ($orderNumber === '' || $orderCode === '') {
|
||||
respond_json(400, ['error' => 'Número de orden y código de orden son requeridos.'], $jsonOptions);
|
||||
}
|
||||
|
||||
load_dotenv_if_needed(['SHALOM_API_KEY']);
|
||||
|
||||
// API
|
||||
// API key fallback from DB (so the app can work even if you can't edit .env)
|
||||
$apiKey = getenv('SHALOM_API_KEY');
|
||||
|
||||
// Fallback: allow setting the key from the DB (so the app works even if you can't edit .env)
|
||||
if (!$apiKey) {
|
||||
try {
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
@ -70,95 +285,46 @@ if (!$apiKey) {
|
||||
$stmt = $pdo->prepare('SELECT valor FROM configuracion WHERE clave = ? LIMIT 1');
|
||||
$stmt->execute(['SHALOM_API_KEY']);
|
||||
$apiKey = $stmt->fetchColumn();
|
||||
if (is_string($apiKey)) {
|
||||
$apiKey = trim($apiKey);
|
||||
} else {
|
||||
$apiKey = null;
|
||||
}
|
||||
$apiKey = is_string($apiKey) ? trim($apiKey) : null;
|
||||
} catch (Throwable $e) {
|
||||
$apiKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$apiKey) {
|
||||
respond_json(500, [
|
||||
'error' => 'Falta SHALOM_API_KEY. Configura la clave en Configuración General (Administrador) o en tu .env para poder consultar el tracking.',
|
||||
], $jsonOptions);
|
||||
$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.
|
||||
$scraperAttempt = run_shalom_web_scraper($orderNumber, $orderCode);
|
||||
if (!empty($scraperAttempt['success'])) {
|
||||
respond_json(200, build_scraper_response($orderNumber, $orderCode, $scraperAttempt), $jsonOptions);
|
||||
}
|
||||
|
||||
$url = 'https://shalom-api.lat/api/track';
|
||||
|
||||
$postData = [
|
||||
'orderNumber' => $orderNumber,
|
||||
'orderCode' => $orderCode,
|
||||
];
|
||||
|
||||
$jsonData = json_encode($postData, JSON_UNESCAPED_UNICODE);
|
||||
if ($jsonData === false) {
|
||||
respond_json(500, ['error' => 'Error serializando la solicitud a Shalom.'], $jsonOptions);
|
||||
if (!empty($scraperAttempt['error'])) {
|
||||
$attemptErrors[] = 'Rastreo web: ' . $scraperAttempt['error'];
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $jsonData,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
"Authorization: Bearer {$apiKey}",
|
||||
],
|
||||
// Timeouts: evitamos que la UI quede esperando indefinidamente
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 25,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
respond_json(502, [
|
||||
'error' => 'Error en la comunicación con la API de Shalom',
|
||||
'details' => $curlError,
|
||||
], $jsonOptions);
|
||||
}
|
||||
|
||||
if ($response === false || $response === null || $response === '') {
|
||||
respond_json(502, [
|
||||
'error' => 'Shalom no devolvió respuesta',
|
||||
'http_code' => $httpCode ?: null,
|
||||
], $jsonOptions);
|
||||
}
|
||||
|
||||
// Error HTTP del proveedor
|
||||
if ($httpCode >= 400) {
|
||||
$errorBody = json_decode($response, true);
|
||||
$shMessage = null;
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($errorBody)) {
|
||||
$shMessage = $errorBody['message'] ?? $errorBody['error'] ?? null;
|
||||
if (!empty($scraperAttempt['details'])) {
|
||||
$details = is_string($scraperAttempt['details']) ? $scraperAttempt['details'] : json_encode($scraperAttempt['details'], $jsonOptions);
|
||||
if ($details) {
|
||||
$attemptErrors[] = 'Detalle web: ' . $details;
|
||||
}
|
||||
|
||||
if ($shMessage) {
|
||||
respond_json($httpCode, [
|
||||
'error' => "Error de la API de Shalom: {$shMessage}",
|
||||
'http_code' => $httpCode,
|
||||
'details' => $errorBody ?: $response,
|
||||
], $jsonOptions);
|
||||
}
|
||||
|
||||
respond_json($httpCode, [
|
||||
'error' => "Error de la API de Shalom (código {$httpCode}).",
|
||||
'http_code' => $httpCode,
|
||||
'details' => $response,
|
||||
], $jsonOptions);
|
||||
}
|
||||
|
||||
// OK: devolvemos tal cual lo que responde Shalom (la UI espera JSON)
|
||||
echo $response;
|
||||
$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);
|
||||
}
|
||||
if (!empty($officialAttempt['error'])) {
|
||||
$attemptErrors[] = 'API oficial: ' . $officialAttempt['error'];
|
||||
}
|
||||
if (!empty($officialAttempt['details'])) {
|
||||
$details = is_string($officialAttempt['details']) ? $officialAttempt['details'] : json_encode($officialAttempt['details'], $jsonOptions);
|
||||
if ($details) {
|
||||
$attemptErrors[] = 'Detalle API: ' . $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.',
|
||||
'tracking_url' => build_public_tracking_url($orderNumber, $orderCode),
|
||||
], $jsonOptions);
|
||||
|
||||
45
update_shalom_status.php
Normal file
45
update_shalom_status.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Acceso no autorizado.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$payload = json_decode(file_get_contents('php://input'), true);
|
||||
$pedidoId = isset($payload['id']) ? (int) $payload['id'] : 0;
|
||||
$status = trim((string) ($payload['status'] ?? ''));
|
||||
|
||||
if ($pedidoId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID de pedido inválido.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($status === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Estado Shalom vacío.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare('UPDATE pedidos SET estado_shalom = :estado_shalom, estado_shalom_actualizado_at = NOW() WHERE id = :id');
|
||||
$stmt->execute([
|
||||
':estado_shalom' => $status,
|
||||
':id' => $pedidoId,
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Estado Shalom guardado.',
|
||||
'checked_at' => date('Y-m-d H:i:s'),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'No se pudo guardar el estado Shalom.'], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user