From 9da54710dc75915eb3e2b4f6a716a9422594ab59 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 24 Jul 2026 10:04:49 +0000 Subject: [PATCH] Autosave: 20260724-100614 --- call_center_pro.php | 34 +++++-- gestiones_callcenter.php | 57 +++++++----- includes/callcenter_test_helpers.php | 132 ++++++++++++++++++++++++++- reparto_auto_worker.php | 42 +++++++++ 4 files changed, 235 insertions(+), 30 deletions(-) create mode 100644 reparto_auto_worker.php diff --git a/call_center_pro.php b/call_center_pro.php index c6103ed7..0b45ae3a 100644 --- a/call_center_pro.php +++ b/call_center_pro.php @@ -586,26 +586,46 @@ try { throw new RuntimeException('El reparto intercalado solo está disponible en TUANI principal.'); } - $repartoResult = cc_test_apply_daily_reparto($pdo, $storeKey, $orders, $assessors, $repartoSettings); + $repartoResult = cc_test_run_locked_reparto_job( + $pdo, + $storeKey, + $assessors, + static function () use ($pdo, $storeKey): array { + return cc_test_load_reparto_orders($pdo, $storeKey); + }, + $repartoSettings, + static function (array $orders) use ($storeKey, $storeConfig): array { + return cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig); + } + ); $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); + $orders = cc_test_load_reparto_orders($pdo, $storeKey); } $repartoSettings = $repartoResult['settings'] ?? $repartoSettings; } } - if ($isMainTuaniStore && ($repartoSettings['reparto_mode'] ?? 'manual') === 'automatico') { - $autoRepartoResult = cc_test_apply_daily_reparto($pdo, $storeKey, $orders, $assessors, $repartoSettings); + if ($isMainTuaniStore && cc_test_reparto_mode_is_auto($repartoSettings['reparto_mode'] ?? 'manual')) { + $autoRepartoResult = cc_test_run_locked_reparto_job( + $pdo, + $storeKey, + $assessors, + static function () use ($pdo, $storeKey): array { + return cc_test_load_reparto_orders($pdo, $storeKey); + }, + $repartoSettings, + static function (array $orders) use ($storeKey, $storeConfig): array { + return cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig); + } + ); $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); + $orders = cc_test_load_reparto_orders($pdo, $storeKey); } $repartoSettings = $autoRepartoResult['settings'] ?? $repartoSettings; } diff --git a/gestiones_callcenter.php b/gestiones_callcenter.php index ebed91e8..219c6ebd 100644 --- a/gestiones_callcenter.php +++ b/gestiones_callcenter.php @@ -775,7 +775,7 @@ try { $postedStates = []; } - $repartoSettings['reparto_mode'] = 'manual'; + $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.'; @@ -784,42 +784,51 @@ try { 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); - } + $repartoResult = cc_test_run_locked_reparto_job( + $pdo, + $storeKey, + $assessors, + static function () use ($pdo, $storeKey): array { + return cc_test_load_reparto_orders($pdo, $storeKey); + }, + $repartoSettings, + static function (array $orders) use ($storeKey, $storeConfig): array { + return cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig); + } + ); $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); + $orders = cc_test_load_reparto_orders($pdo, $storeKey); } $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); + if ($isMainTuaniStore && cc_test_reparto_mode_is_auto($repartoSettings['reparto_mode'] ?? 'manual')) { + $autoRepartoResult = cc_test_run_locked_reparto_job( + $pdo, + $storeKey, + $assessors, + static function () use ($pdo, $storeKey): array { + return cc_test_load_reparto_orders($pdo, $storeKey); + }, + $repartoSettings, + static function (array $orders) use ($storeKey, $storeConfig): array { + return cc_test_filter_reparto_orders($orders, $storeKey, $storeConfig); + } + ); $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); + $orders = cc_test_load_reparto_orders($pdo, $storeKey); } $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); @@ -1672,7 +1681,7 @@ require_once 'layout_header.php'; +
Automático reparte los pedidos pendientes al abrir o refrescar la pantalla.
diff --git a/includes/callcenter_test_helpers.php b/includes/callcenter_test_helpers.php index 78acfd97..cd257a4a 100644 --- a/includes/callcenter_test_helpers.php +++ b/includes/callcenter_test_helpers.php @@ -392,12 +392,25 @@ if (!function_exists('cc_test_ensure_reparto_settings_table')) { 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'; + + if (in_array($value, ['automatico', 'automático', 'auto', 'hibrido', 'híbrido', 'hybrid'], true)) { + return 'automatico'; + } + + return 'manual'; } function cc_test_reparto_mode_label(string $mode): string { - return cc_test_normalize_reparto_mode($mode) === 'automatico' ? 'Automático' : 'Manual'; + return match (cc_test_normalize_reparto_mode($mode)) { + 'automatico' => 'Automático', + default => 'Manual', + }; +} + +function cc_test_reparto_mode_is_auto(?string $mode): bool +{ + return cc_test_normalize_reparto_mode($mode) === 'automatico'; } function cc_test_normalize_reparto_state(?string $value): string @@ -615,6 +628,121 @@ function cc_test_upsert_reparto_settings(PDO $pdo, string $storeKey, array $sett $stmt->execute(); } +function cc_test_reparto_lock_name(string $storeKey): string +{ + $storeKey = trim(mb_strtolower($storeKey)); + return 'cc_test_auto_reparto_' . substr(sha1($storeKey), 0, 24); +} + +function cc_test_acquire_named_lock(PDO $pdo, string $lockName, int $timeoutSeconds = 0): bool +{ + try { + $stmt = $pdo->prepare('SELECT GET_LOCK(:lock_name, :timeout_seconds)'); + $stmt->bindValue(':lock_name', $lockName, PDO::PARAM_STR); + $stmt->bindValue(':timeout_seconds', $timeoutSeconds, PDO::PARAM_INT); + $stmt->execute(); + $result = $stmt->fetchColumn(); + + return (int) $result === 1; + } catch (Throwable $exception) { + error_log('cc_test_acquire_named_lock: ' . $exception->getMessage()); + return false; + } +} + +function cc_test_release_named_lock(PDO $pdo, string $lockName): void +{ + try { + $stmt = $pdo->prepare('SELECT RELEASE_LOCK(:lock_name)'); + $stmt->bindValue(':lock_name', $lockName, PDO::PARAM_STR); + $stmt->execute(); + } catch (Throwable $exception) { + error_log('cc_test_release_named_lock: ' . $exception->getMessage()); + } +} + +function cc_test_load_reparto_orders(PDO $pdo, string $storeKey): array +{ + $storeKey = trim(mb_strtolower($storeKey)); + if ($storeKey === '') { + return []; + } + + $availableStores = function_exists('drive_test_available_stores') ? drive_test_available_stores() : []; + $storeConfig = $availableStores[$storeKey] ?? []; + + if (!empty($storeConfig['incremental_sync']) && function_exists('drive_test_sync_orders_incremental')) { + drive_test_sync_orders_incremental($pdo, $storeKey, 0); + } + + if (!function_exists('drive_test_fetch_orders_from_db') || !function_exists('drive_test_fetch_tracking') || !function_exists('drive_test_merge_tracking')) { + return []; + } + + $orders = drive_test_fetch_orders_from_db($pdo, $storeKey); + $tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key')); + $orders = drive_test_merge_tracking($orders, $tracking); + + return array_values(array_filter($orders, static function (array $order): bool { + return empty($order['eliminado']); + })); +} + +function cc_test_run_locked_reparto_job(PDO $pdo, string $storeKey, array $assessors, callable $loadOrders, array $settings = [], ?callable $filterOrders = null, int $lockTimeoutSeconds = 5): array +{ + $lockName = cc_test_reparto_lock_name($storeKey); + + if (!cc_test_acquire_named_lock($pdo, $lockName, $lockTimeoutSeconds)) { + $currentSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors); + return [ + 'assigned_count' => 0, + 'rotation_index' => (int) ($currentSettings['rotation_index'] ?? 0), + 'settings' => $currentSettings, + 'preview' => cc_test_reparto_preview($assessors, $currentSettings), + 'message' => 'El reparto automático ya se está ejecutando en otro proceso.', + 'locked' => false, + ]; + } + + try { + $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); + } + + $orders = $loadOrders(); + if (!is_array($orders)) { + $orders = []; + } + + if ($filterOrders !== null) { + $filteredOrders = $filterOrders($orders); + if (is_array($filteredOrders)) { + $orders = array_values($filteredOrders); + } + } + + if (empty($orders)) { + return [ + 'assigned_count' => 0, + 'rotation_index' => (int) ($currentSettings['rotation_index'] ?? 0), + 'settings' => $currentSettings, + 'preview' => cc_test_reparto_preview($assessors, $currentSettings), + 'message' => 'No hay pedidos pendientes para repartir.', + 'locked' => true, + ]; + } + + $result = cc_test_apply_daily_reparto($pdo, $storeKey, $orders, $assessors, $currentSettings); + $result['locked'] = true; + + return $result; + } finally { + cc_test_release_named_lock($pdo, $lockName); + } +} + function cc_test_reparto_settings_signature(array $settings): string { $states = $settings['assessor_states'] ?? []; diff --git a/reparto_auto_worker.php b/reparto_auto_worker.php new file mode 100644 index 00000000..176241a9 --- /dev/null +++ b/reparto_auto_worker.php @@ -0,0 +1,42 @@ +