This commit is contained in:
Lizandro Guarnizo
2026-05-27 10:30:47 -05:00
parent c9454ac62f
commit 313e57aace
10 changed files with 333 additions and 24 deletions
+6
View File
@@ -88,6 +88,12 @@ try {
$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
$maxOrder = $db->fetch("SELECT COALESCE(MAX(order_position), 0) as max_order FROM menus");
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* API - Cambiar modo de disponibilidad del asesor
* POST { "available": true|false }
* GET → devuelve estado actual
*/
require_once '../config/config.php';
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
$db = Database::getInstance();
try {
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$row = $db->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']);
}
+2 -1
View File
@@ -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 {
+13 -3
View File
@@ -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) {
<option value="active" ${menuData.status === 'active' ? 'selected' : ''}>Activo</option>
<option value="inactive" ${menuData.status === 'inactive' ? 'selected' : ''}>Inactivo</option>
</select>
<label for="editMenuType" class="form-label mt-2">
<i class="fas fa-layer-group"></i> Tipo de Menú
</label>
<select class="form-control" id="editMenuType">
<option value="main" ${(menuData.menu_type || 'main') === 'main' ? 'selected' : ''}>Principal (normal)</option>
<option value="no_advisor" ${menuData.menu_type === 'no_advisor' ? 'selected' : ''}>🔴 Asesor ausente (secundario)</option>
</select>
</div>
</div>
@@ -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 {
+62
View File
@@ -879,6 +879,17 @@ if (!isUserLoggedIn()) {
v1.4.0-<?php echo substr(time(), -4); ?>
</span>
</h5>
<!-- Toggle disponibilidad de asesor -->
<div style="margin-top:6px; display:flex; align-items:center; gap:8px;">
<span style="font-size:11px; color:rgba(255,255,255,0.8);">Asesor:</span>
<button id="advisor-mode-btn"
onclick="toggleAdvisorMode()"
style="font-size:11px; padding:2px 10px; border-radius:20px; border:none; cursor:pointer; font-weight:600; transition:all 0.2s;"
title="Activar/desactivar menú de asesor ausente">
</button>
<span id="advisor-mode-label" style="font-size:10px; color:rgba(255,255,255,0.7);"></span>
</div>
</div>
<div>
<a href="index.php" class="text-white text-decoration-none">
@@ -8032,5 +8043,56 @@ const citasModal = (() => {
<!-- Barra de sesiones minimizadas (agendamientos en paralelo) -->
<div id="labdom-pills-bar"></div>
<script>
// ── Disponibilidad del asesor (menú secundario no_advisor) ────────────────
(function() {
var btn = document.getElementById('advisor-mode-btn');
var label = document.getElementById('advisor-mode-label');
function applyState(available) {
if (available) {
btn.textContent = '🟢 Disponible';
btn.style.background = '#25d366';
btn.style.color = '#fff';
if (label) label.textContent = 'El bot usa el menú principal';
} else {
btn.textContent = '🔴 Ausente';
btn.style.background = '#dc3545';
btn.style.color = '#fff';
if (label) label.textContent = 'El bot usa el menú de asesor ausente';
}
}
// Cargar estado inicial
fetch('api/set_advisor_mode.php', { credentials: 'same-origin' })
.then(function(r){ return r.json(); })
.then(function(d){ if (d && d.success !== undefined) applyState(d.available); })
.catch(function(){});
window.toggleAdvisorMode = function() {
var current = btn.textContent.indexOf('Disponible') !== -1;
var next = !current;
fetch('api/set_advisor_mode.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ available: next })
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d && d.success) {
applyState(d.available);
var msg = d.available
? '✅ Modo asesor disponible activado. El bot usará el menú principal.'
: '⚠️ Asesor marcado como ausente. El bot usará el menú secundario.';
if (window.showToast) showToast(msg);
else alert(msg);
}
})
.catch(function(){ alert('Error al cambiar el modo de asesor.'); });
};
})();
</script>
</body>
</html>
+14
View File
@@ -1769,6 +1769,8 @@ try {
<label class="form-label">Mensaje de Bienvenida</label>
<textarea class="form-control" id="menu-welcome-message" rows="3" placeholder="¡Hola! Bienvenido. Por favor selecciona una opción:"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Estado</label>
<select class="form-control" id="menu-status">
@@ -1776,6 +1778,18 @@ try {
<option value="inactive">Inactivo</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Tipo de Menú</label>
<select class="form-control" id="menu-type">
<option value="main">Principal (normal)</option>
<option value="no_advisor">🔴 Asesor ausente (secundario)</option>
</select>
<small class="text-muted">El menú <em>Asesor ausente</em> se muestra cuando el asesor está marcado como no disponible.</small>
</div>
</div>
</div>
<hr>
<h6>📋 Opciones del Menú</h6>
@@ -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;
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* Runner de migración: 20260527_add_menu_type_advisor_mode
* Agrega la columna menu_type a la tabla menus y el config advisor_available.
*/
require_once __DIR__ . '/config/config.php';
header('Content-Type: text/plain; charset=utf-8');
$db = Database::getInstance();
$conn = $db->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";
+17 -3
View File
@@ -267,9 +267,13 @@ if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== tru
preview = `<strong>Plantilla:</strong> ${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}}
if (msg.template_parameters) {
const isArray = Array.isArray(msg.template_parameters);
const isObject = typeof msg.template_parameters === 'object' && !isArray;
if (isArray) {
// Array indexado: [0] => "valor1", [1] => "valor2"
const regex = /\{\{[^\}]+\}\}/g;
let paramIndex = 0;
@@ -278,7 +282,17 @@ if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== tru
paramIndex++;
return `<strong>${value}</strong>`;
});
} 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 `<strong>${value}</strong>`;
});
}
}
preview += `<div class="message-preview">${bodyPreview}</div>`;
}
} else {
+67 -2
View File
@@ -790,10 +790,57 @@ 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"
);
@@ -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);
}