40097-vm/includes/callcenter_test_management.php
2026-05-26 15:51:40 +00:00

546 lines
19 KiB
PHP

<?php
require_once __DIR__ . '/callcenter_test_helpers.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',
];
}
if (!function_exists('cc_test_open_states')) {
function cc_test_open_states(): array
{
return ['POR LLAMAR', 'NO CONTESTA', 'DEVOLVER LLAMADA', 'REPROGRAMADO'];
}
}
if (!function_exists('cc_test_closed_states')) {
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 '';
}
}
if (!function_exists('cc_test_table_exists')) {
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;
}