Autosave: 20260707-083540

This commit is contained in:
Flatlogic Bot 2026-07-07 08:34:42 +00:00
parent 17c381597c
commit c4432f5693
9 changed files with 480 additions and 59 deletions

View File

@ -9,8 +9,8 @@ $provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
$pageTitle = 'Call Center Pro | Bandeja de la asesora';
$pageDescription = 'Replica de la bandeja de Call Center Pro para que cada asesora trabaje desde Call Center Pro con la misma interfaz, estados, historial y acciones sobre sus pedidos asignados.';
$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.';
if (!cc_test_current_user_can_access_module(db())) {
http_response_code(403);
@ -129,6 +129,14 @@ if (!isset($allowedViews[$view])) {
$view = 'pendientes_hoy';
}
$storeKey = trim((string) ($_GET['store'] ?? 'flower'));
$storeKey = mb_strtolower($storeKey);
$availableStores = drive_test_available_stores();
if (!isset($availableStores[$storeKey])) {
$storeKey = 'flower';
}
$storeConfig = $availableStores[$storeKey];
$errorMessage = null;
$noticeMessage = null;
$assessors = [];
@ -136,8 +144,8 @@ $orders = [];
$visibleOrders = [];
$modalsHtml = [];
$totalRows = 0;
$loadLimit = 10;
$startRow = 5552;
$loadLimit = 20;
$startRow = (int) ($storeConfig['startRow'] ?? 5552);
$catalogoProductos = [];
$stats = [
'total' => 0,
@ -162,7 +170,7 @@ try {
$catalogoProductos = [];
}
$preview = drive_test_fetch_orders($loadLimit, $startRow);
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
@ -287,17 +295,27 @@ require_once 'layout_header.php';
<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> Call Center Pro</h1>
<h1 class="h2 fw-bold mb-1"><i class="bi bi-headset text-primary"></i> Pedidos Asignados</h1>
<div class="d-flex flex-wrap gap-2 mt-3">
<a href="gestiones_callcenter.php" class="btn btn-sm <?php echo $view !== 'todos' ? 'btn-primary' : 'btn-outline-primary'; ?>">Bandeja principal</a>
<a href="call_center_pro.php" class="btn btn-sm btn-outline-dark">Call Center Pro</a>
<?php if (in_array((string) ($_SESSION['user_role'] ?? ''), ['Administrador', 'admin'], true)): ?>
<a href="gestiones_callcenter.php?view=<?php echo urlencode($view); ?>&store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-sm btn-outline-primary">Base de Datos Pedidos</a>
<?php endif; ?>
<a href="call_center_pro.php?view=<?php echo urlencode($view); ?>&store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-sm btn-primary">Pedidos Asignados</a>
</div>
<ul class="nav nav-pills mt-3" role="tablist" aria-label="Selector de tienda">
<li class="nav-item">
<a class="nav-link <?php echo $storeKey === 'flower' ? 'active' : ''; ?>" href="?view=<?php echo htmlspecialchars($view); ?>&store=flower">Flower</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo $storeKey === 'otra_tienda' ? 'active' : ''; ?>" href="?view=<?php echo htmlspecialchars($view); ?>&store=otra_tienda">TUANI</a>
</li>
</ul>
</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">Extracción: desde fila <?php echo (int) $startRow; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<a href="test_importar_drive.php" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>
</div>
</section>
@ -315,7 +333,7 @@ require_once 'layout_header.php';
<?php else: ?>
<section class="row g-3 mb-4">
<div class="col-md-6 col-xl-2">
<a href="?view=pendientes_hoy" class="text-decoration-none">
<a href="?view=pendientes_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'pendientes_hoy' ? 'bg-dark text-white' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase opacity-75 mb-2">Pendientes de hoy</div>
@ -325,7 +343,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=nuevos_hoy" class="text-decoration-none">
<a href="?view=nuevos_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'nuevos_hoy' ? 'bg-primary-subtle border border-primary' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Nuevos de hoy</div>
@ -335,7 +353,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=confirmados" class="text-decoration-none">
<a href="?view=confirmados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'confirmados' ? 'bg-success-subtle border border-success' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Confirmados</div>
@ -345,7 +363,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=seguimiento" class="text-decoration-none">
<a href="?view=seguimiento&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'seguimiento' ? 'bg-primary-subtle border border-primary' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Seguimiento</div>
@ -355,7 +373,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=observados" class="text-decoration-none">
<a href="?view=observados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'observados' ? 'bg-warning-subtle border border-warning' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Observados</div>
@ -365,7 +383,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=cerrados" class="text-decoration-none">
<a href="?view=cerrados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'cerrados' ? 'bg-secondary-subtle border border-secondary' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Cerrados</div>
@ -375,7 +393,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=todos" class="text-decoration-none">
<a href="?view=todos&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'todos' ? 'bg-light border' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Todos</div>

View File

@ -9,10 +9,11 @@ $provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
$pageTitle = 'Call Center de Prueba | Bandejas de gestión';
$pageDescription = 'Bandejas diarias de Call Center con estados reales, próxima llamada, fecha de entrega, historial y edición de datos del pedido importado desde Drive.';
$pageTitle = 'Base de Datos Pedidos | Asignación';
$pageDescription = 'Bandejas de asignación y gestión de estados para el Call Center, cargadas desde Drive.';
if (!cc_test_current_user_can_access_module(db())) {
$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>";
@ -128,6 +129,14 @@ if (!isset($allowedViews[$view])) {
$view = 'pendientes_hoy';
}
$storeKey = trim((string) ($_GET['store'] ?? 'flower'));
$storeKey = mb_strtolower($storeKey);
$availableStores = drive_test_available_stores();
if (!isset($availableStores[$storeKey])) {
$storeKey = 'flower';
}
$storeConfig = $availableStores[$storeKey];
$errorMessage = null;
$noticeMessage = null;
$assessors = [];
@ -135,8 +144,13 @@ $orders = [];
$visibleOrders = [];
$modalsHtml = [];
$totalRows = 0;
$loadLimit = 10;
$startRow = 5552;
$loadLimit = 20;
$cupoObjetivoPorAsesora = 10;
$advisorOpenCounts = [];
$advisorTotalCounts = [];
$recommendedAssessorKey = null;
$startRow = (int) ($storeConfig['startRow'] ?? 5552);
$catalogoProductos = [];
$stats = [
'total' => 0,
@ -161,7 +175,7 @@ try {
$catalogoProductos = [];
}
$preview = drive_test_fetch_orders($loadLimit, $startRow);
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
@ -191,6 +205,145 @@ try {
$orders = drive_test_merge_tracking($orders, $tracking);
}
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;
}
$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): 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;
}
return cc_test_order_time($a) <=> cc_test_order_time($b);
});
$orderedAssessorIds = [];
foreach (['KARINA', 'ESTEFANYA'] 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);
}
}
$sourceKeys = array_column($orders, 'source_key');
$callCounts = [];
if (!empty($sourceKeys)) {
@ -205,6 +358,16 @@ try {
$openStates = cc_test_open_states();
$closedStates = cc_test_closed_states();
$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);
@ -216,6 +379,16 @@ try {
&& ($proximaDate === null || $proximaDate <= $todayEnd);
$order['es_cerrado'] = in_array($order['estado'], $closedStates, true);
if ($order['user_id'] !== null) {
$userId = (int) $order['user_id'];
if (isset($advisorTotalCounts[$userId])) {
$advisorTotalCounts[$userId]++;
if (in_array($order['estado'], $openStates, true)) {
$advisorOpenCounts[$userId]++;
}
}
}
$stats['total']++;
if ($order['es_nuevo_hoy']) {
$stats['nuevos_hoy']++;
@ -238,6 +411,26 @@ try {
}
unset($order);
$orderedAssessorKeys = ['KARINA', 'ESTEFANYA'];
$orderedAssessorKeys = array_values(array_filter($orderedAssessorKeys, static function (string $key) use ($assessors): bool {
return isset($assessors[$key]);
}));
if (empty($orderedAssessorKeys)) {
$orderedAssessorKeys = array_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;
}
}
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view): bool {
return match ($view) {
'pendientes_hoy' => (bool) ($order['es_pendiente_hoy'] ?? false),
@ -278,17 +471,25 @@ require_once 'layout_header.php';
<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> Call Center de prueba</h1>
<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-3">
<a href="gestiones_callcenter.php" class="btn btn-sm <?php echo $view !== 'todos' ? 'btn-primary' : 'btn-outline-primary'; ?>">Bandeja principal</a>
<a href="call_center_pro.php" class="btn btn-sm btn-outline-dark">Call Center Pro</a>
<a href="?view=pendientes_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-sm <?php echo $view !== 'todos' ? 'btn-primary' : 'btn-outline-primary'; ?>">Bandeja principal</a>
<a href="call_center_pro.php?view=<?php echo urlencode($view); ?>&store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-sm btn-outline-dark">Pedidos Asignados</a>
</div>
<ul class="nav nav-pills mt-3" role="tablist" aria-label="Selector de tienda">
<li class="nav-item">
<a class="nav-link <?php echo $storeKey === 'flower' ? 'active' : ''; ?>" href="?view=<?php echo htmlspecialchars($view); ?>&store=flower">Flower</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo $storeKey === 'otra_tienda' ? 'active' : ''; ?>" href="?view=<?php echo htmlspecialchars($view); ?>&store=otra_tienda">TUANI</a>
</li>
</ul>
</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">Extracción: desde fila <?php echo (int) $startRow; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<a href="test_importar_drive.php" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>
</div>
</section>
@ -306,7 +507,7 @@ require_once 'layout_header.php';
<?php else: ?>
<section class="row g-3 mb-4">
<div class="col-md-6 col-xl-2">
<a href="?view=pendientes_hoy" class="text-decoration-none">
<a href="?view=pendientes_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'pendientes_hoy' ? 'bg-dark text-white' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase opacity-75 mb-2">Pendientes de hoy</div>
@ -316,7 +517,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=nuevos_hoy" class="text-decoration-none">
<a href="?view=nuevos_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'nuevos_hoy' ? 'bg-primary-subtle border border-primary' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Nuevos de hoy</div>
@ -326,7 +527,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=confirmados" class="text-decoration-none">
<a href="?view=confirmados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'confirmados' ? 'bg-success-subtle border border-success' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Confirmados</div>
@ -336,7 +537,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=seguimiento" class="text-decoration-none">
<a href="?view=seguimiento&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'seguimiento' ? 'bg-primary-subtle border border-primary' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Seguimiento</div>
@ -346,7 +547,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=observados" class="text-decoration-none">
<a href="?view=observados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'observados' ? 'bg-warning-subtle border border-warning' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Observados</div>
@ -356,7 +557,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=cerrados" class="text-decoration-none">
<a href="?view=cerrados&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'cerrados' ? 'bg-secondary-subtle border border-secondary' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Cerrados</div>
@ -366,7 +567,7 @@ require_once 'layout_header.php';
</a>
</div>
<div class="col-md-6 col-xl-2">
<a href="?view=todos" class="text-decoration-none">
<a href="?view=todos&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'todos' ? 'bg-light border' : 'bg-white'; ?>">
<div class="card-body">
<div class="small text-uppercase text-muted mb-2">Todos</div>
@ -377,6 +578,63 @@ require_once 'layout_header.php';
</div>
</section>
<section class="mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<?php
$karinaId = (int) ($assessors['KARINA']['id'] ?? 0);
$estefId = (int) ($assessors['ESTEFANYA']['id'] ?? 0);
$karinaPendientes = $karinaId > 0 ? (int) ($advisorOpenCounts[$karinaId] ?? 0) : 0;
$estefPendientes = $estefId > 0 ? (int) ($advisorOpenCounts[$estefId] ?? 0) : 0;
$karinaCupoRestante = max(0, (int) $cupoObjetivoPorAsesora - $karinaPendientes);
$estefCupoRestante = max(0, (int) $cupoObjetivoPorAsesora - $estefPendientes);
$tieneCupos = ($karinaCupoRestante > 0 || $estefCupoRestante > 0);
?>
<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-activity text-primary"></i> Rendimiento por asesora
</h2>
<div class="text-muted small">Pendientes = <strong>POR LLAMAR</strong> / <strong>DEVOLVER LLAMADA</strong> / <strong>OBSERVADO</strong></div>
</div>
<div class="d-flex flex-wrap gap-2 align-items-stretch justify-content-lg-end">
<div class="border rounded-3 bg-light px-3 py-2" style="min-width: 240px;">
<div class="d-flex justify-content-between align-items-center gap-2">
<div class="fw-bold text-primary">KARINA</div>
<span class="badge bg-primary-subtle text-primary-emphasis border">Pendientes: <?php echo (int) $karinaPendientes; ?></span>
</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) $karinaCupoRestante; ?></div>
</div>
</div>
<div class="border rounded-3 bg-light px-3 py-2" style="min-width: 240px;">
<div class="d-flex justify-content-between align-items-center gap-2">
<div class="fw-bold text-success">ESTEFANYA</div>
<span class="badge bg-success-subtle text-success-emphasis border">Pendientes: <?php echo (int) $estefPendientes; ?></span>
</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) $estefCupoRestante; ?></div>
</div>
</div>
<form method="post" class="d-flex flex-column gap-2" 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>
</div>
</div>
</div>
</section>
<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>
@ -471,10 +729,16 @@ require_once 'layout_header.php';
<input type="hidden" name="action" value="assign_assessor">
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="small text-uppercase text-muted fw-semibold mb-1">Asignar a asesora</div>
<?php
$unassigned = ($order['user_id'] === null);
?>
<select name="target_assessor" class="form-select form-select-sm mb-2">
<option value=""<?php echo empty($order['user_id']) ? ' selected' : ''; ?>>Sin asignar</option>
<?php foreach ($assessors as $assessorKey => $assessor): ?>
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo ((int) ($order['user_id'] ?? 0) === (int) ($assessor['id'] ?? 0)) ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
<option value=""<?php echo ($unassigned && empty($recommendedAssessorKey)) ? ' selected' : ''; ?>>Sin asignar</option>
<?php foreach ($assessors as $assessorKey => $assessor):
$isSelected = ($order['user_id'] !== null && (int) ($order['user_id'] ?? 0) === (int) ($assessor['id'] ?? 0))
|| ($unassigned && $recommendedAssessorKey === $assessorKey);
?>
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo $isSelected ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary w-100">Asignar pedido</button>

View File

@ -10,6 +10,76 @@ function drive_test_normalize_header(string $header): string
return $header;
}
function drive_test_available_stores(): array
{
return [
// Hoja original que ya venía funcionando.
'flower' => [
'label' => 'Flower',
'spreadsheet_id' => '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c',
'sheet_gid' => null,
'startRow' => 5552,
],
// Segunda tienda (nuevo link que pasaste).
'otra_tienda' => [
'label' => 'TUANI',
'spreadsheet_id' => '1QYKeBJIIqYm6yW6Ka-5gKdxhUHwXOAc0No7RjnimxTw',
'sheet_gid' => 1523126328,
'startRow' => 5552,
],
];
}
function drive_test_get_store_config(string $storeKey): array
{
$storeKey = trim(mb_strtolower($storeKey));
$stores = drive_test_available_stores();
if (!isset($stores[$storeKey])) {
return $stores['flower'];
}
return $stores[$storeKey] + ['key' => $storeKey];
}
function drive_test_resolve_sheet_a1_range($service, string $spreadsheetId, ?int $sheetGid, string $rangeA1): string
{
if ($sheetGid === null) {
return $rangeA1;
}
static $cache = [];
$cacheKey = $spreadsheetId . '|' . (string) $sheetGid . '|' . $rangeA1;
if (isset($cache[$cacheKey])) {
return $cache[$cacheKey];
}
try {
$meta = $service->spreadsheets->get($spreadsheetId, ['fields' => 'sheets(properties(sheetId,title))']);
foreach (($meta->getSheets() ?? []) as $sheet) {
$props = $sheet->getProperties();
if (!$props) {
continue;
}
if ((int) $props->getSheetId() === (int) $sheetGid) {
$title = trim((string) ($props->getTitle() ?? ''));
if ($title !== '') {
// Sheet names with spaces/special chars must be quoted.
$escaped = str_replace("'", "''", $title);
$cache[$cacheKey] = "'{$escaped}'!{$rangeA1}";
return $cache[$cacheKey];
}
}
}
} catch (Throwable $exception) {
// Fallback to default range.
}
$cache[$cacheKey] = $rangeA1;
return $rangeA1;
}
function drive_test_get_cell(array $row, array $indexes, array $aliases, string $default = ''): string
{
foreach ($aliases as $alias) {
@ -22,11 +92,18 @@ function drive_test_get_cell(array $row, array $indexes, array $aliases, string
return $default;
}
function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array
function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $storeKey = 'flower'): array
{
$storeConfig = drive_test_get_store_config($storeKey);
$credentialsPath = __DIR__ . '/../google_credentials.json';
$spreadsheetId = '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c';
$range = 'A:Z';
$spreadsheetId = (string) ($storeConfig['spreadsheet_id'] ?? '');
$sheetGid = array_key_exists('sheet_gid', $storeConfig) ? ($storeConfig['sheet_gid'] === null ? null : (int) $storeConfig['sheet_gid']) : null;
$rangeA1 = 'A:Z';
if ($spreadsheetId === '') {
throw new RuntimeException('Configuración de Drive para la tienda no válida.');
}
if (!file_exists($credentialsPath)) {
throw new RuntimeException('No se encontró el archivo de credenciales de Google.');
@ -37,6 +114,9 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array
$client->addScope(Google\Service\Sheets::SPREADSHEETS_READONLY);
$service = new Google\Service\Sheets($client);
$range = drive_test_resolve_sheet_a1_range($service, $spreadsheetId, $sheetGid, $rangeA1);
$response = $service->spreadsheets_values->get($spreadsheetId, $range);
$values = $response->getValues();
@ -59,16 +139,16 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array
}
$dataRows = array_slice($values, 1);
$startIndex = max(0, $startRow - 2);
if ($startIndex > 0) {
if ($startIndex > 0 && $startIndex < count($dataRows)) {
$dataRows = array_slice($dataRows, $startIndex);
}
if ($limit > 0) {
$previewRows = array_reverse(array_slice($dataRows, -$limit));
} else {
$previewRows = array_reverse($dataRows);
}
$previewRows = $limit > 0
? array_reverse(array_slice($dataRows, -$limit))
: array_reverse($dataRows);
$orders = [];
foreach ($previewRows as $row) {
@ -89,7 +169,15 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array
$direccion = drive_test_get_cell($row, $headerIndexes, ['DIRECION', 'DIRECCION']);
$referencia = drive_test_get_cell($row, $headerIndexes, ['REFERENCIA']);
$distrito = drive_test_get_cell($row, $headerIndexes, ['DISTRITO']);
$sourceKey = sha1(implode('|', [$codigo, $importId, $nombre, $celular, $producto]));
// Importante: para 'flower' mantenemos EXACTAMENTE el algoritmo anterior
// para no romper el historial/tracking ya existente.
$sourceKeyBase = implode('|', [$codigo, $importId, $nombre, $celular, $producto]);
if (($storeConfig['key'] ?? 'flower') === 'flower') {
$sourceKey = sha1($sourceKeyBase);
} else {
$sourceKey = sha1(($storeConfig['key'] ?? $storeKey) . '|' . $sourceKeyBase);
}
$orders[] = [
'source_key' => $sourceKey,

View File

@ -104,7 +104,7 @@ $pageDescription = 'Entrada rápida al sistema CRM y panel de administración.';
<a class="btn btn-primary" href="<?php echo $isLoggedIn ? 'dashboard.php' : 'login.php'; ?>">
<?php echo $isLoggedIn ? 'Ir al dashboard' : 'Iniciar sesión'; ?>
</a>
<a class="btn btn-secondary" href="gestiones_callcenter.php">Abrir Call Center de prueba</a>
<a class="btn btn-secondary" href="gestiones_callcenter.php">Abrir Base de Datos Pedidos</a>
</div>
<div class="meta">

View File

@ -348,13 +348,13 @@ $navItems = [
'call_center' => [
'url' => 'gestiones_callcenter.php',
'icon' => 'fa-headset',
'text' => 'Call Center (Mis Gestiones)',
'roles' => ['Administrador', 'admin', 'Asesor']
'text' => 'Base de Datos Pedidos',
'roles' => ['Administrador', 'admin']
],
'call_center_pro' => [
'url' => 'call_center_pro.php',
'icon' => 'fa-headset',
'text' => 'Call Center Pro',
'text' => 'Pedidos Asignados',
'roles' => ['Administrador', 'admin', 'Asesor']
],
'simular_asignacion' => [
@ -447,7 +447,7 @@ $navItems = [
<a href="#" class="nav-link"><i class="fas <?php echo $item['icon']; ?>"></i> <?php echo $item['text']; ?></a>
<ul class="submenu <?php echo $isSubmenuActive ? 'show' : ''; ?>">
<?php foreach ($item['submenu'] as $sub_item):
if ($isTestModule || in_array($userRole, $sub_item['roles'])):
if (in_array($userRole, $sub_item['roles'])):
$isActive = $currentPage == $sub_item['url'] ? 'active' : '';
?>
<li class="nav-item">

View File

@ -28,6 +28,23 @@ try {
$pdo = db();
cc_test_ensure_historial_llamadas_table($pdo);
$role = (string) ($_SESSION['user_role'] ?? '');
$isAdmin = in_array($role, ['Administrador', 'admin'], true);
if (!$isAdmin) {
cc_test_ensure_tracking_table($pdo);
$stmtTrack = $pdo->prepare('SELECT user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1');
$stmtTrack->execute([$pedido_id]);
$tracking = $stmtTrack->fetch(PDO::FETCH_ASSOC);
$trackingUserId = $tracking ? (int) ($tracking['user_id'] ?? 0) : null;
if (!$tracking || $trackingUserId !== $asesor_id) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'No autorizado para gestionar este pedido.']);
exit;
}
}
$stmt = $pdo->prepare('INSERT INTO historial_llamadas (pedido_id, asesor_id, resultado, observacion) VALUES (?, ?, ?, ?)');
$stmt->execute([$pedido_id, $asesor_id, $resultado, $observacion !== '' ? $observacion : null]);

View File

@ -39,4 +39,4 @@ foreach ($pedidos as $id) {
echo "<h3>Simulación Exitosa</h3>";
echo "Se han asignado " . count($pedidos) . " pedidos al usuario <strong>$username</strong> (ID: $userId).<br>";
echo "Ahora puedes verlos en el panel de gestión: <a href='gestiones_callcenter.php'>Ir al Panel de Call Center</a>";
echo "Ahora puedes verlos en el panel de gestión: <a href='gestiones_callcenter.php'>Ir a Base de Datos Pedidos</a>";

View File

@ -2,6 +2,7 @@
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/callcenter_test_helpers.php';
require_once __DIR__ . '/includes/drive_test_orders.php';
$pageTitle = 'Importar Drive (Test)';
$pageDescription = 'Vista previa de los últimos 10 pedidos detectados en Google Sheets antes de importarlos al CRM.';
@ -18,8 +19,20 @@ require_once __DIR__ . '/layout_header.php';
require_once __DIR__ . '/vendor/autoload.php';
$credentialsPath = __DIR__ . '/google_credentials.json';
$spreadsheetId = '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c';
$range = 'A:Z';
$storeKey = trim((string) ($_GET['store'] ?? 'flower'));
$storeKey = mb_strtolower($storeKey);
$availableStores = drive_test_available_stores();
if (!isset($availableStores[$storeKey])) {
$storeKey = 'flower';
}
$storeConfig = $availableStores[$storeKey];
$spreadsheetId = (string) ($storeConfig['spreadsheet_id'] ?? '');
$rangeA1 = 'A:Z';
$sheetGid = array_key_exists('sheet_gid', $storeConfig) ? ($storeConfig['sheet_gid'] === null ? null : (int) $storeConfig['sheet_gid']) : null;
$range = $rangeA1;
$headers = [];
$previewRows = [];
$errorMessage = null;
@ -35,6 +48,8 @@ try {
$client->addScope(Google\Service\Sheets::SPREADSHEETS_READONLY);
$service = new Google\Service\Sheets($client);
$range = drive_test_resolve_sheet_a1_range($service, $spreadsheetId, $sheetGid, $rangeA1);
$response = $service->spreadsheets_values->get($spreadsheetId, $range);
$values = $response->getValues();
@ -61,9 +76,12 @@ try {
<h2 class="h4 mb-1">Vista previa de pedidos desde Drive</h2>
<p class="text-muted mb-0">Aquí verás solo los últimos 10 registros encontrados en la hoja, sin guardar nada todavía en la base de datos.</p>
</div>
<span class="badge bg-primary-subtle text-primary-emphasis px-3 py-2">
<?php echo (int) count($previewRows); ?> visibles
</span>
<div class="d-flex flex-wrap gap-2 justify-content-end">
<span class="badge bg-light text-dark border px-3 py-2">Tienda: <?php echo htmlspecialchars((string) ($storeConfig['label'] ?? '')); ?></span>
<span class="badge bg-primary-subtle text-primary-emphasis px-3 py-2">
<?php echo (int) count($previewRows); ?> visibles
</span>
</div>
</div>
<?php if ($errorMessage !== null): ?>

View File

@ -71,10 +71,26 @@ try {
$pdo = db();
cc_test_ensure_tracking_table($pdo);
$stmtCurrent = $pdo->prepare('SELECT estado, numero_cuenta_enviado_at FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1');
$stmtCurrent = $pdo->prepare('SELECT estado, numero_cuenta_enviado_at, user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1');
$stmtCurrent->execute([$sourceKey]);
$currentTracking = $stmtCurrent->fetch(PDO::FETCH_ASSOC) ?: null;
$role = (string) ($_SESSION['user_role'] ?? '');
$isAdmin = in_array($role, ['Administrador', 'admin'], true);
if (!$isAdmin) {
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
$trackingUserId = $currentTracking ? (int) ($currentTracking['user_id'] ?? 0) : null;
if (!$currentTracking || $trackingUserId !== $currentUserId) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'No autorizado para gestionar este pedido.']);
exit;
}
}
$userIdToSet = $isAdmin
? ($currentTracking ? ($currentTracking['user_id'] ?? null) : null)
: (int) $_SESSION['user_id'];
$proximaRaw = trim((string) ($_POST['proxima_llamada_at'] ?? ''));
$proximaLlamada = null;
if ($proximaRaw !== '') {
@ -204,7 +220,7 @@ try {
':source_key' => $sourceKey,
':estado' => $estado,
':nota' => $nota,
':user_id' => (int) $_SESSION['user_id'],
':user_id' => $userIdToSet,
':direccion' => $direccion,
':referencia' => $referencia,
':agencia' => $agencia,