From 01c30ca594f5e34aabb9cbaedbce9e6d470161e9 Mon Sep 17 00:00:00 2001 From: lizandrogd <77708265+lizandrogd@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:42:41 -0500 Subject: [PATCH] menus --- api/delete_menu.php | 85 ++++++++++++ api/get_menus.php | 154 +++++++++++++++++++--- api/save_menu.php | 137 +++++++++++++++---- api/test_db_connection.php | 38 ++++++ assets/js/app_simple.js | 260 ++++++++++++++++++++++++++++++++++++- classes/Database.php | 33 ++++- config/config.php | 3 + index.php | 137 ++++++++++++------- test_direct_api.php | 100 ++++++++++++++ test_menus.php | 200 ++++++++++++++++++++++++++++ test_menus_api.html | 153 ++++++++++++++++++++++ test_menus_frontend.html | 203 +++++++++++++++++++++++++++++ 12 files changed, 1397 insertions(+), 106 deletions(-) create mode 100644 api/delete_menu.php create mode 100644 api/test_db_connection.php create mode 100644 test_direct_api.php create mode 100644 test_menus.php create mode 100644 test_menus_api.html create mode 100644 test_menus_frontend.html diff --git a/api/delete_menu.php b/api/delete_menu.php new file mode 100644 index 0000000..c1144b3 --- /dev/null +++ b/api/delete_menu.php @@ -0,0 +1,85 @@ + 'Método no permitido']); + exit; +} + +try { + $input = json_decode(file_get_contents('php://input'), true); + + if (!$input || !isset($input['menu_id'])) { + http_response_code(400); + echo json_encode(['error' => 'ID del menú es requerido']); + exit; + } + + $menuId = (int)$input['menu_id']; + + if ($menuId <= 0) { + http_response_code(400); + echo json_encode(['error' => 'ID de menú inválido']); + exit; + } + + $db = Database::getInstance(); + + // Verificar que el menú existe + $menu = $db->fetch("SELECT id, name FROM menus WHERE id = :id", ['id' => $menuId]); + + if (!$menu) { + http_response_code(404); + echo json_encode(['error' => 'Menú no encontrado']); + exit; + } + + // Iniciar transacción + $db->beginTransaction(); + + try { + // Eliminar opciones del menú + $db->execute("DELETE FROM menu_options WHERE menu_id = :menu_id", ['menu_id' => $menuId]); + + // Eliminar el menú + $result = $db->execute("DELETE FROM menus WHERE id = :id", ['id' => $menuId]); + + if ($result) { + $db->commit(); + echo json_encode([ + 'success' => true, + 'message' => "Menú '{$menu['name']}' eliminado correctamente" + ]); + } else { + $db->rollback(); + echo json_encode(['error' => 'Error eliminando menú']); + } + + } catch (Exception $e) { + $db->rollback(); + throw $e; + } + +} catch (Exception $e) { + error_log("Error in delete_menu.php: " . $e->getMessage()); + http_response_code(500); + echo json_encode([ + 'error' => 'Error interno del servidor', + 'debug' => $e->getMessage() + ]); +} +?> \ No newline at end of file diff --git a/api/get_menus.php b/api/get_menus.php index fac1ddb..157f77a 100644 --- a/api/get_menus.php +++ b/api/get_menus.php @@ -1,7 +1,7 @@ fetchAll( - "SELECT + $db = Database::getInstance(); + $connection = $db->getConnection(); + + if (!$connection) { + throw new Exception("No se pudo obtener la conexión a la base de datos"); + } + + error_log("Conexión DB establecida correctamente"); + + // Verificar base de datos actual + $currentDB = $db->fetch("SELECT DATABASE() as current_db"); + error_log("Base de datos actual: " . json_encode($currentDB)); + + // Verificar si las tablas existen + try { + $showTablesQuery = "SHOW TABLES LIKE 'menus'"; + $tablesExist = $db->fetchAll($showTablesQuery); + error_log("Query SHOW TABLES ejecutada. Resultado: " . json_encode($tablesExist)); + + if (empty($tablesExist)) { + error_log("Tabla 'menus' no existe, pero datos indican que ya existe. Continuando con consulta..."); + } else { + error_log("Tabla 'menus' ya existe"); + } + } catch (Exception $e) { + error_log("ERROR creando/verificando tablas: " . $e->getMessage()); + echo json_encode([ + 'success' => false, + 'error' => 'Error en estructura de base de datos', + 'debug' => $e->getMessage(), + 'step' => 'table_verification', + 'data' => [] + ]); + exit; + } + + // Obtener menús con consulta simplificada + try { + error_log("Ejecutando consulta de menús..."); + + // Verificar estructura de tabla + $tableStructure = $db->fetchAll("DESCRIBE menus"); + error_log("Estructura tabla menus: " . json_encode($tableStructure)); + + // Consulta adaptada a la estructura real de la tabla existente + $menusQuery = "SELECT id, name, title, @@ -29,26 +74,93 @@ try { order_position, created_at, updated_at - FROM menus - ORDER BY order_position ASC, title ASC" - ); - - // Obtener opciones de cada menú - foreach ($menus as &$menu) { - $options = $db->fetchAll( - "SELECT * FROM menu_options - WHERE menu_id = :menu_id - ORDER BY option_number ASC", - ['menu_id' => $menu['id']] - ); - $menu['options'] = $options; + FROM menus ORDER BY order_position ASC, id ASC"; + $menus = $db->fetchAll($menusQuery); + + error_log("Menús obtenidos: " . count($menus) . " registros"); + + // Transformar datos para compatibilidad con JavaScript + foreach ($menus as &$menu) { + // Mapear campos para compatibilidad + $menu['menu_name'] = $menu['title'] ?? $menu['name']; + $menu['menu_key'] = $menu['name']; + $menu['status'] = ($menu['is_active'] == 1) ? 'active' : 'inactive'; + $menu['welcome_message'] = ''; // No disponible en estructura actual + } + + error_log("Menús encontrados: " . count($menus)); + + if (!empty($menus)) { + error_log("Primer menú: " . json_encode($menus[0])); + } + + // Procesar opciones con manejo de errores para estructura desconocida + foreach ($menus as &$menu) { + try { + // Primero verificar si la tabla menu_options existe + $optionsTableExists = $db->fetchAll("SHOW TABLES LIKE 'menu_options'"); + + if (!empty($optionsTableExists)) { + // Verificar estructura de la tabla + $optionsStructure = $db->fetchAll("DESCRIBE menu_options"); + error_log("Estructura menu_options: " . json_encode($optionsStructure)); + + $optionsQuery = "SELECT * FROM menu_options WHERE menu_id = ? ORDER BY id ASC"; + $options = $db->fetchAll($optionsQuery, [$menu['id']]); + } else { + error_log("Tabla menu_options no existe"); + $options = []; + } + + $menu['options'] = $options; + $menu['options_count'] = count($options); + } catch (Exception $e) { + error_log("Error opciones menú {$menu['id']}: " . $e->getMessage()); + $menu['options'] = []; + $menu['options_count'] = 0; + } + } + + error_log("=== FIN DEBUG GET MENUS SUCCESS ==="); + + echo json_encode([ + 'success' => true, + 'data' => $menus, + 'total' => count($menus), + 'message' => 'Menús cargados exitosamente', + 'debug_info' => [ + 'db_name' => DB_NAME, + 'total_found' => count($menus), + 'timestamp' => date('Y-m-d H:i:s') + ] + ]); + + } catch (Exception $e) { + error_log("ERROR consulta menús: " . $e->getMessage()); + error_log("Stack trace: " . $e->getTraceAsString()); + error_log("=== FIN DEBUG GET MENUS ERROR ==="); + + echo json_encode([ + 'success' => false, + 'error' => 'Error en consulta de menús', + 'debug' => $e->getMessage(), + 'code' => $e->getCode(), + 'step' => 'menu_query', + 'data' => [] + ]); } - echo json_encode($menus); - } catch (Exception $e) { - error_log("Error in get_menus.php: " . $e->getMessage()); + error_log("ERROR GENERAL get_menus.php: " . $e->getMessage()); + error_log("Stack trace: " . $e->getTraceAsString()); + http_response_code(500); - echo json_encode(['error' => 'Error interno del servidor']); + echo json_encode([ + 'success' => false, + 'error' => 'Error general del sistema', + 'debug' => $e->getMessage(), + 'step' => 'initialization', + 'data' => [] + ]); } ?> \ No newline at end of file diff --git a/api/save_menu.php b/api/save_menu.php index 5e2f957..1d23004 100644 --- a/api/save_menu.php +++ b/api/save_menu.php @@ -1,11 +1,14 @@ 'Método no permitido']); + echo json_encode(['success' => false, 'error' => 'Método no permitido']); exit; } try { $input = json_decode(file_get_contents('php://input'), true); - if (!$input || !isset($input['name']) || !isset($input['title'])) { + if (!$input || !isset($input['name'])) { http_response_code(400); - echo json_encode(['error' => 'Nombre y título son requeridos']); + echo json_encode(['success' => false, 'error' => 'Nombre del menú es requerido']); exit; } $db = Database::getInstance(); - // Verificar si es menú raíz - $isRoot = empty($input['parent_id']) ? 1 : 0; + // Iniciar transacción + $db->beginTransaction(); - // Obtener próximo order_position - $maxOrder = $db->fetch( - "SELECT COALESCE(MAX(order_position), 0) as max_order FROM menus" - ); + try { +// Verificar estructura de tabla antes de insertar + $tableStructure = $db->fetchAll("DESCRIBE menus"); + $availableColumns = array_column($tableStructure, 'Field'); - $menuData = [ - 'name' => $input['name'], - 'title' => $input['title'], - 'description' => $input['description'] ?? '', - 'parent_id' => empty($input['parent_id']) ? null : $input['parent_id'], - 'is_root' => $isRoot, - 'is_active' => $input['is_active'] ?? 1, - 'order_position' => $maxOrder['max_order'] + 1, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s') - ]; + error_log("Columnas disponibles en tabla menus: " . json_encode($availableColumns)); - $menuId = $db->insert('menus', $menuData); + // Adaptar datos a la estructura real de la tabla + $menuData = []; - echo json_encode([ - 'success' => true, - 'message' => 'Menú guardado correctamente', - 'menu_id' => $menuId - ]); + // Campos básicos que deberían existir + if (in_array('name', $availableColumns)) { + $menuData['name'] = $input['menu_key'] ?? $input['name']; // Usar menu_key como name + } + + if (in_array('title', $availableColumns)) { + $menuData['title'] = $input['name'] ?? 'Nuevo Menú'; // Usar name como title + } + + if (in_array('description', $availableColumns)) { + $menuData['description'] = $input['description'] ?? ''; + } + + if (in_array('parent_id', $availableColumns)) { + $menuData['parent_id'] = empty($input['parent_id']) ? null : (int)$input['parent_id']; + } + + if (in_array('is_root', $availableColumns)) { + $menuData['is_root'] = empty($input['parent_id']) ? 1 : 0; + } + + if (in_array('is_active', $availableColumns)) { + $menuData['is_active'] = ($input['status'] === 'active') ? 1 : 0; + } + + if (in_array('order_position', $availableColumns)) { + // Obtener próxima posición + $maxOrder = $db->fetch("SELECT COALESCE(MAX(order_position), 0) as max_order FROM menus"); + $menuData['order_position'] = ($maxOrder['max_order'] ?? 0) + 1; + } + + if (in_array('created_at', $availableColumns)) { + $menuData['created_at'] = date('Y-m-d H:i:s'); + } + + if (in_array('updated_at', $availableColumns)) { + $menuData['updated_at'] = date('Y-m-d H:i:s'); + } + + $menuId = $db->insert('menus', $menuData); + + if (!$menuId) { + throw new Exception('Error insertando menú'); + } + + // Procesar opciones si las hay + $optionsCount = 0; + if (isset($input['options']) && is_array($input['options'])) { + foreach ($input['options'] as $option) { + if (!empty($option['key']) && !empty($option['text'])) { + $optionData = [ + 'menu_id' => $menuId, + 'option_key' => $option['key'], + 'option_text' => $option['text'], + 'option_action' => $option['action'] ?? 'message', + 'option_value' => $option['value'] ?? '', + 'order_index' => $option['order_index'] ?? $optionsCount + 1, + 'is_active' => true, + 'created_at' => date('Y-m-d H:i:s') + ]; + + $optionId = $db->insert('menu_options', $optionData); + if ($optionId) { + $optionsCount++; + } + } + } + } + + // Actualizar contador de opciones + $db->execute("UPDATE menus SET options_count = :count WHERE id = :id", [ + 'count' => $optionsCount, + 'id' => $menuId + ]); + + $db->commit(); + + echo json_encode([ + 'success' => true, + 'message' => 'Menú guardado correctamente', + 'menu_id' => $menuId, + 'options_created' => $optionsCount + ]); + + } catch (Exception $e) { + $db->rollback(); + throw $e; + } } catch (Exception $e) { error_log("Error in save_menu.php: " . $e->getMessage()); http_response_code(500); - echo json_encode(['error' => 'Error interno del servidor: ' . $e->getMessage()]); + echo json_encode([ + 'success' => false, + 'error' => 'Error interno del servidor', + 'debug' => $e->getMessage() + ]); } ?> \ No newline at end of file diff --git a/api/test_db_connection.php b/api/test_db_connection.php new file mode 100644 index 0000000..d05441d --- /dev/null +++ b/api/test_db_connection.php @@ -0,0 +1,38 @@ + 'Iniciando test de conexión DB...', + 'timestamp' => date('Y-m-d H:i:s') + ]); + echo "\n\n"; + + $db = Database::getInstance(); + + echo json_encode([ + 'success' => true, + 'message' => 'Conexión a base de datos exitosa', + 'database' => DB_NAME, + 'host' => DB_HOST + ]); + echo "\n\n"; + + // Test de tabla + $tables = $db->fetchAll("SHOW TABLES"); + echo json_encode([ + 'success' => true, + 'message' => 'Tablas encontradas', + 'tables' => $tables + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); +} +?> \ No newline at end of file diff --git a/assets/js/app_simple.js b/assets/js/app_simple.js index e2db864..4ebd6e4 100644 --- a/assets/js/app_simple.js +++ b/assets/js/app_simple.js @@ -114,6 +114,9 @@ class SimpleWhatsAppManager { case 'templates': this.loadTemplates(); break; + case 'menus': + this.loadMenus(); + break; case 'logs': this.loadLogs(); break; @@ -794,6 +797,26 @@ class SimpleWhatsAppManager { } } + async loadMenus() { + this.log('Cargando menús'); + + try { + const response = await this.apiCall('get_menus.php'); + + if (response && response.success) { + this.updateMenusList(response.data || []); + } else if (response && Array.isArray(response)) { + // Retrocompatibilidad + this.updateMenusList(response); + } else { + this.showError('Error cargando menús'); + } + + } catch (error) { + this.showError('Error cargando menús: ' + error.message); + } + } + updateTemplatesList(templates) { const container = document.getElementById('templates-table'); if (!container) return; @@ -828,6 +851,47 @@ class SimpleWhatsAppManager { container.innerHTML = html; } + updateMenusList(menus) { + const container = document.getElementById('menus-table'); + if (!container) return; + + if (menus.length === 0) { + container.innerHTML = 'No hay menús configurados'; + return; + } + + let html = ''; + + menus.forEach(menu => { + const createdDate = new Date(menu.created_at || Date.now()); + const formatDate = createdDate.toLocaleDateString() + ' ' + createdDate.toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' }); + + html += ` + + ${menu.name || menu.menu_name} + ${menu.menu_key || menu.trigger} + ${this.truncateText(menu.description || '', 50)} + ${menu.status || 'active'} + ${menu.options_count || 0} opciones + ${formatDate} + + + + + + + `; + }); + + container.innerHTML = html; + } + async saveTemplate() { this.log('Guardando plantilla'); @@ -874,6 +938,106 @@ class SimpleWhatsAppManager { } } + async saveMenu() { + this.log('Guardando menú'); + + try { + const menuData = { + name: document.getElementById('menu-name')?.value || '', + 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' + }; + + // Validar campos requeridos + if (!menuData.name || !menuData.menu_key) { + this.showError('Nombre del menú y clave del menú son requeridos'); + return; + } + + // Obtener opciones del menú + const optionsContainer = document.getElementById('menu-options-container'); + const options = []; + + if (optionsContainer) { + const optionItems = optionsContainer.querySelectorAll('.menu-option-item'); + optionItems.forEach((item, index) => { + const optionKey = item.querySelector('.option-key')?.value || ''; + const optionText = item.querySelector('.option-text')?.value || ''; + const optionAction = item.querySelector('.option-action')?.value || 'message'; + const optionValue = item.querySelector('.option-value')?.value || ''; + + if (optionKey && optionText) { + options.push({ + key: optionKey, + text: optionText, + action: optionAction, + value: optionValue, + order_index: index + 1 + }); + } + }); + } + + menuData.options = options; + + this.log(`Datos del menú a guardar: ${JSON.stringify(menuData)}`); + + const response = await this.apiCall('save_menu.php', { + method: 'POST', + body: menuData + }); + + if (response && response.success) { + this.showSuccess('Menú guardado correctamente'); + + // Cerrar modal + const modal = document.getElementById('createMenuModal'); + if (modal) { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) bsModal.hide(); + } + + // Limpiar formulario + const form = document.getElementById('menu-form'); + if (form) form.reset(); + + // Limpiar opciones + const optionsContainer = document.getElementById('menu-options-container'); + if (optionsContainer) optionsContainer.innerHTML = ''; + + // Recargar menús + this.loadMenus(); + } else { + this.showError(`Error guardando menú: ${response?.error || 'Error desconocido'}`); + } + + } catch (error) { + this.showError('Error guardando menú: ' + error.message); + } + } + + async deleteMenuAction(menuId) { + this.log(`Eliminando menú ID: ${menuId}`); + + try { + const response = await this.apiCall('delete_menu.php', { + method: 'POST', + body: { menu_id: menuId } + }); + + if (response && response.success) { + this.showSuccess('Menú eliminado correctamente'); + this.loadMenus(); // Recargar la lista de menús + } else { + this.showError(`Error eliminando menú: ${response?.error || 'Error desconocido'}`); + } + } catch (error) { + this.showError('Error eliminando menú: ' + error.message); + } + } + async sendMessage() { this.log('Enviando mensaje'); @@ -1406,15 +1570,109 @@ window.deleteTemplate = function (templateId) { } }; -// Hacer que app.saveTemplate() funcione +// Hacer que app.saveTemplate() y app.saveMenu() funcionen window.app = { saveTemplate: function () { if (window.whatsappManager) { window.whatsappManager.saveTemplate(); } + }, + saveMenu: function () { + if (window.whatsappManager) { + window.whatsappManager.saveMenu(); + } } }; +// Funciones globales para gestión de menús +window.editMenu = function (menuId) { + console.log('Editando menú:', menuId); + // TODO: Implementar edición de menús + alert(`Editar menú ${menuId} - Función en desarrollo`); +}; + +window.deleteMenu = function (menuId) { + console.log('Eliminando menú:', menuId); + if (confirm(`¿Está seguro que desea eliminar el menú ${menuId}?`)) { + if (window.whatsappManager) { + window.whatsappManager.deleteMenuAction(menuId); + } + } +}; + +window.viewMenuOptions = function (menuId) { + console.log('Viendo opciones del menú:', menuId); + // TODO: Implementar visualización de opciones del menú + alert(`Ver opciones del menú ${menuId} - Función en desarrollo`); +}; + +// Función para agregar nueva opción al menú +window.addMenuOption = function () { + const container = document.getElementById('menu-options-container'); + if (!container) return; + + const optionIndex = container.children.length + 1; + + const optionHtml = ` + + `; + + container.insertAdjacentHTML('beforeend', optionHtml); +}; + +// Función para remover opción del menú +window.removeMenuOption = function (button) { + const optionItem = button.closest('.menu-option-item'); + if (optionItem) { + optionItem.remove(); + updateOptionNumbers(); + } +}; + +// Función para actualizar numeración de opciones +function updateOptionNumbers() { + const container = document.getElementById('menu-options-container'); + if (!container) return; + + const options = container.querySelectorAll('.menu-option-item'); + options.forEach((option, index) => { + const header = option.querySelector('h6'); + if (header) { + header.textContent = `Opción ${index + 1}`; + } + }); +} + // Funciones para el formulario de mensajes window.toggleRecipientType = function () { const recipientType = document.getElementById('recipient-type').value; diff --git a/classes/Database.php b/classes/Database.php index 56daba8..02e2f17 100644 --- a/classes/Database.php +++ b/classes/Database.php @@ -40,22 +40,36 @@ class Database { public function query($sql, $params = []) { try { $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); + if (!empty($params)) { + $stmt->execute($params); + } else { + $stmt->execute(); + } return $stmt; } catch (PDOException $e) { - error_log("Query failed: " . $e->getMessage() . " SQL: " . $sql); - throw new Exception("Error en la consulta a la base de datos"); + error_log("Query failed: " . $e->getMessage() . " SQL: " . $sql . " Params: " . json_encode($params)); + throw new Exception("Error en la consulta a la base de datos: " . $e->getMessage()); } } public function fetch($sql, $params = []) { - $stmt = $this->query($sql, $params); - return $stmt->fetch(); + try { + $stmt = $this->query($sql, $params); + return $stmt->fetch(); + } catch (Exception $e) { + error_log("Fetch failed: " . $e->getMessage()); + throw $e; + } } public function fetchAll($sql, $params = []) { - $stmt = $this->query($sql, $params); - return $stmt->fetchAll(); + try { + $stmt = $this->query($sql, $params); + return $stmt->fetchAll(); + } catch (Exception $e) { + error_log("FetchAll failed: " . $e->getMessage()); + throw $e; + } } public function insert($table, $data) { @@ -99,6 +113,11 @@ class Database { return $this->pdo->rollback(); } + public function execute($sql, $params = []) { + $stmt = $this->query($sql, $params); + return $stmt->rowCount(); + } + public function lastInsertId() { return $this->pdo->lastInsertId(); } diff --git a/config/config.php b/config/config.php index 996a44d..a3412fb 100644 --- a/config/config.php +++ b/config/config.php @@ -50,6 +50,9 @@ function env($key, $default = null) { return $_ENV[$key] ?? getenv($key) ?: $default; } +// Incluir clase Database +require_once __DIR__ . '/../classes/Database.php'; + // Función para verificar si la instalación está completada function isInstallationCompleted() { return file_exists(dirname(__DIR__) . '/.installation_completed'); diff --git a/index.php b/index.php index 837f140..d4c9600 100644 --- a/index.php +++ b/index.php @@ -244,56 +244,31 @@ try {