34849-vm/gestiones_callcenter.php
2026-08-10 19:41:44 +00:00

4333 lines
251 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
session_start();
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
require_once 'db/config.php';
require_once 'includes/callcenter_test_helpers.php';
require_once 'includes/drive_test_orders.php';
require_once 'includes/agregados_excel_import.php';
require_once 'includes/contraentrega_cobertura.php';
$provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento();
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
$sedesShalom = cc_test_fetch_shalom_sedes(db());
$pageTitle = 'Base de Datos Pedidos | Asignación';
$pageDescription = 'Bandejas de asignación, reparto intercalado diario y gestión de estados para el Call Center, cargadas desde Drive.';
$role = (string) ($_SESSION['user_role'] ?? '');
if (!in_array($role, ['Administrador', 'admin'], true)) {
http_response_code(403);
require_once 'layout_header.php';
echo "<div class='container py-5'><div class='alert alert-danger mb-0'>Acceso denegado.</div></div>";
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 '<span class="badge bg-warning-subtle text-warning-emphasis border"><i class="bi bi-lock-fill me-1"></i>Celular oculto</span>';
}
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: <strong>' . $recoverablesStoreLabelHtml . '</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja abre por defecto en <strong>Todos</strong>. 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 <strong>' . $recoverablesMainStoreLabelHtml . ' principal</strong>, se marca en gris como <strong>Pedido repetido en tienda</strong> solo en esta bandeja. Las vistas <strong>' . $recoverablesStoreLabelHtml . '</strong> y <strong>Todos</strong> arrancan en la fila ' . (int) ($storeConfig['startRow'] ?? 0) . ', solo toman pedidos desde esa fila y se ordenan por el número <strong>#D</strong> 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. <strong>Promo Final</strong> se habilita para mover el pedido desde el Día 4; la imagen de sustento se puede cargar desde el Día 3 para dejarlo listo. En las vistas <strong>Todos</strong> y <strong>Últimos 4 días hábiles</strong>, la llamada y la gestión siguen disponibles desde el panel.';
$errorMessage = null;
$noticeMessage = null;
$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';
?>
<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-timer {
font-size: .78rem;
line-height: 1;
}
.cc-callcenter-row.is-selected td:first-child {
box-shadow: inset 4px 0 0 #0d6efd;
}
.cc-callcenter-assessor-summary-card {
border-left: .45rem solid var(--cc-assessor-accent, #adb5bd) !important;
background-color: rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .16);
box-shadow: inset 0 0 0 1px rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .22);
}
.cc-callcenter-assign-form {
width: 100%;
padding: .72rem !important;
border: 1px solid rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .38) !important;
border-left: .55rem solid var(--cc-assessor-accent, #adb5bd);
background-color: var(--cc-assessor-accent, #adb5bd);
color: var(--cc-assessor-accent-contrast, #212529);
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned {
border-color: rgba(0, 0, 0, .10) !important;
border-left-color: #FFFFFF;
background-color: #FFFFFF;
color: #212529;
box-shadow: 0 .65rem 1.4rem rgba(15, 23, 42, .06), inset 0 0 0 1px rgba(0, 0, 0, .05);
}
.cc-callcenter-assign-form .text-muted {
color: var(--cc-assessor-accent-contrast, #212529) !important;
opacity: .88;
}
.cc-callcenter-assign-form.is-unassigned .text-muted {
color: #6C757D !important;
opacity: 1;
}
.cc-callcenter-assign-form .cc-callcenter-assessor-badge {
background-color: rgba(255, 255, 255, .18);
color: var(--cc-assessor-accent-contrast, #212529);
border-color: rgba(255, 255, 255, .42) !important;
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assessor-badge {
background-color: #F8F9FA;
color: #6C757D;
border-color: rgba(0, 0, 0, .12) !important;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .04);
}
.cc-callcenter-assign-form .cc-callcenter-assessor-badge .cc-callcenter-color-dot {
border: 1px solid rgba(255, 255, 255, .65);
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assessor-badge .cc-callcenter-color-dot {
background: #ADB5BD;
border-color: rgba(0, 0, 0, .12);
}
.cc-callcenter-assign-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: .35rem;
padding: .5rem;
border-radius: .95rem;
background-color: transparent;
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assign-row {
background-color: #FFFFFF;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}
.cc-callcenter-assign-row .form-select {
min-width: 10rem;
flex: 1 1 10rem;
border-color: rgba(255, 255, 255, .48);
background-color: rgba(255, 255, 255, .18);
color: var(--cc-assessor-accent-contrast, #212529);
font-weight: 700;
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assign-row .form-select {
border-color: rgba(0, 0, 0, .16);
background-color: #FFFFFF;
color: #212529;
}
.cc-callcenter-assign-row .form-select:focus {
border-color: rgba(255, 255, 255, .72);
box-shadow: 0 0 0 .2rem rgba(255, 255, 255, .18);
}
.cc-callcenter-assign-submit {
flex: 0 0 auto;
white-space: nowrap;
border-color: rgba(255, 255, 255, .42) !important;
background-color: rgba(255, 255, 255, .18) !important;
color: var(--cc-assessor-accent-contrast, #212529) !important;
font-weight: 700;
box-shadow: none;
}
.cc-callcenter-assessor-badge {
display: inline-flex;
align-items: center;
gap: .3rem;
background-color: var(--cc-assessor-accent, #adb5bd);
color: var(--cc-assessor-accent-contrast, #212529);
border-color: var(--cc-assessor-accent, #adb5bd) !important;
font-weight: 600;
white-space: nowrap;
}
.cc-callcenter-color-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .3rem;
padding: .2rem .55rem;
white-space: nowrap;
border-color: rgba(255, 255, 255, .42) !important;
background-color: rgba(255, 255, 255, .18);
color: var(--cc-assessor-accent-contrast, #212529);
font-weight: 700;
box-shadow: none;
}
.cc-callcenter-color-trigger:hover:not(:disabled),
.cc-callcenter-assign-submit:hover:not(:disabled) {
filter: brightness(.97);
transform: translateY(-1px);
}
.cc-callcenter-color-trigger:disabled {
opacity: .72;
background-color: rgba(255, 255, 255, .12);
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-color-trigger {
background-color: #F1F3F5;
color: #6C757D;
border-color: rgba(0, 0, 0, .12) !important;
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-color-trigger:disabled {
background-color: #F1F3F5;
}
.cc-callcenter-color-trigger .cc-callcenter-color-dot {
width: .72rem;
height: .72rem;
border-radius: 999px;
background: var(--cc-assessor-accent, #adb5bd);
border: 1px solid rgba(255, 255, 255, .65);
box-shadow: none;
flex: 0 0 auto;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-color-trigger .cc-callcenter-color-dot {
background: #ADB5BD;
border-color: rgba(0, 0, 0, .16);
}
.cc-callcenter-assessor-badge .cc-callcenter-color-dot {
width: .55rem;
height: .55rem;
border-radius: 999px;
background: var(--cc-assessor-accent-contrast, #212529);
border: 1px solid rgba(255, 255, 255, .35);
box-shadow: none;
flex: 0 0 auto;
}
.cc-callcenter-assessor-summary-card .cc-callcenter-color-dot {
width: .55rem;
height: .55rem;
border-radius: 999px;
background: var(--cc-assessor-accent, #adb5bd);
border: 1px solid rgba(0, 0, 0, .12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
flex: 0 0 auto;
}
.cc-callcenter-assessor-summary-card.is-inactive {
opacity: .82;
filter: saturate(.92);
}
.cc-callcenter-assessor-add-card {
border: 1px dashed rgba(13, 110, 253, .28) !important;
background: linear-gradient(180deg, rgba(13, 110, 253, .06), rgba(13, 110, 253, .02));
box-shadow: inset 0 0 0 1px rgba(13, 110, 253, .06);
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.cc-callcenter-assessor-add-card:hover {
transform: translateY(-2px);
border-color: rgba(13, 110, 253, .55) !important;
box-shadow: 0 .75rem 1.5rem rgba(13, 110, 253, .08), inset 0 0 0 1px rgba(13, 110, 253, .08);
}
.cc-callcenter-add-icon {
width: 2.4rem;
height: 2.4rem;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(13, 110, 253, .12);
color: #0d6efd;
flex: 0 0 auto;
}
.cc-callcenter-empty-assessor-state {
border: 1px dashed rgba(13, 110, 253, .2);
background: linear-gradient(180deg, rgba(13, 110, 253, .05), rgba(13, 110, 253, .02));
}
.cc-callcenter-details-summary {
list-style: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .55rem .9rem;
border-radius: 999px;
background: #F8F9FA;
border: 1px solid rgba(0, 0, 0, .08);
color: #495057;
font-weight: 600;
}
.cc-callcenter-details-summary::-webkit-details-marker {
display: none;
}
.cc-callcenter-action-btn {
padding: .2rem .55rem;
line-height: 1.1;
font-size: .78rem;
white-space: nowrap;
}
.cc-reparto-card .card-body {
padding: .85rem !important;
}
.cc-reparto-chip {
padding: .32rem .55rem;
font-size: .72rem;
line-height: 1.15;
}
.cc-reparto-summary-note {
font-size: .82rem;
line-height: 1.35;
}
.cc-reparto-state-grid {
max-height: 18rem;
overflow: auto;
padding-right: .25rem;
}
.cc-reparto-state-card {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .36);
}
@media (min-width: 992px) {
.cc-callcenter-table tbody td {
padding-top: .42rem;
padding-bottom: .42rem;
}
}
.cc-callcenter-modal-timer {
min-width: 6.5rem;
white-space: nowrap;
font-variant-numeric: tabular-nums;
letter-spacing: .02em;
justify-content: center;
}
.cc-callcenter-modal-timer.is-running {
background: rgba(var(--bs-primary-rgb), 0.10) !important;
border-color: rgba(var(--bs-primary-rgb), 0.22) !important;
color: var(--bs-primary-text-emphasis) !important;
}
</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">
<div>
<h1 class="h2 fw-bold mb-1"><i class="bi bi-headset text-primary"></i> Base de Datos Pedidos</h1>
<div class="d-flex flex-wrap gap-2 mt-2">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis border px-3 py-2">Tienda activa: <?php echo htmlspecialchars($storeLabel); ?></span>
<?php if ($isTuaniRecuperablesStore): ?>
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border px-3 py-2">Solo pedidos válidos: IDs #D...</span>
<?php elseif ($canOpenRecoverablesStore): ?>
<a href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', ['store' => $recoverablesStoreKey, 'view' => 'todos'])); ?>" class="btn btn-sm btn-outline-success">Abrir <?php echo htmlspecialchars($recoverablesStoreLabel !== '' ? $recoverablesStoreLabel : 'Carritos recuperables'); ?> (#D...)</a>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<a href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => 'pendientes_hoy', 'store' => $storeKey], $persistedAssessorParams))); ?>" class="btn btn-sm <?php echo $view !== 'todos' ? 'btn-primary' : 'btn-outline-primary'; ?>">Bandeja principal</a>
<a href="<?php echo htmlspecialchars(cc_test_build_url('call_center_pro.php', array_merge(['view' => $callCenterDefaultView, 'store' => $storeKey], $persistedAssessorParams))); ?>" class="btn btn-sm btn-outline-dark"><?php echo htmlspecialchars($callCenterSectionLabel); ?></a>
</div>
<ul class="nav nav-pills mt-3 flex-wrap gap-2" role="tablist" aria-label="Selector de tienda">
<?php foreach ($storeNavGroups as $groupKey => $group): ?>
<?php $groupConfig = $group['config'] ?? []; ?>
<li class="nav-item">
<a class="nav-link <?php echo $activeStoreGroupKey === $groupKey ? 'active' : ''; ?>" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => cc_test_is_recoverables_store_key($groupKey) ? 'todos' : $view, 'store' => $groupKey], $persistedAssessorParams))); ?>">
<?php echo htmlspecialchars((string) ($groupConfig['label'] ?? strtoupper((string) $groupKey))); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php if (!empty($activeStoreGroupChildren)): ?>
<div class="mt-3 p-3 rounded-4 border bg-light">
<div class="small text-uppercase text-muted fw-semibold mb-2">Dentro de <?php echo htmlspecialchars($activeStoreGroupLabel); ?></div>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-sm <?php echo $storeKey === $activeStoreGroupKey ? 'btn-primary' : 'btn-outline-primary'; ?>" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => cc_test_is_recoverables_store_key($activeStoreGroupKey) ? 'todos' : $view, 'store' => $activeStoreGroupKey], $persistedAssessorParams))); ?>">Pedidos <?php echo htmlspecialchars($activeStoreGroupLabel); ?></a>
<?php foreach ($activeStoreGroupChildren as $childKey => $childConfig): ?>
<a class="btn btn-sm <?php echo $storeKey === $childKey ? 'btn-primary' : 'btn-outline-primary'; ?>" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => cc_test_is_recoverables_store_key($childKey) ? 'todos' : $view, 'store' => $childKey], $persistedAssessorParams))); ?>">
<?php echo htmlspecialchars((string) ($childConfig['label'] ?? strtoupper((string) $childKey))); ?>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<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 ($isTuaniRecuperablesStore && $recoverablesNotice !== null): ?><?php echo htmlspecialchars(mb_strtoupper($storeLabel)); ?>: última fila del Drive <?php echo htmlspecialchars((string) $recoverablesNotice['row_label']); ?> · último pedido <?php echo htmlspecialchars((string) $recoverablesNotice['label']); ?><?php elseif ($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">Actualización manual</span>
<?php if ($selectedAssessorFilterLabel !== ''): ?>
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis border px-3 py-2">Asesora: <?php echo htmlspecialchars($selectedAssessorFilterLabel); ?></span>
<a href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', ['view' => $view, 'store' => $storeKey, 'assessor' => 'TODAS'])); ?>" class="btn btn-outline-secondary btn-sm">Ver todas</a>
<?php endif; ?>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
<button type="button" class="btn btn-primary btn-sm" onclick="window.location.reload();"><i class="bi bi-arrow-clockwise me-1"></i>Actualizar pedidos</button>
<?php if ($isTuaniRecuperablesStore && $isAdmin): ?>
<form method="post" class="d-inline" onsubmit="return confirm('Esto volverá a leer la cola desde la fila <?php echo (int) $recoverablesResyncRow; ?>. ¿Continuar?');">
<input type="hidden" name="action" value="reimport_recoverables_queue">
<input type="hidden" name="from_row" value="<?php echo (int) $recoverablesResyncRow; ?>">
<button type="submit" class="btn btn-outline-warning btn-sm"><i class="bi bi-arrow-repeat me-1"></i>Reimportar desde fila <?php echo (int) $recoverablesResyncRow; ?></button>
</form>
<?php endif; ?>
</div>
</div>
</section>
<?php if ($noticeMessage !== null): ?>
<section class="alert alert-success" role="alert">
<?php echo htmlspecialchars($noticeMessage); ?>
</section>
<?php endif; ?>
<?php if ($errorMessage !== null): ?>
<section class="alert alert-danger" role="alert">
<strong>No se pudo cargar el panel.</strong><br>
<?php echo htmlspecialchars($errorMessage); ?>
</section>
<?php else: ?>
<?php if ($recoverablesNotice !== null): ?>
<section class="alert alert-info border-0 shadow-sm mb-4" role="status">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
<div>
<div class="small text-uppercase fw-semibold mb-1">Aviso rápido</div>
<div>
Última fila real del Drive:
<strong><?php echo htmlspecialchars((string) $recoverablesNotice['row_label']); ?></strong>
· Último pedido detectado:
<strong><?php echo htmlspecialchars((string) $recoverablesNotice['label']); ?></strong>
</div>
</div>
<div class="small text-muted">
Te ayuda a comprobar de un vistazo si la cola llegó al final.
</div>
</div>
</section>
<?php endif; ?>
<?php echo $performanceDashboardHtml ?? ''; ?>
<section class="mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
<div>
<h2 class="h5 fw-bold mb-1">
<i class="bi bi-upload text-primary"></i> Agregar pedidos por Excel
</h2>
<div class="text-muted small">
Para: <strong><?php echo htmlspecialchars($storeLabel); ?></strong>.
Los pedidos cargados conservan su número original y no afectan la secuencia de Drive.
</div>
<div class="mt-2">
<a href="download_agregados_pedidos_template.php" class="btn btn-outline-primary btn-sm">Descargar plantilla Excel</a>
<div class="small text-muted mt-2">La plantilla sale con el mismo orden de columnas que tu Drive.</div>
</div>
</div>
<div class="w-100 w-lg-auto">
<form method="post" enctype="multipart/form-data" class="d-flex flex-column gap-2">
<input type="hidden" name="action" value="upload_agregados_excel">
<input
type="file"
name="agregados_excel"
class="form-control form-control-sm"
accept=".xlsx,.csv"
required
>
<button type="submit" class="btn btn-primary btn-sm w-100">Importar pedidos</button>
</form>
</div>
</div>
<?php if (!empty($uploadErrorMessage)): ?>
<div class="alert alert-danger mt-3 mb-0" role="alert">
<?php echo htmlspecialchars($uploadErrorMessage); ?>
</div>
<?php endif; ?>
</div>
</div>
</section>
<section class="row g-3 mb-4">
<?php foreach ($viewCards as $viewKey => $viewCard): ?>
<?php $isActiveCard = $view === $viewKey; ?>
<?php $cardClass = $isActiveCard ? $viewCard['active_class'] : 'bg-white'; ?>
<?php $labelClass = $isActiveCard && str_contains((string) ($viewCard['active_class'] ?? ''), 'text-white') ? 'opacity-75' : 'text-muted'; ?>
<div class="<?php echo htmlspecialchars($viewCardColumnClass); ?>">
<a href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => $viewKey, 'store' => $storeKey], $persistedAssessorParams))); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo htmlspecialchars($cardClass); ?>">
<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>
</div>
</article>
</a>
</div>
<?php endforeach; ?>
</section>
<section class="mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-start gap-3 mb-3">
<div class="flex-grow-1">
<h2 class="h5 fw-bold mb-1">
<i class="bi bi-activity text-primary"></i> Rendimiento por asesora
</h2>
<div class="text-muted small">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.</div>
<div class="d-flex flex-wrap gap-2 mt-2">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis border">Activas: <?php echo (int) $activeAssessorSummaryCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border">Sin actividad: <?php echo (int) $inactiveAssessorSummaryCount; ?></span>
</div>
</div>
<form method="post" class="d-flex flex-column gap-2 align-items-stretch" style="min-width: 220px;">
<input type="hidden" name="action" value="auto_assign_cupo">
<button type="submit" class="btn btn-primary btn-sm w-100" <?php echo $tieneCupos ? '' : 'disabled'; ?>>
Rellenar cupos
</button>
<div class="small text-muted">Asigna pedidos sin asignar hasta completar el cupo.</div>
</form>
</div>
<?php if ($activeAssessorSummaryCount > 0): ?>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3">
<?php foreach ($activeAssessorSummaryCards as $assessorCard): ?>
<?php $pendingCount = (int) ($assessorCard['pending'] ?? 0); ?>
<div class="col">
<div class="card border-0 shadow-sm h-100 cc-callcenter-assessor-summary-card" style="<?php echo htmlspecialchars((string) $assessorCard['accent_style']); ?>">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center gap-2">
<div class="fw-bold d-flex align-items-center gap-2">
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<span><?php echo htmlspecialchars((string) $assessorCard['label']); ?></span>
</div>
<?php if ($pendingCount > 0): ?>
<span class="badge rounded-pill border cc-callcenter-assessor-badge text-nowrap">Pendientes: <?php echo $pendingCount; ?></span>
<?php endif; ?>
</div>
<div class="mt-2">
<div class="small text-muted">Cupo restante (<?php echo (int) $cupoObjetivoPorAsesora; ?>)</div>
<div class="h3 fw-bold mb-0"><?php echo (int) $assessorCard['remaining']; ?></div>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<div class="col">
<a href="<?php echo htmlspecialchars(cc_test_build_url('manage_users.php', ['role' => 'Asesor']) . '#crear-asesora'); ?>" class="card border-0 shadow-sm h-100 text-decoration-none cc-callcenter-assessor-add-card">
<div class="card-body d-flex flex-column justify-content-between h-100">
<div>
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis border">Opcional</span>
<h3 class="h6 fw-bold mt-3 mb-1">Agregar más asesoras</h3>
<p class="text-muted small mb-0">Abre el alta de usuarios cuando quieras sumar otra persona al equipo.</p>
</div>
<div class="d-inline-flex align-items-center gap-2 mt-3 fw-semibold">
<span class="cc-callcenter-add-icon" aria-hidden="true"><i class="bi bi-person-plus"></i></span>
<span>Abrir alta</span>
</div>
</div>
</a>
</div>
</div>
<?php else: ?>
<div class="cc-callcenter-empty-assessor-state rounded-4 p-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div>
<div class="fw-semibold mb-1">No hay asesoras con actividad en este momento</div>
<div class="text-muted small mb-0">Cuando una asesora reciba pedidos, aparecerá aquí. Mientras tanto puedes crear una nueva o revisar las existentes sin actividad.</div>
</div>
<a href="<?php echo htmlspecialchars(cc_test_build_url('manage_users.php', ['role' => 'Asesor']) . '#crear-asesora'); ?>" class="btn btn-outline-primary">
<i class="bi bi-person-plus me-1"></i> Agregar asesora
</a>
</div>
</div>
<?php endif; ?>
<?php if ($inactiveAssessorSummaryCount > 0): ?>
<details class="mt-3">
<summary class="cc-callcenter-details-summary">Ver <?php echo (int) $inactiveAssessorSummaryCount; ?> asesora<?php echo $inactiveAssessorSummaryCount === 1 ? '' : 's'; ?> sin actividad</summary>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3 mt-2">
<?php foreach ($inactiveAssessorSummaryCards as $assessorCard): ?>
<?php $pendingCount = (int) ($assessorCard['pending'] ?? 0); ?>
<div class="col">
<div class="card border-0 shadow-sm h-100 cc-callcenter-assessor-summary-card is-inactive" style="<?php echo htmlspecialchars((string) $assessorCard['accent_style']); ?>">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center gap-2">
<div class="fw-bold d-flex align-items-center gap-2">
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<span><?php echo htmlspecialchars((string) $assessorCard['label']); ?></span>
</div>
<?php if ($pendingCount > 0): ?>
<span class="badge rounded-pill border cc-callcenter-assessor-badge text-nowrap">Pendientes: <?php echo $pendingCount; ?></span>
<?php endif; ?>
</div>
<div class="mt-2">
<div class="small text-muted">Cupo restante (<?php echo (int) $cupoObjetivoPorAsesora; ?>)</div>
<div class="h3 fw-bold mb-0"><?php echo (int) $assessorCard['remaining']; ?></div>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</details>
<?php endif; ?>
</div>
</div>
</section>
<?php if ($isMainTuaniStore): ?>
<?php
$repartoModeLabel = (string) ($repartoPreview['mode_label'] ?? 'Manual');
$repartoModeClass = cc_test_reparto_mode_is_auto((string) ($repartoPreview['mode'] ?? 'manual'))
? 'bg-success-subtle text-success-emphasis border'
: 'bg-secondary-subtle text-secondary-emphasis border';
$repartoNextLabel = (string) ($repartoPreview['next_label'] ?? '');
$repartoAvailableCount = (int) ($repartoPreview['available_count'] ?? 0);
$repartoTotalCount = (int) ($repartoPreview['total_count'] ?? 0);
$repartoRotationDateLabel = !empty($repartoSettings['rotation_date'])
? cc_test_format_date((string) $repartoSettings['rotation_date'], 'Sin fecha')
: 'Sin ciclo aún';
?>
<section class="card border-0 shadow-sm mb-3 cc-reparto-card" id="repartoIntercalado">
<div class="card-body">
<div class="row g-2 align-items-start">
<div class="col-12 col-xl-6">
<div class="d-flex flex-wrap align-items-center gap-1 mb-2">
<span class="badge rounded-pill cc-reparto-chip <?php echo htmlspecialchars($repartoModeClass); ?>">Reparto <?php echo htmlspecialchars($repartoModeLabel); ?></span>
<?php if ($repartoNextLabel !== ''): ?>
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis border cc-reparto-chip">Siguiente: <?php echo htmlspecialchars($repartoNextLabel); ?></span>
<?php else: ?>
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border cc-reparto-chip">Sin asesoras disponibles</span>
<?php endif; ?>
<span class="badge rounded-pill bg-light text-dark border cc-reparto-chip">Sin asignar: <?php echo (int) $repartoUnassignedCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border cc-reparto-chip">Elegibles: <?php echo (int) $repartoEligibleUnassignedCount; ?></span>
</div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<h2 class="h6 fw-bold mb-0">Reparto de pedidos pendientes</h2>
<span class="text-muted small cc-reparto-summary-note">Vista compacta para revisar y cambiar el reparto sin ocupar tanto alto.</span>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill bg-light text-dark border cc-reparto-chip">Asesoras totales: <?php echo (int) $repartoTotalCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border cc-reparto-chip">Disponibles: <?php echo (int) $repartoAvailableCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border cc-reparto-chip">Ciclo: <?php echo htmlspecialchars($repartoRotationDateLabel); ?></span>
</div>
<div class="d-flex flex-wrap gap-2 mt-2">
<?php foreach (($repartoPreview['sequence'] ?? []) as $item): ?>
<span class="badge rounded-pill <?php echo htmlspecialchars((string) ($item['badge_class'] ?? 'bg-light text-dark border')); ?> cc-reparto-chip">
<?php echo htmlspecialchars((string) ($item['label'] ?? '')); ?> · <?php echo htmlspecialchars((string) ($item['state_label'] ?? '')); ?>
</span>
<?php endforeach; ?>
</div>
</div>
<div class="col-12 col-xl-6">
<form method="post" class="border rounded-4 p-2 bg-light h-100">
<input type="hidden" name="action" value="save_reparto_config">
<input type="hidden" name="store" value="<?php echo htmlspecialchars($storeKey); ?>">
<input type="hidden" name="view" value="<?php echo htmlspecialchars($view); ?>">
<div class="mb-2">
<label class="form-label small text-muted fw-semibold mb-1" for="repartoMode">Modo de reparto</label>
<select name="reparto_mode" id="repartoMode" class="form-select form-select-sm">
<?php foreach ([
'manual' => 'Manual',
'automatico' => 'Automático',
] as $modeValue => $modeLabel): ?>
<option value="<?php echo htmlspecialchars($modeValue); ?>"<?php echo ($repartoPreview['mode'] ?? 'manual') === $modeValue ? ' selected' : ''; ?>><?php echo htmlspecialchars($modeLabel); ?></option>
<?php endforeach; ?>
</select>
<div class="form-text">Automático reparte los pedidos pendientes al abrir o refrescar la pantalla.</div>
</div>
<div class="row row-cols-1 row-cols-md-2 g-2 cc-reparto-state-grid">
<?php foreach (($repartoPreview['sequence'] ?? []) as $item): ?>
<div class="col">
<div class="bg-white border rounded-3 p-2 h-100 cc-reparto-state-card">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<div class="fw-semibold small"><?php echo htmlspecialchars((string) ($item['label'] ?? '')); ?></div>
<span class="badge rounded-pill <?php echo htmlspecialchars((string) ($item['badge_class'] ?? 'bg-light text-dark border')); ?> cc-reparto-chip"><?php echo htmlspecialchars((string) ($item['state_label'] ?? '')); ?></span>
</div>
<select name="assessor_state[<?php echo htmlspecialchars((string) ($item['key'] ?? '')); ?>]" class="form-select form-select-sm">
<option value="disponible"<?php echo ($item['state'] ?? 'disponible') === 'disponible' ? ' selected' : ''; ?>>Disponible</option>
<option value="pausada"<?php echo ($item['state'] ?? 'disponible') === 'pausada' ? ' selected' : ''; ?>>Pausada</option>
<option value="ausente"<?php echo ($item['state'] ?? 'disponible') === 'ausente' ? ' selected' : ''; ?>>Ausente</option>
</select>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="d-flex flex-wrap gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Guardar configuración</button>
</div>
</form>
<form method="post" class="mt-2">
<input type="hidden" name="action" value="run_daily_reparto">
<input type="hidden" name="store" value="<?php echo htmlspecialchars($storeKey); ?>">
<input type="hidden" name="view" value="<?php echo htmlspecialchars($view); ?>">
<button type="submit" class="btn btn-outline-primary btn-sm w-100">Distribuir pedidos pendientes</button>
</form>
</div>
</div>
</div>
</section>
<?php endif; ?>
<section class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
<div>
<h2 class="h5 fw-bold mb-1"><?php echo htmlspecialchars($allowedViews[$view]); ?></h2>
<p class="text-muted small mb-1"><?php echo $viewStatesNote; ?></p>
<?php if ($isTuaniRecuperablesStore): ?>
<p class="small text-primary mb-0"><i class="bi bi-info-circle me-1"></i>Este resumen muestra solo los pedidos importados desde la hoja, no contadores de otras pantallas.</p>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<?php if ($isTuaniRecuperablesStore): ?>
<span class="badge bg-success-subtle text-success-emphasis border border-success-subtle">Pedidos importados: <?php echo (int) $recoverablesImportCount; ?></span>
<span class="badge bg-light text-dark border">Filas de origen: <?php echo $recoverablesFirstSourceRow !== null ? (int) $recoverablesFirstSourceRow . '' . (int) $recoverablesLastSourceRow : 'no disponibles'; ?></span>
<span class="badge bg-primary-subtle text-primary-emphasis border">Página <?php echo (int) $todosPage; ?> de <?php echo (int) $todosTotalPages; ?> · hasta <?php echo (int) $todosPageSize; ?> por página</span>
<?php else: ?>
<span class="badge bg-light text-dark border"><?php echo (int) $visibleOrdersTotalCount; ?> pedidos únicos en esta bandeja</span>
<?php if ($todosTotalPages > 1): ?>
<span class="badge bg-primary-subtle text-primary-emphasis border">Página <?php echo (int) $todosPage; ?> de <?php echo (int) $todosTotalPages; ?> · 200 por página</span>
<?php endif; ?>
<?php endif; ?>
</div>
</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">Desde aquí puedes reasignar pedidos tomados sin liberarlos primero; la asesora seleccionada reemplaza a la anterior.</div>
</div>
<div class="table-responsive">
<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>N° Pedido</th>
<th>Cliente</th>
<th>Ubicación editable</th>
<th>Pedido</th>
<th>Gestión</th>
</tr>
</thead>
<tbody>
<?php if (empty($visibleOrders)): ?>
<tr>
<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 únicos en esta bandeja por ahora.
</td>
</tr>
<?php else: ?>
<?php foreach ($visibleOrders as $order): ?>
<?php
$departamentoSeleccionado = trim((string) ($order['sede'] ?? ''));
$provinciaSeleccionada = trim((string) ($order['ciudad'] ?? ''));
$distritoSeleccionado = trim((string) ($order['distrito'] ?? ''));
$provinciasSeleccionadas = $provinciasPorDepartamentoContraentrega[$departamentoSeleccionado] ?? [];
$distritosSeleccionados = $distritosPorProvinciaContraentrega[$provinciaSeleccionada] ?? [];
$modalId = 'modalDriveTest' . $order['source_key'];
$badgeClass = cc_test_badge_class($order['estado']);
$followupDayInfo = $order['seguimiento_dia_info'] ?? null;
$promoFinalBadge = cc_test_promo_final_badge_info($order);
$phoneHiddenByDay1 = cc_test_followup_day1_phone_hidden($order, $pendingPromoFinalCounts);
$semaforoCuenta = cc_test_followup_semaforo($order);
$historial = cc_test_fetch_historial(db(), $order['source_key']);
$logisticaPendienteLabel = !empty($order['pendiente_logistica']) ? cc_test_pending_logistica_label($order) : null;
$logisticaPendienteDetalle = match ($order['pendiente_logistica_destino'] ?? null) {
'ruta' => '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 <strong>CONFIRMADO CONTRAENTREGA</strong> o <strong>CONFIRMADO ENVIO</strong> y completa <strong>Confirmación de pedido</strong> (producto, cantidad y precio) para enviarlo a logística.';
if ($canShowTuaniLogisticaButton) {
if ($order['estado'] === 'CONFIRMADO CONTRAENTREGA') {
if (!empty($order['ruta_contraentrega_pedido_id'])) {
$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 = '<strong>ALERTA:</strong> este pedido está en <strong>CONFIRMADO CONTRAENTREGA</strong>, 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 = '<strong>ALERTA:</strong> este pedido está en <strong>CONFIRMADO ENVIO</strong>, pero aún no se ha subido a Pedidos Rotulados. Usa este botón para subirlo.';
}
}
}
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')) . (!empty($order['pedido_repetido_en_tienda']) ? '--cc-row-bg:#CCCCCC;' : '')); ?>">
<td class="text-center">
<div class="badge bg-light text-dark border mb-2 text-nowrap" title="ID del pedido"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
<div class="badge rounded-pill bg-info-subtle text-info-emphasis border mb-2 text-nowrap d-inline-flex align-items-center gap-1 cc-callcenter-row-timer js-cc-callcenter-modal-timer d-none" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" aria-live="polite" title="Tiempo acumulado del pedido">
<span class="me-1" aria-hidden="true" style="font-size: 0.95em; line-height: 1;">⏱</span><span class="js-cc-callcenter-modal-timer-value">00:00</span>
</div>
<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;
}
}
}
$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'));
?>
<form method="post" class="border rounded-3 p-1 text-start cc-callcenter-assign-form<?php echo $assignFormIsUnassigned ? ' is-unassigned' : ''; ?>" style="<?php echo htmlspecialchars($assignFormStyle); ?>" autocomplete="off">
<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>
<span class="badge border cc-callcenter-assessor-badge">
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<span class="cc-callcenter-assessor-label"><?php echo htmlspecialchars($badgeLabel); ?></span>
</span>
</div>
<div class="cc-callcenter-assign-row">
<input type="hidden" name="target_assessor" class="js-assessor-hidden" value="<?php echo htmlspecialchars((string) $selectedAssessorKeyForSelect); ?>">
<select name="target_assessor" class="form-select form-select-sm js-assessor-select" data-server-assessor="<?php echo htmlspecialchars((string) $selectedAssessorKeyForSelect); ?>" autocomplete="off" data-lpignore="true" data-1p-ignore="true" data-bwignore="true">
<option value=""<?php echo ($selectedAssessorKeyForSelect === '') ? ' selected' : ''; ?>>Sin asignar</option>
<?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; ?>
</select>
<button type="button" class="btn btn-sm cc-callcenter-color-trigger js-assessor-color-trigger" title="Selecciona una asesora para cambiar su color" aria-label="Selecciona una asesora para cambiar su color"<?php echo $selectedAssessorKeyForSelect === '' ? ' disabled' : ''; ?>>
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<i class="bi bi-palette"></i>
<span>Color</span>
</button>
<button type="submit" class="btn btn-sm cc-callcenter-assign-submit cc-callcenter-action-btn"><?php echo htmlspecialchars((string) $buttonLabel); ?></button>
</div>
</form>
<?php endif; ?>
<?php
$phoneLocked = false;
$phoneLockMessage = (string) ($order['llamada_bloqueada_dia3_mensaje'] ?? '');
if ($phoneLocked && $phoneLockMessage === '') {
$phoneLockMessage = 'Celular oculto mientras completas los pendientes del Día 4.';
}
$phoneRowHiddenLabel = 'Número oculto';
$phoneRowHiddenTitle = 'Número oculto hasta completar Promo Final';
?>
<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 $phoneLocked ? '' : htmlspecialchars((string) ($order['celular'] ?? '')); ?>"
data-phone-locked="<?php echo $phoneLocked ? '1' : '0'; ?>"
data-phone-hidden-day1="<?php echo $phoneHiddenByDay1 ? '1' : '0'; ?>"
data-order-label="<?php echo htmlspecialchars(cc_test_order_label($order)); ?>"
data-client-name="<?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Cliente sin nombre')); ?>"
onclick="return registrarLlamada(event, this)"<?php echo $phoneLocked ? ' disabled aria-disabled="true" title="Llamada bloqueada hasta completar el sustento de Día 4"' : ''; ?>>
<i class="bi bi-telephone-outbound"></i> <?php echo $phoneLocked ? 'Llamada bloqueada' : '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="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="d-flex flex-wrap align-items-center gap-2 mb-1">
<div class="fw-bold fs-6 mb-0"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
<?php if ($promoFinalBadge !== null): ?>
<span class="badge rounded-pill <?php echo htmlspecialchars($promoFinalBadge['class']); ?>" title="<?php echo htmlspecialchars($promoFinalBadge['title']); ?>" aria-label="<?php echo htmlspecialchars($promoFinalBadge['title']); ?>">
<i class="bi bi-check2-circle me-1"></i><?php echo htmlspecialchars($promoFinalBadge['label']); ?>
</span>
<?php endif; ?>
</div>
<div class="small text-muted">ID Drive: <?php echo htmlspecialchars(cc_test_display_value($order['import_id'] ?? null, 'Sin ID')); ?></div>
</td>
<td>
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Sin nombre')); ?></div>
<div class="small text-muted mb-1">
Celular:
<?php if ($phoneHiddenByDay1): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border" title="<?php echo htmlspecialchars($phoneRowHiddenTitle, ENT_QUOTES); ?>" aria-label="<?php echo htmlspecialchars($phoneRowHiddenTitle, ENT_QUOTES); ?>">
<i class="bi bi-lock-fill me-1"></i><?php echo htmlspecialchars($phoneRowHiddenLabel, ENT_QUOTES); ?>
</span>
<?php else: ?>
<?php echo htmlspecialchars(cc_test_display_value($order['celular'], 'Sin celular')); ?>
<?php endif; ?>
</div>
<div class="small text-muted">DNI: <?php echo htmlspecialchars(cc_test_display_value($order['dni'], 'Sin DNI')); ?></div>
</td>
<td>
<div class="small"><strong>Dirección:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['direccion'])); ?></div>
<div class="small"><strong>Referencia:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['referencia'])); ?></div>
<div class="small"><strong>Departamento:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['sede'])); ?></div>
<div class="small"><strong>Provincia:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['ciudad'])); ?></div>
<div class="small"><strong>Distrito:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['distrito'])); ?></div>
<div class="small"><strong>Coordenadas:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['coordenadas'], 'Sin coordenadas')); ?></div>
<?php if (!empty(trim((string) ($order['distrito_drive'] ?? '')))): ?>
<div class="small"><strong>DISTRITO 1:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['distrito_drive'])); ?></div>
<?php endif; ?>
</td>
<td>
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_display_value($order['producto'], 'Sin producto')); ?></div>
<div class="small text-muted">Cantidad: <?php echo htmlspecialchars(cc_test_display_value($order['cantidad'])); ?> · <?php echo htmlspecialchars(cc_test_format_price($order['precio'])); ?></div>
<div class="small text-muted">Ingreso Drive: <?php echo htmlspecialchars(cc_test_format_datetime($order['import_id'] ?? null, 'Sin fecha en Drive')); ?></div>
<div class="small text-muted">Observación Drive: <?php echo htmlspecialchars(cc_test_display_value($order['observaciones'], 'Sin observaciones')); ?></div>
</td>
<td>
<div class="d-flex flex-wrap gap-2 mb-2">
<span class="badge rounded-pill <?php echo htmlspecialchars($badgeClass); ?>"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></span>
<?php if (!empty($order['pedido_repetido_en_tienda'])): ?>
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis border"><?php echo htmlspecialchars((string) ($order['pedido_repetido_en_tienda_label'] ?? 'Pedido repetido en tienda')); ?></span>
<?php endif; ?>
<span class="badge rounded-pill bg-light text-dark border"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span> llamadas</span>
<?php if (!empty($logisticaPendienteLabel)): ?>
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border"><?php echo htmlspecialchars($logisticaPendienteLabel); ?></span>
<?php endif; ?>
<?php if ($semaforoCuenta !== null): ?>
<span class="badge rounded-pill <?php echo htmlspecialchars($semaforoCuenta['class']); ?>">Seguimiento <?php echo htmlspecialchars($semaforoCuenta['label']); ?></span>
<?php endif; ?>
</div>
<?php if (!empty($logisticaPendienteDetalle)): ?>
<div class="small text-warning-emphasis fw-semibold mb-1"><?php echo htmlspecialchars($logisticaPendienteDetalle); ?></div>
<?php endif; ?>
<?php if ($semaforoCuenta !== null): ?>
<div class="small text-muted mb-1">Número de cuenta enviado hace <?php echo (int) $semaforoCuenta['days']; ?> día<?php echo ((int) $semaforoCuenta['days'] === 1) ? '' : 's'; ?> · <?php echo htmlspecialchars($semaforoCuenta['range']); ?></div>
<?php endif; ?>
<?php if ($followupDayInfo !== null): ?>
<div class="small text-muted mb-1">Seguimiento: <?php echo htmlspecialchars($followupDayInfo['display_label']); ?> · desde <?php echo htmlspecialchars($followupDayInfo['started_at_label']); ?></div>
<?php if (!empty($followupDayInfo['notice_text'])): ?>
<div class="small <?php echo htmlspecialchars($followupDayInfo['row_notice_class']); ?> fw-semibold mb-1"><?php echo htmlspecialchars($followupDayInfo['notice_text']); ?></div>
<?php endif; ?>
<?php endif; ?>
<div class="small text-muted">Próxima llamada: <?php echo htmlspecialchars(cc_test_format_datetime($order['proxima_llamada_at'] ?? null)); ?></div>
<?php if (!empty($order['fecha_entrega_programada'])): ?>
<div class="small text-muted">Entrega programada: <?php echo htmlspecialchars(cc_test_format_date($order['fecha_entrega_programada'] ?? null)); ?></div>
<?php endif; ?>
<?php if (!empty($order['ruta_contraentrega_pedido_id'])): ?>
<div class="small text-success">Subido a Ruta Contraentrega #<?php echo (int) $order['ruta_contraentrega_pedido_id']; ?><?php if (!empty($order['ruta_contraentrega_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['ruta_contraentrega_subido_at'] ?? null, '')); ?><?php endif; ?></div>
<?php endif; ?>
<?php if (!empty($order['pedido_rotulado_pedido_id'])): ?>
<div class="small text-primary">Subido a Pedidos Rotulados #<?php echo (int) $order['pedido_rotulado_pedido_id']; ?><?php if (!empty($order['pedido_rotulado_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['pedido_rotulado_subido_at'] ?? null, '')); ?><?php endif; ?></div>
<?php endif; ?>
<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>
</tr>
<?php $rowHtml = ob_get_clean(); echo $rowHtml; ?>
<?php ob_start(); ?>
<div class="modal fade" id="<?php echo htmlspecialchars($modalId); ?>" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header align-items-start gap-2">
<div class="flex-grow-1 min-w-0">
<div class="d-flex flex-wrap align-items-center gap-2 mb-1">
<div class="small text-primary fw-semibold mb-0"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
<?php if ($promoFinalBadge !== null): ?>
<span class="badge rounded-pill <?php echo htmlspecialchars($promoFinalBadge['class']); ?>" title="<?php echo htmlspecialchars($promoFinalBadge['title']); ?>" aria-label="<?php echo htmlspecialchars($promoFinalBadge['title']); ?>">
<i class="bi bi-check2-circle me-1"></i><?php echo htmlspecialchars($promoFinalBadge['label']); ?>
</span>
<?php endif; ?>
<?php if ($followupDayInfo !== null): ?>
<span class="badge rounded-pill <?php echo htmlspecialchars($followupDayInfo['badge_class']); ?>"><?php echo htmlspecialchars($followupDayInfo['display_label']); ?></span>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<h3 class="modal-title h5 mb-0"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Cliente sin nombre')); ?></h3>
<?php if ($canShowTuaniLogisticaButton): ?>
<button type="button" class="btn btn-primary btn-sm px-3" id="subir-logistica-<?php echo htmlspecialchars($order['source_key']); ?>" data-route-pedido-id="<?php echo (int) ($order['ruta_contraentrega_pedido_id'] ?? 0); ?>" data-rotulado-pedido-id="<?php echo (int) ($order['pedido_rotulado_pedido_id'] ?? 0); ?>" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this, { subirLogistica: true })"><?php echo htmlspecialchars($subirLogisticaButtonLabel); ?></button>
<?php endif; ?>
</div>
<div class="small text-muted mt-1">
<?php echo htmlspecialchars(cc_test_display_value($order['producto'], 'Sin producto')); ?> ·
<?php if ($phoneHiddenByDay1): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border" title="Número oculto" aria-label="Número oculto">
<i class="bi bi-lock-fill me-1"></i>Número oculto
</span>
<?php else: ?>
<?php echo htmlspecialchars(cc_test_display_value($order['celular'], 'Sin celular')); ?>
<?php endif; ?>
</div>
<?php if ($followupDayInfo !== null): ?>
<div class="small mt-2 <?php echo htmlspecialchars(!empty($followupDayInfo['notice_text']) ? $followupDayInfo['text_class'] : 'text-muted'); ?><?php echo !empty($followupDayInfo['notice_text']) ? ' fw-semibold' : ''; ?>">
<?php echo htmlspecialchars(!empty($followupDayInfo['notice_text']) ? $followupDayInfo['notice_text'] : ('Seguimiento asignado el ' . $followupDayInfo['started_at_label'] . '.')); ?>
</div>
<?php endif; ?>
<?php $promoFinalProofPath = trim((string) ($order['promo_final_evidencia_path'] ?? '')); ?>
<?php $promoFinalMoveAvailable = !empty($order['promo_final_habilitado']) && in_array($order['estado'], cc_test_followup_tracking_states(), true); ?>
<?php $promoFinalEvidenceAvailable = !empty($order['promo_final_evidencia_habilitada']); ?>
<?php $promoFinalThresholdDay = (int) ($followupDayInfo['promo_final_threshold_day'] ?? 4); ?>
<?php $promoFinalEvidenceDay = (int) ($followupDayInfo['promo_final_evidence_day'] ?? max(1, $promoFinalThresholdDay - 1)); ?>
<?php if ($promoFinalMoveAvailable || $promoFinalEvidenceAvailable || $promoFinalProofPath !== ''): ?>
<div
class="mt-2 js-promo-final-controls"
id="promo-final-controls-<?php echo htmlspecialchars($order['source_key']); ?>"
data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"
data-promo-final-candidate="<?php echo !empty($order['promo_final_habilitado']) ? '1' : '0'; ?>"
data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>"
>
<div class="d-flex flex-wrap gap-2 align-items-center">
<button type="button" class="btn btn-outline-danger btn-sm px-3 <?php echo $promoFinalMoveAvailable ? '' : 'd-none'; ?>" id="mover-promo-final-<?php echo htmlspecialchars($order['source_key']); ?>" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this, { moverPromoFinal: true })">Mover a Promo Final</button>
<?php if ($promoFinalProofPath !== ''): ?>
<a href="<?php echo htmlspecialchars($promoFinalProofPath); ?>" class="btn btn-outline-success btn-sm" target="_blank" rel="noopener">Ver sustento Promo Final</a>
<?php endif; ?>
</div>
<div class="small mt-2 <?php echo $promoFinalProofPath !== '' ? 'text-success fw-semibold' : 'text-danger-emphasis'; ?>" id="mover-promo-final-note-<?php echo htmlspecialchars($order['source_key']); ?>">
<?php if ($promoFinalProofPath !== ''): ?>
Sustento de Promo Final cargado<?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
<?php elseif ((int) ($followupDayInfo['day_number'] ?? 0) < $promoFinalThresholdDay): ?>
Ya puedes cargar la imagen desde el Día <?php echo $promoFinalEvidenceDay; ?> para dejarlo listo; el movimiento a Promo Final sigue desde el Día <?php echo $promoFinalThresholdDay; ?>.
<?php else: ?>
Ya puedes moverlo a Promo Final; si aún no cargaste la imagen, súbela antes de usar el botón.
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($canShowTuaniLogisticaButton): ?>
<div class="<?php echo htmlspecialchars($subirLogisticaNoteClass); ?>" id="subir-logistica-note-<?php echo htmlspecialchars($order['source_key']); ?>" role="alert"><?php echo $subirLogisticaNoteHtml; ?></div>
<?php endif; ?>
</div>
<div class="d-flex flex-column align-items-end gap-2 flex-shrink-0">
<span class="badge rounded-pill bg-light text-body-emphasis border shadow-sm d-inline-flex align-items-center cc-callcenter-modal-timer js-cc-callcenter-modal-timer" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" aria-live="polite">
<span class="me-1" aria-hidden="true" style="font-size: 0.95em; line-height: 1;">⏱</span><span class="js-cc-callcenter-modal-timer-value">00:00</span>
</span>
<button type="button" class="btn-close mt-1" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
</div>
<div class="modal-body">
<div class="border border-primary-subtle rounded-4 bg-primary-subtle p-2 mb-3 sticky-top shadow-sm" id="airDroidModalCard-<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<div class="small text-muted mb-1">Llamar / Gestionar</div>
<h4 class="h6 fw-bold mb-2"><?php echo $phoneHiddenByDay1 ? 'Número oculto' : 'Número listo para copiar'; ?></h4>
<div class="fs-4 fw-semibold lh-sm text-primary" id="airDroidPhoneNumber-<?php echo htmlspecialchars($order['source_key']); ?>">
<?php if ($phoneHiddenByDay1): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border" title="Número oculto" aria-label="Número oculto">
<i class="bi bi-lock-fill me-1"></i>Número oculto
</span>
<?php else: ?>
<?php echo htmlspecialchars(cc_test_display_value($order['celular'], 'Sin celular')); ?>
<?php endif; ?>
</div>
</div>
<div class="d-flex flex-column gap-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="airDroidCopyButton-<?php echo htmlspecialchars($order['source_key']); ?>" data-phone="<?php echo $phoneHiddenByDay1 ? '' : htmlspecialchars((string) ($order['celular'] ?? '')); ?>" onclick="copyPhoneButtonAction(this); return false;"<?php echo $phoneHiddenByDay1 ? ' disabled' : ''; ?>><?php echo $phoneHiddenByDay1 ? 'Número oculto' : 'Copiar número'; ?></button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="openAirDroidWeb()">Abrir AirDroid Web</button>
</div>
</div>
</div>
<div class="border border-success-subtle rounded-4 bg-white p-3 mb-4 shadow-sm cc-readonly-block" aria-disabled="true" style="user-select:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none;">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<div class="small text-muted mb-1">Origen del cliente</div>
<h4 class="h6 fw-bold mb-0">Pedido del cliente</h4>
</div>
<span class="badge bg-success-subtle text-success-emphasis">Solo lectura / origen</span>
</div>
<div class="row g-3">
<div class="col-12">
<label for="producto-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Producto</label>
<textarea class="form-control bg-light text-muted" id="producto-<?php echo htmlspecialchars($order['source_key']); ?>" readonly tabindex="-1" aria-readonly="true" spellcheck="false" rows="2" style="pointer-events:none; user-select:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; caret-color:transparent; resize:none; overflow-wrap:anywhere; white-space:pre-wrap; line-height:1.35;"><?php echo htmlspecialchars((string) ($order['producto'] ?? '')); ?></textarea>
</div>
<div class="col-sm-6 col-lg-3">
<label for="cantidad-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Cantidad</label>
<input type="text" class="form-control bg-light text-muted" id="cantidad-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['cantidad'] ?? '')); ?>" placeholder="Cantidad" readonly tabindex="-1" aria-readonly="true" style="pointer-events:none; user-select:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; caret-color:transparent;">
</div>
<div class="col-sm-6 col-lg-3">
<label for="precio-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Precio</label>
<input type="text" class="form-control bg-light text-muted" id="precio-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['precio'] ?? '')); ?>" placeholder="S/ 0.00" inputmode="decimal" readonly tabindex="-1" aria-readonly="true" style="pointer-events:none; user-select:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; caret-color:transparent;">
</div>
</div>
</div>
<div class="border border-info-subtle rounded-4 bg-white p-3 mb-4 shadow-sm">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<div class="small text-muted mb-1">Se confirma después</div>
<h4 class="h6 fw-bold mb-0">Confirmación de pedido</h4>
</div>
<span class="badge bg-info-subtle text-info-emphasis">Producto del sistema, cantidad y precio</span>
</div>
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-6">
<label for="confirmacion_producto-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Producto</label>
<input
type="text"
class="form-control w-100"
id="confirmacion_producto-<?php echo htmlspecialchars($order['source_key']); ?>"
list="catalogo-productos-list"
value="<?php echo htmlspecialchars((string) ($order['confirmacion_producto'] ?? '')); ?>"
placeholder="Escribe para buscar un producto"
title="Escribe para buscar un producto del sistema"
autocomplete="off"
spellcheck="false"
aria-autocomplete="list"
>
</div>
<div class="col-sm-6 col-lg-3">
<label for="confirmacion_cantidad-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Cantidad</label>
<input type="text" class="form-control" id="confirmacion_cantidad-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['confirmacion_cantidad'] ?? '')); ?>" placeholder="Cantidad">
</div>
<div class="col-sm-6 col-lg-3">
<label for="confirmacion_precio-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Precio</label>
<input type="text" class="form-control" id="confirmacion_precio-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['confirmacion_precio'] ?? '')); ?>" placeholder="S/ 0.00" inputmode="decimal">
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="button" class="btn btn-outline-secondary btn-sm js-toggle-confirmacion-extra" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">Agregar producto adicional</button>
</div>
<div class="border rounded-3 bg-light p-3 mt-3 d-none js-confirmacion-extra-block" id="confirmacion_extra_block-<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div class="fw-semibold">Producto adicional distinto</div>
<button type="button" class="btn btn-link text-danger text-decoration-none p-0 js-remove-confirmacion-extra" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">Quitar</button>
</div>
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-6">
<label for="confirmacion_producto_extra-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Producto adicional</label>
<input
type="text"
class="form-control w-100"
id="confirmacion_producto_extra-<?php echo htmlspecialchars($order['source_key']); ?>"
list="catalogo-productos-list"
value="<?php echo htmlspecialchars((string) ($order['confirmacion_producto_extra'] ?? '')); ?>"
placeholder="Escribe para buscar un producto adicional"
title="Escribe para buscar un producto adicional"
autocomplete="off"
spellcheck="false"
aria-autocomplete="list"
>
</div>
<div class="col-sm-6 col-lg-3">
<label for="confirmacion_cantidad_extra-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Cantidad</label>
<input type="text" class="form-control" id="confirmacion_cantidad_extra-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['confirmacion_cantidad_extra'] ?? '')); ?>" placeholder="Cantidad">
</div>
<div class="col-sm-6 col-lg-3">
<label for="confirmacion_precio_extra-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Precio</label>
<input type="text" class="form-control" id="confirmacion_precio_extra-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['confirmacion_precio_extra'] ?? '')); ?>" placeholder="S/ 0.00" inputmode="decimal">
</div>
</div>
</div>
<div class="form-text mt-3">Antes de usar <strong>Subir pedido</strong>, completa producto, cantidad y precio en esta sección.</div>
</div>
<div class="row g-3 mb-4">
<div class="col-lg-3 col-md-6">
<div class="border rounded p-3 h-100 bg-light-subtle">
<div class="small text-muted">Estado actual</div>
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></div>
<?php if ($semaforoCuenta !== null): ?>
<div class="mt-2">
<span class="badge rounded-pill <?php echo htmlspecialchars($semaforoCuenta['class']); ?>">Seguimiento <?php echo htmlspecialchars($semaforoCuenta['label']); ?></span>
</div>
<div class="small text-muted mt-2">Enviado hace <?php echo (int) $semaforoCuenta['days']; ?> día<?php echo ((int) $semaforoCuenta['days'] === 1) ? '' : 's'; ?> · <?php echo htmlspecialchars($semaforoCuenta['description']); ?></div>
<?php else: ?>
<div class="small text-muted mt-2">El semáforo se activa cuando marques “Se envió número de cuenta”.</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="border rounded p-3 h-100 bg-light-subtle">
<div class="small text-muted">Total llamadas</div>
<div class="fw-semibold"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span></div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="border rounded p-3 h-100 bg-light-subtle">
<div class="small text-muted"><?php echo cc_test_requires_delivery_date($order['estado']) ? 'Entrega programada' : 'Próxima llamada'; ?></div>
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_requires_delivery_date($order['estado']) ? cc_test_format_date($order['fecha_entrega_programada'] ?? null) : cc_test_format_datetime($order['proxima_llamada_at'] ?? null)); ?></div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="border rounded p-3 h-100 bg-light-subtle">
<div class="small text-muted">Ingreso en Drive</div>
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_format_datetime($order['import_id'] ?? null, 'Sin fecha')); ?></div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-7">
<h4 class="h6 fw-bold mb-3">Gestión comercial</h4>
<div class="row g-3">
<div class="col-md-4">
<label for="estado-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Estado</label>
<select class="form-select" id="estado-<?php echo htmlspecialchars($order['source_key']); ?>" onchange="toggleAgendaFields('<?php echo htmlspecialchars($order['source_key']); ?>')">
<?php foreach (cc_test_valid_states() as $estadoOption): ?>
<option value="<?php echo htmlspecialchars($estadoOption); ?>" <?php echo $order['estado'] === $estadoOption ? 'selected' : ''; ?>>
<?php echo htmlspecialchars(cc_test_state_label($estadoOption)); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4 js-next-call-group" id="next-call-group-<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="proxima-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Próxima llamada</label>
<input type="datetime-local" class="form-control" id="proxima-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars(cc_test_format_datetime_input($order['proxima_llamada_at'] ?? null)); ?>">
</div>
<div class="col-md-4 js-delivery-group <?php echo cc_test_requires_delivery_date($order['estado']) ? '' : 'd-none'; ?>" id="delivery-group-<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="fecha-entrega-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Fecha de entrega</label>
<input type="date" class="form-control" id="fecha-entrega-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars(cc_test_format_date_input($order['fecha_entrega_programada'] ?? null)); ?>">
<div class="form-text">Úsalo cuando el cliente quede en Confirmado fecha.</div>
</div>
<div class="col-12">
<label for="nota-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">COLOCAR AQUI OBSERVACION DEL CLIENTE O (DEDICATORIA O GRABADO)</label>
<textarea class="form-control" id="nota-<?php echo htmlspecialchars($order['source_key']); ?>" rows="3" placeholder="Ej.: Solicita dedicatoria para hija María, Ej.: Desea antes de las 4PM"><?php echo htmlspecialchars($order['nota_seguimiento']); ?></textarea>
</div>
<?php $numeroCuentaSedeIdSeleccionado = trim((string) ($order['numero_cuenta_sede_id'] ?? '')); ?>
<?php $numeroCuentaDniSeleccionado = trim((string) ($order['numero_cuenta_dni'] ?? '')); ?>
<?php $mostrarCamposNumeroCuenta = cc_test_requires_account_number_fields($order['estado']); ?>
<div class="col-md-6 js-numero-cuenta-group <?php echo $mostrarCamposNumeroCuenta ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="numero_cuenta_sede_id-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">SEDE / ID</label>
<input type="text" class="form-control" id="numero_cuenta_sede_id-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars($numeroCuentaSedeIdSeleccionado); ?>" placeholder="Ej.: SJL / 1542">
<div class="form-text">Se muestra en <strong>SE ENVIO NUMERO DE CUENTA</strong> y se mantiene en <strong>CONFIRMADO ENVIO</strong>.</div>
</div>
<div class="col-md-6 js-numero-cuenta-group <?php echo $mostrarCamposNumeroCuenta ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="numero_cuenta_dni-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">N° DNI</label>
<input type="text" class="form-control" id="numero_cuenta_dni-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars($numeroCuentaDniSeleccionado); ?>" placeholder="Ej.: 76543210">
</div>
<?php $promoFinalProofPath = trim((string) ($order['promo_final_evidencia_path'] ?? '')); ?>
<?php $canceladoProofPath = trim((string) ($order['cancelado_evidencia_path'] ?? '')); ?>
<?php $promoFinalThresholdDay = (int) ($followupDayInfo['promo_final_threshold_day'] ?? 4); ?>
<?php $promoFinalEvidenceDay = (int) ($followupDayInfo['promo_final_evidence_day'] ?? max(1, $promoFinalThresholdDay - 1)); ?>
<?php $mostrarPromoFinalProof = !empty($order['promo_final_evidencia_habilitada']) || $promoFinalProofPath !== ''; ?>
<?php $mostrarCanceladoProof = $order['estado'] === 'CANCELADO' || $canceladoProofPath !== ''; ?>
<div class="col-12 js-promo-final-proof-group <?php echo $mostrarPromoFinalProof ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" data-promo-final-candidate="<?php echo !empty($order['promo_final_habilitado']) ? '1' : '0'; ?>" data-promo-final-evidence-ready="<?php echo !empty($order['promo_final_evidencia_habilitada']) ? '1' : '0'; ?>" data-has-existing="<?php echo $promoFinalProofPath !== '' ? '1' : '0'; ?>">
<label for="promo_final_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Imagen de sustento para Promo Final</label>
<input type="file" class="form-control" id="promo_final_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" accept=".jpg,.jpeg,.png,.webp">
<div class="form-text">Puedes cargarla desde el Día <?php echo $promoFinalEvidenceDay; ?>. Para mover el pedido a <strong>Promo Final</strong> seguirá siendo obligatoria desde el Día <?php echo $promoFinalThresholdDay; ?>. La supervisora podrá revisarla.</div>
<?php if ($promoFinalProofPath !== ''): ?>
<div class="small text-success mt-2">
Sustento actual: <a href="<?php echo htmlspecialchars($promoFinalProofPath); ?>" target="_blank" rel="noopener">ver imagen</a><?php if (!empty($order['promo_final_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['promo_final_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
</div>
<?php endif; ?>
</div>
<div class="col-12 js-cancelado-proof-group <?php echo $mostrarCanceladoProof ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" data-has-existing="<?php echo $canceladoProofPath !== '' ? '1' : '0'; ?>">
<label for="cancelado_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Imagen de sustento para Cancelado</label>
<input type="file" class="form-control" id="cancelado_evidencia-<?php echo htmlspecialchars($order['source_key']); ?>" accept=".jpg,.jpeg,.png,.webp">
<div class="form-text">Obligatoria cuando el estado sea <strong>Cancelado</strong>. Así evitas cancelaciones sin motivo.</div>
<?php if ($canceladoProofPath !== ''): ?>
<div class="small text-success mt-2">
Sustento actual: <a href="<?php echo htmlspecialchars($canceladoProofPath); ?>" target="_blank" rel="noopener">ver imagen</a><?php if (!empty($order['cancelado_evidencia_subido_at'])): ?> · <?php echo htmlspecialchars(cc_test_format_datetime($order['cancelado_evidencia_subido_at'] ?? null, '')); ?><?php endif; ?>.
</div>
<?php endif; ?>
</div>
</div>
<hr>
<h4 class="h6 fw-bold mb-3">Datos editables del pedido</h4>
<div class="row g-3">
<div class="col-md-6">
<label for="direccion-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Dirección</label>
<textarea class="form-control" id="direccion-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['direccion'] ?? '')); ?></textarea>
</div>
<div class="col-md-6">
<label for="referencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Referencia</label>
<textarea class="form-control" id="referencia-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['referencia'] ?? '')); ?></textarea>
</div>
<?php $agenciaSeleccionada = cc_test_normalize_shipping_agency($order['agencia'] ?? '') ?? strtoupper(trim((string) ($order['agencia'] ?? ''))); ?>
<?php $sedeAgenciaSeleccionada = trim((string) ($order['sede_agencia'] ?? '')); ?>
<?php $montoAdelantadoSeleccionado = trim((string) ($order['monto_adelantado'] ?? '')); ?>
<?php $mostrarCamposEnvio = cc_test_requires_shipping_details($order['estado']); ?>
<div class="col-md-4">
<label for="sede-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Departamento</label>
<select class="form-select js-location-department" id="sede-<?php echo htmlspecialchars($order['source_key']); ?>">
<option value="">Seleccione departamento</option>
<?php foreach ($departamentosContraentrega as $departamentoOption): ?>
<option value="<?php echo htmlspecialchars($departamentoOption); ?>" <?php echo $departamentoSeleccionado === $departamentoOption ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($departamentoOption); ?>
</option>
<?php endforeach; ?>
<?php if ($departamentoSeleccionado !== '' && !in_array($departamentoSeleccionado, $departamentosContraentrega, true)): ?>
<option value="<?php echo htmlspecialchars($departamentoSeleccionado); ?>" selected>
<?php echo htmlspecialchars($departamentoSeleccionado); ?>
</option>
<?php endif; ?>
</select>
</div>
<div class="col-md-4">
<label for="ciudad-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Provincia</label>
<select class="form-select js-location-province" id="ciudad-<?php echo htmlspecialchars($order['source_key']); ?>">
<option value="">Seleccione provincia</option>
<?php foreach ($provinciasSeleccionadas as $provinciaOption): ?>
<option value="<?php echo htmlspecialchars($provinciaOption); ?>" <?php echo $provinciaSeleccionada === $provinciaOption ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($provinciaOption); ?>
</option>
<?php endforeach; ?>
<?php if ($provinciaSeleccionada !== '' && !in_array($provinciaSeleccionada, $provinciasSeleccionadas, true)): ?>
<option value="<?php echo htmlspecialchars($provinciaSeleccionada); ?>" selected>
<?php echo htmlspecialchars($provinciaSeleccionada); ?>
</option>
<?php endif; ?>
</select>
</div>
<div class="col-md-4">
<label for="distrito_select-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Distrito</label>
<select class="form-select js-location-district <?php echo empty($distritosSeleccionados) ? 'd-none' : ''; ?>" id="distrito_select-<?php echo htmlspecialchars($order['source_key']); ?>">
<option value="">Seleccione primero provincia</option>
<?php foreach ($distritosSeleccionados as $distritoOption): ?>
<option value="<?php echo htmlspecialchars($distritoOption); ?>" <?php echo $distritoSeleccionado === $distritoOption ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($distritoOption); ?>
</option>
<?php endforeach; ?>
<?php if ($distritoSeleccionado !== '' && !in_array($distritoSeleccionado, $distritosSeleccionados, true)): ?>
<option value="<?php echo htmlspecialchars($distritoSeleccionado); ?>" selected>
<?php echo htmlspecialchars($distritoSeleccionado); ?>
</option>
<?php endif; ?>
</select>
<input type="text" class="form-control mt-2 js-location-district-manual <?php echo empty($distritosSeleccionados) ? '' : 'd-none'; ?>" id="distrito_manual-<?php echo htmlspecialchars($order['source_key']); ?>" placeholder="Escriba el distrito si aún no está en cobertura" value="<?php echo htmlspecialchars($distritoSeleccionado); ?>">
<input type="hidden" id="distrito-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars($distritoSeleccionado); ?>">
<div class="form-text">Si la provincia aún no tiene cobertura cargada, podrás escribir el distrito manualmente.</div>
</div>
<div class="col-md-4">
<label for="coordenadas-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Coordenadas</label>
<input type="text" class="form-control" id="coordenadas-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['coordenadas'] ?? '')); ?>" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="-12.082029, -77.069024">
<div class="form-text">Obligatorias para <strong>Subir pedido</strong> a Ruta Contraentrega.</div>
</div>
<div class="col-md-4 js-envio-group <?php echo $mostrarCamposEnvio ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="agencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Agencia</label>
<select class="form-select" id="agencia-<?php echo htmlspecialchars($order['source_key']); ?>">
<option value="">Seleccione agencia</option>
<?php foreach (cc_test_shipping_agency_options() as $agenciaOption): ?>
<option value="<?php echo htmlspecialchars($agenciaOption); ?>" <?php echo $agenciaSeleccionada === $agenciaOption ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($agenciaOption); ?>
</option>
<?php endforeach; ?>
<?php if ($agenciaSeleccionada !== '' && !in_array($agenciaSeleccionada, cc_test_shipping_agency_options(), true)): ?>
<option value="<?php echo htmlspecialchars($agenciaSeleccionada); ?>" selected>
<?php echo htmlspecialchars($agenciaSeleccionada); ?>
</option>
<?php endif; ?>
</select>
</div>
<div class="col-md-4 js-envio-group <?php echo $mostrarCamposEnvio ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="sede_agencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Sede de envío</label>
<input type="text" class="form-control" id="sede_agencia-<?php echo htmlspecialchars($order['source_key']); ?>" placeholder="Seleccione o escriba la sede de envío" value="<?php echo htmlspecialchars($sedeAgenciaSeleccionada); ?>" <?php echo ($agenciaSeleccionada === 'SHALOM' && !empty($sedesShalom)) ? 'list="cc-sedes-shalom-list"' : ''; ?>>
</div>
<div class="col-md-2 js-envio-group <?php echo $mostrarCamposEnvio ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="dni-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">DNI</label>
<input type="text" class="form-control" id="dni-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['dni'] ?? '')); ?>">
<div class="form-text">Obligatorio para <strong>CONFIRMADO ENVIO</strong>.</div>
</div>
<div class="col-md-2 js-envio-group <?php echo $mostrarCamposEnvio ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="monto_adelantado-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Monto de adelanto</label>
<input type="text" class="form-control" id="monto_adelantado-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars($montoAdelantadoSeleccionado); ?>" inputmode="decimal" placeholder="0.00">
</div>
</div>
</div>
<div class="col-lg-5">
<div class="mb-4 border rounded p-4 bg-light-subtle">
<div class="fw-semibold mb-3 fs-5">Origen vs edición</div>
<div class="mb-1"><strong>Dirección Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['direccion_drive'] ?? null)); ?></div>
<div class="mb-1"><strong>Referencia Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['referencia_drive'] ?? null)); ?></div>
<div class="mb-1"><strong>Distrito 1:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['distrito_drive'] ?? null)); ?></div>
<div class="mb-1"><strong>Distrito:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['distrito'] ?? null)); ?></div>
<div class="mb-1"><strong>Observación Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['observaciones_drive'] ?? null)); ?></div>
<div><strong>Coordenadas Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['coordenadas_drive'] ?? null)); ?></div>
</div>
<h4 class="h6 fw-bold mb-3">Historial de llamadas</h4>
<?php if (empty($historial)): ?>
<div class="border rounded p-3 text-muted small">Aún no hay llamadas registradas para este cliente.</div>
<?php else: ?>
<div class="list-group list-group-flush border rounded overflow-hidden">
<?php foreach ($historial as $h): ?>
<div class="list-group-item small py-3">
<div class="d-flex justify-content-between gap-2">
<span class="fw-semibold"><?php echo htmlspecialchars($h['asesor'] ?? 'Asesor'); ?></span>
<span class="text-muted"><?php echo htmlspecialchars(date('d/m/Y H:i', strtotime($h['fecha_llamada']))); ?></span>
</div>
<div class="text-primary fw-semibold mt-1"><?php echo htmlspecialchars($h['resultado']); ?></div>
<?php if (!empty($h['observacion'])): ?>
<div class="text-muted mt-1"><?php echo htmlspecialchars($h['observacion']); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="modal-footer d-flex justify-content-between flex-wrap gap-2">
<div class="small text-muted">
<?php if ($canShowTuaniLogisticaButton): ?>
<strong>Guardar gestión</strong> solo guarda el historial. Para enviarlo a logística, completa <strong>Confirmación de pedido</strong> (producto, cantidad y precio) y usa el <strong>botón del encabezado</strong> junto al nombre del cliente.
<?php else: ?>
Los cambios se guardan en la base local del módulo de prueba.
<?php endif; ?>
</div>
<div class="d-flex gap-2 ms-auto flex-wrap">
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
<button type="button" class="btn btn-outline-danger" onclick="eliminarPedido('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Eliminar del panel</button>
<?php endif; ?>
<button type="button" class="btn btn-primary" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Guardar gestión</button>
</div>
</div>
</div>
</div>
</div>
<?php
$modalHtml = ob_get_clean();
if (is_resource($modalsHtml)) {
fwrite($modalsHtml, $modalHtml . PHP_EOL);
}
?>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($todosTotalPages > 1): ?>
<div class="border-top bg-light-subtle px-3 py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
<div class="small text-muted">
<?php if ($visibleOrdersTotalCount > 0): ?>
Mostrando <?php echo (int) $todosPageStart + 1; ?>-<?php echo (int) min($todosPageEnd, $visibleOrdersTotalCount); ?> de <?php echo (int) $visibleOrdersTotalCount; ?> pedidos · 200 por página
<?php else: ?>
No hay pedidos para mostrar en esta vista.
<?php endif; ?>
</div>
<?php if ($todosTotalPages > 1): ?>
<nav aria-label="Paginación de pedidos">
<ul class="pagination pagination-sm mb-0 flex-wrap">
<li class="page-item<?php echo $todosPage <= 1 ? ' disabled' : ''; ?>">
<?php if ($todosPage <= 1): ?>
<span class="page-link">«</span>
<?php else: ?>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => $view, 'store' => $storeKey, 'page' => $todosPage - 1], $persistedAssessorParams))); ?>" aria-label="Anterior">«</a>
<?php endif; ?>
</li>
<?php foreach ($todosPaginationPages as $pageEntry): ?>
<?php if ($pageEntry === 'ellipsis'): ?>
<li class="page-item disabled"><span class="page-link">…</span></li>
<?php else: ?>
<li class="page-item<?php echo (int) $pageEntry === $todosPage ? ' active' : ''; ?>">
<?php if ((int) $pageEntry === $todosPage): ?>
<span class="page-link" aria-current="page"><?php echo (int) $pageEntry; ?></span>
<?php else: ?>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => $view, 'store' => $storeKey, 'page' => (int) $pageEntry], $persistedAssessorParams))); ?>"><?php echo (int) $pageEntry; ?></a>
<?php endif; ?>
</li>
<?php endif; ?>
<?php endforeach; ?>
<li class="page-item<?php echo $todosPage >= $todosTotalPages ? ' disabled' : ''; ?>">
<?php if ($todosPage >= $todosTotalPages): ?>
<span class="page-link">»</span>
<?php else: ?>
<a class="page-link" href="<?php echo htmlspecialchars(cc_test_build_url('gestiones_callcenter.php', array_merge(['view' => $view, 'store' => $storeKey, 'page' => $todosPage + 1], $persistedAssessorParams))); ?>" aria-label="Siguiente">»</a>
<?php endif; ?>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</section>
<?php if (!empty($catalogoProductos)): ?>
<datalist id="catalogo-productos-list">
<?php
$catalogoProductoNombres = [];
foreach ($catalogoProductos as $catalogoProducto):
$catalogoProductoNombre = trim((string) ($catalogoProducto['nombre'] ?? ''));
if ($catalogoProductoNombre === '' || isset($catalogoProductoNombres[$catalogoProductoNombre])) {
continue;
}
$catalogoProductoNombres[$catalogoProductoNombre] = true;
?>
<option value="<?php echo htmlspecialchars($catalogoProductoNombre); ?>"></option>
<?php endforeach; ?>
</datalist>
<?php endif; ?>
<?php if (is_resource($modalsHtml)): ?>
<?php rewind($modalsHtml); fpassthru($modalsHtml); fclose($modalsHtml); ?>
<?php endif; ?>
<div class="modal fade" id="ccAssessorColorModal" tabindex="-1" aria-labelledby="ccAssessorColorModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form method="post" id="ccAssessorColorForm">
<div class="modal-header align-items-start gap-2">
<div>
<div class="small text-uppercase text-muted fw-semibold">Color de asesora</div>
<h3 class="modal-title h5 mb-0" id="ccAssessorColorModalLabel">Configurar color</h3>
</div>
<button type="button" class="btn-close mt-1" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<input type="hidden" name="action" value="update_assessor_color">
<input type="hidden" name="assessor_key" id="ccAssessorColorAssessorKey">
<div class="mb-3">
<label for="ccAssessorColorAssessorLabel" class="form-label">Asesora</label>
<input type="text" class="form-control" id="ccAssessorColorAssessorLabel" readonly>
</div>
<div class="mb-3">
<label for="ccAssessorColorInput" class="form-label">Color</label>
<div class="d-flex align-items-center gap-3 flex-wrap">
<input type="color" class="form-control form-control-color" id="ccAssessorColorInput" name="color_hex" value="#99EAFD" title="Elegir color">
<code class="small" id="ccAssessorColorHexText">#99EAFD</code>
</div>
<div class="form-text">Este color se aplicará a la franja, al badge y al panel de rendimiento de la asesora.</div>
</div>
<div class="rounded-4 border p-3" id="ccAssessorColorPreview">
<div class="small text-muted">Vista previa</div>
<div class="fw-semibold mt-1" id="ccAssessorColorPreviewLabel">Asesora</div>
<code class="small" id="ccAssessorColorPreviewHex">#99EAFD</code>
</div>
</div>
<div class="modal-footer d-flex justify-content-between flex-wrap gap-2">
<div class="small text-muted">Guarda un color diferente para reconocer rápido cada asesora.</div>
<div class="d-flex gap-2 ms-auto flex-wrap">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar color</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="position-fixed bottom-0 end-0 p-3" id="airDroidAssistPanel" style="z-index: 1080; max-width: 26rem; width: min(26rem, calc(100vw - 1.5rem));">
<div class="card border-primary shadow-lg d-none" id="airDroidAssistCard">
<div class="card-header bg-primary text-white d-flex align-items-start justify-content-between gap-3">
<div>
<div class="small opacity-75">Llamada preparada para AirDroid</div>
<h3 class="h5 mb-0">Número listo para copiar</h3>
</div>
<button type="button" class="btn-close btn-close-white" id="airDroidHideButton" aria-label="Ocultar"></button>
</div>
<div class="card-body">
<div class="rounded-4 border bg-light-subtle p-2">
<div class="small text-muted">Pedido</div>
<div class="fw-semibold" id="airDroidOrderLabel">-</div>
<div class="small text-muted mt-2">Cliente</div>
<div class="fw-semibold" id="airDroidClientName">-</div>
<div class="small text-muted mt-2">Número</div>
<div class="fs-5 fw-semibold lh-sm text-primary" id="airDroidPhoneNumber">-</div>
</div>
</div>
<div class="card-footer d-flex flex-wrap justify-content-end gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="airDroidCopyButton">Copiar número</button>
<button type="button" class="btn btn-outline-primary btn-sm" id="airDroidOpenButton">Abrir AirDroid Web</button>
</div>
</div>
</div>
<?php endif; ?>
<?php if (!empty($sedesShalom)): ?>
<datalist id="cc-sedes-shalom-list">
<?php foreach ($sedesShalom as $sedeShalomOption): ?>
<option value="<?php echo htmlspecialchars($sedeShalomOption); ?>"></option>
<?php endforeach; ?>
</datalist>
<?php endif; ?>
</main>
<script>
const provinciasPorDepartamento = <?php echo json_encode($provinciasPorDepartamentoContraentrega, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
const distritosPorProvincia = <?php echo json_encode($distritosPorProvinciaContraentrega, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
const ccAssessorCatalog = <?php echo json_encode($assessorUiCatalog, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
const ccCallCenterTimerStorageKey = 'cc-callcenter-modal-timers:v1';
const ccCallCenterTimerModalPrefix = 'modalDriveTest';
let ccCallCenterTimerTotals = ccCallCenterTimerReadTotals();
let ccCallCenterTimerRunningSince = {};
let ccCallCenterTimerTickHandle = null;
function ccCallCenterTimerReadTotals() {
try {
const raw = window.localStorage.getItem(ccCallCenterTimerStorageKey);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return {};
}
const cleaned = {};
Object.keys(parsed).forEach(function (sourceKey) {
const numericTotal = Number(parsed[sourceKey]);
if (sourceKey && Number.isFinite(numericTotal) && numericTotal >= 0) {
cleaned[sourceKey] = Math.floor(numericTotal);
}
});
return cleaned;
} catch (error) {
return {};
}
}
function ccCallCenterTimerWriteTotals() {
try {
window.localStorage.setItem(ccCallCenterTimerStorageKey, JSON.stringify(ccCallCenterTimerTotals));
} catch (error) {
// Si el navegador bloquea localStorage, el contador sigue funcionando en esta sesión.
}
}
function ccCallCenterTimerGetTotal(sourceKey) {
const numericTotal = Number(ccCallCenterTimerTotals[sourceKey] || 0);
return Number.isFinite(numericTotal) && numericTotal > 0 ? Math.floor(numericTotal) : 0;
}
function ccCallCenterTimerSetTotal(sourceKey, totalMs) {
if (!sourceKey) {
return;
}
ccCallCenterTimerTotals[sourceKey] = Math.max(0, Math.floor(Number(totalMs) || 0));
ccCallCenterTimerWriteTotals();
}
function ccCallCenterTimerFormat(totalMs) {
const totalSeconds = Math.max(0, Math.floor(Number(totalMs) / 1000));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const paddedMinutes = String(minutes).padStart(2, '0');
const paddedSeconds = String(seconds).padStart(2, '0');
if (hours > 0) {
return String(hours) + ':' + paddedMinutes + ':' + paddedSeconds;
}
return paddedMinutes + ':' + paddedSeconds;
}
function ccCallCenterTimerResolveSourceKey(modalElement) {
if (!modalElement) {
return '';
}
const explicitSourceKey = String(modalElement.dataset?.sourceKey || '').trim();
if (explicitSourceKey !== '') {
return explicitSourceKey;
}
const modalId = String(modalElement.id || '').trim();
if (modalId.startsWith(ccCallCenterTimerModalPrefix)) {
return modalId.slice(ccCallCenterTimerModalPrefix.length);
}
return '';
}
function ccCallCenterTimerStopTicker() {
if (ccCallCenterTimerTickHandle !== null) {
window.clearInterval(ccCallCenterTimerTickHandle);
ccCallCenterTimerTickHandle = null;
}
}
function ccCallCenterTimerRenderSource(sourceKey) {
if (!sourceKey) {
return;
}
const nodes = document.querySelectorAll('.js-cc-callcenter-modal-timer[data-source-key="' + sourceKey + '"]');
if (!nodes.length) {
return;
}
const runningSince = ccCallCenterTimerRunningSince[sourceKey] || null;
const totalMs = ccCallCenterTimerGetTotal(sourceKey) + (runningSince ? (Date.now() - runningSince) : 0);
const label = ccCallCenterTimerFormat(totalMs);
const ariaLabel = (runningSince ? 'Tiempo abierto' : 'Tiempo acumulado') + ': ' + label;
nodes.forEach(function (node) {
const valueNode = node.querySelector('.js-cc-callcenter-modal-timer-value');
if (valueNode) {
valueNode.textContent = label;
} else {
node.textContent = label;
}
node.classList.toggle('is-running', !!runningSince);
node.classList.toggle('d-none', node.classList.contains('cc-callcenter-row-timer') && !runningSince && totalMs <= 0);
node.setAttribute('aria-label', ariaLabel);
node.setAttribute('title', ariaLabel);
});
}
function ccCallCenterTimerRenderAll() {
const sourceKeys = new Set();
document.querySelectorAll('.js-cc-callcenter-modal-timer').forEach(function (node) {
const sourceKey = String(node.dataset.sourceKey || '').trim();
if (sourceKey !== '') {
sourceKeys.add(sourceKey);
}
});
Object.keys(ccCallCenterTimerRunningSince).forEach(function (sourceKey) {
if (sourceKey) {
sourceKeys.add(sourceKey);
}
});
sourceKeys.forEach(function (sourceKey) {
ccCallCenterTimerRenderSource(sourceKey);
});
}
function ccCallCenterTimerEnsureTicker() {
if (ccCallCenterTimerTickHandle !== null) {
return;
}
ccCallCenterTimerTickHandle = window.setInterval(function () {
const runningKeys = Object.keys(ccCallCenterTimerRunningSince);
if (!runningKeys.length) {
ccCallCenterTimerStopTicker();
return;
}
runningKeys.forEach(function (sourceKey) {
if (ccCallCenterTimerRunningSince[sourceKey]) {
ccCallCenterTimerRenderSource(sourceKey);
}
});
}, 1000);
}
function ccCallCenterTimerStart(sourceKey) {
if (!sourceKey) {
return;
}
if (!ccCallCenterTimerRunningSince[sourceKey]) {
ccCallCenterTimerRunningSince[sourceKey] = Date.now();
}
ccCallCenterTimerRenderSource(sourceKey);
ccCallCenterTimerEnsureTicker();
}
function ccCallCenterTimerStop(sourceKey) {
if (!sourceKey) {
return;
}
const startedAt = ccCallCenterTimerRunningSince[sourceKey];
if (startedAt) {
const elapsed = Date.now() - startedAt;
ccCallCenterTimerSetTotal(sourceKey, ccCallCenterTimerGetTotal(sourceKey) + elapsed);
delete ccCallCenterTimerRunningSince[sourceKey];
}
ccCallCenterTimerRenderSource(sourceKey);
if (!Object.keys(ccCallCenterTimerRunningSince).length) {
ccCallCenterTimerStopTicker();
}
}
function ccCallCenterTimerFlushRunningTimers() {
Object.keys(ccCallCenterTimerRunningSince).forEach(function (sourceKey) {
const startedAt = ccCallCenterTimerRunningSince[sourceKey];
if (startedAt) {
const elapsed = Date.now() - startedAt;
ccCallCenterTimerSetTotal(sourceKey, ccCallCenterTimerGetTotal(sourceKey) + elapsed);
}
delete ccCallCenterTimerRunningSince[sourceKey];
});
ccCallCenterTimerStopTicker();
ccCallCenterTimerRenderAll();
}
function ccCallCenterTimerBindEvents() {
document.addEventListener('shown.bs.modal', function (event) {
const sourceKey = ccCallCenterTimerResolveSourceKey(event.target);
if (sourceKey) {
ccCallCenterTimerStart(sourceKey);
}
});
document.addEventListener('hidden.bs.modal', function (event) {
const sourceKey = ccCallCenterTimerResolveSourceKey(event.target);
if (sourceKey) {
ccCallCenterTimerStop(sourceKey);
}
});
window.addEventListener('beforeunload', ccCallCenterTimerFlushRunningTimers);
window.addEventListener('pagehide', ccCallCenterTimerFlushRunningTimers);
window.addEventListener('pageshow', ccCallCenterTimerRenderAll);
window.addEventListener('storage', function (event) {
if (event.key === ccCallCenterTimerStorageKey) {
ccCallCenterTimerTotals = ccCallCenterTimerReadTotals();
ccCallCenterTimerRenderAll();
}
});
}
ccCallCenterTimerBindEvents();
ccCallCenterTimerRenderAll();
function getLocationControls(sourceKey) {
return {
department: document.getElementById('sede-' + sourceKey),
province: document.getElementById('ciudad-' + sourceKey),
districtSelect: document.getElementById('distrito_select-' + sourceKey),
districtManual: document.getElementById('distrito_manual-' + sourceKey),
districtHidden: document.getElementById('distrito-' + sourceKey)
};
}
function syncDistrictHidden(sourceKey, value) {
const controls = getLocationControls(sourceKey);
if (controls.districtHidden) {
controls.districtHidden.value = value || '';
}
}
function renderDistrictOptions(sourceKey, preserveSelection = true) {
const controls = getLocationControls(sourceKey);
if (!controls.province || !controls.districtSelect || !controls.districtManual || !controls.districtHidden) {
return;
}
const province = controls.province.value || '';
const currentValue = preserveSelection ? (controls.districtHidden.value || controls.districtSelect.value || controls.districtManual.value || '') : '';
const districts = province ? (distritosPorProvincia[province] || []) : [];
controls.districtSelect.innerHTML = '';
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = province ? (districts.length ? 'Seleccione distrito' : 'Sin cobertura registrada') : 'Seleccione primero provincia';
controls.districtSelect.appendChild(emptyOption);
if (!province) {
controls.districtSelect.classList.remove('d-none');
controls.districtSelect.disabled = true;
controls.districtManual.classList.add('d-none');
controls.districtManual.value = '';
syncDistrictHidden(sourceKey, '');
return;
}
if (districts.length > 0) {
districts.forEach(function(distrito) {
const option = document.createElement('option');
option.value = distrito;
option.textContent = distrito;
if (distrito === currentValue) {
option.selected = true;
}
controls.districtSelect.appendChild(option);
});
if (currentValue && !districts.includes(currentValue)) {
const legacyOption = document.createElement('option');
legacyOption.value = currentValue;
legacyOption.textContent = currentValue + ' (actual)';
legacyOption.selected = true;
controls.districtSelect.appendChild(legacyOption);
}
controls.districtSelect.classList.remove('d-none');
controls.districtSelect.disabled = false;
controls.districtManual.classList.add('d-none');
controls.districtManual.value = '';
controls.districtSelect.value = currentValue || controls.districtSelect.value || '';
syncDistrictHidden(sourceKey, controls.districtSelect.value);
return;
}
controls.districtSelect.classList.add('d-none');
controls.districtSelect.disabled = true;
controls.districtManual.classList.remove('d-none');
controls.districtManual.value = currentValue;
syncDistrictHidden(sourceKey, currentValue);
}
function renderProvinceOptions(sourceKey, preserveSelection = true) {
const controls = getLocationControls(sourceKey);
if (!controls.department || !controls.province) {
return;
}
const department = controls.department.value || '';
const currentValue = preserveSelection ? (controls.province.value || '') : '';
const provinces = department ? (provinciasPorDepartamento[department] || []) : [];
controls.province.innerHTML = '';
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = department ? (provinces.length ? 'Seleccione provincia' : 'Sin cobertura registrada') : 'Seleccione primero departamento';
controls.province.appendChild(emptyOption);
provinces.forEach(function(provincia) {
const option = document.createElement('option');
option.value = provincia;
option.textContent = provincia;
if (provincia === currentValue) {
option.selected = true;
}
controls.province.appendChild(option);
});
if (currentValue && !provinces.includes(currentValue)) {
const legacyOption = document.createElement('option');
legacyOption.value = currentValue;
legacyOption.textContent = currentValue + ' (actual)';
legacyOption.selected = true;
controls.province.appendChild(legacyOption);
}
controls.province.disabled = !department && currentValue === '';
controls.province.value = currentValue || controls.province.value || '';
renderDistrictOptions(sourceKey, preserveSelection);
}
function updateShippingSedeList(sourceKey) {
const agenciaSelect = document.getElementById('agencia-' + sourceKey);
const sedeInput = document.getElementById('sede_agencia-' + sourceKey);
const sedesListId = 'cc-sedes-shalom-list';
if (!agenciaSelect || !sedeInput) {
return;
}
if (agenciaSelect.value === 'SHALOM' && document.getElementById(sedesListId)) {
sedeInput.setAttribute('list', sedesListId);
} else {
sedeInput.removeAttribute('list');
}
}
function updateLogisticaButton(sourceKey) {
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
const button = document.getElementById('subir-logistica-' + sourceKey);
const note = document.getElementById('subir-logistica-note-' + sourceKey);
if (!button) {
return;
}
const routeId = parseInt(button.dataset.routePedidoId || '0', 10);
const rotuladoId = parseInt(button.dataset.rotuladoPedidoId || '0', 10);
if (estado === 'CONFIRMADO CONTRAENTREGA') {
button.disabled = false;
button.textContent = routeId > 0 ? 'Actualizar en ruta' : 'Subir a ruta';
if (note) {
note.className = 'alert ' + (routeId > 0 ? 'alert-success' : 'alert-warning') + ' py-2 px-3 mt-2 mb-0';
note.innerHTML = routeId > 0
? 'En Ruta Contraentrega #' + routeId + '. Si cambias algo, usa este botón para actualizarlo.'
: '<strong>ALERTA:</strong> este pedido está en <strong>CONFIRMADO CONTRAENTREGA</strong>, pero aún no se ha subido a Ruta Contraentrega. Usa este botón para subirlo.';
}
return;
}
if (estado === 'CONFIRMADO ENVIO') {
button.disabled = false;
button.textContent = rotuladoId > 0 ? 'Actualizar rotulado' : 'Subir a rotulados';
if (note) {
note.className = 'alert ' + (rotuladoId > 0 ? 'alert-success' : 'alert-warning') + ' py-2 px-3 mt-2 mb-0';
note.innerHTML = rotuladoId > 0
? 'En Pedidos Rotulados #' + rotuladoId + '. Si cambias algo, usa este botón para actualizarlo.'
: '<strong>ALERTA:</strong> este pedido está en <strong>CONFIRMADO ENVIO</strong>, pero aún no se ha subido a Pedidos Rotulados. Usa este botón para subirlo.';
}
return;
}
button.disabled = true;
button.textContent = 'Subir pedido';
if (note) {
note.className = 'alert alert-secondary py-2 px-3 mt-2 mb-0';
note.textContent = 'Selecciona CONFIRMADO CONTRAENTREGA o CONFIRMADO ENVIO y completa Confirmación de pedido (producto, cantidad y precio) para enviarlo a logística.';
}
}
const PROMO_FINAL_FOLLOWUP_STATES = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO', 'SE ENVIO NUMERO DE CUENTA'];
function updatePromoFinalControls(sourceKey) {
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
const followupStates = PROMO_FINAL_FOLLOWUP_STATES;
document.querySelectorAll('.js-promo-final-controls[data-source-key="' + sourceKey + '"]').forEach(block => {
const isCandidate = block.dataset.promoFinalCandidate === '1';
const hasExisting = block.dataset.hasExisting === '1';
const canUsePromoFinal = isCandidate && followupStates.includes(estado);
const canUploadPromoFinalEvidence = block.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
block.classList.toggle('d-none', !hasExisting && !canUsePromoFinal && !canUploadPromoFinalEvidence);
const moveButton = block.querySelector('button[id^="mover-promo-final-"]');
if (moveButton) {
moveButton.classList.toggle('d-none', !canUsePromoFinal);
}
});
document.querySelectorAll('.js-promo-final-proof-group[data-source-key="' + sourceKey + '"]').forEach(group => {
const isCandidate = group.dataset.promoFinalCandidate === '1';
const hasExisting = group.dataset.hasExisting === '1';
const canUsePromoFinal = isCandidate && followupStates.includes(estado);
const canUploadPromoFinalEvidence = group.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
group.classList.toggle('d-none', !hasExisting && !canUsePromoFinal && !canUploadPromoFinalEvidence);
});
}
function updateCanceladoEvidenceControls(sourceKey) {
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
document.querySelectorAll('.js-cancelado-proof-group[data-source-key="' + sourceKey + '"]').forEach(group => {
const hasExisting = group.dataset.hasExisting === '1';
group.classList.toggle('d-none', !hasExisting && estado !== 'CANCELADO');
});
}
function toggleAgendaFields(sourceKey) {
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
const nextCallGroup = document.getElementById('next-call-group-' + sourceKey);
const deliveryGroup = document.getElementById('delivery-group-' + sourceKey);
const nextCallInput = document.getElementById('proxima-' + sourceKey);
const deliveryInput = document.getElementById('fecha-entrega-' + sourceKey);
const needsDeliveryDate = estado === 'CONFIRMADO CONTRAENTREGA';
const needsShippingDetails = estado === 'CONFIRMADO ENVIO';
const needsAccountNumberDetails = ['SE ENVIO NUMERO DE CUENTA', 'CONFIRMADO ENVIO'].includes(estado);
const needsNextCall = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO'].includes(estado);
if (nextCallGroup) {
nextCallGroup.classList.toggle('d-none', !needsNextCall);
}
if (deliveryGroup) {
deliveryGroup.classList.toggle('d-none', !needsDeliveryDate);
}
document.querySelectorAll('.js-envio-group[data-source-key="' + sourceKey + '"]').forEach(group => {
group.classList.toggle('d-none', !needsShippingDetails);
});
document.querySelectorAll('.js-numero-cuenta-group[data-source-key="' + sourceKey + '"]').forEach(group => {
group.classList.toggle('d-none', !needsAccountNumberDetails);
});
if (!needsNextCall && nextCallInput) {
nextCallInput.value = '';
}
if (!needsDeliveryDate && deliveryInput) {
deliveryInput.value = '';
}
updateShippingSedeList(sourceKey);
updateLogisticaButton(sourceKey);
updatePromoFinalControls(sourceKey);
updateCanceladoEvidenceControls(sourceKey);
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('select[id^="estado-"]').forEach(select => {
toggleAgendaFields(select.id.replace('estado-', ''));
});
document.querySelectorAll('select[id^="agencia-"]').forEach(select => {
const sourceKey = select.id.replace('agencia-', '');
updateShippingSedeList(sourceKey);
select.addEventListener('change', () => {
updateShippingSedeList(sourceKey);
});
});
document.querySelectorAll('select[id^="sede-"]').forEach(select => {
const sourceKey = select.id.replace('sede-', '');
renderProvinceOptions(sourceKey, true);
select.addEventListener('change', () => {
renderProvinceOptions(sourceKey, false);
});
});
document.querySelectorAll('select[id^="ciudad-"]').forEach(select => {
const sourceKey = select.id.replace('ciudad-', '');
select.addEventListener('change', () => {
renderDistrictOptions(sourceKey, false);
});
});
document.querySelectorAll('select[id^="distrito_select-"]').forEach(select => {
const sourceKey = select.id.replace('distrito_select-', '');
select.addEventListener('change', () => {
syncDistrictHidden(sourceKey, select.value);
});
});
document.querySelectorAll('input[id^="distrito_manual-"]').forEach(input => {
const sourceKey = input.id.replace('distrito_manual-', '');
input.addEventListener('input', () => {
syncDistrictHidden(sourceKey, input.value);
});
});
document.querySelectorAll('input[id^="confirmacion_producto-"], input[id^="confirmacion_producto_extra-"]').forEach(input => {
const syncTitle = () => {
const value = (input.value || '').trim();
input.title = value !== '' ? value : (input.getAttribute('placeholder') || 'Escribe para buscar un producto');
};
syncTitle();
input.addEventListener('input', syncTitle);
input.addEventListener('change', syncTitle);
});
document.querySelectorAll('.js-toggle-confirmacion-extra').forEach(button => {
button.addEventListener('click', () => {
const sourceKey = button.dataset.sourceKey || '';
const block = document.getElementById('confirmacion_extra_block-' + sourceKey);
if (!block) return;
block.classList.remove('d-none');
button.classList.add('d-none');
});
});
document.querySelectorAll('.js-remove-confirmacion-extra').forEach(button => {
button.addEventListener('click', () => {
const sourceKey = button.dataset.sourceKey || '';
const block = document.getElementById('confirmacion_extra_block-' + sourceKey);
const toggle = document.querySelector('.js-toggle-confirmacion-extra[data-source-key="' + sourceKey + '"]');
if (block) {
block.classList.add('d-none');
const productoExtra = document.getElementById('confirmacion_producto_extra-' + sourceKey);
const cantidadExtra = document.getElementById('confirmacion_cantidad_extra-' + sourceKey);
const precioExtra = document.getElementById('confirmacion_precio_extra-' + sourceKey);
if (productoExtra) productoExtra.value = '';
if (cantidadExtra) cantidadExtra.value = '';
if (precioExtra) precioExtra.value = '';
}
if (toggle) {
toggle.classList.remove('d-none');
}
});
});
document.querySelectorAll('.js-confirmacion-extra-block').forEach(block => {
const sourceKey = block.id.replace('confirmacion_extra_block-', '');
const productoExtra = document.getElementById('confirmacion_producto_extra-' + sourceKey);
const cantidadExtra = document.getElementById('confirmacion_cantidad_extra-' + sourceKey);
const precioExtra = document.getElementById('confirmacion_precio_extra-' + sourceKey);
const toggle = document.querySelector('.js-toggle-confirmacion-extra[data-source-key="' + sourceKey + '"]');
const hasValue = [productoExtra, cantidadExtra, precioExtra].some(input => (input?.value || '').trim() !== '');
if (hasValue) {
block.classList.remove('d-none');
if (toggle) toggle.classList.add('d-none');
}
});
});
function eliminarPedido(sourceKey, trigger) {
const confirmed = window.confirm('¿Seguro que deseas eliminar este pedido del panel? Se ocultará del Call Center y no borrará la fila original de Drive.');
if (!confirmed) {
return;
}
const body = new FormData();
body.append('source_key', sourceKey);
body.append('eliminar_pedido', '1');
if (trigger) {
trigger.disabled = true;
}
fetch('update_callcenter_test_tracking.php', {
method: 'POST',
body: body
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.message || 'No se pudo eliminar el pedido del panel.');
}
alert(data.message || 'Pedido eliminado del panel correctamente.');
window.location.reload();
})
.catch(error => {
alert(error.message || 'Ocurrió un error al eliminar el pedido del panel.');
})
.finally(() => {
if (trigger) {
trigger.disabled = false;
}
});
}
function guardarGestion(sourceKey, trigger, options = {}) {
const subirLogistica = options.subirLogistica === true || options.subirRuta === true;
const moverPromoFinal = options.moverPromoFinal === true;
const estado = document.getElementById('estado-' + sourceKey)?.value || 'POR LLAMAR';
const promoProofInput = document.getElementById('promo_final_evidencia-' + sourceKey);
const cancelProofInput = document.getElementById('cancelado_evidencia-' + sourceKey);
const promoProofGroup = document.querySelector('.js-promo-final-proof-group[data-source-key="' + sourceKey + '"]');
const cancelProofGroup = document.querySelector('.js-cancelado-proof-group[data-source-key="' + sourceKey + '"]');
const hasExistingPromoProof = promoProofGroup?.dataset.hasExisting === '1';
const hasExistingCancelProof = cancelProofGroup?.dataset.hasExisting === '1';
const followupStates = PROMO_FINAL_FOLLOWUP_STATES;
const canUploadPromoFinalEvidence = promoProofGroup?.dataset.promoFinalEvidenceReady === '1' && followupStates.includes(estado);
if (subirLogistica) {
const confirmacionProducto = document.getElementById('confirmacion_producto-' + sourceKey);
const confirmacionCantidad = document.getElementById('confirmacion_cantidad-' + sourceKey);
const confirmacionPrecio = document.getElementById('confirmacion_precio-' + sourceKey);
const confirmacionFields = [
confirmacionProducto,
confirmacionCantidad,
confirmacionPrecio
];
const missingField = confirmacionFields.find(field => !(field?.value || '').trim());
if (missingField) {
alert('Completa producto, cantidad y precio en Confirmación de pedido antes de subir el pedido.');
missingField.focus();
if (typeof missingField.select === 'function') {
missingField.select();
}
return;
}
}
if (!moverPromoFinal && promoProofInput?.files?.[0] && !canUploadPromoFinalEvidence) {
alert('La imagen de sustento solo se puede cargar desde el Día 3.');
return;
}
if (moverPromoFinal && !hasExistingPromoProof && (!promoProofInput?.files || promoProofInput.files.length === 0)) {
alert('Debes subir una imagen de sustento antes de mover el pedido a Promo Final.');
return;
}
if (estado === 'CANCELADO' && !hasExistingCancelProof && (!cancelProofInput?.files || cancelProofInput.files.length === 0)) {
alert('Debes subir una imagen de sustento para guardar el pedido en estado Cancelado.');
return;
}
const body = new FormData();
const fields = {
source_key: sourceKey,
estado: estado,
proxima_llamada_at: document.getElementById('proxima-' + sourceKey)?.value || '',
fecha_entrega_programada: document.getElementById('fecha-entrega-' + sourceKey)?.value || '',
nota_seguimiento: document.getElementById('nota-' + sourceKey)?.value || '',
direccion: document.getElementById('direccion-' + sourceKey)?.value || '',
referencia: document.getElementById('referencia-' + sourceKey)?.value || '',
agencia: document.getElementById('agencia-' + sourceKey)?.value || '',
sede_agencia: document.getElementById('sede_agencia-' + sourceKey)?.value || '',
sede: document.getElementById('sede-' + sourceKey)?.value || '',
ciudad: document.getElementById('ciudad-' + sourceKey)?.value || '',
distrito: document.getElementById('distrito-' + sourceKey)?.value || '',
coordenadas: document.getElementById('coordenadas-' + sourceKey)?.value || '',
dni: document.getElementById('dni-' + sourceKey)?.value || '',
numero_cuenta_sede_id: document.getElementById('numero_cuenta_sede_id-' + sourceKey)?.value || '',
numero_cuenta_dni: document.getElementById('numero_cuenta_dni-' + sourceKey)?.value || '',
monto_adelantado: document.getElementById('monto_adelantado-' + sourceKey)?.value || '',
observaciones: document.getElementById('observaciones-' + sourceKey)?.value || '',
producto: document.getElementById('producto-' + sourceKey)?.value || '',
cantidad: document.getElementById('cantidad-' + sourceKey)?.value || '',
precio: document.getElementById('precio-' + sourceKey)?.value || '',
confirmacion_producto: document.getElementById('confirmacion_producto-' + sourceKey)?.value || '',
confirmacion_cantidad: document.getElementById('confirmacion_cantidad-' + sourceKey)?.value || '',
confirmacion_precio: document.getElementById('confirmacion_precio-' + sourceKey)?.value || '',
confirmacion_producto_extra: document.getElementById('confirmacion_producto_extra-' + sourceKey)?.value || '',
confirmacion_cantidad_extra: document.getElementById('confirmacion_cantidad_extra-' + sourceKey)?.value || '',
confirmacion_precio_extra: document.getElementById('confirmacion_precio_extra-' + sourceKey)?.value || '',
subir_a_logistica: subirLogistica ? '1' : '0',
mover_a_promo_final: moverPromoFinal ? '1' : '0'
};
Object.entries(fields).forEach(([key, value]) => {
body.append(key, value);
});
if ((moverPromoFinal || canUploadPromoFinalEvidence) && promoProofInput?.files?.[0]) {
body.append('promo_final_evidencia', promoProofInput.files[0]);
}
if (estado === 'CANCELADO' && cancelProofInput?.files?.[0]) {
body.append('cancelado_evidencia', cancelProofInput.files[0]);
}
if (trigger) {
trigger.disabled = true;
}
fetch('update_callcenter_test_tracking.php', {
method: 'POST',
body: body
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.message || 'No se pudo guardar la gestión');
}
if (subirLogistica || moverPromoFinal || (canUploadPromoFinalEvidence && promoProofInput?.files?.[0])) {
alert(data.message || 'Gestión actualizada correctamente.');
}
window.location.reload();
})
.catch(error => {
alert(error.message || 'Ocurrió un error al guardar la gestión.');
})
.finally(() => {
if (trigger) {
trigger.disabled = false;
}
});
}
function actualizarContadorLlamadas(sourceKey, total) {
document.querySelectorAll('.js-call-count-number[data-source-key="' + sourceKey + '"]').forEach(node => {
node.textContent = String(total);
});
}
function normalizarNumeroTelefono(value) {
return String(value || '').replace(/\D+/g, '');
}
function copyTextLegacy(text) {
const input = document.createElement('input');
input.type = 'text';
input.value = text;
input.setAttribute('readonly', 'readonly');
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.appendChild(input);
input.select();
input.setSelectionRange(0, input.value.length);
let copied = false;
try {
copied = document.execCommand('copy');
} catch (error) {
copied = false;
}
document.body.removeChild(input);
return copied;
}
function flashCopyButtonFeedback(button) {
if (!button) {
return;
}
const defaultLabel = button.dataset.defaultCopyLabel || button.textContent || 'Copiar número';
button.dataset.defaultCopyLabel = defaultLabel;
if (button._copyFeedbackTimer) {
window.clearTimeout(button._copyFeedbackTimer);
button._copyFeedbackTimer = null;
}
button.textContent = 'Copiado ✅';
button.setAttribute('aria-live', 'polite');
button._copyFeedbackTimer = window.setTimeout(() => {
if (button.textContent === 'Copiado ✅') {
button.textContent = defaultLabel;
}
button.removeAttribute('aria-live');
button._copyFeedbackTimer = null;
}, 1300);
}
function copyPhoneButtonAction(button) {
if (!button) {
return Promise.resolve(false);
}
const phone = button.dataset.phone || '';
return copyPhoneToClipboard(phone).then(copied => {
if (copied) {
flashCopyButtonFeedback(button);
} else {
alert('No se pudo copiar el número automáticamente.');
}
return copied;
}).catch(() => {
alert('No se pudo copiar el número automáticamente.');
return false;
});
}
function copyPhoneToClipboard(phone) {
if (!phone) {
return Promise.resolve(false);
}
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(phone)
.then(() => true)
.catch(() => copyTextLegacy(phone));
}
return Promise.resolve(copyTextLegacy(phone));
}
function openAirDroidWeb() {
return window.open('https://web.airdroid.com/', '_blank', 'noopener');
}
function showAirDroidHelper(details) {
const sourceKey = details.sourceKey || '';
const phone = details.phone || '-';
const orderLabel = details.orderLabel || 'Sin número';
const clientName = details.clientName || 'Cliente sin nombre';
const registered = !!details.registered;
const copied = !!details.copied;
const modalId = details.modalId || '';
const phoneLocked = !!details.phoneLocked;
const phoneHiddenByDay1 = !!details.phoneHiddenByDay1;
if (modalId) {
const modalElement = document.getElementById(modalId);
if (modalElement && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalElement, { backdrop: 'static', keyboard: false }).show();
}
}
const orderNode = document.getElementById('airDroidOrderLabel-' + sourceKey);
const clientNode = document.getElementById('airDroidClientName-' + sourceKey);
const phoneNode = document.getElementById('airDroidPhoneNumber-' + sourceKey);
const copyButton = document.getElementById('airDroidCopyButton-' + sourceKey);
const openButton = document.getElementById('airDroidOpenButton-' + sourceKey);
if (orderNode) orderNode.textContent = orderLabel;
if (clientNode) clientNode.textContent = clientName;
if (phoneNode) {
if (phoneHiddenByDay1) {
phoneNode.innerHTML = '<span class="badge bg-warning-subtle text-warning-emphasis border" title="Número oculto hasta completar Promo Final" aria-label="Número oculto hasta completar Promo Final"><i class="bi bi-lock-fill me-1"></i>Número oculto</span>';
} else {
phoneNode.textContent = phone !== '-' ? phone : 'Sin celular';
}
}
if (copyButton) {
if (phoneHiddenByDay1) {
copyButton.dataset.phone = '';
copyButton.disabled = true;
copyButton.textContent = 'Número oculto';
copyButton.dataset.defaultCopyLabel = 'Número oculto';
copyButton.removeAttribute('aria-live');
copyButton.setAttribute('title', 'Número oculto hasta completar Promo Final');
} else {
copyButton.dataset.phone = phone !== '-' ? phone : '';
copyButton.disabled = false;
copyButton.textContent = 'Copiar número';
copyButton.dataset.defaultCopyLabel = 'Copiar número';
copyButton.removeAttribute('aria-live');
copyButton.removeAttribute('title');
}
}
openButton.onclick = function () {
openAirDroidWeb();
};
}
function registrarLlamada(event, trigger) {
if (event) {
event.preventDefault();
}
const sourceKey = trigger?.dataset?.sourceKey || '';
const modalId = trigger?.dataset?.modalId || '';
const phone = normalizarNumeroTelefono(trigger?.dataset?.phone || '');
const orderLabel = trigger?.dataset?.orderLabel || '';
const clientName = trigger?.dataset?.clientName || '';
const phoneLocked = false;
const phoneHiddenByDay1 = trigger?.dataset?.phoneHiddenDay1 === '1';
if (!sourceKey) {
alert('No se encontró el pedido para registrar la llamada.');
return false;
}
if (modalId) {
const modalElement = document.getElementById(modalId);
if (modalElement && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalElement, { backdrop: 'static', keyboard: false }).show();
}
}
if (trigger) {
trigger.classList.add('disabled');
trigger.setAttribute('aria-disabled', 'true');
}
const copyPromise = copyPhoneToClipboard(phone).catch(() => false);
const body = new URLSearchParams({
pedido_id: sourceKey,
resultado: 'Llamada iniciada - AirDroid',
observacion: 'Clic en botón Llamar / AirDroid desde el panel'
});
const savePromise = fetch('save_llamada.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString(),
keepalive: true
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.error || 'No se pudo registrar la llamada');
}
if (typeof data.total_llamadas !== 'undefined') {
actualizarContadorLlamadas(sourceKey, Number(data.total_llamadas) || 0);
}
return true;
});
Promise.allSettled([savePromise, copyPromise]).then(results => {
const registerResult = results[0];
const copyResult = results[1];
const registered = registerResult.status === 'fulfilled' && registerResult.value === true;
const copied = copyResult.status === 'fulfilled' && copyResult.value === true;
if (!registered) {
const error = registerResult.reason;
console.error('Error al registrar llamada:', error);
alert((error && error.message) || 'No se pudo registrar la llamada.');
}
showAirDroidHelper({
sourceKey,
modalId,
phone,
phoneLocked,
phoneHiddenByDay1,
orderLabel,
clientName,
registered,
copied
});
}).finally(() => {
if (trigger) {
trigger.classList.remove('disabled');
trigger.removeAttribute('aria-disabled');
}
});
return false;
}
const airDroidCopyButton = document.getElementById('airDroidCopyButton');
if (airDroidCopyButton) {
airDroidCopyButton.addEventListener('click', function () {
copyPhoneButtonAction(this);
});
}
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;
const ccAssessorColorModalElement = document.getElementById('ccAssessorColorModal');
const ccAssessorColorModalInstance = ccAssessorColorModalElement && window.bootstrap && window.bootstrap.Modal
? window.bootstrap.Modal.getOrCreateInstance(ccAssessorColorModalElement)
: null;
const ccAssessorColorAssessorKey = document.getElementById('ccAssessorColorAssessorKey');
const ccAssessorColorAssessorLabel = document.getElementById('ccAssessorColorAssessorLabel');
const ccAssessorColorInput = document.getElementById('ccAssessorColorInput');
const ccAssessorColorHexText = document.getElementById('ccAssessorColorHexText');
const ccAssessorColorPreview = document.getElementById('ccAssessorColorPreview');
const ccAssessorColorPreviewLabel = document.getElementById('ccAssessorColorPreviewLabel');
const ccAssessorColorPreviewHex = document.getElementById('ccAssessorColorPreviewHex');
function normalizeAssessorHexColor(value) {
value = String(value || '').trim().toUpperCase();
if (!value) {
return '';
}
if (value.charAt(0) !== '#') {
value = '#' + value;
}
return /^#[0-9A-F]{6}$/.test(value) ? value : '';
}
function assessorHexToRgbTriplet(hex) {
const normalized = normalizeAssessorHexColor(hex);
if (!normalized) {
return null;
}
const intValue = parseInt(normalized.slice(1), 16);
return [
(intValue >> 16) & 255,
(intValue >> 8) & 255,
intValue & 255
];
}
function syncAssessorColorPreview(colorHex, assessorLabel) {
const resolvedHex = normalizeAssessorHexColor(colorHex) || '#ADB5BD';
const rgb = assessorHexToRgbTriplet(resolvedHex) || [173, 181, 189];
const backgroundColor = 'rgba(' + rgb.join(', ') + ', .16)';
const borderColor = 'rgba(' + rgb.join(', ') + ', .28)';
if (ccAssessorColorPreview) {
ccAssessorColorPreview.style.background = backgroundColor;
ccAssessorColorPreview.style.borderColor = borderColor;
}
if (ccAssessorColorPreviewLabel) {
ccAssessorColorPreviewLabel.textContent = assessorLabel || 'Asesora';
}
if (ccAssessorColorPreviewHex) {
ccAssessorColorPreviewHex.textContent = resolvedHex;
}
if (ccAssessorColorHexText) {
ccAssessorColorHexText.textContent = resolvedHex;
}
}
function syncAssessorFormAccent(form) {
if (!form) {
return;
}
const select = form.querySelector('.js-assessor-select');
const colorButton = form.querySelector('.js-assessor-color-trigger');
const assessorKey = select ? (select.value || '') : '';
const config = assessorKey && ccAssessorCatalog[assessorKey] ? ccAssessorCatalog[assessorKey] : null;
const isUnassigned = !assessorKey || !config;
const colorHex = isUnassigned
? '#FFFFFF'
: (normalizeAssessorHexColor(config ? config.color_hex : '') || '#ADB5BD');
const rgb = assessorHexToRgbTriplet(colorHex) || [255, 255, 255];
const contrastHex = isUnassigned
? '#212529'
: (normalizeAssessorHexColor(config ? config.contrast_hex : '') || '#212529');
form.classList.toggle('is-unassigned', isUnassigned);
form.style.setProperty('--cc-assessor-accent', colorHex);
form.style.setProperty('--cc-assessor-accent-rgb', rgb.join(', '));
form.style.setProperty('--cc-assessor-accent-contrast', contrastHex);
if (colorButton) {
colorButton.disabled = !assessorKey;
const buttonTitle = assessorKey && config
? 'Cambiar color de ' + config.label
: 'Selecciona una asesora para cambiar su color';
colorButton.title = buttonTitle;
colorButton.setAttribute('aria-label', buttonTitle);
const dot = colorButton.querySelector('.cc-callcenter-color-dot');
if (dot) {
dot.style.backgroundColor = isUnassigned ? '#ADB5BD' : colorHex;
}
}
}
function restoreServerAssessorSelection(select) {
if (!select) {
return;
}
const form = select.closest('form');
const hiddenInput = form ? form.querySelector('.js-assessor-hidden') : null;
const serverAssessor = typeof select.dataset.serverAssessor === 'string'
? (select.dataset.serverAssessor || '')
: (hiddenInput ? (hiddenInput.value || '') : '');
if ((select.value || '') !== serverAssessor) {
select.value = serverAssessor;
}
if (hiddenInput && (hiddenInput.value || '') !== (select.value || '')) {
hiddenInput.value = select.value || '';
}
}
function syncAssessorHiddenInput(form) {
if (!form) {
return;
}
const select = form.querySelector('.js-assessor-select');
const hiddenInput = form.querySelector('.js-assessor-hidden');
if (!select || !hiddenInput) {
return;
}
hiddenInput.value = select.value || '';
}
function syncAllAssessorFormsFromServer() {
document.querySelectorAll('.cc-callcenter-assign-form').forEach(function (form) {
const select = form.querySelector('.js-assessor-select');
restoreServerAssessorSelection(select);
syncAssessorFormAccent(form);
});
}
function openAssessorColorModal(trigger) {
const form = trigger ? trigger.closest('form') : null;
const select = form ? form.querySelector('.js-assessor-select') : null;
restoreServerAssessorSelection(select);
const assessorKey = select ? (select.value || '') : '';
const config = assessorKey && ccAssessorCatalog[assessorKey] ? ccAssessorCatalog[assessorKey] : null;
if (!assessorKey || !config) {
alert('Selecciona una asesora para cambiar su color.');
return;
}
const colorHex = normalizeAssessorHexColor(config.color_hex) || '#ADB5BD';
if (ccAssessorColorAssessorKey) {
ccAssessorColorAssessorKey.value = assessorKey;
}
if (ccAssessorColorAssessorLabel) {
ccAssessorColorAssessorLabel.value = config.label || assessorKey;
}
if (ccAssessorColorInput) {
ccAssessorColorInput.value = colorHex.toLowerCase();
}
syncAssessorColorPreview(colorHex, config.label || assessorKey);
if (ccAssessorColorModalInstance) {
ccAssessorColorModalInstance.show();
}
}
document.querySelectorAll('.cc-callcenter-assign-form').forEach(function (form) {
const select = form.querySelector('.js-assessor-select');
restoreServerAssessorSelection(select);
syncAssessorFormAccent(form);
if (select) {
select.addEventListener('change', function () {
syncAssessorHiddenInput(form);
syncAssessorFormAccent(form);
});
}
form.addEventListener('submit', function () {
syncAssessorHiddenInput(form);
});
const colorButton = form.querySelector('.js-assessor-color-trigger');
if (colorButton) {
colorButton.addEventListener('click', function () {
openAssessorColorModal(this);
});
}
});
window.addEventListener('pageshow', function (event) {
if (event && event.persisted) {
window.location.reload();
return;
}
syncAllAssessorFormsFromServer();
});
window.addEventListener('load', function () {
syncAllAssessorFormsFromServer();
});
window.requestAnimationFrame(syncAllAssessorFormsFromServer);
window.setTimeout(syncAllAssessorFormsFromServer, 0);
window.setTimeout(syncAllAssessorFormsFromServer, 120);
window.setTimeout(syncAllAssessorFormsFromServer, 600);
if (ccAssessorColorInput) {
ccAssessorColorInput.addEventListener('input', function () {
const label = ccAssessorColorAssessorLabel ? (ccAssessorColorAssessorLabel.value || 'Asesora') : 'Asesora';
syncAssessorColorPreview(this.value, label);
});
}
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();
}
</script>
<?php require_once 'layout_footer.php'; ?>