Acceso denegado.
"; require_once 'layout_footer.php'; exit(); } function cc_test_parse_datetime(?string $value): ?DateTimeImmutable { $value = trim((string) $value); if ($value === '') { return null; } try { return new DateTimeImmutable($value); } catch (Throwable $exception) { return null; } } function cc_test_format_datetime(?string $value, string $fallback = 'Sin programar'): string { $date = cc_test_parse_datetime($value); return $date ? $date->format('d/m/Y h:i A') : $fallback; } function cc_test_format_datetime_input(?string $value): string { $date = cc_test_parse_datetime($value); return $date ? $date->format('Y-m-d\TH:i') : ''; } function cc_test_format_date(?string $value, string $fallback = 'Sin fecha'): string { $date = cc_test_parse_datetime($value); return $date ? $date->format('d/m/Y') : $fallback; } function cc_test_format_date_input(?string $value): string { $date = cc_test_parse_datetime($value); return $date ? $date->format('Y-m-d') : ''; } function cc_test_format_price(?string $value): string { $value = trim((string) $value); return $value !== '' ? 'S/ ' . $value : 'No registrado'; } function cc_test_display_value(?string $value, string $fallback = 'No registrado'): string { $value = trim((string) $value); return $value !== '' ? $value : $fallback; } function cc_test_phone_hidden_html(?string $value, string $fallback = 'Sin celular'): string { $value = trim((string) $value); if ($value === '') { return htmlspecialchars($fallback); } return 'Celular oculto'; } function cc_test_build_url(string $path, array $params = []): string { foreach ($params as $key => $value) { if ($value === '' || $value === null) { unset($params[$key]); } } $query = http_build_query($params); return $query !== '' ? $path . '?' . $query : $path; } function cc_test_order_label(array $order): string { $codigo = trim((string) ($order['codigo'] ?? '')); if ($codigo !== '') { return '#' . ltrim($codigo, '#'); } if (!empty($order['is_agregado'])) { return 'AGREGADOS'; } return 'Sin número'; } function cc_test_promo_final_badge_info(array $order): ?array { if (empty($order['es_promo_final'])) { return null; } return [ 'label' => 'Promo Final', 'title' => 'Pedido movido a Promo Final', 'class' => 'bg-success-subtle text-success-emphasis border', ]; } function cc_test_badge_class(string $estado): string { return match (cc_test_normalize_state($estado)) { 'POR LLAMAR', 'DEVOLVER LLAMADA' => 'bg-white text-dark border', 'OBSERVADO' => 'bg-warning-subtle text-warning-emphasis', 'SE ENVIO NUMERO DE CUENTA' => 'bg-success-subtle text-success-emphasis', 'CONFIRMADO CONTRAENTREGA' => 'cc-badge-orange', 'CONFIRMADO ENVIO' => 'bg-success text-white', 'CANCELADO' => 'bg-danger-subtle text-danger-emphasis', 'REPETIDO' => 'bg-secondary-subtle text-secondary-emphasis', default => 'bg-white text-dark border', }; } function cc_test_order_time(array $order): int { foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado'] as $field) { $date = cc_test_parse_datetime($order[$field] ?? null); if ($date) { return $date->getTimestamp(); } } // Fallback recency for Drive imports where import_id is not a datetime (e.g. "#35898"). return cc_test_import_time($order); } function cc_test_import_time(array $order): int { foreach (['drive_imported_at', 'first_seen_at'] as $field) { $date = cc_test_parse_datetime($order[$field] ?? null); if ($date) { return $date->getTimestamp(); } } $sourceRow = (int) ($order['source_row'] ?? 0); if ($sourceRow > 0) { return $sourceRow; } $importRaw = trim((string) ($order['import_id'] ?? '')); if ($importRaw !== '') { $date = cc_test_parse_datetime($importRaw); if ($date) { return $date->getTimestamp(); } $digits = preg_replace('/\D+/', '', $importRaw); if ($digits !== '') { return (int) $digits; } } $codigoRaw = trim((string) ($order['codigo'] ?? '')); $digits = preg_replace('/\D+/', '', $codigoRaw); if ($digits !== '') { return (int) $digits; } return (int) ($order['id'] ?? 0); } if (!function_exists('cc_test_tuani_reparto_is_eligible')) { function cc_test_tuani_reparto_is_eligible(array $order, string $storeKey, array $storeConfig): bool { return true; } } if (!function_exists('cc_test_filter_reparto_orders')) { function cc_test_filter_reparto_orders(array $orders, string $storeKey, array $storeConfig): array { return $orders; } } function cc_test_followup_semaforo(array $order): ?array { if (cc_test_normalize_state((string) ($order['estado'] ?? '')) !== 'SE ENVIO NUMERO DE CUENTA') { return null; } return cc_test_account_followup_semaforo($order['numero_cuenta_enviado_at'] ?? null); } $defaultView = 'todos'; $view = cc_test_resolve_callcenter_context_key($_GET['view'] ?? '', cc_test_callcenter_view_session_key()); $availableStores = drive_test_available_stores(); $storeKey = cc_test_resolve_callcenter_context_key($_GET['store'] ?? '', cc_test_callcenter_store_session_key(), 'flower'); if (!isset($availableStores[$storeKey])) { $storeKey = 'flower'; } $storeConfig = $availableStores[$storeKey]; $storeLabel = (string) ($storeConfig['label'] ?? strtoupper($storeKey)); $recoverablesStoreKey = trim((string) ($storeConfig['recoverables_store_key'] ?? '')); $recoverablesStoreLabel = $recoverablesStoreKey !== '' && isset($availableStores[$recoverablesStoreKey]) ? (string) ($availableStores[$recoverablesStoreKey]['label'] ?? strtoupper($recoverablesStoreKey)) : ''; $recoverablesMainStoreKey = trim((string) ($storeConfig['main_store_key'] ?? '')); $recoverablesMainStoreLabel = $recoverablesMainStoreKey !== '' && isset($availableStores[$recoverablesMainStoreKey]) ? (string) ($availableStores[$recoverablesMainStoreKey]['label'] ?? strtoupper($recoverablesMainStoreKey)) : $storeLabel; $recoverablesStoreLabelHtml = htmlspecialchars($storeLabel); $recoverablesMainStoreLabelHtml = htmlspecialchars($recoverablesMainStoreLabel); $canOpenRecoverablesStore = $recoverablesStoreKey !== '' && isset($availableStores[$recoverablesStoreKey]); $usesIncrementalSync = !empty($storeConfig['incremental_sync']); $storeNav = drive_test_store_navigation_state($availableStores, $storeKey); $storeNavGroups = $storeNav['groups']; $activeStoreGroupKey = (string) ($storeNav['active_group_key'] ?? $storeKey); $activeStoreGroupLabel = (string) ($storeNav['active_group_label'] ?? $storeLabel); $activeStoreGroupChildren = $storeNav['active_group_children'] ?? []; $parentStoreLabel = (string) ($storeNav['parent_label'] ?? ''); $isMainTuaniStore = cc_test_is_tuani_main_store_key($storeKey); $isTuaniRecuperablesStore = cc_test_is_recoverables_store_key($storeKey); if ($isTuaniRecuperablesStore && trim((string) ($_GET['view'] ?? '')) === '') { $view = 'todos'; } $canShowTuaniLogisticaButton = $isMainTuaniStore || $isTuaniRecuperablesStore; $postedAction = trim((string) ($_POST['action'] ?? '')); $recoverablesResyncRequested = $isTuaniRecuperablesStore && $isAdmin && $postedAction === 'reimport_recoverables_queue'; $recoverablesResyncRow = $isTuaniRecuperablesStore ? (int) ($storeConfig['startRow'] ?? 2000) : null; $viewCardColumnClass = $isTuaniRecuperablesStore ? 'col-md-6 col-xl-2' : 'col-md-6 col-xl-2'; $callCenterDefaultView = ($isMainTuaniStore || $isTuaniRecuperablesStore) ? cc_test_callcenter_pro_default_view_key($storeKey) : $view; $viewCards = $isTuaniRecuperablesStore ? [ 'todos' => [ 'label' => 'Todos', 'heading' => 'Todos los pedidos cargados', 'stat_key' => 'total', 'active_class' => 'bg-light border', ], 'confirmados' => [ 'label' => 'Confirmados', 'heading' => 'Confirmados', 'stat_key' => 'confirmados', 'active_class' => 'bg-success-subtle border border-success', ], 'recuperados' => [ 'label' => 'Recuperados', 'heading' => 'Recuperados', 'stat_key' => 'recuperados', 'active_class' => 'bg-warning-subtle border border-warning', ], 'seguimiento' => [ 'label' => 'Seguimiento', 'heading' => 'Seguimiento', 'stat_key' => 'seguimiento', 'active_class' => 'bg-primary-subtle border border-primary', ], 'observados' => [ 'label' => 'Observados', 'heading' => 'Observados', 'stat_key' => 'observados', 'active_class' => 'bg-warning-subtle border border-warning', ], 'pendientes_hoy' => [ 'label' => $storeLabel, 'heading' => $storeLabel, 'stat_key' => 'pendientes_hoy', 'active_class' => 'bg-dark text-white', ], ] : [ 'pendientes_hoy' => [ 'label' => 'Bandeja principal', 'heading' => 'Bandeja principal', 'stat_key' => 'pendientes_hoy', 'active_class' => 'bg-dark text-white', ], 'nuevos_hoy' => [ 'label' => 'Nuevos de hoy', 'heading' => 'Nuevos de hoy', 'stat_key' => 'nuevos_hoy', 'active_class' => 'bg-primary-subtle border border-primary', ], 'confirmados' => [ 'label' => 'Confirmados', 'heading' => 'Confirmados', 'stat_key' => 'confirmados', 'active_class' => 'bg-success-subtle border border-success', ], 'recuperados' => [ 'label' => 'Recuperados', 'heading' => 'Recuperados', 'stat_key' => 'recuperados', 'active_class' => 'bg-warning-subtle border border-warning', ], 'seguimiento' => [ 'label' => 'Seguimiento', 'heading' => 'Seguimiento', 'stat_key' => 'seguimiento', 'active_class' => 'bg-primary-subtle border border-primary', ], 'promo_final' => [ 'label' => 'Promo Final', 'heading' => 'Promo Final', 'stat_key' => 'promo_final', 'active_class' => 'bg-danger-subtle border border-danger', ], 'observados' => [ 'label' => 'Observados', 'heading' => 'Observados', 'stat_key' => 'observados', 'active_class' => 'bg-warning-subtle border border-warning', ], 'cerrados' => [ 'label' => 'Cerrados', 'heading' => 'Cerrados / descartados', 'stat_key' => 'cerrados', 'active_class' => 'bg-secondary-subtle border border-secondary', ], 'todos' => [ 'label' => 'Todos', 'heading' => 'Todos los pedidos cargados', 'stat_key' => 'total', 'active_class' => 'bg-light border', ], ]; $allowedViews = []; foreach ($viewCards as $viewKey => $cardConfig) { $allowedViews[$viewKey] = $cardConfig['heading']; } if ($view === '') { $view = $defaultView; } if (!isset($allowedViews[$view])) { $view = isset($allowedViews[$defaultView]) ? $defaultView : (array_key_first($allowedViews) ?: $defaultView); } $_SESSION[cc_test_callcenter_store_session_key()] = cc_test_normalize_context_key($storeKey); $_SESSION[cc_test_callcenter_view_session_key()] = cc_test_normalize_context_key($view); $viewStatesNote = $isTuaniRecuperablesStore ? 'Estados disponibles: ' . $recoverablesStoreLabelHtml . ', Seguimiento, Observados, Confirmados, Recuperados y Todos. Esta bandeja abre por defecto en Todos. La 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 ' . $recoverablesMainStoreLabelHtml . ' principal, se marca en gris como Pedido repetido en tienda solo en esta bandeja. Las vistas ' . $recoverablesStoreLabelHtml . ' y Todos arrancan en la fila ' . (int) ($storeConfig['startRow'] ?? 0) . ', solo toman pedidos desde esa fila y se ordenan por el número #D más alto, lo que mantiene arriba el código más reciente, al más antiguo, 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. Promo Final 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 Todos y Últimos 4 días hábiles, la llamada y la gestión siguen disponibles desde el panel.'; $errorMessage = null; $noticeMessage = null; $uploadErrorMessage = null; $assessors = []; $orders = []; $visibleOrders = []; $modalsHtml = fopen('php://temp', 'w+'); if ($modalsHtml === false) { $modalsHtml = null; } $totalRows = 0; $agregadosCount = 0; $lastProcessedRow = null; $recoverablesImportCount = 0; $recoverablesFirstSourceRow = null; $recoverablesLastSourceRow = null; $recoverablesNotice = null; $selectedAssessorFilter = ''; $selectedAssessorFilterLabel = ''; $persistedAssessorParams = []; $repartoSettings = [ 'store_key' => $storeKey, 'reparto_mode' => 'manual', 'rotation_date' => null, 'rotation_index' => 0, 'assessor_states' => [], ]; $repartoPreview = [ 'mode' => 'manual', 'mode_label' => 'Manual', 'sequence' => [], 'ordered_keys' => [], 'states' => [], 'available_count' => 0, 'total_count' => 0, 'rotation_index' => 0, 'rotation_date' => null, 'next_key' => null, 'next_label' => '', 'next_position' => null, ]; $repartoUnassignedCount = 0; $repartoEligibleUnassignedCount = 0; $cupoObjetivoPorAsesora = 10; $advisorOpenCounts = []; $advisorTotalCounts = []; $recommendedAssessorKey = null; $startRow = (int) ($storeConfig['startRow'] ?? 0); $recoverablesMinimumSourceRow = $isTuaniRecuperablesStore ? (int) ($storeConfig['startRow'] ?? 0) : 0; $catalogoProductos = []; $stats = [ 'total' => 0, 'pendientes_hoy' => 0, 'nuevos_hoy' => 0, 'confirmados' => 0, 'recuperados' => 0, 'seguimiento' => 0, 'promo_final' => 0, 'observados' => 0, 'cerrados' => 0, ]; try { $pdo = db(); cc_test_ensure_tracking_table($pdo); cc_test_ensure_historial_llamadas_table($pdo); $assessors = cc_test_fetch_assessors($pdo); $selectedAssessorFilter = $isTuaniRecuperablesStore ? '' : cc_test_resolve_callcenter_assessor_filter($assessors, $_GET['assessor'] ?? null, false, false); $selectedAssessorFilterLabel = $selectedAssessorFilter !== '' ? (string) ($assessors[$selectedAssessorFilter]['label'] ?? $selectedAssessorFilter) : ''; $persistedAssessorParams = []; if ($selectedAssessorFilterLabel !== '') { $pageTitle = 'Base de Datos Pedidos | Asignación · ' . $selectedAssessorFilterLabel; } if ($isMainTuaniStore) { $repartoSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors); } try { $stmtProductos = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC"); $catalogoProductos = $stmtProductos->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable $exception) { $catalogoProductos = []; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'upload_agregados_excel') { try { $file = $_FILES['agregados_excel'] ?? null; if (!is_array($file)) { throw new RuntimeException('No se encontró el archivo de Excel.'); } $maxSize = 10 * 1024 * 1024; // 10 MB if ((int) ($file['size'] ?? 0) > $maxSize) { throw new RuntimeException('El archivo es demasiado grande. Máximo 10 MB.'); } $res = cc_agregados_import_from_uploaded_file($pdo, $storeKey, $file); $inserted = (int) ($res['inserted'] ?? 0); $skipped = (int) ($res['skipped'] ?? 0); $noticeMessage = 'Importación completada: ' . $inserted . ' pedidos agregados.' . ($skipped > 0 ? ' Filas ignoradas: ' . $skipped . '.' : ''); $uploadErrorMessage = null; } catch (Throwable $e) { $uploadErrorMessage = $e->getMessage(); } } if ($usesIncrementalSync) { $syncInfo = drive_test_sync_orders_incremental($pdo, $storeKey, 0, $recoverablesResyncRequested ? $recoverablesResyncRow : null); $totalRows = (int) ($syncInfo['drive_rows_total'] ?? 0); $lastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0); $startRow = (int) ($syncInfo['next_start_row'] ?? $startRow); $orders = drive_test_fetch_orders_from_db($pdo, $storeKey); $agregadosCount = 0; foreach ($orders as $o) { if (!empty($o['is_agregado'])) { $agregadosCount++; } } } else { $preview = drive_test_fetch_orders(100, $startRow, $storeKey); $totalRows = (int) ($preview['total_rows'] ?? 0); $orders = $preview['orders'] ?? []; $agregadosOrders = drive_test_fetch_orders_from_db($pdo, $storeKey, true); $agregadosCount = count($agregadosOrders); if (!empty($agregadosOrders)) { $orders = array_merge($orders, $agregadosOrders); } } if ($recoverablesResyncRequested) { $noticeMessage = 'Reimportación desde fila ' . $recoverablesResyncRow . ' completada. La cola volvió a contrastarse sin duplicar pedidos ya cargados.'; } $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 ($isTuaniRecuperablesStore) { $orders = cc_test_filter_recoverables_orders($orders, $recoverablesMinimumSourceRow); $recoverablesSummary = cc_test_recoverables_summary($orders); if ($recoverablesSummary !== null) { $recoverablesImportCount = (int) ($recoverablesSummary['import_count'] ?? count($orders)); $recoverablesFirstSourceRow = $recoverablesSummary['first_source_row'] !== null ? (int) $recoverablesSummary['first_source_row'] : null; $recoverablesLastSourceRow = $recoverablesSummary['last_source_row'] !== null ? (int) $recoverablesSummary['last_source_row'] : null; $recoverablesNotice = $recoverablesSummary['notice'] ?? null; $recoverablesNotice = null; } else { $recoverablesImportCount = count($orders); $recoverablesFirstSourceRow = null; $recoverablesLastSourceRow = null; $recoverablesNotice = null; } } if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'assign_assessor') { $sourceKey = trim((string) ($_POST['source_key'] ?? '')); $targetValue = trim((string) ($_POST['target_assessor'] ?? '')); if ($sourceKey === '' || !preg_match('/^[a-f0-9]{40}$/', $sourceKey)) { throw new RuntimeException('Pedido de prueba inválido para asignar.'); } $targetKey = trim(mb_strtoupper($targetValue)); $targetKey = preg_replace('/\s+/', ' ', $targetKey) ?? $targetKey; $selectedAssessor = $targetKey !== '' ? ($assessors[$targetKey] ?? null) : null; if ($targetKey !== '' && !$selectedAssessor) { throw new RuntimeException('No se encontró la asesora seleccionada.'); } $targetUserId = $selectedAssessor ? (int) $selectedAssessor['id'] : null; $stmtCurrent = $pdo->prepare('SELECT user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1'); $stmtCurrent->execute([$sourceKey]); $currentUserIdRaw = $stmtCurrent->fetchColumn(); $currentUserId = $currentUserIdRaw !== null ? ((int) $currentUserIdRaw > 0 ? (int) $currentUserIdRaw : null) : null; $isAdmin = in_array((string) ($_SESSION['user_role'] ?? ''), ['Administrador', 'admin'], true); // Los asesores no pueden cambiar pedidos de otra asesora; el administrador sí puede reasignar directamente. if (!$isAdmin && $currentUserId !== null && $targetUserId !== null && (int) $currentUserId !== (int) $targetUserId) { $currentLabel = 'otra asesora'; foreach ($assessors as $assessorKey => $assessor) { if ((int) ($assessor['id'] ?? 0) === (int) $currentUserId) { $currentLabel = (string) ($assessor['label'] ?? $assessorKey); break; } } throw new RuntimeException('Este pedido ya está asignado a ' . $currentLabel . '. Para cambiarlo primero déjalo en "Sin asignar".'); } cc_test_upsert_assignee($pdo, $sourceKey, $targetUserId); $noticeMessage = $selectedAssessor ? 'Pedido asignado a ' . $selectedAssessor['label'] . '.' : 'Pedido dejado sin asignar.'; $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'] ?? '') === '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; $pdo->beginTransaction(); try { foreach ($sourceKeys as $sourceKey) { $currentUserId = $currentAssignments[$sourceKey] ?? null; 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.'; } $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'] ?? '') === 'update_assessor_color') { $assessorKey = cc_test_normalize_user_key((string) ($_POST['assessor_key'] ?? '')); if ($assessorKey === '' || !isset($assessors[$assessorKey])) { throw new RuntimeException('No se encontró la asesora seleccionada.'); } $colorHex = cc_test_normalize_hex_color((string) ($_POST['color_hex'] ?? '')); if ($colorHex === null) { throw new RuntimeException('Selecciona un color válido.'); } cc_test_ensure_column($pdo, 'users', 'color_hex', 'VARCHAR(7) NULL AFTER `nombre_asesor`'); $stmtUpdateAssessorColor = $pdo->prepare('UPDATE users SET color_hex = :color_hex WHERE id = :id LIMIT 1'); $stmtUpdateAssessorColor->bindValue(':color_hex', $colorHex, PDO::PARAM_STR); $stmtUpdateAssessorColor->bindValue(':id', (int) ($assessors[$assessorKey]['id'] ?? 0), PDO::PARAM_INT); $stmtUpdateAssessorColor->execute(); $noticeMessage = 'Color actualizado para ' . ($assessors[$assessorKey]['label'] ?? $assessorKey) . '.'; $assessors[$assessorKey]['color_hex'] = $colorHex; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'auto_assign_cupo') { $openStates = cc_test_open_states(); if (empty($assessors)) { throw new RuntimeException('No hay asesoras configuradas en el sistema.'); } $pendingByAssessorId = []; foreach ($assessors as $assessor) { $id = (int) ($assessor['id'] ?? 0); if ($id > 0) { $pendingByAssessorId[$id] = 0; } } // Contamos cuántos pedidos siguen activos (POR LLAMAR / DEVOLVER LLAMADA / OBSERVADO). foreach ($orders as $o) { if ($o['user_id'] === null) { continue; } $uid = (int) ($o['user_id'] ?? 0); if (!isset($pendingByAssessorId[$uid])) { continue; } $estadoNorm = cc_test_normalize_state((string) ($o['estado'] ?? '')); if (in_array($estadoNorm, $openStates, true)) { $pendingByAssessorId[$uid]++; } } $missingByAssessorId = []; foreach ($pendingByAssessorId as $uid => $pending) { $missingByAssessorId[$uid] = max(0, (int) $cupoObjetivoPorAsesora - (int) $pending); } $missingTotal = array_sum($missingByAssessorId); // Pedidos sin asignar y que todavía requieren llamada. $eligibleUnassigned = []; foreach ($orders as $o) { if ($o['user_id'] !== null) { continue; } if (!cc_test_tuani_reparto_is_eligible($o, $storeKey, $storeConfig)) { continue; } $estadoNorm = cc_test_normalize_state((string) ($o['estado'] ?? '')); if (!in_array($estadoNorm, $openStates, true)) { continue; } $eligibleUnassigned[] = $o; } if ($missingTotal <= 0) { $noticeMessage = 'Cupos completos: ninguna asesora necesita pedidos por el momento.'; } elseif (empty($eligibleUnassigned)) { $noticeMessage = 'No hay pedidos sin asignar (pendientes) para rellenar los cupos.'; } else { usort($eligibleUnassigned, static function (array $a, array $b) use ($storeKey): int { $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) { return $aDue <=> $bDue; } if ($aDue && !$bDue) { return -1; } if (!$aDue && $bDue) { return 1; } if (cc_test_is_tuani_store_key($storeKey)) { $aSourceRow = (int) ($a['source_row'] ?? 0); $bSourceRow = (int) ($b['source_row'] ?? 0); if ($aSourceRow > 0 && $bSourceRow > 0 && $aSourceRow !== $bSourceRow) { return $bSourceRow <=> $aSourceRow; } } return cc_test_order_time($a) <=> cc_test_order_time($b); }); $orderedAssessorIds = []; foreach (cc_test_ordered_assessor_keys($assessors) as $assessorKey) { if (isset($assessors[$assessorKey])) { $orderedAssessorIds[] = (int) ($assessors[$assessorKey]['id'] ?? 0); } } if (empty($orderedAssessorIds)) { $orderedAssessorIds = array_map('intval', array_keys($pendingByAssessorId)); } $assignedCount = 0; foreach ($eligibleUnassigned as $order) { if ($missingTotal <= 0) { break; } $chosenId = null; $minPending = PHP_INT_MAX; foreach ($orderedAssessorIds as $uid) { $uid = (int) $uid; if ($uid <= 0) { continue; } $missing = $missingByAssessorId[$uid] ?? 0; if ($missing <= 0) { continue; } $pending = $pendingByAssessorId[$uid] ?? 0; if ($pending < $minPending) { $minPending = $pending; $chosenId = $uid; } } if ($chosenId === null) { break; } cc_test_upsert_assignee($pdo, (string) ($order['source_key'] ?? ''), $chosenId); $pendingByAssessorId[$chosenId] = ((int) ($pendingByAssessorId[$chosenId] ?? 0)) + 1; $missingByAssessorId[$chosenId] = max(0, (int) ($missingByAssessorId[$chosenId] ?? 0) - 1); $missingTotal--; $assignedCount++; } if ($assignedCount > 0) { $noticeMessage = 'Auto-asignación lista: se asignaron ' . $assignedCount . ' pedidos para completar cupos.'; } else { $noticeMessage = 'No se asignaron pedidos (puede que ya no existan cupos o pedidos elegibles).'; } $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); $orders = drive_test_merge_tracking($orders, $tracking); } } if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_reparto_config') { if (!$isMainTuaniStore) { throw new RuntimeException('Esta configuración solo aplica a TUANI principal.'); } $postedMode = cc_test_normalize_reparto_mode($_POST['reparto_mode'] ?? 'manual'); $postedStates = $_POST['assessor_state'] ?? []; if (!is_array($postedStates)) { $postedStates = []; } $repartoSettings['reparto_mode'] = $postedMode; $repartoSettings['assessor_states'] = cc_test_reparto_normalize_states($postedStates, $assessors); cc_test_upsert_reparto_settings($pdo, $storeKey, $repartoSettings); $noticeMessage = 'Configuración de reparto guardada.'; } elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'run_daily_reparto') { if (!$isMainTuaniStore) { throw new RuntimeException('El reparto intercalado solo está disponible en TUANI principal.'); } $repartoResult = cc_test_run_locked_reparto_job( $pdo, $storeKey, $assessors, static function () use ($pdo, $storeKey): array { return cc_test_load_reparto_orders($pdo, $storeKey); }, $repartoSettings, static function (array $orders) use ($storeKey, $storeConfig): array { return cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig); } ); $assignedCount = (int) ($repartoResult['assigned_count'] ?? 0); $noticeMessage = $assignedCount > 0 ? 'Reparto intercalado: ' . $assignedCount . ' pedido(s) asignado(s).' : (string) ($repartoResult['message'] ?? 'No había pedidos pendientes o no había asesoras disponibles.'); if ($assignedCount > 0) { $orders = cc_test_load_reparto_orders($pdo, $storeKey); } $repartoSettings = $repartoResult['settings'] ?? $repartoSettings; } if ($isMainTuaniStore && cc_test_reparto_mode_is_auto($repartoSettings['reparto_mode'] ?? 'manual')) { $autoRepartoResult = cc_test_run_locked_reparto_job( $pdo, $storeKey, $assessors, static function () use ($pdo, $storeKey): array { return cc_test_load_reparto_orders($pdo, $storeKey); }, $repartoSettings, static function (array $orders) use ($storeKey, $storeConfig): array { return cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig); } ); $autoAssignedCount = (int) ($autoRepartoResult['assigned_count'] ?? 0); if ($autoAssignedCount > 0) { $noticeMessage = trim((string) ($noticeMessage !== null ? $noticeMessage . ' ' : '') . 'Reparto automático: ' . $autoAssignedCount . ' pedido(s) asignado(s).'); $orders = cc_test_load_reparto_orders($pdo, $storeKey); } $repartoSettings = $autoRepartoResult['settings'] ?? $repartoSettings; } if ($isMainTuaniStore) { $repartoSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors); } $repartoPreview = cc_test_reparto_preview($assessors, $repartoSettings); $repartoEligibleOrders = cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig); foreach ($repartoEligibleOrders as $repartoOrder) { if ((int) ($repartoOrder['user_id'] ?? 0) <= 0) { $repartoEligibleUnassignedCount++; } } foreach ($orders as $repartoOrder) { if ((int) ($repartoOrder['user_id'] ?? 0) <= 0) { $repartoUnassignedCount++; } } $sourceKeys = array_column($orders, 'source_key'); $callCounts = []; if (!empty($sourceKeys)) { $placeholders = implode(',', array_fill(0, count($sourceKeys), '?')); $stmtCalls = $pdo->prepare("SELECT pedido_id, COUNT(*) as total FROM historial_llamadas WHERE pedido_id IN ($placeholders) GROUP BY pedido_id"); $stmtCalls->execute($sourceKeys); $callCounts = $stmtCalls->fetchAll(PDO::FETCH_KEY_PAIR); } $todayStart = new DateTimeImmutable('today'); $todayEnd = $todayStart->setTime(23, 59, 59); $openStates = cc_test_open_states(); $closedStates = cc_test_closed_states(); $recoverablesRecentSourceRowThreshold = null; if ($isTuaniRecuperablesStore) { $recoverablesMaxSourceRow = 0; foreach ($orders as $recoverablesThresholdOrder) { $recoverablesSourceRow = (int) ($recoverablesThresholdOrder['source_row'] ?? 0); if ($recoverablesSourceRow > $recoverablesMaxSourceRow) { $recoverablesMaxSourceRow = $recoverablesSourceRow; } } if ($recoverablesMaxSourceRow > 0) { $recoverablesRecentSourceRowThreshold = max(0, $recoverablesMaxSourceRow - 4); } } $advisorOpenCounts = []; $advisorTotalCounts = []; foreach ($assessors as $assessor) { $id = (int) ($assessor['id'] ?? 0); if ($id > 0) { $advisorOpenCounts[$id] = 0; $advisorTotalCounts[$id] = 0; } } foreach ($orders as &$order) { $order['estado'] = cc_test_normalize_state((string) ($order['estado'] ?? '')); $order['total_llamadas'] = (int) ($callCounts[$order['source_key']] ?? 0); $firstSeenDate = cc_test_parse_datetime($order['first_seen_at'] ?? null); $proximaDate = cc_test_parse_datetime($order['proxima_llamada_at'] ?? null); $followupDayInfo = cc_test_followup_day_info($order); $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']; $order['llamada_bloqueada_dia3'] = false; $order['llamada_bloqueada_dia3_mensaje'] = ''; $order['pendiente_logistica_destino'] = cc_test_is_tuani_store_key($storeKey) ? cc_test_pending_logistica_destination($order) : null; $order['pendiente_logistica'] = $order['pendiente_logistica_destino'] !== null; if ($isTuaniRecuperablesStore) { $order['es_nuevo_hoy'] = $recoverablesRecentSourceRowThreshold !== null && (int) ($order['source_row'] ?? 0) >= $recoverablesRecentSourceRowThreshold; } else { $order['es_nuevo_hoy'] = $firstSeenDate ? $firstSeenDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false; } $order['es_pendiente_hoy'] = !$order['es_promo_final'] && (in_array($order['estado'], $openStates, true) || $order['pendiente_logistica']); $order['es_cerrado'] = in_array($order['estado'], $closedStates, true); $order['assessor_key'] = cc_test_dashboard_bucket_key($order, $assessors, false); $order['assessor_label'] = $order['assessor_key'] !== '' ? (string) ($assessors[$order['assessor_key']]['label'] ?? $order['assessor_key']) : 'Sin asignar'; $order['matches_assessor_filter'] = $selectedAssessorFilter === '' || $order['assessor_key'] === $selectedAssessorFilter; if ($selectedAssessorFilter !== '' && !($order['matches_assessor_filter'] ?? false)) { continue; } if ($order['user_id'] !== null) { $userId = (int) $order['user_id']; if (isset($advisorTotalCounts[$userId])) { $advisorTotalCounts[$userId]++; if ($order['es_pendiente_hoy']) { $advisorOpenCounts[$userId]++; } } } $stats['total']++; if ($order['es_nuevo_hoy']) { $stats['nuevos_hoy']++; } if ($order['es_promo_final']) { $stats['promo_final']++; } if ($order['es_pendiente_hoy']) { $stats['pendientes_hoy']++; } if (in_array($order['estado'], cc_test_confirmed_states(), true)) { $stats['confirmados']++; } if (cc_test_is_recovered_today($order, $todayStart)) { $stats['recuperados']++; } if ($order['estado'] === 'SE ENVIO NUMERO DE CUENTA' && !$order['es_promo_final']) { $stats['seguimiento']++; } if ($order['estado'] === 'OBSERVADO' && !$order['es_promo_final']) { $stats['observados']++; } if ($order['es_cerrado']) { $stats['cerrados']++; } } unset($order); $pendingPromoFinalCounts = cc_test_followup_pending_promo_final_counts($orders); $performanceOrders = $selectedAssessorFilter !== '' ? array_values(array_filter($orders, static function (array $order) use ($selectedAssessorFilter): bool { return ($order['matches_assessor_filter'] ?? false) === true; })) : $orders; $performanceValueOverride = ''; $performanceMetaTitleOverride = ''; $performanceDashboard = cc_test_build_performance_dashboard($performanceOrders, $assessors, 7); if ($isTuaniRecuperablesStore) { $performanceDashboard = cc_test_filter_dashboard_status_chart($performanceDashboard, ['Confirmados nuevos', 'Recuperados']); $recoverablesStatusDataset = (array) (($performanceDashboard['status_chart']['datasets'][0] ?? []) ?: []); $recoverablesStatusValues = array_map(static function ($value) { return (int) $value; }, array_values((array) ($recoverablesStatusDataset['data'] ?? []))); if (array_sum($recoverablesStatusValues) > 0) { $performanceValueOverride = ''; $performanceMetaTitleOverride = 'Confirmados nuevos + recuperados cubren el periodo mostrado.'; } } $performanceDashboardHtml = cc_test_render_performance_dashboard($performanceDashboard, [ 'title' => 'KPI diario por asesora', 'subtitle' => $isTuaniRecuperablesStore ? 'En carritos recuperables solo se muestran confirmados nuevos y recuperados; cuando hay datos, esas dos categorías visibles cubren el periodo mostrado y los repetidos solo suman cantidad.' : 'Confirmación = pedidos asignados hoy confirmados hoy, sin contar repetidos. Recuperación = pedidos asignados antes de hoy confirmados hoy. Rendimiento del día = confirmados totales (nuevos + recuperados) sobre asignados efectivos sin repetidos.', 'chart1_title' => $isTuaniRecuperablesStore ? 'Confirmados nuevos vs recuperados' : 'Distribución de estados', 'chart1_note' => $isTuaniRecuperablesStore ? 'La dona muestra solo confirmados nuevos y recuperados en esta bandeja.' : 'La dona separa los pedidos de hoy por estado: confirmados nuevos, recuperados, POR LLAMAR, DEVOLVER LLAMADA, OBSERVADO, SE ENVIO NUMERO DE CUENTA, repetidos (solo cantidad) y cancelados.', 'chart2_title' => 'Evolución de los últimos 7 días', 'chart2_note' => 'La línea de confirmados separa pedidos asignados hoy y pedidos asignados antes de hoy.', 'footnote' => 'Seguimiento abierto = pedidos asignados hoy en POR LLAMAR, DEVOLVER LLAMADA, OBSERVADO y SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.', 'performance_label' => $isTuaniRecuperablesStore ? 'Rendimiento del periodo' : 'Rendimiento del día', 'performance_value_hidden' => $isTuaniRecuperablesStore, 'status_detail_heading' => $isTuaniRecuperablesStore ? 'Detalle del KPI del periodo (%)' : 'Detalle del KPI de hoy (%)', 'performance_value' => $isTuaniRecuperablesStore ? '' : '', 'performance_meta_title' => $isTuaniRecuperablesStore ? 'Confirmados nuevos + recuperados cubren el periodo mostrado.' : '', ]); $orderedAssessorKeys = cc_test_ordered_assessor_keys($assessors); $recommendedAssessorKey = null; $minPending = PHP_INT_MAX; foreach ($orderedAssessorKeys as $assessorKey) { $id = (int) ($assessors[$assessorKey]['id'] ?? 0); $pending = (int) ($advisorOpenCounts[$id] ?? 0); if ($pending < $minPending) { $minPending = $pending; $recommendedAssessorKey = $assessorKey; } } $assessorSummaryStyles = [ 'KARINA' => ['text' => 'text-primary', 'badge' => 'bg-primary-subtle text-primary-emphasis border'], 'ESTEFANYA' => ['text' => 'text-success', 'badge' => 'bg-success-subtle text-success-emphasis border'], 'CARMEN' => ['text' => 'text-warning', 'badge' => 'bg-warning-subtle text-warning-emphasis border'], ]; $assessorSummaryCards = []; $tieneCupos = false; foreach ($orderedAssessorKeys as $assessorKey) { if (!isset($assessors[$assessorKey])) { continue; } $assessorId = (int) ($assessors[$assessorKey]['id'] ?? 0); if ($assessorId <= 0) { continue; } $pending = (int) ($advisorOpenCounts[$assessorId] ?? 0); $remaining = max(0, (int) $cupoObjetivoPorAsesora - $pending); if ($remaining > 0) { $tieneCupos = true; } $styles = $assessorSummaryStyles[$assessorKey] ?? ['text' => 'text-secondary', 'badge' => 'bg-secondary-subtle text-secondary-emphasis border']; $assessorSummaryCards[] = [ 'label' => (string) ($assessors[$assessorKey]['label'] ?? $assessorKey), 'pending' => $pending, 'remaining' => $remaining, 'text_class' => (string) ($styles['text'] ?? 'text-secondary'), 'badge_class' => (string) ($styles['badge'] ?? 'bg-secondary-subtle text-secondary-emphasis border'), 'accent_style' => cc_test_assessor_css_vars($assessorKey, $assessors[$assessorKey]['color_hex'] ?? null), ]; } usort($assessorSummaryCards, static function (array $a, array $b): int { $aPending = (int) ($a['pending'] ?? 0); $bPending = (int) ($b['pending'] ?? 0); if ($aPending !== $bPending) { return $bPending <=> $aPending; } return strcmp((string) ($a['label'] ?? ''), (string) ($b['label'] ?? '')); }); $activeAssessorSummaryCards = array_values(array_filter( $assessorSummaryCards, static fn (array $card): bool => (int) ($card['pending'] ?? 0) > 0 )); $inactiveAssessorSummaryCards = array_values(array_filter( $assessorSummaryCards, static fn (array $card): bool => (int) ($card['pending'] ?? 0) <= 0 )); $activeAssessorSummaryCount = count($activeAssessorSummaryCards); $inactiveAssessorSummaryCount = count($inactiveAssessorSummaryCards); $assessorUiCatalog = []; foreach ($orderedAssessorKeys as $assessorKey) { if (!isset($assessors[$assessorKey])) { continue; } $catalogColorHex = cc_test_assessor_effective_color_hex($assessorKey, $assessors[$assessorKey]['color_hex'] ?? null); $assessorUiCatalog[$assessorKey] = [ 'label' => (string) ($assessors[$assessorKey]['label'] ?? $assessorKey), 'color_hex' => $catalogColorHex, 'contrast_hex' => cc_test_assessor_contrast_text_hex($catalogColorHex), ]; } $visibleOrders = array_values(array_filter($performanceOrders, static function (array $order) use ($view, $todayStart): bool { return match ($view) { 'pendientes_hoy' => (bool) ($order['es_pendiente_hoy'] ?? false), 'nuevos_hoy' => (bool) ($order['es_nuevo_hoy'] ?? false), 'confirmados' => in_array(($order['estado'] ?? ''), cc_test_confirmed_states(), true), 'recuperados' => cc_test_is_recovered_today($order, $todayStart), 'seguimiento' => ($order['estado'] ?? '') === 'SE ENVIO NUMERO DE CUENTA' && !($order['es_promo_final'] ?? false), 'promo_final' => (bool) ($order['es_promo_final'] ?? false), 'observados' => ($order['estado'] ?? '') === 'OBSERVADO' && !($order['es_promo_final'] ?? false), 'cerrados' => (bool) ($order['es_cerrado'] ?? false), default => true, }; })); usort($visibleOrders, static function (array $a, array $b) use ($view, $storeKey): int { $aImport = cc_test_import_time($a); $bImport = cc_test_import_time($b); if (cc_test_is_recoverables_store_key($storeKey) && in_array($view, ['pendientes_hoy', 'todos'], true)) { return cc_test_compare_recoverables_orders_desc($a, $b); } if ($view === 'promo_final') { $aDays = (int) ($a['dias_seguimiento'] ?? 0); $bDays = (int) ($b['dias_seguimiento'] ?? 0); if ($aDays !== $bDays) { return $bDays <=> $aDays; } if ($aImport !== $bImport) { return $aImport <=> $bImport; } } if ($view === 'recuperados') { $aRecovered = cc_test_dashboard_action_datetime($a); $bRecovered = cc_test_dashboard_action_datetime($b); if ($aRecovered && $bRecovered && $aRecovered != $bRecovered) { return $bRecovered <=> $aRecovered; } if ($aRecovered && !$bRecovered) { return -1; } if (!$aRecovered && $bRecovered) { return 1; } } if ($view === 'nuevos_hoy') { $aNew = cc_test_parse_datetime($a['first_seen_at'] ?? null); $bNew = cc_test_parse_datetime($b['first_seen_at'] ?? null); if ($aNew && $bNew && $aNew != $bNew) { return $bNew <=> $aNew; } if ($aNew && !$bNew) { return -1; } if (!$aNew && $bNew) { return 1; } } if ($view === "pendientes_hoy") { $aPendientePromoFinal = !empty($a['promo_final_pendiente']); $bPendientePromoFinal = !empty($b['promo_final_pendiente']); if ($aPendientePromoFinal !== $bPendientePromoFinal) { return $aPendientePromoFinal ? -1 : 1; } $aPendienteLogistica = !empty($a['pendiente_logistica']); $bPendienteLogistica = !empty($b['pendiente_logistica']); if ($aPendienteLogistica !== $bPendienteLogistica) { return $aPendienteLogistica ? -1 : 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) { return $aDue <=> $bDue; } if ($aDue && !$bDue) { return 1; } if (!$aDue && $bDue) { return -1; } if (cc_test_is_tuani_store_key($storeKey)) { $aSourceRow = (int) ($a['source_row'] ?? 0); $bSourceRow = (int) ($b['source_row'] ?? 0); if ($aSourceRow > 0 && $bSourceRow > 0 && $aSourceRow !== $bSourceRow) { return $bSourceRow <=> $aSourceRow; } } } if ($view === 'todos') { if (cc_test_is_recoverables_store_key($storeKey)) { return cc_test_compare_recoverables_orders_desc($a, $b); } if ($aImport !== $bImport) { return $bImport <=> $aImport; } $aSourceRow = (int) ($a['source_row'] ?? 0); $bSourceRow = (int) ($b['source_row'] ?? 0); if ($aSourceRow !== $bSourceRow) { return $bSourceRow <=> $aSourceRow; } $aId = (int) ($a['id'] ?? 0); $bId = (int) ($b['id'] ?? 0); if ($aId !== $bId) { return $bId <=> $aId; } return strcmp((string) ($a['source_key'] ?? ''), (string) ($b['source_key'] ?? '')); } if ($aImport !== $bImport) { return $bImport <=> $aImport; } return cc_test_order_time($b) <=> cc_test_order_time($a); }); $visibleOrdersTotalCount = count($visibleOrders); $todosPageSize = 200; $todosPage = 1; $todosTotalPages = 1; $todosPageStart = 0; $todosPageEnd = 0; $todosPaginationPages = []; $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(); } if ($isTuaniRecuperablesStore) { $pageTitle = $storeLabel . ' | Base de Datos Pedidos'; $pageDescription = 'Bandeja compartida para que cualquier asesora revise ' . mb_strtolower($storeLabel) . ' con vistas de seguimiento y observados cargados desde Drive.'; } else { $pageTitle = 'Base de Datos Pedidos | Asignación'; $pageDescription = 'Bandejas de asignación y gestión de estados para el Call Center, cargadas desde Drive.'; } $callCenterSectionLabel = $isTuaniRecuperablesStore ? $storeLabel : 'Pedidos Asignados'; require_once 'layout_header.php'; ?>

Base de Datos Pedidos

Tienda activa: Solo pedidos válidos: IDs #D... Abrir (#D...)
Dentro de
Pedidos $childConfig): ?>
Drive detectado: filas Agregados: : última fila del Drive · último pedido : distribución desde fila · último procesado Extracción: desde fila Actualización manual Asesora: Ver todas Ver vista previa Drive
Aviso rápido
Última fila real del Drive: · Último pedido detectado:
Te ayuda a comprobar de un vistazo si la cola llegó al final.

Agregar pedidos por Excel

Para: . Los pedidos cargados conservan su número original y no afectan la secuencia de Drive.
Descargar plantilla Excel
La plantilla sale con el mismo orden de columnas que tu Drive.
$viewCard): ?>

Rendimiento por asesora

Mostramos primero las asesoras con actividad para que la vista se vea más limpia. Si necesitas sumar otra, usa la tarjeta de alta al final.
Activas: Sin actividad:
Asigna pedidos sin asignar hasta completar el cupo.
0): ?>
No hay asesoras con actividad en este momento
Cuando una asesora reciba pedidos, aparecerá aquí. Mientras tanto puedes crear una nueva o revisar las existentes sin actividad.
Agregar asesora
0): ?>
Ver asesora sin actividad
0): ?> Pendientes:
Cupo restante ()
Reparto Siguiente: Sin asesoras disponibles Sin asignar: Elegibles:

Reparto de pedidos pendientes

Vista compacta para revisar y cambiar el reparto sin ocupar tanto alto.
Asesoras totales: Disponibles: Ciclo:
·
Automático reparte los pedidos pendientes al abrir o refrescar la pantalla.

Este resumen muestra solo los pedidos importados desde la hoja, no contadores de otras pantallas.

Pedidos importados: Filas de origen: Página de · hasta por página pedidos únicos en esta bandeja 1): ?> Página de · 200 por página
0 seleccionados
Usa el botón de la izquierda de cada pedido y asigna varios de una sola vez.
Desde aquí puedes reasignar pedidos tomados sin liberarlos primero; la asesora seleccionada reemplaza a la anterior.
'Falta subirlo a Ruta Contraentrega.', 'rotulado' => 'Falta subirlo a Pedidos Rotulados.', default => '', }; $subirLogisticaButtonLabel = 'Subir pedido'; $subirLogisticaNoteClass = 'alert alert-secondary py-2 px-3 mt-2 mb-0'; $subirLogisticaNoteHtml = 'Selecciona CONFIRMADO CONTRAENTREGA o CONFIRMADO ENVIO y completa Confirmación de pedido (producto, cantidad y precio) para enviarlo a logística.'; if ($canShowTuaniLogisticaButton) { if ($order['estado'] === 'CONFIRMADO CONTRAENTREGA') { if (!empty($order['ruta_contraentrega_pedido_id'])) { $subirLogisticaButtonLabel = 'Actualizar en ruta'; $subirLogisticaNoteClass = 'alert alert-success py-2 px-3 mt-2 mb-0'; $subirLogisticaNoteHtml = 'En Ruta Contraentrega #' . (int) $order['ruta_contraentrega_pedido_id'] . '. Si cambias algo, usa este botón para actualizarlo.'; } else { $subirLogisticaButtonLabel = 'Subir a ruta'; $subirLogisticaNoteClass = 'alert alert-warning py-2 px-3 mt-2 mb-0'; $subirLogisticaNoteHtml = 'ALERTA: este pedido está en CONFIRMADO CONTRAENTREGA, pero aún no se ha subido a Ruta Contraentrega. Usa este botón para subirlo.'; } } elseif ($order['estado'] === 'CONFIRMADO ENVIO') { if (!empty($order['pedido_rotulado_pedido_id'])) { $subirLogisticaButtonLabel = 'Actualizar rotulado'; $subirLogisticaNoteClass = 'alert alert-success py-2 px-3 mt-2 mb-0'; $subirLogisticaNoteHtml = 'En Pedidos Rotulados #' . (int) $order['pedido_rotulado_pedido_id'] . '. Si cambias algo, usa este botón para actualizarlo.'; } else { $subirLogisticaButtonLabel = 'Subir a rotulados'; $subirLogisticaNoteClass = 'alert alert-warning py-2 px-3 mt-2 mb-0'; $subirLogisticaNoteHtml = 'ALERTA: este pedido está en CONFIRMADO ENVIO, pero aún no se ha subido a Pedidos Rotulados. Usa este botón para subirlo.'; } } } ob_start(); ?>
Sel. Asignación / acciones N° Pedido Cliente Ubicación editable Pedido Gestión
No hay pedidos únicos en esta bandeja por ahora.
00:00
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; } } } $selectedAssessorKeyForSelect = $assignedAssessorKey !== null ? (string) $assignedAssessorKey : ''; $buttonLabel = $unassigned ? 'Asignar pedido' : 'Guardar asignación'; $assignAccentKey = $selectedAssessorKeyForSelect !== '' ? (string) $selectedAssessorKeyForSelect : ($assignedAssessorKey !== null ? (string) $assignedAssessorKey : ''); $assignAccentHex = $assignAccentKey !== '' ? cc_test_assessor_effective_color_hex( $assignAccentKey, $assessors[$assignAccentKey]['color_hex'] ?? null ) : '#FFFFFF'; $assignFormStyle = cc_test_assessor_css_vars( $assignAccentKey !== '' ? $assignAccentKey : 'DEFAULT', $assignAccentHex ); $assignFormIsUnassigned = $assignAccentKey === ''; $badgeLabel = $unassigned ? 'Sin asignar' : (string) ($assessors[$assignedAssessorKey]['label'] ?? ($assignedAssessorKey ?? 'Asignado')); ?>
Asignar a asesora
WhatsApp
DÍA
ID Drive:
Celular:
DNI:
Dirección:
Referencia:
Departamento:
Provincia:
Distrito:
Coordenadas:
DISTRITO 1:
Cantidad: ·
Ingreso Drive:
Observación Drive:
llamadas Seguimiento
Número de cuenta enviado hace día ·
Seguimiento: · desde
Próxima llamada:
Entrega programada:
Subido a Ruta Contraentrega # ·
Subido a Pedidos Rotulados # ·
Última gestión:
Nota:
1): ?>
0): ?> Mostrando - de pedidos · 200 por página No hay pedidos para mostrar en esta vista.
1): ?>
Llamada preparada para AirDroid

Número listo para copiar

Pedido
-
Cliente
-
Número
-