p
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -1769,12 +1769,26 @@ 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="mb-3">
|
||||
<label class="form-label">Estado</label>
|
||||
<select class="form-control" id="menu-status">
|
||||
<option value="active">Activo</option>
|
||||
<option value="inactive">Inactivo</option>
|
||||
</select>
|
||||
<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">
|
||||
<option value="active">Activo</option>
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
+24
-10
@@ -267,18 +267,32 @@ 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}}
|
||||
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 `<strong>${value}</strong>`;
|
||||
});
|
||||
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 `<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 {
|
||||
|
||||
+69
-4
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user