diff --git a/call_center_pro.php b/call_center_pro.php index b4b6778c..1bfab7ad 100644 --- a/call_center_pro.php +++ b/call_center_pro.php @@ -332,6 +332,7 @@ $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']; @@ -347,7 +348,7 @@ if ($isTuaniRecuperablesStore && trim((string) ($_GET['view'] ?? '')) === '') { } $canShowTuaniLogisticaButton = $isMainTuaniStore || $isTuaniRecuperablesStore; $postedAction = trim((string) ($_POST['action'] ?? '')); -$recoverablesResyncRequested = $isTuaniRecuperablesStore && $postedAction === 'reimport_recoverables_queue'; +$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'; $defaultView = cc_test_callcenter_pro_default_view_key($storeKey); @@ -587,6 +588,7 @@ try { $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; @@ -854,9 +856,23 @@ try { })); } + $performanceValueOverride = ''; + $performanceMetaTitleOverride = ''; $performanceDashboard = cc_test_build_performance_dashboard_period($performanceOrders, $assessors, $kpiPeriodStart, $kpiPeriodEnd); + 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.'; + } + } + $performanceStatusHeading = $kpiPeriodKey === 'today' ? 'Detalle del KPI de hoy (%)' : 'Detalle del KPI del periodo (%)'; $performanceSubtitle = $isTuaniRecuperablesStore - ? 'Los indicadores de esta bandeja se calculan sobre todos los pedidos visibles para cualquier asesora en el periodo seleccionado; los repetidos solo suman cantidad y no bajan el porcentaje.' + ? '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.' : ($isAdmin ? 'Confirmación = pedidos asignados en el periodo confirmados en el mismo periodo, sin contar repetidos. Recuperación = pedidos asignados antes del periodo confirmados dentro del periodo. Rendimiento = confirmados totales (nuevos + recuperados) sobre asignados efectivos sin repetidos.' : 'Tus KPIs del periodo seleccionado se calculan sobre tus pedidos asignados en ese rango. Confirmación = asignados del periodo confirmados en el mismo periodo, sin contar repetidos. Recuperación = asignados antes del periodo confirmados dentro del periodo. Rendimiento = confirmados totales (nuevos + recuperados) sobre asignados efectivos sin repetidos.'); @@ -865,11 +881,15 @@ try { $performanceTitle = $kpiPeriodKey === 'today' ? 'KPI compartido de recuperación' : 'KPI compartido de recuperación · ' . $kpiPeriodLabel; - $performanceChart1Title = 'Distribución de estados de la bandeja'; - $performanceChart1Note = 'La dona separa los pedidos del periodo seleccionado por estado: confirmados nuevos, recuperados, POR LLAMAR, DEVOLVER LLAMADA, OBSERVADO, SE ENVIO NUMERO DE CUENTA, repetidos (solo cantidad) y cancelados.'; + $performanceChart1Title = 'Confirmados nuevos vs recuperados'; + $performanceChart1Note = 'La dona muestra solo confirmados nuevos y recuperados en esta bandeja.'; $performanceChart2Title = $kpiPeriodKey === 'today' ? 'Evolución de hoy' : 'Evolución de ' . $kpiPeriodTitleLabel; $performanceChart2Note = 'La línea muestra cómo se mueve la bandeja compartida en el periodo seleccionado.'; $performanceFootnote = 'Los indicadores se calculan sobre todos los pedidos visibles de esta bandeja compartida; los repetidos solo suman cantidad y no bajan el porcentaje.'; + $performanceLabel = 'Rendimiento del periodo'; + $performanceValueOverride = ''; + $performanceMetaTitleOverride = 'Confirmados nuevos + recuperados cubren el periodo mostrado.'; + $performanceStatusHeading = 'Detalle del KPI del periodo (%)'; } else { if ($isAdmin) { $performanceTitle = $selectedAssessorFilterLabel !== '' @@ -895,8 +915,11 @@ try { 'chart2_title' => $performanceChart2Title, 'chart2_note' => $performanceChart2Note, 'footnote' => $performanceFootnote, - 'performance_label' => $kpiPeriodKey === 'today' ? 'Rendimiento del día' : 'Rendimiento del periodo', - 'status_detail_heading' => $kpiPeriodKey === 'today' ? 'Detalle del KPI de hoy (%)' : 'Detalle del KPI del periodo (%)', + 'performance_label' => $performanceLabel, + 'performance_value_hidden' => $isTuaniRecuperablesStore, + 'status_detail_heading' => $performanceStatusHeading, + 'performance_value' => $performanceValueOverride, + 'performance_meta_title' => $performanceMetaTitleOverride, ]); $visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view, $selectedAssessorFilter, $todayStart): bool { @@ -1398,7 +1421,7 @@ require_once 'layout_header.php'; Tienda activa: Solo pedidos válidos: IDs #D... - + Abrir (#D...) @@ -1439,7 +1462,7 @@ require_once 'layout_header.php'; Actualización en vivo cada 5 min Ver vista previa Drive - +
diff --git a/gestiones_callcenter.php b/gestiones_callcenter.php index c4c281df..815c1003 100644 --- a/gestiones_callcenter.php +++ b/gestiones_callcenter.php @@ -229,6 +229,7 @@ $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']; @@ -244,7 +245,7 @@ if ($isTuaniRecuperablesStore && trim((string) ($_GET['view'] ?? '')) === '') { } $canShowTuaniLogisticaButton = $isMainTuaniStore || $isTuaniRecuperablesStore; $postedAction = trim((string) ($_POST['action'] ?? '')); -$recoverablesResyncRequested = $isTuaniRecuperablesStore && $postedAction === 'reimport_recoverables_queue'; +$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; @@ -515,6 +516,7 @@ try { $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; @@ -1037,15 +1039,35 @@ try { })) : $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' => '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' => 'Distribución de estados', - 'chart1_note' => '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.', + '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); @@ -1654,7 +1676,7 @@ require_once 'layout_header.php'; Tienda activa: Solo pedidos válidos: IDs #D... - + Abrir (#D...) @@ -1697,7 +1719,7 @@ require_once 'layout_header.php'; Ver vista previa Drive - + diff --git a/includes/callcenter_test_helpers.php b/includes/callcenter_test_helpers.php index fb0f0f8c..11301ed2 100644 --- a/includes/callcenter_test_helpers.php +++ b/includes/callcenter_test_helpers.php @@ -1306,6 +1306,40 @@ function cc_test_current_user_can_access_module(?PDO $pdo = null): bool return cc_test_is_allowed_module_user($role, $username, $nombreAsesor); } +function cc_test_current_user_identity_keys(): array +{ + $keys = []; + foreach ([(string) ($_SESSION['nombre_asesor'] ?? ''), (string) ($_SESSION['username'] ?? '')] as $candidate) { + $key = cc_test_normalize_user_key($candidate); + if ($key !== '' && !in_array($key, $keys, true)) { + $keys[] = $key; + } + } + + return $keys; +} + +function cc_test_current_user_can_view_flower_recoverables(): bool +{ + $role = trim((string) ($_SESSION['user_role'] ?? '')); + if (in_array($role, ['Administrador', 'admin'], true)) { + return true; + } + + return !in_array('CARMEN', cc_test_current_user_identity_keys(), true); +} + +function cc_test_filter_visible_drive_stores(array $availableStores): array +{ + if (cc_test_current_user_can_view_flower_recoverables()) { + return $availableStores; + } + + unset($availableStores['flower_recuperables']); + + return $availableStores; +} + function cc_test_upsert_assignee(PDO $pdo, string $sourceKey, ?int $userId): void { cc_test_ensure_tracking_table($pdo); @@ -2599,6 +2633,74 @@ function cc_test_build_performance_dashboard_period(array $orders, array $assess ]; } +function cc_test_filter_dashboard_status_chart(array $dashboard, array $keepLabels): array +{ + $keepLookup = []; + foreach ($keepLabels as $label) { + $label = trim((string) $label); + if ($label !== '') { + $keepLookup[$label] = true; + } + } + + if ($keepLookup === []) { + return $dashboard; + } + + $statusChart = (array) ($dashboard['status_chart'] ?? []); + $labels = array_values((array) ($statusChart['labels'] ?? [])); + $datasets = array_values((array) ($statusChart['datasets'] ?? [])); + + if ($labels === [] || $datasets === []) { + return $dashboard; + } + + $keptIndexes = []; + $filteredLabels = []; + foreach ($labels as $index => $label) { + $label = (string) $label; + if (!isset($keepLookup[$label])) { + continue; + } + + $keptIndexes[] = $index; + $filteredLabels[] = $label; + } + + if ($filteredLabels === []) { + return $dashboard; + } + + $filteredDatasets = []; + foreach ($datasets as $dataset) { + $dataset = (array) $dataset; + $filteredDataset = $dataset; + + foreach (['data', 'backgroundColor', 'borderColor'] as $field) { + if (!isset($dataset[$field]) || !is_array($dataset[$field])) { + continue; + } + + $values = array_values($dataset[$field]); + $filteredValues = []; + foreach ($keptIndexes as $index) { + if (array_key_exists($index, $values)) { + $filteredValues[] = $values[$index]; + } + } + $filteredDataset[$field] = $filteredValues; + } + + $filteredDatasets[] = $filteredDataset; + } + + $statusChart['labels'] = $filteredLabels; + $statusChart['datasets'] = $filteredDatasets; + $dashboard['status_chart'] = $statusChart; + + return $dashboard; +} + function cc_test_render_performance_dashboard(array $dashboard, array $options = []): string { $title = trim((string) ($options['title'] ?? 'KPI diario por asesora')); @@ -2639,15 +2741,25 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options = $confirmedTodayTotal = (int) ($summary['confirmed_today_total'] ?? 0); $effectiveAssignedToday = (int) ($summary['assigned_effective_today'] ?? max(0, (int) ($summary['assigned_today'] ?? 0) - (int) ($summary['repeated_today'] ?? 0))); $performanceTodayRate = (float) ($summary['performance_today_rate'] ?? 0); - $performanceTodayMetaTitle = sprintf( - '%d confirmados sobre %d asignados efectivos = %d nuevos + %d recuperado%s', - $confirmedTodayTotal, - $effectiveAssignedToday, - $confirmedTodayNew, - $confirmedTodayRecovered, - $confirmedTodayRecovered === 1 ? '' : 's' - ); - $performanceTodayValue = number_format($performanceTodayRate, 1) . '%'; + $hidePerformanceTodayValue = !empty($options['performance_value_hidden']); + $performanceTodayMetaTitle = trim((string) ($options['performance_meta_title'] ?? '')); + if ($performanceTodayMetaTitle === '') { + $performanceTodayMetaTitle = sprintf( + '%d confirmados sobre %d asignados efectivos = %d nuevos + %d recuperado%s', + $confirmedTodayTotal, + $effectiveAssignedToday, + $confirmedTodayNew, + $confirmedTodayRecovered, + $confirmedTodayRecovered === 1 ? '' : 's' + ); + } + $performanceTodayValue = ''; + if (!$hidePerformanceTodayValue) { + $performanceTodayValue = trim((string) ($options['performance_value'] ?? '')); + if ($performanceTodayValue === '') { + $performanceTodayValue = number_format($performanceTodayRate, 1) . '%'; + } + } $statusLegendItems = []; if ($statusTotal > 0) { @@ -2748,7 +2860,9 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options =
- + + +
diff --git a/includes/drive_test_orders.php b/includes/drive_test_orders.php index 8122a56e..d38eba91 100644 --- a/includes/drive_test_orders.php +++ b/includes/drive_test_orders.php @@ -12,7 +12,7 @@ function drive_test_normalize_header(string $header): string function drive_test_available_stores(): array { - return [ + $stores = [ // Hoja original que ya venía funcionando. 'flower' => [ 'label' => 'Flower', @@ -81,6 +81,12 @@ function drive_test_available_stores(): array 'incremental_sync' => true, ], ]; + + if (function_exists('cc_test_filter_visible_drive_stores')) { + $stores = cc_test_filter_visible_drive_stores($stores); + } + + return $stores; } function drive_test_store_navigation_groups(array $availableStores): array