Autosave: 20260723-201741
This commit is contained in:
parent
2924efbd4b
commit
4a8d769440
@ -49,6 +49,7 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'panel_status') {
|
||||
$panelOrders = array_values(array_filter($panelOrders, static function (array $order): bool {
|
||||
return empty($order['eliminado']);
|
||||
}));
|
||||
$panelOrders = cc_test_mark_recoverables_duplicate_orders($pdo, $panelOrders, $panelStoreKey);
|
||||
} else {
|
||||
$stmtPanel = $pdo->prepare("
|
||||
SELECT source_key, user_id, estado, assigned_at, updated_at, eliminado_at
|
||||
@ -60,9 +61,16 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'panel_status') {
|
||||
$panelOrders = $stmtPanel->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$panelSignatureParts = [cc_test_panel_orders_signature($panelOrders)];
|
||||
if ($panelStoreKey === 'otra_tienda') {
|
||||
$panelAssessors = cc_test_fetch_assessors($pdo);
|
||||
$panelRepartoSettings = cc_test_fetch_reparto_settings($pdo, $panelStoreKey, $panelAssessors);
|
||||
$panelSignatureParts[] = cc_test_reparto_settings_signature($panelRepartoSettings);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'signature' => cc_test_panel_orders_signature($panelOrders),
|
||||
'signature' => sha1(implode('|', $panelSignatureParts)),
|
||||
'total' => count($panelOrders),
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
@ -253,6 +261,7 @@ function cc_test_panel_orders_signature(array $orders): string
|
||||
'source_key' => (string) ($order['source_key'] ?? ''),
|
||||
'user_id' => (int) ($order['user_id'] ?? 0),
|
||||
'estado' => cc_test_normalize_state((string) ($order['estado'] ?? '')),
|
||||
'pedido_repetido_en_tienda' => !empty($order['pedido_repetido_en_tienda']) ? 1 : 0,
|
||||
'assigned_at' => trim((string) ($order['assigned_at'] ?? '')),
|
||||
'updated_at' => trim((string) ($order['updated_at'] ?? ($order['seguimiento_actualizado'] ?? ''))),
|
||||
'eliminado_at' => trim((string) ($order['eliminado_at'] ?? '')),
|
||||
@ -391,8 +400,8 @@ if (!isset($allowedViews[$view])) {
|
||||
}
|
||||
|
||||
$viewStatesNote = $isTuaniRecuperablesStore
|
||||
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja.'
|
||||
: 'Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega, Confirmado envío, Cancelado y Repetido. <strong>Promo Final</strong> se habilita desde el Día 4 y exige imagen de sustento para mover el pedido.';
|
||||
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja. Si el teléfono ya existe en <strong>TUANI principal</strong>, se marca en gris como <strong>Pedido repetido en tienda</strong> solo en esta bandeja.'
|
||||
: 'Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega, Confirmado envío, Cancelado y Repetido. <strong>Promo Final</strong> se habilita desde el Día 4 y exige imagen de sustento para mover el pedido. Además, esta bandeja principal incluye reparto intercalado diario: en modo automático reparte pedidos sin asignar entre asesoras activas; en modo manual el supervisor los mueve pedido por pedido.';
|
||||
|
||||
$errorMessage = null;
|
||||
$noticeMessage = null;
|
||||
@ -421,11 +430,37 @@ $stats = [
|
||||
];
|
||||
|
||||
$panelSignature = '';
|
||||
$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;
|
||||
$repartoSourceOrders = [];
|
||||
|
||||
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");
|
||||
@ -469,47 +504,115 @@ try {
|
||||
return empty($order['eliminado']);
|
||||
}));
|
||||
|
||||
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.');
|
||||
}
|
||||
$postedAction = trim((string) ($_POST['action'] ?? ''));
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($postedAction === '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.');
|
||||
}
|
||||
$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.');
|
||||
}
|
||||
|
||||
$targetUserId = $selectedAssessor ? (int) $selectedAssessor['id'] : null;
|
||||
$targetUserId = $selectedAssessor ? (int) $selectedAssessor['id'] : null;
|
||||
|
||||
$stmtCurrent = $pdo->prepare('SELECT user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1');
|
||||
$stmtCurrent->execute([$sourceKey]);
|
||||
$currentUserIdRaw = $stmtCurrent->fetchColumn();
|
||||
$currentUserId = $currentUserIdRaw !== null ? ((int) $currentUserIdRaw > 0 ? (int) $currentUserIdRaw : null) : null;
|
||||
$stmtCurrent = $pdo->prepare('SELECT user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1');
|
||||
$stmtCurrent->execute([$sourceKey]);
|
||||
$currentAssignedUserIdRaw = $stmtCurrent->fetchColumn();
|
||||
$currentAssignedUserId = $currentAssignedUserIdRaw !== null ? ((int) $currentAssignedUserIdRaw > 0 ? (int) $currentAssignedUserIdRaw : null) : null;
|
||||
|
||||
// Si el pedido ya está asignado, evitamos que se reasigne a otra asesora (solo permitimos dejarlo sin asignar).
|
||||
if ($currentUserId !== null && $targetUserId !== null && (int) $currentUserId !== (int) $targetUserId) {
|
||||
$currentLabel = 'otra asesora';
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === (int) $currentUserId) {
|
||||
$currentLabel = (string) ($assessor['label'] ?? $assessorKey);
|
||||
break;
|
||||
// Si el pedido ya está asignado, evitamos que se reasigne a otra asesora (solo permitimos dejarlo sin asignar).
|
||||
if ($currentAssignedUserId !== null && $targetUserId !== null && (int) $currentAssignedUserId !== (int) $targetUserId) {
|
||||
$currentLabel = 'otra asesora';
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === (int) $currentAssignedUserId) {
|
||||
$currentLabel = (string) ($assessor['label'] ?? $assessorKey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Este pedido ya está asignado a ' . $currentLabel . '. Para cambiarlo primero déjalo en "Sin asignar".');
|
||||
}
|
||||
|
||||
cc_test_upsert_assignee($pdo, $sourceKey, $targetUserId);
|
||||
$noticeMessage = $selectedAssessor
|
||||
? 'Pedido asignado a ' . $selectedAssessor['label'] . '.'
|
||||
: 'Pedido dejado sin asignar.';
|
||||
|
||||
if ($isMainTuaniStore && $currentAssignedUserId === null && $targetUserId !== null) {
|
||||
$repartoPreviewBefore = cc_test_reparto_preview($assessors, $repartoSettings);
|
||||
$orderedKeys = $repartoPreviewBefore['ordered_keys'] ?? [];
|
||||
$targetPosition = array_search($targetKey, $orderedKeys, true);
|
||||
if ($targetPosition !== false && !empty($orderedKeys)) {
|
||||
$repartoSettings['rotation_date'] = (new DateTimeImmutable('today'))->format('Y-m-d');
|
||||
$repartoSettings['rotation_index'] = (((int) $targetPosition) + 1) % count($orderedKeys);
|
||||
cc_test_upsert_reparto_settings($pdo, $storeKey, $repartoSettings);
|
||||
$repartoSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors);
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Este pedido ya está asignado a ' . $currentLabel . '. Para cambiarlo primero déjalo en "Sin asignar".');
|
||||
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
|
||||
$orders = drive_test_merge_tracking($orders, $tracking);
|
||||
} elseif ($postedAction === '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'] = $postedMode;
|
||||
$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 ($postedAction === 'run_daily_reparto') {
|
||||
if (!$isMainTuaniStore) {
|
||||
throw new RuntimeException('El reparto intercalado solo está disponible en TUANI principal.');
|
||||
}
|
||||
|
||||
$repartoResult = cc_test_apply_daily_reparto($pdo, $storeKey, $orders, $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;
|
||||
}
|
||||
}
|
||||
|
||||
cc_test_upsert_assignee($pdo, $sourceKey, $targetUserId);
|
||||
$noticeMessage = $selectedAssessor
|
||||
? 'Pedido asignado a ' . $selectedAssessor['label'] . '.'
|
||||
: 'Pedido dejado sin asignar.';
|
||||
if ($isMainTuaniStore && ($repartoSettings['reparto_mode'] ?? 'manual') === 'automatico') {
|
||||
$autoRepartoResult = cc_test_apply_daily_reparto($pdo, $storeKey, $orders, $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;
|
||||
}
|
||||
|
||||
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
|
||||
$orders = drive_test_merge_tracking($orders, $tracking);
|
||||
if ($isMainTuaniStore) {
|
||||
$repartoSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors);
|
||||
$repartoPreview = cc_test_reparto_preview($assessors, $repartoSettings);
|
||||
}
|
||||
$repartoSourceOrders = $orders;
|
||||
$repartoUnassignedCount = 0;
|
||||
foreach ($repartoSourceOrders as $repartoOrder) {
|
||||
if ((int) ($repartoOrder['user_id'] ?? 0) <= 0) {
|
||||
$repartoUnassignedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$sourceKeys = array_column($orders, 'source_key');
|
||||
@ -599,6 +702,8 @@ try {
|
||||
}
|
||||
unset($order);
|
||||
|
||||
$orders = cc_test_mark_recoverables_duplicate_orders($pdo, $orders, $storeKey);
|
||||
|
||||
$performanceOrders = $orders;
|
||||
if ($selectedAssessorFilter !== '') {
|
||||
$performanceOrders = array_values(array_filter($orders, static function (array $order) use ($selectedAssessorFilter): bool {
|
||||
@ -639,6 +744,9 @@ try {
|
||||
]);
|
||||
|
||||
$panelSignature = cc_test_panel_orders_signature($orders);
|
||||
if ($isMainTuaniStore) {
|
||||
$panelSignature .= '|'. cc_test_reparto_settings_signature($repartoSettings);
|
||||
}
|
||||
|
||||
$visibleOrders = array_values(array_filter($orders, static function (array $order) use ($view, $selectedAssessorFilter, $todayStart): bool {
|
||||
if ($selectedAssessorFilter !== '' && !($order['matches_assessor_filter'] ?? false)) {
|
||||
@ -1046,6 +1154,107 @@ 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">
|
||||
<div>
|
||||
@ -1129,9 +1338,13 @@ require_once 'layout_header.php';
|
||||
}
|
||||
}
|
||||
}
|
||||
$rowBackgroundStyle = cc_test_row_background_style((string) ($order['estado'] ?? 'POR LLAMAR'));
|
||||
if ($isTuaniRecuperablesStore && !empty($order['pedido_repetido_en_tienda'])) {
|
||||
$rowBackgroundStyle .= '--cc-row-bg:#CCCCCC;';
|
||||
}
|
||||
ob_start();
|
||||
?>
|
||||
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars(cc_test_row_background_style((string) ($order['estado'] ?? 'POR LLAMAR'))); ?>">
|
||||
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars($rowBackgroundStyle); ?>">
|
||||
<td>
|
||||
<?php if ($followupDayInfo !== null): ?>
|
||||
<div class="small text-uppercase fw-semibold <?php echo htmlspecialchars($followupDayInfo['text_class']); ?> mb-1"><?php echo htmlspecialchars($followupDayInfo['display_label']); ?></div>
|
||||
@ -1164,6 +1377,9 @@ require_once 'layout_header.php';
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
<span class="badge rounded-pill <?php echo htmlspecialchars($badgeClass); ?>"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></span>
|
||||
<?php if (!empty($order['pedido_repetido_en_tienda'])): ?>
|
||||
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis border"><?php echo htmlspecialchars((string) ($order['pedido_repetido_en_tienda_label'] ?? 'Pedido repetido en tienda')); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="badge rounded-pill bg-light text-dark border"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span> llamadas</span>
|
||||
<?php if (!empty($logisticaPendienteLabel)): ?>
|
||||
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border"><?php echo htmlspecialchars($logisticaPendienteLabel); ?></span>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS `callcenter_test_reparto_settings` (
|
||||
`store_key` VARCHAR(50) NOT NULL,
|
||||
`reparto_mode` VARCHAR(20) NOT NULL DEFAULT 'manual',
|
||||
`rotation_date` DATE NULL,
|
||||
`rotation_index` INT NOT NULL DEFAULT 0,
|
||||
`assessor_states_json` LONGTEXT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`store_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@ -297,7 +297,7 @@ if (!isset($allowedViews[$view])) {
|
||||
}
|
||||
|
||||
$viewStatesNote = $isTuaniRecuperablesStore
|
||||
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja.'
|
||||
? 'Estados disponibles: <strong>Carritos recuperables</strong>, <strong>Seguimiento</strong>, <strong>Observados</strong>, <strong>Confirmados</strong>, <strong>Recuperados</strong> y <strong>Todos</strong>. Esta bandeja compartida está disponible para cualquier asesora y, cuando una asesora gestiona un carrito recuperable, el sistema lo asigna automáticamente a quien lo trabaja. Si el teléfono ya existe en <strong>TUANI principal</strong>, se marca en gris como <strong>Pedido repetido en tienda</strong> solo en esta bandeja.'
|
||||
: 'Estados disponibles: Por llamar, Devolver llamada, Observado, Se envió número de cuenta, Confirmado contraentrega, Confirmado envío, Cancelado y Repetido. <strong>Promo Final</strong> se habilita desde el Día 4 y exige imagen de sustento para mover el pedido.';
|
||||
|
||||
$errorMessage = null;
|
||||
@ -793,6 +793,8 @@ try {
|
||||
}
|
||||
unset($order);
|
||||
|
||||
$orders = cc_test_mark_recoverables_duplicate_orders($pdo, $orders, $storeKey);
|
||||
|
||||
$performanceDashboard = cc_test_build_performance_dashboard($orders, $assessors, 7);
|
||||
$performanceDashboardHtml = cc_test_render_performance_dashboard($performanceDashboard, [
|
||||
'title' => 'KPI diario por asesora',
|
||||
@ -1466,7 +1468,7 @@ require_once 'layout_header.php';
|
||||
}
|
||||
ob_start();
|
||||
?>
|
||||
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars(cc_test_row_background_style((string) ($order['estado'] ?? 'POR LLAMAR'))); ?>">
|
||||
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars(cc_test_row_background_style((string) ($order['estado'] ?? 'POR LLAMAR')) . (!empty($order['pedido_repetido_en_tienda']) ? '--cc-row-bg:#CCCCCC;' : '')); ?>">
|
||||
<td class="text-center">
|
||||
<div class="d-flex justify-content-center">
|
||||
<button
|
||||
@ -1617,6 +1619,9 @@ require_once 'layout_header.php';
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
<span class="badge rounded-pill <?php echo htmlspecialchars($badgeClass); ?>"><?php echo htmlspecialchars(cc_test_state_label($order['estado'])); ?></span>
|
||||
<?php if (!empty($order['pedido_repetido_en_tienda'])): ?>
|
||||
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis border"><?php echo htmlspecialchars((string) ($order['pedido_repetido_en_tienda_label'] ?? 'Pedido repetido en tienda')); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="badge rounded-pill bg-light text-dark border"><span class="js-call-count-number" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"><?php echo (int) $order['total_llamadas']; ?></span> llamadas</span>
|
||||
<?php if (!empty($logisticaPendienteLabel)): ?>
|
||||
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border"><?php echo htmlspecialchars($logisticaPendienteLabel); ?></span>
|
||||
|
||||
@ -250,6 +250,526 @@ function cc_test_is_tuani_main_store_key(string $storeKey): bool
|
||||
return $storeKey === 'otra_tienda';
|
||||
}
|
||||
|
||||
if (!function_exists('cc_test_normalize_phone_digits')) {
|
||||
function cc_test_normalize_phone_digits(string $value): string
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', trim($value));
|
||||
return is_string($digits) ? $digits : '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cc_test_phone_match_keys')) {
|
||||
function cc_test_phone_match_keys(string $value): array
|
||||
{
|
||||
$digits = cc_test_normalize_phone_digits($value);
|
||||
if ($digits === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$keys = [$digits];
|
||||
|
||||
if (strlen($digits) > 2 && strncmp($digits, '00', 2) === 0) {
|
||||
$withoutInternationalPrefix = substr($digits, 2);
|
||||
if ($withoutInternationalPrefix !== '') {
|
||||
$keys[] = $withoutInternationalPrefix;
|
||||
if (strlen($withoutInternationalPrefix) > 9 && strncmp($withoutInternationalPrefix, '51', 2) === 0) {
|
||||
$keys[] = substr($withoutInternationalPrefix, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen($digits) > 9 && strncmp($digits, '51', 2) === 0) {
|
||||
$keys[] = substr($digits, 2);
|
||||
}
|
||||
|
||||
$keys = array_values(array_unique(array_filter($keys, static fn ($key) => trim((string) $key) !== '')));
|
||||
return $keys;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cc_test_fetch_store_phone_index')) {
|
||||
function cc_test_fetch_store_phone_index(PDO $pdo, string $storeKey): array
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
$storeKey = trim(mb_strtolower($storeKey));
|
||||
if ($storeKey === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (array_key_exists($storeKey, $cache)) {
|
||||
return $cache[$storeKey];
|
||||
}
|
||||
|
||||
if (!cc_test_table_exists($pdo, 'callcenter_test_orders')) {
|
||||
$cache[$storeKey] = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT DISTINCT celular FROM callcenter_test_orders WHERE store_key = ? AND celular IS NOT NULL AND celular <> ''");
|
||||
$stmt->execute([$storeKey]);
|
||||
|
||||
$index = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $phoneValue) {
|
||||
foreach (cc_test_phone_match_keys((string) $phoneValue) as $phoneKey) {
|
||||
$index[$phoneKey] = true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
error_log('cc_test_fetch_store_phone_index: ' . $exception->getMessage());
|
||||
$index = [];
|
||||
}
|
||||
|
||||
$cache[$storeKey] = $index;
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cc_test_mark_recoverables_duplicate_orders')) {
|
||||
function cc_test_mark_recoverables_duplicate_orders(PDO $pdo, array $orders, string $storeKey, string $mainStoreKey = 'otra_tienda'): array
|
||||
{
|
||||
$storeKey = trim(mb_strtolower($storeKey));
|
||||
if ($storeKey !== 'tuani_recuperables') {
|
||||
return $orders;
|
||||
}
|
||||
|
||||
$mainStoreKey = trim(mb_strtolower($mainStoreKey));
|
||||
if ($mainStoreKey === '') {
|
||||
return $orders;
|
||||
}
|
||||
|
||||
$mainPhoneIndex = cc_test_fetch_store_phone_index($pdo, $mainStoreKey);
|
||||
foreach ($orders as &$order) {
|
||||
$isDuplicate = false;
|
||||
foreach (cc_test_phone_match_keys((string) ($order['celular'] ?? '')) as $phoneKey) {
|
||||
if (isset($mainPhoneIndex[$phoneKey])) {
|
||||
$isDuplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$order['pedido_repetido_en_tienda'] = $isDuplicate;
|
||||
$order['pedido_repetido_en_tienda_label'] = $isDuplicate ? 'Pedido repetido en tienda' : '';
|
||||
}
|
||||
unset($order);
|
||||
|
||||
return $orders;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cc_test_ensure_reparto_settings_table')) {
|
||||
function cc_test_ensure_reparto_settings_table(PDO $pdo): void
|
||||
{
|
||||
static $checked = false;
|
||||
if ($checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_reparto_settings` (
|
||||
`store_key` VARCHAR(50) NOT NULL,
|
||||
`reparto_mode` VARCHAR(20) NOT NULL DEFAULT 'manual',
|
||||
`rotation_date` DATE NULL,
|
||||
`rotation_index` INT NOT NULL DEFAULT 0,
|
||||
`assessor_states_json` LONGTEXT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`store_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
|
||||
$checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_normalize_reparto_mode(?string $value): string
|
||||
{
|
||||
$value = mb_strtolower(trim((string) $value));
|
||||
return in_array($value, ['automatico', 'automático', 'auto'], true) ? 'automatico' : 'manual';
|
||||
}
|
||||
|
||||
function cc_test_reparto_mode_label(string $mode): string
|
||||
{
|
||||
return cc_test_normalize_reparto_mode($mode) === 'automatico' ? 'Automático' : 'Manual';
|
||||
}
|
||||
|
||||
function cc_test_normalize_reparto_state(?string $value): string
|
||||
{
|
||||
$value = mb_strtolower(trim((string) $value));
|
||||
|
||||
return match ($value) {
|
||||
'pausada', 'pausado' => 'pausada',
|
||||
'ausente', 'ausencia' => 'ausente',
|
||||
default => 'disponible',
|
||||
};
|
||||
}
|
||||
|
||||
function cc_test_reparto_state_label(string $state): string
|
||||
{
|
||||
return match (cc_test_normalize_reparto_state($state)) {
|
||||
'pausada' => 'Pausada',
|
||||
'ausente' => 'Ausente',
|
||||
default => 'Disponible',
|
||||
};
|
||||
}
|
||||
|
||||
function cc_test_reparto_state_badge_class(string $state): string
|
||||
{
|
||||
return match (cc_test_normalize_reparto_state($state)) {
|
||||
'pausada' => 'bg-warning-subtle text-warning-emphasis border',
|
||||
'ausente' => 'bg-secondary-subtle text-secondary-emphasis border',
|
||||
default => 'bg-success-subtle text-success-emphasis border',
|
||||
};
|
||||
}
|
||||
|
||||
function cc_test_ordered_assessor_keys(array $assessors): array
|
||||
{
|
||||
$ordered = [];
|
||||
$defaultKeys = cc_test_default_assessor_keys();
|
||||
|
||||
foreach ($defaultKeys as $key) {
|
||||
if (isset($assessors[$key])) {
|
||||
$ordered[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$remaining = array_values(array_diff(array_keys($assessors), $ordered));
|
||||
if (!empty($remaining)) {
|
||||
sort($remaining, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
$ordered = array_merge($ordered, $remaining);
|
||||
}
|
||||
|
||||
return $ordered;
|
||||
}
|
||||
|
||||
function cc_test_reparto_normalize_states($states, array $assessors): array
|
||||
{
|
||||
$states = is_array($states) ? $states : [];
|
||||
$normalized = [];
|
||||
|
||||
foreach (cc_test_ordered_assessor_keys($assessors) as $assessorKey) {
|
||||
$candidateKey = cc_test_normalize_user_key((string) $assessorKey);
|
||||
$normalized[$candidateKey] = cc_test_normalize_reparto_state($states[$assessorKey] ?? $states[$candidateKey] ?? 'disponible');
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
function cc_test_reparto_preview(array $assessors, array $settings): array
|
||||
{
|
||||
$orderedKeys = cc_test_ordered_assessor_keys($assessors);
|
||||
$states = cc_test_reparto_normalize_states($settings['assessor_states'] ?? [], $assessors);
|
||||
$sequence = [];
|
||||
$availableCount = 0;
|
||||
|
||||
foreach ($orderedKeys as $assessorKey) {
|
||||
$state = $states[$assessorKey] ?? 'disponible';
|
||||
$isAvailable = $state === 'disponible';
|
||||
if ($isAvailable) {
|
||||
$availableCount++;
|
||||
}
|
||||
|
||||
$sequence[] = [
|
||||
'key' => $assessorKey,
|
||||
'label' => (string) ($assessors[$assessorKey]['label'] ?? $assessorKey),
|
||||
'state' => $state,
|
||||
'state_label' => cc_test_reparto_state_label($state),
|
||||
'badge_class' => cc_test_reparto_state_badge_class($state),
|
||||
'available' => $isAvailable,
|
||||
'color_hex' => $assessors[$assessorKey]['color_hex'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
$totalCount = count($orderedKeys);
|
||||
$rotationIndex = max(0, (int) ($settings['rotation_index'] ?? 0));
|
||||
$todayKey = (new DateTimeImmutable('today'))->format('Y-m-d');
|
||||
$rotationDate = trim((string) ($settings['rotation_date'] ?? ''));
|
||||
$effectiveIndex = ($totalCount > 0 && $rotationDate === $todayKey) ? ($rotationIndex % $totalCount) : 0;
|
||||
|
||||
$nextKey = null;
|
||||
$nextPosition = null;
|
||||
if ($totalCount > 0) {
|
||||
for ($offset = 0; $offset < $totalCount; $offset++) {
|
||||
$position = ($effectiveIndex + $offset) % $totalCount;
|
||||
$candidateKey = $orderedKeys[$position];
|
||||
if (($states[$candidateKey] ?? 'disponible') === 'disponible') {
|
||||
$nextKey = $candidateKey;
|
||||
$nextPosition = $position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => cc_test_normalize_reparto_mode($settings['reparto_mode'] ?? null),
|
||||
'mode_label' => cc_test_reparto_mode_label((string) ($settings['reparto_mode'] ?? 'manual')),
|
||||
'sequence' => $sequence,
|
||||
'ordered_keys' => $orderedKeys,
|
||||
'states' => $states,
|
||||
'available_count' => $availableCount,
|
||||
'total_count' => $totalCount,
|
||||
'rotation_index' => $effectiveIndex,
|
||||
'rotation_date' => $rotationDate !== '' ? $rotationDate : null,
|
||||
'next_key' => $nextKey,
|
||||
'next_label' => $nextKey !== null ? (string) ($assessors[$nextKey]['label'] ?? $nextKey) : '',
|
||||
'next_position' => $nextPosition,
|
||||
];
|
||||
}
|
||||
|
||||
function cc_test_fetch_reparto_settings(PDO $pdo, string $storeKey, array $assessors = []): array
|
||||
{
|
||||
cc_test_ensure_reparto_settings_table($pdo);
|
||||
|
||||
$storeKey = trim(mb_strtolower($storeKey));
|
||||
$defaults = [
|
||||
'store_key' => $storeKey,
|
||||
'reparto_mode' => 'manual',
|
||||
'rotation_date' => null,
|
||||
'rotation_index' => 0,
|
||||
'assessor_states' => cc_test_reparto_normalize_states([], $assessors),
|
||||
];
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare('SELECT store_key, reparto_mode, rotation_date, rotation_index, assessor_states_json FROM callcenter_test_reparto_settings WHERE store_key = ? LIMIT 1');
|
||||
$stmt->execute([$storeKey]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) {
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
$rawStates = [];
|
||||
$statesJson = trim((string) ($row['assessor_states_json'] ?? ''));
|
||||
if ($statesJson !== '') {
|
||||
$decoded = json_decode($statesJson, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$rawStates = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'store_key' => $storeKey,
|
||||
'reparto_mode' => cc_test_normalize_reparto_mode($row['reparto_mode'] ?? null),
|
||||
'rotation_date' => trim((string) ($row['rotation_date'] ?? '')) !== '' ? (string) $row['rotation_date'] : null,
|
||||
'rotation_index' => max(0, (int) ($row['rotation_index'] ?? 0)),
|
||||
'assessor_states' => cc_test_reparto_normalize_states($rawStates, $assessors),
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
error_log('cc_test_fetch_reparto_settings: ' . $exception->getMessage());
|
||||
return $defaults;
|
||||
}
|
||||
}
|
||||
|
||||
function cc_test_upsert_reparto_settings(PDO $pdo, string $storeKey, array $settings): void
|
||||
{
|
||||
cc_test_ensure_reparto_settings_table($pdo);
|
||||
|
||||
$storeKey = trim(mb_strtolower($storeKey));
|
||||
$mode = cc_test_normalize_reparto_mode($settings['reparto_mode'] ?? null);
|
||||
$rotationDate = trim((string) ($settings['rotation_date'] ?? ''));
|
||||
if ($rotationDate !== '') {
|
||||
try {
|
||||
$rotationDate = (new DateTimeImmutable($rotationDate))->format('Y-m-d');
|
||||
} catch (Throwable $exception) {
|
||||
$rotationDate = null;
|
||||
}
|
||||
} else {
|
||||
$rotationDate = null;
|
||||
}
|
||||
|
||||
$rotationIndex = max(0, (int) ($settings['rotation_index'] ?? 0));
|
||||
$assessorStates = $settings['assessor_states'] ?? [];
|
||||
if (!is_array($assessorStates)) {
|
||||
$assessorStates = [];
|
||||
}
|
||||
|
||||
$statesJson = json_encode($assessorStates, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($statesJson === false) {
|
||||
$statesJson = '{}';
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO callcenter_test_reparto_settings (store_key, reparto_mode, rotation_date, rotation_index, assessor_states_json)
|
||||
VALUES (:store_key, :reparto_mode, :rotation_date, :rotation_index, :assessor_states_json)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
reparto_mode = VALUES(reparto_mode),
|
||||
rotation_date = VALUES(rotation_date),
|
||||
rotation_index = VALUES(rotation_index),
|
||||
assessor_states_json = VALUES(assessor_states_json),
|
||||
updated_at = CURRENT_TIMESTAMP");
|
||||
|
||||
$stmt->bindValue(':store_key', $storeKey, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':reparto_mode', $mode, PDO::PARAM_STR);
|
||||
if ($rotationDate === null) {
|
||||
$stmt->bindValue(':rotation_date', null, PDO::PARAM_NULL);
|
||||
} else {
|
||||
$stmt->bindValue(':rotation_date', $rotationDate, PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->bindValue(':rotation_index', $rotationIndex, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':assessor_states_json', $statesJson, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
function cc_test_reparto_settings_signature(array $settings): string
|
||||
{
|
||||
$states = $settings['assessor_states'] ?? [];
|
||||
if (!is_array($states)) {
|
||||
$states = [];
|
||||
}
|
||||
|
||||
ksort($states);
|
||||
$payload = [
|
||||
'mode' => cc_test_normalize_reparto_mode($settings['reparto_mode'] ?? null),
|
||||
'rotation_date' => trim((string) ($settings['rotation_date'] ?? '')),
|
||||
'rotation_index' => max(0, (int) ($settings['rotation_index'] ?? 0)),
|
||||
'assessor_states' => $states,
|
||||
];
|
||||
|
||||
return sha1(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
function cc_test_apply_daily_reparto(PDO $pdo, string $storeKey, array $orders, array $assessors, array $settings = []): array
|
||||
{
|
||||
cc_test_ensure_reparto_settings_table($pdo);
|
||||
|
||||
$storeKey = trim(mb_strtolower($storeKey));
|
||||
$currentSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors);
|
||||
if (!empty($settings)) {
|
||||
$currentSettings = array_replace($currentSettings, $settings);
|
||||
$currentSettings['assessor_states'] = cc_test_reparto_normalize_states($currentSettings['assessor_states'] ?? [], $assessors);
|
||||
}
|
||||
|
||||
$preview = cc_test_reparto_preview($assessors, $currentSettings);
|
||||
$orderedKeys = $preview['ordered_keys'] ?? [];
|
||||
$states = $preview['states'] ?? [];
|
||||
$totalCount = (int) ($preview['total_count'] ?? 0);
|
||||
$todayKey = (new DateTimeImmutable('today'))->format('Y-m-d');
|
||||
$effectiveIndex = (int) ($preview['rotation_index'] ?? 0);
|
||||
|
||||
if ($totalCount <= 0) {
|
||||
return [
|
||||
'assigned_count' => 0,
|
||||
'rotation_index' => 0,
|
||||
'settings' => $currentSettings,
|
||||
'preview' => $preview,
|
||||
'message' => 'No hay asesoras configuradas para repartir.',
|
||||
];
|
||||
}
|
||||
|
||||
if (($currentSettings['rotation_date'] ?? null) !== $todayKey) {
|
||||
$effectiveIndex = 0;
|
||||
} else {
|
||||
$effectiveIndex = $effectiveIndex % $totalCount;
|
||||
}
|
||||
|
||||
$pendingOrders = [];
|
||||
foreach ($orders as $order) {
|
||||
if (!empty($order['eliminado'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int) ($order['user_id'] ?? 0) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trim((string) ($order['source_key'] ?? '')) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pendingOrders[] = $order;
|
||||
}
|
||||
|
||||
usort($pendingOrders, static function (array $a, array $b): int {
|
||||
$aTime = 0;
|
||||
foreach (['drive_imported_at', 'first_seen_at'] as $field) {
|
||||
$value = trim((string) ($a[$field] ?? ''));
|
||||
if ($value !== '') {
|
||||
$parsed = strtotime($value);
|
||||
if ($parsed !== false) {
|
||||
$aTime = $parsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$bTime = 0;
|
||||
foreach (['drive_imported_at', 'first_seen_at'] as $field) {
|
||||
$value = trim((string) ($b[$field] ?? ''));
|
||||
if ($value !== '') {
|
||||
$parsed = strtotime($value);
|
||||
if ($parsed !== false) {
|
||||
$bTime = $parsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($aTime !== $bTime) {
|
||||
return $aTime <=> $bTime;
|
||||
}
|
||||
|
||||
$aRow = (int) ($a['source_row'] ?? 0);
|
||||
$bRow = (int) ($b['source_row'] ?? 0);
|
||||
if ($aRow !== $bRow) {
|
||||
return $aRow <=> $bRow;
|
||||
}
|
||||
|
||||
$aId = (int) ($a['id'] ?? 0);
|
||||
$bId = (int) ($b['id'] ?? 0);
|
||||
if ($aId !== $bId) {
|
||||
return $aId <=> $bId;
|
||||
}
|
||||
|
||||
return strcmp((string) ($a['source_key'] ?? ''), (string) ($b['source_key'] ?? ''));
|
||||
});
|
||||
|
||||
$assignedCount = 0;
|
||||
$totalOrders = count($orderedKeys);
|
||||
foreach ($pendingOrders as $order) {
|
||||
$chosenKey = null;
|
||||
$chosenPosition = null;
|
||||
|
||||
for ($offset = 0; $offset < $totalOrders; $offset++) {
|
||||
$position = ($effectiveIndex + $offset) % $totalOrders;
|
||||
$candidateKey = $orderedKeys[$position];
|
||||
if (($states[$candidateKey] ?? 'disponible') === 'disponible') {
|
||||
$chosenKey = $candidateKey;
|
||||
$chosenPosition = $position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($chosenKey === null || $chosenPosition === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
$chosenAssessor = $assessors[$chosenKey] ?? null;
|
||||
$userId = (int) ($chosenAssessor['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cc_test_upsert_assignee($pdo, (string) $order['source_key'], $userId);
|
||||
$assignedCount++;
|
||||
$effectiveIndex = ($chosenPosition + 1) % $totalOrders;
|
||||
}
|
||||
|
||||
if ($assignedCount > 0) {
|
||||
$currentSettings['rotation_date'] = $todayKey;
|
||||
$currentSettings['rotation_index'] = $effectiveIndex;
|
||||
cc_test_upsert_reparto_settings($pdo, $storeKey, $currentSettings);
|
||||
}
|
||||
|
||||
$preview = cc_test_reparto_preview($assessors, $currentSettings);
|
||||
|
||||
return [
|
||||
'assigned_count' => $assignedCount,
|
||||
'rotation_index' => $effectiveIndex,
|
||||
'settings' => $currentSettings,
|
||||
'preview' => $preview,
|
||||
'message' => $assignedCount > 0
|
||||
? 'Se asignaron ' . $assignedCount . ' pedidos.'
|
||||
: 'No había pedidos pendientes o no había asesoras disponibles.',
|
||||
];
|
||||
}
|
||||
|
||||
function cc_test_fetch_assessors(PDO $pdo, array $allowedNames = []): array
|
||||
{
|
||||
if (empty($allowedNames)) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user