Autosave: 20260715-044358

This commit is contained in:
Flatlogic Bot 2026-07-15 04:42:47 +00:00
parent 589f83f35c
commit bb9b9b3670
6 changed files with 543 additions and 133 deletions

View File

@ -168,6 +168,8 @@ if (!isset($availableStores[$storeKey])) {
$storeKey = 'flower';
}
$storeConfig = $availableStores[$storeKey];
$storeLabel = (string) ($storeConfig['label'] ?? strtoupper($storeKey));
$usesIncrementalSync = !empty($storeConfig['incremental_sync']);
$errorMessage = null;
$noticeMessage = null;
@ -177,8 +179,7 @@ $visibleOrders = [];
$modalsHtml = [];
$totalRows = 0;
$agregadosCount = 0;
$tuaniLastProcessedRow = null;
$loadLimit = $storeKey === 'otra_tienda' ? 0 : 100;
$lastProcessedRow = null;
$startRow = (int) ($storeConfig['startRow'] ?? 5552);
$catalogoProductos = [];
$stats = [
@ -205,10 +206,10 @@ try {
$catalogoProductos = [];
}
if ($storeKey === 'otra_tienda') {
if ($usesIncrementalSync) {
$syncInfo = drive_test_sync_orders_incremental($pdo, $storeKey, 0);
$totalRows = (int) ($syncInfo['drive_rows_total'] ?? 0);
$tuaniLastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0);
$lastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0);
$startRow = (int) ($syncInfo['next_start_row'] ?? $startRow);
$orders = drive_test_fetch_orders_from_db($pdo, $storeKey);
@ -220,7 +221,7 @@ try {
}
}
} else {
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$preview = drive_test_fetch_orders(100, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
@ -398,6 +399,18 @@ try {
return $aPendienteLogistica ? -1 : 1;
}
$aAssigned = cc_test_parse_datetime($a['assigned_at'] ?? null);
$bAssigned = cc_test_parse_datetime($b['assigned_at'] ?? null);
if ($aAssigned && $bAssigned && $aAssigned != $bAssigned) {
return $bAssigned <=> $aAssigned;
}
if ($aAssigned && !$bAssigned) {
return -1;
}
if (!$aAssigned && $bAssigned) {
return 1;
}
$aDue = cc_test_parse_datetime($a["proxima_llamada_at"] ?? null);
$bDue = cc_test_parse_datetime($b["proxima_llamada_at"] ?? null);
if ($aDue && $bDue && $aDue != $bDue) {
@ -455,7 +468,7 @@ require_once 'layout_header.php';
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Agregados: <?php echo (int) $agregadosCount; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($storeKey === 'otra_tienda' && $tuaniLastProcessedRow !== null): ?>TUANI: distribución desde fila <?php echo (int) ($storeConfig['startRow'] ?? $startRow); ?> · último procesado <?php echo (int) $tuaniLastProcessedRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($usesIncrementalSync && $lastProcessedRow !== null): ?><?php echo htmlspecialchars(mb_strtoupper($storeLabel)); ?>: distribución desde fila <?php echo (int) ($storeConfig['startRow'] ?? $startRow); ?> · último procesado <?php echo (int) $lastProcessedRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>

View File

@ -274,9 +274,15 @@ try {
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
$is_admin = in_array($user_role, ['Administrador', 'admin'], true);
$search_query = trim($_GET['q'] ?? '');
if (!$is_admin) {
http_response_code(403);
exit('Acceso denegado.');
}
// Exportar solo los pedidos con fecha de entrega "mañana"
$date_condition = "DATE(p.fecha_entrega) = DATE_ADD(CURDATE(), INTERVAL 1 DAY)";

View File

@ -169,6 +169,8 @@ if (!isset($availableStores[$storeKey])) {
$storeKey = 'flower';
}
$storeConfig = $availableStores[$storeKey];
$storeLabel = (string) ($storeConfig['label'] ?? strtoupper($storeKey));
$usesIncrementalSync = !empty($storeConfig['incremental_sync']);
$errorMessage = null;
$noticeMessage = null;
@ -179,8 +181,7 @@ $visibleOrders = [];
$modalsHtml = [];
$totalRows = 0;
$agregadosCount = 0;
$tuaniLastProcessedRow = null;
$loadLimit = $storeKey === 'otra_tienda' ? 0 : 100;
$lastProcessedRow = null;
$cupoObjetivoPorAsesora = 10;
$advisorOpenCounts = [];
@ -233,10 +234,10 @@ try {
}
}
if ($storeKey === 'otra_tienda') {
if ($usesIncrementalSync) {
$syncInfo = drive_test_sync_orders_incremental($pdo, $storeKey, 0);
$totalRows = (int) ($syncInfo['drive_rows_total'] ?? 0);
$tuaniLastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0);
$lastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0);
$startRow = (int) ($syncInfo['next_start_row'] ?? $startRow);
$orders = drive_test_fetch_orders_from_db($pdo, $storeKey);
@ -248,7 +249,7 @@ try {
}
}
} else {
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$preview = drive_test_fetch_orders(100, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
@ -311,6 +312,108 @@ try {
}));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'bulk_assign_assessor') {
if (empty($assessors)) {
throw new RuntimeException('No hay asesoras configuradas en el sistema.');
}
$targetValue = trim((string) ($_POST['target_assessor'] ?? ''));
$targetKey = trim(mb_strtoupper($targetValue));
$targetKey = preg_replace('/\s+/', ' ', $targetKey) ?? $targetKey;
$selectedAssessor = $targetKey !== '' ? ($assessors[$targetKey] ?? null) : null;
if (!$selectedAssessor) {
throw new RuntimeException('Selecciona la asesora para la asignación rápida.');
}
$targetUserId = (int) ($selectedAssessor['id'] ?? 0);
if ($targetUserId <= 0) {
throw new RuntimeException('La asesora seleccionada no es válida.');
}
$availableSourceKeys = array_fill_keys(array_column($orders, 'source_key'), true);
$sourceKeys = [];
foreach ((array) ($_POST['source_keys'] ?? []) as $sourceKeyRaw) {
$sourceKey = trim((string) $sourceKeyRaw);
if ($sourceKey === '' || !preg_match('/^[a-f0-9]{40}$/', $sourceKey)) {
continue;
}
if (!isset($availableSourceKeys[$sourceKey])) {
continue;
}
$sourceKeys[$sourceKey] = $sourceKey;
}
if (empty($sourceKeys)) {
throw new RuntimeException('Selecciona al menos un pedido válido para la asignación rápida.');
}
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
$stmtCurrent = $pdo->prepare("SELECT source_key, user_id FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
$stmtCurrent->execute(array_values($sourceKeys));
$currentAssignments = [];
while ($row = $stmtCurrent->fetch(PDO::FETCH_ASSOC)) {
$currentAssignments[(string) ($row['source_key'] ?? '')] = isset($row['user_id']) && (int) $row['user_id'] > 0
? (int) $row['user_id']
: null;
}
$assignedCount = 0;
$sameAssessorCount = 0;
$lockedCount = 0;
$pdo->beginTransaction();
try {
foreach ($sourceKeys as $sourceKey) {
$currentUserId = $currentAssignments[$sourceKey] ?? null;
if ($currentUserId !== null && (int) $currentUserId !== $targetUserId) {
$lockedCount++;
continue;
}
if ($currentUserId !== null && (int) $currentUserId === $targetUserId) {
$sameAssessorCount++;
continue;
}
cc_test_upsert_assignee($pdo, $sourceKey, $targetUserId);
$assignedCount++;
}
$pdo->commit();
} catch (Throwable $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
$messageParts = [];
if ($assignedCount > 0) {
$messageParts[] = 'Asignación rápida lista: ' . $assignedCount . ' pedido' . ($assignedCount === 1 ? '' : 's') . ' asignado' . ($assignedCount === 1 ? '' : 's') . ' a ' . $selectedAssessor['label'] . '.';
}
if ($sameAssessorCount > 0) {
$messageParts[] = $sameAssessorCount . ' ya estaba' . ($sameAssessorCount === 1 ? '' : 'n') . ' con esa misma asesora.';
}
if ($lockedCount > 0) {
$messageParts[] = $lockedCount . ' ya estaba' . ($lockedCount === 1 ? '' : 'n') . ' asignado' . ($lockedCount === 1 ? '' : 's') . ' a otra asesora y se dejó igual.';
}
$noticeMessage = !empty($messageParts)
? implode(' ', $messageParts)
: 'No hubo cambios en la asignación rápida.';
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
$orders = drive_test_merge_tracking($orders, $tracking);
$orders = array_values(array_filter($orders, static function (array $order): bool {
return empty($order['eliminado']);
}));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'auto_assign_cupo') {
$openStates = cc_test_open_states();
@ -671,6 +774,70 @@ try {
require_once 'layout_header.php';
?>
<style>
.cc-quick-select-btn {
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 999px;
}
.cc-callcenter-table tbody td {
padding-top: .5rem;
padding-bottom: .5rem;
}
.cc-callcenter-table .small {
line-height: 1.15;
}
.cc-callcenter-row.is-selected td:first-child {
box-shadow: inset 4px 0 0 #0d6efd;
}
.cc-callcenter-assign-form {
padding: .5rem !important;
}
.cc-callcenter-assign-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: .35rem;
}
.cc-callcenter-assign-row .form-select {
min-width: 11rem;
flex: 1 1 11rem;
}
.cc-callcenter-assign-submit {
flex: 0 0 auto;
white-space: nowrap;
}
.cc-callcenter-actions {
gap: .35rem !important;
}
.cc-callcenter-action-btn {
padding: .2rem .55rem;
line-height: 1.1;
font-size: .78rem;
white-space: nowrap;
}
@media (min-width: 992px) {
.cc-callcenter-table tbody td {
padding-top: .42rem;
padding-bottom: .42rem;
}
}
</style>
<main class="container-fluid py-4">
<section class="mb-4">
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3">
@ -692,7 +859,7 @@ require_once 'layout_header.php';
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Agregados: <?php echo (int) $agregadosCount; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($storeKey === 'otra_tienda' && $tuaniLastProcessedRow !== null): ?>TUANI: distribución desde fila <?php echo (int) ($storeConfig['startRow'] ?? $startRow); ?> · último procesado <?php echo (int) $tuaniLastProcessedRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($usesIncrementalSync && $lastProcessedRow !== null): ?><?php echo htmlspecialchars(mb_strtoupper($storeLabel)); ?>: distribución desde fila <?php echo (int) ($storeConfig['startRow'] ?? $startRow); ?> · último procesado <?php echo (int) $lastProcessedRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>
@ -882,22 +1049,45 @@ require_once 'layout_header.php';
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
</div>
<div class="card-body p-0">
<div class="border-bottom bg-light-subtle px-3 py-3">
<form method="post" id="bulkAssignForm" class="d-flex flex-column flex-xl-row align-items-xl-center gap-2 gap-xl-3">
<input type="hidden" name="action" value="bulk_assign_assessor">
<div id="bulkAssignSourceKeys"></div>
<div class="d-flex flex-wrap align-items-center gap-2">
<span class="badge text-bg-primary rounded-pill px-3 py-2"><span id="bulkSelectedCount">0</span> seleccionados</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="toggleSelectAllOrders"<?php echo empty($visibleOrders) ? ' disabled' : ''; ?>>Marcar visibles</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearSelectedOrders" disabled>Limpiar</button>
</div>
<div class="small text-muted">Usa el botón de la izquierda de cada pedido y asigna varios de una sola vez.</div>
<div class="d-flex flex-column flex-sm-row gap-2 ms-xl-auto">
<select name="target_assessor" class="form-select form-select-sm" style="min-width: 220px;"<?php echo empty($assessors) ? ' disabled' : ''; ?>>
<option value="">Elegir asesora</option>
<?php foreach ($assessors as $assessorKey => $assessor): ?>
<option value="<?php echo htmlspecialchars($assessorKey); ?>"><?php echo htmlspecialchars($assessor['label']); ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary" id="bulkAssignSubmit" disabled>Asignar seleccionados</button>
</div>
</form>
<div class="small text-muted mt-2">Si un pedido ya está tomado por otra asesora, el sistema lo deja igual y te avisa.</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<table class="table table-hover align-middle mb-0 cc-callcenter-table">
<thead class="table-light">
<tr>
<th class="text-center" style="width: 64px;">Sel.</th>
<th class="text-start" style="min-width: 16rem;">Asignación / acciones</th>
<th> Pedido</th>
<th>Cliente</th>
<th>Ubicación editable</th>
<th>Pedido</th>
<th>Gestión</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($visibleOrders)): ?>
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<td colspan="7" 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.
</td>
@ -950,6 +1140,123 @@ require_once 'layout_header.php';
ob_start();
?>
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars(cc_test_row_background_style((string) ($order['estado'] ?? 'POR LLAMAR'))); ?>">
<td class="text-center">
<div class="d-flex justify-content-center">
<button
type="button"
class="btn btn-sm btn-outline-primary cc-quick-select-btn js-quick-select-btn"
data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"
aria-pressed="false"
aria-label="Seleccionar pedido <?php echo htmlspecialchars(cc_test_order_label($order)); ?>"
title="Seleccionar este pedido para asignación rápida"
>
<i class="bi bi-plus-lg"></i>
</button>
<input type="checkbox" class="d-none js-bulk-order-checkbox" value="<?php echo htmlspecialchars($order['source_key']); ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
</div>
</td>
<td class="align-middle" style="min-width: 16rem;">
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
<?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;
}
}
}
$assignFormClass = 'border rounded-3 p-1 text-start shadow-sm cc-callcenter-assign-form';
if ($unassigned) {
$assignFormClass .= ' bg-light';
} elseif ($assignedAssessorKey === 'KARINA') {
$assignFormClass .= ' bg-primary-subtle border-primary';
} elseif ($assignedAssessorKey === 'ESTEFANYA') {
$assignFormClass .= ' bg-success-subtle border-success';
} elseif ($assignedAssessorKey === 'CARMEN') {
$assignFormClass .= ' bg-warning-subtle border-warning';
} else {
$assignFormClass .= ' bg-secondary-subtle border-secondary';
}
if ($unassigned) {
$selectedAssessorKeyForSelect = !empty($recommendedAssessorKey) ? (string) $recommendedAssessorKey : '';
} else {
$selectedAssessorKeyForSelect = $assignedAssessorKey !== null ? (string) $assignedAssessorKey : '';
}
$buttonLabel = $unassigned
? 'Asignar pedido'
: ($assignedAssessorKey !== null ? 'Guardar asignación' : 'Desasignar');
?>
<form method="post" class="<?php echo $assignFormClass; ?>">
<input type="hidden" name="action" value="assign_assessor">
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="d-flex justify-content-between align-items-center gap-2 mb-1">
<div class="small text-uppercase text-muted fw-semibold">Asignar a asesora</div>
<?php if ($unassigned): ?>
<span class="badge bg-light text-dark border">Sin asignar</span>
<?php elseif ($assignedAssessorKey === 'KARINA'): ?>
<span class="badge bg-primary-subtle text-primary-emphasis border">KARINA</span>
<?php elseif ($assignedAssessorKey === 'ESTEFANYA'): ?>
<span class="badge bg-success-subtle text-success-emphasis border">ESTEFANYA</span>
<?php elseif ($assignedAssessorKey === 'CARMEN'): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border">CARMEN</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary-emphasis border">Asignado</span>
<?php endif; ?>
</div>
<div class="cc-callcenter-assign-row">
<select name="target_assessor" class="form-select form-select-sm">
<option value=""<?php echo ($selectedAssessorKeyForSelect === '') ? ' selected' : ''; ?>>Sin asignar</option>
<?php if ($unassigned): ?>
<?php foreach ($assessors as $assessorKey => $assessor): ?>
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo ($selectedAssessorKeyForSelect === $assessorKey) ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
<?php endforeach; ?>
<?php elseif ($assignedAssessorKey !== null): ?>
<option value="<?php echo htmlspecialchars((string) $assignedAssessorKey); ?>" selected><?php echo htmlspecialchars((string) ($assessors[$assignedAssessorKey]['label'] ?? $assignedAssessorKey)); ?></option>
<?php endif; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary cc-callcenter-assign-submit cc-callcenter-action-btn"><?php echo htmlspecialchars((string) $buttonLabel); ?></button>
</div>
</form>
<?php endif; ?>
<div class="d-flex flex-column gap-1 align-items-stretch cc-callcenter-actions<?php echo in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true) ? ' mt-1' : ''; ?>">
<?php if (!empty($order['telefono_url'])): ?>
<button
type="button"
class="btn btn-sm btn-primary cc-callcenter-action-btn"
data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"
data-modal-id="<?php echo htmlspecialchars($modalId); ?>"
data-phone="<?php echo 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)">
<i class="bi bi-telephone-outbound"></i> Llamar / Gestionar
</button>
<?php else: ?>
<button type="button" class="btn btn-sm btn-primary cc-callcenter-action-btn text-nowrap" disabled>Sin teléfono</button>
<?php endif; ?>
<?php if (!empty($order['whatsapp_url'])): ?>
<a href="<?php echo htmlspecialchars($order['whatsapp_url']); ?>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success cc-callcenter-action-btn">
<i class="bi bi-whatsapp"></i> WhatsApp
</a>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-dark cc-callcenter-action-btn" data-bs-toggle="modal" data-bs-target="#<?php echo htmlspecialchars($modalId); ?>">
<i class="bi bi-sliders"></i> Gestionar
</button>
</div>
</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>
@ -1015,106 +1322,6 @@ require_once 'layout_header.php';
<div class="small text-muted">Última gestión: <?php echo htmlspecialchars(cc_test_format_datetime($order['ultima_gestion_at'] ?? ($order['seguimiento_actualizado'] ?? null), 'Aún no gestionado')); ?></div>
<div class="small text-muted mt-1">Nota: <?php echo htmlspecialchars(cc_test_display_value($order['nota_seguimiento'], 'Sin nota interna')); ?></div>
</td>
<td class="text-center">
<div class="d-flex flex-column gap-2 align-items-stretch">
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
<?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;
}
}
}
$assignFormClass = 'border rounded-3 p-2 text-start shadow-sm';
if ($unassigned) {
$assignFormClass .= ' bg-light';
} elseif ($assignedAssessorKey === 'KARINA') {
$assignFormClass .= ' bg-primary-subtle border-primary';
} elseif ($assignedAssessorKey === 'ESTEFANYA') {
$assignFormClass .= ' bg-success-subtle border-success';
} elseif ($assignedAssessorKey === 'CARMEN') {
$assignFormClass .= ' bg-warning-subtle border-warning';
} else {
$assignFormClass .= ' bg-secondary-subtle border-secondary';
}
if ($unassigned) {
$selectedAssessorKeyForSelect = !empty($recommendedAssessorKey) ? (string) $recommendedAssessorKey : '';
} else {
$selectedAssessorKeyForSelect = $assignedAssessorKey !== null ? (string) $assignedAssessorKey : '';
}
$buttonLabel = $unassigned
? 'Asignar pedido'
: ($assignedAssessorKey !== null ? 'Guardar asignación' : 'Desasignar');
?>
<form method="post" class="<?php echo $assignFormClass; ?>">
<input type="hidden" name="action" value="assign_assessor">
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="d-flex justify-content-between align-items-center gap-2 mb-1">
<div class="small text-uppercase text-muted fw-semibold">Asignar a asesora</div>
<?php if ($unassigned): ?>
<span class="badge bg-light text-dark border">Sin asignar</span>
<?php elseif ($assignedAssessorKey === 'KARINA'): ?>
<span class="badge bg-primary-subtle text-primary-emphasis border">KARINA</span>
<?php elseif ($assignedAssessorKey === 'ESTEFANYA'): ?>
<span class="badge bg-success-subtle text-success-emphasis border">ESTEFANYA</span>
<?php elseif ($assignedAssessorKey === 'CARMEN'): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border">CARMEN</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary-emphasis border">Asignado</span>
<?php endif; ?>
</div>
<select name="target_assessor" class="form-select form-select-sm mb-2">
<option value=""<?php echo ($selectedAssessorKeyForSelect === '') ? ' selected' : ''; ?>>Sin asignar</option>
<?php if ($unassigned): ?>
<?php foreach ($assessors as $assessorKey => $assessor): ?>
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo ($selectedAssessorKeyForSelect === $assessorKey) ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
<?php endforeach; ?>
<?php elseif ($assignedAssessorKey !== null): ?>
<option value="<?php echo htmlspecialchars((string) $assignedAssessorKey); ?>" selected><?php echo htmlspecialchars((string) ($assessors[$assignedAssessorKey]['label'] ?? $assignedAssessorKey)); ?></option>
<?php endif; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary w-100"><?php echo htmlspecialchars((string) $buttonLabel); ?></button>
</form>
<?php endif; ?>
<?php if (!empty($order['telefono_url'])): ?>
<button
type="button"
class="btn btn-sm btn-primary"
data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"
data-modal-id="<?php echo htmlspecialchars($modalId); ?>"
data-phone="<?php echo 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)">
<i class="bi bi-telephone-outbound"></i> Llamar / Gestionar
</button>
<?php else: ?>
<button type="button" class="btn btn-sm btn-primary" disabled>Sin teléfono</button>
<?php endif; ?>
<?php if (!empty($order['whatsapp_url'])): ?>
<a href="<?php echo htmlspecialchars($order['whatsapp_url']); ?>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success">
<i class="bi bi-whatsapp"></i> WhatsApp
</a>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal" data-bs-target="#<?php echo htmlspecialchars($modalId); ?>">
<i class="bi bi-sliders"></i> Gestionar
</button>
</div>
</td>
</tr>
<?php $rowHtml = ob_get_clean(); echo $rowHtml; ?>
@ -2238,6 +2445,142 @@ if (airDroidCopyButton) {
});
}
const bulkAssignForm = document.getElementById('bulkAssignForm');
const bulkAssignSourceKeys = document.getElementById('bulkAssignSourceKeys');
const bulkSelectedCount = document.getElementById('bulkSelectedCount');
const bulkAssignSubmit = document.getElementById('bulkAssignSubmit');
const toggleSelectAllOrders = document.getElementById('toggleSelectAllOrders');
const clearSelectedOrders = document.getElementById('clearSelectedOrders');
const bulkOrderCheckboxes = Array.from(document.querySelectorAll('.js-bulk-order-checkbox'));
const quickSelectButtons = Array.from(document.querySelectorAll('.js-quick-select-btn'));
const bulkAssessorSelect = bulkAssignForm ? bulkAssignForm.querySelector('select[name="target_assessor"]') : null;
function getSelectedBulkOrderCheckboxes() {
return bulkOrderCheckboxes.filter(function (checkbox) {
return checkbox.checked;
});
}
function setQuickSelectButtonState(button, selected) {
if (!button) {
return;
}
button.classList.toggle('btn-primary', selected);
button.classList.toggle('btn-outline-primary', !selected);
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
button.setAttribute('title', selected ? 'Quitar de la selección rápida' : 'Seleccionar este pedido para asignación rápida');
button.innerHTML = selected ? '<i class="bi bi-check-lg"></i>' : '<i class="bi bi-plus-lg"></i>';
}
function syncBulkAssignSelectionUi() {
const selectedCheckboxes = getSelectedBulkOrderCheckboxes();
if (bulkSelectedCount) {
bulkSelectedCount.textContent = String(selectedCheckboxes.length);
}
if (bulkAssignSourceKeys) {
bulkAssignSourceKeys.innerHTML = '';
selectedCheckboxes.forEach(function (checkbox) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'source_keys[]';
input.value = checkbox.value;
bulkAssignSourceKeys.appendChild(input);
});
}
bulkOrderCheckboxes.forEach(function (checkbox) {
const row = checkbox.closest('tr');
const button = checkbox.parentElement ? checkbox.parentElement.querySelector('.js-quick-select-btn') : null;
if (row) {
row.classList.toggle('is-selected', checkbox.checked);
}
setQuickSelectButtonState(button, checkbox.checked);
});
const anySelected = selectedCheckboxes.length > 0;
const canSubmit = anySelected && !!bulkAssessorSelect && !bulkAssessorSelect.disabled && bulkAssessorSelect.value !== '';
if (bulkAssignSubmit) {
bulkAssignSubmit.disabled = !canSubmit;
}
if (clearSelectedOrders) {
clearSelectedOrders.disabled = !anySelected;
}
if (toggleSelectAllOrders) {
const allSelected = bulkOrderCheckboxes.length > 0 && selectedCheckboxes.length === bulkOrderCheckboxes.length;
toggleSelectAllOrders.textContent = allSelected ? 'Desmarcar visibles' : 'Marcar visibles';
toggleSelectAllOrders.disabled = bulkOrderCheckboxes.length === 0;
}
}
quickSelectButtons.forEach(function (button) {
button.addEventListener('click', function () {
const sourceKey = this.dataset.sourceKey || '';
const checkbox = document.querySelector('.js-bulk-order-checkbox[data-source-key="' + sourceKey + '"]');
if (!checkbox) {
return;
}
checkbox.checked = !checkbox.checked;
syncBulkAssignSelectionUi();
});
});
bulkOrderCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener('change', syncBulkAssignSelectionUi);
});
if (bulkAssessorSelect) {
bulkAssessorSelect.addEventListener('change', syncBulkAssignSelectionUi);
}
if (toggleSelectAllOrders) {
toggleSelectAllOrders.addEventListener('click', function () {
const shouldSelectAll = bulkOrderCheckboxes.some(function (checkbox) {
return !checkbox.checked;
});
bulkOrderCheckboxes.forEach(function (checkbox) {
checkbox.checked = shouldSelectAll;
});
syncBulkAssignSelectionUi();
});
}
if (clearSelectedOrders) {
clearSelectedOrders.addEventListener('click', function () {
bulkOrderCheckboxes.forEach(function (checkbox) {
checkbox.checked = false;
});
syncBulkAssignSelectionUi();
});
}
if (bulkAssignForm) {
bulkAssignForm.addEventListener('submit', function (event) {
const selectedCheckboxes = getSelectedBulkOrderCheckboxes();
const assessorSelect = this.querySelector('select[name="target_assessor"]');
if (selectedCheckboxes.length === 0) {
event.preventDefault();
alert('Primero selecciona al menos un pedido.');
return;
}
if (!assessorSelect || !assessorSelect.value) {
event.preventDefault();
alert('Elige la asesora para hacer la asignación rápida.');
return;
}
});
syncBulkAssignSelectionUi();
}
window.setInterval(function () {
window.location.reload();
}, 600000);

View File

@ -30,6 +30,7 @@ function cc_test_ensure_tracking_table(PDO $pdo): void
`estado` VARCHAR(40) NOT NULL DEFAULT 'POR LLAMAR',
`nota_seguimiento` TEXT NULL,
`user_id` INT NULL,
`assigned_at` DATETIME NULL,
`direccion` TEXT NULL,
`referencia` TEXT NULL,
`agencia` VARCHAR(80) NULL,
@ -78,6 +79,7 @@ function cc_test_ensure_tracking_table(PDO $pdo): void
KEY `idx_callcenter_test_tracking_proxima` (`proxima_llamada_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'assigned_at', 'DATETIME NULL AFTER `user_id`');
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'direccion', 'TEXT NULL AFTER `user_id`');
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'referencia', 'TEXT NULL AFTER `direccion`');
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'agencia', 'VARCHAR(80) NULL AFTER `referencia`');
@ -119,6 +121,11 @@ function cc_test_ensure_tracking_table(PDO $pdo): void
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'pedido_rotulado_subido_por', 'INT NULL AFTER `pedido_rotulado_subido_at`');
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'ultima_gestion_at', 'DATETIME NULL AFTER `ruta_contraentrega_subido_por`');
$stmtAssignedBackfill = $pdo->query("SELECT id FROM callcenter_test_tracking WHERE assigned_at IS NULL AND user_id IS NOT NULL LIMIT 1");
if ($stmtAssignedBackfill && $stmtAssignedBackfill->fetchColumn()) {
$pdo->exec("UPDATE callcenter_test_tracking SET assigned_at = created_at, updated_at = updated_at WHERE assigned_at IS NULL AND user_id IS NOT NULL");
}
$checked = true;
}
@ -215,18 +222,27 @@ function cc_test_upsert_assignee(PDO $pdo, string $sourceKey, ?int $userId): voi
{
cc_test_ensure_tracking_table($pdo);
$stmt = $pdo->prepare("INSERT INTO callcenter_test_tracking (source_key, user_id, ultima_gestion_at)
VALUES (:source_key, :user_id, CURRENT_TIMESTAMP)
$assignedAt = $userId === null ? null : (new DateTimeImmutable('now'))->format('Y-m-d H:i:s');
$stmt = $pdo->prepare("INSERT INTO callcenter_test_tracking (source_key, user_id, assigned_at, ultima_gestion_at)
VALUES (:source_key, :user_id, :assigned_at, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
user_id = VALUES(user_id),
assigned_at = CASE
WHEN VALUES(user_id) IS NULL THEN NULL
WHEN assigned_at IS NULL OR user_id IS NULL OR user_id <> VALUES(user_id) THEN VALUES(assigned_at)
ELSE assigned_at
END,
ultima_gestion_at = VALUES(ultima_gestion_at),
updated_at = CURRENT_TIMESTAMP");
$stmt->bindValue(':source_key', $sourceKey, PDO::PARAM_STR);
if ($userId === null) {
$stmt->bindValue(':user_id', null, PDO::PARAM_NULL);
$stmt->bindValue(':assigned_at', null, PDO::PARAM_NULL);
} else {
$stmt->bindValue(':user_id', $userId, PDO::PARAM_INT);
$stmt->bindValue(':assigned_at', $assignedAt, PDO::PARAM_STR);
}
$stmt->execute();

View File

@ -18,8 +18,10 @@ function drive_test_available_stores(): array
'label' => 'Flower',
'spreadsheet_id' => '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c',
'sheet_gid' => null,
'startRow' => 5552,
'enforce_db_start_row' => false,
'startRow' => 7796, // Pedido #37431
'min_codigo_number' => 37431,
'enforce_db_start_row' => true,
'incremental_sync' => true,
],
// Segunda tienda (nuevo link que pasaste).
'otra_tienda' => [
@ -28,6 +30,7 @@ function drive_test_available_stores(): array
'sheet_gid' => 1523126328,
'startRow' => 6804,
'enforce_db_start_row' => true,
'incremental_sync' => true,
],
];
}
@ -94,6 +97,16 @@ function drive_test_get_cell(array $row, array $indexes, array $aliases, string
return $default;
}
function drive_test_extract_codigo_number(?string $codigo): ?int
{
$digits = preg_replace('/\D+/', '', trim((string) $codigo));
if ($digits === '') {
return null;
}
return (int) $digits;
}
function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $storeKey = 'flower'): array
{
$storeConfig = drive_test_get_store_config($storeKey);
@ -157,6 +170,7 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $
$previewRows = array_reverse($previewRows, true);
$orders = [];
$minCodigoNumber = isset($storeConfig['min_codigo_number']) ? (int) ($storeConfig['min_codigo_number'] ?? 0) : 0;
foreach ($previewRows as $rowIndex => $row) {
$codigo = trim((string) ($row[0] ?? ''));
@ -177,6 +191,11 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $
$referencia = drive_test_get_cell($row, $headerIndexes, ['REFERENCIA']);
$distrito = drive_test_get_cell($row, $headerIndexes, ['DISTRITO']);
$sourceRow = (int) $rowIndex + 2;
$codigoNumber = drive_test_extract_codigo_number($codigo);
if ($minCodigoNumber > 0 && ($codigoNumber === null || $codigoNumber < $minCodigoNumber)) {
continue;
}
// Importante: para 'flower' mantenemos EXACTAMENTE el algoritmo anterior
// para no romper el historial/tracking ya existente.
@ -229,7 +248,7 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array
cc_test_ensure_tracking_table($pdo);
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, user_id, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, numero_cuenta_sede_id, numero_cuenta_dni, observaciones, coordenadas, producto, cantidad, precio, monto_adelantado, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, confirmacion_producto_extra, confirmacion_cantidad_extra, confirmacion_precio_extra, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, promo_final_evidencia_path, promo_final_evidencia_subido_at, promo_final_evidencia_subido_por, cancelado_evidencia_path, cancelado_evidencia_subido_at, cancelado_evidencia_subido_por, eliminado_at, eliminado_por, ruta_contraentrega_pedido_id, ruta_contraentrega_subido_at, ruta_contraentrega_subido_por, pedido_rotulado_pedido_id, pedido_rotulado_subido_at, pedido_rotulado_subido_por, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, user_id, assigned_at, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, numero_cuenta_sede_id, numero_cuenta_dni, observaciones, coordenadas, producto, cantidad, precio, monto_adelantado, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, confirmacion_producto_extra, confirmacion_cantidad_extra, confirmacion_precio_extra, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, promo_final_evidencia_path, promo_final_evidencia_subido_at, promo_final_evidencia_subido_por, cancelado_evidencia_path, cancelado_evidencia_subido_at, cancelado_evidencia_subido_por, eliminado_at, eliminado_por, ruta_contraentrega_pedido_id, ruta_contraentrega_subido_at, ruta_contraentrega_subido_por, pedido_rotulado_pedido_id, pedido_rotulado_subido_at, pedido_rotulado_subido_por, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
$stmt->execute($sourceKeys);
$tracking = [];
@ -251,6 +270,7 @@ function drive_test_merge_tracking(array $orders, array $tracking): array
$userId = array_key_exists('user_id', (array) $current) ? $current['user_id'] : null;
$order['user_id'] = $userId !== null ? ((int) $userId > 0 ? (int) $userId : null) : null;
$order['seguimiento_actualizado'] = $current['updated_at'] ?? null;
$order['assigned_at'] = $current['assigned_at'] ?? null;
$order['proxima_llamada_at'] = $current['proxima_llamada_at'] ?? null;
$order['fecha_entrega_programada'] = $current['fecha_entrega_programada'] ?? null;
$order['numero_cuenta_enviado_at'] = $current['numero_cuenta_enviado_at'] ?? null;
@ -628,12 +648,21 @@ function drive_test_fetch_orders_from_db(PDO $pdo, string $storeKey, ?bool $only
$stmt->execute($params);
$orders = [];
$minCodigoNumber = isset($storeConfig['min_codigo_number']) ? (int) ($storeConfig['min_codigo_number'] ?? 0) : 0;
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$isAgregado = (int) ($row['is_agregado'] ?? 0);
$codigo = (string) ($row['codigo'] ?? '');
$codigoNumber = drive_test_extract_codigo_number($codigo);
if ($minCodigoNumber > 0 && $isAgregado !== 1 && ($codigoNumber === null || $codigoNumber < $minCodigoNumber)) {
continue;
}
$orders[] = [
'id' => (int) ($row['id'] ?? 0),
'is_agregado' => (int) ($row['is_agregado'] ?? 0),
'is_agregado' => $isAgregado,
'source_key' => (string) ($row['source_key'] ?? ''),
'codigo' => (string) ($row['codigo'] ?? ''),
'codigo' => $codigo,
'import_id' => (string) ($row['import_id'] ?? ''),
'source_row' => isset($row['source_row']) && (int) $row['source_row'] > 0 ? (int) $row['source_row'] : null,
'drive_imported_at' => (string) ($row['drive_imported_at'] ?? ''),

View File

@ -88,6 +88,7 @@ ensureTipoPaqueteEnumDefinition($pdo);
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
$is_admin = in_array($user_role, ['Administrador', 'admin'], true);
// Fetch years for the filter
$years_query = "SELECT DISTINCT YEAR(created_at) as year FROM pedidos";
@ -337,17 +338,19 @@ include 'layout_header.php';
AGREGAR PEDIDOS CONTRAENTREGA
</button>
<?php endif; ?>
<?php
$excelParams = array_filter([
'q' => $search_query,
], static function ($value) {
return $value !== '' && $value !== null;
});
$excelUrl = 'download_ruta_contraentrega.php' . (!empty($excelParams) ? '?' . http_build_query($excelParams) : '');
?>
<a href="<?php echo htmlspecialchars($excelUrl); ?>" class="btn btn-success">
<i class="fas fa-file-excel me-1"></i> Descargar Excel (Mañana)
</a>
<?php if ($is_admin): ?>
<?php
$excelParams = array_filter([
'q' => $search_query,
], static function ($value) {
return $value !== '' && $value !== null;
});
$excelUrl = 'download_ruta_contraentrega.php' . (!empty($excelParams) ? '?' . http_build_query($excelParams) : '');
?>
<a href="<?php echo htmlspecialchars($excelUrl); ?>" class="btn btn-success">
<i class="fas fa-file-excel me-1"></i> Descargar Excel (Mañana)
</a>
<?php endif; ?>
</div>
</form>
</div>