Autosave: 20260731-070936

This commit is contained in:
Flatlogic Bot 2026-07-31 07:08:01 +00:00
parent ccb58cbbb2
commit a902ed41a0
7 changed files with 148 additions and 31 deletions

View File

@ -437,6 +437,35 @@ h1, .h1 {
border: 1px solid #ff9800 !important;
}
/* Call center: badge para día de seguimiento */
.cc-followup-day-badge {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: .06rem;
min-width: 3.9rem;
min-height: 3.9rem;
padding: .4rem .45rem;
border-radius: .95rem;
text-align: center;
font-weight: 800;
line-height: 1;
letter-spacing: .02em;
box-shadow: 0 .45rem 1rem rgba(15, 23, 42, .08);
margin-bottom: .55rem;
}
.cc-followup-day-badge-label {
font-size: .62rem;
letter-spacing: .08em;
text-transform: uppercase;
}
.cc-followup-day-badge-number {
font-size: 1.15rem;
}
/* Call center: Estado de fila (sombreado completo) */
.cc-callcenter-row td {
background-color: var(--cc-row-bg, #ffffff) !important;

View File

@ -422,7 +422,7 @@ $_SESSION[cc_test_callcenter_view_session_key()] = cc_test_normalize_context_key
$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 4 y exige imagen de sustento para mover el pedido. 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: 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 pedidos nuevos desde el 29/07/2026 y desde el Día 4 para pedidos anteriores; cuando haya sustento pendiente en ese nuevo flujo, la bandeja principal queda visible pero bloqueada hasta completarlo. 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.';
$errorMessage = null;
$noticeMessage = null;
@ -979,6 +979,20 @@ try {
return cc_test_order_time($b) <=> cc_test_order_time($a);
});
$assessorInboxLockedOrders = [];
if (!$isAdmin && !$isTuaniRecuperablesStore) {
$assessorInboxLockedOrders = array_values(array_filter($orders, static function (array $order): bool {
return !empty($order['promo_final_pendiente'])
&& (int) ($order['seguimiento_dia_info']['promo_final_threshold_day'] ?? 4) === 3;
}));
}
$assessorInboxLockedCount = count($assessorInboxLockedOrders);
$assessorInboxLocked = $assessorInboxLockedCount > 0;
$assessorInboxLockedView = $assessorInboxLocked && $view === 'pendientes_hoy';
$assessorInboxLockNotice = $assessorInboxLocked
? 'Tienes ' . $assessorInboxLockedCount . ' pedido' . ($assessorInboxLockedCount === 1 ? '' : 's') . ' del nuevo flujo con sustento pendiente para Promo Final. La bandeja principal queda bloqueada hasta completar ese paso.'
: '';
$visibleOrdersTotalCount = count($visibleOrders);
$todosPageSize = 200;
$todosPage = 1;
@ -1340,6 +1354,9 @@ require_once 'layout_header.php';
<div class="card-body">
<div class="small text-uppercase <?php echo htmlspecialchars($labelClass); ?> mb-2"><?php echo htmlspecialchars((string) ($viewCard['label'] ?? $viewKey)); ?></div>
<div class="display-6 fw-bold mb-0"><?php echo (int) ($stats[$viewCard['stat_key']] ?? 0); ?></div>
<?php if ($viewKey === 'pendientes_hoy' && $assessorInboxLocked): ?>
<div class="small fw-semibold text-danger mt-2"><i class="bi bi-lock-fill me-1"></i>Bloqueado</div>
<?php endif; ?>
</div>
</article>
</a>
@ -1388,20 +1405,38 @@ require_once 'layout_header.php';
<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"><?php echo $viewStatesNote; ?></p>
<?php if ($assessorInboxLockedView): ?>
<p class="text-danger small mb-1"><i class="bi bi-lock-fill me-1"></i><?php echo htmlspecialchars($assessorInboxLockNotice); ?></p>
<?php else: ?>
<p class="text-muted small mb-1"><?php echo $viewStatesNote; ?></p>
<?php endif; ?>
</div>
<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 (int) $visibleOrdersTotalCount; ?> pedidos únicos en esta bandeja</span>
<?php if ($assessorInboxLockedView): ?>
<span class="badge bg-danger-subtle text-danger-emphasis border">Bloqueado por sustento de Día 3</span>
<?php endif; ?>
<?php if ($view === 'todos'): ?>
<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>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<?php if ($assessorInboxLockedView): ?>
<div class="p-3 pb-0">
<div class="alert alert-warning border-warning-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-lock-fill me-1"></i>Bandeja bloqueada hasta completar el sustento</div>
<div class="small mb-0"><?php echo htmlspecialchars($assessorInboxLockNotice); ?> Usa <strong>Últimos 3 días hábiles</strong> para subir la imagen y mover esos pedidos a Promo Final.</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-warning btn-sm text-nowrap">Ir a Últimos 3 días</a>
</div>
</div>
<?php endif; ?>
<div class="table-responsive <?php echo $assessorInboxLockedView ? 'opacity-50 pe-none user-select-none' : ''; ?>">
<table class="table table-hover align-top mb-0 cc-callcenter-table">
<thead class="table-light">
<tr>
@ -1479,8 +1514,11 @@ require_once 'layout_header.php';
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars($rowBackgroundStyle); ?>">
<td>
<?php if ($followupDayInfo !== null): ?>
<div class="small text-uppercase fw-semibold <?php echo htmlspecialchars($followupDayInfo['text_class']); ?> mb-1"><?php echo htmlspecialchars($followupDayInfo['display_label']); ?></div>
<?php endif; ?>
<div class="cc-followup-day-badge <?php echo htmlspecialchars($followupDayInfo['badge_class']); ?>" title="<?php echo htmlspecialchars($followupDayInfo['description']); ?>" aria-label="<?php echo htmlspecialchars($followupDayInfo['display_label']); ?>">
<span class="cc-followup-day-badge-label">DÍA</span>
<span class="cc-followup-day-badge-number"><?php echo htmlspecialchars($followupDayInfo['compact_label']); ?></span>
</div>
<?php endif; ?>
<div class="fw-bold fs-6"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
<div class="small text-muted">ID Drive: <?php echo htmlspecialchars(cc_test_display_value($order['import_id'] ?? null, 'Sin ID')); ?></div>
</td>
@ -1874,7 +1912,7 @@ require_once 'layout_header.php';
<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'; ?>">
<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 4. La supervisora podrá revisarla.</div>
<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'] ?? 4); ?>. 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; ?>.

View File

@ -218,9 +218,13 @@ if (isset($_SESSION['error_message'])) {
<div class="form-group mt-3">
<label for="imagen">Imagen de la Tarjeta</label>
<div id="image-preview" class="mt-2 mb-2"></div>
<input type="file" name="imagen" id="imagen" class="form-control-file">
<input type="file" name="imagen" id="imagen" class="form-control-file" accept="image/*">
<input type="hidden" name="current_imagen" id="current_imagen">
<small class="form-text text-muted">Sube una imagen para añadir o reemplazar la actual.</small>
<div id="remove-image-wrap" class="form-check mt-2" style="display:none;">
<input class="form-check-input" type="checkbox" name="remove_imagen" id="remove_imagen" value="1">
<label class="form-check-label" for="remove_imagen">Quitar la imagen actual</label>
</div>
<small class="form-text text-muted">Sube una imagen para añadir o reemplazar la actual, o marca la opción para eliminar la imagen existente.</small>
</div>
<div class="form-group mt-3">
<label for="column_id">Columna del Kanban</label>
@ -428,13 +432,19 @@ function editCard(card) {
document.getElementById('texto_informativo').value = card.texto_informativo;
document.getElementById('column_id').value = card.column_id;
document.getElementById('current_imagen').value = card.imagen_url;
const imagePreview = document.getElementById('image-preview');
const removeImageWrap = document.getElementById('remove-image-wrap');
const removeImageCheckbox = document.getElementById('remove_imagen');
imagePreview.innerHTML = '';
removeImageCheckbox.checked = false;
if (card.imagen_url) {
imagePreview.innerHTML = `<p class="mb-1">Imagen actual:</p><img src="${card.imagen_url}?t=${new Date().getTime()}" style="width: 100px; height: auto; border-radius: 5px;"/>`;
removeImageWrap.style.display = 'block';
} else {
removeImageWrap.style.display = 'none';
}
const formCard = document.querySelector('#form-title').closest('.card');
formCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
@ -444,7 +454,21 @@ function resetForm() {
document.getElementById('info-id').value = '';
document.querySelector('form[action="save_info_producto.php"]').reset();
document.getElementById('image-preview').innerHTML = '';
document.getElementById('remove-image-wrap').style.display = 'none';
document.getElementById('remove_imagen').checked = false;
}
document.getElementById('imagen').addEventListener('change', function () {
if (this.files.length > 0) {
document.getElementById('remove_imagen').checked = false;
}
});
document.getElementById('remove_imagen').addEventListener('change', function () {
if (this.checked) {
document.getElementById('imagen').value = '';
}
});
</script>
<?php require_once 'layout_footer.php'; ?>

View File

@ -320,7 +320,7 @@ $_SESSION[cc_test_callcenter_view_session_key()] = cc_test_normalize_context_key
$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 4 y exige imagen de sustento para mover el pedido.';
: '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 pedidos nuevos desde el 29/07/2026 y desde el Día 4 para pedidos anteriores; exige imagen de sustento para mover el pedido.';
$errorMessage = null;
$noticeMessage = null;
@ -2070,8 +2070,11 @@ require_once 'layout_header.php';
</td>
<td>
<?php if ($followupDayInfo !== null): ?>
<div class="small text-uppercase fw-semibold <?php echo htmlspecialchars($followupDayInfo['text_class']); ?> mb-1"><?php echo htmlspecialchars($followupDayInfo['display_label']); ?></div>
<?php endif; ?>
<div class="cc-followup-day-badge <?php echo htmlspecialchars($followupDayInfo['badge_class']); ?>" title="<?php echo htmlspecialchars($followupDayInfo['description']); ?>" aria-label="<?php echo htmlspecialchars($followupDayInfo['display_label']); ?>">
<span class="cc-followup-day-badge-label">DÍA</span>
<span class="cc-followup-day-badge-number"><?php echo htmlspecialchars($followupDayInfo['compact_label']); ?></span>
</div>
<?php endif; ?>
<div class="fw-bold fs-6"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
<div class="small text-muted">ID Drive: <?php echo htmlspecialchars(cc_test_display_value($order['import_id'] ?? null, 'Sin ID')); ?></div>
</td>
@ -2384,7 +2387,7 @@ require_once 'layout_header.php';
<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'; ?>">
<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 4. La supervisora podrá revisarla.</div>
<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'] ?? 4); ?>. 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; ?>.

View File

@ -1330,6 +1330,22 @@ function cc_test_followup_started_at(array $order): ?DateTimeImmutable
return null;
}
function cc_test_followup_promo_final_cutoff(): DateTimeImmutable
{
return new DateTimeImmutable('2026-07-29 00:00:00');
}
function cc_test_followup_promo_final_threshold_day(array $order): int
{
$startedAt = cc_test_followup_started_at($order);
if (!$startedAt instanceof DateTimeImmutable) {
return 4;
}
$cutoff = cc_test_followup_promo_final_cutoff();
return $startedAt->format('Y-m-d') >= $cutoff->format('Y-m-d') ? 3 : 4;
}
function cc_test_followup_day_info(array $order): ?array
{
$estado = cc_test_normalize_state((string) ($order['estado'] ?? ''));
@ -1350,28 +1366,29 @@ function cc_test_followup_day_info(array $order): ?array
}
$dayNumber = $daysElapsed + 1;
$promoFinalThresholdDay = cc_test_followup_promo_final_threshold_day($order);
$badgeClass = 'bg-primary-subtle text-primary-emphasis border';
$textClass = 'text-primary-emphasis';
$description = 'Seguimiento dentro de la ventana óptima.';
$noticeText = '';
$rowNoticeClass = '';
if ($dayNumber === 2) {
$badgeClass = 'bg-info-subtle text-info-emphasis border';
$textClass = 'text-info-emphasis';
$description = 'Aún está dentro de los 3 días de seguimiento.';
} elseif ($dayNumber === 3) {
$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.';
$rowNoticeClass = 'text-warning-emphasis';
} elseif ($dayNumber >= 4) {
if ($dayNumber >= $promoFinalThresholdDay) {
$badgeClass = 'bg-danger-subtle text-danger-emphasis border';
$textClass = 'text-danger-emphasis';
$description = 'Ya debe pasar a Promo Final.';
$noticeText = 'Este pedido lleva ' . $dayNumber . ' días y debe moverse a Promo Final con imagen de sustento.';
$rowNoticeClass = 'text-danger-emphasis';
} elseif ($dayNumber === max(1, $promoFinalThresholdDay - 1)) {
$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.';
$rowNoticeClass = 'text-warning-emphasis';
} elseif ($dayNumber === 2) {
$badgeClass = 'bg-info-subtle text-info-emphasis border';
$textClass = 'text-info-emphasis';
$description = 'Aún está dentro de los 3 días de seguimiento.';
}
return [
@ -1384,7 +1401,8 @@ function cc_test_followup_day_info(array $order): ?array
'notice_text' => $noticeText,
'row_notice_class' => $rowNoticeClass,
'started_at_label' => $startedAt->format('d/m/Y'),
'is_promo_final' => $dayNumber >= 4,
'promo_final_threshold_day' => $promoFinalThresholdDay,
'is_promo_final' => $dayNumber >= $promoFinalThresholdDay,
];
}

View File

@ -78,7 +78,8 @@ if (isset($_POST['banner_text'])) {
$producto_id = !empty($_POST['producto_id']) ? filter_input(INPUT_POST, 'producto_id', FILTER_VALIDATE_INT) : null;
$texto_informativo = $_POST['texto_informativo'] ?? '';
$column_id = filter_input(INPUT_POST, 'column_id', FILTER_VALIDATE_INT);
$current_imagen = $_POST['current_imagen'] ?? '';
$current_imagen = trim($_POST['current_imagen'] ?? '');
$remove_imagen = !empty($_POST['remove_imagen']) && $_POST['remove_imagen'] === '1';
$is_new_entry = empty($id);
$has_new_image = isset($_FILES['imagen']) && $_FILES['imagen']['error'] === UPLOAD_ERR_OK;
@ -89,11 +90,14 @@ if (isset($_POST['banner_text'])) {
exit;
}
$imagen_name = $current_imagen;
if (isset($_FILES['imagen']) && $_FILES['imagen']['error'] === UPLOAD_ERR_OK) {
if ($remove_imagen) {
if (!empty($current_imagen) && file_exists($current_imagen)) {
unlink($current_imagen);
}
$imagen_name = '';
} elseif ($has_new_image) {
$file = $_FILES['imagen'];
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file['type'], $allowedTypes)) {

View File

@ -1078,7 +1078,8 @@ try {
'import_id' => $sourceOrderForPromo['import_id'] ?? null,
]);
if (empty($promoFinalDayInfo['is_promo_final'])) {
throw new RuntimeException('Este pedido aún no puede pasar a Promo Final. Recién se habilita desde el Día 4.');
$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 . '.');
}
if ($promoFinalEvidenceFile === null && $promoFinalEvidencePath === null) {