790 lines
28 KiB
PHP
790 lines
28 KiB
PHP
<?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_column_exists(PDO $pdo, string $table, string $column): bool
|
||
{
|
||
static $cache = [];
|
||
$key = $table . '.' . $column;
|
||
if (array_key_exists($key, $cache)) {
|
||
return $cache[$key];
|
||
}
|
||
|
||
try {
|
||
$stmt = $pdo->prepare("SHOW COLUMNS FROM `{$table}` LIKE ?");
|
||
$stmt->execute([$column]);
|
||
$cache[$key] = (bool) $stmt->fetch(PDO::FETCH_ASSOC);
|
||
} catch (Throwable $exception) {
|
||
$cache[$key] = false;
|
||
}
|
||
|
||
return $cache[$key];
|
||
}
|
||
|
||
function cc_test_normalize_hex_color(?string $value): ?string
|
||
{
|
||
$value = strtoupper(trim((string) $value));
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
if ($value[0] !== '#') {
|
||
$value = '#' . $value;
|
||
}
|
||
|
||
return preg_match('/^#[0-9A-F]{6}$/', $value) ? $value : null;
|
||
}
|
||
|
||
function cc_test_hex_to_rgb_triplet(string $hex): ?array
|
||
{
|
||
$normalized = cc_test_normalize_hex_color($hex);
|
||
if ($normalized === null) {
|
||
return null;
|
||
}
|
||
|
||
$normalized = ltrim($normalized, '#');
|
||
|
||
return [
|
||
hexdec(substr($normalized, 0, 2)),
|
||
hexdec(substr($normalized, 2, 2)),
|
||
hexdec(substr($normalized, 4, 2)),
|
||
];
|
||
}
|
||
|
||
function cc_test_assessor_default_color_hex(string $assessorKey): string
|
||
{
|
||
return match (cc_test_normalize_user_key($assessorKey)) {
|
||
'KARINA' => '#0D6EFD',
|
||
'ESTEFANYA' => '#198754',
|
||
'CARMEN' => '#FF00FF',
|
||
default => '#6C757D',
|
||
};
|
||
}
|
||
|
||
function cc_test_assessor_effective_color_hex(string $assessorKey, ?string $storedColorHex = null): string
|
||
{
|
||
return cc_test_normalize_hex_color($storedColorHex) ?? cc_test_assessor_default_color_hex($assessorKey);
|
||
}
|
||
|
||
function cc_test_hex_relative_luminance(string $hex): float
|
||
{
|
||
$rgb = cc_test_hex_to_rgb_triplet($hex) ?? [108, 117, 125];
|
||
$linear = array_map(
|
||
static function (int $channel): float {
|
||
$value = $channel / 255;
|
||
return $value <= 0.03928
|
||
? $value / 12.92
|
||
: pow(($value + 0.055) / 1.055, 2.4);
|
||
},
|
||
$rgb
|
||
);
|
||
|
||
return (0.2126 * $linear[0]) + (0.7152 * $linear[1]) + (0.0722 * $linear[2]);
|
||
}
|
||
|
||
function cc_test_assessor_contrast_text_hex(string $hex): string
|
||
{
|
||
static $darkText = '#212529';
|
||
static $darkLuminance = null;
|
||
|
||
if ($darkLuminance === null) {
|
||
$darkLuminance = cc_test_hex_relative_luminance($darkText);
|
||
}
|
||
|
||
$luminance = cc_test_hex_relative_luminance($hex);
|
||
$contrastWithWhite = 1.05 / ($luminance + 0.05);
|
||
$contrastWithDark = ($luminance + 0.05) / ($darkLuminance + 0.05);
|
||
|
||
return $contrastWithDark >= $contrastWithWhite ? $darkText : '#FFFFFF';
|
||
}
|
||
|
||
function cc_test_assessor_css_vars(string $assessorKey, ?string $storedColorHex = null): string
|
||
{
|
||
$hex = cc_test_assessor_effective_color_hex($assessorKey, $storedColorHex);
|
||
$rgb = cc_test_hex_to_rgb_triplet($hex) ?? [108, 117, 125];
|
||
|
||
return '--cc-assessor-accent:' . $hex . '; --cc-assessor-accent-rgb:' . implode(', ', $rgb) . '; --cc-assessor-accent-contrast:' . cc_test_assessor_contrast_text_hex($hex) . ';';
|
||
}
|
||
|
||
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,
|
||
`assigned_at` DATETIME NULL,
|
||
`direccion` TEXT NULL,
|
||
`referencia` TEXT NULL,
|
||
`agencia` VARCHAR(80) NULL,
|
||
`sede_agencia` VARCHAR(120) NULL,
|
||
`sede` VARCHAR(120) NULL,
|
||
`ciudad` VARCHAR(120) NULL,
|
||
`distrito` VARCHAR(120) NULL,
|
||
`dni` VARCHAR(40) NULL,
|
||
`observaciones` TEXT NULL,
|
||
`coordenadas` VARCHAR(255) NULL,
|
||
`producto` VARCHAR(255) NULL,
|
||
`cantidad` VARCHAR(50) NULL,
|
||
`precio` VARCHAR(80) NULL,
|
||
`monto_adelantado` DECIMAL(10,2) NULL,
|
||
`confirmacion_producto` VARCHAR(255) NULL,
|
||
`confirmacion_cantidad` VARCHAR(50) NULL,
|
||
`confirmacion_precio` VARCHAR(80) NULL,
|
||
`confirmacion_producto_extra` VARCHAR(255) NULL,
|
||
`confirmacion_cantidad_extra` VARCHAR(50) NULL,
|
||
`confirmacion_precio_extra` VARCHAR(80) NULL,
|
||
`proxima_llamada_at` DATETIME NULL,
|
||
`fecha_entrega_programada` DATE NULL,
|
||
`numero_cuenta_enviado_at` DATETIME NULL,
|
||
`numero_cuenta_sede_id` VARCHAR(120) NULL,
|
||
`numero_cuenta_dni` VARCHAR(40) NULL,
|
||
`promo_final_evidencia_path` VARCHAR(255) NULL,
|
||
`promo_final_evidencia_subido_at` DATETIME NULL,
|
||
`promo_final_evidencia_subido_por` INT NULL,
|
||
`cancelado_evidencia_path` VARCHAR(255) NULL,
|
||
`cancelado_evidencia_subido_at` DATETIME NULL,
|
||
`cancelado_evidencia_subido_por` INT NULL,
|
||
`eliminado_at` DATETIME NULL,
|
||
`eliminado_por` INT NULL,
|
||
`ruta_contraentrega_pedido_id` INT NULL,
|
||
`ruta_contraentrega_subido_at` DATETIME NULL,
|
||
`ruta_contraentrega_subido_por` INT NULL,
|
||
`pedido_rotulado_pedido_id` INT NULL,
|
||
`pedido_rotulado_subido_at` DATETIME NULL,
|
||
`pedido_rotulado_subido_por` INT 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', 'assigned_at', 'DATETIME NULL AFTER `user_id`');
|
||
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', 'agencia', 'VARCHAR(80) NULL AFTER `referencia`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'sede_agencia', 'VARCHAR(120) NULL AFTER `agencia`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'sede', 'VARCHAR(120) NULL AFTER `sede_agencia`');
|
||
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', 'coordenadas', 'VARCHAR(255) NULL AFTER `observaciones`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'producto', 'VARCHAR(255) NULL AFTER `coordenadas`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'cantidad', 'VARCHAR(50) NULL AFTER `producto`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'precio', 'VARCHAR(80) NULL AFTER `cantidad`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'monto_adelantado', 'DECIMAL(10,2) NULL AFTER `precio`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_producto', 'VARCHAR(255) NULL AFTER `precio`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_cantidad', 'VARCHAR(50) NULL AFTER `confirmacion_producto`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_precio', 'VARCHAR(80) NULL AFTER `confirmacion_cantidad`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_producto_extra', 'VARCHAR(255) NULL AFTER `confirmacion_precio`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_cantidad_extra', 'VARCHAR(50) NULL AFTER `confirmacion_producto_extra`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'confirmacion_precio_extra', 'VARCHAR(80) NULL AFTER `confirmacion_cantidad_extra`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'proxima_llamada_at', 'DATETIME NULL AFTER `confirmacion_precio`');
|
||
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', 'numero_cuenta_enviado_at', 'DATETIME NULL AFTER `fecha_entrega_programada`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'numero_cuenta_sede_id', 'VARCHAR(120) NULL AFTER `numero_cuenta_enviado_at`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'numero_cuenta_dni', 'VARCHAR(40) NULL AFTER `numero_cuenta_sede_id`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'promo_final_evidencia_path', 'VARCHAR(255) NULL AFTER `numero_cuenta_dni`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'promo_final_evidencia_subido_at', 'DATETIME NULL AFTER `promo_final_evidencia_path`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'promo_final_evidencia_subido_por', 'INT NULL AFTER `promo_final_evidencia_subido_at`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'cancelado_evidencia_path', 'VARCHAR(255) NULL AFTER `promo_final_evidencia_subido_por`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'cancelado_evidencia_subido_at', 'DATETIME NULL AFTER `cancelado_evidencia_path`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'cancelado_evidencia_subido_por', 'INT NULL AFTER `cancelado_evidencia_subido_at`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'eliminado_at', 'DATETIME NULL AFTER `cancelado_evidencia_subido_por`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'eliminado_por', 'INT NULL AFTER `eliminado_at`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'ruta_contraentrega_pedido_id', 'INT NULL AFTER `eliminado_por`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'ruta_contraentrega_subido_at', 'DATETIME NULL AFTER `ruta_contraentrega_pedido_id`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'ruta_contraentrega_subido_por', 'INT NULL AFTER `ruta_contraentrega_subido_at`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'pedido_rotulado_pedido_id', 'INT NULL AFTER `ruta_contraentrega_subido_por`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'pedido_rotulado_subido_at', 'DATETIME NULL AFTER `pedido_rotulado_pedido_id`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'pedido_rotulado_subido_por', 'INT NULL AFTER `pedido_rotulado_subido_at`');
|
||
cc_test_ensure_column($pdo, 'callcenter_test_tracking', 'ultima_gestion_at', 'DATETIME NULL AFTER `ruta_contraentrega_subido_por`');
|
||
|
||
$stmtAssignedBackfill = $pdo->query("SELECT id FROM callcenter_test_tracking WHERE assigned_at IS NULL AND user_id IS NOT NULL LIMIT 1");
|
||
if ($stmtAssignedBackfill && $stmtAssignedBackfill->fetchColumn()) {
|
||
$pdo->exec("UPDATE callcenter_test_tracking SET assigned_at = created_at, updated_at = updated_at WHERE assigned_at IS NULL AND user_id IS NOT NULL");
|
||
}
|
||
|
||
$checked = true;
|
||
}
|
||
|
||
function cc_test_default_assessor_keys(): array
|
||
{
|
||
return ['KARINA', 'ESTEFANYA', 'CARMEN'];
|
||
}
|
||
|
||
function cc_test_fetch_assessors(PDO $pdo, array $allowedNames = []): array
|
||
{
|
||
if (empty($allowedNames)) {
|
||
$allowedNames = cc_test_default_assessor_keys();
|
||
}
|
||
|
||
$hasColorColumn = cc_test_column_exists($pdo, 'users', 'color_hex');
|
||
$selectColorColumn = $hasColorColumn ? ', color_hex' : ', NULL AS color_hex';
|
||
$stmt = $pdo->prepare("SELECT id, username, nombre_asesor{$selectColorColumn} FROM users WHERE role = 'Asesor'");
|
||
$stmt->execute();
|
||
|
||
$allowedLookup = array_fill_keys(array_map('strtoupper', $allowedNames), true);
|
||
$byKey = [];
|
||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||
$key = trim(mb_strtoupper((string) ($row['nombre_asesor'] ?: $row['username'] ?: '')));
|
||
$key = preg_replace('/\s+/', ' ', $key) ?? $key;
|
||
if ($key === '' || (!empty($allowedLookup) && !isset($allowedLookup[$key]))) {
|
||
continue;
|
||
}
|
||
|
||
$byKey[$key] = [
|
||
'id' => (int) $row['id'],
|
||
'label' => trim((string) ($row['nombre_asesor'] ?: $row['username'] ?: ('Asesor #' . (int) $row['id']))),
|
||
'color_hex' => cc_test_normalize_hex_color($row['color_hex'] ?? null),
|
||
];
|
||
}
|
||
|
||
return $byKey;
|
||
}
|
||
|
||
function cc_test_normalize_user_key(?string $value): string
|
||
{
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
|
||
$value = preg_replace('/\s+/', ' ', $value) ?? $value;
|
||
|
||
return mb_strtoupper($value);
|
||
}
|
||
|
||
function cc_test_allowed_module_user_keys(): array
|
||
{
|
||
return cc_test_default_assessor_keys();
|
||
}
|
||
|
||
function cc_test_is_allowed_module_user(?string $role = null, ?string $username = null, ?string $nombreAsesor = null): bool
|
||
{
|
||
$role = trim((string) $role);
|
||
if (in_array($role, ['Administrador', 'admin'], true)) {
|
||
return true;
|
||
}
|
||
|
||
$allowedLookup = array_fill_keys(cc_test_allowed_module_user_keys(), true);
|
||
foreach ([$username, $nombreAsesor] as $candidate) {
|
||
$key = cc_test_normalize_user_key($candidate);
|
||
if ($key !== '' && isset($allowedLookup[$key])) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function cc_test_current_user_can_access_module(?PDO $pdo = null): bool
|
||
{
|
||
$role = $_SESSION['user_role'] ?? '';
|
||
$username = $_SESSION['username'] ?? '';
|
||
$nombreAsesor = null;
|
||
|
||
if ($pdo instanceof PDO && !empty($_SESSION['user_id']) && !in_array($role, ['Administrador', 'admin'], true)) {
|
||
try {
|
||
$stmt = $pdo->prepare('SELECT username, nombre_asesor FROM users WHERE id = ? LIMIT 1');
|
||
$stmt->execute([$_SESSION['user_id']]);
|
||
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||
$username = $row['username'] ?? $username;
|
||
$nombreAsesor = $row['nombre_asesor'] ?? null;
|
||
}
|
||
} catch (Throwable $exception) {
|
||
// Fall back to the session values if the lookup fails.
|
||
}
|
||
}
|
||
|
||
return cc_test_is_allowed_module_user($role, $username, $nombreAsesor);
|
||
}
|
||
|
||
function cc_test_upsert_assignee(PDO $pdo, string $sourceKey, ?int $userId): void
|
||
{
|
||
cc_test_ensure_tracking_table($pdo);
|
||
|
||
$assignedAt = $userId === null ? null : (new DateTimeImmutable('now'))->format('Y-m-d H:i:s');
|
||
|
||
$stmt = $pdo->prepare("INSERT INTO callcenter_test_tracking (source_key, user_id, assigned_at, ultima_gestion_at)
|
||
VALUES (:source_key, :user_id, :assigned_at, CURRENT_TIMESTAMP)
|
||
ON DUPLICATE KEY UPDATE
|
||
user_id = VALUES(user_id),
|
||
assigned_at = CASE
|
||
WHEN VALUES(user_id) IS NULL THEN NULL
|
||
WHEN assigned_at IS NULL OR user_id IS NULL OR user_id <> VALUES(user_id) THEN VALUES(assigned_at)
|
||
ELSE assigned_at
|
||
END,
|
||
ultima_gestion_at = VALUES(ultima_gestion_at),
|
||
updated_at = CURRENT_TIMESTAMP");
|
||
|
||
$stmt->bindValue(':source_key', $sourceKey, PDO::PARAM_STR);
|
||
if ($userId === null) {
|
||
$stmt->bindValue(':user_id', null, PDO::PARAM_NULL);
|
||
$stmt->bindValue(':assigned_at', null, PDO::PARAM_NULL);
|
||
} else {
|
||
$stmt->bindValue(':user_id', $userId, PDO::PARAM_INT);
|
||
$stmt->bindValue(':assigned_at', $assignedAt, PDO::PARAM_STR);
|
||
}
|
||
|
||
$stmt->execute();
|
||
}
|
||
|
||
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_normalize_state(string $estado): string
|
||
{
|
||
$estado = trim($estado);
|
||
|
||
return match ($estado) {
|
||
'CONFIRMADO CONTRAENTREGA FECHA', 'CONFIRMADO FECHA', 'CONTRAENTREGA CONFIRMADO' => 'CONFIRMADO CONTRAENTREGA',
|
||
'ENVIO REPETIDO' => 'REPETIDO',
|
||
default => $estado,
|
||
};
|
||
}
|
||
|
||
function cc_test_valid_states(): array
|
||
{
|
||
return [
|
||
'POR LLAMAR',
|
||
'DEVOLVER LLAMADA',
|
||
'OBSERVADO',
|
||
'SE ENVIO NUMERO DE CUENTA',
|
||
'CONFIRMADO CONTRAENTREGA',
|
||
'CONFIRMADO ENVIO',
|
||
'CANCELADO',
|
||
'REPETIDO',
|
||
];
|
||
}
|
||
|
||
if (!function_exists('cc_test_open_states')) {
|
||
function cc_test_open_states(): array
|
||
{
|
||
return ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO'];
|
||
}
|
||
}
|
||
|
||
function cc_test_confirmed_states(): array
|
||
{
|
||
return ['CONFIRMADO CONTRAENTREGA', 'CONFIRMADO ENVIO'];
|
||
}
|
||
|
||
if (!function_exists('cc_test_closed_states')) {
|
||
function cc_test_closed_states(): array
|
||
{
|
||
return ['CANCELADO', 'REPETIDO'];
|
||
}
|
||
}
|
||
|
||
function cc_test_requires_delivery_date(string $estado): bool
|
||
{
|
||
return cc_test_normalize_state($estado) === 'CONFIRMADO CONTRAENTREGA';
|
||
}
|
||
|
||
function cc_test_requires_shipping_details(string $estado): bool
|
||
{
|
||
return cc_test_normalize_state($estado) === 'CONFIRMADO ENVIO';
|
||
}
|
||
|
||
function cc_test_requires_account_number_fields(string $estado): bool
|
||
{
|
||
return cc_test_normalize_state($estado) === 'SE ENVIO NUMERO DE CUENTA';
|
||
}
|
||
|
||
function cc_test_pending_logistica_destination(array $order): ?string
|
||
{
|
||
$estado = cc_test_normalize_state((string) ($order['estado'] ?? ''));
|
||
|
||
if ($estado === 'CONFIRMADO CONTRAENTREGA') {
|
||
return !empty($order['ruta_contraentrega_pedido_id']) && (int) $order['ruta_contraentrega_pedido_id'] > 0
|
||
? null
|
||
: 'ruta';
|
||
}
|
||
|
||
if ($estado === 'CONFIRMADO ENVIO') {
|
||
return !empty($order['pedido_rotulado_pedido_id']) && (int) $order['pedido_rotulado_pedido_id'] > 0
|
||
? null
|
||
: 'rotulado';
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function cc_test_has_pending_logistica_upload(array $order): bool
|
||
{
|
||
return cc_test_pending_logistica_destination($order) !== null;
|
||
}
|
||
|
||
function cc_test_pending_logistica_label(array $order): ?string
|
||
{
|
||
return cc_test_has_pending_logistica_upload($order) ? 'PENDIENTE A SUBIR' : null;
|
||
}
|
||
|
||
function cc_test_account_followup_semaforo(?string $value): ?array
|
||
{
|
||
$date = cc_test_parse_datetime($value);
|
||
if (!$date) {
|
||
return null;
|
||
}
|
||
|
||
$now = new DateTimeImmutable('now');
|
||
$days = (int) $date->diff($now)->format('%a');
|
||
if ($date > $now) {
|
||
$days = 0;
|
||
}
|
||
|
||
if ($days <= 1) {
|
||
return [
|
||
'class' => 'bg-success-subtle text-success-emphasis',
|
||
'label' => 'Verde',
|
||
'range' => '0–1 días',
|
||
'days' => $days,
|
||
'description' => 'Aún está en ventana de seguimiento amable.',
|
||
];
|
||
}
|
||
|
||
if ($days === 2) {
|
||
return [
|
||
'class' => 'bg-warning-subtle text-warning-emphasis',
|
||
'label' => 'Amarillo',
|
||
'range' => '2 días',
|
||
'days' => $days,
|
||
'description' => 'Ya conviene insistir con llamada o WhatsApp.',
|
||
];
|
||
}
|
||
|
||
return [
|
||
'class' => 'bg-danger-subtle text-danger-emphasis',
|
||
'label' => 'Rojo',
|
||
'range' => '3+ días',
|
||
'days' => $days,
|
||
'description' => 'Necesita seguimiento urgente para no perder el pedido.',
|
||
];
|
||
}
|
||
|
||
function cc_test_followup_tracking_states(): array
|
||
{
|
||
return ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO', 'SE ENVIO NUMERO DE CUENTA'];
|
||
}
|
||
|
||
function cc_test_followup_parse_datetime(?string $value): ?DateTimeImmutable
|
||
{
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
$formats = [
|
||
'Y-m-d\TH:i:sP',
|
||
'Y-m-d\TH:i:s',
|
||
'Y-m-d\TH:i',
|
||
'Y-m-d H:i:s',
|
||
'Y-m-d H:i',
|
||
DateTimeInterface::ATOM,
|
||
'Y-m-d',
|
||
'd/m/Y H:i:s',
|
||
'd/m/Y H:i',
|
||
'd/m/Y',
|
||
];
|
||
|
||
foreach ($formats as $format) {
|
||
try {
|
||
$date = DateTimeImmutable::createFromFormat($format, $value);
|
||
if ($date instanceof DateTimeImmutable) {
|
||
return $date;
|
||
}
|
||
} catch (Throwable $exception) {
|
||
// Intentar el siguiente formato.
|
||
}
|
||
}
|
||
|
||
try {
|
||
return new DateTimeImmutable($value);
|
||
} catch (Throwable $exception) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function cc_test_followup_started_at(array $order): ?DateTimeImmutable
|
||
{
|
||
foreach (['drive_imported_at', 'first_seen_at', 'import_id'] as $field) {
|
||
$date = cc_test_followup_parse_datetime($order[$field] ?? null);
|
||
if ($date instanceof DateTimeImmutable) {
|
||
return $date;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function cc_test_followup_day_info(array $order): ?array
|
||
{
|
||
$estado = cc_test_normalize_state((string) ($order['estado'] ?? ''));
|
||
if (!in_array($estado, cc_test_followup_tracking_states(), true)) {
|
||
return null;
|
||
}
|
||
|
||
$startedAt = cc_test_followup_started_at($order);
|
||
if (!$startedAt) {
|
||
return null;
|
||
}
|
||
|
||
$today = new DateTimeImmutable('today', $startedAt->getTimezone());
|
||
$startedDay = $startedAt->setTime(0, 0, 0);
|
||
$daysElapsed = (int) $startedDay->diff($today)->format('%a');
|
||
if ($startedDay > $today) {
|
||
$daysElapsed = 0;
|
||
}
|
||
|
||
$dayNumber = $daysElapsed + 1;
|
||
$badgeClass = 'bg-primary-subtle text-primary-emphasis border';
|
||
$textClass = 'text-primary-emphasis';
|
||
$description = 'Seguimiento dentro de la ventana óptima.';
|
||
$noticeText = '';
|
||
$rowNoticeClass = '';
|
||
|
||
if ($dayNumber === 2) {
|
||
$badgeClass = 'bg-info-subtle text-info-emphasis border';
|
||
$textClass = 'text-info-emphasis';
|
||
$description = 'Aún está dentro de los 3 días de seguimiento.';
|
||
} elseif ($dayNumber === 3) {
|
||
$badgeClass = 'bg-warning-subtle text-warning-emphasis border';
|
||
$textClass = 'text-warning-emphasis';
|
||
$description = 'Último día de seguimiento antes de Promo Final.';
|
||
$noticeText = 'Último día de seguimiento. Mañana debe pasar a Promo Final con imagen de sustento.';
|
||
$rowNoticeClass = 'text-warning-emphasis';
|
||
} elseif ($dayNumber >= 4) {
|
||
$badgeClass = 'bg-danger-subtle text-danger-emphasis border';
|
||
$textClass = 'text-danger-emphasis';
|
||
$description = 'Ya debe pasar a Promo Final.';
|
||
$noticeText = 'Este pedido lleva ' . $dayNumber . ' días y debe moverse a Promo Final con imagen de sustento.';
|
||
$rowNoticeClass = 'text-danger-emphasis';
|
||
}
|
||
|
||
return [
|
||
'day_number' => $dayNumber,
|
||
'display_label' => $dayNumber >= 4 ? 'Día 4+' : 'Día ' . $dayNumber,
|
||
'compact_label' => $dayNumber >= 4 ? '4+' : (string) $dayNumber,
|
||
'badge_class' => $badgeClass,
|
||
'text_class' => $textClass,
|
||
'description' => $description,
|
||
'notice_text' => $noticeText,
|
||
'row_notice_class' => $rowNoticeClass,
|
||
'started_at_label' => $startedAt->format('d/m/Y'),
|
||
'is_promo_final' => $dayNumber >= 4,
|
||
];
|
||
}
|
||
|
||
function cc_test_state_label(string $estado): string
|
||
{
|
||
return match (cc_test_normalize_state($estado)) {
|
||
'SE ENVIO NUMERO DE CUENTA' => 'SE ENVIÓ NÚMERO DE CUENTA',
|
||
'CONFIRMADO CONTRAENTREGA' => 'CONFIRMADO CONTRAENTREGA',
|
||
'CONFIRMADO ENVIO' => 'CONFIRMADO ENVIO',
|
||
'REPETIDO' => 'REPETIDO',
|
||
default => $estado,
|
||
};
|
||
}
|
||
|
||
function cc_test_normalize_contraentrega_coordinates(string $value): ?string
|
||
{
|
||
$value = trim($value);
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
if (!preg_match('/^\s*(-?\d{1,2}\.\d{6,})\s*,\s*(-?\d{1,3}\.\d{6,})\s*$/', $value, $matches)) {
|
||
return null;
|
||
}
|
||
|
||
$latitud = (float) $matches[1];
|
||
$longitud = (float) $matches[2];
|
||
|
||
if ($latitud < -90 || $latitud > 90 || $longitud < -180 || $longitud > 180) {
|
||
return null;
|
||
}
|
||
|
||
return $matches[1] . ', ' . $matches[2];
|
||
}
|
||
|
||
function cc_test_parse_quantity(?string $value): ?int
|
||
{
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
if (!preg_match('/-?\d+/', $value, $matches)) {
|
||
return null;
|
||
}
|
||
|
||
$cantidad = (int) $matches[0];
|
||
if ($cantidad <= 0) {
|
||
return null;
|
||
}
|
||
|
||
return $cantidad;
|
||
}
|
||
|
||
function cc_test_parse_amount(?string $value): ?float
|
||
{
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
$value = str_ireplace(['S/', 'USD', '$'], '', $value);
|
||
$value = preg_replace('/\s+/', '', $value) ?? $value;
|
||
|
||
if (strpos($value, ',') !== false && strpos($value, '.') === false) {
|
||
$value = str_replace(',', '.', $value);
|
||
} else {
|
||
$value = str_replace(',', '', $value);
|
||
}
|
||
|
||
$value = preg_replace('/[^0-9.\-]/', '', $value) ?? $value;
|
||
if ($value === '' || !is_numeric($value)) {
|
||
return null;
|
||
}
|
||
|
||
return round((float) $value, 2);
|
||
}
|
||
|
||
function cc_test_shipping_agency_options(): array
|
||
{
|
||
return ['SHALOM', 'OLVA', 'OTROS'];
|
||
}
|
||
|
||
function cc_test_normalize_shipping_agency(?string $value): ?string
|
||
{
|
||
$value = strtoupper(trim((string) $value));
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
return match ($value) {
|
||
'OLVA COURIER', 'OLVA' => 'OLVA',
|
||
'SHALOM' => 'SHALOM',
|
||
'OTRO', 'OTROS', 'OTRAS' => 'OTROS',
|
||
default => $value,
|
||
};
|
||
}
|
||
|
||
function cc_test_fetch_shalom_sedes(PDO $pdo): array
|
||
{
|
||
if (!cc_test_table_exists($pdo, 'sedes_shalom')) {
|
||
return [];
|
||
}
|
||
|
||
$stmt = $pdo->query('SELECT nombre_sede FROM sedes_shalom ORDER BY nombre_sede ASC');
|
||
$sedes = $stmt ? $stmt->fetchAll(PDO::FETCH_COLUMN) : [];
|
||
|
||
return is_array($sedes) ? array_values(array_filter(array_map('strval', $sedes), static fn ($value) => trim($value) !== '')) : [];
|
||
}
|
||
|
||
function cc_test_row_background_style(string $estado): string
|
||
{
|
||
$color = match (cc_test_normalize_state($estado)) {
|
||
// Blanco
|
||
'POR LLAMAR', 'DEVOLVER LLAMADA' => '#ffffff',
|
||
|
||
// Amarillo
|
||
'OBSERVADO' => '#FFFF00',
|
||
|
||
// Verde brillante
|
||
'SE ENVIO NUMERO DE CUENTA' => '#00FF00',
|
||
|
||
// Naranja
|
||
'CONFIRMADO CONTRAENTREGA' => '#ffb74d',
|
||
|
||
// Verde Google
|
||
'CONFIRMADO ENVIO' => '#34A853',
|
||
|
||
// Rojo
|
||
'CANCELADO' => '#FF0000',
|
||
|
||
// Plomo
|
||
'REPETIDO' => '#CCCCCC',
|
||
|
||
default => '#ffffff',
|
||
};
|
||
|
||
// We set a CSS variable so we can paint the entire row (all <td> cells).
|
||
// Bootstrap/table styles often apply opaque backgrounds to <td>, which can hide
|
||
// the <tr> background. A CSS rule below will apply this variable to every cell.
|
||
return '--cc-row-bg:' . $color . ';';
|
||
}
|
||
|
||
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_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);
|
||
}
|