Autosave: 20260724-100614

This commit is contained in:
Flatlogic Bot 2026-07-24 10:04:49 +00:00
parent 5437326dee
commit 9da54710dc
4 changed files with 235 additions and 30 deletions

View File

@ -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;
}

View File

@ -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';
<?php if ($isMainTuaniStore): ?>
<?php
$repartoModeLabel = (string) ($repartoPreview['mode_label'] ?? 'Manual');
$repartoModeClass = ($repartoPreview['mode'] ?? 'manual') === 'automatico'
$repartoModeClass = cc_test_reparto_mode_is_auto((string) ($repartoPreview['mode'] ?? 'manual'))
? 'bg-success-subtle text-success-emphasis border'
: 'bg-secondary-subtle text-secondary-emphasis border';
$repartoNextLabel = (string) ($repartoPreview['next_label'] ?? '');
@ -1721,8 +1730,14 @@ require_once 'layout_header.php';
<div class="mb-2">
<label class="form-label small text-muted fw-semibold mb-1" for="repartoMode">Modo de reparto</label>
<select name="reparto_mode" id="repartoMode" class="form-select form-select-sm">
<option value="manual"<?php echo ($repartoPreview['mode'] ?? 'manual') === 'manual' ? ' selected' : ''; ?>>Manual</option>
<?php foreach ([
'manual' => 'Manual',
'automatico' => 'Automático',
] as $modeValue => $modeLabel): ?>
<option value="<?php echo htmlspecialchars($modeValue); ?>"<?php echo ($repartoPreview['mode'] ?? 'manual') === $modeValue ? ' selected' : ''; ?>><?php echo htmlspecialchars($modeLabel); ?></option>
<?php endforeach; ?>
</select>
<div class="form-text">Automático reparte los pedidos pendientes al abrir o refrescar la pantalla.</div>
</div>
<div class="row row-cols-1 row-cols-md-2 g-2 cc-reparto-state-grid">
<?php foreach (($repartoPreview['sequence'] ?? []) as $item): ?>

View File

@ -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'] ?? [];

42
reparto_auto_worker.php Normal file
View File

@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/callcenter_test_helpers.php';
require_once __DIR__ . '/includes/drive_test_orders.php';
if (php_sapi_name() !== 'cli') {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo "Este worker solo se ejecuta por CLI." . PHP_EOL;
exit(0);
}
$pdo = db();
$storeKey = 'otra_tienda';
$availableStores = drive_test_available_stores();
$storeConfig = $availableStores[$storeKey] ?? [];
$assessors = cc_test_fetch_assessors($pdo);
$repartoSettings = cc_test_fetch_reparto_settings($pdo, $storeKey, $assessors);
if (!cc_test_reparto_mode_is_auto($repartoSettings['reparto_mode'] ?? 'manual')) {
echo '[' . date('Y-m-d H:i:s') . '] Reparto automático omitido: modo manual en ' . $storeKey . PHP_EOL;
exit(0);
}
$result = 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) ($result['assigned_count'] ?? 0);
$message = (string) ($result['message'] ?? '');
$status = !empty($result['locked']) ? 'ok' : 'busy';
echo '[' . date('Y-m-d H:i:s') . '] ' . $storeKey . ' | estado=' . $status . ' | asignados=' . $assignedCount . ' | ' . $message . PHP_EOL;