Autosave: 20260806-194852

This commit is contained in:
Flatlogic Bot 2026-08-06 19:47:07 +00:00
parent e983920c84
commit a5e8a92a73
4 changed files with 309 additions and 213 deletions

View File

@ -14,7 +14,7 @@ $provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
$pageTitle = 'Pedidos Asignados | Bandeja de la asesora';
$pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con vistas de hoy, últimos 3 días hábiles, estados, historial y acciones.';
$pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con vistas de hoy, últimos 4 días hábiles, estados, historial y acciones.';
$kpiPeriodConfig = cc_test_resolve_kpi_period((string) ($_GET['kpi_period'] ?? 'today'), (string) ($_GET['kpi_date'] ?? ''));
$kpiPeriodKey = (string) ($kpiPeriodConfig['key'] ?? 'today');
@ -386,8 +386,8 @@ $viewCards = $isTuaniRecuperablesStore ? [
'active_class' => 'bg-primary-subtle border border-primary',
],
'ultimos_3_dias' => [
'label' => 'Últimos 3 días hábiles',
'heading' => 'Pedidos asignados de los últimos 3 días hábiles',
'label' => 'Últimos 4 días hábiles',
'heading' => 'Pedidos asignados de los últimos 4 días hábiles',
'stat_key' => 'ultimos_3_dias',
'active_class' => 'bg-info-subtle border border-info',
],
@ -456,8 +456,8 @@ $_SESSION[cc_test_callcenter_store_session_key()] = cc_test_normalize_context_ke
$_SESSION[cc_test_callcenter_view_session_key()] = cc_test_normalize_context_key($view);
$viewStatesNote = $isTuaniRecuperablesStore
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja. Si el teléfono ya existe en <strong>TUANI principal</strong>, se marca en gris como <strong>Pedido repetido en tienda</strong> solo en esta bandeja.'
: '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 3 para todos los pedidos; desde ese momento el número queda visible para llamar y gestionar, y el sustento solo se usa para mover el pedido a Promo Final. Además, esta bandeja principal incluye reparto intercalado diario: en modo automático reparte pedidos sin asignar entre asesoras activas; en modo manual el supervisor los mueve pedido por pedido. También puedes usar la vista <strong>Últimos 3 días hábiles</strong> para concentrar el seguimiento reciente.';
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja. Si el teléfono ya existe en <strong>TUANI principal</strong>, se marca en gris como <strong>Pedido repetido en tienda</strong> solo en esta bandeja. La vista <strong>Todos</strong> arranca en la fila 2000 y se pagina en bloques de 200 pedidos.'
: '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 para mover el pedido desde el Día 4; la imagen de sustento se puede cargar desde el Día 3 para dejarlo listo antes del pase. Además, esta bandeja principal incluye reparto intercalado diario: en modo automático reparte pedidos sin asignar entre asesoras activas; en modo manual el supervisor los mueve pedido por pedido. También puedes usar la vista <strong>Últimos 4 días hábiles</strong> para concentrar el seguimiento reciente.';
$errorMessage = null;
$noticeMessage = null;
@ -705,7 +705,7 @@ try {
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
$isAdmin = in_array($role, ['Administrador', 'admin'], true);
if ($isAdmin) {
$selectedAssessorFilter = cc_test_resolve_callcenter_assessor_filter($assessors, $_GET['assessor'] ?? null, true, true);
$selectedAssessorFilter = cc_test_resolve_callcenter_assessor_filter($assessors, $_GET['assessor'] ?? null, !$isTuaniRecuperablesStore, true);
} else {
unset($_SESSION[cc_test_callcenter_assessor_filter_session_key()]);
$selectedAssessorFilter = '';
@ -734,6 +734,7 @@ try {
$order['seguimiento_dia_info'] = $followupDayInfo;
$order['dias_seguimiento'] = $followupDayInfo['day_number'] ?? null;
$order['promo_final_habilitado'] = !empty($followupDayInfo['is_promo_final']);
$order['promo_final_evidencia_habilitada'] = !empty($followupDayInfo['can_upload_promo_final_evidence']);
$order['promo_final_evidencia_cargada'] = trim((string) ($order['promo_final_evidencia_path'] ?? '')) !== '';
$order['es_promo_final'] = $order['promo_final_habilitado'] && $order['promo_final_evidencia_cargada'];
$order['promo_final_pendiente'] = $order['promo_final_habilitado'] && !$order['promo_final_evidencia_cargada'];
@ -743,7 +744,7 @@ try {
$order['pendiente_logistica'] = $order['pendiente_logistica_destino'] !== null;
$order['es_nuevo_hoy'] = $assignedDate ? $assignedDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false;
$order['es_ultimos_3_dias'] = cc_test_is_within_last_days_window($order, $todayStart, 3);
$order['es_ultimos_3_dias'] = cc_test_is_within_last_days_window($order, $todayStart, 4);
$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);
@ -789,8 +790,6 @@ try {
$pendingPromoFinalCounts = cc_test_followup_pending_promo_final_counts($orders);
$orders = cc_test_mark_recoverables_duplicate_orders($pdo, $orders, $storeKey);
$performanceOrders = $orders;
if ($selectedAssessorFilter !== '') {
$performanceOrders = array_values(array_filter($orders, static function (array $order) use ($selectedAssessorFilter): bool {
@ -989,7 +988,9 @@ try {
$aSourceRow = (int) ($a['source_row'] ?? 0);
$bSourceRow = (int) ($b['source_row'] ?? 0);
if ($aSourceRow !== $bSourceRow) {
return $bSourceRow <=> $aSourceRow;
return cc_test_is_tuani_recoverables_store_key($storeKey)
? ($aSourceRow <=> $bSourceRow)
: ($bSourceRow <=> $aSourceRow);
}
$aId = (int) ($a['id'] ?? 0);
@ -1022,7 +1023,7 @@ try {
$assessorInboxLocked = $assessorInboxLockedCount > 0;
$assessorInboxLockedView = $assessorInboxLocked && $view === 'nuevos_hoy';
$assessorInboxLockNotice = $assessorInboxLocked
? 'Tienes ' . $assessorInboxLockedCount . ' pedido' . ($assessorInboxLockedCount === 1 ? '' : 's') . ' pendiente' . ($assessorInboxLockedCount === 1 ? '' : 's') . ' de Promo Final. Mientras existan pendientes, los pedidos de Día 1 mantienen el número oculto; cuando los muevas todos a Promo Final, el celular volverá a mostrarse.'
? 'Tienes ' . $assessorInboxLockedCount . ' pedido' . ($assessorInboxLockedCount === 1 ? '' : 's') . ' pendiente' . ($assessorInboxLockedCount === 1 ? '' : 's') . ' de Promo Final. Mientras existan pendientes de sustento, los pedidos de Día 1 mantienen el número oculto; cuando ya no queden pendientes, el celular volverá a mostrarse.'
: '';
$visibleOrdersTotalCount = count($visibleOrders);
@ -1033,45 +1034,46 @@ try {
$todosPageEnd = 0;
$todosPaginationPages = [];
if ($view === 'todos') {
$requestedPage = (int) ($_GET['page'] ?? 1);
if ($requestedPage < 1) {
$requestedPage = 1;
}
$todosTotalPages = max(1, (int) ceil($visibleOrdersTotalCount / $todosPageSize));
if ($requestedPage > $todosTotalPages) {
$requestedPage = $todosTotalPages;
}
$todosPage = $requestedPage;
$todosPageStart = ($todosPage - 1) * $todosPageSize;
$visibleOrders = array_slice($visibleOrders, $todosPageStart, $todosPageSize);
$todosPageEnd = $todosPageStart + count($visibleOrders);
if ($todosTotalPages <= 7) {
$todosPaginationPages = range(1, $todosTotalPages);
} else {
$todosPaginationPages[] = 1;
$windowStart = max(2, $todosPage - 1);
$windowEnd = min($todosTotalPages - 1, $todosPage + 1);
if ($windowStart > 2) {
$todosPaginationPages[] = 'ellipsis';
}
for ($pageNumber = $windowStart; $pageNumber <= $windowEnd; $pageNumber++) {
$todosPaginationPages[] = $pageNumber;
}
if ($windowEnd < $todosTotalPages - 1) {
$todosPaginationPages[] = 'ellipsis';
}
$todosPaginationPages[] = $todosTotalPages;
}
$requestedPage = (int) ($_GET['page'] ?? 1);
if ($requestedPage < 1) {
$requestedPage = 1;
}
$todosTotalPages = max(1, (int) ceil($visibleOrdersTotalCount / $todosPageSize));
if ($requestedPage > $todosTotalPages) {
$requestedPage = $todosTotalPages;
}
$todosPage = $requestedPage;
$todosPageStart = ($todosPage - 1) * $todosPageSize;
$visibleOrders = array_slice($visibleOrders, $todosPageStart, $todosPageSize);
$todosPageEnd = $todosPageStart + count($visibleOrders);
if ($isTuaniRecuperablesStore) {
$visibleOrders = cc_test_mark_recoverables_duplicate_orders($pdo, $visibleOrders, $storeKey);
}
if ($todosTotalPages <= 7) {
$todosPaginationPages = range(1, $todosTotalPages);
} else {
$todosPaginationPages[] = 1;
$windowStart = max(2, $todosPage - 1);
$windowEnd = min($todosTotalPages - 1, $todosPage + 1);
if ($windowStart > 2) {
$todosPaginationPages[] = 'ellipsis';
}
for ($pageNumber = $windowStart; $pageNumber <= $windowEnd; $pageNumber++) {
$todosPaginationPages[] = $pageNumber;
}
if ($windowEnd < $todosTotalPages - 1) {
$todosPaginationPages[] = 'ellipsis';
}
$todosPaginationPages[] = $todosTotalPages;
}
} catch (Throwable $exception) {
$errorMessage = $exception->getMessage();
}
@ -1118,7 +1120,7 @@ if ($isTuaniRecuperablesStore) {
$pageDescription = 'Bandeja compartida para que cualquier asesora gestione carritos recuperables con vistas de seguimiento y observados desde Drive.';
} else {
$pageTitle = 'Pedidos Asignados | Bandeja de la asesora';
$pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con vistas de hoy, últimos 3 días hábiles, estados, historial y acciones.';
$pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con vistas de hoy, últimos 4 días hábiles, estados, historial y acciones.';
}
$callCenterSectionLabel = $isTuaniRecuperablesStore ? 'Carritos recuperables' : 'Pedidos Asignados';
@ -1495,7 +1497,7 @@ require_once 'layout_header.php';
<?php if ($assessorInboxLockedView): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border">Pedidos con sustento pendiente para Promo Final</span>
<?php endif; ?>
<?php if ($view === 'todos'): ?>
<?php if ($todosTotalPages > 1): ?>
<span class="badge bg-primary-subtle text-primary-emphasis border">Página <?php echo (int) $todosPage; ?> de <?php echo (int) $todosTotalPages; ?> · 200 por página</span>
<?php endif; ?>
</div>
@ -1506,9 +1508,9 @@ require_once 'layout_header.php';
<div class="alert alert-info border-info-subtle d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-0">
<div>
<div class="fw-semibold mb-1"><i class="bi bi-info-circle me-1"></i>Pedidos con sustento pendiente para Promo Final</div>
<div class="small mb-0"><?php echo htmlspecialchars($assessorInboxLockNotice); ?> Usa <strong>Últimos 3 días hábiles</strong> si necesitas completar ese sustento más rápido.</div>
<div class="small mb-0"><?php echo htmlspecialchars($assessorInboxLockNotice); ?> Usa <strong>Últimos 4 días hábiles</strong> si necesitas completar ese sustento más rápido.</div>
</div>
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'ultimos_3_dias']))); ?>" class="btn btn-outline-info btn-sm text-nowrap">Ir a Últimos 3 días</a>
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge($callCenterParams, ['view' => 'ultimos_3_dias']))); ?>" class="btn btn-outline-info btn-sm text-nowrap">Ir a Últimos 4 días hábiles</a>
</div>
</div>
<?php endif; ?>
@ -1558,7 +1560,7 @@ require_once 'layout_header.php';
};
$subirLogisticaButtonLabel = 'Subir pedido';
$subirLogisticaNoteClass = 'alert alert-secondary py-2 px-3 mt-2 mb-0';
$subirLogisticaNoteHtml = 'Selecciona <strong>CONFIRMADO CONTRAENTREGA</strong> o <strong>CONFIRMADO ENVIO</strong> para enviarlo a logística.';
$subirLogisticaNoteHtml = 'Selecciona <strong>CONFIRMADO CONTRAENTREGA</strong> o <strong>CONFIRMADO ENVIO</strong> y completa <strong>Confirmación de pedido</strong> (producto, cantidad y precio) para enviarlo a logística.';
if ($canShowTuaniLogisticaButton) {
if ($order['estado'] === 'CONFIRMADO CONTRAENTREGA') {
if (!empty($order['ruta_contraentrega_pedido_id'])) {
@ -1589,7 +1591,7 @@ require_once 'layout_header.php';
$phoneLocked = false;
$phoneLockMessage = (string) ($order['llamada_bloqueada_dia3_mensaje'] ?? '');
if ($phoneLocked && $phoneLockMessage === '') {
$phoneLockMessage = 'Celular oculto mientras completas los pendientes del Día 3.';
$phoneLockMessage = 'Celular oculto mientras completas los pendientes del Día 4.';
}
$phoneHiddenByDay1 = cc_test_followup_day1_phone_hidden($order, $pendingPromoFinalCounts);
$phoneRowHiddenLabel = 'Número oculto';
@ -1750,7 +1752,7 @@ require_once 'layout_header.php';
data-phone="<?php echo $phoneLocked ? '' : htmlspecialchars((string) ($order['celular'] ?? '')); ?>"
data-order-label="<?php echo htmlspecialchars(cc_test_order_label($order)); ?>"
data-client-name="<?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Cliente sin nombre')); ?>"
onclick="return registrarLlamada(event, this)"<?php echo $phoneLocked ? ' disabled aria-disabled="true" title="Llamada bloqueada hasta completar el sustento de Día 3"' : ''; ?>>
onclick="return registrarLlamada(event, this)"<?php echo $phoneLocked ? ' disabled aria-disabled="true" title="Llamada bloqueada hasta completar el sustento de Día 4"' : ''; ?>>
<i class="bi bi-telephone-outbound"></i> <?php echo $phoneLocked ? 'Llamada bloqueada' : 'Llamar / Gestionar'; ?>
</button>
<?php else: ?>
@ -1809,7 +1811,10 @@ require_once 'layout_header.php';
<?php endif; ?>
<?php $promoFinalProofPath = trim((string) ($order['promo_final_evidencia_path'] ?? '')); ?>
<?php $promoFinalMoveAvailable = !empty($order['promo_final_habilitado']) && in_array($order['estado'], cc_test_followup_tracking_states(), true); ?>
<?php if ($promoFinalMoveAvailable || $promoFinalProofPath !== ''): ?>
<?php $promoFinalEvidenceAvailable = !empty($order['promo_final_evidencia_habilitada']); ?>
<?php $promoFinalThresholdDay = (int) ($followupDayInfo['promo_final_threshold_day'] ?? 4); ?>
<?php $promoFinalEvidenceDay = (int) ($followupDayInfo['promo_final_evidence_day'] ?? max(1, $promoFinalThresholdDay - 1)); ?>
<?php if ($promoFinalMoveAvailable || $promoFinalEvidenceAvailable || $promoFinalProofPath !== ''): ?>
<div
class="mt-2 js-promo-final-controls"
id="promo-final-controls-<?php echo htmlspecialchars($order['source_key']); ?>"
@ -1818,16 +1823,18 @@ require_once 'layout_header.php';
data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>"
>
<div class="d-flex flex-wrap gap-2 align-items-center">
<button type="button" class="btn btn-outline-danger btn-sm px-3 <?php echo ($promoFinalMoveAvailable && $promoFinalProofPath === '') ? '' : 'd-none'; ?>" id="mover-promo-final-<?php echo htmlspecialchars($order['source_key']); ?>" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this, { moverPromoFinal: true })">Mover a Promo Final</button>
<button type="button" class="btn btn-outline-danger btn-sm px-3 <?php echo $promoFinalMoveAvailable ? '' : 'd-none'; ?>" id="mover-promo-final-<?php echo htmlspecialchars($order['source_key']); ?>" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this, { moverPromoFinal: true })">Mover a Promo Final</button>
<?php if ($promoFinalProofPath !== ''): ?>
<a href="<?php echo htmlspecialchars($promoFinalProofPath); ?>" class="btn btn-outline-success btn-sm" target="_blank" rel="noopener">Ver sustento Promo Final</a>
<?php endif; ?>
</div>
<div class="small mt-2 <?php echo $promoFinalProofPath !== '' ? 'text-success fw-semibold' : 'text-danger-emphasis'; ?>" id="mover-promo-final-note-<?php echo htmlspecialchars($order['source_key']); ?>">
<?php if ($promoFinalProofPath !== ''): ?>
Promo Final con sustento cargado<?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
Sustento de Promo Final cargado<?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
<?php elseif ((int) ($followupDayInfo['day_number'] ?? 0) < $promoFinalThresholdDay): ?>
Ya puedes cargar la imagen desde el Día <?php echo $promoFinalEvidenceDay; ?> para dejarlo listo; el movimiento a Promo Final sigue desde el Día <?php echo $promoFinalThresholdDay; ?>.
<?php else: ?>
Debes subir una imagen de sustento y luego usar este botón para moverlo a Promo Final.
Ya puedes moverlo a Promo Final; si aún no cargaste la imagen, súbela antes de usar el botón.
<?php endif; ?>
</div>
</div>
@ -1958,6 +1965,7 @@ require_once 'layout_header.php';
</div>
</div>
</div>
<div class="form-text mt-3">Antes de usar <strong>Subir pedido</strong>, completa producto, cantidad y precio en esta sección.</div>
</div>
<div class="row g-3 mb-4">
@ -2033,12 +2041,14 @@ require_once 'layout_header.php';
</div>
<?php $promoFinalProofPath = trim((string) ($order['promo_final_evidencia_path'] ?? '')); ?>
<?php $canceladoProofPath = trim((string) ($order['cancelado_evidencia_path'] ?? '')); ?>
<?php $mostrarPromoFinalProof = !empty($order['promo_final_habilitado']) || $promoFinalProofPath !== ''; ?>
<?php $promoFinalThresholdDay = (int) ($followupDayInfo['promo_final_threshold_day'] ?? 4); ?>
<?php $promoFinalEvidenceDay = (int) ($followupDayInfo['promo_final_evidence_day'] ?? max(1, $promoFinalThresholdDay - 1)); ?>
<?php $mostrarPromoFinalProof = !empty($order['promo_final_evidencia_habilitada']) || $promoFinalProofPath !== ''; ?>
<?php $mostrarCanceladoProof = $order['estado'] === 'CANCELADO' || $canceladoProofPath !== ''; ?>
<div class="col-12 js-promo-final-proof-group <?php echo $mostrarPromoFinalProof ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" data-promo-final-candidate="<?php echo !empty($order['promo_final_habilitado']) ? '1' : '0'; ?>" data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>">
<div class="col-12 js-promo-final-proof-group <?php echo $mostrarPromoFinalProof ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" data-promo-final-candidate="<?php echo !empty($order['promo_final_habilitado']) ? '1' : '0'; ?>" data-promo-final-evidence-ready="<?php echo !empty($order['promo_final_evidencia_habilitada']) ? '1' : '0'; ?>" data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>">
<label for="promo_final_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Imagen de sustento para Promo Final</label>
<input type="file" class="form-control" id="promo_final_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" accept=".jpg,.jpeg,.png,.webp">
<div class="form-text">Obligatoria al mover el pedido a <strong>Promo Final</strong> desde el Día <?php echo (int) ($followupDayInfo['promo_final_threshold_day'] ?? 3); ?>. La supervisora podrá revisarla.</div>
<div class="form-text">Puedes cargarla desde el Día <?php echo $promoFinalEvidenceDay; ?>. Para mover el pedido a <strong>Promo Final</strong> seguirá siendo obligatoria desde el Día <?php echo $promoFinalThresholdDay; ?>. La supervisora podrá revisarla.</div>
<?php if ($promoFinalProofPath !== ''): ?>
<div class="small text-success mt-2">
Sustento actual: <a href="<?php echo htmlspecialchars($promoFinalProofPath); ?>" target="_blank" rel="noopener">ver imagen</a><?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
@ -2203,7 +2213,7 @@ require_once 'layout_header.php';
<div class="modal-footer d-flex justify-content-between flex-wrap gap-2">
<div class="small text-muted">
<?php if ($canShowTuaniLogisticaButton): ?>
<strong>Guardar gestión</strong> solo guarda el historial. Para enviarlo a logística, usa el <strong>botón del encabezado</strong> junto al nombre del cliente.
<strong>Guardar gestión</strong> solo guarda el historial. Para enviarlo a logística, completa <strong>Confirmación de pedido</strong> (producto, cantidad y precio) y usa el <strong>botón del encabezado</strong> junto al nombre del cliente.
<?php else: ?>
Los cambios se guardan en la base local del módulo de prueba.
<?php endif; ?>
@ -2224,7 +2234,7 @@ require_once 'layout_header.php';
</tbody>
</table>
</div>
<?php if ($view === 'todos'): ?>
<?php if ($todosTotalPages > 1): ?>
<div class="border-top bg-light-subtle px-3 py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
<div class="small text-muted">
<?php if ($visibleOrdersTotalCount > 0): ?>
@ -2752,23 +2762,26 @@ function updateLogisticaButton(sourceKey) {
button.textContent = 'Subir pedido';
if (note) {
note.className = 'alert alert-secondary py-2 px-3 mt-2 mb-0';
note.textContent = 'Selecciona CONFIRMADO CONTRAENTREGA o CONFIRMADO ENVIO para enviarlo a logística.';
note.textContent = 'Selecciona CONFIRMADO CONTRAENTREGA o CONFIRMADO ENVIO y completa Confirmación de pedido (producto, cantidad y precio) para enviarlo a logística.';
}
}
const PROMO_FINAL_FOLLOWUP_STATES = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO', 'SE ENVIO NUMERO DE CUENTA'];
function updatePromoFinalControls(sourceKey) {
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
const followupStates = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO', 'SE ENVIO NUMERO DE CUENTA'];
const followupStates = PROMO_FINAL_FOLLOWUP_STATES;
document.querySelectorAll('.js-promo-final-controls[data-source-key="' + sourceKey + '"]').forEach(block => {
const isCandidate = block.dataset.promoFinalCandidate === '1';
const hasExisting = block.dataset.hasExisting === '1';
const canUsePromoFinal = isCandidate && followupStates.includes(estado);
block.classList.toggle('d-none', !hasExisting && !canUsePromoFinal);
const canUploadPromoFinalEvidence = block.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
block.classList.toggle('d-none', !hasExisting && !canUsePromoFinal && !canUploadPromoFinalEvidence);
const moveButton = block.querySelector('button[id^="mover-promo-final-"]');
if (moveButton) {
moveButton.classList.toggle('d-none', !canUsePromoFinal || hasExisting);
moveButton.classList.toggle('d-none', !canUsePromoFinal);
}
});
@ -2776,7 +2789,8 @@ function updatePromoFinalControls(sourceKey) {
const isCandidate = group.dataset.promoFinalCandidate === '1';
const hasExisting = group.dataset.hasExisting === '1';
const canUsePromoFinal = isCandidate && followupStates.includes(estado);
group.classList.toggle('d-none', !hasExisting && !canUsePromoFinal);
const canUploadPromoFinalEvidence = group.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
group.classList.toggle('d-none', !hasExisting && !canUsePromoFinal && !canUploadPromoFinalEvidence);
});
}
@ -2969,6 +2983,33 @@ function guardarGestion(sourceKey, trigger, options = {}) {
const cancelProofGroup = document.querySelector('.js-cancelado-proof-group[data-source-key="' + sourceKey + '"]');
const hasExistingPromoProof = promoProofGroup?.dataset.hasExisting === '1';
const hasExistingCancelProof = cancelProofGroup?.dataset.hasExisting === '1';
const followupStates = PROMO_FINAL_FOLLOWUP_STATES;
const canUploadPromoFinalEvidence = promoProofGroup?.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
if (subirLogistica) {
const confirmacionProducto = document.getElementById('confirmacion_producto-' + sourceKey);
const confirmacionCantidad = document.getElementById('confirmacion_cantidad-' + sourceKey);
const confirmacionPrecio = document.getElementById('confirmacion_precio-' + sourceKey);
const confirmacionFields = [
confirmacionProducto,
confirmacionCantidad,
confirmacionPrecio
];
const missingField = confirmacionFields.find(field => !(field?.value || '').trim());
if (missingField) {
alert('Completa producto, cantidad y precio en Confirmación de pedido antes de subir el pedido.');
missingField.focus();
if (typeof missingField.select === 'function') {
missingField.select();
}
return;
}
}
if (!moverPromoFinal && promoProofInput?.files?.[0] && !canUploadPromoFinalEvidence) {
alert('La imagen de sustento solo se puede cargar desde el Día 3.');
return;
}
if (moverPromoFinal && !hasExistingPromoProof && (!promoProofInput?.files || promoProofInput.files.length === 0)) {
alert('Debes subir una imagen de sustento antes de mover el pedido a Promo Final.');
@ -3017,7 +3058,7 @@ function guardarGestion(sourceKey, trigger, options = {}) {
body.append(key, value);
});
if (moverPromoFinal && promoProofInput?.files?.[0]) {
if ((moverPromoFinal || canUploadPromoFinalEvidence) && promoProofInput?.files?.[0]) {
body.append('promo_final_evidencia', promoProofInput.files[0]);
}
@ -3038,7 +3079,7 @@ function guardarGestion(sourceKey, trigger, options = {}) {
if (!data.success) {
throw new Error(data.message || 'No se pudo guardar la gestión');
}
if (subirLogistica || moverPromoFinal) {
if (subirLogistica || moverPromoFinal || (canUploadPromoFinalEvidence && promoProofInput?.files?.[0])) {
alert(data.message || 'Gestión actualizada correctamente.');
}
ccCallCenterPanelNavigate();

View File

@ -347,8 +347,8 @@ $_SESSION[cc_test_callcenter_store_session_key()] = cc_test_normalize_context_ke
$_SESSION[cc_test_callcenter_view_session_key()] = cc_test_normalize_context_key($view);
$viewStatesNote = $isTuaniRecuperablesStore
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja. Si el teléfono ya existe en <strong>TUANI principal</strong>, se marca en gris como <strong>Pedido repetido en tienda</strong> solo en esta bandeja.'
: '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 3 para todos los pedidos; desde ese momento el número se mantiene visible para llamar y gestionar, y el sustento solo se usa para mover el pedido a Promo Final. En las vistas <strong>Todos</strong> y <strong>Últimos 3 días hábiles</strong>, la llamada y la gestión siguen disponibles desde el panel.';
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja. Si el teléfono ya existe en <strong>TUANI principal</strong>, se marca en gris como <strong>Pedido repetido en tienda</strong> solo en esta bandeja. La vista <strong>Todos</strong> arranca en la fila 2000 y se pagina en bloques de 200 pedidos.'
: '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 para mover el pedido desde el Día 4; la imagen de sustento se puede cargar desde el Día 3 para dejarlo listo. En las vistas <strong>Todos</strong> y <strong>Últimos 4 días hábiles</strong>, la llamada y la gestión siguen disponibles desde el panel.';
$errorMessage = null;
$noticeMessage = null;
@ -912,6 +912,7 @@ try {
$order['seguimiento_dia_info'] = $followupDayInfo;
$order['dias_seguimiento'] = $followupDayInfo['day_number'] ?? null;
$order['promo_final_habilitado'] = !empty($followupDayInfo['is_promo_final']);
$order['promo_final_evidencia_habilitada'] = !empty($followupDayInfo['can_upload_promo_final_evidence']);
$order['promo_final_evidencia_cargada'] = trim((string) ($order['promo_final_evidencia_path'] ?? '')) !== '';
$order['es_promo_final'] = $order['promo_final_habilitado'] && $order['promo_final_evidencia_cargada'];
$order['promo_final_pendiente'] = $order['promo_final_habilitado'] && !$order['promo_final_evidencia_cargada'];
@ -973,8 +974,6 @@ try {
$pendingPromoFinalCounts = cc_test_followup_pending_promo_final_counts($orders);
$orders = cc_test_mark_recoverables_duplicate_orders($pdo, $orders, $storeKey);
$performanceOrders = $selectedAssessorFilter !== ''
? array_values(array_filter($orders, static function (array $order) use ($selectedAssessorFilter): bool {
return ($order['matches_assessor_filter'] ?? false) === true;
@ -1174,7 +1173,9 @@ try {
$aSourceRow = (int) ($a['source_row'] ?? 0);
$bSourceRow = (int) ($b['source_row'] ?? 0);
if ($aSourceRow !== $bSourceRow) {
return $bSourceRow <=> $aSourceRow;
return cc_test_is_tuani_recoverables_store_key($storeKey)
? ($aSourceRow <=> $bSourceRow)
: ($bSourceRow <=> $aSourceRow);
}
$aId = (int) ($a['id'] ?? 0);
@ -1201,45 +1202,46 @@ try {
$todosPageEnd = 0;
$todosPaginationPages = [];
if ($view === 'todos') {
$requestedPage = (int) ($_GET['page'] ?? 1);
if ($requestedPage < 1) {
$requestedPage = 1;
}
$todosTotalPages = max(1, (int) ceil($visibleOrdersTotalCount / $todosPageSize));
if ($requestedPage > $todosTotalPages) {
$requestedPage = $todosTotalPages;
}
$todosPage = $requestedPage;
$todosPageStart = ($todosPage - 1) * $todosPageSize;
$visibleOrders = array_slice($visibleOrders, $todosPageStart, $todosPageSize);
$todosPageEnd = $todosPageStart + count($visibleOrders);
if ($todosTotalPages <= 7) {
$todosPaginationPages = range(1, $todosTotalPages);
} else {
$todosPaginationPages[] = 1;
$windowStart = max(2, $todosPage - 1);
$windowEnd = min($todosTotalPages - 1, $todosPage + 1);
if ($windowStart > 2) {
$todosPaginationPages[] = 'ellipsis';
}
for ($pageNumber = $windowStart; $pageNumber <= $windowEnd; $pageNumber++) {
$todosPaginationPages[] = $pageNumber;
}
if ($windowEnd < $todosTotalPages - 1) {
$todosPaginationPages[] = 'ellipsis';
}
$todosPaginationPages[] = $todosTotalPages;
}
$requestedPage = (int) ($_GET['page'] ?? 1);
if ($requestedPage < 1) {
$requestedPage = 1;
}
$todosTotalPages = max(1, (int) ceil($visibleOrdersTotalCount / $todosPageSize));
if ($requestedPage > $todosTotalPages) {
$requestedPage = $todosTotalPages;
}
$todosPage = $requestedPage;
$todosPageStart = ($todosPage - 1) * $todosPageSize;
$visibleOrders = array_slice($visibleOrders, $todosPageStart, $todosPageSize);
$todosPageEnd = $todosPageStart + count($visibleOrders);
if ($isTuaniRecuperablesStore) {
$visibleOrders = cc_test_mark_recoverables_duplicate_orders($pdo, $visibleOrders, $storeKey);
}
if ($todosTotalPages <= 7) {
$todosPaginationPages = range(1, $todosTotalPages);
} else {
$todosPaginationPages[] = 1;
$windowStart = max(2, $todosPage - 1);
$windowEnd = min($todosTotalPages - 1, $todosPage + 1);
if ($windowStart > 2) {
$todosPaginationPages[] = 'ellipsis';
}
for ($pageNumber = $windowStart; $pageNumber <= $windowEnd; $pageNumber++) {
$todosPaginationPages[] = $pageNumber;
}
if ($windowEnd < $todosTotalPages - 1) {
$todosPaginationPages[] = 'ellipsis';
}
$todosPaginationPages[] = $todosTotalPages;
}
} catch (Throwable $exception) {
$errorMessage = $exception->getMessage();
}
@ -1908,7 +1910,7 @@ require_once 'layout_header.php';
<p class="text-muted small mb-1"><?php echo $viewStatesNote; ?></p>
</div>
<span class="badge bg-light text-dark border"><?php echo (int) $visibleOrdersTotalCount; ?> pedidos únicos en esta bandeja</span>
<?php if ($view === 'todos'): ?>
<?php if ($todosTotalPages > 1): ?>
<span class="badge bg-primary-subtle text-primary-emphasis border">Página <?php echo (int) $todosPage; ?> de <?php echo (int) $todosTotalPages; ?> · 200 por página</span>
<?php endif; ?>
</div>
@ -1979,7 +1981,7 @@ require_once 'layout_header.php';
};
$subirLogisticaButtonLabel = 'Subir pedido';
$subirLogisticaNoteClass = 'alert alert-secondary py-2 px-3 mt-2 mb-0';
$subirLogisticaNoteHtml = 'Selecciona <strong>CONFIRMADO CONTRAENTREGA</strong> o <strong>CONFIRMADO ENVIO</strong> para enviarlo a logística.';
$subirLogisticaNoteHtml = 'Selecciona <strong>CONFIRMADO CONTRAENTREGA</strong> o <strong>CONFIRMADO ENVIO</strong> y completa <strong>Confirmación de pedido</strong> (producto, cantidad y precio) para enviarlo a logística.';
if ($canShowTuaniLogisticaButton) {
if ($order['estado'] === 'CONFIRMADO CONTRAENTREGA') {
if (!empty($order['ruta_contraentrega_pedido_id'])) {
@ -2105,7 +2107,7 @@ require_once 'layout_header.php';
$phoneLocked = false;
$phoneLockMessage = (string) ($order['llamada_bloqueada_dia3_mensaje'] ?? '');
if ($phoneLocked && $phoneLockMessage === '') {
$phoneLockMessage = 'Celular oculto mientras completas los pendientes del Día 3.';
$phoneLockMessage = 'Celular oculto mientras completas los pendientes del Día 4.';
}
$phoneRowHiddenLabel = 'Número oculto';
$phoneRowHiddenTitle = 'Número oculto hasta completar Promo Final';
@ -2121,7 +2123,7 @@ require_once 'layout_header.php';
data-phone-hidden-day1="<?php echo $phoneHiddenByDay1 ? '1' : '0'; ?>"
data-order-label="<?php echo htmlspecialchars(cc_test_order_label($order)); ?>"
data-client-name="<?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Cliente sin nombre')); ?>"
onclick="return registrarLlamada(event, this)"<?php echo $phoneLocked ? ' disabled aria-disabled="true" title="Llamada bloqueada hasta completar el sustento de Día 3"' : ''; ?>>
onclick="return registrarLlamada(event, this)"<?php echo $phoneLocked ? ' disabled aria-disabled="true" title="Llamada bloqueada hasta completar el sustento de Día 4"' : ''; ?>>
<i class="bi bi-telephone-outbound"></i> <?php echo $phoneLocked ? 'Llamada bloqueada' : 'Llamar / Gestionar'; ?>
</button>
<?php else: ?>
@ -2267,7 +2269,10 @@ require_once 'layout_header.php';
<?php endif; ?>
<?php $promoFinalProofPath = trim((string) ($order['promo_final_evidencia_path'] ?? '')); ?>
<?php $promoFinalMoveAvailable = !empty($order['promo_final_habilitado']) && in_array($order['estado'], cc_test_followup_tracking_states(), true); ?>
<?php if ($promoFinalMoveAvailable || $promoFinalProofPath !== ''): ?>
<?php $promoFinalEvidenceAvailable = !empty($order['promo_final_evidencia_habilitada']); ?>
<?php $promoFinalThresholdDay = (int) ($followupDayInfo['promo_final_threshold_day'] ?? 4); ?>
<?php $promoFinalEvidenceDay = (int) ($followupDayInfo['promo_final_evidence_day'] ?? max(1, $promoFinalThresholdDay - 1)); ?>
<?php if ($promoFinalMoveAvailable || $promoFinalEvidenceAvailable || $promoFinalProofPath !== ''): ?>
<div
class="mt-2 js-promo-final-controls"
id="promo-final-controls-<?php echo htmlspecialchars($order['source_key']); ?>"
@ -2276,16 +2281,18 @@ require_once 'layout_header.php';
data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>"
>
<div class="d-flex flex-wrap gap-2 align-items-center">
<button type="button" class="btn btn-outline-danger btn-sm px-3 <?php echo ($promoFinalMoveAvailable && $promoFinalProofPath === '') ? '' : 'd-none'; ?>" id="mover-promo-final-<?php echo htmlspecialchars($order['source_key']); ?>" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this, { moverPromoFinal: true })">Mover a Promo Final</button>
<button type="button" class="btn btn-outline-danger btn-sm px-3 <?php echo $promoFinalMoveAvailable ? '' : 'd-none'; ?>" id="mover-promo-final-<?php echo htmlspecialchars($order['source_key']); ?>" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this, { moverPromoFinal: true })">Mover a Promo Final</button>
<?php if ($promoFinalProofPath !== ''): ?>
<a href="<?php echo htmlspecialchars($promoFinalProofPath); ?>" class="btn btn-outline-success btn-sm" target="_blank" rel="noopener">Ver sustento Promo Final</a>
<?php endif; ?>
</div>
<div class="small mt-2 <?php echo $promoFinalProofPath !== '' ? 'text-success fw-semibold' : 'text-danger-emphasis'; ?>" id="mover-promo-final-note-<?php echo htmlspecialchars($order['source_key']); ?>">
<?php if ($promoFinalProofPath !== ''): ?>
Promo Final con sustento cargado<?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
Sustento de Promo Final cargado<?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
<?php elseif ((int) ($followupDayInfo['day_number'] ?? 0) < $promoFinalThresholdDay): ?>
Ya puedes cargar la imagen desde el Día <?php echo $promoFinalEvidenceDay; ?> para dejarlo listo; el movimiento a Promo Final sigue desde el Día <?php echo $promoFinalThresholdDay; ?>.
<?php else: ?>
Debes subir una imagen de sustento y luego usar este botón para moverlo a Promo Final.
Ya puedes moverlo a Promo Final; si aún no cargaste la imagen, súbela antes de usar el botón.
<?php endif; ?>
</div>
</div>
@ -2418,6 +2425,7 @@ require_once 'layout_header.php';
</div>
</div>
</div>
<div class="form-text mt-3">Antes de usar <strong>Subir pedido</strong>, completa producto, cantidad y precio en esta sección.</div>
</div>
<div class="row g-3 mb-4">
@ -2496,12 +2504,14 @@ require_once 'layout_header.php';
</div>
<?php $promoFinalProofPath = trim((string) ($order['promo_final_evidencia_path'] ?? '')); ?>
<?php $canceladoProofPath = trim((string) ($order['cancelado_evidencia_path'] ?? '')); ?>
<?php $mostrarPromoFinalProof = !empty($order['promo_final_habilitado']) || $promoFinalProofPath !== ''; ?>
<?php $promoFinalThresholdDay = (int) ($followupDayInfo['promo_final_threshold_day'] ?? 4); ?>
<?php $promoFinalEvidenceDay = (int) ($followupDayInfo['promo_final_evidence_day'] ?? max(1, $promoFinalThresholdDay - 1)); ?>
<?php $mostrarPromoFinalProof = !empty($order['promo_final_evidencia_habilitada']) || $promoFinalProofPath !== ''; ?>
<?php $mostrarCanceladoProof = $order['estado'] === 'CANCELADO' || $canceladoProofPath !== ''; ?>
<div class="col-12 js-promo-final-proof-group <?php echo $mostrarPromoFinalProof ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" data-promo-final-candidate="<?php echo !empty($order['promo_final_habilitado']) ? '1' : '0'; ?>" data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>">
<div class="col-12 js-promo-final-proof-group <?php echo $mostrarPromoFinalProof ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" data-promo-final-candidate="<?php echo !empty($order['promo_final_habilitado']) ? '1' : '0'; ?>" data-promo-final-evidence-ready="<?php echo !empty($order['promo_final_evidencia_habilitada']) ? '1' : '0'; ?>" data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>">
<label for="promo_final_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Imagen de sustento para Promo Final</label>
<input type="file" class="form-control" id="promo_final_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" accept=".jpg,.jpeg,.png,.webp">
<div class="form-text">Obligatoria al mover el pedido a <strong>Promo Final</strong> desde el Día <?php echo (int) ($followupDayInfo['promo_final_threshold_day'] ?? 3); ?>. La supervisora podrá revisarla.</div>
<div class="form-text">Puedes cargarla desde el Día <?php echo $promoFinalEvidenceDay; ?>. Para mover el pedido a <strong>Promo Final</strong> seguirá siendo obligatoria desde el Día <?php echo $promoFinalThresholdDay; ?>. La supervisora podrá revisarla.</div>
<?php if ($promoFinalProofPath !== ''): ?>
<div class="small text-success mt-2">
Sustento actual: <a href="<?php echo htmlspecialchars($promoFinalProofPath); ?>" target="_blank" rel="noopener">ver imagen</a><?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
@ -2661,7 +2671,7 @@ require_once 'layout_header.php';
<div class="modal-footer d-flex justify-content-between flex-wrap gap-2">
<div class="small text-muted">
<?php if ($canShowTuaniLogisticaButton): ?>
<strong>Guardar gestión</strong> solo guarda el historial. Para enviarlo a logística, usa el <strong>botón del encabezado</strong> junto al nombre del cliente.
<strong>Guardar gestión</strong> solo guarda el historial. Para enviarlo a logística, completa <strong>Confirmación de pedido</strong> (producto, cantidad y precio) y usa el <strong>botón del encabezado</strong> junto al nombre del cliente.
<?php else: ?>
Los cambios se guardan en la base local del módulo de prueba.
<?php endif; ?>
@ -2687,7 +2697,7 @@ require_once 'layout_header.php';
</tbody>
</table>
</div>
<?php if ($view === 'todos'): ?>
<?php if ($todosTotalPages > 1): ?>
<div class="border-top bg-light-subtle px-3 py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
<div class="small text-muted">
<?php if ($visibleOrdersTotalCount > 0): ?>
@ -2703,7 +2713,7 @@ require_once 'layout_header.php';
<?php if ($todosPage <= 1): ?>
<span class="page-link">«</span>
<?php else: ?>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => 'todos', 'store' => $storeKey, 'page' => $todosPage - 1], $persistedAssessorParams))); ?>" aria-label="Anterior">«</a>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => $view, 'store' => $storeKey, 'page' => $todosPage - 1], $persistedAssessorParams))); ?>" aria-label="Anterior">«</a>
<?php endif; ?>
</li>
<?php foreach ($todosPaginationPages as $pageEntry): ?>
@ -2714,7 +2724,7 @@ require_once 'layout_header.php';
<?php if ((int) $pageEntry === $todosPage): ?>
<span class="page-link" aria-current="page"><?php echo (int) $pageEntry; ?></span>
<?php else: ?>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => 'todos', 'store' => $storeKey, 'page' => (int) $pageEntry], $persistedAssessorParams))); ?>"><?php echo (int) $pageEntry; ?></a>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => $view, 'store' => $storeKey, 'page' => (int) $pageEntry], $persistedAssessorParams))); ?>"><?php echo (int) $pageEntry; ?></a>
<?php endif; ?>
</li>
<?php endif; ?>
@ -2723,7 +2733,7 @@ require_once 'layout_header.php';
<?php if ($todosPage >= $todosTotalPages): ?>
<span class="page-link">»</span>
<?php else: ?>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => 'todos', 'store' => $storeKey, 'page' => $todosPage + 1], $persistedAssessorParams))); ?>" aria-label="Siguiente">»</a>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => $view, 'store' => $storeKey, 'page' => $todosPage + 1], $persistedAssessorParams))); ?>" aria-label="Siguiente">»</a>
<?php endif; ?>
</li>
</ul>
@ -3250,23 +3260,26 @@ function updateLogisticaButton(sourceKey) {
button.textContent = 'Subir pedido';
if (note) {
note.className = 'alert alert-secondary py-2 px-3 mt-2 mb-0';
note.textContent = 'Selecciona CONFIRMADO CONTRAENTREGA o CONFIRMADO ENVIO para enviarlo a logística.';
note.textContent = 'Selecciona CONFIRMADO CONTRAENTREGA o CONFIRMADO ENVIO y completa Confirmación de pedido (producto, cantidad y precio) para enviarlo a logística.';
}
}
const PROMO_FINAL_FOLLOWUP_STATES = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO', 'SE ENVIO NUMERO DE CUENTA'];
function updatePromoFinalControls(sourceKey) {
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
const followupStates = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO', 'SE ENVIO NUMERO DE CUENTA'];
const followupStates = PROMO_FINAL_FOLLOWUP_STATES;
document.querySelectorAll('.js-promo-final-controls[data-source-key="' + sourceKey + '"]').forEach(block => {
const isCandidate = block.dataset.promoFinalCandidate === '1';
const hasExisting = block.dataset.hasExisting === '1';
const canUsePromoFinal = isCandidate && followupStates.includes(estado);
block.classList.toggle('d-none', !hasExisting && !canUsePromoFinal);
const canUploadPromoFinalEvidence = block.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
block.classList.toggle('d-none', !hasExisting && !canUsePromoFinal && !canUploadPromoFinalEvidence);
const moveButton = block.querySelector('button[id^="mover-promo-final-"]');
if (moveButton) {
moveButton.classList.toggle('d-none', !canUsePromoFinal || hasExisting);
moveButton.classList.toggle('d-none', !canUsePromoFinal);
}
});
@ -3274,7 +3287,8 @@ function updatePromoFinalControls(sourceKey) {
const isCandidate = group.dataset.promoFinalCandidate === '1';
const hasExisting = group.dataset.hasExisting === '1';
const canUsePromoFinal = isCandidate && followupStates.includes(estado);
group.classList.toggle('d-none', !hasExisting && !canUsePromoFinal);
const canUploadPromoFinalEvidence = group.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
group.classList.toggle('d-none', !hasExisting && !canUsePromoFinal && !canUploadPromoFinalEvidence);
});
}
@ -3464,6 +3478,33 @@ function guardarGestion(sourceKey, trigger, options = {}) {
const cancelProofGroup = document.querySelector('.js-cancelado-proof-group[data-source-key="' + sourceKey + '"]');
const hasExistingPromoProof = promoProofGroup?.dataset.hasExisting === '1';
const hasExistingCancelProof = cancelProofGroup?.dataset.hasExisting === '1';
const followupStates = PROMO_FINAL_FOLLOWUP_STATES;
const canUploadPromoFinalEvidence = promoProofGroup?.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
if (subirLogistica) {
const confirmacionProducto = document.getElementById('confirmacion_producto-' + sourceKey);
const confirmacionCantidad = document.getElementById('confirmacion_cantidad-' + sourceKey);
const confirmacionPrecio = document.getElementById('confirmacion_precio-' + sourceKey);
const confirmacionFields = [
confirmacionProducto,
confirmacionCantidad,
confirmacionPrecio
];
const missingField = confirmacionFields.find(field => !(field?.value || '').trim());
if (missingField) {
alert('Completa producto, cantidad y precio en Confirmación de pedido antes de subir el pedido.');
missingField.focus();
if (typeof missingField.select === 'function') {
missingField.select();
}
return;
}
}
if (!moverPromoFinal && promoProofInput?.files?.[0] && !canUploadPromoFinalEvidence) {
alert('La imagen de sustento solo se puede cargar desde el Día 3.');
return;
}
if (moverPromoFinal && !hasExistingPromoProof && (!promoProofInput?.files || promoProofInput.files.length === 0)) {
alert('Debes subir una imagen de sustento antes de mover el pedido a Promo Final.');
@ -3512,7 +3553,7 @@ function guardarGestion(sourceKey, trigger, options = {}) {
body.append(key, value);
});
if (moverPromoFinal && promoProofInput?.files?.[0]) {
if ((moverPromoFinal || canUploadPromoFinalEvidence) && promoProofInput?.files?.[0]) {
body.append('promo_final_evidencia', promoProofInput.files[0]);
}
@ -3533,7 +3574,7 @@ function guardarGestion(sourceKey, trigger, options = {}) {
if (!data.success) {
throw new Error(data.message || 'No se pudo guardar la gestión');
}
if (subirLogistica || moverPromoFinal) {
if (subirLogistica || moverPromoFinal || (canUploadPromoFinalEvidence && promoProofInput?.files?.[0])) {
alert(data.message || 'Gestión actualizada correctamente.');
}
window.location.reload();

View File

@ -361,7 +361,7 @@ if (!function_exists('cc_test_mark_recoverables_duplicate_orders')) {
function cc_test_mark_recoverables_duplicate_orders(PDO $pdo, array $orders, string $storeKey, string $mainStoreKey = 'otra_tienda'): array
{
$storeKey = trim(mb_strtolower($storeKey));
if ($storeKey !== 'tuani_recuperables') {
if ($storeKey !== 'tuani_recuperables' || empty($orders)) {
return $orders;
}
@ -370,20 +370,57 @@ if (!function_exists('cc_test_mark_recoverables_duplicate_orders')) {
return $orders;
}
$mainPhoneIndex = cc_test_fetch_store_phone_index($pdo, $mainStoreKey);
foreach ($orders as &$order) {
$isDuplicate = false;
foreach (cc_test_phone_match_keys((string) ($order['celular'] ?? '')) as $phoneKey) {
if (isset($mainPhoneIndex[$phoneKey])) {
$isDuplicate = true;
break;
}
$targetPhoneMap = [];
foreach ($orders as $index => $order) {
$orders[$index]['pedido_repetido_en_tienda'] = false;
$orders[$index]['pedido_repetido_en_tienda_label'] = '';
$phoneKeys = cc_test_phone_match_keys((string) ($order['celular'] ?? ''));
if (empty($phoneKeys)) {
continue;
}
$order['pedido_repetido_en_tienda'] = $isDuplicate;
$order['pedido_repetido_en_tienda_label'] = $isDuplicate ? 'Pedido repetido en tienda' : '';
foreach ($phoneKeys as $phoneKey) {
$targetPhoneMap[$phoneKey][] = $index;
}
}
if (empty($targetPhoneMap)) {
return $orders;
}
try {
drive_test_ensure_orders_table($pdo);
$stmt = $pdo->prepare("SELECT celular FROM callcenter_test_orders WHERE store_key = ? AND celular IS NOT NULL AND celular <> ''");
$stmt->execute([$mainStoreKey]);
$remainingTargets = count($targetPhoneMap);
while (($phoneValue = $stmt->fetchColumn()) !== false) {
$phoneKeys = cc_test_phone_match_keys((string) $phoneValue);
if (empty($phoneKeys)) {
continue;
}
foreach ($phoneKeys as $phoneKey) {
if (!isset($targetPhoneMap[$phoneKey])) {
continue;
}
foreach ($targetPhoneMap[$phoneKey] as $orderIndex) {
$orders[$orderIndex]['pedido_repetido_en_tienda'] = true;
$orders[$orderIndex]['pedido_repetido_en_tienda_label'] = 'Pedido repetido en tienda';
}
unset($targetPhoneMap[$phoneKey]);
$remainingTargets--;
if ($remainingTargets <= 0) {
break 2;
}
}
}
} catch (Throwable $exception) {
error_log('cc_test_mark_recoverables_duplicate_orders: ' . $exception->getMessage());
}
unset($order);
return $orders;
}
@ -1368,7 +1405,12 @@ function cc_test_followup_is_from_promo_final_cutoff(array $order): bool
function cc_test_followup_promo_final_threshold_day(array $order): int
{
return 3;
return 4;
}
function cc_test_followup_promo_final_evidence_day(array $order): int
{
return max(1, cc_test_followup_promo_final_threshold_day($order) - 1);
}
function cc_test_followup_day_info(array $order): ?array
@ -1392,6 +1434,7 @@ function cc_test_followup_day_info(array $order): ?array
$dayNumber = $daysElapsed + 1;
$promoFinalThresholdDay = cc_test_followup_promo_final_threshold_day($order);
$promoFinalEvidenceDay = cc_test_followup_promo_final_evidence_day($order);
$badgeClass = 'bg-primary-subtle text-primary-emphasis border';
$textClass = 'text-primary-emphasis';
$description = 'Seguimiento dentro de la ventana óptima.';
@ -1408,7 +1451,7 @@ function cc_test_followup_day_info(array $order): ?array
$badgeClass = 'bg-warning-subtle text-warning-emphasis border';
$textClass = 'text-warning-emphasis';
$description = 'Último día de seguimiento antes de Promo Final.';
$noticeText = 'Último día de seguimiento. Mañana debe pasar a Promo Final con imagen de sustento.';
$noticeText = 'Último día de seguimiento. Ya puedes cargar la imagen de sustento; mañana se podrá mover a Promo Final.';
$rowNoticeClass = 'text-warning-emphasis';
} elseif ($dayNumber === 2) {
$badgeClass = 'bg-info-subtle text-info-emphasis border';
@ -1427,6 +1470,8 @@ function cc_test_followup_day_info(array $order): ?array
'row_notice_class' => $rowNoticeClass,
'started_at_label' => $startedAt->format('d/m/Y'),
'promo_final_threshold_day' => $promoFinalThresholdDay,
'promo_final_evidence_day' => $promoFinalEvidenceDay,
'can_upload_promo_final_evidence' => $dayNumber >= $promoFinalEvidenceDay,
'is_promo_final' => $dayNumber >= $promoFinalThresholdDay,
];
}

View File

@ -238,36 +238,12 @@ function cc_test_prepare_route_order_payload(array $sourceOrder, array $formData
throw new RuntimeException('Ingresa coordenadas válidas con el formato -12.082029, -77.069024.');
}
$productoBase = trim((string) ($formData['producto'] ?? ''));
if ($productoBase === '') {
$productoBase = trim((string) ($sourceOrder['producto'] ?? ''));
}
$cantidadBase = trim((string) ($formData['cantidad'] ?? ''));
if ($cantidadBase === '') {
$cantidadBase = trim((string) ($sourceOrder['cantidad'] ?? ''));
}
$precioBase = trim((string) ($formData['precio'] ?? ''));
if ($precioBase === '') {
$precioBase = trim((string) ($sourceOrder['precio'] ?? ''));
}
$productoPrincipal = trim((string) ($formData['confirmacion_producto'] ?? ''));
if ($productoPrincipal === '') {
$productoPrincipal = $productoBase;
}
$cantidadPrincipalRaw = trim((string) ($formData['confirmacion_cantidad'] ?? ''));
if ($cantidadPrincipalRaw === '') {
$cantidadPrincipalRaw = $cantidadBase;
}
$precioPrincipalRaw = trim((string) ($formData['confirmacion_precio'] ?? ''));
if ($precioPrincipalRaw === '') {
$precioPrincipalRaw = $precioBase;
}
if ($productoPrincipal === '') {
throw new RuntimeException('El pedido no tiene producto principal para subir a Ruta Contraentrega.');
if ($productoPrincipal === '' || $cantidadPrincipalRaw === '' || $precioPrincipalRaw === '') {
throw new RuntimeException('Para subir el pedido a Ruta Contraentrega completa producto, cantidad y precio en Confirmación de pedido.');
}
$cantidadPrincipal = cc_test_parse_quantity($cantidadPrincipalRaw);
@ -552,36 +528,12 @@ function cc_test_prepare_rotulado_order_payload(array $sourceOrder, array $formD
throw new RuntimeException('Debes ingresar un monto de adelanto válido antes de subir el pedido a Pedidos Rotulados.');
}
$productoBase = trim((string) ($formData['producto'] ?? ''));
if ($productoBase === '') {
$productoBase = trim((string) ($sourceOrder['producto'] ?? ''));
}
$cantidadBase = trim((string) ($formData['cantidad'] ?? ''));
if ($cantidadBase === '') {
$cantidadBase = trim((string) ($sourceOrder['cantidad'] ?? ''));
}
$precioBase = trim((string) ($formData['precio'] ?? ''));
if ($precioBase === '') {
$precioBase = trim((string) ($sourceOrder['precio'] ?? ''));
}
$productoPrincipal = trim((string) ($formData['confirmacion_producto'] ?? ''));
if ($productoPrincipal === '') {
$productoPrincipal = $productoBase;
}
$cantidadPrincipalRaw = trim((string) ($formData['confirmacion_cantidad'] ?? ''));
if ($cantidadPrincipalRaw === '') {
$cantidadPrincipalRaw = $cantidadBase;
}
$precioPrincipalRaw = trim((string) ($formData['confirmacion_precio'] ?? ''));
if ($precioPrincipalRaw === '') {
$precioPrincipalRaw = $precioBase;
}
if ($productoPrincipal === '') {
throw new RuntimeException('El pedido no tiene producto principal para subir a Pedidos Rotulados.');
if ($productoPrincipal === '' || $cantidadPrincipalRaw === '' || $precioPrincipalRaw === '') {
throw new RuntimeException('Para subir el pedido a Pedidos Rotulados completa producto, cantidad y precio en Confirmación de pedido.');
}
$cantidadPrincipal = cc_test_parse_quantity($cantidadPrincipalRaw);
@ -902,8 +854,9 @@ try {
if (!$isAdmin) {
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
$trackingUserId = $currentTracking ? (int) ($currentTracking['user_id'] ?? 0) : null;
$recoverablesAutoassignStates = ['SE ENVIO NUMERO DE CUENTA', 'OBSERVADO'];
$canClaimRecoverable = $isRecoverablesQueue && $trackingUserId === null && in_array($estado, $recoverablesAutoassignStates, true);
// En la bandeja de carritos recuperables, cualquier pedido sin asignar
// debe poder ser tomado por la asesora, sin importar el estado inicial.
$canClaimRecoverable = $isRecoverablesQueue && $trackingUserId === null;
if ($currentTracking === null) {
if (!$canClaimRecoverable) {
http_response_code(403);
@ -994,6 +947,19 @@ try {
throw new RuntimeException('Este pedido fue eliminado del panel por un administrador.');
}
if ($moverAPromoFinal || $promoFinalEvidenceFile !== null) {
$promoFinalSourceOrder = cc_test_fetch_source_order($pdo, $sourceKey);
if (!$promoFinalSourceOrder) {
throw new RuntimeException('No encontré el pedido base para gestionar el sustento de Promo Final.');
}
$promoFinalDayInfo = cc_test_followup_day_info(array_merge($promoFinalSourceOrder, [
'estado' => $estado,
'assigned_at' => $currentTracking['assigned_at'] ?? null,
]));
$promoFinalEvidenceCanUpload = !empty($promoFinalDayInfo['can_upload_promo_final_evidence']);
}
$userIdToSet = $isAdmin
? ($currentTracking ? ((int) ($currentTracking['user_id'] ?? 0) > 0 ? (int) $currentTracking['user_id'] : null) : null)
: (int) $_SESSION['user_id'];
@ -1066,17 +1032,12 @@ try {
throw new RuntimeException('Solo puedes mover a Promo Final pedidos que sigan en seguimiento.');
}
$sourceOrderForPromo = cc_test_fetch_source_order($pdo, $sourceKey);
if (!$sourceOrderForPromo) {
throw new RuntimeException('No encontré el pedido base para moverlo a Promo Final.');
if (!$promoFinalSourceOrder) {
throw new RuntimeException('No encontré el pedido base para gestionar el sustento de Promo Final.');
}
$promoFinalDayInfo = cc_test_followup_day_info(array_merge($sourceOrderForPromo, [
'estado' => $estado,
'assigned_at' => $currentTracking['assigned_at'] ?? null,
]));
if (empty($promoFinalDayInfo['is_promo_final'])) {
$promoFinalThresholdDay = (int) ($promoFinalDayInfo['promo_final_threshold_day'] ?? 3);
$promoFinalThresholdDay = (int) ($promoFinalDayInfo['promo_final_threshold_day'] ?? 4);
throw new RuntimeException('Este pedido aún no puede pasar a Promo Final. Recién se habilita desde el Día ' . $promoFinalThresholdDay . '.');
}
@ -1085,12 +1046,17 @@ try {
}
}
if ($promoFinalEvidenceFile !== null && !$moverAPromoFinal && !$promoFinalEvidenceCanUpload) {
$promoFinalEvidenceThresholdDay = (int) ($promoFinalDayInfo['promo_final_evidence_day'] ?? max(1, (int) ($promoFinalDayInfo['promo_final_threshold_day'] ?? 4) - 1));
throw new RuntimeException('La imagen de sustento solo se puede cargar desde el Día ' . $promoFinalEvidenceThresholdDay . '.');
}
if ($estado === 'CANCELADO' && $canceladoEvidenceFile === null && $canceladoEvidencePath === null) {
throw new RuntimeException('Debes subir una imagen de sustento para guardar el pedido en estado Cancelado.');
}
$actorUserId = (int) ($_SESSION['user_id'] ?? 0);
if ($moverAPromoFinal && $promoFinalEvidenceFile !== null) {
if ($promoFinalEvidenceFile !== null && ($moverAPromoFinal || $promoFinalEvidenceCanUpload)) {
$storedPromoFinalEvidence = cc_test_store_tracking_evidence_image($promoFinalEvidenceFile, 'promo_final', $sourceKey, $actorUserId);
$createdFiles[] = $storedPromoFinalEvidence['absolute_path'];
$promoFinalEvidencePath = $storedPromoFinalEvidence['relative_path'];
@ -1380,6 +1346,9 @@ try {
}
$message = 'Gestión actualizada correctamente.';
if ($promoFinalEvidenceFile !== null && !$moverAPromoFinal && $promoFinalEvidenceCanUpload) {
$message = 'Sustento de Promo Final cargado correctamente.';
}
if ($moverAPromoFinal && $promoFinalEvidencePath !== null) {
$message = 'Pedido movido a Promo Final correctamente.';
}