Autosave: 20260723-224733
This commit is contained in:
parent
4a8d769440
commit
4a8c7a39ad
@ -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. Tú 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>N° 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'; ?>
|
||||
|
||||
@ -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;
|
||||
@ -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: ?>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'] ?? ''),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user