diff --git a/call_center_pro.php b/call_center_pro.php index b4c19a66..4a6aba85 100644 --- a/call_center_pro.php +++ b/call_center_pro.php @@ -16,6 +16,28 @@ $departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega $pageTitle = 'Pedidos Asignados | Bandeja de la asesora'; $pageDescription = 'Bandeja para que cada asesora gestione sus pedidos asignados con la misma interfaz, estados, historial y acciones.'; +$kpiPeriodConfig = cc_test_resolve_kpi_period((string) ($_GET['kpi_period'] ?? 'today'), (string) ($_GET['kpi_date'] ?? '')); +$kpiPeriodKey = (string) ($kpiPeriodConfig['key'] ?? 'today'); +$kpiPeriodLabel = (string) ($kpiPeriodConfig['label'] ?? 'Hoy'); +$kpiPeriodCustomDate = $kpiPeriodConfig['custom_date'] instanceof DateTimeImmutable ? $kpiPeriodConfig['custom_date'] : null; +$kpiPeriodCustomDateInput = $kpiPeriodCustomDate instanceof DateTimeImmutable ? $kpiPeriodCustomDate->format('Y-m-d') : ''; +$kpiPeriodTitleLabel = match ($kpiPeriodKey) { + 'today' => 'diario', + 'yesterday' => 'ayer', + '7', '15', '30' => 'los ' . mb_strtolower($kpiPeriodLabel), + default => mb_strtolower($kpiPeriodLabel), +}; +$kpiPeriodStart = $kpiPeriodConfig['start'] instanceof DateTimeImmutable ? $kpiPeriodConfig['start'] : new DateTimeImmutable('today'); +$kpiPeriodEnd = $kpiPeriodConfig['end'] instanceof DateTimeImmutable ? $kpiPeriodConfig['end'] : $kpiPeriodStart; +$kpiPeriodButtons = [ + 'today' => 'Hoy', + 'yesterday' => 'Ayer', + '7' => '7 Días', + '15' => '15 Días', + '30' => '30 Días', + 'month' => 'Este Mes', +]; + if (!cc_test_current_user_can_access_module($pdo)) { http_response_code(403); require_once 'layout_header.php'; @@ -676,28 +698,39 @@ try { })); } - $performanceDashboard = cc_test_build_performance_dashboard($performanceOrders, $assessors, 7); + $performanceDashboard = cc_test_build_performance_dashboard_period($performanceOrders, $assessors, $kpiPeriodStart, $kpiPeriodEnd); + $performanceSubtitle = $isTuaniRecuperablesStore + ? 'Los indicadores de esta bandeja se calculan sobre todos los pedidos visibles para cualquier asesora en el periodo seleccionado.' + : ($isAdmin + ? 'Confirmación = pedidos asignados en el periodo confirmados en el mismo periodo. Recuperación = pedidos asignados antes del periodo confirmados dentro del periodo. Rendimiento = confirmados totales (nuevos + recuperados) sobre asignados del periodo.' + : 'Tus KPIs del periodo seleccionado se calculan sobre tus pedidos asignados en ese rango. Confirmación = asignados del periodo confirmados en el mismo periodo. Recuperación = asignados antes del periodo confirmados dentro del periodo. Rendimiento = confirmados totales (nuevos + recuperados) sobre asignados del periodo.'); + if ($isTuaniRecuperablesStore) { - $performanceTitle = 'KPI compartido de recuperación'; - $performanceSubtitle = 'Los indicadores de esta bandeja se calculan sobre todos los pedidos visibles para cualquier asesora.'; + $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 visibles por estado en esta bandeja compartida.'; - $performanceChart2Title = 'Evolución de los últimos 7 días'; - $performanceChart2Note = 'La línea muestra cómo se mueve la bandeja compartida en los últimos 7 días.'; + $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 y cancelados.'; + $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.'; } else { - $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 asignados antes de hoy confirmados hoy. Rendimiento del día = confirmados totales (nuevos + recuperados) sobre asignados hoy.' - : 'Tus KPIs de hoy se calculan sobre tus pedidos asignados hoy. Confirmación = asignados hoy confirmados hoy. Recuperación = asignados antes de hoy confirmados hoy. Rendimiento del día = confirmados totales (nuevos + recuperados) sobre asignados hoy.'; + if ($isAdmin) { + $performanceTitle = $selectedAssessorFilterLabel !== '' + ? ($kpiPeriodKey === 'today' ? 'KPI diario · ' . $selectedAssessorFilterLabel : 'KPI de ' . $kpiPeriodTitleLabel . ' · ' . $selectedAssessorFilterLabel) + : ($kpiPeriodKey === 'today' ? 'KPI diario por asesora' : 'KPI de ' . $kpiPeriodTitleLabel . ' por asesora'); + } else { + $performanceTitle = $kpiPeriodKey === 'today' ? 'Mi KPI diario' : 'Mi KPI de ' . $kpiPeriodTitleLabel; + } $performanceChart1Title = $isAdmin ? 'Distribución de estados' : 'Mi distribución de estados'; - $performanceChart1Note = 'La dona separa los pedidos de hoy por estado: confirmados nuevos, recuperados, POR LLAMAR, DEVOLVER LLAMADA, OBSERVADO, SE ENVIO NUMERO DE CUENTA, repetidos y cancelados.'; - $performanceChart2Title = $isAdmin ? 'Evolución de los últimos 7 días' : 'Tu evolución de los últimos 7 días'; - $performanceChart2Note = 'La línea de confirmados separa pedidos asignados hoy y pedidos asignados antes de hoy.'; - $performanceFootnote = '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.'; + $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 y cancelados.'; + $performanceChart2Title = $kpiPeriodKey === 'today' + ? ($isAdmin ? 'Evolución de hoy' : 'Tu evolución de hoy') + : ($isAdmin ? 'Evolución de ' . $kpiPeriodTitleLabel : 'Tu evolución de ' . $kpiPeriodTitleLabel); + $performanceChart2Note = 'La línea de confirmados separa pedidos asignados y recuperados dentro del periodo seleccionado.'; + $performanceFootnote = 'Seguimiento abierto = pedidos asignados en el periodo en POR LLAMAR, DEVOLVER LLAMADA, OBSERVADO y SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.'; } + $performanceDashboardHtml = cc_test_render_performance_dashboard($performanceDashboard, [ 'title' => $performanceTitle, 'subtitle' => $performanceSubtitle, @@ -706,6 +739,8 @@ 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 (%)', ]); $visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view, $selectedAssessorFilter, $todayStart): bool { @@ -828,7 +863,11 @@ try { $callCenterBaseParams = [ 'view' => $view, 'store' => $storeKey, + 'kpi_period' => $kpiPeriodKey, ]; +if ($kpiPeriodCustomDateInput !== '') { + $callCenterBaseParams['kpi_date'] = $kpiPeriodCustomDateInput; +} $callCenterParams = $callCenterBaseParams; if ($selectedAssessorFilter !== '') { $callCenterParams['assessor'] = $selectedAssessorFilter; @@ -1083,6 +1122,41 @@ require_once 'layout_header.php'; + +
+
+
+
+
+
Filtro KPI
+

Panel KPI ·

+
Filtra el panel para ver el avance del periodo seleccionado.
+
+
+
+ $periodLabel): ?> + + +
+
+ + + + + + + +
+ + +
+
+
+
+
+
+
+
@@ -1108,6 +1182,10 @@ require_once 'layout_header.php';
+ + + +
diff --git a/download_ruta_contraentrega.php b/download_ruta_contraentrega.php index 3f7ae1ad..4cf866e1 100644 --- a/download_ruta_contraentrega.php +++ b/download_ruta_contraentrega.php @@ -277,15 +277,23 @@ try { $is_admin = in_array($user_role, ['Administrador', 'admin'], true); $search_query = trim($_GET['q'] ?? ''); + $selected_delivery_date = trim((string)($_GET['fecha_entrega'] ?? '')); + if ($selected_delivery_date !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $selected_delivery_date)) { + $selected_delivery_date = ''; + } + if ($selected_delivery_date !== '') { + [$deliveryYear, $deliveryMonth, $deliveryDay] = array_map('intval', explode('-', $selected_delivery_date)); + if (!checkdate($deliveryMonth, $deliveryDay, $deliveryYear)) { + $selected_delivery_date = ''; + } + } if (!$is_admin) { http_response_code(403); exit('Acceso denegado.'); } - // Exportar solo los pedidos con fecha de entrega "mañana" - $date_condition = "DATE(p.fecha_entrega) = DATE_ADD(CURDATE(), INTERVAL 1 DAY)"; - + // Exportar pedidos con fecha de entrega seleccionada; si no hay filtro, por defecto mañana $sql = "SELECT p.* FROM pedidos p WHERE p.estado IN ('RUTA_CONTRAENTREGA', 'NO CONTESTO, VOLVER A LLAMAR', 'NO CONTESTO, DEVOLVER LLAMADA', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA', 'RETORNADO')"; $params = []; @@ -301,8 +309,13 @@ $is_admin = in_array($user_role, ['Administrador', 'admin'], true); $params[] = "%$search_query%"; } - // Siempre: pedidos cuya fecha de entrega sea mañana - $sql .= " AND $date_condition"; + if ($selected_delivery_date !== '') { + $sql .= " AND DATE(p.fecha_entrega) = ?"; + $params[] = $selected_delivery_date; + } else { + // Siempre: pedidos cuya fecha de entrega sea mañana + $sql .= " AND DATE(p.fecha_entrega) = DATE_ADD(CURDATE(), INTERVAL 1 DAY)"; + } $sql .= " ORDER BY p.created_at DESC"; $stmt = $pdo->prepare($sql); @@ -440,7 +453,11 @@ $is_admin = in_array($user_role, ['Administrador', 'admin'], true); } } unset($row); - $filename = 'ruta_contraentrega_' . date('Y-m-d_H-i') . '.xlsx'; + if ($selected_delivery_date !== '') { + $filename = 'ruta_contraentrega_' . $selected_delivery_date . '.xlsx'; + } else { + $filename = 'ruta_contraentrega_' . date('Y-m-d_H-i') . '.xlsx'; + } SimpleXLSXGen::fromArray($rows, 'Ruta Contraentrega')->downloadAs($filename); exit; } catch (Throwable $e) { diff --git a/includes/callcenter_test_helpers.php b/includes/callcenter_test_helpers.php index 61de05f7..125b0772 100644 --- a/includes/callcenter_test_helpers.php +++ b/includes/callcenter_test_helpers.php @@ -1426,6 +1426,83 @@ function cc_test_dashboard_action_datetime(array $order): ?DateTimeImmutable return null; } +function cc_test_resolve_kpi_period(string $period, ?string $customDate = null): array +{ + $period = mb_strtolower(trim($period)); + $today = new DateTimeImmutable('today'); + $todayEnd = $today->setTime(23, 59, 59); + + $start = $today; + $end = $todayEnd; + $label = 'Hoy'; + $trendDays = 1; + $customDateValue = null; + + $customDate = trim((string) $customDate); + if ($customDate !== '' && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $customDate, $matches) === 1 && checkdate((int) $matches[2], (int) $matches[3], (int) $matches[1])) { + $customDateValue = new DateTimeImmutable($customDate); + } + + if (in_array($period, ['custom', 'date', 'specific'], true)) { + if ($customDateValue instanceof DateTimeImmutable) { + $start = $customDateValue->setTime(0, 0, 0); + $end = $customDateValue->setTime(23, 59, 59); + $label = $customDateValue->format('d/m/Y'); + $trendDays = 1; + $period = 'custom'; + } else { + $period = 'today'; + } + } + + switch ($period) { + case 'yesterday': + $start = $today->sub(new DateInterval('P1D')); + $end = $start->setTime(23, 59, 59); + $label = 'Ayer'; + break; + case '7': + $start = $today->sub(new DateInterval('P6D')); + $end = $todayEnd; + $label = 'Últimos 7 días'; + $trendDays = 7; + break; + case '15': + $start = $today->sub(new DateInterval('P14D')); + $end = $todayEnd; + $label = 'Últimos 15 días'; + $trendDays = 15; + break; + case '30': + $start = $today->sub(new DateInterval('P29D')); + $end = $todayEnd; + $label = 'Últimos 30 días'; + $trendDays = 30; + break; + case 'month': + $start = $today->modify('first day of this month'); + $end = $todayEnd; + $label = 'Este mes'; + $trendDays = max(1, $start->diff($end)->days + 1); + break; + case 'custom': + break; + case 'today': + default: + $period = 'today'; + break; + } + + return [ + 'key' => $period, + 'label' => $label, + 'start' => $start, + 'end' => $end, + 'trend_days' => $trendDays, + 'custom_date' => $customDateValue, + ]; +} + function cc_test_is_recovered_today(array $order, ?DateTimeImmutable $todayStart = null): bool { $estado = cc_test_normalize_state((string) ($order['estado'] ?? '')); @@ -1832,6 +1909,367 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in ]; } +function cc_test_build_performance_dashboard_period(array $orders, array $assessors, DateTimeImmutable $scopeStart, DateTimeImmutable $scopeEnd): array +{ + if ($scopeStart > $scopeEnd) { + [$scopeStart, $scopeEnd] = [$scopeEnd, $scopeStart]; + } + + $trendDays = max(1, $scopeStart->diff($scopeEnd)->days + 1); + + $trend = []; + $trendLabels = []; + for ($i = 0; $i < $trendDays; $i++) { + $day = $scopeStart->add(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 = cc_test_ordered_assessor_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, + 'repeated' => 0, + ]; + } + + $summary = [ + 'scope_total' => 0, + 'assigned_today' => 0, + 'confirmed_today_new' => 0, + 'confirmed_today_recovered' => 0, + 'confirmed_today_total' => 0, + 'canceled_today' => 0, + 'repeated_today' => 0, + 'followup_open' => 0, + 'observados_today' => 0, + 'confirm_rate_today' => 0.0, + 'recovery_rate_today' => 0.0, + 'performance_today_rate' => 0.0, + ]; + + $hasUnassignedBucket = false; + $followupStateCounts = [ + 'POR LLAMAR' => 0, + 'DEVOLVER LLAMADA' => 0, + 'OBSERVADO' => 0, + 'SE ENVIO NUMERO DE CUENTA' => 0, + ]; + + foreach ($orders as $order) { + $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, + 'repeated' => 0, + ]; + } + + $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'; + $isRepeated = $estado === 'REPETIDO'; + $isFollowupState = in_array($estado, cc_test_followup_tracking_states(), true); + $isFollowupOpen = $isFollowupState && !$isPromoFinal; + $assignedInScope = $assignedDate instanceof DateTimeImmutable && $assignedDate >= $scopeStart && $assignedDate <= $scopeEnd; + $actionInScope = $actionDate instanceof DateTimeImmutable && $actionDate >= $scopeStart && $actionDate <= $scopeEnd; + + if (!$assignedInScope && !$actionInScope) { + continue; + } + + $summary['scope_total']++; + + if ($assignedInScope) { + $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 (isset($followupStateCounts[$estado])) { + $followupStateCounts[$estado]++; + } + + if ($estado === 'OBSERVADO') { + $summary['observados_today']++; + } + } + } + + if ($isConfirmed && $actionInScope && $assignedDate instanceof DateTimeImmutable) { + $isNewConfirmation = $assignedKey === $actionKey; + $isRecoveredConfirmation = $assignedDate < $actionDate; + + if ($isNewConfirmation) { + $bucketStats[$bucketKey]['confirmed_new']++; + $summary['confirmed_today_new']++; + } elseif ($isRecoveredConfirmation) { + $bucketStats[$bucketKey]['recovered']++; + $summary['confirmed_today_recovered']++; + } + + if (($isNewConfirmation || $isRecoveredConfirmation) && isset($trend[$actionKey])) { + $trend[$actionKey][$isNewConfirmation ? 'confirmed_new' : 'recovered']++; + } + } + + if ($isCanceled && $assignedInScope && $actionInScope && $assignedKey === $actionKey) { + $bucketStats[$bucketKey]['canceled']++; + $summary['canceled_today']++; + + if (isset($trend[$actionKey])) { + $trend[$actionKey]['canceled']++; + } + } + + if ($isRepeated && $assignedInScope && $actionInScope && $assignedKey === $actionKey) { + $bucketStats[$bucketKey]['repeated']++; + $summary['repeated_today']++; + } + } + + $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; + $summary['performance_today_rate'] = $summary['assigned_today'] > 0 + ? round(($summary['confirmed_today_total'] / $summary['assigned_today']) * 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, + 'status_chart' => [ + 'labels' => ['Confirmados nuevos', 'Recuperados', 'Por llamar', 'Devolver llamada', 'Observados', 'Se envió número de cuenta', 'Repetidos', 'Cancelados'], + 'datasets' => [ + [ + 'label' => 'Estados', + 'data' => [ + (int) ($summary['confirmed_today_new'] ?? 0), + (int) ($summary['confirmed_today_recovered'] ?? 0), + (int) ($followupStateCounts['POR LLAMAR'] ?? 0), + (int) ($followupStateCounts['DEVOLVER LLAMADA'] ?? 0), + (int) ($followupStateCounts['OBSERVADO'] ?? 0), + (int) ($followupStateCounts['SE ENVIO NUMERO DE CUENTA'] ?? 0), + (int) ($summary['repeated_today'] ?? 0), + (int) ($summary['canceled_today'] ?? 0), + ], + 'backgroundColor' => [ + 'rgba(25, 135, 84, 0.9)', + 'rgba(20, 184, 166, 0.84)', + 'rgba(13, 110, 253, 0.88)', + 'rgba(13, 202, 240, 0.86)', + 'rgba(255, 193, 7, 0.82)', + 'rgba(253, 126, 20, 0.84)', + 'rgba(108, 117, 125, 0.84)', + 'rgba(220, 53, 69, 0.9)', + ], + 'borderColor' => [ + 'rgba(25, 135, 84, 1)', + 'rgba(13, 148, 136, 1)', + 'rgba(13, 110, 253, 1)', + 'rgba(13, 202, 240, 1)', + 'rgba(245, 158, 11, 1)', + 'rgba(253, 126, 20, 1)', + 'rgba(108, 117, 125, 1)', + 'rgba(220, 53, 69, 1)', + ], + 'borderWidth' => 2, + 'hoverOffset' => 6, + ], + ], + ], + 'advisor_count' => $advisorCount, + 'advisor_chart' => [ + 'labels' => $advisorLabels, + 'datasets' => [ + [ + 'label' => 'Asignados en el periodo', + 'data' => $advisorAssigned, + 'backgroundColor' => 'rgba(13, 110, 253, 0.75)', + 'borderColor' => 'rgba(13, 110, 253, 1)', + 'borderWidth' => 1, + 'borderRadius' => 6, + 'maxBarThickness' => 22, + ], + [ + 'label' => 'Confirmados nuevos', + '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', + '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')); @@ -1841,6 +2279,8 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options = $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 asignados hoy y pedidos asignados antes de hoy.')); $footnote = trim((string) ($options['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.')); + $performanceLabel = trim((string) ($options['performance_label'] ?? 'Rendimiento del día')); + $statusDetailHeading = trim((string) ($options['status_detail_heading'] ?? 'Detalle del KPI de hoy (%)')); $sectionId = preg_replace('/[^A-Za-z0-9_-]+/', '', (string) ($options['section_id'] ?? 'ccPerformanceDashboard')); if ($sectionId === '') { $sectionId = 'ccPerformanceDashboard'; @@ -1966,12 +2406,12 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options =
- Rendimiento del día +
-
Detalle del KPI de hoy (%)
+
diff --git a/index.php b/index.php index dab2d86a..93f82418 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,16 @@ 'assessor', @@ -108,15 +118,15 @@ $pageDescription = 'Entrada rápida al sistema CRM y panel de administración.';

Panel CRM y Administración

- Ya tienes una sesión activa. Entra directo al dashboard para continuar con la operación. + Ya tienes una sesión activa. Entra directo al módulo que corresponde a tu rol para continuar con la operación. Accede al sistema para gestionar pedidos, inventario, llamadas y operaciones desde un solo lugar.

diff --git a/layout_header.php b/layout_header.php index 8090c629..8da25655 100644 --- a/layout_header.php +++ b/layout_header.php @@ -14,6 +14,12 @@ require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/includes/callcenter_test_helpers.php'; $userRole = $_SESSION['user_role'] ?? ''; +$currentPage = basename($_SERVER['PHP_SELF']); +$isMarketingUser = strcasecmp(trim((string) $userRole), 'Marketing') === 0; +if ($isMarketingUser && $currentPage !== 'marketing_produccion.php') { + header('Location: marketing_produccion.php'); + exit(); +} $currentUserId = (int) ($_SESSION['user_id'] ?? 0); $currentUserUsername = $_SESSION['username'] ?? ''; $currentUserNombreAsesor = null; @@ -346,13 +352,13 @@ $navItems = [ 'marketing_group' => [ 'icon' => 'fa-bullhorn', 'text' => 'Marketing', - 'roles' => ['Administrador', 'admin'], + 'roles' => ['Administrador', 'admin', 'Marketing', 'marketing'], 'submenu' => [ 'marketing_produccion' => [ 'url' => 'marketing_produccion.php', 'icon' => 'fa-video', 'text' => 'Producción de Video', - 'roles' => ['Administrador', 'admin'] + 'roles' => ['Administrador', 'admin', 'Marketing', 'marketing'] ], 'calculo_costos_v3' => [ 'url' => 'calculo_costos_v3.php', @@ -447,7 +453,7 @@ $navItems = [
diff --git a/ruta_contraentrega.php b/ruta_contraentrega.php index cac4bad9..3e6c80c4 100644 --- a/ruta_contraentrega.php +++ b/ruta_contraentrega.php @@ -107,6 +107,16 @@ $years = $years_stmt->fetchAll(PDO::FETCH_COLUMN); $selected_month = $_GET['mes'] ?? ''; $selected_year = $_GET['año'] ?? ''; $search_query = $_GET['q'] ?? ''; +$selected_delivery_date = trim((string)($_GET['fecha_entrega'] ?? '')); +if ($selected_delivery_date !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $selected_delivery_date)) { + $selected_delivery_date = ''; +} +if ($selected_delivery_date !== '') { + [$deliveryYear, $deliveryMonth, $deliveryDay] = array_map('intval', explode('-', $selected_delivery_date)); + if (!checkdate($deliveryMonth, $deliveryDay, $deliveryYear)) { + $selected_delivery_date = ''; + } +} $period = $_GET['period'] ?? ''; $date_condition = ''; @@ -152,6 +162,11 @@ if (!empty($search_query)) { $params[] = "%$search_query%"; } +if (!empty($selected_delivery_date)) { + $sql .= " AND DATE(p.fecha_entrega) = ?"; + $params[] = $selected_delivery_date; +} + if (!empty($date_condition)) { $sql .= " AND $date_condition"; } else { @@ -322,12 +337,16 @@ include 'layout_header.php';
+
+ + +
- Fecha: + Fecha de registro: Hoy Ayer 7 Días @@ -347,13 +366,15 @@ include 'layout_header.php'; $search_query, + 'fecha_entrega' => $selected_delivery_date, ], static function ($value) { return $value !== '' && $value !== null; }); $excelUrl = 'download_ruta_contraentrega.php' . (!empty($excelParams) ? '?' . http_build_query($excelParams) : ''); + $excelLabel = !empty($selected_delivery_date) ? date('d/m/Y', strtotime($selected_delivery_date)) : 'Mañana'; ?> - Descargar Excel (Mañana) + Descargar Excel ()
@@ -640,6 +661,29 @@ include 'layout_header.php'; const errorBox = document.getElementById('ccVerifErrorBox'); const successBox = document.getElementById('ccVerifSuccessBox'); + let activeVerificationButton = null; + + function getVerificationButtonState(tracking, imagePath, seguimiento) { + const hasVerification = !!(tracking && imagePath); + const isMatch = hasVerification && !!seguimiento && seguimiento === tracking; + + return { + className: isMatch ? 'cc-status-match' : (hasVerification ? 'cc-status-pending' : 'cc-status-none'), + icon: isMatch ? '✅' : '📷', + }; + } + + function syncVerificationButton(buttonEl, tracking, imagePath, seguimiento) { + if (!buttonEl) return; + + const state = getVerificationButtonState(tracking, imagePath, seguimiento); + buttonEl.setAttribute('data-verificacion-tracking', tracking || ''); + buttonEl.setAttribute('data-verificacion-imagen', imagePath || ''); + buttonEl.setAttribute('data-seguimiento', seguimiento || ''); + buttonEl.classList.remove('cc-status-none', 'cc-status-pending', 'cc-status-match'); + buttonEl.classList.add(state.className); + buttonEl.textContent = state.icon; + } function showBox(boxEl, msg) { boxEl.textContent = msg; @@ -668,6 +712,7 @@ include 'layout_header.php'; const btn = event.relatedTarget; if (!btn) return; + activeVerificationButton = btn; hideBoxes(); const pedidoId = btn.getAttribute('data-pedido-id') || ''; @@ -755,13 +800,21 @@ include 'layout_header.php'; return; } + const savedTracking = (data.tracking_verificacion || tracking || '').trim(); + const savedImage = (data.imagen || '').trim(); + const seguimientoActual = activeVerificationButton ? (activeVerificationButton.getAttribute('data-seguimiento') || '').trim() : ''; + const targetButton = activeVerificationButton || document.querySelector('.cc-verificacion-btn[data-pedido-id="' + pedidoId + '"]'); + showBox(successBox, 'Verificación guardada ✅'); + trackingSavedEl.textContent = savedTracking || 'N/A'; + seguimientoActualEl.textContent = seguimientoActual || 'N/A'; + copyBtn.disabled = !(savedTracking && savedImage); + syncVerificationButton(targetButton, savedTracking, savedImage, seguimientoActual); + + await new Promise((resolve) => setTimeout(resolve, 250)); const modalInstance = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl); modalInstance.hide(); - - // Reload to update icon/status - window.location.href = window.location.pathname + window.location.search; } catch (e) { console.error(e); showBox(errorBox, 'Error de conexión al guardar.'); diff --git a/save_contraentrega_verificacion.php b/save_contraentrega_verificacion.php index 25f06096..279ee6b6 100644 --- a/save_contraentrega_verificacion.php +++ b/save_contraentrega_verificacion.php @@ -155,13 +155,19 @@ try { $stmt->execute([$relativePath, $tracking_verificacion, $user_id, $pedido_id]); + $response = [ + 'success' => true, + 'pedido_id' => $pedido_id, + 'imagen' => $relativePath, + 'tracking_verificacion' => $tracking_verificacion, + ]; + if ($stmt->rowCount() <= 0) { // Still treat as success because UPDATE might not change values - echo json_encode(['success' => true, 'info' => 'La verificación se guardó, pero no hubo cambios.']); - exit; + $response['info'] = 'La verificación se guardó, pero no hubo cambios.'; } - echo json_encode(['success' => true, 'imagen' => $relativePath, 'tracking_verificacion' => $tracking_verificacion]); + echo json_encode($response); } catch (Throwable $e) { error_log('save_contraentrega_verificacion.php: ' . $e->getMessage()); http_response_code(500);