Autosave: 20260723-224733

This commit is contained in:
Flatlogic Bot 2026-07-23 22:46:09 +00:00
parent 4a8d769440
commit 4a8c7a39ad
5 changed files with 384 additions and 137 deletions

View File

@ -897,6 +897,25 @@ require_once 'layout_header.php';
line-height: 1.15;
}
.cc-callcenter-table {
min-width: 1600px;
}
.cc-callcenter-table thead th {
position: sticky;
top: 0;
z-index: 4;
background-color: #f8f9fa;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .08);
vertical-align: bottom;
}
.cc-callcenter-table tbody td {
vertical-align: top;
overflow-wrap: anywhere;
word-break: break-word;
}
.cc-callcenter-assign-form {
width: 100%;
padding: .72rem !important;
@ -1154,106 +1173,6 @@ require_once 'layout_header.php';
</section>
<?php endif; ?>
<?php if ($isMainTuaniStore): ?>
<?php
$repartoModeLabel = (string) ($repartoPreview['mode_label'] ?? 'Manual');
$repartoModeClass = ($repartoPreview['mode'] ?? 'manual') === 'automatico'
? 'bg-success-subtle text-success-emphasis border'
: 'bg-secondary-subtle text-secondary-emphasis border';
$repartoNextLabel = (string) ($repartoPreview['next_label'] ?? '');
$repartoAvailableCount = (int) ($repartoPreview['available_count'] ?? 0);
$repartoTotalCount = (int) ($repartoPreview['total_count'] ?? 0);
$repartoRotationDateLabel = !empty($repartoSettings['rotation_date'])
? cc_test_format_date((string) $repartoSettings['rotation_date'], 'Sin fecha')
: 'Sin ciclo aún';
?>
<section class="card border-0 shadow-sm mb-4">
<div class="card-body">
<div class="row g-3 align-items-start">
<div class="col-12 col-xl-7">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<span class="badge rounded-pill <?php echo htmlspecialchars($repartoModeClass); ?>">Reparto <?php echo htmlspecialchars($repartoModeLabel); ?></span>
<?php if ($repartoNextLabel !== ''): ?>
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis border">Siguiente: <?php echo htmlspecialchars($repartoNextLabel); ?></span>
<?php else: ?>
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border">Sin asesoras disponibles</span>
<?php endif; ?>
<span class="badge rounded-pill bg-light text-dark border">Sin asignar: <?php echo (int) $repartoUnassignedCount; ?></span>
</div>
<h2 class="h5 fw-bold mb-2">Reparto intercalado diario</h2>
<p class="text-muted small mb-3">Las asesoras activas reciben los pedidos sin asignar en orden. Si una está pausada o ausente, se salta automáticamente. El supervisor puede cambiar el estado en cualquier momento.</p>
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill bg-light text-dark border">Asesoras totales: <?php echo (int) $repartoTotalCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border">Disponibles: <?php echo (int) $repartoAvailableCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border">Ciclo: <?php echo htmlspecialchars($repartoRotationDateLabel); ?></span>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<?php foreach (($repartoPreview['sequence'] ?? []) as $item): ?>
<span class="badge rounded-pill <?php echo htmlspecialchars((string) ($item['badge_class'] ?? 'bg-light text-dark border')); ?>">
<?php echo htmlspecialchars((string) ($item['label'] ?? '')); ?> · <?php echo htmlspecialchars((string) ($item['state_label'] ?? '')); ?>
</span>
<?php endforeach; ?>
</div>
</div>
<?php if ($isAdmin): ?>
<div class="col-12 col-xl-5">
<form method="post" class="border rounded-4 p-3 bg-light h-100">
<input type="hidden" name="action" value="save_reparto_config">
<input type="hidden" name="store" value="<?php echo htmlspecialchars($storeKey); ?>">
<input type="hidden" name="view" value="<?php echo htmlspecialchars($view); ?>">
<?php if ($selectedAssessorFilter !== ''): ?>
<input type="hidden" name="assessor" value="<?php echo htmlspecialchars($selectedAssessorFilter); ?>">
<?php endif; ?>
<div class="mb-3">
<label class="form-label small text-uppercase text-muted fw-semibold mb-1" for="repartoMode">Modo de reparto</label>
<select name="reparto_mode" id="repartoMode" class="form-select">
<option value="manual"<?php echo ($repartoPreview['mode'] ?? 'manual') === 'manual' ? ' selected' : ''; ?>>Manual</option>
<option value="automatico"<?php echo ($repartoPreview['mode'] ?? 'manual') === 'automatico' ? ' selected' : ''; ?>>Automático intercalado</option>
</select>
</div>
<div class="row g-2">
<?php foreach (($repartoPreview['sequence'] ?? []) as $item): ?>
<div class="col-12">
<div class="bg-white border rounded-4 p-2">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<div class="fw-semibold"><?php echo htmlspecialchars((string) ($item['label'] ?? '')); ?></div>
<span class="badge rounded-pill <?php echo htmlspecialchars((string) ($item['badge_class'] ?? 'bg-light text-dark border')); ?>"><?php echo htmlspecialchars((string) ($item['state_label'] ?? '')); ?></span>
</div>
<select name="assessor_state[<?php echo htmlspecialchars((string) ($item['key'] ?? '')); ?>]" class="form-select form-select-sm">
<option value="disponible"<?php echo ($item['state'] ?? 'disponible') === 'disponible' ? ' selected' : ''; ?>>Disponible</option>
<option value="pausada"<?php echo ($item['state'] ?? 'disponible') === 'pausada' ? ' selected' : ''; ?>>Pausada</option>
<option value="ausente"<?php echo ($item['state'] ?? 'disponible') === 'ausente' ? ' selected' : ''; ?>>Ausente</option>
</select>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button type="submit" class="btn btn-primary">Guardar configuración</button>
</div>
</form>
<form method="post" class="mt-3">
<input type="hidden" name="action" value="run_daily_reparto">
<input type="hidden" name="store" value="<?php echo htmlspecialchars($storeKey); ?>">
<input type="hidden" name="view" value="<?php echo htmlspecialchars($view); ?>">
<?php if ($selectedAssessorFilter !== ''): ?>
<input type="hidden" name="assessor" value="<?php echo htmlspecialchars($selectedAssessorFilter); ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline-primary w-100">Distribuir pedidos pendientes</button>
</form>
</div>
<?php else: ?>
<div class="col-12 col-xl-5">
<div class="border rounded-4 p-3 bg-light h-100">
<div class="small text-uppercase text-muted fw-semibold mb-2">Control de reparto</div>
<div class="text-muted small mb-0">Solo el administrador puede cambiar el modo o pausar asesoras. ves aquí el orden intercalado y el siguiente turno.</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</section>
<?php endif; ?>
<section class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
@ -1265,12 +1184,12 @@ require_once 'layout_header.php';
<?php if ($selectedAssessorFilterLabel !== ''): ?>
<span class="badge bg-primary-subtle text-primary-emphasis border border-primary-subtle">Asesora: <?php echo htmlspecialchars($selectedAssessorFilterLabel); ?></span>
<?php endif; ?>
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos únicos en esta bandeja</span>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 cc-callcenter-table">
<table class="table table-hover align-top mb-0 cc-callcenter-table">
<thead class="table-light">
<tr>
<th> Pedido</th>
@ -1287,9 +1206,9 @@ require_once 'layout_header.php';
<td colspan="6" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
<?php if ($selectedAssessorFilterLabel !== ''): ?>
No hay pedidos en esta bandeja para <?php echo htmlspecialchars($selectedAssessorFilterLabel); ?>.
No hay pedidos únicos en esta bandeja para <?php echo htmlspecialchars($selectedAssessorFilterLabel); ?>.
<?php else: ?>
No hay pedidos en esta bandeja por ahora.
No hay pedidos únicos en esta bandeja por ahora.
<?php endif; ?>
</td>
</tr>
@ -2623,7 +2542,7 @@ if (airDroidCopyButton) {
});
}
const ccPanelLiveEnabled = <?php echo $isAdmin ? 'false' : 'true'; ?>;
const ccPanelLiveEnabled = <?php echo ($isAdmin || $isTuaniRecuperablesStore) ? 'false' : 'true'; ?>;
if (ccPanelLiveEnabled) {
const ccPanelRoot = document.querySelector('[data-cc-panel-root="1"]');
if (ccPanelRoot) {
@ -2678,9 +2597,11 @@ if (ccPanelLiveEnabled) {
}
}
window.setInterval(function () {
window.location.reload();
}, 600000);
if (ccPanelLiveEnabled) {
window.setInterval(function () {
window.location.reload();
}, 600000);
}
</script>
<?php require_once 'layout_footer.php'; ?>

View File

@ -0,0 +1,12 @@
-- 094_expand_callcenter_test_orders_precio_text.sql
-- Expand callcenter_test_orders text fields so long or misaligned Drive values do not break TUANI panel loads.
ALTER TABLE callcenter_test_orders
MODIFY COLUMN codigo TEXT NULL,
MODIFY COLUMN import_id TEXT NULL,
MODIFY COLUMN nombre TEXT NULL,
MODIFY COLUMN celular TEXT NULL,
MODIFY COLUMN cantidad TEXT NULL,
MODIFY COLUMN precio TEXT NULL,
MODIFY COLUMN pais TEXT NULL,
MODIFY COLUMN coordenadas TEXT NULL,
MODIFY COLUMN metodo TEXT NULL;

View File

@ -12,7 +12,7 @@ $departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega
$sedesShalom = cc_test_fetch_shalom_sedes(db());
$pageTitle = 'Base de Datos Pedidos | Asignación';
$pageDescription = 'Bandejas de asignación y gestión de estados para el Call Center, cargadas desde Drive.';
$pageDescription = 'Bandejas de asignación, reparto intercalado diario y gestión de estados para el Call Center, cargadas desde Drive.';
$role = (string) ($_SESSION['user_role'] ?? '');
if (!in_array($role, ['Administrador', 'admin'], true)) {
@ -163,6 +163,17 @@ function cc_test_import_time(array $order): int
return (int) ($order['id'] ?? 0);
}
function cc_test_tuani_reparto_is_eligible(array $order, string $storeKey, array $storeConfig): bool
{
return true;
}
function cc_test_filter_reparto_orders(array $orders, string $storeKey, array $storeConfig): array
{
return $orders;
}
function cc_test_followup_semaforo(array $order): ?array
{
if (cc_test_normalize_state((string) ($order['estado'] ?? '')) !== 'SE ENVIO NUMERO DE CUENTA') {
@ -311,6 +322,30 @@ $totalRows = 0;
$agregadosCount = 0;
$lastProcessedRow = null;
$repartoSettings = [
'store_key' => $storeKey,
'reparto_mode' => 'manual',
'rotation_date' => null,
'rotation_index' => 0,
'assessor_states' => [],
];
$repartoPreview = [
'mode' => 'manual',
'mode_label' => 'Manual',
'sequence' => [],
'ordered_keys' => [],
'states' => [],
'available_count' => 0,
'total_count' => 0,
'rotation_index' => 0,
'rotation_date' => null,
'next_key' => null,
'next_label' => '',
'next_position' => null,
];
$repartoUnassignedCount = 0;
$repartoEligibleUnassignedCount = 0;
$cupoObjetivoPorAsesora = 10;
$advisorOpenCounts = [];
$advisorTotalCounts = [];
@ -334,6 +369,9 @@ try {
cc_test_ensure_tracking_table($pdo);
cc_test_ensure_historial_llamadas_table($pdo);
$assessors = cc_test_fetch_assessors($pdo);
if ($isMainTuaniStore) {
$repartoSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors);
}
try {
$stmtProductos = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
@ -611,6 +649,10 @@ try {
continue;
}
if (!cc_test_tuani_reparto_is_eligible($o, $storeKey, $storeConfig)) {
continue;
}
$estadoNorm = cc_test_normalize_state((string) ($o['estado'] ?? ''));
if (!in_array($estadoNorm, $openStates, true)) {
continue;
@ -712,6 +754,76 @@ try {
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_reparto_config') {
if (!$isMainTuaniStore) {
throw new RuntimeException('Esta configuración solo aplica a TUANI principal.');
}
$postedMode = cc_test_normalize_reparto_mode($_POST['reparto_mode'] ?? 'manual');
$postedStates = $_POST['assessor_state'] ?? [];
if (!is_array($postedStates)) {
$postedStates = [];
}
$repartoSettings['reparto_mode'] = 'manual';
$repartoSettings['assessor_states'] = cc_test_reparto_normalize_states($postedStates, $assessors);
cc_test_upsert_reparto_settings($pdo, $storeKey, $repartoSettings);
$noticeMessage = 'Configuración de reparto guardada.';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'run_daily_reparto') {
if (!$isMainTuaniStore) {
throw new RuntimeException('El reparto intercalado solo está disponible en TUANI principal.');
}
$repartoOrders = cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig);
if (empty($repartoOrders)) {
$repartoResult = [
'assigned_count' => 0,
'settings' => $repartoSettings,
'message' => 'No hay pedidos pendientes para repartir.',
];
} else {
$repartoResult = cc_test_apply_daily_reparto($pdo, $storeKey, $repartoOrders, $assessors, $repartoSettings);
}
$assignedCount = (int) ($repartoResult['assigned_count'] ?? 0);
$noticeMessage = $assignedCount > 0
? 'Reparto intercalado: ' . $assignedCount . ' pedido(s) asignado(s).'
: (string) ($repartoResult['message'] ?? 'No había pedidos pendientes o no había asesoras disponibles.');
if ($assignedCount > 0) {
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
$orders = drive_test_merge_tracking($orders, $tracking);
}
$repartoSettings = $repartoResult['settings'] ?? $repartoSettings;
}
if ($isMainTuaniStore && ($repartoSettings['reparto_mode'] ?? 'manual') === 'automatico') {
$repartoOrders = cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig);
$autoRepartoResult = cc_test_apply_daily_reparto($pdo, $storeKey, $repartoOrders, $assessors, $repartoSettings);
$autoAssignedCount = (int) ($autoRepartoResult['assigned_count'] ?? 0);
if ($autoAssignedCount > 0) {
$noticeMessage = trim((string) ($noticeMessage !== null ? $noticeMessage . ' ' : '') . 'Reparto automático: ' . $autoAssignedCount . ' pedido(s) asignado(s).');
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
$orders = drive_test_merge_tracking($orders, $tracking);
}
$repartoSettings = $autoRepartoResult['settings'] ?? $repartoSettings;
}
if ($isMainTuaniStore) {
$repartoSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors);
$repartoSettings['reparto_mode'] = 'manual';
}
$repartoPreview = cc_test_reparto_preview($assessors, $repartoSettings);
$repartoEligibleOrders = cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig);
foreach ($repartoEligibleOrders as $repartoOrder) {
if ((int) ($repartoOrder['user_id'] ?? 0) <= 0) {
$repartoEligibleUnassignedCount++;
}
}
foreach ($orders as $repartoOrder) {
if ((int) ($repartoOrder['user_id'] ?? 0) <= 0) {
$repartoUnassignedCount++;
}
}
$sourceKeys = array_column($orders, 'source_key');
$callCounts = [];
if (!empty($sourceKeys)) {
@ -1369,13 +1481,99 @@ require_once 'layout_header.php';
</div>
</section>
<?php if ($isMainTuaniStore): ?>
<?php
$repartoModeLabel = (string) ($repartoPreview['mode_label'] ?? 'Manual');
$repartoModeClass = ($repartoPreview['mode'] ?? 'manual') === 'automatico'
? 'bg-success-subtle text-success-emphasis border'
: 'bg-secondary-subtle text-secondary-emphasis border';
$repartoNextLabel = (string) ($repartoPreview['next_label'] ?? '');
$repartoAvailableCount = (int) ($repartoPreview['available_count'] ?? 0);
$repartoTotalCount = (int) ($repartoPreview['total_count'] ?? 0);
$repartoRotationDateLabel = !empty($repartoSettings['rotation_date'])
? cc_test_format_date((string) $repartoSettings['rotation_date'], 'Sin fecha')
: 'Sin ciclo aún';
?>
<section class="card border-0 shadow-sm mb-4" id="repartoIntercalado">
<div class="card-body">
<div class="row g-3 align-items-start">
<div class="col-12 col-xl-7">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<span class="badge rounded-pill <?php echo htmlspecialchars($repartoModeClass); ?>">Reparto <?php echo htmlspecialchars($repartoModeLabel); ?></span>
<?php if ($repartoNextLabel !== ''): ?>
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis border">Siguiente: <?php echo htmlspecialchars($repartoNextLabel); ?></span>
<?php else: ?>
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border">Sin asesoras disponibles</span>
<?php endif; ?>
<span class="badge rounded-pill bg-light text-dark border">Sin asignar: <?php echo (int) $repartoUnassignedCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border">Elegibles para reparto: <?php echo (int) $repartoEligibleUnassignedCount; ?></span>
</div>
<h2 class="h5 fw-bold mb-2">Reparto de pedidos pendientes</h2>
<p class="text-muted small mb-3">El historial anterior se mantiene visible. El reparto manual trabaja sobre los pedidos actuales sin ocultar los anteriores.</p>
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill bg-light text-dark border">Asesoras totales: <?php echo (int) $repartoTotalCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border">Disponibles: <?php echo (int) $repartoAvailableCount; ?></span>
<span class="badge rounded-pill bg-light text-dark border">Ciclo: <?php echo htmlspecialchars($repartoRotationDateLabel); ?></span>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<?php foreach (($repartoPreview['sequence'] ?? []) as $item): ?>
<span class="badge rounded-pill <?php echo htmlspecialchars((string) ($item['badge_class'] ?? 'bg-light text-dark border')); ?>">
<?php echo htmlspecialchars((string) ($item['label'] ?? '')); ?> · <?php echo htmlspecialchars((string) ($item['state_label'] ?? '')); ?>
</span>
<?php endforeach; ?>
</div>
</div>
<div class="col-12 col-xl-5">
<form method="post" class="border rounded-4 p-3 bg-light h-100">
<input type="hidden" name="action" value="save_reparto_config">
<input type="hidden" name="store" value="<?php echo htmlspecialchars($storeKey); ?>">
<input type="hidden" name="view" value="<?php echo htmlspecialchars($view); ?>">
<div class="mb-3">
<label class="form-label small text-uppercase text-muted fw-semibold mb-1" for="repartoMode">Modo de reparto</label>
<select name="reparto_mode" id="repartoMode" class="form-select">
<option value="manual"<?php echo ($repartoPreview['mode'] ?? 'manual') === 'manual' ? ' selected' : ''; ?>>Manual</option>
</select>
</div>
<div class="row g-2">
<?php foreach (($repartoPreview['sequence'] ?? []) as $item): ?>
<div class="col-12">
<div class="bg-white border rounded-4 p-2">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<div class="fw-semibold"><?php echo htmlspecialchars((string) ($item['label'] ?? '')); ?></div>
<span class="badge rounded-pill <?php echo htmlspecialchars((string) ($item['badge_class'] ?? 'bg-light text-dark border')); ?>"><?php echo htmlspecialchars((string) ($item['state_label'] ?? '')); ?></span>
</div>
<select name="assessor_state[<?php echo htmlspecialchars((string) ($item['key'] ?? '')); ?>]" class="form-select form-select-sm">
<option value="disponible"<?php echo ($item['state'] ?? 'disponible') === 'disponible' ? ' selected' : ''; ?>>Disponible</option>
<option value="pausada"<?php echo ($item['state'] ?? 'disponible') === 'pausada' ? ' selected' : ''; ?>>Pausada</option>
<option value="ausente"<?php echo ($item['state'] ?? 'disponible') === 'ausente' ? ' selected' : ''; ?>>Ausente</option>
</select>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button type="submit" class="btn btn-primary">Guardar configuración</button>
</div>
</form>
<form method="post" class="mt-3">
<input type="hidden" name="action" value="run_daily_reparto">
<input type="hidden" name="store" value="<?php echo htmlspecialchars($storeKey); ?>">
<input type="hidden" name="view" value="<?php echo htmlspecialchars($view); ?>">
<button type="submit" class="btn btn-outline-primary w-100">Distribuir pedidos pendientes</button>
</form>
</div>
</div>
</div>
</section>
<?php endif; ?>
<section class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2">
<div>
<h2 class="h5 fw-bold mb-1"><?php echo htmlspecialchars($allowedViews[$view]); ?></h2>
<p class="text-muted small mb-1"><?php echo $viewStatesNote; ?></p>
</div>
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos en esta bandeja</span>
<span class="badge bg-light text-dark border"><?php echo count($visibleOrders); ?> pedidos únicos en esta bandeja</span>
</div>
<div class="card-body p-0">
<div class="border-bottom bg-light-subtle px-3 py-3">
@ -1418,7 +1616,7 @@ require_once 'layout_header.php';
<tr>
<td colspan="7" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
No hay pedidos en esta bandeja por ahora.
No hay pedidos únicos en esta bandeja por ahora.
</td>
</tr>
<?php else: ?>

View File

@ -160,10 +160,10 @@ function cc_test_ensure_schema(PDO $pdo): void
$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,
`codigo` TEXT NULL,
`import_id` TEXT NULL,
`drive_imported_at` DATETIME NULL,
`nombre` VARCHAR(255) DEFAULT NULL,
`nombre` TEXT NULL,
`direccion_drive` TEXT NULL,
`referencia_drive` TEXT NULL,
`sede_drive` VARCHAR(120) NULL,
@ -171,13 +171,13 @@ function cc_test_ensure_schema(PDO $pdo): void
`distrito_drive` VARCHAR(120) NULL,
`dni_drive` LONGTEXT NULL,
`observaciones_drive` TEXT NULL,
`celular` VARCHAR(40) DEFAULT NULL,
`celular` TEXT NULL,
`producto` TEXT NULL,
`cantidad` VARCHAR(60) NULL,
`precio` VARCHAR(60) NULL,
`pais` VARCHAR(80) NULL,
`coordenadas` VARCHAR(255) NULL,
`metodo` VARCHAR(120) NULL,
`cantidad` TEXT NULL,
`precio` TEXT NULL,
`pais` TEXT NULL,
`coordenadas` TEXT NULL,
`metodo` TEXT 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`),
@ -205,6 +205,48 @@ function cc_test_ensure_schema(PDO $pdo): void
error_log('cc_test_ensure_schema: ' . $exception->getMessage());
}
$textColumns = [
'codigo' => 'AFTER `source_key`',
'import_id' => 'AFTER `codigo`',
'nombre' => 'AFTER `import_id`',
'celular' => 'AFTER `observaciones_drive`',
'cantidad' => 'AFTER `producto`',
'precio' => 'AFTER `cantidad`',
'pais' => 'AFTER `precio`',
'coordenadas' => 'AFTER `pais`',
'metodo' => 'AFTER `coordenadas`',
];
foreach ($textColumns as $columnName => $afterClause) {
cc_test_ensure_column($pdo, 'callcenter_test_orders', $columnName, "TEXT NULL {$afterClause}");
}
try {
$colStmt = $pdo->prepare("
SELECT COLUMN_NAME, DATA_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'callcenter_test_orders'
AND COLUMN_NAME IN ('codigo', 'import_id', 'nombre', 'celular', 'cantidad', 'precio', 'pais', 'coordenadas', 'metodo')
");
$colStmt->execute();
$dataTypes = [];
foreach ($colStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$columnName = strtolower((string) ($row['COLUMN_NAME'] ?? ''));
$dataTypes[$columnName] = strtolower((string) ($row['DATA_TYPE'] ?? ''));
}
foreach ($textColumns as $columnName => $afterClause) {
$dataType = $dataTypes[$columnName] ?? '';
if (!in_array($dataType, ['text', 'mediumtext', 'longtext'], true)) {
$pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `{$columnName}` TEXT NULL {$afterClause}");
}
}
} catch (Throwable $exception) {
error_log('cc_test_ensure_schema text columns: ' . $exception->getMessage());
}
$pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_tracking` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`source_key` CHAR(40) NOT NULL,

View File

@ -29,8 +29,8 @@ function drive_test_available_stores(): array
'sheet_title' => 'TUANI PEDIDOS',
'spreadsheet_id' => '1QYKeBJIIqYm6yW6Ka-5gKdxhUHwXOAc0No7RjnimxTw',
'sheet_gid' => 1523126328,
'startRow' => 6804,
'enforce_db_start_row' => true,
'startRow' => 5552,
'enforce_db_start_row' => false,
'incremental_sync' => true,
],
// Bandeja compartida de carritos recuperables: misma estructura, pero desde la fila 2000.
@ -354,6 +354,31 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array
return $tracking;
}
function drive_test_get_existing_source_keys_by_row(PDO $pdo, string $storeKey): array
{
$storeKey = mb_strtolower(trim($storeKey));
if ($storeKey === '') {
return [];
}
drive_test_ensure_orders_table($pdo);
$stmt = $pdo->prepare('SELECT source_row, source_key FROM callcenter_test_orders WHERE store_key = ? AND source_row IS NOT NULL ORDER BY COALESCE(drive_imported_at, first_seen_at) DESC, id DESC');
$stmt->execute([$storeKey]);
$sourceKeysByRow = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$sourceRow = isset($row['source_row']) ? (int) $row['source_row'] : 0;
$sourceKey = trim((string) ($row['source_key'] ?? ''));
if ($sourceRow > 0 && $sourceKey !== '' && !isset($sourceKeysByRow[$sourceRow])) {
$sourceKeysByRow[$sourceRow] = $sourceKey;
}
}
return $sourceKeysByRow;
}
function drive_test_merge_tracking(array $orders, array $tracking): array
{
$editableFields = ['direccion', 'referencia', 'agencia', 'sede_agencia', 'sede', 'ciudad', 'distrito', 'dni', 'numero_cuenta_sede_id', 'numero_cuenta_dni', 'observaciones', 'coordenadas', 'producto', 'cantidad', 'precio', 'monto_adelantado', 'confirmacion_producto', 'confirmacion_cantidad', 'confirmacion_precio', 'confirmacion_producto_extra', 'confirmacion_cantidad_extra', 'confirmacion_precio_extra'];
@ -451,12 +476,12 @@ function drive_test_ensure_orders_table(PDO $pdo): void
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`store_key` VARCHAR(50) NULL,
`source_key` CHAR(40) NOT NULL,
`codigo` VARCHAR(80) DEFAULT NULL,
`import_id` VARCHAR(120) DEFAULT NULL,
`codigo` TEXT NULL,
`import_id` TEXT NULL,
`source_row` INT NULL,
`is_agregado` TINYINT(1) NOT NULL DEFAULT 0,
`drive_imported_at` DATETIME NULL,
`nombre` VARCHAR(255) DEFAULT NULL,
`nombre` TEXT NULL,
`direccion_drive` TEXT NULL,
`referencia_drive` TEXT NULL,
`sede_drive` TEXT NULL,
@ -464,13 +489,13 @@ function drive_test_ensure_orders_table(PDO $pdo): void
`distrito_drive` TEXT NULL,
`dni_drive` LONGTEXT NULL,
`observaciones_drive` TEXT NULL,
`celular` VARCHAR(40) DEFAULT NULL,
`celular` TEXT NULL,
`producto` TEXT NULL,
`cantidad` VARCHAR(60) NULL,
`precio` VARCHAR(60) NULL,
`pais` VARCHAR(80) NULL,
`coordenadas` VARCHAR(255) NULL,
`metodo` VARCHAR(120) NULL,
`cantidad` TEXT NULL,
`precio` TEXT NULL,
`pais` TEXT NULL,
`coordenadas` TEXT NULL,
`metodo` TEXT 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`),
@ -485,6 +510,22 @@ function drive_test_ensure_orders_table(PDO $pdo): void
// Asegura columna para pedidos cargados manualmente (Agregados)
cc_test_ensure_column($pdo, "callcenter_test_orders", "is_agregado", "TINYINT(1) NOT NULL DEFAULT 0");
$textColumns = [
'codigo' => 'AFTER `source_key`',
'import_id' => 'AFTER `codigo`',
'nombre' => 'AFTER `import_id`',
'celular' => 'AFTER `observaciones_drive`',
'cantidad' => 'AFTER `producto`',
'precio' => 'AFTER `cantidad`',
'pais' => 'AFTER `precio`',
'coordenadas' => 'AFTER `pais`',
'metodo' => 'AFTER `coordenadas`',
];
foreach ($textColumns as $columnName => $afterClause) {
cc_test_ensure_column($pdo, 'callcenter_test_orders', $columnName, "TEXT NULL {$afterClause}");
}
// Asegurar que los campos de ubicación y DNI soporten valores largos desde Google Sheets.
try {
$colStmt = $pdo->prepare("
@ -517,6 +558,32 @@ function drive_test_ensure_orders_table(PDO $pdo): void
error_log('drive_test_ensure_orders_table: ' . $exception->getMessage());
}
try {
$colStmt = $pdo->prepare("
SELECT COLUMN_NAME, DATA_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'callcenter_test_orders'
AND COLUMN_NAME IN ('codigo', 'import_id', 'nombre', 'celular', 'cantidad', 'precio', 'pais', 'coordenadas', 'metodo')
");
$colStmt->execute();
$dataTypes = [];
foreach ($colStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$columnName = strtolower((string) ($row['COLUMN_NAME'] ?? ''));
$dataTypes[$columnName] = strtolower((string) ($row['DATA_TYPE'] ?? ''));
}
foreach ($textColumns as $columnName => $afterClause) {
$dataType = $dataTypes[$columnName] ?? '';
if (!in_array($dataType, ['text', 'mediumtext', 'longtext'], true)) {
$pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `{$columnName}` TEXT NULL {$afterClause}");
}
}
} catch (Throwable $exception) {
error_log('drive_test_ensure_orders_table text columns: ' . $exception->getMessage());
}
$checked = true;
}
@ -550,6 +617,8 @@ function drive_test_sync_orders_incremental(PDO $pdo, string $storeKey, int $ove
drive_test_ensure_orders_table($pdo);
drive_test_ensure_import_checkpoints_table($pdo);
$existingSourceKeysByRow = drive_test_get_existing_source_keys_by_row($pdo, $storeKey);
$stmtCheckpoint = $pdo->prepare('SELECT last_processed_row, baseline_start_row FROM drive_test_import_checkpoints WHERE store_key = ? LIMIT 1');
$stmtCheckpoint->execute([$storeKey]);
$checkpointData = $stmtCheckpoint->fetch(PDO::FETCH_ASSOC) ?: [];
@ -647,6 +716,9 @@ function drive_test_sync_orders_incremental(PDO $pdo, string $storeKey, int $ove
foreach ($driveOrders as $order) {
$driveImportedAt = drive_test_parse_datetime_mysql($order['import_id'] ?? null);
$sourceRow = isset($order['source_row']) && (int) $order['source_row'] > 0 ? (int) $order['source_row'] : null;
if ($sourceRow !== null && isset($existingSourceKeysByRow[$sourceRow])) {
$order['source_key'] = $existingSourceKeysByRow[$sourceRow];
}
$stmtUpsert->execute([
':store_key' => $storeKey,
@ -754,14 +826,16 @@ function drive_test_fetch_orders_from_db(PDO $pdo, string $storeKey, ?bool $only
$stmt->execute($params);
$orders = [];
$minCodigoNumber = isset($storeConfig['min_codigo_number']) ? (int) ($storeConfig['min_codigo_number'] ?? 0) : 0;
$seenSourceRows = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$isAgregado = (int) ($row['is_agregado'] ?? 0);
$codigo = (string) ($row['codigo'] ?? '');
$codigoNumber = drive_test_extract_codigo_number($codigo);
if ($minCodigoNumber > 0 && $isAgregado !== 1 && ($codigoNumber === null || $codigoNumber < $minCodigoNumber)) {
continue;
$sourceRow = isset($row['source_row']) && (int) $row['source_row'] > 0 ? (int) $row['source_row'] : null;
if ($sourceRow !== null) {
if (isset($seenSourceRows[$sourceRow])) {
continue;
}
$seenSourceRows[$sourceRow] = true;
}
$orders[] = [
@ -770,7 +844,7 @@ function drive_test_fetch_orders_from_db(PDO $pdo, string $storeKey, ?bool $only
'source_key' => (string) ($row['source_key'] ?? ''),
'codigo' => $codigo,
'import_id' => (string) ($row['import_id'] ?? ''),
'source_row' => isset($row['source_row']) && (int) $row['source_row'] > 0 ? (int) $row['source_row'] : null,
'source_row' => $sourceRow,
'drive_imported_at' => (string) ($row['drive_imported_at'] ?? ''),
'first_seen_at' => (string) ($row['first_seen_at'] ?? ''),
'nombre' => (string) ($row['nombre'] ?? ''),