Manual code backup after billing restore 2026-05-26

This commit is contained in:
Flatlogic Support 2026-05-26 15:51:40 +00:00
parent 10a0221760
commit a630d16063
11 changed files with 1734 additions and 70 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
node_modules/
*/node_modules/
*/build/
assets/uploads/

1349
call_center_pro.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,14 @@ $departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega
$pageTitle = 'Call Center de Prueba | Bandejas de gestión';
$pageDescription = 'Bandejas diarias de Call Center con estados reales, próxima llamada, fecha de entrega, historial y edición de datos del pedido importado desde Drive.';
if (!cc_test_current_user_can_access_module(db())) {
http_response_code(403);
require_once 'layout_header.php';
echo "<div class='container py-5'><div class='alert alert-danger mb-0'>Acceso denegado.</div></div>";
require_once 'layout_footer.php';
exit();
}
function cc_test_parse_datetime(?string $value): ?DateTimeImmutable
{
$value = trim((string) $value);
@ -121,11 +129,14 @@ if (!isset($allowedViews[$view])) {
}
$errorMessage = null;
$noticeMessage = null;
$assessors = [];
$orders = [];
$visibleOrders = [];
$modalsHtml = [];
$totalRows = 0;
$loadLimit = 10;
$startRow = 5552;
$catalogoProductos = [];
$stats = [
'total' => 0,
@ -141,6 +152,7 @@ try {
$pdo = db();
cc_test_ensure_tracking_table($pdo);
cc_test_ensure_historial_llamadas_table($pdo);
$assessors = cc_test_fetch_assessors($pdo);
try {
$stmtProductos = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
@ -149,13 +161,36 @@ try {
$catalogoProductos = [];
}
$preview = drive_test_fetch_orders($loadLimit);
$preview = drive_test_fetch_orders($loadLimit, $startRow);
$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);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'assign_assessor') {
$sourceKey = trim((string) ($_POST['source_key'] ?? ''));
$targetValue = trim((string) ($_POST['target_assessor'] ?? ''));
if ($sourceKey === '' || !preg_match('/^[a-f0-9]{40}$/', $sourceKey)) {
throw new RuntimeException('Pedido de prueba inválido para asignar.');
}
$targetKey = trim(mb_strtoupper($targetValue));
$targetKey = preg_replace('/\s+/', ' ', $targetKey) ?? $targetKey;
$selectedAssessor = $targetKey !== '' ? ($assessors[$targetKey] ?? null) : null;
if ($targetKey !== '' && !$selectedAssessor) {
throw new RuntimeException('No se encontró la asesora seleccionada.');
}
cc_test_upsert_assignee($pdo, $sourceKey, $selectedAssessor ? (int) $selectedAssessor['id'] : null);
$noticeMessage = $selectedAssessor
? 'Pedido asignado a ' . $selectedAssessor['label'] . '.'
: 'Pedido dejado sin asignar.';
$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)) {
@ -244,35 +279,31 @@ require_once 'layout_header.php';
<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">Ahora el panel trabaja por <strong>bandejas</strong>: <strong>Nuevos de hoy</strong>, <strong>Pendientes de hoy</strong>, <strong>Seguimiento</strong>, <strong>Confirmados</strong>, <strong>Observados</strong> y <strong>Cerrados</strong>. Así no se mezcla todo cuando entran pedidos diarios.</p>
<div class="d-flex flex-wrap gap-2 mt-3">
<a href="gestiones_callcenter.php" class="btn btn-sm <?php echo $view !== 'todos' ? 'btn-primary' : 'btn-outline-primary'; ?>">Bandeja principal</a>
<a href="call_center_pro.php" class="btn btn-sm btn-outline-dark">Call Center Pro</a>
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Carga operativa: últimos <?php echo (int) $loadLimit; ?> registros</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Extracción: desde fila <?php echo (int) $startRow; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<a href="test_importar_drive.php" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>
</div>
</section>
<?php if ($noticeMessage !== null): ?>
<section class="alert alert-success" role="alert">
<?php echo htmlspecialchars($noticeMessage); ?>
</section>
<?php endif; ?>
<?php if ($errorMessage !== null): ?>
<section class="alert alert-danger" role="alert">
<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 gestionas cada cliente con estado, próxima llamada y observaciones internas. Los casos abiertos siguen apareciendo en <strong>Pendientes de hoy</strong>, <strong>Seguimiento</strong> o <strong>Observados</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, departamento, provincia, distrito, DNI y observaciones</div>
</div>
</div>
</section>
<section class="row g-3 mb-4">
<div class="col-md-6 col-xl-2">
<a href="?view=pendientes_hoy" class="text-decoration-none">
@ -351,7 +382,6 @@ require_once 'layout_header.php';
<div>
<h2 class="h5 fw-bold mb-1"><?php echo htmlspecialchars($allowedViews[$view]); ?></h2>
<p class="text-muted small mb-1">Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega, Confirmado envío, Cancelado y Repetido.</p>
<p class="small text-primary mb-0"><i class="bi bi-phone"></i> El botón <strong>Llamar / AirDroid</strong> registra el intento, copia el número y muestra la ayuda visual para pegar en tu app de AirDroid.</p>
</div>
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
</div>
@ -436,6 +466,20 @@ require_once 'layout_header.php';
</td>
<td class="text-center">
<div class="d-flex flex-column gap-2 align-items-stretch">
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
<form method="post" class="border rounded-3 bg-light p-2 text-start shadow-sm">
<input type="hidden" name="action" value="assign_assessor">
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="small text-uppercase text-muted fw-semibold mb-1">Asignar a asesora</div>
<select name="target_assessor" class="form-select form-select-sm mb-2">
<option value=""<?php echo empty($order['user_id']) ? ' selected' : ''; ?>>Sin asignar</option>
<?php foreach ($assessors as $assessorKey => $assessor): ?>
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo ((int) ($order['user_id'] ?? 0) === (int) ($assessor['id'] ?? 0)) ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary w-100">Asignar pedido</button>
</form>
<?php endif; ?>
<?php if (!empty($order['telefono_url'])): ?>
<button
type="button"
@ -523,8 +567,8 @@ require_once 'layout_header.php';
</div>
<span class="badge bg-info-subtle text-info-emphasis">Producto del sistema, cantidad y precio</span>
</div>
<div class="row g-3">
<div class="col-12">
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-6">
<label for="confirmacion_producto-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Producto</label>
<select class="form-select w-100" id="confirmacion_producto-<?php echo htmlspecialchars($order['source_key']); ?>" title="Seleccione un producto del sistema">
<option value="">Seleccione un producto del sistema</option>
@ -532,7 +576,6 @@ require_once 'layout_header.php';
<option value="<?php echo htmlspecialchars((string) ($catalogoProducto['nombre'] ?? '')); ?>" <?php echo ((string) ($order['confirmacion_producto'] ?? '') === (string) ($catalogoProducto['nombre'] ?? '')) ? 'selected' : ''; ?>><?php echo htmlspecialchars((string) ($catalogoProducto['nombre'] ?? '')); ?></option>
<?php endforeach; ?>
</select>
<div class="form-text text-break" id="confirmacion_producto_resumen-<?php echo htmlspecialchars($order['source_key']); ?>">Producto seleccionado: <?php echo htmlspecialchars((string) ($order['confirmacion_producto'] ?? 'Sin producto seleccionado')); ?></div>
</div>
<div class="col-sm-6 col-lg-3">
<label for="confirmacion_cantidad-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Cantidad</label>
@ -543,6 +586,34 @@ require_once 'layout_header.php';
<input type="text" class="form-control" id="confirmacion_precio-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['confirmacion_precio'] ?? '')); ?>" placeholder="S/ 0.00" inputmode="decimal">
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="button" class="btn btn-outline-secondary btn-sm js-toggle-confirmacion-extra" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">Agregar producto adicional</button>
</div>
<div class="border rounded-3 bg-light p-3 mt-3 d-none js-confirmacion-extra-block" id="confirmacion_extra_block-<?php echo htmlspecialchars($order['source_key']); ?>">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div class="fw-semibold">Producto adicional distinto</div>
<button type="button" class="btn btn-link text-danger text-decoration-none p-0 js-remove-confirmacion-extra" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">Quitar</button>
</div>
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-6">
<label for="confirmacion_producto_extra-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Producto adicional</label>
<select class="form-select w-100" id="confirmacion_producto_extra-<?php echo htmlspecialchars($order['source_key']); ?>" title="Seleccione un producto adicional">
<option value="">Seleccione un producto adicional</option>
<?php foreach ($catalogoProductos as $catalogoProducto): ?>
<option value="<?php echo htmlspecialchars((string) ($catalogoProducto['nombre'] ?? '')); ?>" <?php echo ((string) ($order['confirmacion_producto_extra'] ?? '') === (string) ($catalogoProducto['nombre'] ?? '')) ? 'selected' : ''; ?>><?php echo htmlspecialchars((string) ($catalogoProducto['nombre'] ?? '')); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-6 col-lg-3">
<label for="confirmacion_cantidad_extra-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Cantidad</label>
<input type="text" class="form-control" id="confirmacion_cantidad_extra-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['confirmacion_cantidad_extra'] ?? '')); ?>" placeholder="Cantidad">
</div>
<div class="col-sm-6 col-lg-3">
<label for="confirmacion_precio_extra-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">Precio</label>
<input type="text" class="form-control" id="confirmacion_precio_extra-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars((string) ($order['confirmacion_precio_extra'] ?? '')); ?>" placeholder="S/ 0.00" inputmode="decimal">
</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
@ -970,17 +1041,56 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('select[id^="confirmacion_producto-"]').forEach(select => {
const sourceKey = select.id.replace('confirmacion_producto-', '');
const resumen = document.getElementById('confirmacion_producto_resumen-' + sourceKey);
const syncResumen = () => {
const syncTitle = () => {
const selectedOption = select.selectedOptions && select.selectedOptions[0] ? select.selectedOptions[0] : null;
const label = selectedOption && selectedOption.textContent ? selectedOption.textContent.trim() : (select.value || 'Sin producto seleccionado');
if (resumen) {
resumen.textContent = 'Producto seleccionado: ' + label;
}
select.title = label;
};
syncResumen();
select.addEventListener('change', syncResumen);
syncTitle();
select.addEventListener('change', syncTitle);
});
document.querySelectorAll('.js-toggle-confirmacion-extra').forEach(button => {
button.addEventListener('click', () => {
const sourceKey = button.dataset.sourceKey || '';
const block = document.getElementById('confirmacion_extra_block-' + sourceKey);
if (!block) return;
block.classList.remove('d-none');
button.classList.add('d-none');
});
});
document.querySelectorAll('.js-remove-confirmacion-extra').forEach(button => {
button.addEventListener('click', () => {
const sourceKey = button.dataset.sourceKey || '';
const block = document.getElementById('confirmacion_extra_block-' + sourceKey);
const toggle = document.querySelector('.js-toggle-confirmacion-extra[data-source-key="' + sourceKey + '"]');
if (block) {
block.classList.add('d-none');
const productoExtra = document.getElementById('confirmacion_producto_extra-' + sourceKey);
const cantidadExtra = document.getElementById('confirmacion_cantidad_extra-' + sourceKey);
const precioExtra = document.getElementById('confirmacion_precio_extra-' + sourceKey);
if (productoExtra) productoExtra.value = '';
if (cantidadExtra) cantidadExtra.value = '';
if (precioExtra) precioExtra.value = '';
}
if (toggle) {
toggle.classList.remove('d-none');
}
});
});
document.querySelectorAll('.js-confirmacion-extra-block').forEach(block => {
const sourceKey = block.id.replace('confirmacion_extra_block-', '');
const productoExtra = document.getElementById('confirmacion_producto_extra-' + sourceKey);
const cantidadExtra = document.getElementById('confirmacion_cantidad_extra-' + sourceKey);
const precioExtra = document.getElementById('confirmacion_precio_extra-' + sourceKey);
const toggle = document.querySelector('.js-toggle-confirmacion-extra[data-source-key="' + sourceKey + '"]');
const hasValue = [productoExtra, cantidadExtra, precioExtra].some(input => (input?.value || '').trim() !== '');
if (hasValue) {
block.classList.remove('d-none');
if (toggle) toggle.classList.add('d-none');
}
});
});
@ -1005,7 +1115,10 @@ function guardarGestion(sourceKey, trigger) {
precio: document.getElementById('precio-' + sourceKey)?.value || '',
confirmacion_producto: document.getElementById('confirmacion_producto-' + sourceKey)?.value || '',
confirmacion_cantidad: document.getElementById('confirmacion_cantidad-' + sourceKey)?.value || '',
confirmacion_precio: document.getElementById('confirmacion_precio-' + sourceKey)?.value || ''
confirmacion_precio: document.getElementById('confirmacion_precio-' + sourceKey)?.value || '',
confirmacion_producto_extra: document.getElementById('confirmacion_producto_extra-' + sourceKey)?.value || '',
confirmacion_cantidad_extra: document.getElementById('confirmacion_cantidad_extra-' + sourceKey)?.value || '',
confirmacion_precio_extra: document.getElementById('confirmacion_precio_extra-' + sourceKey)?.value || ''
});
if (trigger) {
@ -1218,6 +1331,10 @@ if (airDroidCopyButton) {
});
});
}
window.setInterval(function () {
window.location.reload();
}, 600000);
</script>
<?php require_once 'layout_footer.php'; ?>

View File

@ -45,6 +45,9 @@ function cc_test_ensure_tracking_table(PDO $pdo): void
`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,
@ -72,6 +75,9 @@ function cc_test_ensure_tracking_table(PDO $pdo): void
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`');
@ -80,6 +86,107 @@ function cc_test_ensure_tracking_table(PDO $pdo): void
$checked = true;
}
function cc_test_fetch_assessors(PDO $pdo, array $allowedNames = ['KARINA', 'ESTEFANYA']): array
{
$stmt = $pdo->prepare("SELECT id, username, nombre_asesor 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']))),
];
}
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 ['KARINA', 'ESTEFANYA'];
}
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);
$stmt = $pdo->prepare("INSERT INTO callcenter_test_tracking (source_key, user_id, ultima_gestion_at)
VALUES (:source_key, :user_id, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
user_id = VALUES(user_id),
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);
} else {
$stmt->bindValue(':user_id', $userId, PDO::PARAM_INT);
}
$stmt->execute();
}
function cc_test_ensure_historial_llamadas_table(PDO $pdo): void
{
static $checked = false;
@ -127,9 +234,11 @@ function cc_test_valid_states(): array
];
}
function cc_test_open_states(): array
{
return ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO'];
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
@ -137,9 +246,11 @@ function cc_test_confirmed_states(): array
return ['CONFIRMADO CONTRAENTREGA', 'CONFIRMADO ENVIO'];
}
function cc_test_closed_states(): array
{
return ['CANCELADO', 'REPETIDO'];
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
@ -200,17 +311,19 @@ function cc_test_state_label(string $estado): string
};
}
function cc_test_table_exists(PDO $pdo, string $tableName): bool
{
static $cache = [];
if (array_key_exists($tableName, $cache)) {
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];
}
$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

View File

@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/callcenter_test_helpers.php';
require_once __DIR__ . '/drive_test_orders.php';
function cc_test_allowed_states(): array
@ -15,14 +16,18 @@ function cc_test_allowed_states(): array
];
}
function cc_test_open_states(): array
{
return ['POR LLAMAR', 'NO CONTESTA', 'DEVOLVER LLAMADA', 'REPROGRAMADO'];
if (!function_exists('cc_test_open_states')) {
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'];
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
@ -105,19 +110,21 @@ function cc_test_format_datetime_local(?string $value): string
}
}
function cc_test_table_exists(PDO $pdo, string $tableName): bool
{
static $cache = [];
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();
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

View File

@ -22,7 +22,7 @@ function drive_test_get_cell(array $row, array $indexes, array $aliases, string
return $default;
}
function drive_test_fetch_orders(int $limit = 10): array
function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552): array
{
$credentialsPath = __DIR__ . '/../google_credentials.json';
$spreadsheetId = '1SSmQuR9quxeQbMKNMDkRe8-n1gU7WuEfsFaJ3WKFO-c';
@ -59,7 +59,16 @@ function drive_test_fetch_orders(int $limit = 10): array
}
$dataRows = array_slice($values, 1);
$previewRows = array_reverse(array_slice($dataRows, -$limit));
$startIndex = max(0, $startRow - 2);
if ($startIndex > 0) {
$dataRows = array_slice($dataRows, $startIndex);
}
if ($limit > 0) {
$previewRows = array_reverse(array_slice($dataRows, -$limit));
} else {
$previewRows = array_reverse($dataRows);
}
$orders = [];
foreach ($previewRows as $row) {
@ -122,7 +131,7 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array
cc_test_ensure_tracking_table($pdo);
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, observaciones, producto, cantidad, precio, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, user_id, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, observaciones, producto, cantidad, precio, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
$stmt->execute($sourceKeys);
$tracking = [];
@ -141,6 +150,7 @@ function drive_test_merge_tracking(array $orders, array $tracking): array
$current = $tracking[$order['source_key']] ?? null;
$order['estado'] = $current['estado'] ?? 'POR LLAMAR';
$order['nota_seguimiento'] = $current['nota_seguimiento'] ?? '';
$order['user_id'] = array_key_exists('user_id', (array) $current) ? ($current['user_id'] !== null ? (int) $current['user_id'] : null) : null;
$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;

View File

@ -10,7 +10,29 @@ if (!isset($_SESSION['user_id'])) {
exit();
}
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/callcenter_test_helpers.php';
$userRole = $_SESSION['user_role'] ?? '';
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
$currentUserUsername = $_SESSION['username'] ?? '';
$currentUserNombreAsesor = null;
if ($currentUserId > 0 && !in_array($userRole, ['Administrador', 'admin'], true)) {
try {
$pdo = db();
$stmt = $pdo->prepare('SELECT username, nombre_asesor FROM users WHERE id = ? LIMIT 1');
$stmt->execute([$currentUserId]);
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$currentUserUsername = $row['username'] ?? $currentUserUsername;
$currentUserNombreAsesor = $row['nombre_asesor'] ?? null;
}
} catch (Throwable $exception) {
// Use the session values as a fallback.
}
}
$canAccessTestModule = cc_test_is_allowed_module_user($userRole, $currentUserUsername, $currentUserNombreAsesor);
// Define navigation items for each role
$navItems = [
@ -281,6 +303,12 @@ $navItems = [
'text' => 'Call Center (Mis Gestiones)',
'roles' => ['Administrador', 'admin', 'Asesor']
],
'call_center_pro' => [
'url' => 'call_center_pro.php',
'icon' => 'fa-headset',
'text' => 'Call Center Pro',
'roles' => ['Administrador', 'admin', 'Asesor']
],
'simular_asignacion' => [
'url' => 'simular_asignacion.php',
'icon' => 'fa-magic',
@ -352,7 +380,12 @@ $navItems = [
$currentPage = basename($_SERVER['PHP_SELF']);
foreach ($navItems as $key => $item):
if (in_array($userRole, $item['roles'])):
$isTestModule = ($key === 'modulo_pruebas_group');
if ($isTestModule && !$canAccessTestModule) {
continue;
}
if ($isTestModule || in_array($userRole, $item['roles'])):
if (isset($item['submenu'])):
$isSubmenuActive = false;
foreach ($item['submenu'] as $sub_item) {
@ -366,7 +399,7 @@ $navItems = [
<a href="#" class="nav-link"><i class="fas <?php echo $item['icon']; ?>"></i> <?php echo $item['text']; ?></a>
<ul class="submenu <?php echo $isSubmenuActive ? 'show' : ''; ?>">
<?php foreach ($item['submenu'] as $sub_item):
if (in_array($userRole, $sub_item['roles'])):
if ($isTestModule || in_array($userRole, $sub_item['roles'])):
$isActive = $currentPage == $sub_item['url'] ? 'active' : '';
?>
<li class="nav-item">

View File

@ -5,8 +5,8 @@ session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Sesión no iniciada']);
if (!isset($_SESSION['user_id']) || !cc_test_current_user_can_access_module(db())) {
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}

View File

@ -1,7 +1,19 @@
<?php
session_start();
require_once 'db/config.php';
require_once 'includes/callcenter_test_helpers.php';
$db = db();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
if (!cc_test_current_user_can_access_module($db)) {
http_response_code(403);
die('Acceso denegado.');
}
// 1. Buscamos al primer usuario para la prueba
$user = $db->query("SELECT id, username FROM users LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if (!$user) {

View File

@ -1,14 +1,20 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/callcenter_test_helpers.php';
$pageTitle = 'Importar Drive (Test)';
$pageDescription = 'Vista previa de los últimos 10 pedidos detectados en Google Sheets antes de importarlos al CRM.';
require_once __DIR__ . '/layout_header.php';
if (($_SESSION['user_role'] ?? '') !== 'Administrador' && ($_SESSION['user_role'] ?? '') !== 'admin') {
echo "<div class='alert alert-danger'>Acceso denegado.</div>";
if (!cc_test_current_user_can_access_module(db())) {
http_response_code(403);
require_once __DIR__ . '/layout_header.php';
echo "<div class='container py-5'><div class='alert alert-danger mb-0'>Acceso denegado.</div></div>";
require_once __DIR__ . '/layout_footer.php';
exit();
}
require_once __DIR__ . '/layout_header.php';
require_once __DIR__ . '/vendor/autoload.php';
$credentialsPath = __DIR__ . '/google_credentials.json';

View File

@ -5,7 +5,7 @@ require_once 'includes/callcenter_test_helpers.php';
header('Content-Type: application/json; charset=utf-8');
if (!isset($_SESSION['user_id'])) {
if (!isset($_SESSION['user_id']) || !cc_test_current_user_can_access_module(db())) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'No autorizado']);
exit;
@ -64,6 +64,9 @@ try {
$confirmacionProducto = cc_test_normalize_nullable_text('confirmacion_producto', 255);
$confirmacionCantidad = cc_test_normalize_nullable_text('confirmacion_cantidad', 50);
$confirmacionPrecio = cc_test_normalize_nullable_text('confirmacion_precio', 80);
$confirmacionProductoExtra = cc_test_normalize_nullable_text('confirmacion_producto_extra', 255);
$confirmacionCantidadExtra = cc_test_normalize_nullable_text('confirmacion_cantidad_extra', 50);
$confirmacionPrecioExtra = cc_test_normalize_nullable_text('confirmacion_precio_extra', 80);
$pdo = db();
cc_test_ensure_tracking_table($pdo);
@ -133,6 +136,9 @@ try {
confirmacion_producto,
confirmacion_cantidad,
confirmacion_precio,
confirmacion_producto_extra,
confirmacion_cantidad_extra,
confirmacion_precio_extra,
proxima_llamada_at,
fecha_entrega_programada,
numero_cuenta_enviado_at,
@ -157,6 +163,9 @@ try {
:confirmacion_producto,
:confirmacion_cantidad,
:confirmacion_precio,
:confirmacion_producto_extra,
:confirmacion_cantidad_extra,
:confirmacion_precio_extra,
:proxima_llamada_at,
:fecha_entrega_programada,
:numero_cuenta_enviado_at,
@ -181,6 +190,9 @@ try {
confirmacion_producto = VALUES(confirmacion_producto),
confirmacion_cantidad = VALUES(confirmacion_cantidad),
confirmacion_precio = VALUES(confirmacion_precio),
confirmacion_producto_extra = VALUES(confirmacion_producto_extra),
confirmacion_cantidad_extra = VALUES(confirmacion_cantidad_extra),
confirmacion_precio_extra = VALUES(confirmacion_precio_extra),
proxima_llamada_at = VALUES(proxima_llamada_at),
fecha_entrega_programada = VALUES(fecha_entrega_programada),
numero_cuenta_enviado_at = VALUES(numero_cuenta_enviado_at),
@ -208,6 +220,9 @@ try {
':confirmacion_producto' => $confirmacionProducto,
':confirmacion_cantidad' => $confirmacionCantidad,
':confirmacion_precio' => $confirmacionPrecio,
':confirmacion_producto_extra' => $confirmacionProductoExtra,
':confirmacion_cantidad_extra' => $confirmacionCantidadExtra,
':confirmacion_precio_extra' => $confirmacionPrecioExtra,
':proxima_llamada_at' => $proximaLlamada,
':fecha_entrega_programada' => $fechaEntrega,
':numero_cuenta_enviado_at' => $numeroCuentaEnviadoAt,