From 0897d311c3507454ae561f315b690f0010d10f91 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 21 Jul 2026 18:49:58 +0000 Subject: [PATCH] Autosave: 20260721-185118 --- assets/css/style.css | 106 +++++ call_center_pro.php | 66 +++- gestiones_callcenter.php | 56 ++- includes/callcenter_test_helpers.php | 555 +++++++++++++++++++++++++++ includes/drive_test_orders.php | 4 +- 5 files changed, 761 insertions(+), 26 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index f89014a8..b5c2c454 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -445,3 +445,109 @@ h1, .h1 { .cc-callcenter-row:hover td { background-color: var(--cc-row-bg, #ffffff) !important; } + +/* Call center KPI dashboard */ +.cc-kpi-panel { + background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); + border: 1px solid rgba(13, 110, 253, .08) !important; +} + +.cc-kpi-pill-group { + align-items: stretch; +} + +.cc-kpi-pill { + min-width: 10.5rem; + padding: .85rem 1rem; + border-radius: 1rem; + border: 1px solid rgba(15, 23, 42, .08); + background: #ffffff; + box-shadow: 0 .7rem 1.35rem rgba(15, 23, 42, .05); + display: flex; + flex-direction: column; + gap: .15rem; + color: #111827; +} + +.cc-kpi-label { + font-size: .72rem; + font-weight: 700; + letter-spacing: .04em; + text-transform: uppercase; + line-height: 1.05; + opacity: .72; +} + +.cc-kpi-value { + font-size: 1.35rem; + font-weight: 800; + line-height: 1.05; +} + +.cc-kpi-pill-primary { + background: linear-gradient(135deg, rgba(13, 110, 253, .14), rgba(13, 110, 253, .04)); +} + +.cc-kpi-pill-success { + background: linear-gradient(135deg, rgba(25, 135, 84, .14), rgba(25, 135, 84, .04)); +} + +.cc-kpi-pill-warning { + background: linear-gradient(135deg, rgba(255, 193, 7, .18), rgba(255, 193, 7, .06)); +} + +.cc-kpi-pill-info { + background: linear-gradient(135deg, rgba(13, 202, 240, .14), rgba(13, 202, 240, .04)); +} + +.cc-kpi-pill-danger { + background: linear-gradient(135deg, rgba(220, 53, 69, .14), rgba(220, 53, 69, .04)); +} + +.cc-kpi-pill-dark { + background: linear-gradient(135deg, rgba(33, 37, 41, .10), rgba(33, 37, 41, .03)); +} + +.cc-kpi-pill-secondary { + background: linear-gradient(135deg, rgba(108, 117, 125, .12), rgba(108, 117, 125, .04)); +} + +.cc-kpi-chart-card { + border-radius: 1.2rem; + overflow: hidden; + background: #ffffff; +} + +.cc-kpi-chart-wrap { + position: relative; + min-height: 320px; +} + +.cc-kpi-chart-wrap canvas { + width: 100% !important; + height: 320px !important; +} + +.cc-kpi-footnote { + font-size: .85rem; + color: #6c757d; +} + +@media (max-width: 1199.98px) { + .cc-kpi-pill { + min-width: 9.5rem; + } + + .cc-kpi-chart-wrap, + .cc-kpi-chart-wrap canvas { + min-height: 280px; + height: 280px !important; + } +} + +@media (max-width: 575.98px) { + .cc-kpi-pill { + min-width: calc(50% - .5rem); + flex: 1 1 calc(50% - .5rem); + } +} \ No newline at end of file diff --git a/call_center_pro.php b/call_center_pro.php index 78f149db..c85942d4 100644 --- a/call_center_pro.php +++ b/call_center_pro.php @@ -182,8 +182,16 @@ function cc_test_order_time(array $order): int function cc_test_import_time(array $order): int { - if (!empty($order['is_agregado'])) { - return (int) ($order['id'] ?? 0); + 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'] ?? '')); @@ -201,7 +209,11 @@ function cc_test_import_time(array $order): int $codigoRaw = trim((string) ($order['codigo'] ?? '')); $digits = preg_replace('/\D+/', '', $codigoRaw); - return $digits !== '' ? (int) $digits : 0; + if ($digits !== '') { + return (int) $digits; + } + + return (int) ($order['id'] ?? 0); } function cc_test_followup_semaforo(array $order): ?array { @@ -237,7 +249,7 @@ function cc_test_panel_orders_signature(array $orders): string $view = $_GET['view'] ?? 'pendientes_hoy'; $allowedViews = [ 'pendientes_hoy' => 'Bandeja principal', - 'nuevos_hoy' => 'Nuevos de hoy', + 'nuevos_hoy' => 'Pedidos asignados hoy', 'confirmados' => 'Confirmados', 'seguimiento' => 'Seguimiento', 'promo_final' => 'Promo Final', @@ -410,7 +422,7 @@ try { foreach ($orders as &$order) { $order['estado'] = cc_test_normalize_state((string) ($order['estado'] ?? '')); $order['total_llamadas'] = (int) ($callCounts[$order['source_key']] ?? 0); - $importDate = cc_test_parse_datetime($order['import_id'] ?? null); + $assignedDate = cc_test_parse_datetime($order['assigned_at'] ?? null); $proximaDate = cc_test_parse_datetime($order['proxima_llamada_at'] ?? null); $followupDayInfo = cc_test_followup_day_info($order); $order['seguimiento_dia_info'] = $followupDayInfo; @@ -424,7 +436,7 @@ try { : null; $order['pendiente_logistica'] = $order['pendiente_logistica_destino'] !== null; - $order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false; + $order['es_nuevo_hoy'] = $assignedDate ? $assignedDate->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_find_assessor_key_by_user_id(isset($order['user_id']) ? (int) $order['user_id'] : null, $assessors); @@ -462,6 +474,23 @@ try { } unset($order); + $performanceDashboard = cc_test_build_performance_dashboard($orders, $assessors, 7); + $performanceTitle = $isAdmin + ? ($selectedAssessorFilterLabel !== '' ? 'KPI diario · ' . $selectedAssessorFilterLabel : 'KPI diario por asesora') + : 'Mi KPI diario'; + $performanceSubtitle = $isAdmin + ? 'Confirmación = pedidos asignados hoy confirmados hoy. Recuperación = pedidos de días anteriores confirmados hoy.' + : 'Tus KPIs de hoy se calculan sobre tus pedidos asignados. Confirmación y recuperación se separan para ver tu productividad real.'; + $performanceDashboardHtml = cc_test_render_performance_dashboard($performanceDashboard, [ + 'title' => $performanceTitle, + 'subtitle' => $performanceSubtitle, + 'chart1_title' => $isAdmin ? 'Rendimiento por asesora hoy' : 'Mi rendimiento de hoy', + 'chart1_note' => 'Asignados, confirmados nuevos, recuperados y cancelados de hoy.', + 'chart2_title' => $isAdmin ? 'Evolución de los últimos 7 días' : 'Tu evolución de los últimos 7 días', + 'chart2_note' => 'La línea de confirmados separa pedidos nuevos y recuperados.', + 'footnote' => 'Seguimiento abierto = pedidos en SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.', + ]); + $panelSignature = cc_test_panel_orders_signature($orders); $visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view, $selectedAssessorFilter): bool { @@ -482,15 +511,23 @@ try { })); usort($visibleOrders, static function (array $a, array $b) use ($view, $storeKey): int { - $aAgregado = !empty($a['is_agregado']); - $bAgregado = !empty($b['is_agregado']); - if ($aAgregado !== $bAgregado) { - return $aAgregado ? -1 : 1; - } - $aImport = cc_test_import_time($a); $bImport = cc_test_import_time($b); + if ($view === 'nuevos_hoy') { + $aAssigned = cc_test_parse_datetime($a['assigned_at'] ?? null); + $bAssigned = cc_test_parse_datetime($b['assigned_at'] ?? null); + if ($aAssigned && $bAssigned && $aAssigned != $bAssigned) { + return $bAssigned <=> $aAssigned; + } + if ($aAssigned && !$bAssigned) { + return -1; + } + if (!$aAssigned && $bAssigned) { + return 1; + } + } + if ($view === 'promo_final') { $aDays = (int) ($a['dias_seguimiento'] ?? 0); $bDays = (int) ($b['dias_seguimiento'] ?? 0); @@ -770,6 +807,9 @@ require_once 'layout_header.php'; + + +
@@ -785,7 +825,7 @@ require_once 'layout_header.php';
-
Nuevos de hoy
+
Asignados hoy
diff --git a/gestiones_callcenter.php b/gestiones_callcenter.php index 56b4d30e..646a7888 100644 --- a/gestiones_callcenter.php +++ b/gestiones_callcenter.php @@ -117,8 +117,16 @@ function cc_test_order_time(array $order): int function cc_test_import_time(array $order): int { - if (!empty($order['is_agregado'])) { - return (int) ($order['id'] ?? 0); + 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'] ?? '')); @@ -136,7 +144,11 @@ function cc_test_import_time(array $order): int $codigoRaw = trim((string) ($order['codigo'] ?? '')); $digits = preg_replace('/\D+/', '', $codigoRaw); - return $digits !== '' ? (int) $digits : 0; + if ($digits !== '') { + return (int) $digits; + } + + return (int) ($order['id'] ?? 0); } function cc_test_followup_semaforo(array $order): ?array { @@ -610,7 +622,7 @@ try { foreach ($orders as &$order) { $order['estado'] = cc_test_normalize_state((string) ($order['estado'] ?? '')); $order['total_llamadas'] = (int) ($callCounts[$order['source_key']] ?? 0); - $importDate = cc_test_parse_datetime($order['import_id'] ?? null); + $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; @@ -624,7 +636,7 @@ try { : null; $order['pendiente_logistica'] = $order['pendiente_logistica_destino'] !== null; - $order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false; + $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); @@ -663,6 +675,17 @@ try { } unset($order); + $performanceDashboard = cc_test_build_performance_dashboard($orders, $assessors, 7); + $performanceDashboardHtml = cc_test_render_performance_dashboard($performanceDashboard, [ + 'title' => 'KPI diario por asesora', + 'subtitle' => 'Confirmación = pedidos asignados hoy confirmados hoy. Recuperación = pedidos de días anteriores confirmados hoy.', + 'chart1_title' => 'Rendimiento por asesora hoy', + 'chart1_note' => 'Asignados, confirmados nuevos, recuperados y cancelados de hoy.', + 'chart2_title' => 'Evolución de los últimos 7 días', + 'chart2_note' => 'La línea de confirmados separa pedidos nuevos y recuperados.', + 'footnote' => 'Seguimiento abierto = pedidos en SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.', + ]); + $orderedAssessorKeys = array_values(array_filter(cc_test_allowed_module_user_keys(), static function (string $key) use ($assessors): bool { return isset($assessors[$key]); })); @@ -744,12 +767,6 @@ try { })); usort($visibleOrders, static function (array $a, array $b) use ($view, $storeKey): int { - $aAgregado = !empty($a['is_agregado']); - $bAgregado = !empty($b['is_agregado']); - if ($aAgregado !== $bAgregado) { - return $aAgregado ? -1 : 1; - } - $aImport = cc_test_import_time($a); $bImport = cc_test_import_time($b); @@ -764,6 +781,20 @@ try { } } + 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']); @@ -1075,6 +1106,9 @@ require_once 'layout_header.php';
+ + +
diff --git a/includes/callcenter_test_helpers.php b/includes/callcenter_test_helpers.php index 2389b055..c825bd9a 100644 --- a/includes/callcenter_test_helpers.php +++ b/includes/callcenter_test_helpers.php @@ -620,6 +620,561 @@ function cc_test_followup_day_info(array $order): ?array ]; } +function cc_test_dashboard_date_key(?string $value): ?string +{ + $date = cc_test_followup_parse_datetime($value); + return $date instanceof DateTimeImmutable ? $date->format('Y-m-d') : null; +} + +function cc_test_dashboard_origin_datetime(array $order): ?DateTimeImmutable +{ + foreach (['first_seen_at', 'drive_imported_at', 'import_id', 'assigned_at', 'updated_at'] as $field) { + $date = cc_test_followup_parse_datetime($order[$field] ?? null); + if ($date instanceof DateTimeImmutable) { + return $date; + } + } + + return null; +} + +function cc_test_dashboard_action_datetime(array $order): ?DateTimeImmutable +{ + $estado = cc_test_normalize_state((string) ($order['estado'] ?? '')); + + $fields = match ($estado) { + 'CONFIRMADO CONTRAENTREGA' => ['ruta_contraentrega_subido_at', 'updated_at', 'ultima_gestion_at', 'assigned_at'], + 'CONFIRMADO ENVIO' => ['pedido_rotulado_subido_at', 'updated_at', 'ultima_gestion_at', 'assigned_at'], + 'CANCELADO' => ['cancelado_evidencia_subido_at', 'updated_at', 'ultima_gestion_at', 'assigned_at'], + 'SE ENVIO NUMERO DE CUENTA' => ['numero_cuenta_enviado_at', 'updated_at', 'ultima_gestion_at', 'assigned_at'], + default => ['updated_at', 'ultima_gestion_at', 'assigned_at'], + }; + + foreach ($fields as $field) { + $date = cc_test_followup_parse_datetime($order[$field] ?? null); + if ($date instanceof DateTimeImmutable) { + return $date; + } + } + + return null; +} + +function cc_test_dashboard_bucket_key(array $order, array $assessors, bool $includeUnassigned = true): string +{ + $assessorKey = trim((string) ($order['assessor_key'] ?? '')); + if ($assessorKey !== '') { + return $assessorKey; + } + + $userId = (int) ($order['user_id'] ?? 0); + if ($userId > 0) { + foreach ($assessors as $key => $assessor) { + if ((int) ($assessor['id'] ?? 0) === $userId) { + return (string) $key; + } + } + } + + return $includeUnassigned ? 'SIN_ASIGNAR' : ''; +} + +function cc_test_dashboard_bucket_label(string $bucketKey, array $assessors): string +{ + if ($bucketKey === '' || $bucketKey === 'SIN_ASIGNAR') { + return 'Sin asignar'; + } + + return (string) ($assessors[$bucketKey]['label'] ?? $bucketKey); +} + +function cc_test_build_performance_dashboard(array $orders, array $assessors, int $trendDays = 7): array +{ + $trendDays = max(1, $trendDays); + $today = new DateTimeImmutable('today'); + $todayKey = $today->format('Y-m-d'); + + $trend = []; + $trendLabels = []; + for ($i = $trendDays - 1; $i >= 0; $i--) { + $day = $today->sub(new DateInterval('P' . $i . 'D')); + $dayKey = $day->format('Y-m-d'); + $trend[$dayKey] = [ + 'label' => $day->format('d/m'), + 'assigned_today' => 0, + 'confirmed_new' => 0, + 'recovered' => 0, + 'canceled' => 0, + ]; + $trendLabels[] = $day->format('d/m'); + } + + $orderedAssessorKeys = array_values(array_filter(cc_test_allowed_module_user_keys(), static function (string $key) use ($assessors): bool { + return isset($assessors[$key]); + })); + if (empty($orderedAssessorKeys)) { + $orderedAssessorKeys = array_keys($assessors); + } + + $bucketStats = []; + foreach ($orderedAssessorKeys as $assessorKey) { + $bucketStats[$assessorKey] = [ + 'label' => cc_test_dashboard_bucket_label($assessorKey, $assessors), + 'assigned_today' => 0, + 'confirmed_new' => 0, + 'recovered' => 0, + 'canceled' => 0, + 'followup_open' => 0, + ]; + } + + $summary = [ + 'scope_total' => 0, + 'assigned_today' => 0, + 'confirmed_today_new' => 0, + 'confirmed_today_recovered' => 0, + 'confirmed_today_total' => 0, + 'canceled_today' => 0, + 'followup_open' => 0, + 'confirm_rate_today' => 0.0, + 'recovery_rate_today' => 0.0, + ]; + + $hasUnassignedBucket = false; + + foreach ($orders as $order) { + $summary['scope_total']++; + + $bucketKey = cc_test_dashboard_bucket_key($order, $assessors, true); + if ($bucketKey === '') { + continue; + } + if ($bucketKey === 'SIN_ASIGNAR') { + $hasUnassignedBucket = true; + } + if (!isset($bucketStats[$bucketKey])) { + $bucketStats[$bucketKey] = [ + 'label' => cc_test_dashboard_bucket_label($bucketKey, $assessors), + 'assigned_today' => 0, + 'confirmed_new' => 0, + 'recovered' => 0, + 'canceled' => 0, + 'followup_open' => 0, + ]; + } + + $originDate = cc_test_dashboard_origin_datetime($order); + $originKey = $originDate ? $originDate->format('Y-m-d') : null; + $assignedDate = cc_test_followup_parse_datetime($order['assigned_at'] ?? null); + $assignedKey = $assignedDate ? $assignedDate->format('Y-m-d') : null; + $actionDate = cc_test_dashboard_action_datetime($order); + $actionKey = $actionDate ? $actionDate->format('Y-m-d') : null; + $estado = cc_test_normalize_state((string) ($order['estado'] ?? '')); + $followupInfo = cc_test_followup_day_info($order); + $isPromoFinal = !empty($followupInfo['is_promo_final']) && !empty($order['promo_final_evidence_path']); + $isConfirmed = in_array($estado, cc_test_confirmed_states(), true); + $isCanceled = $estado === 'CANCELADO'; + $isFollowupOpen = $estado === 'SE ENVIO NUMERO DE CUENTA' && !$isPromoFinal; + + if ($assignedKey === $todayKey) { + $bucketStats[$bucketKey]['assigned_today']++; + $summary['assigned_today']++; + + if (isset($trend[$assignedKey])) { + $trend[$assignedKey]['assigned_today']++; + } + } + + if ($isFollowupOpen) { + $bucketStats[$bucketKey]['followup_open']++; + $summary['followup_open']++; + } + + if ($isConfirmed && $actionKey === $todayKey) { + if ($originKey !== null && $originKey < $todayKey) { + $bucketStats[$bucketKey]['recovered']++; + $summary['confirmed_today_recovered']++; + } else { + $bucketStats[$bucketKey]['confirmed_new']++; + $summary['confirmed_today_new']++; + } + + if (isset($trend[$actionKey])) { + if ($originKey !== null && $originKey < $actionKey) { + $trend[$actionKey]['recovered']++; + } else { + $trend[$actionKey]['confirmed_new']++; + } + } + } + + if ($isCanceled && $actionKey === $todayKey) { + $bucketStats[$bucketKey]['canceled']++; + $summary['canceled_today']++; + + if (isset($trend[$actionKey])) { + $trend[$actionKey]['canceled']++; + } + } + } + + $summary['confirmed_today_total'] = $summary['confirmed_today_new'] + $summary['confirmed_today_recovered']; + $summary['confirm_rate_today'] = $summary['assigned_today'] > 0 + ? round(($summary['confirmed_today_new'] / $summary['assigned_today']) * 100, 1) + : 0.0; + $summary['recovery_rate_today'] = $summary['confirmed_today_total'] > 0 + ? round(($summary['confirmed_today_recovered'] / $summary['confirmed_today_total']) * 100, 1) + : 0.0; + + $advisorLabels = []; + $advisorAssigned = []; + $advisorConfirmedNew = []; + $advisorRecovered = []; + $advisorCanceled = []; + $advisorFollowup = []; + + $finalBucketKeys = $orderedAssessorKeys; + foreach (array_keys($bucketStats) as $bucketKey) { + if (!in_array($bucketKey, $finalBucketKeys, true) && $bucketKey !== 'SIN_ASIGNAR') { + $finalBucketKeys[] = $bucketKey; + } + } + if ($hasUnassignedBucket) { + $finalBucketKeys[] = 'SIN_ASIGNAR'; + } + + foreach ($finalBucketKeys as $bucketKey) { + if (!isset($bucketStats[$bucketKey])) { + continue; + } + + $bucket = $bucketStats[$bucketKey]; + $advisorLabels[] = $bucket['label']; + $advisorAssigned[] = (int) $bucket['assigned_today']; + $advisorConfirmedNew[] = (int) $bucket['confirmed_new']; + $advisorRecovered[] = (int) $bucket['recovered']; + $advisorCanceled[] = (int) $bucket['canceled']; + $advisorFollowup[] = (int) $bucket['followup_open']; + } + + if (empty($advisorLabels)) { + $advisorLabels = ['Sin datos']; + $advisorAssigned = [0]; + $advisorConfirmedNew = [0]; + $advisorRecovered = [0]; + $advisorCanceled = [0]; + $advisorFollowup = [0]; + } + + $trendAssigned = []; + $trendConfirmedNew = []; + $trendRecovered = []; + $trendCanceled = []; + foreach (array_keys($trend) as $dayKey) { + $trendAssigned[] = (int) $trend[$dayKey]['assigned_today']; + $trendConfirmedNew[] = (int) $trend[$dayKey]['confirmed_new']; + $trendRecovered[] = (int) $trend[$dayKey]['recovered']; + $trendCanceled[] = (int) $trend[$dayKey]['canceled']; + } + + $advisorCount = 0; + foreach ($advisorLabels as $advisorLabel) { + if ($advisorLabel !== 'Sin datos' && $advisorLabel !== 'Sin asignar') { + $advisorCount++; + } + } + + return [ + 'summary' => $summary, + 'advisor_count' => $advisorCount, + 'advisor_chart' => [ + 'labels' => $advisorLabels, + 'datasets' => [ + [ + 'label' => 'Asignados hoy', + 'data' => $advisorAssigned, + 'backgroundColor' => 'rgba(13, 110, 253, 0.75)', + 'borderColor' => 'rgba(13, 110, 253, 1)', + 'borderWidth' => 1, + 'borderRadius' => 6, + 'maxBarThickness' => 22, + ], + [ + 'label' => 'Confirmados nuevos hoy', + 'data' => $advisorConfirmedNew, + 'backgroundColor' => 'rgba(25, 135, 84, 0.78)', + 'borderColor' => 'rgba(25, 135, 84, 1)', + 'borderWidth' => 1, + 'borderRadius' => 6, + 'maxBarThickness' => 22, + ], + [ + 'label' => 'Recuperados de otros días', + 'data' => $advisorRecovered, + 'backgroundColor' => 'rgba(255, 193, 7, 0.82)', + 'borderColor' => 'rgba(245, 158, 11, 1)', + 'borderWidth' => 1, + 'borderRadius' => 6, + 'maxBarThickness' => 22, + ], + [ + 'label' => 'Cancelados hoy', + 'data' => $advisorCanceled, + 'backgroundColor' => 'rgba(220, 53, 69, 0.75)', + 'borderColor' => 'rgba(220, 53, 69, 1)', + 'borderWidth' => 1, + 'borderRadius' => 6, + 'maxBarThickness' => 22, + ], + ], + ], + 'trend_chart' => [ + 'labels' => $trendLabels, + 'datasets' => [ + [ + 'label' => 'Asignados', + 'data' => $trendAssigned, + 'borderColor' => 'rgba(13, 110, 253, 1)', + 'backgroundColor' => 'rgba(13, 110, 253, 0.12)', + 'borderWidth' => 2, + 'tension' => 0.35, + 'fill' => false, + 'pointRadius' => 2, + 'pointHoverRadius' => 4, + ], + [ + 'label' => 'Confirmados nuevos', + 'data' => $trendConfirmedNew, + 'borderColor' => 'rgba(25, 135, 84, 1)', + 'backgroundColor' => 'rgba(25, 135, 84, 0.12)', + 'borderWidth' => 2, + 'tension' => 0.35, + 'fill' => false, + 'pointRadius' => 2, + 'pointHoverRadius' => 4, + ], + [ + 'label' => 'Recuperados', + 'data' => $trendRecovered, + 'borderColor' => 'rgba(245, 158, 11, 1)', + 'backgroundColor' => 'rgba(245, 158, 11, 0.12)', + 'borderWidth' => 2, + 'tension' => 0.35, + 'fill' => false, + 'pointRadius' => 2, + 'pointHoverRadius' => 4, + ], + [ + 'label' => 'Cancelados', + 'data' => $trendCanceled, + 'borderColor' => 'rgba(220, 53, 69, 1)', + 'backgroundColor' => 'rgba(220, 53, 69, 0.12)', + 'borderWidth' => 2, + 'tension' => 0.35, + 'fill' => false, + 'pointRadius' => 2, + 'pointHoverRadius' => 4, + ], + ], + ], + ]; +} + +function cc_test_render_performance_dashboard(array $dashboard, array $options = []): string +{ + $title = trim((string) ($options['title'] ?? 'KPI diario por asesora')); + $subtitle = trim((string) ($options['subtitle'] ?? 'Confirmación = pedidos de hoy cerrados hoy. Recuperación = pedidos de otros días cerrados hoy.')); + $chart1Title = trim((string) ($options['chart1_title'] ?? 'Rendimiento por asesora hoy')); + $chart1Note = trim((string) ($options['chart1_note'] ?? 'Asignados, confirmados nuevos, recuperados y cancelados de hoy.')); + $chart2Title = trim((string) ($options['chart2_title'] ?? 'Evolución de los últimos 7 días')); + $chart2Note = trim((string) ($options['chart2_note'] ?? 'La línea de confirmados separa pedidos nuevos y recuperados.')); + $footnote = trim((string) ($options['footnote'] ?? 'Seguimiento abierto = pedidos en SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.')); + $sectionId = preg_replace('/[^A-Za-z0-9_-]+/', '', (string) ($options['section_id'] ?? 'ccPerformanceDashboard')); + if ($sectionId === '') { + $sectionId = 'ccPerformanceDashboard'; + } + + $summary = (array) ($dashboard['summary'] ?? []); + $advisorChart = (array) ($dashboard['advisor_chart'] ?? []); + $trendChart = (array) ($dashboard['trend_chart'] ?? []); + $advisorCount = (int) ($dashboard['advisor_count'] ?? 0); + + $badges = [ + ['label' => 'Asignados hoy', 'value' => (int) ($summary['assigned_today'] ?? 0), 'class' => 'cc-kpi-pill-primary'], + ['label' => 'Confirmados hoy', 'value' => (int) ($summary['confirmed_today_total'] ?? 0), 'class' => 'cc-kpi-pill-success'], + ['label' => 'Recuperados', 'value' => (int) ($summary['confirmed_today_recovered'] ?? 0), 'class' => 'cc-kpi-pill-warning'], + ['label' => 'Seguimiento abierto', 'value' => (int) ($summary['followup_open'] ?? 0), 'class' => 'cc-kpi-pill-info'], + ['label' => 'Cancelados hoy', 'value' => (int) ($summary['canceled_today'] ?? 0), 'class' => 'cc-kpi-pill-danger'], + ['label' => '% confirmación', 'value' => number_format((float) ($summary['confirm_rate_today'] ?? 0), 1) . '%', 'class' => 'cc-kpi-pill-dark'], + ['label' => '% recuperación', 'value' => number_format((float) ($summary['recovery_rate_today'] ?? 0), 1) . '%', 'class' => 'cc-kpi-pill-secondary'], + ]; + + $extraLineParts = []; + if (isset($summary['confirmed_today_new'])) { + $extraLineParts[] = (int) $summary['confirmed_today_new'] . ' confirmados nuevos'; + } + if (isset($summary['confirmed_today_recovered'])) { + $extraLineParts[] = (int) $summary['confirmed_today_recovered'] . ' recuperados'; + } + if (isset($summary['canceled_today'])) { + $extraLineParts[] = (int) $summary['canceled_today'] . ' cancelados'; + } + if (isset($summary['followup_open'])) { + $extraLineParts[] = (int) $summary['followup_open'] . ' en seguimiento'; + } + $extraLine = implode(' · ', $extraLineParts); + + ob_start(); + ?> +
+
+
+
+
+
Panel KPI
+

+

+
+
+ +
+ + +
+ +
+
+ +
+ + +
+
+
+
+
+
+

+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+

+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ + + + execute($params);