diff --git a/db/migrations/074_create_historial_llamadas_table.sql b/db/migrations/074_create_historial_llamadas_table.sql new file mode 100644 index 00000000..9d5ed6c3 --- /dev/null +++ b/db/migrations/074_create_historial_llamadas_table.sql @@ -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; diff --git a/db/migrations/075_expand_callcenter_test_tracking.sql b/db/migrations/075_expand_callcenter_test_tracking.sql new file mode 100644 index 00000000..4ed7c5e1 --- /dev/null +++ b/db/migrations/075_expand_callcenter_test_tracking.sql @@ -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`; diff --git a/gestiones_callcenter.php b/gestiones_callcenter.php index 934c3c13..91a43e9d 100644 --- a/gestiones_callcenter.php +++ b/gestiones_callcenter.php @@ -1,89 +1,48 @@ ['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';
-
+

Call Center de prueba

-

Este panel usa solo los últimos 10 registros de Importar Drive (Test) para confirmar pedidos sin tocar pedidos reales.

+

Ahora el panel trabaja por bandejas: Nuevos de hoy, Pendientes de hoy, Confirmados y Cerrados. Así no se mezcla todo cuando entran pedidos diarios.

- Fuente: Importar Drive (Test) - Hoja detectada: registros - Ver vista previa + Drive detectado: filas + Carga operativa: últimos registros + Ver vista previa Drive
+
+
+
+

Flujo recomendado ya listo para prueba

+

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 Pendientes de hoy hasta que los cierres o confirmes.

+
+
+
Campos editables del módulo
+
Dirección, referencia, sede / ID, ciudad, distrito, DNI y observaciones
+
+
+
+
-
-

-

Datos tomados solo desde Importar Drive (Test): nombre, celular, dirección, referencia, distrito, ciudad, producto, cantidad, precio, método, sede / ID, DNI y observaciones.

+

+

Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado fecha 📅, Contraentrega confirmado, Cancelado y Envío repetido.

- pedidos + pedidos en esta bandeja
- - + + + - + - - + + + + @@ -417,19 +541,63 @@ require_once 'layout_header.php'; + + + + diff --git a/includes/callcenter_test_helpers.php b/includes/callcenter_test_helpers.php new file mode 100644 index 00000000..7b605f4d --- /dev/null +++ b/includes/callcenter_test_helpers.php @@ -0,0 +1,155 @@ +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); +} diff --git a/includes/callcenter_test_management.php b/includes/callcenter_test_management.php new file mode 100644 index 00000000..ef7a75cc --- /dev/null +++ b/includes/callcenter_test_management.php @@ -0,0 +1,538 @@ + '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; +} diff --git a/includes/drive_test_orders.php b/includes/drive_test_orders.php index 47f5f48b..9d3a7d55 100644 --- a/includes/drive_test_orders.php +++ b/includes/drive_test_orders.php @@ -1,5 +1,6 @@ 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 : ''; diff --git a/index.php b/index.php index dd2c8f82..d693afda 100644 --- a/index.php +++ b/index.php @@ -1,3 +1,128 @@ + + + + + + <?php echo htmlspecialchars($pageTitle); ?> + + + + +
+ +

Panel CRM y Administración

+

+ + Ya tienes una sesión activa. Entra directo al dashboard para continuar con la operación. + + Accede al sistema para gestionar pedidos, inventario, llamadas y operaciones desde un solo lugar. + +

+ + + +
+
+ Estado + +
+
+ Zona horaria + America/Lima +
+
+ Versión + CRM / Panel de administración +
+
+ + +
+ + diff --git a/save_llamada.php b/save_llamada.php index 164c0da6..47d3b514 100644 --- a/save_llamada.php +++ b/save_llamada.php @@ -1,5 +1,6 @@ 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']); } diff --git a/update_callcenter_test_tracking.php b/update_callcenter_test_tracking.php index 155fd1bd..d9d6d0aa 100644 --- a/update_callcenter_test_tracking.php +++ b/update_callcenter_test_tracking.php @@ -1,6 +1,7 @@ $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.', ]); }
Cliente y contactoUbicaciónN° PedidoClienteUbicación editable PedidoSeguimientoGestión Acciones
+ - No hay pedidos en esta vista del módulo de prueba. + No hay pedidos en esta bandeja por ahora.
-
-
Pedido:
-
Drive:
-
DNI:
- - - - - -
Celular: No registrado
- +
+
ID Drive:
-
-
-
Ref.:
-
·
- - Ver ubicación - +
+
Celular:
+
DNI:
-
-
Cantidad: · Precio:
-
Método:
-
Sede / ID:
+
Dirección:
+
Referencia:
+
Ciudad / Distrito:
+
Sede / ID:
- - 0): ?> -
- - llamadas - -
+
+
Cantidad: ·
+
Ingreso Drive:
+
Observación Drive:
+
+
+ + llamadas +
+
Próxima llamada:
+ +
Entrega programada 📅:
-
Obs. Drive:
-
Nota interna:
+
Última gestión:
+
Nota:
-
- +
- - Llamar + + Llamar + + - + WhatsApp +