Autosave: 20260525-031836
This commit is contained in:
parent
00437625c8
commit
623d4cd358
12
db/migrations/074_create_historial_llamadas_table.sql
Normal file
12
db/migrations/074_create_historial_llamadas_table.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- Create call history table for the Call Center test panel.
|
||||
CREATE TABLE IF NOT EXISTS `historial_llamadas` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`pedido_id` VARCHAR(80) NOT NULL,
|
||||
`asesor_id` INT UNSIGNED NOT NULL,
|
||||
`resultado` VARCHAR(120) NOT NULL,
|
||||
`observacion` TEXT NULL,
|
||||
`fecha_llamada` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_historial_llamadas_pedido` (`pedido_id`),
|
||||
INDEX `idx_historial_llamadas_asesor` (`asesor_id`),
|
||||
INDEX `idx_historial_llamadas_fecha` (`fecha_llamada`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
10
db/migrations/075_expand_callcenter_test_tracking.sql
Normal file
10
db/migrations/075_expand_callcenter_test_tracking.sql
Normal file
@ -0,0 +1,10 @@
|
||||
ALTER TABLE `callcenter_test_tracking`
|
||||
ADD COLUMN IF NOT EXISTS `direccion` TEXT NULL AFTER `user_id`,
|
||||
ADD COLUMN IF NOT EXISTS `referencia` TEXT NULL AFTER `direccion`,
|
||||
ADD COLUMN IF NOT EXISTS `sede` VARCHAR(120) NULL AFTER `referencia`,
|
||||
ADD COLUMN IF NOT EXISTS `ciudad` VARCHAR(120) NULL AFTER `sede`,
|
||||
ADD COLUMN IF NOT EXISTS `distrito` VARCHAR(120) NULL AFTER `ciudad`,
|
||||
ADD COLUMN IF NOT EXISTS `dni` VARCHAR(40) NULL AFTER `distrito`,
|
||||
ADD COLUMN IF NOT EXISTS `observaciones` TEXT NULL AFTER `dni`,
|
||||
ADD COLUMN IF NOT EXISTS `proxima_llamada_at` DATETIME NULL AFTER `observaciones`,
|
||||
ADD COLUMN IF NOT EXISTS `ultima_gestion_at` DATETIME NULL AFTER `proxima_llamada_at`;
|
||||
@ -1,89 +1,48 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/callcenter_test_helpers.php';
|
||||
require_once 'includes/drive_test_orders.php';
|
||||
|
||||
$pageTitle = 'Call Center de Prueba | Importar Drive (Test)';
|
||||
$pageDescription = 'Panel de seguimiento para confirmar pedidos usando únicamente los últimos 10 registros de Importar Drive (Test).';
|
||||
$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.';
|
||||
|
||||
$view = $_GET['view'] ?? 'pendientes';
|
||||
$allowedViews = [
|
||||
'pendientes' => ['estado' => 'POR LLAMAR', 'titulo' => 'Pedidos por llamar'],
|
||||
'reintentos' => ['estado' => 'REINTENTO', 'titulo' => 'Pedidos en reintento'],
|
||||
'confirmados' => ['estado' => 'CONFIRMADO', 'titulo' => 'Pedidos confirmados'],
|
||||
'todos' => ['estado' => null, 'titulo' => 'Todos los pedidos de prueba'],
|
||||
];
|
||||
|
||||
if (!isset($allowedViews[$view])) {
|
||||
$view = 'pendientes';
|
||||
}
|
||||
|
||||
$errorMessage = null;
|
||||
$orders = [];
|
||||
$stats = [
|
||||
'total' => 0,
|
||||
'pendientes' => 0,
|
||||
'reintentos' => 0,
|
||||
'confirmados' => 0,
|
||||
];
|
||||
$visibleOrders = [];
|
||||
$totalRows = 0;
|
||||
|
||||
try {
|
||||
$preview = drive_test_fetch_orders(10);
|
||||
$totalRows = (int) ($preview['total_rows'] ?? 0);
|
||||
$orders = $preview['orders'] ?? [];
|
||||
|
||||
$tracking = drive_test_fetch_tracking(db(), array_column($orders, 'source_key'));
|
||||
$orders = drive_test_merge_tracking($orders, $tracking);
|
||||
|
||||
// Obtener conteo de llamadas
|
||||
$sourceKeys = array_column($orders, 'source_key');
|
||||
$callCounts = [];
|
||||
if (!empty($sourceKeys)) {
|
||||
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
|
||||
$stmtCalls = db()->prepare("SELECT pedido_id, COUNT(*) as total FROM historial_llamadas WHERE pedido_id IN ($placeholders) GROUP BY pedido_id");
|
||||
$stmtCalls->execute($sourceKeys);
|
||||
$callCounts = $stmtCalls->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
}
|
||||
|
||||
foreach ($orders as &$order) {
|
||||
$order['total_llamadas'] = $callCounts[$order['source_key']] ?? 0;
|
||||
$stats['total']++;
|
||||
if ($order['estado'] === 'POR LLAMAR') {
|
||||
$stats['pendientes']++;
|
||||
} elseif ($order['estado'] === 'REINTENTO') {
|
||||
$stats['reintentos']++;
|
||||
} elseif ($order['estado'] === 'CONFIRMADO') {
|
||||
$stats['confirmados']++;
|
||||
}
|
||||
}
|
||||
|
||||
$expectedState = $allowedViews[$view]['estado'];
|
||||
if ($expectedState === null) {
|
||||
$visibleOrders = $orders;
|
||||
} else {
|
||||
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($expectedState): bool {
|
||||
return ($order['estado'] ?? '') === $expectedState;
|
||||
}));
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$errorMessage = $exception->getMessage();
|
||||
}
|
||||
|
||||
function cc_test_badge_class(string $estado): string
|
||||
{
|
||||
return match ($estado) {
|
||||
'CONFIRMADO' => 'bg-success-subtle text-success-emphasis',
|
||||
'REINTENTO' => 'bg-info-subtle text-info-emphasis',
|
||||
default => 'bg-warning-subtle text-warning-emphasis',
|
||||
};
|
||||
}
|
||||
|
||||
function cc_test_display_value(?string $value, string $fallback = 'No registrado'): string
|
||||
function cc_test_parse_datetime(?string $value): ?DateTimeImmutable
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
return $value !== '' ? $value : $fallback;
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new DateTimeImmutable($value);
|
||||
} catch (Throwable $exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_format_datetime(?string $value, string $fallback = 'Sin programar'): string
|
||||
{
|
||||
$date = cc_test_parse_datetime($value);
|
||||
return $date ? $date->format('d/m/Y h:i A') : $fallback;
|
||||
}
|
||||
|
||||
function cc_test_format_datetime_input(?string $value): string
|
||||
{
|
||||
$date = cc_test_parse_datetime($value);
|
||||
return $date ? $date->format('Y-m-d\TH:i') : '';
|
||||
}
|
||||
|
||||
function cc_test_format_date(?string $value, string $fallback = 'Sin fecha'): string
|
||||
{
|
||||
$date = cc_test_parse_datetime($value);
|
||||
return $date ? $date->format('d/m/Y') : $fallback;
|
||||
}
|
||||
|
||||
function cc_test_format_date_input(?string $value): string
|
||||
{
|
||||
$date = cc_test_parse_datetime($value);
|
||||
return $date ? $date->format('Y-m-d') : '';
|
||||
}
|
||||
|
||||
function cc_test_format_price(?string $value): string
|
||||
@ -92,18 +51,154 @@ function cc_test_format_price(?string $value): string
|
||||
return $value !== '' ? 'S/ ' . $value : 'No registrado';
|
||||
}
|
||||
|
||||
function cc_test_format_drive_datetime(?string $value): string
|
||||
function cc_test_display_value(?string $value, string $fallback = 'No registrado'): string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return 'No registrada';
|
||||
return $value !== '' ? $value : $fallback;
|
||||
}
|
||||
|
||||
function cc_test_order_label(array $order): string
|
||||
{
|
||||
$codigo = trim((string) ($order['codigo'] ?? ''));
|
||||
if ($codigo !== '') {
|
||||
return '#' . ltrim($codigo, '#');
|
||||
}
|
||||
|
||||
try {
|
||||
return (new DateTimeImmutable($value))->format('d/m/Y h:i A');
|
||||
} catch (Throwable $exception) {
|
||||
return $value;
|
||||
return 'Sin número';
|
||||
}
|
||||
|
||||
function cc_test_badge_class(string $estado): string
|
||||
{
|
||||
return match ($estado) {
|
||||
'CONFIRMADO FECHA', 'CONTRAENTREGA CONFIRMADO' => 'bg-success-subtle text-success-emphasis',
|
||||
'DEVOLVER LLAMADA' => 'bg-info-subtle text-info-emphasis',
|
||||
'OBSERVADO' => 'bg-warning-subtle text-warning-emphasis',
|
||||
'CANCELADO' => 'bg-danger-subtle text-danger-emphasis',
|
||||
'ENVIO REPETIDO' => 'bg-secondary-subtle text-secondary-emphasis',
|
||||
'SE ENVIO NUMERO DE CUENTA' => 'bg-primary-subtle text-primary-emphasis',
|
||||
default => 'bg-dark-subtle text-dark-emphasis',
|
||||
};
|
||||
}
|
||||
|
||||
function cc_test_order_time(array $order): int
|
||||
{
|
||||
foreach (['proxima_llamada_at', 'ultima_gestion_at', 'seguimiento_actualizado', 'import_id'] as $field) {
|
||||
$date = cc_test_parse_datetime($order[$field] ?? null);
|
||||
if ($date) {
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$view = $_GET['view'] ?? 'pendientes_hoy';
|
||||
$allowedViews = [
|
||||
'pendientes_hoy' => 'Pendientes de hoy',
|
||||
'nuevos_hoy' => 'Nuevos de hoy',
|
||||
'confirmados' => 'Confirmados',
|
||||
'cerrados' => 'Cerrados / descartados',
|
||||
'todos' => 'Todos los pedidos cargados',
|
||||
];
|
||||
if (!isset($allowedViews[$view])) {
|
||||
$view = 'pendientes_hoy';
|
||||
}
|
||||
|
||||
$errorMessage = null;
|
||||
$orders = [];
|
||||
$visibleOrders = [];
|
||||
$modalsHtml = [];
|
||||
$totalRows = 0;
|
||||
$loadLimit = 10;
|
||||
$stats = [
|
||||
'total' => 0,
|
||||
'pendientes_hoy' => 0,
|
||||
'nuevos_hoy' => 0,
|
||||
'confirmados' => 0,
|
||||
'cerrados' => 0,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
cc_test_ensure_tracking_table($pdo);
|
||||
cc_test_ensure_historial_llamadas_table($pdo);
|
||||
|
||||
$preview = drive_test_fetch_orders($loadLimit);
|
||||
$totalRows = (int) ($preview['total_rows'] ?? 0);
|
||||
$orders = $preview['orders'] ?? [];
|
||||
|
||||
$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)) {
|
||||
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
|
||||
$stmtCalls = $pdo->prepare("SELECT pedido_id, COUNT(*) as total FROM historial_llamadas WHERE pedido_id IN ($placeholders) GROUP BY pedido_id");
|
||||
$stmtCalls->execute($sourceKeys);
|
||||
$callCounts = $stmtCalls->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
}
|
||||
|
||||
$todayStart = new DateTimeImmutable('today');
|
||||
$todayEnd = $todayStart->setTime(23, 59, 59);
|
||||
$openStates = cc_test_open_states();
|
||||
$closedStates = cc_test_closed_states();
|
||||
|
||||
foreach ($orders as &$order) {
|
||||
$order['total_llamadas'] = (int) ($callCounts[$order['source_key']] ?? 0);
|
||||
$importDate = cc_test_parse_datetime($order['import_id'] ?? null);
|
||||
$proximaDate = cc_test_parse_datetime($order['proxima_llamada_at'] ?? null);
|
||||
|
||||
$order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false;
|
||||
$order['es_pendiente_hoy'] = in_array($order['estado'], $openStates, true)
|
||||
&& ($proximaDate === null || $proximaDate <= $todayEnd);
|
||||
$order['es_cerrado'] = in_array($order['estado'], $closedStates, true);
|
||||
|
||||
$stats['total']++;
|
||||
if ($order['es_nuevo_hoy']) {
|
||||
$stats['nuevos_hoy']++;
|
||||
}
|
||||
if ($order['es_pendiente_hoy']) {
|
||||
$stats['pendientes_hoy']++;
|
||||
}
|
||||
if (in_array($order['estado'], cc_test_confirmed_states(), true)) {
|
||||
$stats['confirmados']++;
|
||||
}
|
||||
if ($order['es_cerrado']) {
|
||||
$stats['cerrados']++;
|
||||
}
|
||||
}
|
||||
unset($order);
|
||||
|
||||
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view): bool {
|
||||
return match ($view) {
|
||||
'pendientes_hoy' => (bool) ($order['es_pendiente_hoy'] ?? false),
|
||||
'nuevos_hoy' => (bool) ($order['es_nuevo_hoy'] ?? false),
|
||||
'confirmados' => in_array(($order['estado'] ?? ''), cc_test_confirmed_states(), true),
|
||||
'cerrados' => (bool) ($order['es_cerrado'] ?? false),
|
||||
default => true,
|
||||
};
|
||||
}));
|
||||
|
||||
usort($visibleOrders, static function (array $a, array $b) use ($view): int {
|
||||
if ($view === 'pendientes_hoy') {
|
||||
$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($b) <=> cc_test_order_time($a);
|
||||
});
|
||||
} catch (Throwable $exception) {
|
||||
$errorMessage = $exception->getMessage();
|
||||
}
|
||||
|
||||
require_once 'layout_header.php';
|
||||
@ -111,57 +206,60 @@ require_once 'layout_header.php';
|
||||
|
||||
<main class="container-fluid py-4">
|
||||
<section class="mb-4">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
|
||||
<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>
|
||||
<p class="text-muted mb-0">Este panel usa solo los últimos 10 registros de <strong>Importar Drive (Test)</strong> para confirmar pedidos sin tocar pedidos reales.</p>
|
||||
<p class="text-muted mb-0">Ahora el panel trabaja por <strong>bandejas</strong>: <strong>Nuevos de hoy</strong>, <strong>Pendientes de hoy</strong>, <strong>Confirmados</strong> y <strong>Cerrados</strong>. Así no se mezcla todo cuando entran pedidos diarios.</p>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">Fuente: Importar Drive (Test)</span>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">Hoja detectada: <?php echo (int) $totalRows; ?> registros</span>
|
||||
<a href="test_importar_drive.php" class="btn btn-outline-primary btn-sm">Ver vista previa</a>
|
||||
<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">Carga operativa: últimos <?php echo (int) $loadLimit; ?> registros</span>
|
||||
<a href="test_importar_drive.php" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($errorMessage !== null): ?>
|
||||
<section class="alert alert-danger" role="alert">
|
||||
<strong>No se pudo cargar el panel de Call Center de prueba.</strong><br>
|
||||
<strong>No se pudo cargar el panel.</strong><br>
|
||||
<?php echo htmlspecialchars($errorMessage); ?>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="alert alert-primary border-0 shadow-sm mb-4">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<h2 class="h5 fw-bold mb-2">Flujo recomendado ya listo para prueba</h2>
|
||||
<p class="mb-0">Los pedidos nuevos entran desde Drive. Luego tú gestionas cada cliente con estado, próxima llamada y observaciones internas. Los casos abiertos siguen apareciendo en <strong>Pendientes de hoy</strong> hasta que los cierres o confirmes.</p>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="small text-muted">Campos editables del módulo</div>
|
||||
<div class="fw-semibold">Dirección, referencia, sede / ID, ciudad, distrito, DNI y observaciones</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<a href="?view=todos" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'todos' ? 'bg-dark text-white' : 'bg-white'; ?>">
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=pendientes_hoy" 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">Total visibles</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['total']; ?></div>
|
||||
<div class="small text-uppercase opacity-75 mb-2">Pendientes de hoy</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['pendientes_hoy']; ?></div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<a href="?view=pendientes" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'pendientes' ? 'bg-warning-subtle border border-warning' : 'bg-white'; ?>">
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=nuevos_hoy" 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">Por llamar</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['pendientes']; ?></div>
|
||||
<div class="small text-uppercase text-muted mb-2">Nuevos de hoy</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['nuevos_hoy']; ?></div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<a href="?view=reintentos" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'reintentos' ? 'bg-info-subtle border border-info' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Reintento</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['reintentos']; ?></div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=confirmados" 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">
|
||||
@ -171,245 +269,271 @@ require_once 'layout_header.php';
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-2">
|
||||
<a href="?view=cerrados" 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>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['cerrados']; ?></div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<a href="?view=todos" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'todos' ? 'bg-warning-subtle border border-warning' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase text-muted mb-2">Todos los pedidos cargados</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['total']; ?></div>
|
||||
<div class="small text-muted mt-2">Usa esta vista solo para revisión general.</div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</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>
|
||||
<h2 class="h5 fw-bold mb-1"><?php echo htmlspecialchars($allowedViews[$view]['titulo']); ?></h2>
|
||||
<p class="text-muted small mb-0">Datos tomados solo desde <strong>Importar Drive (Test)</strong>: nombre, celular, dirección, referencia, distrito, ciudad, producto, cantidad, precio, método, sede / ID, DNI y observaciones.</p>
|
||||
<h2 class="h5 fw-bold mb-1"><?php echo htmlspecialchars($allowedViews[$view]); ?></h2>
|
||||
<p class="text-muted small mb-0">Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado fecha 📅, Contraentrega confirmado, Cancelado y Envío repetido.</p>
|
||||
</div>
|
||||
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos</span>
|
||||
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Cliente y contacto</th>
|
||||
<th>Ubicación</th>
|
||||
<th>N° Pedido</th>
|
||||
<th>Cliente</th>
|
||||
<th>Ubicación editable</th>
|
||||
<th>Pedido</th>
|
||||
<th>Seguimiento</th>
|
||||
<th>Gestión</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($visibleOrders)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
|
||||
No hay pedidos en esta vista del módulo de prueba.
|
||||
No hay pedidos en esta bandeja por ahora.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($visibleOrders as $order): ?>
|
||||
<?php
|
||||
$modalId = 'modalDriveTest' . substr($order['source_key'], 0, 10);
|
||||
$modalId = 'modalDriveTest' . $order['source_key'];
|
||||
$badgeClass = cc_test_badge_class($order['estado']);
|
||||
$precioLabel = cc_test_format_price($order['precio'] ?? '');
|
||||
$driveDateLabel = cc_test_format_drive_datetime($order['import_id'] ?? '');
|
||||
$historial = cc_test_fetch_historial(db(), $order['source_key']);
|
||||
ob_start();
|
||||
?>
|
||||
<tr>
|
||||
<tr data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<td>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'])); ?></div>
|
||||
<div class="small text-muted">Pedido: <?php echo htmlspecialchars(cc_test_display_value($order['codigo'], 'Sin código')); ?></div>
|
||||
<div class="small text-muted">Drive: <?php echo htmlspecialchars($driveDateLabel); ?></div>
|
||||
<div class="small text-muted">DNI: <?php echo htmlspecialchars(cc_test_display_value($order['dni'])); ?></div>
|
||||
<?php if (!empty($order['telefono_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($order['telefono_url']); ?>" class="text-decoration-none small">
|
||||
<i class="bi bi-telephone-fill"></i> <?php echo htmlspecialchars(cc_test_display_value($order['celular'])); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<div class="small text-muted">Celular: No registrado</div>
|
||||
<?php endif; ?>
|
||||
<div class="fw-bold fs-6"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
|
||||
<div class="small text-muted">ID Drive: <?php echo htmlspecialchars(cc_test_display_value($order['import_id'] ?? null, 'Sin ID')); ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-primary"><?php echo htmlspecialchars(cc_test_display_value($order['distrito'])); ?></div>
|
||||
<div class="small"><?php echo htmlspecialchars(cc_test_display_value($order['direccion'])); ?></div>
|
||||
<div class="small text-muted">Ref.: <?php echo htmlspecialchars(cc_test_display_value($order['referencia'])); ?></div>
|
||||
<div class="small text-muted"><?php echo htmlspecialchars(cc_test_display_value($order['ciudad'])); ?> · <?php echo htmlspecialchars(cc_test_display_value($order['pais'])); ?></div>
|
||||
<?php if (!empty($order['coordenadas'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($order['coordenadas']); ?>" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-link px-0 text-decoration-none">Ver ubicación</a>
|
||||
<?php endif; ?>
|
||||
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Sin nombre')); ?></div>
|
||||
<div class="small text-muted mb-1">Celular: <?php echo htmlspecialchars(cc_test_display_value($order['celular'], 'Sin número')); ?></div>
|
||||
<div class="small text-muted">DNI: <?php echo htmlspecialchars(cc_test_display_value($order['dni'], 'Sin DNI')); ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small fw-semibold"><?php echo nl2br(htmlspecialchars(cc_test_display_value($order['producto']))); ?></div>
|
||||
<div class="small text-muted">Cantidad: <?php echo htmlspecialchars(cc_test_display_value($order['cantidad'], '1')); ?> · Precio: <span class="text-success fw-bold"><?php echo htmlspecialchars($precioLabel); ?></span></div>
|
||||
<div class="small text-muted">Método: <?php echo htmlspecialchars(cc_test_display_value($order['metodo'])); ?></div>
|
||||
<div class="small text-muted">Sede / ID: <?php echo htmlspecialchars(cc_test_display_value($order['sede'])); ?></div>
|
||||
<div class="small"><strong>Dirección:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['direccion'])); ?></div>
|
||||
<div class="small"><strong>Referencia:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['referencia'])); ?></div>
|
||||
<div class="small"><strong>Ciudad / Distrito:</strong> <?php echo htmlspecialchars(cc_test_display_value(trim(($order['ciudad'] ?? '') . ' / ' . ($order['distrito'] ?? '')), 'No registrado')); ?></div>
|
||||
<div class="small"><strong>Sede / ID:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['sede'])); ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?php echo $badgeClass; ?> mb-2"><?php echo htmlspecialchars($order['estado']); ?></span>
|
||||
<?php if ($order['total_llamadas'] > 0): ?>
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-secondary" title="Intentos de llamada">
|
||||
<i class="bi bi-telephone-outbound"></i> <?php echo (int) $order['total_llamadas']; ?> llamadas
|
||||
</span>
|
||||
</div>
|
||||
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_display_value($order['producto'], 'Sin producto')); ?></div>
|
||||
<div class="small text-muted">Cantidad: <?php echo htmlspecialchars(cc_test_display_value($order['cantidad'])); ?> · <?php echo htmlspecialchars(cc_test_format_price($order['precio'])); ?></div>
|
||||
<div class="small text-muted">Ingreso Drive: <?php echo htmlspecialchars(cc_test_format_datetime($order['import_id'] ?? null, 'Sin fecha en Drive')); ?></div>
|
||||
<div class="small text-muted">Observación Drive: <?php echo htmlspecialchars(cc_test_display_value($order['observaciones'], 'Sin observaciones')); ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
<span class="badge rounded-pill <?php echo htmlspecialchars($badgeClass); ?>"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></span>
|
||||
<span class="badge rounded-pill bg-light text-dark border"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span> llamadas</span>
|
||||
</div>
|
||||
<div class="small text-muted">Próxima llamada: <?php echo htmlspecialchars(cc_test_format_datetime($order['proxima_llamada_at'] ?? null)); ?></div>
|
||||
<?php if (!empty($order['fecha_entrega_programada'])): ?>
|
||||
<div class="small text-muted">Entrega programada 📅: <?php echo htmlspecialchars(cc_test_format_date($order['fecha_entrega_programada'] ?? null)); ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="small text-muted">Obs. Drive: <?php echo htmlspecialchars(cc_test_display_value($order['observaciones'], 'Sin observaciones')); ?></div>
|
||||
<div class="small text-muted">Nota interna: <?php echo htmlspecialchars(cc_test_display_value($order['nota_seguimiento'], 'Sin nota')); ?></div>
|
||||
<div class="small text-muted">Última gestión: <?php echo htmlspecialchars(cc_test_format_datetime($order['ultima_gestion_at'] ?? ($order['seguimiento_actualizado'] ?? null), 'Aún no gestionado')); ?></div>
|
||||
<div class="small text-muted mt-1">Nota: <?php echo htmlspecialchars(cc_test_display_value($order['nota_seguimiento'], 'Sin nota interna')); ?></div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-center gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#<?php echo htmlspecialchars($modalId); ?>">
|
||||
<i class="bi bi-eye"></i> Ver
|
||||
</button>
|
||||
<div class="d-flex flex-column gap-2 align-items-stretch">
|
||||
<?php if (!empty($order['telefono_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($order['telefono_url']); ?>"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
onclick="registrarLlamada('<?php echo htmlspecialchars($order['source_key']); ?>')">
|
||||
<i class="bi bi-telephone"></i> Llamar
|
||||
<a href="<?php echo htmlspecialchars($order['telefono_url']); ?>" class="btn btn-sm btn-primary" onclick="return registrarLlamada(event, '<?php echo htmlspecialchars($order['source_key']); ?>', '<?php echo htmlspecialchars($order['telefono_url']); ?>', this)">
|
||||
<i class="bi bi-telephone-outbound"></i> Llamar
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-primary" disabled>Sin teléfono</button>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($order['whatsapp_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($order['whatsapp_url']); ?>" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-success">
|
||||
<a href="<?php echo htmlspecialchars($order['whatsapp_url']); ?>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success">
|
||||
<i class="bi bi-whatsapp"></i> WhatsApp
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal" data-bs-target="#<?php echo htmlspecialchars($modalId); ?>">
|
||||
<i class="bi bi-sliders"></i> Gestionar
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $rowHtml = ob_get_clean(); echo $rowHtml; ?>
|
||||
|
||||
<?php ob_start(); ?>
|
||||
<div class="modal fade" id="<?php echo htmlspecialchars($modalId); ?>" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h3 class="h5 mb-1"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'])); ?></h3>
|
||||
<div class="small text-muted">Pedido de prueba cargado desde Importar Drive (Test)</div>
|
||||
<div class="small text-primary fw-semibold mb-1"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
|
||||
<h3 class="modal-title h5 mb-1"><?php echo htmlspecialchars(cc_test_display_value($order['nombre'], 'Cliente sin nombre')); ?></h3>
|
||||
<div class="small text-muted"><?php echo htmlspecialchars(cc_test_display_value($order['producto'], 'Sin producto')); ?> · <?php echo htmlspecialchars(cc_test_display_value($order['celular'], 'Sin celular')); ?></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small d-block">Código</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['codigo'], 'Sin código')); ?></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small d-block">Fecha detectada en Drive</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars($driveDateLabel); ?></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small d-block">Estado de seguimiento</label>
|
||||
<div><span class="badge <?php echo $badgeClass; ?>"><?php echo htmlspecialchars($order['estado']); ?></span></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small d-block">Celular</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['celular'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small d-block">DNI</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['dni'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="text-muted small d-block">Dirección</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['direccion'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small d-block">Referencia</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['referencia'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="text-muted small d-block">Distrito</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['distrito'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="text-muted small d-block">Ciudad</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['ciudad'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small d-block">País</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['pais'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small d-block">Método</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['metodo'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small d-block">Sede / ID</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['sede'])); ?></div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="text-muted small d-block">Producto</label>
|
||||
<div class="fw-bold"><?php echo nl2br(htmlspecialchars(cc_test_display_value($order['producto']))); ?></div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="text-muted small d-block">Cantidad</label>
|
||||
<div class="fw-bold"><?php echo htmlspecialchars(cc_test_display_value($order['cantidad'], '1')); ?></div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="text-muted small d-block">Precio</label>
|
||||
<div class="fw-bold text-success"><?php echo htmlspecialchars($precioLabel); ?></div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="text-muted small d-block">Observaciones del Drive</label>
|
||||
<div class="p-3 bg-light rounded small"><?php echo nl2br(htmlspecialchars(cc_test_display_value($order['observaciones'], 'Sin observaciones'))); ?></div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="text-muted small d-block">Coordenadas</label>
|
||||
<?php if (!empty($order['coordenadas'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($order['coordenadas']); ?>" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-secondary">Abrir ubicación</a>
|
||||
<?php else: ?>
|
||||
<div class="fw-bold">No registradas</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<h4 class="h6 fw-bold"><i class="bi bi-clock-history"></i> Historial de llamadas</h4>
|
||||
<?php
|
||||
$stmtHist = db()->prepare("SELECT h.*, u.nombre as asesor FROM historial_llamadas h LEFT JOIN usuarios u ON h.asesor_id = u.id WHERE h.pedido_id = ? ORDER BY h.fecha_llamada DESC");
|
||||
$stmtHist->execute([$order['source_key']]);
|
||||
$historial = $stmtHist->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<?php if (empty($historial)): ?>
|
||||
<p class="text-muted small">No hay intentos de llamada registrados aún.</p>
|
||||
<?php else: ?>
|
||||
<div class="list-group list-group-flush border rounded">
|
||||
<?php foreach ($historial as $h): ?>
|
||||
<div class="list-group-item small py-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="fw-bold"><?php echo htmlspecialchars($h['asesor'] ?? 'Desconocido'); ?></span>
|
||||
<span class="text-muted"><?php echo date('d/m/Y H:i', strtotime($h['fecha_llamada'])); ?></span>
|
||||
</div>
|
||||
<div class="text-primary"><?php echo htmlspecialchars($h['resultado']); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="border rounded p-3 h-100 bg-light-subtle">
|
||||
<div class="small text-muted">Estado actual</div>
|
||||
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="border rounded p-3 h-100 bg-light-subtle">
|
||||
<div class="small text-muted">Total llamadas</div>
|
||||
<div class="fw-semibold"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="border rounded p-3 h-100 bg-light-subtle">
|
||||
<div class="small text-muted"><?php echo cc_test_requires_delivery_date($order['estado']) ? 'Entrega programada' : 'Próxima llamada'; ?></div>
|
||||
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_requires_delivery_date($order['estado']) ? cc_test_format_date($order['fecha_entrega_programada'] ?? null) : cc_test_format_datetime($order['proxima_llamada_at'] ?? null)); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="border rounded p-3 h-100 bg-light-subtle">
|
||||
<div class="small text-muted">Ingreso en Drive</div>
|
||||
<div class="fw-semibold"><?php echo htmlspecialchars(cc_test_format_datetime($order['import_id'] ?? null, 'Sin fecha')); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="col-md-4">
|
||||
<label for="estado-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Estado de seguimiento</label>
|
||||
<select class="form-select" id="estado-<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<?php foreach (['POR LLAMAR', 'REINTENTO', 'CONFIRMADO'] as $estadoOption): ?>
|
||||
<option value="<?php echo htmlspecialchars($estadoOption); ?>" <?php echo $order['estado'] === $estadoOption ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($estadoOption); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h4 class="h6 fw-bold mb-3">Gestión comercial</h4>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="estado-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Estado</label>
|
||||
<select class="form-select" id="estado-<?php echo htmlspecialchars($order['source_key']); ?>" onchange="toggleAgendaFields('<?php echo htmlspecialchars($order['source_key']); ?>')">
|
||||
<?php foreach (cc_test_valid_states() as $estadoOption): ?>
|
||||
<option value="<?php echo htmlspecialchars($estadoOption); ?>" <?php echo $order['estado'] === $estadoOption ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars(cc_test_state_label($estadoOption)); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 js-next-call-group" id="next-call-group-<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<label for="proxima-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Próxima llamada</label>
|
||||
<input type="datetime-local" class="form-control" id="proxima-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars(cc_test_format_datetime_input($order['proxima_llamada_at'] ?? null)); ?>">
|
||||
</div>
|
||||
<div class="col-md-4 js-delivery-group <?php echo cc_test_requires_delivery_date($order['estado']) ? '' : 'd-none'; ?>" id="delivery-group-<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<label for="fecha-entrega-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Fecha de entrega 📅</label>
|
||||
<input type="date" class="form-control" id="fecha-entrega-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars(cc_test_format_date_input($order['fecha_entrega_programada'] ?? null)); ?>">
|
||||
<div class="form-text">Úsalo cuando el cliente quede en Confirmado fecha.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Resumen</label>
|
||||
<div class="border rounded px-3 py-2 bg-light-subtle small h-100 d-flex align-items-center">Los estados abiertos vuelven a <strong class="ms-1">Pendientes de hoy</strong>; Confirmado fecha 📅 te permite programar la entrega.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="nota-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Nota interna</label>
|
||||
<textarea class="form-control" id="nota-<?php echo htmlspecialchars($order['source_key']); ?>" rows="3" placeholder="Ejemplo: cliente pidió volver a llamar después de las 5 pm."><?php echo htmlspecialchars($order['nota_seguimiento']); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4 class="h6 fw-bold mb-3">Datos editables del pedido</h4>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="direccion-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Dirección</label>
|
||||
<textarea class="form-control" id="direccion-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['direccion'] ?? '')); ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="referencia-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Referencia</label>
|
||||
<textarea class="form-control" id="referencia-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['referencia'] ?? '')); ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="sede-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Sede / ID</label>
|
||||
<input type="text" class="form-control" id="sede-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['sede'] ?? '')); ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="ciudad-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Ciudad</label>
|
||||
<input type="text" class="form-control" id="ciudad-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['ciudad'] ?? '')); ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="distrito-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Distrito</label>
|
||||
<input type="text" class="form-control" id="distrito-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['distrito'] ?? '')); ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="dni-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">DNI</label>
|
||||
<input type="text" class="form-control" id="dni-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['dni'] ?? '')); ?>">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label for="observaciones-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Observaciones del pedido</label>
|
||||
<textarea class="form-control" id="observaciones-<?php echo htmlspecialchars($order['source_key']); ?>" rows="2"><?php echo htmlspecialchars((string) ($order['observaciones'] ?? '')); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label for="nota-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Nota interna de seguimiento</label>
|
||||
<textarea class="form-control" id="nota-<?php echo htmlspecialchars($order['source_key']); ?>" rows="3" placeholder="Ejemplo: cliente pidió devolver la llamada en la tarde."><?php echo htmlspecialchars($order['nota_seguimiento']); ?></textarea>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h4 class="h6 fw-bold mb-3">Historial de llamadas</h4>
|
||||
<?php if (empty($historial)): ?>
|
||||
<div class="border rounded p-3 text-muted small">Aún no hay llamadas registradas para este cliente.</div>
|
||||
<?php else: ?>
|
||||
<div class="list-group list-group-flush border rounded overflow-hidden">
|
||||
<?php foreach ($historial as $h): ?>
|
||||
<div class="list-group-item small py-3">
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<span class="fw-semibold"><?php echo htmlspecialchars($h['asesor'] ?? 'Asesor'); ?></span>
|
||||
<span class="text-muted"><?php echo htmlspecialchars(date('d/m/Y H:i', strtotime($h['fecha_llamada']))); ?></span>
|
||||
</div>
|
||||
<div class="text-primary fw-semibold mt-1"><?php echo htmlspecialchars($h['resultado']); ?></div>
|
||||
<?php if (!empty($h['observacion'])): ?>
|
||||
<div class="text-muted mt-1"><?php echo htmlspecialchars($h['observacion']); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mt-4 border rounded p-3 bg-light-subtle small">
|
||||
<div class="fw-semibold mb-2">Origen vs edición</div>
|
||||
<div><strong>Dirección Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['direccion_drive'] ?? null)); ?></div>
|
||||
<div><strong>Referencia Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['referencia_drive'] ?? null)); ?></div>
|
||||
<div><strong>Observación Drive:</strong> <?php echo htmlspecialchars(cc_test_display_value($order['observaciones_drive'] ?? null)); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-content-between">
|
||||
<div class="small text-muted">Los cambios se guardan solo en el módulo de prueba.</div>
|
||||
<div class="small text-muted">Los cambios se guardan en la base local del módulo de prueba.</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
<button type="button" class="btn btn-primary" onclick="guardarSeguimiento('<?php echo htmlspecialchars($order['source_key']); ?>')">Guardar seguimiento</button>
|
||||
<button type="button" class="btn btn-primary" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Guardar gestión</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $modalsHtml[] = ob_get_clean(); ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
@ -417,19 +541,63 @@ require_once 'layout_header.php';
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($modalsHtml)): ?>
|
||||
<?php echo implode("
|
||||
", $modalsHtml); ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function guardarSeguimiento(sourceKey) {
|
||||
const estado = document.getElementById('estado-' + sourceKey)?.value || 'POR LLAMAR';
|
||||
const nota = document.getElementById('nota-' + sourceKey)?.value || '';
|
||||
function toggleAgendaFields(sourceKey) {
|
||||
const estado = document.getElementById('estado-' + sourceKey)?.value || '';
|
||||
const nextCallGroup = document.getElementById('next-call-group-' + sourceKey);
|
||||
const deliveryGroup = document.getElementById('delivery-group-' + sourceKey);
|
||||
const nextCallInput = document.getElementById('proxima-' + sourceKey);
|
||||
const deliveryInput = document.getElementById('fecha-entrega-' + sourceKey);
|
||||
const needsDeliveryDate = estado === 'CONFIRMADO FECHA';
|
||||
|
||||
if (nextCallGroup) {
|
||||
nextCallGroup.classList.toggle('d-none', needsDeliveryDate);
|
||||
}
|
||||
if (deliveryGroup) {
|
||||
deliveryGroup.classList.toggle('d-none', !needsDeliveryDate);
|
||||
}
|
||||
if (needsDeliveryDate && nextCallInput) {
|
||||
nextCallInput.value = '';
|
||||
}
|
||||
if (!needsDeliveryDate && deliveryInput) {
|
||||
deliveryInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('select[id^="estado-"]').forEach(select => {
|
||||
toggleAgendaFields(select.id.replace('estado-', ''));
|
||||
});
|
||||
});
|
||||
|
||||
function guardarGestion(sourceKey, trigger) {
|
||||
const body = new URLSearchParams({
|
||||
source_key: sourceKey,
|
||||
estado: estado,
|
||||
nota_seguimiento: nota
|
||||
estado: document.getElementById('estado-' + sourceKey)?.value || 'POR LLAMAR',
|
||||
proxima_llamada_at: document.getElementById('proxima-' + sourceKey)?.value || '',
|
||||
fecha_entrega_programada: document.getElementById('fecha-entrega-' + sourceKey)?.value || '',
|
||||
nota_seguimiento: document.getElementById('nota-' + sourceKey)?.value || '',
|
||||
direccion: document.getElementById('direccion-' + sourceKey)?.value || '',
|
||||
referencia: document.getElementById('referencia-' + sourceKey)?.value || '',
|
||||
sede: document.getElementById('sede-' + sourceKey)?.value || '',
|
||||
ciudad: document.getElementById('ciudad-' + sourceKey)?.value || '',
|
||||
distrito: document.getElementById('distrito-' + sourceKey)?.value || '',
|
||||
dni: document.getElementById('dni-' + sourceKey)?.value || '',
|
||||
observaciones: document.getElementById('observaciones-' + sourceKey)?.value || ''
|
||||
});
|
||||
|
||||
if (trigger) {
|
||||
trigger.disabled = true;
|
||||
}
|
||||
|
||||
fetch('update_callcenter_test_tracking.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
@ -438,16 +606,36 @@ function guardarSeguimiento(sourceKey) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'No se pudo guardar');
|
||||
throw new Error(data.message || 'No se pudo guardar la gestión');
|
||||
}
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.message || 'Ocurrió un error al guardar el seguimiento.');
|
||||
alert(error.message || 'Ocurrió un error al guardar la gestión.');
|
||||
})
|
||||
.finally(() => {
|
||||
if (trigger) {
|
||||
trigger.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registrarLlamada(sourceKey) {
|
||||
function actualizarContadorLlamadas(sourceKey, total) {
|
||||
document.querySelectorAll('.js-call-count-number[data-source-key="' + sourceKey + '"]').forEach(node => {
|
||||
node.textContent = String(total);
|
||||
});
|
||||
}
|
||||
|
||||
function registrarLlamada(event, sourceKey, phoneUrl, trigger) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (trigger) {
|
||||
trigger.classList.add('disabled');
|
||||
trigger.setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
pedido_id: sourceKey,
|
||||
resultado: 'Llamada iniciada',
|
||||
@ -457,18 +645,35 @@ function registrarLlamada(sourceKey) {
|
||||
fetch('save_llamada.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: body.toString()
|
||||
body: body.toString(),
|
||||
keepalive: true
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Llamada registrada correctamente');
|
||||
// Opcional: recargar para actualizar el contador,
|
||||
// pero como abre la app de llamadas, tal vez sea mejor no interrumpir.
|
||||
// window.location.reload();
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'No se pudo registrar la llamada');
|
||||
}
|
||||
|
||||
if (typeof data.total_llamadas !== 'undefined') {
|
||||
actualizarContadorLlamadas(sourceKey, Number(data.total_llamadas) || 0);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error al registrar llamada:', error));
|
||||
.catch(error => {
|
||||
console.error('Error al registrar llamada:', error);
|
||||
alert(error.message || 'No se pudo registrar la llamada.');
|
||||
})
|
||||
.finally(() => {
|
||||
if (trigger) {
|
||||
trigger.classList.remove('disabled');
|
||||
trigger.removeAttribute('aria-disabled');
|
||||
}
|
||||
|
||||
if (phoneUrl) {
|
||||
window.location.href = phoneUrl;
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
155
includes/callcenter_test_helpers.php
Normal file
155
includes/callcenter_test_helpers.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
function cc_test_ensure_column(PDO $pdo, string $table, string $column, string $definition): void
|
||||
{
|
||||
static $cache = [];
|
||||
$key = $table . '.' . $column;
|
||||
if (isset($cache[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SHOW COLUMNS FROM `{$table}` LIKE ?");
|
||||
$stmt->execute([$column]);
|
||||
if (!$stmt->fetch()) {
|
||||
$pdo->exec("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}");
|
||||
}
|
||||
|
||||
$cache[$key] = true;
|
||||
}
|
||||
|
||||
function cc_test_ensure_tracking_table(PDO $pdo): void
|
||||
{
|
||||
static $checked = false;
|
||||
if ($checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_tracking` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`source_key` CHAR(40) NOT NULL,
|
||||
`estado` VARCHAR(40) NOT NULL DEFAULT 'POR LLAMAR',
|
||||
`nota_seguimiento` TEXT NULL,
|
||||
`user_id` INT NULL,
|
||||
`direccion` TEXT NULL,
|
||||
`referencia` TEXT NULL,
|
||||
`sede` VARCHAR(120) NULL,
|
||||
`ciudad` VARCHAR(120) NULL,
|
||||
`distrito` VARCHAR(120) NULL,
|
||||
`dni` VARCHAR(40) NULL,
|
||||
`observaciones` TEXT NULL,
|
||||
`proxima_llamada_at` DATETIME NULL,
|
||||
`fecha_entrega_programada` DATE NULL,
|
||||
`ultima_gestion_at` DATETIME NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_callcenter_test_tracking_source_key` (`source_key`),
|
||||
KEY `idx_callcenter_test_tracking_estado` (`estado`),
|
||||
KEY `idx_callcenter_test_tracking_proxima` (`proxima_llamada_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'direccion', 'TEXT NULL AFTER `user_id`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'referencia', 'TEXT NULL AFTER `direccion`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'sede', 'VARCHAR(120) NULL AFTER `referencia`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'ciudad', 'VARCHAR(120) NULL AFTER `sede`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'distrito', 'VARCHAR(120) NULL AFTER `ciudad`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'dni', 'VARCHAR(40) NULL AFTER `distrito`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'observaciones', 'TEXT NULL AFTER `dni`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'proxima_llamada_at', 'DATETIME NULL AFTER `observaciones`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'fecha_entrega_programada', 'DATE NULL AFTER `proxima_llamada_at`');
|
||||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'ultima_gestion_at', 'DATETIME NULL AFTER `fecha_entrega_programada`');
|
||||
|
||||
$checked = true;
|
||||
}
|
||||
|
||||
function cc_test_ensure_historial_llamadas_table(PDO $pdo): void
|
||||
{
|
||||
static $checked = false;
|
||||
if ($checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `historial_llamadas` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`pedido_id` VARCHAR(80) NOT NULL,
|
||||
`asesor_id` INT UNSIGNED NOT NULL,
|
||||
`resultado` VARCHAR(120) NOT NULL,
|
||||
`observacion` TEXT NULL,
|
||||
`fecha_llamada` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_historial_llamadas_pedido` (`pedido_id`),
|
||||
INDEX `idx_historial_llamadas_asesor` (`asesor_id`),
|
||||
INDEX `idx_historial_llamadas_fecha` (`fecha_llamada`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
|
||||
$checked = true;
|
||||
}
|
||||
|
||||
function cc_test_valid_states(): array
|
||||
{
|
||||
return [
|
||||
'POR LLAMAR',
|
||||
'DEVOLVER LLAMADA',
|
||||
'OBSERVADO',
|
||||
'SE ENVIO NUMERO DE CUENTA',
|
||||
'CONFIRMADO FECHA',
|
||||
'CONTRAENTREGA CONFIRMADO',
|
||||
'CANCELADO',
|
||||
'ENVIO REPETIDO',
|
||||
];
|
||||
}
|
||||
|
||||
function cc_test_open_states(): array
|
||||
{
|
||||
return ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO', 'SE ENVIO NUMERO DE CUENTA'];
|
||||
}
|
||||
|
||||
function cc_test_confirmed_states(): array
|
||||
{
|
||||
return ['CONFIRMADO FECHA', 'CONTRAENTREGA CONFIRMADO'];
|
||||
}
|
||||
|
||||
function cc_test_closed_states(): array
|
||||
{
|
||||
return ['CANCELADO', 'ENVIO REPETIDO'];
|
||||
}
|
||||
|
||||
function cc_test_requires_delivery_date(string $estado): bool
|
||||
{
|
||||
return $estado === 'CONFIRMADO FECHA';
|
||||
}
|
||||
|
||||
function cc_test_state_label(string $estado): string
|
||||
{
|
||||
return match ($estado) {
|
||||
'SE ENVIO NUMERO DE CUENTA' => 'SE ENVIÓ NÚMERO DE CUENTA',
|
||||
'CONFIRMADO FECHA' => 'CONFIRMADO FECHA 📅',
|
||||
'ENVIO REPETIDO' => 'ENVÍO REPETIDO',
|
||||
default => $estado,
|
||||
};
|
||||
}
|
||||
|
||||
function cc_test_table_exists(PDO $pdo, string $tableName): bool
|
||||
{
|
||||
static $cache = [];
|
||||
if (array_key_exists($tableName, $cache)) {
|
||||
return $cache[$tableName];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SHOW TABLES LIKE ?');
|
||||
$stmt->execute([$tableName]);
|
||||
$cache[$tableName] = (bool) $stmt->fetchColumn();
|
||||
return $cache[$tableName];
|
||||
}
|
||||
|
||||
function cc_test_fetch_historial(PDO $pdo, string $pedidoId): array
|
||||
{
|
||||
if (cc_test_table_exists($pdo, 'usuarios')) {
|
||||
$stmt = $pdo->prepare("SELECT h.*, u.nombre as asesor FROM historial_llamadas h LEFT JOIN usuarios u ON h.asesor_id = u.id WHERE h.pedido_id = ? ORDER BY h.fecha_llamada DESC");
|
||||
$stmt->execute([$pedidoId]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT h.*, NULL as asesor FROM historial_llamadas h WHERE h.pedido_id = ? ORDER BY h.fecha_llamada DESC");
|
||||
$stmt->execute([$pedidoId]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
538
includes/callcenter_test_management.php
Normal file
538
includes/callcenter_test_management.php
Normal file
@ -0,0 +1,538 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/drive_test_orders.php';
|
||||
|
||||
function cc_test_allowed_states(): array
|
||||
{
|
||||
return [
|
||||
'POR LLAMAR',
|
||||
'NO CONTESTA',
|
||||
'DEVOLVER LLAMADA',
|
||||
'REPROGRAMADO',
|
||||
'CONFIRMADO',
|
||||
'NO INTERESADO',
|
||||
'NUMERO EQUIVOCADO',
|
||||
'DUPLICADO',
|
||||
];
|
||||
}
|
||||
|
||||
function cc_test_open_states(): array
|
||||
{
|
||||
return ['POR LLAMAR', 'NO CONTESTA', 'DEVOLVER LLAMADA', 'REPROGRAMADO'];
|
||||
}
|
||||
|
||||
function cc_test_closed_states(): array
|
||||
{
|
||||
return ['NO INTERESADO', 'NUMERO EQUIVOCADO', 'DUPLICADO'];
|
||||
}
|
||||
|
||||
function cc_test_state_badge_class(string $estado): string
|
||||
{
|
||||
return match ($estado) {
|
||||
'CONFIRMADO' => 'bg-success-subtle text-success-emphasis',
|
||||
'DEVOLVER LLAMADA', 'REPROGRAMADO' => 'bg-info-subtle text-info-emphasis',
|
||||
'NO CONTESTA' => 'bg-secondary-subtle text-secondary-emphasis',
|
||||
'NO INTERESADO', 'NUMERO EQUIVOCADO', 'DUPLICADO' => 'bg-danger-subtle text-danger-emphasis',
|
||||
default => 'bg-warning-subtle text-warning-emphasis',
|
||||
};
|
||||
}
|
||||
|
||||
function cc_test_display_value(?string $value, string $fallback = 'No registrado'): string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
return $value !== '' ? $value : $fallback;
|
||||
}
|
||||
|
||||
function cc_test_format_price(?string $value): string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
return $value !== '' ? 'S/ ' . $value : 'No registrado';
|
||||
}
|
||||
|
||||
function cc_test_parse_datetime(?string $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$formats = [
|
||||
'Y-m-d\\TH:i',
|
||||
'Y-m-d H:i:s',
|
||||
'Y-m-d H:i',
|
||||
DateTimeInterface::ATOM,
|
||||
'Y-m-d',
|
||||
];
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$date = DateTimeImmutable::createFromFormat($format, $value);
|
||||
if ($date instanceof DateTimeImmutable) {
|
||||
return $date->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return (new DateTimeImmutable($value))->format('Y-m-d H:i:s');
|
||||
} catch (Throwable $exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_format_datetime(?string $value, string $fallback = 'Sin programar'): string
|
||||
{
|
||||
$parsed = cc_test_parse_datetime($value);
|
||||
if ($parsed === null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return (new DateTimeImmutable($parsed))->format('d/m/Y h:i A');
|
||||
} catch (Throwable $exception) {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_format_datetime_local(?string $value): string
|
||||
{
|
||||
$parsed = cc_test_parse_datetime($value);
|
||||
if ($parsed === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return (new DateTimeImmutable($parsed))->format('Y-m-d\\TH:i');
|
||||
} catch (Throwable $exception) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_table_exists(PDO $pdo, string $tableName): bool
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
if (array_key_exists($tableName, $cache)) {
|
||||
return $cache[$tableName];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SHOW TABLES LIKE ?');
|
||||
$stmt->execute([$tableName]);
|
||||
$cache[$tableName] = (bool) $stmt->fetchColumn();
|
||||
|
||||
return $cache[$tableName];
|
||||
}
|
||||
|
||||
function cc_test_column_exists(PDO $pdo, string $tableName, string $columnName): bool
|
||||
{
|
||||
static $cache = [];
|
||||
$cacheKey = $tableName . '.' . $columnName;
|
||||
|
||||
if (array_key_exists($cacheKey, $cache)) {
|
||||
return $cache[$cacheKey];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SHOW COLUMNS FROM `{$tableName}` LIKE ?");
|
||||
$stmt->execute([$columnName]);
|
||||
$cache[$cacheKey] = (bool) $stmt->fetchColumn();
|
||||
|
||||
return $cache[$cacheKey];
|
||||
}
|
||||
|
||||
function cc_test_add_column_if_missing(PDO $pdo, string $tableName, string $columnName, string $definition): void
|
||||
{
|
||||
if (!cc_test_column_exists($pdo, $tableName, $columnName)) {
|
||||
$pdo->exec("ALTER TABLE `{$tableName}` ADD COLUMN `{$columnName}` {$definition}");
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_ensure_schema(PDO $pdo): void
|
||||
{
|
||||
static $checked = false;
|
||||
if ($checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_orders` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`source_key` CHAR(40) NOT NULL,
|
||||
`codigo` VARCHAR(80) DEFAULT NULL,
|
||||
`import_id` VARCHAR(120) DEFAULT NULL,
|
||||
`drive_imported_at` DATETIME NULL,
|
||||
`nombre` VARCHAR(255) DEFAULT NULL,
|
||||
`direccion_drive` TEXT NULL,
|
||||
`referencia_drive` TEXT NULL,
|
||||
`sede_drive` VARCHAR(120) NULL,
|
||||
`ciudad_drive` VARCHAR(120) NULL,
|
||||
`distrito_drive` VARCHAR(120) NULL,
|
||||
`dni_drive` VARCHAR(40) NULL,
|
||||
`observaciones_drive` TEXT NULL,
|
||||
`celular` VARCHAR(40) DEFAULT NULL,
|
||||
`producto` TEXT NULL,
|
||||
`cantidad` VARCHAR(60) NULL,
|
||||
`precio` VARCHAR(60) NULL,
|
||||
`pais` VARCHAR(80) NULL,
|
||||
`coordenadas` VARCHAR(255) NULL,
|
||||
`metodo` VARCHAR(120) NULL,
|
||||
`first_seen_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_seen_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uniq_callcenter_test_orders_source_key` (`source_key`),
|
||||
KEY `idx_callcenter_test_orders_drive_imported_at` (`drive_imported_at`),
|
||||
KEY `idx_callcenter_test_orders_last_seen_at` (`last_seen_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_tracking` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`source_key` CHAR(40) NOT NULL,
|
||||
`estado` VARCHAR(40) NOT NULL DEFAULT 'POR LLAMAR',
|
||||
`nota_seguimiento` TEXT NULL,
|
||||
`proxima_llamada` DATETIME NULL,
|
||||
`fecha_ultimo_contacto` DATETIME NULL,
|
||||
`direccion_editada` TEXT NULL,
|
||||
`referencia_editada` TEXT NULL,
|
||||
`sede_editada` VARCHAR(120) NULL,
|
||||
`ciudad_editada` VARCHAR(120) NULL,
|
||||
`distrito_editado` VARCHAR(120) NULL,
|
||||
`dni_editado` VARCHAR(40) NULL,
|
||||
`observaciones_editadas` TEXT NULL,
|
||||
`user_id` INT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uniq_callcenter_test_tracking_source_key` (`source_key`),
|
||||
KEY `idx_callcenter_test_tracking_estado` (`estado`),
|
||||
KEY `idx_callcenter_test_tracking_proxima` (`proxima_llamada`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'proxima_llamada', 'DATETIME NULL AFTER `nota_seguimiento`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'fecha_ultimo_contacto', 'DATETIME NULL AFTER `proxima_llamada`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'direccion_editada', 'TEXT NULL AFTER `fecha_ultimo_contacto`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'referencia_editada', 'TEXT NULL AFTER `direccion_editada`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'sede_editada', 'VARCHAR(120) NULL AFTER `referencia_editada`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'ciudad_editada', 'VARCHAR(120) NULL AFTER `sede_editada`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'distrito_editado', 'VARCHAR(120) NULL AFTER `ciudad_editada`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'dni_editado', 'VARCHAR(40) NULL AFTER `distrito_editado`');
|
||||
cc_test_add_column_if_missing($pdo, 'callcenter_test_tracking', 'observaciones_editadas', 'TEXT NULL AFTER `dni_editado`');
|
||||
|
||||
if (cc_test_table_exists($pdo, 'historial_llamadas')) {
|
||||
cc_test_add_column_if_missing($pdo, 'historial_llamadas', 'pedido_id', 'VARCHAR(80) NULL');
|
||||
cc_test_add_column_if_missing($pdo, 'historial_llamadas', 'asesor_id', 'INT NULL');
|
||||
cc_test_add_column_if_missing($pdo, 'historial_llamadas', 'resultado', 'VARCHAR(120) NULL');
|
||||
cc_test_add_column_if_missing($pdo, 'historial_llamadas', 'observacion', 'TEXT NULL');
|
||||
cc_test_add_column_if_missing($pdo, 'historial_llamadas', 'fecha_llamada', 'DATETIME NULL DEFAULT CURRENT_TIMESTAMP');
|
||||
} else {
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `historial_llamadas` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`pedido_id` VARCHAR(80) NOT NULL,
|
||||
`asesor_id` INT NULL,
|
||||
`resultado` VARCHAR(120) NOT NULL,
|
||||
`observacion` TEXT NULL,
|
||||
`fecha_llamada` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_historial_llamadas_pedido` (`pedido_id`),
|
||||
INDEX `idx_historial_llamadas_asesor` (`asesor_id`),
|
||||
INDEX `idx_historial_llamadas_fecha` (`fecha_llamada`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
}
|
||||
|
||||
$checked = true;
|
||||
}
|
||||
|
||||
function cc_test_sync_orders_from_drive(PDO $pdo, int $limit = 250): array
|
||||
{
|
||||
cc_test_ensure_schema($pdo);
|
||||
|
||||
$preview = drive_test_fetch_orders($limit);
|
||||
$orders = $preview['orders'] ?? [];
|
||||
|
||||
if (empty($orders)) {
|
||||
return [
|
||||
'synced_count' => 0,
|
||||
'total_rows' => (int) ($preview['total_rows'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO callcenter_test_orders (
|
||||
source_key, codigo, import_id, drive_imported_at, nombre,
|
||||
direccion_drive, referencia_drive, sede_drive, ciudad_drive, distrito_drive,
|
||||
dni_drive, observaciones_drive, celular, producto, cantidad, precio,
|
||||
pais, coordenadas, metodo
|
||||
) VALUES (
|
||||
:source_key, :codigo, :import_id, :drive_imported_at, :nombre,
|
||||
:direccion_drive, :referencia_drive, :sede_drive, :ciudad_drive, :distrito_drive,
|
||||
:dni_drive, :observaciones_drive, :celular, :producto, :cantidad, :precio,
|
||||
:pais, :coordenadas, :metodo
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
codigo = VALUES(codigo),
|
||||
import_id = VALUES(import_id),
|
||||
drive_imported_at = VALUES(drive_imported_at),
|
||||
nombre = VALUES(nombre),
|
||||
direccion_drive = VALUES(direccion_drive),
|
||||
referencia_drive = VALUES(referencia_drive),
|
||||
sede_drive = VALUES(sede_drive),
|
||||
ciudad_drive = VALUES(ciudad_drive),
|
||||
distrito_drive = VALUES(distrito_drive),
|
||||
dni_drive = VALUES(dni_drive),
|
||||
observaciones_drive = VALUES(observaciones_drive),
|
||||
celular = VALUES(celular),
|
||||
producto = VALUES(producto),
|
||||
cantidad = VALUES(cantidad),
|
||||
precio = VALUES(precio),
|
||||
pais = VALUES(pais),
|
||||
coordenadas = VALUES(coordenadas),
|
||||
metodo = VALUES(metodo),
|
||||
last_seen_at = CURRENT_TIMESTAMP'
|
||||
);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$stmt->execute([
|
||||
':source_key' => $order['source_key'],
|
||||
':codigo' => $order['codigo'] ?: null,
|
||||
':import_id' => $order['import_id'] ?: null,
|
||||
':drive_imported_at' => cc_test_parse_datetime($order['import_id'] ?? ''),
|
||||
':nombre' => $order['nombre'] ?: null,
|
||||
':direccion_drive' => $order['direccion'] ?: null,
|
||||
':referencia_drive' => $order['referencia'] ?: null,
|
||||
':sede_drive' => $order['sede'] ?: null,
|
||||
':ciudad_drive' => $order['ciudad'] ?: null,
|
||||
':distrito_drive' => $order['distrito'] ?: null,
|
||||
':dni_drive' => $order['dni'] ?: null,
|
||||
':observaciones_drive' => $order['observaciones'] ?: null,
|
||||
':celular' => $order['celular'] ?: null,
|
||||
':producto' => $order['producto'] ?: null,
|
||||
':cantidad' => $order['cantidad'] ?: null,
|
||||
':precio' => $order['precio'] ?: null,
|
||||
':pais' => $order['pais'] ?: null,
|
||||
':coordenadas' => $order['coordenadas'] ?: null,
|
||||
':metodo' => $order['metodo'] ?: null,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'synced_count' => count($orders),
|
||||
'total_rows' => (int) ($preview['total_rows'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
function cc_test_fetch_dashboard_orders(PDO $pdo): array
|
||||
{
|
||||
cc_test_ensure_schema($pdo);
|
||||
|
||||
$sql = "SELECT
|
||||
o.source_key,
|
||||
o.codigo,
|
||||
o.import_id,
|
||||
o.drive_imported_at,
|
||||
o.nombre,
|
||||
o.celular,
|
||||
o.producto,
|
||||
o.cantidad,
|
||||
o.precio,
|
||||
o.pais,
|
||||
o.coordenadas,
|
||||
o.metodo,
|
||||
o.direccion_drive,
|
||||
o.referencia_drive,
|
||||
o.sede_drive,
|
||||
o.ciudad_drive,
|
||||
o.distrito_drive,
|
||||
o.dni_drive,
|
||||
o.observaciones_drive,
|
||||
o.first_seen_at,
|
||||
o.last_seen_at,
|
||||
COALESCE(NULLIF(t.estado, ''), 'POR LLAMAR') AS estado,
|
||||
COALESCE(t.nota_seguimiento, '') AS nota_seguimiento,
|
||||
t.proxima_llamada,
|
||||
t.fecha_ultimo_contacto,
|
||||
t.user_id,
|
||||
t.updated_at AS tracking_updated_at,
|
||||
t.direccion_editada,
|
||||
t.referencia_editada,
|
||||
t.sede_editada,
|
||||
t.ciudad_editada,
|
||||
t.distrito_editado,
|
||||
t.dni_editado,
|
||||
t.observaciones_editadas,
|
||||
COALESCE(NULLIF(t.direccion_editada, ''), NULLIF(o.direccion_drive, '')) AS direccion_final,
|
||||
COALESCE(NULLIF(t.referencia_editada, ''), NULLIF(o.referencia_drive, '')) AS referencia_final,
|
||||
COALESCE(NULLIF(t.sede_editada, ''), NULLIF(o.sede_drive, '')) AS sede_final,
|
||||
COALESCE(NULLIF(t.ciudad_editada, ''), NULLIF(o.ciudad_drive, '')) AS ciudad_final,
|
||||
COALESCE(NULLIF(t.distrito_editado, ''), NULLIF(o.distrito_drive, '')) AS distrito_final,
|
||||
COALESCE(NULLIF(t.dni_editado, ''), NULLIF(o.dni_drive, '')) AS dni_final,
|
||||
COALESCE(NULLIF(t.observaciones_editadas, ''), NULLIF(o.observaciones_drive, '')) AS observaciones_final,
|
||||
IFNULL(c.total_llamadas, 0) AS total_llamadas,
|
||||
c.ultima_llamada,
|
||||
COALESCE(u.nombre_asesor, u.username, '') AS gestor
|
||||
FROM callcenter_test_orders o
|
||||
LEFT JOIN callcenter_test_tracking t ON t.source_key = o.source_key
|
||||
LEFT JOIN (
|
||||
SELECT pedido_id, COUNT(*) AS total_llamadas, MAX(fecha_llamada) AS ultima_llamada
|
||||
FROM historial_llamadas
|
||||
GROUP BY pedido_id
|
||||
) c ON c.pedido_id = o.source_key
|
||||
LEFT JOIN users u ON u.id = t.user_id
|
||||
ORDER BY COALESCE(o.drive_imported_at, o.first_seen_at) DESC, o.id DESC";
|
||||
|
||||
$stmt = $pdo->query($sql);
|
||||
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($orders as &$order) {
|
||||
$digits = preg_replace('/\D+/', '', (string) ($order['celular'] ?? ''));
|
||||
$order['telefono_url'] = $digits !== '' ? 'tel:' . $digits : '';
|
||||
$order['whatsapp_url'] = $digits !== '' ? 'https://wa.me/' . $digits : '';
|
||||
}
|
||||
unset($order);
|
||||
|
||||
return $orders;
|
||||
}
|
||||
|
||||
function cc_test_is_same_day(?string $value, DateTimeImmutable $day): bool
|
||||
{
|
||||
$parsed = cc_test_parse_datetime($value);
|
||||
if ($parsed === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return (new DateTimeImmutable($parsed))->format('Y-m-d') === $day->format('Y-m-d');
|
||||
} catch (Throwable $exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_is_due_today(array $order, DateTimeImmutable $endOfToday): bool
|
||||
{
|
||||
if (!in_array($order['estado'] ?? '', cc_test_open_states(), true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nextCall = cc_test_parse_datetime($order['proxima_llamada'] ?? null);
|
||||
if ($nextCall === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return new DateTimeImmutable($nextCall) <= $endOfToday;
|
||||
} catch (Throwable $exception) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_matches_search(array $order, string $search): bool
|
||||
{
|
||||
$search = trim($search);
|
||||
if ($search === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$haystack = mb_strtolower(implode(' ', [
|
||||
$order['codigo'] ?? '',
|
||||
$order['nombre'] ?? '',
|
||||
$order['celular'] ?? '',
|
||||
$order['dni_final'] ?? '',
|
||||
$order['direccion_final'] ?? '',
|
||||
$order['distrito_final'] ?? '',
|
||||
$order['ciudad_final'] ?? '',
|
||||
$order['producto'] ?? '',
|
||||
]));
|
||||
|
||||
return str_contains($haystack, mb_strtolower($search));
|
||||
}
|
||||
|
||||
function cc_test_build_dashboard(array $orders, string $view, string $search = ''): array
|
||||
{
|
||||
$today = new DateTimeImmutable('today');
|
||||
$endOfToday = $today->setTime(23, 59, 59);
|
||||
$stats = [
|
||||
'todos' => count($orders),
|
||||
'nuevos_hoy' => 0,
|
||||
'pendientes_hoy' => 0,
|
||||
'confirmados' => 0,
|
||||
'cerrados' => 0,
|
||||
];
|
||||
$visibleOrders = [];
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$createdReference = $order['drive_imported_at'] ?: $order['first_seen_at'];
|
||||
$isNewToday = cc_test_is_same_day($createdReference, $today);
|
||||
$isConfirmed = ($order['estado'] ?? '') === 'CONFIRMADO';
|
||||
$isClosed = in_array($order['estado'] ?? '', cc_test_closed_states(), true);
|
||||
$isPendingToday = cc_test_is_due_today($order, $endOfToday);
|
||||
|
||||
if ($isNewToday) {
|
||||
$stats['nuevos_hoy']++;
|
||||
}
|
||||
if ($isPendingToday) {
|
||||
$stats['pendientes_hoy']++;
|
||||
}
|
||||
if ($isConfirmed) {
|
||||
$stats['confirmados']++;
|
||||
}
|
||||
if ($isClosed) {
|
||||
$stats['cerrados']++;
|
||||
}
|
||||
|
||||
$order['is_new_today'] = $isNewToday;
|
||||
$order['is_pending_today'] = $isPendingToday;
|
||||
$order['is_confirmed'] = $isConfirmed;
|
||||
$order['is_closed'] = $isClosed;
|
||||
|
||||
$matchesView = match ($view) {
|
||||
'nuevos_hoy' => $isNewToday,
|
||||
'confirmados' => $isConfirmed,
|
||||
'cerrados' => $isClosed,
|
||||
'todos' => true,
|
||||
default => $isPendingToday,
|
||||
};
|
||||
|
||||
if ($matchesView && cc_test_matches_search($order, $search)) {
|
||||
$visibleOrders[] = $order;
|
||||
}
|
||||
}
|
||||
|
||||
usort($visibleOrders, static function (array $a, array $b) use ($view): int {
|
||||
$aNext = cc_test_parse_datetime($a['proxima_llamada'] ?? null);
|
||||
$bNext = cc_test_parse_datetime($b['proxima_llamada'] ?? null);
|
||||
|
||||
if ($view === 'pendientes_hoy') {
|
||||
if ($aNext === null && $bNext !== null) {
|
||||
return -1;
|
||||
}
|
||||
if ($aNext !== null && $bNext === null) {
|
||||
return 1;
|
||||
}
|
||||
if ($aNext !== null && $bNext !== null && $aNext !== $bNext) {
|
||||
return strcmp($aNext, $bNext);
|
||||
}
|
||||
}
|
||||
|
||||
$aBase = cc_test_parse_datetime($a['drive_imported_at'] ?? null) ?: cc_test_parse_datetime($a['first_seen_at'] ?? null) ?: '';
|
||||
$bBase = cc_test_parse_datetime($b['drive_imported_at'] ?? null) ?: cc_test_parse_datetime($b['first_seen_at'] ?? null) ?: '';
|
||||
return strcmp($bBase, $aBase);
|
||||
});
|
||||
|
||||
return [
|
||||
'stats' => $stats,
|
||||
'visible_orders' => $visibleOrders,
|
||||
];
|
||||
}
|
||||
|
||||
function cc_test_fetch_histories(PDO $pdo, array $sourceKeys): array
|
||||
{
|
||||
if (empty($sourceKeys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
|
||||
$sql = "SELECT h.*, COALESCE(u.nombre_asesor, u.username, CONCAT('Asesor #', h.asesor_id)) AS asesor
|
||||
FROM historial_llamadas h
|
||||
LEFT JOIN users u ON u.id = h.asesor_id
|
||||
WHERE h.pedido_id IN ($placeholders)
|
||||
ORDER BY h.fecha_llamada DESC, h.id DESC";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($sourceKeys);
|
||||
|
||||
$histories = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$histories[$row['pedido_id']][] = $row;
|
||||
}
|
||||
|
||||
return $histories;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/callcenter_test_helpers.php';
|
||||
|
||||
function drive_test_normalize_header(string $header): string
|
||||
{
|
||||
@ -116,8 +117,10 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array
|
||||
return [];
|
||||
}
|
||||
|
||||
cc_test_ensure_tracking_table($pdo);
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
|
||||
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
|
||||
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, direccion, referencia, sede, ciudad, distrito, dni, observaciones, proxima_llamada_at, fecha_entrega_programada, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
|
||||
$stmt->execute($sourceKeys);
|
||||
|
||||
$tracking = [];
|
||||
@ -130,11 +133,26 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array
|
||||
|
||||
function drive_test_merge_tracking(array $orders, array $tracking): array
|
||||
{
|
||||
$editableFields = ['direccion', 'referencia', 'sede', 'ciudad', 'distrito', 'dni', 'observaciones'];
|
||||
|
||||
foreach ($orders as &$order) {
|
||||
$current = $tracking[$order['source_key']] ?? null;
|
||||
$order['estado'] = $current['estado'] ?? 'POR LLAMAR';
|
||||
$order['nota_seguimiento'] = $current['nota_seguimiento'] ?? '';
|
||||
$order['seguimiento_actualizado'] = $current['updated_at'] ?? null;
|
||||
$order['proxima_llamada_at'] = $current['proxima_llamada_at'] ?? null;
|
||||
$order['fecha_entrega_programada'] = $current['fecha_entrega_programada'] ?? null;
|
||||
$order['ultima_gestion_at'] = $current['ultima_gestion_at'] ?? ($current['updated_at'] ?? null);
|
||||
|
||||
foreach ($editableFields as $field) {
|
||||
$order[$field . '_drive'] = $order[$field] ?? '';
|
||||
$trackedValue = $current[$field] ?? null;
|
||||
$order[$field . '_editado'] = $trackedValue;
|
||||
if ($trackedValue !== null && trim((string) $trackedValue) !== '') {
|
||||
$order[$field] = $trackedValue;
|
||||
}
|
||||
}
|
||||
|
||||
$digits = preg_replace('/\D+/', '', $order['celular'] ?? '');
|
||||
$order['whatsapp_url'] = $digits !== '' ? 'https://wa.me/' . $digits : '';
|
||||
$order['telefono_url'] = $digits !== '' ? 'tel:' . $digits : '';
|
||||
|
||||
129
index.php
129
index.php
@ -1,3 +1,128 @@
|
||||
<?php
|
||||
header('Location: login.php');
|
||||
exit();
|
||||
session_start();
|
||||
$isLoggedIn = !empty($_SESSION['user_id']);
|
||||
$pageTitle = 'Inicio | Panel CRM y Administración';
|
||||
$pageDescription = 'Entrada rápida al sistema CRM y panel de administración.';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?php echo htmlspecialchars($pageTitle); ?></title>
|
||||
<meta name="description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f8fb;
|
||||
--card: rgba(255,255,255,.84);
|
||||
--text: #102033;
|
||||
--muted: #5f6c7b;
|
||||
--accent: #2563eb;
|
||||
--accent-2: #14b8a6;
|
||||
--border: rgba(16,32,51,.10);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(37,99,235,.12), transparent 28%),
|
||||
radial-gradient(circle at bottom right, rgba(20,184,166,.12), transparent 24%),
|
||||
var(--bg);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.card {
|
||||
width: min(720px, 100%);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 28px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(16,32,51,.10);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(37,99,235,.10);
|
||||
color: var(--accent);
|
||||
font-size: .9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
h1 { margin: 18px 0 10px; font-size: clamp(2rem, 4vw, 3.4rem); line-height: 1.05; }
|
||||
p { margin: 0; color: var(--muted); font-size: 1.05rem; line-height: 1.6; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
padding: 0 18px;
|
||||
border-radius: 14px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
transition: transform .2s ease, box-shadow .2s ease, background .2s ease;
|
||||
}
|
||||
.btn:hover { transform: translateY(-1px); }
|
||||
.btn-primary { background: var(--accent); color: #fff; box-shadow: 0 12px 24px rgba(37,99,235,.24); }
|
||||
.btn-secondary { background: rgba(16,32,51,.05); color: var(--text); }
|
||||
.meta {
|
||||
margin-top: 26px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.meta div {
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,.72);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.meta strong { display: block; margin-bottom: 4px; }
|
||||
.footer { margin-top: 24px; font-size: .95rem; color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
<span class="eyebrow"><?php echo $isLoggedIn ? 'Sesión activa' : 'Acceso al sistema'; ?></span>
|
||||
<h1>Panel CRM y Administración</h1>
|
||||
<p>
|
||||
<?php if ($isLoggedIn): ?>
|
||||
Ya tienes una sesión activa. Entra directo al dashboard para continuar con la operación.
|
||||
<?php else: ?>
|
||||
Accede al sistema para gestionar pedidos, inventario, llamadas y operaciones desde un solo lugar.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<div>
|
||||
<strong>Estado</strong>
|
||||
<span><?php echo $isLoggedIn ? 'Sesión autenticada' : 'Sin sesión iniciada'; ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Zona horaria</strong>
|
||||
<span>America/Lima</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Versión</strong>
|
||||
<span>CRM / Panel de administración</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">Si estás comenzando, usa el botón principal para entrar al sistema.</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/callcenter_test_helpers.php';
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
@ -9,22 +10,33 @@ if (!isset($_SESSION['user_id'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$pedido_id = $_POST['pedido_id'] ?? '';
|
||||
$resultado = $_POST['resultado'] ?? 'Llamada iniciada';
|
||||
$observacion = $_POST['observacion'] ?? '';
|
||||
$asesor_id = $_SESSION['user_id'];
|
||||
$pedido_id = trim((string) ($_POST['pedido_id'] ?? ''));
|
||||
$resultado = trim((string) ($_POST['resultado'] ?? 'Llamada iniciada'));
|
||||
$observacion = trim((string) ($_POST['observacion'] ?? ''));
|
||||
$asesor_id = (int) $_SESSION['user_id'];
|
||||
|
||||
if (empty($pedido_id)) {
|
||||
if ($pedido_id === '') {
|
||||
echo json_encode(['success' => false, 'error' => 'ID de pedido faltante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($resultado === '') {
|
||||
$resultado = 'Llamada iniciada';
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("INSERT INTO historial_llamadas (pedido_id, asesor_id, resultado, observacion) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$pedido_id, $asesor_id, $resultado, $observacion]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
cc_test_ensure_historial_llamadas_table($pdo);
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO historial_llamadas (pedido_id, asesor_id, resultado, observacion) VALUES (?, ?, ?, ?)');
|
||||
$stmt->execute([$pedido_id, $asesor_id, $resultado, $observacion !== '' ? $observacion : null]);
|
||||
|
||||
$countStmt = $pdo->prepare('SELECT COUNT(*) FROM historial_llamadas WHERE pedido_id = ?');
|
||||
$countStmt->execute([$pedido_id]);
|
||||
$totalLlamadas = (int) $countStmt->fetchColumn();
|
||||
|
||||
echo json_encode(['success' => true, 'total_llamadas' => $totalLlamadas]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('save_llamada.php: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'error' => 'No se pudo registrar la llamada']);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/callcenter_test_helpers.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
@ -16,10 +17,23 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
function cc_test_normalize_nullable_text(string $key, int $maxLen = 3000): ?string
|
||||
{
|
||||
$value = trim((string) ($_POST[$key] ?? ''));
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mb_strlen($value) > $maxLen) {
|
||||
throw new RuntimeException('El campo ' . $key . ' es demasiado largo.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
$sourceKey = trim((string) ($_POST['source_key'] ?? ''));
|
||||
$estado = trim((string) ($_POST['estado'] ?? 'POR LLAMAR'));
|
||||
$nota = trim((string) ($_POST['nota_seguimiento'] ?? ''));
|
||||
$validStates = ['POR LLAMAR', 'REINTENTO', 'CONFIRMADO'];
|
||||
$validStates = cc_test_valid_states();
|
||||
|
||||
if ($sourceKey === '' || !preg_match('/^[a-f0-9]{40}$/', $sourceKey)) {
|
||||
http_response_code(400);
|
||||
@ -33,21 +47,97 @@ if (!in_array($estado, $validStates, true)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (mb_strlen($nota) > 3000) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'La nota es demasiado larga']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$nota = cc_test_normalize_nullable_text('nota_seguimiento', 3000);
|
||||
$direccion = cc_test_normalize_nullable_text('direccion', 1000);
|
||||
$referencia = cc_test_normalize_nullable_text('referencia', 1000);
|
||||
$sede = cc_test_normalize_nullable_text('sede', 120);
|
||||
$ciudad = cc_test_normalize_nullable_text('ciudad', 120);
|
||||
$distrito = cc_test_normalize_nullable_text('distrito', 120);
|
||||
$dni = cc_test_normalize_nullable_text('dni', 40);
|
||||
$observaciones = cc_test_normalize_nullable_text('observaciones', 3000);
|
||||
|
||||
$proximaRaw = trim((string) ($_POST['proxima_llamada_at'] ?? ''));
|
||||
$proximaLlamada = null;
|
||||
if ($proximaRaw !== '') {
|
||||
$proximaLlamada = DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $proximaRaw);
|
||||
if (!$proximaLlamada) {
|
||||
throw new RuntimeException('La fecha de próxima llamada no es válida.');
|
||||
}
|
||||
$proximaLlamada = $proximaLlamada->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$fechaEntregaRaw = trim((string) ($_POST['fecha_entrega_programada'] ?? ''));
|
||||
$fechaEntrega = null;
|
||||
if ($fechaEntregaRaw !== '') {
|
||||
$fechaEntregaDate = DateTimeImmutable::createFromFormat('Y-m-d', $fechaEntregaRaw);
|
||||
if (!$fechaEntregaDate) {
|
||||
throw new RuntimeException('La fecha de entrega no es válida.');
|
||||
}
|
||||
$fechaEntrega = $fechaEntregaDate->format('Y-m-d');
|
||||
}
|
||||
|
||||
if (cc_test_requires_delivery_date($estado) && $fechaEntrega === null) {
|
||||
throw new RuntimeException('Debes seleccionar la fecha de entrega para CONFIRMADO FECHA 📅.');
|
||||
}
|
||||
|
||||
if (!in_array($estado, cc_test_open_states(), true)) {
|
||||
$proximaLlamada = null;
|
||||
}
|
||||
|
||||
if (!cc_test_requires_delivery_date($estado)) {
|
||||
$fechaEntrega = null;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
cc_test_ensure_tracking_table($pdo);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO callcenter_test_tracking (source_key, estado, nota_seguimiento, user_id)
|
||||
VALUES (:source_key, :estado, :nota, :user_id)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
'INSERT INTO callcenter_test_tracking (
|
||||
source_key,
|
||||
estado,
|
||||
nota_seguimiento,
|
||||
user_id,
|
||||
direccion,
|
||||
referencia,
|
||||
sede,
|
||||
ciudad,
|
||||
distrito,
|
||||
dni,
|
||||
observaciones,
|
||||
proxima_llamada_at,
|
||||
fecha_entrega_programada,
|
||||
ultima_gestion_at
|
||||
) VALUES (
|
||||
:source_key,
|
||||
:estado,
|
||||
:nota,
|
||||
:user_id,
|
||||
:direccion,
|
||||
:referencia,
|
||||
:sede,
|
||||
:ciudad,
|
||||
:distrito,
|
||||
:dni,
|
||||
:observaciones,
|
||||
:proxima_llamada_at,
|
||||
:fecha_entrega_programada,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
estado = VALUES(estado),
|
||||
nota_seguimiento = VALUES(nota_seguimiento),
|
||||
user_id = VALUES(user_id),
|
||||
direccion = VALUES(direccion),
|
||||
referencia = VALUES(referencia),
|
||||
sede = VALUES(sede),
|
||||
ciudad = VALUES(ciudad),
|
||||
distrito = VALUES(distrito),
|
||||
dni = VALUES(dni),
|
||||
observaciones = VALUES(observaciones),
|
||||
proxima_llamada_at = VALUES(proxima_llamada_at),
|
||||
fecha_entrega_programada = VALUES(fecha_entrega_programada),
|
||||
ultima_gestion_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP'
|
||||
);
|
||||
|
||||
@ -56,17 +146,29 @@ try {
|
||||
':estado' => $estado,
|
||||
':nota' => $nota,
|
||||
':user_id' => (int) $_SESSION['user_id'],
|
||||
':direccion' => $direccion,
|
||||
':referencia' => $referencia,
|
||||
':sede' => $sede,
|
||||
':ciudad' => $ciudad,
|
||||
':distrito' => $distrito,
|
||||
':dni' => $dni,
|
||||
':observaciones' => $observaciones,
|
||||
':proxima_llamada_at' => $proximaLlamada,
|
||||
':fecha_entrega_programada' => $fechaEntrega,
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Seguimiento actualizado correctamente.',
|
||||
'message' => 'Gestión actualizada correctamente.',
|
||||
'estado' => $estado,
|
||||
'proxima_llamada_at' => $proximaLlamada,
|
||||
'fecha_entrega_programada' => $fechaEntrega,
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
http_response_code(500);
|
||||
error_log('update_callcenter_test_tracking.php: ' . $exception->getMessage());
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'No se pudo guardar el seguimiento.',
|
||||
'message' => $exception instanceof RuntimeException ? $exception->getMessage() : 'No se pudo guardar la gestión.',
|
||||
]);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user