diff --git a/api/save_menu.php b/api/save_menu.php index 6a9ef14..136eae2 100644 --- a/api/save_menu.php +++ b/api/save_menu.php @@ -87,6 +87,12 @@ try { // Mapear status a is_active: 'active' -> 1, cualquier otra cosa -> 0 $menuData['is_active'] = (($input['status'] ?? 'active') === 'active') ? 1 : 0; } + + // Guardar tipo de menú (main | no_advisor) + if (in_array('menu_type', $availableColumns)) { + $rawType = $input['menu_type'] ?? 'main'; + $menuData['menu_type'] = in_array($rawType, ['main', 'no_advisor'], true) ? $rawType : 'main'; + } if (in_array('order_position', $availableColumns) && !$isUpdate) { // Solo establecer order_position para menús nuevos diff --git a/api/set_advisor_mode.php b/api/set_advisor_mode.php new file mode 100644 index 0000000..448cc3d --- /dev/null +++ b/api/set_advisor_mode.php @@ -0,0 +1,70 @@ +fetch( + "SELECT config_value FROM system_config WHERE config_key = 'advisor_available' LIMIT 1" + ); + $available = $row ? (bool)(int)$row['config_value'] : true; + echo json_encode(['success' => true, 'available' => $available]); + exit; + } + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['success' => false, 'error' => 'Método no permitido']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true); + if (!isset($input['available'])) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Falta el campo "available"']); + exit; + } + + $value = $input['available'] ? '1' : '0'; + + // Upsert en system_config + $existing = $db->fetch( + "SELECT id FROM system_config WHERE config_key = 'advisor_available' LIMIT 1" + ); + + if ($existing) { + $db->update( + 'system_config', + ['config_value' => $value, 'updated_at' => date('Y-m-d H:i:s')], + 'config_key = :k', + ['k' => 'advisor_available'] + ); + } else { + $db->insert('system_config', [ + 'config_key' => 'advisor_available', + 'config_value' => $value, + 'description' => 'Indica si hay asesores disponibles (1=sí, 0=no).', + 'created_at' => date('Y-m-d H:i:s'), + ]); + } + + $label = $value === '1' ? 'disponible' : 'ausente'; + error_log("[AdvisorMode] Modo asesor cambiado a '{$label}' por usuario=" . ($_SESSION['user_id'] ?? 'unknown')); + + echo json_encode(['success' => true, 'available' => (bool)(int)$value, 'label' => $label]); + +} catch (Exception $e) { + error_log('[set_advisor_mode] Error: ' . $e->getMessage()); + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Error interno del servidor']); +} diff --git a/assets/js/app.js b/assets/js/app.js index 50b2d58..a271256 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -785,7 +785,8 @@ class WhatsAppBotManager { title: document.getElementById('menu-title').value, description: document.getElementById('menu-description').value, parent_id: document.getElementById('menu-parent').value || null, - is_active: document.getElementById('menu-active').checked ? 1 : 0 + is_active: document.getElementById('menu-active').checked ? 1 : 0, + menu_type: document.getElementById('menu-type')?.value || 'main' }; try { diff --git a/assets/js/app_simple.js b/assets/js/app_simple.js index e47b61a..0374f61 100644 --- a/assets/js/app_simple.js +++ b/assets/js/app_simple.js @@ -1147,7 +1147,8 @@ class SimpleWhatsAppManager { menu_key: document.getElementById('menu-key')?.value || '', description: document.getElementById('menu-description')?.value || '', welcome_message: document.getElementById('menu-welcome-message')?.value || '', - status: document.getElementById('menu-status')?.value || 'active' + status: document.getElementById('menu-status')?.value || 'active', + menu_type: document.getElementById('menu-type')?.value || 'main' }; // Validar campos requeridos @@ -3841,6 +3842,13 @@ function showEditMenuModal(menuData) { + + @@ -4007,13 +4015,14 @@ window.saveMenuEdit = async function() { const menuDescription = document.getElementById('editMenuDescription').value.trim(); const menuWelcome = document.getElementById('editMenuWelcome').value.trim(); const menuStatus = document.getElementById('editMenuStatus').value; + const menuType = document.getElementById('editMenuType')?.value || 'main'; if (!menuName || !menuKey) { window.whatsappManager.showError('Nombre y clave del menú son obligatorios'); return; } - console.log('Guardando menú editado:', { menuId, menuName, menuKey, menuStatus }); + console.log('Guardando menú editado:', { menuId, menuName, menuKey, menuStatus, menuType }); const menuData = { id: menuId, @@ -4021,7 +4030,8 @@ window.saveMenuEdit = async function() { menu_key: menuKey, description: menuDescription, welcome_message: menuWelcome, - status: menuStatus + status: menuStatus, + menu_type: menuType }; try { diff --git a/conversations.php b/conversations.php index aa4c0ab..5941ec3 100644 --- a/conversations.php +++ b/conversations.php @@ -879,6 +879,17 @@ if (!isUserLoggedIn()) { v1.4.0- + +
+ Asesor: + + +
@@ -8032,5 +8043,56 @@ const citasModal = (() => {
+ + \ No newline at end of file diff --git a/index.php b/index.php index ef76387..db3dd23 100644 --- a/index.php +++ b/index.php @@ -1769,12 +1769,26 @@ try {
-
- - +
+
+
+ + +
+
+
+
+ + + El menú Asesor ausente se muestra cuando el asesor está marcado como no disponible. +
+

diff --git a/migrations/20260527_add_menu_type_advisor_mode.sql b/migrations/20260527_add_menu_type_advisor_mode.sql new file mode 100644 index 0000000..ae58ec6 --- /dev/null +++ b/migrations/20260527_add_menu_type_advisor_mode.sql @@ -0,0 +1,17 @@ +-- ===================================================== +-- MIGRACIÓN: Menú secundario para asesor ausente +-- Fecha: 27 de mayo de 2026 +-- ===================================================== + +-- 1. Agregar columna menu_type a la tabla menus +-- Valores: 'main' (menú principal normal), 'no_advisor' (menú cuando no hay asesor) +ALTER TABLE menus +ADD COLUMN IF NOT EXISTS menu_type ENUM('main','no_advisor') NOT NULL DEFAULT 'main' + COMMENT 'Tipo de menú: main=principal, no_advisor=aparece cuando no hay asesor disponible'; + +-- 2. Insertar config de disponibilidad de asesores (1=disponibles, 0=ausentes) +INSERT INTO system_config (config_key, config_value, description, created_at) +VALUES ('advisor_available', '1', 'Indica si hay asesores disponibles (1=sí, 0=no). Cuando es 0, el bot muestra el menú secundario no_advisor.', NOW()) +ON DUPLICATE KEY UPDATE description = VALUES(description); + +SELECT '✅ Migración 20260527 completada: menu_type y advisor_available' AS status; diff --git a/run_20260527_menu_type.php b/run_20260527_menu_type.php new file mode 100644 index 0000000..125bb75 --- /dev/null +++ b/run_20260527_menu_type.php @@ -0,0 +1,50 @@ +getConnection(); + +$steps = []; + +// 1. Agregar columna menu_type a menus (si no existe) +try { + $cols = $db->fetchAll("SHOW COLUMNS FROM menus LIKE 'menu_type'"); + if (empty($cols)) { + $conn->exec("ALTER TABLE menus ADD COLUMN menu_type ENUM('main','no_advisor') NOT NULL DEFAULT 'main' COMMENT 'Tipo de menú: main=principal, no_advisor=aparece cuando no hay asesor'"); + $steps[] = "✅ Columna menu_type agregada a menus"; + } else { + $steps[] = "⚠️ Columna menu_type ya existe en menus (omitida)"; + } +} catch (Exception $e) { + $steps[] = "❌ Error agregando menu_type: " . $e->getMessage(); +} + +// 2. Insertar/actualizar system_config advisor_available +try { + $existing = $db->fetch("SELECT id FROM system_config WHERE config_key = 'advisor_available' LIMIT 1"); + if ($existing) { + $steps[] = "⚠️ Config advisor_available ya existe (omitida)"; + } else { + $db->insert('system_config', [ + 'config_key' => 'advisor_available', + 'config_value' => '1', + 'description' => 'Indica si hay asesores disponibles (1=sí, 0=no). Cuando es 0, el bot muestra el menú secundario no_advisor.', + 'created_at' => date('Y-m-d H:i:s'), + ]); + $steps[] = "✅ Config advisor_available insertada (valor inicial: 1)"; + } +} catch (Exception $e) { + $steps[] = "❌ Error insertando config advisor_available: " . $e->getMessage(); +} + +foreach ($steps as $s) { + echo $s . "\n"; +} +echo "\n✔ Migración 20260527 completada.\n"; diff --git a/scheduled_messages.php b/scheduled_messages.php index 7eb0e01..12f38a5 100644 --- a/scheduled_messages.php +++ b/scheduled_messages.php @@ -267,18 +267,32 @@ if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== tru preview = `Plantilla: ${msg.template_display_name || msg.template_name}`; if (msg.template_body) { let bodyPreview = msg.template_body; - if (msg.template_parameters && Array.isArray(msg.template_parameters)) { - // Reemplazar todas las variables en orden de aparición - // Soporta tanto {{1}}, {{2}} como {{nombre_tema}}, {{fecha}} - const regex = /\{\{[^\}]+\}\}/g; - let paramIndex = 0; + + if (msg.template_parameters) { + const isArray = Array.isArray(msg.template_parameters); + const isObject = typeof msg.template_parameters === 'object' && !isArray; - bodyPreview = bodyPreview.replace(regex, (match) => { - const value = msg.template_parameters[paramIndex] || match; - paramIndex++; - return `${value}`; - }); + if (isArray) { + // Array indexado: [0] => "valor1", [1] => "valor2" + const regex = /\{\{[^\}]+\}\}/g; + let paramIndex = 0; + + bodyPreview = bodyPreview.replace(regex, (match) => { + const value = msg.template_parameters[paramIndex] || match; + paramIndex++; + return `${value}`; + }); + } else if (isObject) { + // Objeto asociativo: {nombre_tema: "valor1", fecha: "valor2"} + const regex = /\{\{([^\}]+)\}\}/g; + + bodyPreview = bodyPreview.replace(regex, (match, varName) => { + const value = msg.template_parameters[varName] || match; + return `${value}`; + }); + } } + preview += `
${bodyPreview}
`; } } else { diff --git a/services/BotService.php b/services/BotService.php index c6cd86b..081fdb2 100644 --- a/services/BotService.php +++ b/services/BotService.php @@ -790,21 +790,68 @@ class BotService { } /** - * Mostrar menú principal + * Mostrar menú principal. + * Si no hay asesores disponibles (advisor_available=0) y existe un menú de tipo + * 'no_advisor' activo, lo muestra primero en lugar del menú raíz. */ private function showMainMenu($phoneNumber) { try { error_log("[BotService] showMainMenu - phone={$phoneNumber} fetching root menu"); } catch (Throwable $t) {} + + // Verificar disponibilidad de asesores + $advisorAvailable = true; + try { + $cfg = $this->db->fetch( + "SELECT config_value FROM system_config WHERE config_key = 'advisor_available' LIMIT 1" + ); + if ($cfg !== null && $cfg !== false) { + $advisorAvailable = (bool)(int)$cfg['config_value']; + } + } catch (Exception $e) { + error_log('[BotService] showMainMenu - failed to read advisor_available: ' . $e->getMessage()); + } + + // Si no hay asesor disponible, intentar mostrar el menú secundario primero + if (!$advisorAvailable) { + try { + // Verificar que la columna menu_type ya existe (puede no existir si la migración aún no corrió). + // Se cachea en static para no hacer SHOW COLUMNS en cada mensaje. + static $menuTypeColChecked = null; + if ($menuTypeColChecked === null) { + $colExists = $this->db->fetchAll("SHOW COLUMNS FROM menus LIKE 'menu_type'"); + $menuTypeColChecked = !empty($colExists); + if (!$menuTypeColChecked) { + error_log('[BotService] showMainMenu - column menu_type not yet migrated, run run_20260527_menu_type.php'); + } + } + + if ($menuTypeColChecked) { + $noAdvisorMenu = $this->db->fetch( + "SELECT * FROM menus WHERE menu_type = 'no_advisor' AND is_active = 1 ORDER BY order_position, id LIMIT 1" + ); + if ($noAdvisorMenu) { + try { error_log("[BotService] showMainMenu - no advisor available, showing no_advisor menu id={$noAdvisorMenu['id']} phone={$phoneNumber}"); } catch (Throwable $t) {} + $this->showMenu($phoneNumber, $noAdvisorMenu['id']); + return; + } + error_log('[BotService] showMainMenu - advisor absent but no no_advisor menu configured, falling back to root'); + } + } catch (Exception $e) { + error_log('[BotService] showMainMenu - error fetching no_advisor menu: ' . $e->getMessage()); + } + } + + // Menú raíz principal (comportamiento normal) $mainMenu = $this->db->fetch( "SELECT * FROM menus WHERE is_root = 1 AND is_active = 1 ORDER BY order_position LIMIT 1" ); try { error_log("[BotService] showMainMenu - fetched root menu=" . json_encode($mainMenu)); } catch (Throwable $t) {} - + if ($mainMenu) { $this->showMenu($phoneNumber, $mainMenu['id']); } else { try { error_log("[BotService] showMainMenu - no root menu found for phone={$phoneNumber}"); } catch (Throwable $t) {} $this->whatsappService->sendTextMessage( - $phoneNumber, + $phoneNumber, "❌ No hay menús configurados. Contacte con soporte." ); } @@ -1224,9 +1271,27 @@ class BotService { } /** - * Enviar mensaje por defecto cuando no hay coincidencias + * Enviar mensaje por defecto cuando no hay coincidencias. + * Si el asesor está marcado como ausente (advisor_available=0), muestra el menú + * secundario en lugar del mensaje genérico, para que todos los clientes que escriban + * vean el menú de asesor ausente hasta que se reactive la disponibilidad. */ private function sendDefaultNoMatch($phoneNumber) { + // Si el asesor está ausente, redirigir al menú secundario + try { + $cfg = $this->db->fetch( + "SELECT config_value FROM system_config WHERE config_key = 'advisor_available' LIMIT 1" + ); + $advisorAbsent = ($cfg !== null && $cfg !== false) && !(bool)(int)$cfg['config_value']; + if ($advisorAbsent) { + error_log("[BotService] sendDefaultNoMatch - advisor absent, redirecting to showMainMenu for phone={$phoneNumber}"); + $this->showMainMenu($phoneNumber); + return; + } + } catch (Exception $e) { + error_log('[BotService] sendDefaultNoMatch - failed to read advisor_available: ' . $e->getMessage()); + } + $defaultMessage = "🤖 No entendí su mensaje. Escriba *MENU* para ver las opciones disponibles o *ASESOR* para obtener ayuda."; $this->whatsappService->sendTextMessage($phoneNumber, $defaultMessage); }