4333 lines
251 KiB
PHP
4333 lines
251 KiB
PHP
<?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'; ?>
|