delete test

This commit is contained in:
lizandrogd
2026-01-13 01:48:33 -05:00
parent b0d44b0095
commit 0544e81463
34 changed files with 3141 additions and 3337 deletions
+298
View File
@@ -0,0 +1,298 @@
# 🏥 Sistema de Chatbot WhatsApp - Laboratorio Ximena Caicedo
## 📋 Descripción
Sistema completo de chatbot para WhatsApp diseñado específicamente para el Laboratorio Ximena Caicedo en Cúcuta, Colombia. Incluye flujos conversacionales inteligentes, detección de horarios, gestión de estados, respuestas automáticas y mucho más.
## ✨ Características Principales
### 🤖 Bot Inteligente
- ✅ Procesamiento de Lenguaje Natural (NLP) básico
- ✅ Detección de palabras clave y sinónimos
- ✅ Respuestas automáticas contextuales
- ✅ Detección de sentimientos y frustración
- ✅ Escalamiento automático a asesores humanos
### 📅 Gestión de Horarios
- ✅ Detección automática de horario de atención
- ✅ Mensajes diferentes dentro/fuera de horario
- ✅ Configuración por día de la semana
- ✅ Soporte para horarios partidos (7-12, 14-17)
### 🗂️ Sistema de Menús
- ✅ Menú principal con 5 opciones
- ✅ Submenú de exámenes con información detallada
- ✅ Navegación intuitiva con números
- ✅ Opción de volver al menú anterior
### 🔄 Gestión de Estados
- ✅ Tracking del estado de cada conversación
- ✅ Flujos de agendamiento paso a paso
- ✅ Validación de datos del paciente
- ✅ Timeout automático de estados inactivos
### 💬 Respuestas Automáticas
- ✅ Más de 15 triggers configurados
- ✅ Respuestas por palabras clave
- ✅ Información de precios, horarios, ubicación
- ✅ Instrucciones para exámenes específicos
### 📸 Procesamiento de Multimedia
- ✅ Recepción de órdenes médicas
- ✅ Fotos de documentos de identidad
- ✅ Validación de imágenes
- ✅ Almacenamiento seguro
## 🚀 Instalación
### Requisitos Previos
- PHP 8.0 o superior
- MySQL 5.7 o superior
- Servidor web (Apache/Nginx)
- Cuenta de WhatsApp Business API
### Paso 1: Clonar/Descargar Archivos
Asegúrate de tener todos los archivos en tu servidor.
### Paso 2: Configurar Base de Datos
1. Crea una base de datos MySQL
2. Importa las tablas base si no existen:
```sql
-- Ejecutar los scripts SQL en este orden:
1. create_tables.sql (tablas base)
2. migrations/add_conversation_states.sql (tabla de estados)
3. setup_laboratorio_ximena.sql (datos del laboratorio)
```
### Paso 3: Configurar Credenciales
Edita `config/config.php` con tus credenciales:
```php
define('DB_HOST', 'localhost');
define('DB_NAME', 'whatsapp_bot');
define('DB_USER', 'tu_usuario');
define('DB_PASS', 'tu_password');
```
### Paso 4: Ejecutar Instalación Automática
Visita en tu navegador:
```
http://tu-dominio.com/install_laboratorio.php
```
Este script:
- ✅ Verifica la conexión a la base de datos
- ✅ Crea la tabla de estados
- ✅ Importa menús y respuestas automáticas
- ✅ Verifica los servicios del sistema
- ✅ Muestra estadísticas finales
### Paso 5: Configurar WhatsApp API
1. Ve al panel de administración: `http://tu-dominio.com/index.php`
2. Navega a la pestaña "Configuración"
3. Ingresa:
- Token de Acceso de WhatsApp Business API
- Phone Number ID
- Webhook Verify Token
- API URL (por defecto: https://graph.facebook.com/v22.0/)
### Paso 6: Configurar Webhook
1. En Meta for Developers, configura el webhook:
- URL: `https://tu-dominio.com/api/webhook.php`
- Verify Token: El mismo que configuraste en el paso anterior
- Suscripciones: `messages`, `message_status`
## 📚 Estructura de Archivos
```
whatsapp/
├── services/
│ ├── BusinessHoursService.php # Gestión de horarios
│ ├── ConversationStateService.php # Estados de conversación
│ ├── NLPService.php # Procesamiento de lenguaje
│ ├── MenuService.php # Gestión de menús
│ ├── PatientDataValidator.php # Validación de datos
│ ├── BotServiceLab.php # Servicio principal del bot
│ └── WhatsAppService.php # Integración con WhatsApp API
├── api/
│ └── webhook.php # Recepción de mensajes
├── migrations/
│ └── add_conversation_states.sql # Migración de tabla de estados
├── setup_laboratorio_ximena.sql # Setup completo del laboratorio
├── install_laboratorio.php # Instalador automático
└── README_LABORATORIO.md # Este archivo
```
## 🎯 Flujos Implementados
### 1. Menú Principal
```
1️⃣ Agendar toma a domicilio
2️⃣ Consultar resultados
3️⃣ Información sobre exámenes
4️⃣ Ubicación y horarios
5️⃣ Hablar con un asesor
```
### 2. Submenú de Exámenes
```
1️⃣ Pruebas de embarazo
2️⃣ Prueba de paternidad
3️⃣ Helicobacter pylori (en aliento)
4️⃣ Orina de 24 horas
5️⃣ Solicitar cotización
0️⃣ Volver al menú principal
```
### 3. Palabras Clave Automáticas
- **"precio", "costo", "cuánto"** → Información de precios
- **"resultado", "resultados"** → Instrucciones para descargar
- **"horario", "cuando"** → Horarios de atención
- **"domicilio", "casa"** → Servicio a domicilio
- **"embarazo"** → Pruebas de embarazo
- **"paternidad", "ADN"** → Prueba de paternidad
- **"ubicación", "donde"** → Dirección y mapa
- **"asesor", "ayuda", "persona"** → Transferir a humano
- **"gracias"** → Mensaje de despedida
- **"menú", "inicio"** → Mostrar menú principal
## ⚙️ Configuración Avanzada
### Horarios de Atención
Edita en la base de datos o desde el panel:
```sql
UPDATE system_config
SET config_value = '7:00-12:00,14:00-17:00'
WHERE config_key = 'business_hours_weekday';
UPDATE system_config
SET config_value = '8:00-11:00'
WHERE config_key = 'business_hours_saturday';
```
### Agregar Nueva Respuesta Automática
Desde el panel de administración:
1. Ve a "Respuestas Automáticas"
2. Click en "Nueva Respuesta"
3. Configura:
- Tipo de trigger (keyword, contains, exact, etc.)
- Palabras clave (separadas por comas)
- Texto de respuesta
- Prioridad (1-100, mayor = más prioridad)
### Personalizar Menús
1. Ve a "Menús Configurados"
2. Click en el menú que quieres editar
3. Modifica el mensaje de bienvenida o las opciones
4. Guarda los cambios
## 🔧 Personalización
### Cambiar Nombre del Laboratorio
Edita `setup_laboratorio_ximena.sql` antes de instalar, reemplaza:
```sql
'Laboratorio Ximena Caicedo' 'Tu Laboratorio'
```
### Modificar Valores de Exámenes
Edita los mensajes en la tabla `menu_options` o `autoresponses`:
```sql
UPDATE menu_options
SET response_text = REPLACE(response_text, '$28.000', '$30.000')
WHERE response_text LIKE '%$28.000%';
```
### Agregar Nuevo Examen al Submenú
```sql
INSERT INTO menu_options (menu_id, option_key, option_order, option_text, response_type, response_text, is_active)
VALUES
(
(SELECT id FROM menus WHERE menu_key = 'lab_menu_examenes'),
'6',
6,
'Nuevo Examen',
'text',
'Información del nuevo examen...',
1
);
```
## 📊 Monitoreo y Logs
### Ver Conversaciones Recientes
```sql
SELECT phone_number, message_text, direction, created_at
FROM conversations
ORDER BY created_at DESC
LIMIT 50;
```
### Ver Estados Activos
```sql
SELECT phone_number, state, state_data, updated_at
FROM user_states
WHERE updated_at > DATE_SUB(NOW(), INTERVAL 1 HOUR);
```
### Ver Logs de Webhook
```sql
SELECT * FROM webhook_logs
ORDER BY created_at DESC
LIMIT 20;
```
## 🐛 Solución de Problemas
### El bot no responde
1. Verifica que el webhook esté configurado correctamente
2. Revisa los logs de webhook: `SELECT * FROM webhook_logs ORDER BY created_at DESC LIMIT 1`
3. Verifica las credenciales de WhatsApp API en "Configuración"
### Mensajes fuera de horario no funcionan
1. Verifica la zona horaria del servidor: `date_default_timezone_get()`
2. Revisa la configuración de horarios en `system_config`
3. Asegúrate de que `BusinessHoursService.php` existe
### Estados no se guardan
1. Verifica que la tabla `user_states` existe:
```sql
SHOW TABLES LIKE 'user_states';
```
2. Si no existe, ejecuta: `migrations/add_conversation_states.sql`
### NLP no detecta palabras clave
1. Revisa que las palabras estén en minúsculas en la BD
2. Verifica que `autoresponses` tiene registros activos:
```sql
SELECT COUNT(*) FROM autoresponses WHERE is_active = 1;
```
3. Aumenta la prioridad de la respuesta automática
## 📞 Soporte
Para soporte técnico o consultas:
- 📧 Email: soporte@ejemplo.com
- 📱 WhatsApp: +57 XXX XXX XXXX
- 💻 GitHub: [repositorio]
## 📝 Changelog
### Versión 1.0.0 (13 de enero de 2026)
- ✅ Implementación completa del sistema
- ✅ Menús del laboratorio
- ✅ 15+ respuestas automáticas
- ✅ Detección de horarios
- ✅ Gestión de estados
- ✅ NLP básico
- ✅ Validación de datos de pacientes
- ✅ Instalador automático
## 📜 Licencia
Este sistema fue desarrollado específicamente para Laboratorio Ximena Caicedo.
Todos los derechos reservados © 2026
---
**¡Sistema listo para producción! 🚀**
Para comenzar, ejecuta `install_laboratorio.php` y sigue las instrucciones.
-152
View File
@@ -1,152 +0,0 @@
<?php
/**
* API de prueba para configuraciones (sin autenticación)
*/
require_once '../config/config.php';
// Simular autenticación para pruebas
session_start();
$_SESSION['admin_logged_in'] = true;
$_SESSION['admin_username'] = 'test_admin';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
try {
// Respuesta simple para test de conectividad
if ($action === 'ping') {
echo json_encode([
'success' => true,
'message' => 'API funcionando correctamente',
'timestamp' => date('Y-m-d H:i:s'),
'method' => $method
]);
exit;
}
switch ($method) {
case 'GET':
switch ($action) {
case 'status':
// Estado general del sistema
$allConfigs = getAllConfigsFromDB();
$whatsappValidation = validateWhatsAppConfigFromDB();
echo json_encode([
'success' => true,
'data' => [
'total_configs' => count($allConfigs),
'whatsapp_configured' => $whatsappValidation['valid'],
'database_migrated' => !empty($allConfigs),
'installation_completed' => isInstallationCompleted(),
'server_time' => date('Y-m-d H:i:s')
]
]);
break;
case 'all':
// Obtener todas las configuraciones
$configs = getAllConfigsFromDB();
echo json_encode([
'success' => true,
'data' => $configs
]);
break;
case 'whatsapp':
// Obtener configuración de WhatsApp
$whatsappConfig = getWhatsAppConfigFromDB();
echo json_encode([
'success' => true,
'data' => $whatsappConfig
]);
break;
case 'test_db':
// Test de conexión a BD
try {
$pdo = createDbConnection();
$tableExists = $pdo->query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0;
echo json_encode([
'success' => true,
'data' => [
'db_connected' => true,
'table_exists' => $tableExists,
'config_count' => $tableExists ? $pdo->query("SELECT COUNT(*) FROM system_config")->fetchColumn() : 0
]
]);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => 'Error de BD: ' . $e->getMessage()
]);
}
break;
default:
throw new Exception('Acción GET no reconocida: ' . $action);
}
break;
case 'POST':
$input = json_decode(file_get_contents('php://input'), true);
switch ($action) {
case 'save_test':
// Guardar configuración de prueba
$key = 'api_test_' . time();
$value = 'Test desde API - ' . date('Y-m-d H:i:s');
$result = saveConfigToDB($key, $value, 'Prueba desde API');
echo json_encode([
'success' => $result,
'message' => $result ? 'Configuración guardada correctamente' : 'Error guardando configuración',
'data' => [
'key' => $key,
'value' => $value
]
]);
break;
case 'save_whatsapp_test':
// Guardar config de WhatsApp de prueba
$testConfig = [
'token' => 'test_token_' . time(),
'phone_number_id' => '123456789',
'webhook_verify_token' => 'webhook_' . time()
];
$result = saveWhatsAppConfigToDB($testConfig);
echo json_encode([
'success' => $result,
'message' => $result ? 'WhatsApp config guardada' : 'Error guardando WhatsApp config',
'data' => $testConfig
]);
break;
default:
throw new Exception('Acción POST no reconocida: ' . $action);
}
break;
default:
throw new Exception('Método HTTP no permitido: ' . $method);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
?>
-38
View File
@@ -1,38 +0,0 @@
<?php
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
try {
echo json_encode([
'message' => '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()
]);
}
?>
+267
View File
@@ -0,0 +1,267 @@
<?php
/**
* Script de Instalación Automática
* Laboratorio Ximena Caicedo - Sistema de Chatbot WhatsApp
* Fecha: 13 de enero de 2026
*/
require_once __DIR__ . '/config/config.php';
// Configuración de tiempo de ejecución
set_time_limit(300); // 5 minutos
ini_set('display_errors', 1);
error_reporting(E_ALL);
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Instalación - Laboratorio Ximena Caicedo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 0;
}
.install-container {
max-width: 800px;
margin: 0 auto;
}
.install-card {
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
}
.step {
padding: 15px;
margin: 10px 0;
border-radius: 8px;
background: #f8f9fa;
}
.step.success {
background: #d4edda;
border-left: 4px solid #28a745;
}
.step.error {
background: #f8d7da;
border-left: 4px solid #dc3545;
}
.step.running {
background: #d1ecf1;
border-left: 4px solid #17a2b8;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo i {
font-size: 60px;
color: #667eea;
}
</style>
</head>
<body>
<div class="install-container">
<div class="install-card">
<div class="logo">
<i class="fas fa-flask"></i>
<h2>Laboratorio Ximena Caicedo</h2>
<p class="text-muted">Instalación del Sistema de Chatbot WhatsApp</p>
</div>
<div id="installation-steps">
<?php
$success = true;
$steps = [];
try {
// PASO 1: Verificar conexión a la base de datos
echo '<div class="step running">';
echo '<i class="fas fa-database"></i> Verificando conexión a la base de datos...';
flush();
$db = Database::getInstance();
echo ' <span class="text-success"><i class="fas fa-check-circle"></i> Conectado</span>';
echo '</div>';
$steps[] = ['name' => 'Conexión DB', 'status' => 'success'];
// PASO 2: Crear tabla de estados si no existe
echo '<div class="step running">';
echo '<i class="fas fa-table"></i> Creando tabla de estados de conversación...';
flush();
$sqlStates = file_get_contents(__DIR__ . '/migrations/add_conversation_states.sql');
$statementsStates = array_filter(array_map('trim', explode(';', $sqlStates)));
foreach ($statementsStates as $statement) {
if (!empty($statement) && substr($statement, 0, 6) !== 'SELECT') {
$db->getConnection()->exec($statement);
}
}
echo ' <span class="text-success"><i class="fas fa-check-circle"></i> Completado</span>';
echo '</div>';
$steps[] = ['name' => 'Tabla user_states', 'status' => 'success'];
// PASO 3: Importar menús y respuestas automáticas
echo '<div class="step running">';
echo '<i class="fas fa-list"></i> Importando menús y respuestas automáticas...';
flush();
$sqlSetup = file_get_contents(__DIR__ . '/setup_laboratorio_ximena.sql');
$statementsSetup = array_filter(array_map('trim', explode(';', $sqlSetup)));
$menuCount = 0;
$responseCount = 0;
foreach ($statementsSetup as $statement) {
if (!empty($statement) && substr($statement, 0, 2) !== '--') {
if (stripos($statement, 'INSERT INTO menus') !== false) {
$menuCount++;
} elseif (stripos($statement, 'INSERT INTO autoresponses') !== false) {
$responseCount++;
}
try {
$db->getConnection()->exec($statement);
} catch (Exception $e) {
// Ignorar errores de duplicados
if (strpos($e->getMessage(), 'Duplicate entry') === false) {
throw $e;
}
}
}
}
echo " <span class=\"text-success\"><i class=\"fas fa-check-circle\"></i> {$menuCount} menús, {$responseCount} respuestas</span>";
echo '</div>';
$steps[] = ['name' => 'Menús y respuestas', 'status' => 'success'];
// PASO 4: Verificar servicios
echo '<div class="step running">';
echo '<i class="fas fa-cogs"></i> Verificando servicios del sistema...';
flush();
$services = [
'BusinessHoursService',
'ConversationStateService',
'NLPService',
'MenuService',
'PatientDataValidator',
'BotServiceLab'
];
$servicesOk = true;
foreach ($services as $service) {
$file = __DIR__ . "/services/{$service}.php";
if (!file_exists($file)) {
echo " <span class=\"text-danger\"><i class=\"fas fa-times-circle\"></i> Falta {$service}.php</span>";
$servicesOk = false;
break;
}
}
if ($servicesOk) {
echo ' <span class="text-success"><i class="fas fa-check-circle"></i> ' . count($services) . ' servicios verificados</span>';
$steps[] = ['name' => 'Servicios', 'status' => 'success'];
} else {
$success = false;
}
echo '</div>';
// PASO 5: Obtener estadísticas
echo '<div class="step running">';
echo '<i class="fas fa-chart-bar"></i> Obteniendo estadísticas...';
flush();
$stmt = $db->query("
SELECT
(SELECT COUNT(*) FROM menus WHERE is_active = 1) as menus_activos,
(SELECT COUNT(*) FROM menu_options WHERE is_active = 1) as opciones_activas,
(SELECT COUNT(*) FROM autoresponses WHERE is_active = 1) as respuestas_activas,
(SELECT COUNT(*) FROM users) as total_usuarios
");
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
echo ' <span class="text-success"><i class="fas fa-check-circle"></i> Completado</span>';
echo '</div>';
// Mostrar resumen
if ($success) {
echo '<div class="alert alert-success mt-4">';
echo '<h4><i class="fas fa-check-circle"></i> ¡Instalación Completada Exitosamente!</h4>';
echo '<hr>';
echo '<h5>📊 Estadísticas del Sistema:</h5>';
echo '<ul>';
echo "<li><strong>Menús activos:</strong> {$stats['menus_activos']}</li>";
echo "<li><strong>Opciones de menú:</strong> {$stats['opciones_activas']}</li>";
echo "<li><strong>Respuestas automáticas:</strong> {$stats['respuestas_activas']}</li>";
echo "<li><strong>Usuarios registrados:</strong> {$stats['total_usuarios']}</li>";
echo '</ul>';
echo '<hr>';
echo '<h5>🚀 Próximos Pasos:</h5>';
echo '<ol>';
echo '<li>Configura tus credenciales de WhatsApp Business API en el panel de administración</li>';
echo '<li>Prueba el menú principal enviando un mensaje de WhatsApp</li>';
echo '<li>Revisa las respuestas automáticas en la sección correspondiente</li>';
echo '<li>Personaliza los mensajes según las necesidades del laboratorio</li>';
echo '</ol>';
echo '<div class="text-center mt-4">';
echo '<a href="../index.php" class="btn btn-primary btn-lg">';
echo '<i class="fas fa-arrow-right"></i> Ir al Panel de Administración';
echo '</a>';
echo '</div>';
echo '</div>';
}
} catch (Exception $e) {
$success = false;
echo '<div class="step error">';
echo '<i class="fas fa-times-circle"></i> Error: ' . htmlspecialchars($e->getMessage());
echo '</div>';
echo '<div class="alert alert-danger mt-4">';
echo '<h4><i class="fas fa-exclamation-triangle"></i> Error en la Instalación</h4>';
echo '<p>Hubo un problema durante la instalación. Por favor:</p>';
echo '<ol>';
echo '<li>Verifica que la base de datos existe y tiene permisos correctos</li>';
echo '<li>Revisa la configuración en <code>config/config.php</code></li>';
echo '<li>Consulta el error arriba para más detalles</li>';
echo '<li>Intenta ejecutar la instalación nuevamente</li>';
echo '</ol>';
echo '<div class="text-center mt-3">';
echo '<button class="btn btn-warning" onclick="location.reload()">';
echo '<i class="fas fa-redo"></i> Reintentar Instalación';
echo '</button>';
echo '</div>';
echo '</div>';
}
?>
</div>
<?php if ($success): ?>
<div class="card mt-4 bg-light">
<div class="card-body">
<h5><i class="fas fa-book"></i> Documentación</h5>
<p>Para más información sobre cómo usar el sistema:</p>
<ul>
<li><a href="../REPORTE_SISTEMA_COMPLETO.md" target="_blank">Reporte del Sistema Completo</a></li>
<li><a href="../flujos_chatbot_whatsapp.md" target="_blank">Flujos del Chatbot</a></li>
<li><a href="../diagramas_flujo_chatbot.md" target="_blank">Diagramas de Flujo</a></li>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
-- =====================================================
-- MIGRACIÓN: Tabla de Estados de Conversación
-- Fecha: 13 de enero de 2026
-- =====================================================
-- Crear tabla user_states si no existe
CREATE TABLE IF NOT EXISTS user_states (
id INT AUTO_INCREMENT PRIMARY KEY,
phone_number VARCHAR(20) NOT NULL,
state VARCHAR(50) NOT NULL,
state_data TEXT NULL COMMENT 'JSON con datos adicionales del estado',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_phone (phone_number),
INDEX idx_state (state),
INDEX idx_updated (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Agregar columna parent_menu_id si no existe
ALTER TABLE menus
ADD COLUMN IF NOT EXISTS parent_menu_id INT NULL COMMENT 'ID del menú padre para submenús',
ADD CONSTRAINT fk_parent_menu FOREIGN KEY (parent_menu_id) REFERENCES menus(id) ON DELETE SET NULL;
-- Agregar índice para mejorar rendimiento
ALTER TABLE autoresponses
ADD INDEX IF NOT EXISTS idx_trigger_type (trigger_type),
ADD INDEX IF NOT EXISTS idx_priority (priority DESC);
SELECT '✅ Migración completada exitosamente' as status;
+387
View File
@@ -0,0 +1,387 @@
<?php
/**
* Bot Service Mejorado - Laboratorio Ximena Caicedo
* Integra todos los flujos y servicios del chatbot
* Fecha: 13 de enero de 2026
*/
require_once __DIR__ . '/WhatsAppService.php';
require_once __DIR__ . '/BusinessHoursService.php';
require_once __DIR__ . '/ConversationStateService.php';
require_once __DIR__ . '/NLPService.php';
require_once __DIR__ . '/MenuService.php';
class BotServiceLab
{
private $db;
private $whatsappService;
private $businessHoursService;
private $stateService;
private $nlpService;
private $menuService;
public function __construct()
{
$this->db = Database::getInstance();
$this->whatsappService = new WhatsAppService();
$this->businessHoursService = new BusinessHoursService();
$this->stateService = new ConversationStateService();
$this->nlpService = new NLPService();
$this->menuService = new MenuService();
}
/**
* Procesa un mensaje entrante
* @param array $user
* @param string $messageText
* @param string $messageType
* @param string|null $mediaId
*/
public function processMessage(array $user, string $messageText, string $messageType = 'text', ?string $mediaId = null): void
{
$phoneNumber = $user['phone_number'];
try {
// 1. Verificar si el usuario está con un asesor
if ($this->stateService->isWaitingForAdvisor($phoneNumber)) {
// No procesar automáticamente, está con asesor humano
$this->logMessage($phoneNumber, "Usuario con asesor, mensaje no procesado automáticamente");
return;
}
// 2. Verificar horario de atención
if (!$this->businessHoursService->isBusinessHours()) {
if ($this->businessHoursService->shouldShowOutOfHoursMessage($phoneNumber)) {
$message = $this->businessHoursService->getMessage();
$this->sendMessage($phoneNumber, $message);
}
return;
}
// 3. Verificar si el estado ha expirado
if ($this->stateService->isStateExpired($phoneNumber)) {
$this->stateService->clearState($phoneNumber);
}
// 4. Procesar según el tipo de mensaje
if ($messageType !== 'text' && $mediaId) {
$this->processMediaMessage($user, $messageType, $mediaId, $messageText);
return;
}
// 5. Verificar si es el primer mensaje (bienvenida)
if ($this->isFirstMessage($phoneNumber)) {
$this->sendWelcomeMessage($phoneNumber);
return;
}
// 6. Procesar mensaje de texto con NLP
$this->processTextMessage($user, $messageText);
} catch (Exception $e) {
error_log("Error en BotServiceLab->processMessage: " . $e->getMessage());
$this->sendMessage($phoneNumber, "Lo siento, ocurrió un error. Por favor intenta de nuevo o escribe *ASESOR* para ayuda.");
}
}
/**
* Procesa un mensaje de texto
* @param array $user
* @param string $messageText
*/
private function processTextMessage(array $user, string $messageText): void
{
$phoneNumber = $user['phone_number'];
// Procesar con NLP para encontrar intención
$response = $this->nlpService->processMessage($messageText, $phoneNumber);
if (!$response) {
// No se encontró respuesta, enviar mensaje por defecto
$this->sendMessage($phoneNumber, "❓ No he comprendido tu mensaje.\n\nEscribe *MENÚ* para ver las opciones disponibles.");
return;
}
// Incrementar intentos si no entendió
if ($response['type'] === 'default') {
$attempts = $this->stateService->incrementAttempts($phoneNumber);
// Si son 3 intentos fallidos, transferir a asesor
if ($attempts >= 3) {
$this->transferToAdvisor($phoneNumber, 'No se pudo entender el mensaje después de 3 intentos');
return;
}
} else {
// Resetear intentos si entendió correctamente
$this->stateService->resetAttempts($phoneNumber);
}
// Detectar frustración
if ($this->nlpService->detectsFrustration($messageText)) {
$this->transferToAdvisor($phoneNumber, 'Frustración detectada');
return;
}
// Ejecutar acción según el tipo de respuesta
$this->executeResponse($phoneNumber, $response);
}
/**
* Ejecuta la acción según el tipo de respuesta
* @param string $phoneNumber
* @param array $response
*/
private function executeResponse(string $phoneNumber, array $response): void
{
switch ($response['type']) {
case 'text':
$this->sendMessage($phoneNumber, $response['message']);
break;
case 'menu':
$menuId = $response['menu_id'] ?? null;
if ($menuId) {
$this->sendMenu($phoneNumber, $menuId);
}
break;
case 'template':
$templateName = $response['template_name'] ?? null;
if ($templateName) {
$this->sendTemplate($phoneNumber, $templateName);
}
break;
case 'transfer':
$message = $response['message'] ?? 'Conectando con asesor...';
$this->transferToAdvisor($phoneNumber, 'Solicitud del usuario', $message);
break;
}
}
/**
* Procesa mensajes multimedia
* @param array $user
* @param string $messageType
* @param string $mediaId
* @param string $caption
*/
private function processMediaMessage(array $user, string $messageType, string $mediaId, string $caption = ''): void
{
$phoneNumber = $user['phone_number'];
$currentState = $this->stateService->getState($phoneNumber);
// Si está esperando foto de orden médica o documento
if ($currentState && in_array($currentState['state'], [
ConversationStateService::STATE_AWAITING_ORDER_PHOTO,
ConversationStateService::STATE_AWAITING_ID_PHOTO
])) {
if ($messageType === 'image' || $messageType === 'document') {
$this->handleUploadedDocument($user, $mediaId, $currentState['state']);
} else {
$this->sendMessage($phoneNumber, "Por favor envía una imagen o documento PDF.");
}
return;
}
// Si no está esperando media, informar
$this->sendMessage($phoneNumber, "He recibido tu archivo. ¿En qué puedo ayudarte?\n\nEscribe *MENÚ* para ver las opciones.");
}
/**
* Maneja documentos subidos (órdenes médicas, ID, etc.)
* @param array $user
* @param string $mediaId
* @param string $expectedType
*/
private function handleUploadedDocument(array $user, string $mediaId, string $expectedType): void
{
$phoneNumber = $user['phone_number'];
try {
// Descargar el archivo desde WhatsApp
$mediaUrl = $this->whatsappService->downloadMedia($mediaId);
if (!$mediaUrl) {
$this->sendMessage($phoneNumber, "❌ No pude descargar el archivo. Por favor intenta de nuevo.");
return;
}
// Guardar información del documento
if ($expectedType === ConversationStateService::STATE_AWAITING_ORDER_PHOTO) {
$this->stateService->updateStateData($phoneNumber, [
'order_photo_url' => $mediaUrl,
'order_received_at' => date('Y-m-d H:i:s')
]);
$this->sendMessage($phoneNumber, "✅ Orden médica recibida.\n\nAhora necesito los datos del paciente:\n\n✓ Nombre completo:\n✓ Número de documento:\n✓ Dirección completa:\n✓ Número de celular:\n✓ Correo electrónico:\n✓ ¿Es particular o por seguro?\n\nEnvíalos en UN SOLO mensaje.");
$this->stateService->setState($phoneNumber, ConversationStateService::STATE_AWAITING_PATIENT_DATA);
} elseif ($expectedType === ConversationStateService::STATE_AWAITING_ID_PHOTO) {
$this->stateService->updateStateData($phoneNumber, [
'id_photo_url' => $mediaUrl,
'id_received_at' => date('Y-m-d H:i:s')
]);
$this->sendMessage($phoneNumber, "✅ Documento de identidad recibido.\n\nProcesando tu solicitud...");
// Aquí iría lógica adicional según el flujo
$this->stateService->setState($phoneNumber, ConversationStateService::STATE_VALIDATING_DATA);
}
} catch (Exception $e) {
error_log("Error downloading media: " . $e->getMessage());
$this->sendMessage($phoneNumber, "❌ Hubo un problema al procesar el archivo. Por favor intenta nuevamente.");
}
}
/**
* Envía el mensaje de bienvenida
* @param string $phoneNumber
*/
private function sendWelcomeMessage(string $phoneNumber): void
{
$message = $this->businessHoursService->getMessage();
$this->sendMessage($phoneNumber, $message);
// Si está en horario, enviar menú principal
if ($this->businessHoursService->isBusinessHours()) {
$mainMenuId = $this->menuService->getMainMenuId();
if ($mainMenuId) {
$this->stateService->setCurrentMenu($phoneNumber, $mainMenuId);
}
}
}
/**
* Envía un menú
* @param string $phoneNumber
* @param int $menuId
*/
private function sendMenu(string $phoneNumber, int $menuId): void
{
$menu = $this->menuService->getMenu($menuId);
if (!$menu) {
$this->sendMessage($phoneNumber, "❌ Menú no encontrado.");
return;
}
$this->sendMessage($phoneNumber, $menu['welcome_message']);
$this->stateService->setCurrentMenu($phoneNumber, $menuId);
}
/**
* Transfiere la conversación a un asesor
* @param string $phoneNumber
* @param string $reason
* @param string $message
*/
private function transferToAdvisor(string $phoneNumber, string $reason, string $message = null): void
{
// Enviar mensaje al usuario
$userMessage = $message ?? '👤 SOLICITUD DE ASESOR\n\nUn momento por favor, estamos conectando tu consulta con uno de nuestros asesores.\n\nEn breve te atenderemos.\n\n*Tu consulta es importante para nosotros* 💙';
$this->sendMessage($phoneNumber, $userMessage);
// Marcar en el estado
$this->stateService->markForTransfer($phoneNumber, $reason);
// Aquí se podría notificar al equipo de asesores
// Por ejemplo, enviar notificación, email, etc.
$this->logMessage($phoneNumber, "Transferido a asesor: $reason");
}
/**
* Envía una plantilla de WhatsApp
* @param string $phoneNumber
* @param string $templateName
* @param array $parameters
*/
private function sendTemplate(string $phoneNumber, string $templateName, array $parameters = []): void
{
try {
$result = $this->whatsappService->sendTemplateMessage($phoneNumber, $templateName, $parameters);
if (!$result) {
$this->sendMessage($phoneNumber, "❌ No pude enviar la plantilla. Por favor contacta a un asesor.");
}
} catch (Exception $e) {
error_log("Error sending template: " . $e->getMessage());
$this->sendMessage($phoneNumber, "❌ Error al enviar plantilla.");
}
}
/**
* Envía un mensaje de texto
* @param string $phoneNumber
* @param string $message
* @return bool
*/
private function sendMessage(string $phoneNumber, string $message): bool
{
try {
$result = $this->whatsappService->sendTextMessage($phoneNumber, $message);
if ($result) {
// Guardar en BD
$this->saveOutgoingMessage($phoneNumber, $message);
}
return $result;
} catch (Exception $e) {
error_log("Error sending message: " . $e->getMessage());
return false;
}
}
/**
* Guarda un mensaje saliente en la BD
* @param string $phoneNumber
* @param string $message
*/
private function saveOutgoingMessage(string $phoneNumber, string $message): void
{
try {
$stmt = $this->db->prepare("
INSERT INTO conversations
(phone_number, message_type, message_text, direction, status, created_at)
VALUES (?, 'text', ?, 'outgoing', 'sent', NOW())
");
$stmt->execute([$phoneNumber, $message]);
} catch (Exception $e) {
error_log("Error saving outgoing message: " . $e->getMessage());
}
}
/**
* Verifica si es el primer mensaje del usuario
* @param string $phoneNumber
* @return bool
*/
private function isFirstMessage(string $phoneNumber): bool
{
$stmt = $this->db->prepare("
SELECT COUNT(*) as count
FROM conversations
WHERE phone_number = ?
AND direction = 'incoming'
");
$stmt->execute([$phoneNumber]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result && $result['count'] == 1; // Solo tiene un mensaje (el actual)
}
/**
* Registra un log de la conversación
* @param string $phoneNumber
* @param string $message
*/
private function logMessage(string $phoneNumber, string $message): void
{
error_log("[Bot - $phoneNumber] $message");
}
}
+221
View File
@@ -0,0 +1,221 @@
<?php
/**
* Servicio de Horarios de Atención
* Detecta si el bot está dentro del horario de atención
* y proporciona mensajes personalizados
*/
class BusinessHoursService
{
private $db;
public function __construct()
{
$this->db = Database::getInstance();
}
/**
* Verifica si actualmente estamos en horario de atención
* @return bool
*/
public function isBusinessHours(): bool
{
$now = new DateTime('now', new DateTimeZone('America/Bogota'));
$dayOfWeek = (int)$now->format('N'); // 1=Lunes, 7=Domingo
$currentTime = $now->format('H:i');
// Domingo (7) siempre cerrado
if ($dayOfWeek === 7) {
return false;
}
// Sábado (6)
if ($dayOfWeek === 6) {
$saturdayHours = $this->getBusinessHoursConfig('business_hours_saturday');
return $this->isTimeInRange($currentTime, $saturdayHours);
}
// Lunes a Viernes (1-5)
$weekdayHours = $this->getBusinessHoursConfig('business_hours_weekday');
return $this->isTimeInRange($currentTime, $weekdayHours);
}
/**
* Obtiene el mensaje apropiado según el horario
* @return string
*/
public function getMessage(): string
{
if ($this->isBusinessHours()) {
return $this->getWelcomeMessage();
} else {
return $this->getOutOfHoursMessage();
}
}
/**
* Obtiene el mensaje de bienvenida
* @return string
*/
private function getWelcomeMessage(): string
{
$stmt = $this->db->prepare("
SELECT config_value
FROM system_config
WHERE config_key = 'welcome_message'
LIMIT 1
");
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
return $result['config_value'];
}
// Mensaje por defecto
return "¡Hola! 👋 Bienvenido(a) a nuestro servicio de WhatsApp.\n\nEscribe MENÚ para ver las opciones.";
}
/**
* Obtiene el mensaje fuera de horario
* @return string
*/
private function getOutOfHoursMessage(): string
{
$stmt = $this->db->prepare("
SELECT config_value
FROM system_config
WHERE config_key = 'out_of_hours_message'
LIMIT 1
");
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
return $result['config_value'];
}
// Mensaje por defecto
return "GRACIAS POR COMUNICARSE CON NOSOTROS ⏳\n\nEn este momento NO nos encontramos disponibles.\n\nTe responderemos lo antes posible en nuestro horario de atención.\n\n💻 Este es un mensaje automático\nGracias por tu comprensión 😊";
}
/**
* Obtiene la configuración de horarios
* @param string $key
* @return string
*/
private function getBusinessHoursConfig(string $key): string
{
$stmt = $this->db->prepare("
SELECT config_value
FROM system_config
WHERE config_key = ?
LIMIT 1
");
$stmt->execute([$key]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['config_value'] : '';
}
/**
* Verifica si una hora está dentro de un rango
* @param string $currentTime Formato HH:mm
* @param string $rangesString Formato "HH:mm-HH:mm,HH:mm-HH:mm"
* @return bool
*/
private function isTimeInRange(string $currentTime, string $rangesString): bool
{
if (empty($rangesString) || $rangesString === 'closed') {
return false;
}
// Dividir por comas para múltiples rangos (ej: "7:00-12:00,14:00-17:00")
$ranges = explode(',', $rangesString);
foreach ($ranges as $range) {
$times = explode('-', trim($range));
if (count($times) !== 2) {
continue;
}
$start = trim($times[0]);
$end = trim($times[1]);
if ($currentTime >= $start && $currentTime <= $end) {
return true;
}
}
return false;
}
/**
* Obtiene información detallada del horario actual
* @return array
*/
public function getCurrentStatus(): array
{
$now = new DateTime('now', new DateTimeZone('America/Bogota'));
$dayOfWeek = (int)$now->format('N');
$dayName = $this->getDayName($dayOfWeek);
$currentTime = $now->format('H:i');
$isOpen = $this->isBusinessHours();
return [
'is_open' => $isOpen,
'current_day' => $dayName,
'current_time' => $currentTime,
'day_of_week' => $dayOfWeek,
'timezone' => 'America/Bogota'
];
}
/**
* Obtiene el nombre del día en español
* @param int $dayNumber 1-7 (1=Lunes)
* @return string
*/
private function getDayName(int $dayNumber): string
{
$days = [
1 => 'Lunes',
2 => 'Martes',
3 => 'Miércoles',
4 => 'Jueves',
5 => 'Viernes',
6 => 'Sábado',
7 => 'Domingo'
];
return $days[$dayNumber] ?? 'Desconocido';
}
/**
* Determina si se debe mostrar el mensaje de fuera de horario
* @param string $phoneNumber
* @return bool
*/
public function shouldShowOutOfHoursMessage(string $phoneNumber): bool
{
if ($this->isBusinessHours()) {
return false;
}
// Verificar si ya se le envió el mensaje fuera de horario hoy
$stmt = $this->db->prepare("
SELECT id
FROM conversations
WHERE phone_number = ?
AND message_type = 'outgoing'
AND message_text LIKE '%NO nos encontramos disponibles%'
AND DATE(created_at) = CURDATE()
LIMIT 1
");
$stmt->execute([$phoneNumber]);
// Si ya se le envió hoy, no enviar de nuevo
return $stmt->fetch() === false;
}
}
+293
View File
@@ -0,0 +1,293 @@
<?php
/**
* Servicio de Gestión de Estados de Conversación
* Controla el flujo y contexto de las conversaciones
*/
class ConversationStateService
{
private $db;
// Estados posibles
const STATE_INITIAL = 'initial';
const STATE_MAIN_MENU = 'main_menu';
const STATE_SUBMENU_EXAMS = 'submenu_exams';
const STATE_REQUESTING_HOME_SERVICE = 'requesting_home_service';
const STATE_AWAITING_ORDER_PHOTO = 'awaiting_order_photo';
const STATE_AWAITING_PATIENT_DATA = 'awaiting_patient_data';
const STATE_AWAITING_ID_PHOTO = 'awaiting_id_photo';
const STATE_VALIDATING_DATA = 'validating_data';
const STATE_QUOTATION = 'quotation';
const STATE_AWAITING_CONFIRMATION = 'awaiting_confirmation';
const STATE_SCHEDULED = 'scheduled';
const STATE_REQUESTING_RESULTS_HELP = 'requesting_results_help';
const STATE_WITH_ADVISOR = 'with_advisor';
const STATE_COMPLETED = 'completed';
public function __construct()
{
$this->db = Database::getInstance();
}
/**
* Obtiene el estado actual de un usuario
* @param string $phoneNumber
* @return array|null
*/
public function getState(string $phoneNumber): ?array
{
$stmt = $this->db->prepare("
SELECT * FROM user_states
WHERE phone_number = ?
ORDER BY id DESC
LIMIT 1
");
$stmt->execute([$phoneNumber]);
$state = $stmt->fetch(PDO::FETCH_ASSOC);
if ($state && $state['state_data']) {
$state['state_data'] = json_decode($state['state_data'], true);
}
return $state ?: null;
}
/**
* Establece un nuevo estado para el usuario
* @param string $phoneNumber
* @param string $state
* @param array $data Datos adicionales del estado
* @return bool
*/
public function setState(string $phoneNumber, string $state, array $data = []): bool
{
$stmt = $this->db->prepare("
INSERT INTO user_states (phone_number, state, state_data, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
ON DUPLICATE KEY UPDATE
state = VALUES(state),
state_data = VALUES(state_data),
updated_at = NOW()
");
$stateDataJson = !empty($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : null;
return $stmt->execute([$phoneNumber, $state, $stateDataJson]);
}
/**
* Actualiza los datos del estado actual sin cambiar el estado
* @param string $phoneNumber
* @param array $data
* @return bool
*/
public function updateStateData(string $phoneNumber, array $data): bool
{
$currentState = $this->getState($phoneNumber);
if (!$currentState) {
return false;
}
$existingData = $currentState['state_data'] ?? [];
$mergedData = array_merge($existingData, $data);
$stmt = $this->db->prepare("
UPDATE user_states
SET state_data = ?, updated_at = NOW()
WHERE phone_number = ?
");
return $stmt->execute([
json_encode($mergedData, JSON_UNESCAPED_UNICODE),
$phoneNumber
]);
}
/**
* Obtiene datos específicos del estado
* @param string $phoneNumber
* @param string $key
* @return mixed
*/
public function getStateData(string $phoneNumber, string $key)
{
$state = $this->getState($phoneNumber);
if (!$state || !isset($state['state_data'])) {
return null;
}
return $state['state_data'][$key] ?? null;
}
/**
* Limpia el estado del usuario
* @param string $phoneNumber
* @return bool
*/
public function clearState(string $phoneNumber): bool
{
$stmt = $this->db->prepare("
DELETE FROM user_states
WHERE phone_number = ?
");
return $stmt->execute([$phoneNumber]);
}
/**
* Verifica si el usuario está en un flujo específico
* @param string $phoneNumber
* @param string $state
* @return bool
*/
public function isInState(string $phoneNumber, string $state): bool
{
$currentState = $this->getState($phoneNumber);
return $currentState && $currentState['state'] === $state;
}
/**
* Obtiene el contador de intentos
* @param string $phoneNumber
* @return int
*/
public function getAttemptCount(string $phoneNumber): int
{
return (int)($this->getStateData($phoneNumber, 'attempt_count') ?? 0);
}
/**
* Incrementa el contador de intentos
* @param string $phoneNumber
* @return int Nuevo número de intentos
*/
public function incrementAttempts(string $phoneNumber): int
{
$attempts = $this->getAttemptCount($phoneNumber) + 1;
$this->updateStateData($phoneNumber, ['attempt_count' => $attempts]);
return $attempts;
}
/**
* Resetea el contador de intentos
* @param string $phoneNumber
* @return bool
*/
public function resetAttempts(string $phoneNumber): bool
{
return $this->updateStateData($phoneNumber, ['attempt_count' => 0]);
}
/**
* Guarda datos temporales del agendamiento
* @param string $phoneNumber
* @param array $appointmentData
* @return bool
*/
public function saveAppointmentData(string $phoneNumber, array $appointmentData): bool
{
return $this->updateStateData($phoneNumber, [
'appointment' => $appointmentData,
'appointment_started_at' => date('Y-m-d H:i:s')
]);
}
/**
* Obtiene datos del agendamiento en curso
* @param string $phoneNumber
* @return array|null
*/
public function getAppointmentData(string $phoneNumber): ?array
{
return $this->getStateData($phoneNumber, 'appointment');
}
/**
* Verifica si el estado ha expirado (más de 30 minutos de inactividad)
* @param string $phoneNumber
* @return bool
*/
public function isStateExpired(string $phoneNumber): bool
{
$state = $this->getState($phoneNumber);
if (!$state) {
return false;
}
$updatedAt = strtotime($state['updated_at']);
$now = time();
$diffMinutes = ($now - $updatedAt) / 60;
// Expirar después de 30 minutos
return $diffMinutes > 30;
}
/**
* Limpia estados expirados de todos los usuarios
* @return int Número de estados eliminados
*/
public function cleanExpiredStates(): int
{
$stmt = $this->db->prepare("
DELETE FROM user_states
WHERE updated_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)
AND state NOT IN (?, ?)
");
$stmt->execute([
self::STATE_SCHEDULED,
self::STATE_WITH_ADVISOR
]);
return $stmt->rowCount();
}
/**
* Obtiene el menú actual del usuario
* @param string $phoneNumber
* @return int|null ID del menú
*/
public function getCurrentMenuId(string $phoneNumber): ?int
{
return $this->getStateData($phoneNumber, 'current_menu_id');
}
/**
* Establece el menú actual
* @param string $phoneNumber
* @param int $menuId
* @return bool
*/
public function setCurrentMenu(string $phoneNumber, int $menuId): bool
{
return $this->updateStateData($phoneNumber, ['current_menu_id' => $menuId]);
}
/**
* Marca la conversación para transferir a asesor
* @param string $phoneNumber
* @param string $reason
* @return bool
*/
public function markForTransfer(string $phoneNumber, string $reason = ''): bool
{
return $this->setState($phoneNumber, self::STATE_WITH_ADVISOR, [
'transfer_reason' => $reason,
'transferred_at' => date('Y-m-d H:i:s')
]);
}
/**
* Verifica si el usuario está esperando asesor
* @param string $phoneNumber
* @return bool
*/
public function isWaitingForAdvisor(string $phoneNumber): bool
{
return $this->isInState($phoneNumber, self::STATE_WITH_ADVISOR);
}
}
+222
View File
@@ -0,0 +1,222 @@
<?php
/**
* Servicio de Gestión de Menús
* Maneja la obtención y navegación de menús
*/
class MenuService
{
private $db;
public function __construct()
{
$this->db = Database::getInstance();
}
/**
* Obtiene un menú por su ID
* @param int $menuId
* @return array|null
*/
public function getMenu(int $menuId): ?array
{
$stmt = $this->db->prepare("
SELECT * FROM menus
WHERE id = ?
AND status = 'active'
LIMIT 1
");
$stmt->execute([$menuId]);
$menu = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$menu) {
return null;
}
// Obtener opciones del menú
$menu['options'] = $this->getMenuOptions($menuId);
return $menu;
}
/**
* Obtiene un menú por su clave
* @param string $menuKey
* @return array|null
*/
public function getMenuByKey(string $menuKey): ?array
{
$stmt = $this->db->prepare("
SELECT * FROM menus
WHERE menu_key = ?
AND status = 'active'
LIMIT 1
");
$stmt->execute([$menuKey]);
$menu = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$menu) {
return null;
}
$menu['options'] = $this->getMenuOptions($menu['id']);
return $menu;
}
/**
* Obtiene las opciones de un menú
* @param int $menuId
* @return array
*/
public function getMenuOptions(int $menuId): array
{
$stmt = $this->db->prepare("
SELECT * FROM menu_options
WHERE menu_id = ?
AND is_active = 1
ORDER BY option_order ASC
");
$stmt->execute([$menuId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Obtiene el ID del menú principal
* @return int|null
*/
public function getMainMenuId(): ?int
{
$stmt = $this->db->prepare("
SELECT id FROM menus
WHERE menu_key = 'lab_menu_principal'
AND status = 'active'
LIMIT 1
");
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? (int)$result['id'] : null;
}
/**
* Procesa una opción seleccionada del menú
* @param int $menuId
* @param string $optionKey
* @return array|null
*/
public function processMenuOption(int $menuId, string $optionKey): ?array
{
$stmt = $this->db->prepare("
SELECT * FROM menu_options
WHERE menu_id = ?
AND option_key = ?
AND is_active = 1
LIMIT 1
");
$stmt->execute([$menuId, $optionKey]);
$option = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$option) {
return null;
}
return [
'type' => $option['response_type'],
'text' => $option['response_text'],
'next_menu_id' => $option['next_menu_id']
];
}
/**
* Formatea un menú con su welcome_message y opciones
* @param int $menuId
* @return string
*/
public function formatMenuMessage(int $menuId): string
{
$menu = $this->getMenu($menuId);
if (!$menu) {
return "❌ Menú no disponible.";
}
$message = $menu['welcome_message'];
// Si el welcome_message no incluye las opciones, agregarlas
if (!empty($menu['options']) && strpos($message, '1️⃣') === false) {
$message .= "\n\n";
foreach ($menu['options'] as $option) {
$emoji = $this->getNumberEmoji($option['option_key']);
$message .= "$emoji {$option['option_text']}\n";
}
}
return $message;
}
/**
* Obtiene el emoji de número para una opción
* @param string $number
* @return string
*/
private function getNumberEmoji(string $number): string
{
$emojis = [
'0' => '0️⃣',
'1' => '1️⃣',
'2' => '2️⃣',
'3' => '3️⃣',
'4' => '4️⃣',
'5' => '5️⃣',
'6' => '6️⃣',
'7' => '7️⃣',
'8' => '8️⃣',
'9' => '9️⃣'
];
return $emojis[$number] ?? $number;
}
/**
* Obtiene todos los menús activos
* @return array
*/
public function getAllMenus(): array
{
$stmt = $this->db->prepare("
SELECT m.*,
(SELECT COUNT(*) FROM menu_options WHERE menu_id = m.id AND is_active = 1) as options_count
FROM menus m
WHERE m.status = 'active'
ORDER BY m.created_at ASC
");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Obtiene el menú padre de un menú
* @param int $menuId
* @return array|null
*/
public function getParentMenu(int $menuId): ?array
{
$stmt = $this->db->prepare("
SELECT parent_menu_id FROM menus
WHERE id = ?
LIMIT 1
");
$stmt->execute([$menuId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result || !$result['parent_menu_id']) {
return null;
}
return $this->getMenu($result['parent_menu_id']);
}
}
+377
View File
@@ -0,0 +1,377 @@
<?php
/**
* Servicio de Procesamiento de Lenguaje Natural (NLP)
* Detecta intenciones y palabras clave en los mensajes
*/
class NLPService
{
private $db;
public function __construct()
{
$this->db = Database::getInstance();
}
/**
* Procesa un mensaje y encuentra la mejor respuesta automática
* @param string $message
* @param string $phoneNumber
* @return array|null ['type' => 'autoresponse|menu|transfer', 'data' => array]
*/
public function processMessage(string $message, string $phoneNumber): ?array
{
// Normalizar mensaje
$normalizedMessage = $this->normalizeText($message);
// 1. Verificar si es una opción de menú numérica
if (is_numeric(trim($message))) {
return $this->processMenuOption($message, $phoneNumber);
}
// 2. Buscar respuesta automática por palabra clave
$response = $this->findAutoResponse($normalizedMessage);
if ($response) {
return $this->formatResponse($response);
}
// 3. Si no encuentra nada, devolver respuesta por defecto
return $this->getDefaultResponse();
}
/**
* Normaliza el texto para comparación
* @param string $text
* @return string
*/
private function normalizeText(string $text): string
{
// Convertir a minúsculas
$text = mb_strtolower($text, 'UTF-8');
// Remover acentos
$text = $this->removeAccents($text);
// Remover emojis y caracteres especiales (excepto espacios y comas)
$text = preg_replace('/[^\p{L}\p{N}\s,]/u', '', $text);
// Remover espacios extras
$text = preg_replace('/\s+/', ' ', $text);
return trim($text);
}
/**
* Remueve acentos de un texto
* @param string $text
* @return string
*/
private function removeAccents(string $text): string
{
$accents = [
'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u',
'Á' => 'a', 'É' => 'e', 'Í' => 'i', 'Ó' => 'o', 'Ú' => 'u',
'ñ' => 'n', 'Ñ' => 'n', 'ü' => 'u', 'Ü' => 'u'
];
return strtr($text, $accents);
}
/**
* Busca la mejor respuesta automática para el mensaje
* @param string $normalizedMessage
* @return array|null
*/
private function findAutoResponse(string $normalizedMessage): ?array
{
// Obtener todas las respuestas automáticas activas ordenadas por prioridad
$stmt = $this->db->prepare("
SELECT * FROM autoresponses
WHERE is_active = 1
AND trigger_type != 'welcome'
ORDER BY priority DESC, id ASC
");
$stmt->execute();
$autoResponses = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($autoResponses as $response) {
if ($this->matchesTrigger($normalizedMessage, $response)) {
return $response;
}
}
return null;
}
/**
* Verifica si un mensaje coincide con un trigger
* @param string $message
* @param array $response
* @return bool
*/
private function matchesTrigger(string $message, array $response): bool
{
$triggerType = $response['trigger_type'];
$triggerValue = $response['trigger_value'];
switch ($triggerType) {
case 'keyword':
return $this->matchesKeywords($message, $triggerValue);
case 'contains':
return $this->containsPhrase($message, $triggerValue);
case 'exact':
return $this->exactMatch($message, $triggerValue);
case 'default':
return true; // Siempre coincide (pero tiene prioridad baja)
default:
return false;
}
}
/**
* Verifica si el mensaje contiene alguna palabra clave
* @param string $message
* @param string $keywordsString Palabras separadas por comas
* @return bool
*/
private function matchesKeywords(string $message, string $keywordsString): bool
{
if (empty($keywordsString)) {
return false;
}
$keywords = array_map('trim', explode(',', $keywordsString));
$keywords = array_map([$this, 'normalizeText'], $keywords);
$messageWords = explode(' ', $message);
foreach ($keywords as $keyword) {
// Verificar si la palabra clave completa está en el mensaje
if (strpos($message, $keyword) !== false) {
return true;
}
// O si alguna palabra del mensaje coincide exactamente
if (in_array($keyword, $messageWords)) {
return true;
}
}
return false;
}
/**
* Verifica si el mensaje contiene una frase
* @param string $message
* @param string $phrase
* @return bool
*/
private function containsPhrase(string $message, string $phrase): bool
{
$normalizedPhrase = $this->normalizeText($phrase);
return strpos($message, $normalizedPhrase) !== false;
}
/**
* Verifica coincidencia exacta
* @param string $message
* @param string $exact
* @return bool
*/
private function exactMatch(string $message, string $exact): bool
{
$normalizedExact = $this->normalizeText($exact);
return $message === $normalizedExact;
}
/**
* Procesa una opción numérica de menú
* @param string $option
* @param string $phoneNumber
* @return array|null
*/
private function processMenuOption(string $option, string $phoneNumber): ?array
{
$stateService = new ConversationStateService();
$currentMenuId = $stateService->getCurrentMenuId($phoneNumber);
if (!$currentMenuId) {
// Si no hay menú actual, buscar el menú principal
$stmt = $this->db->prepare("
SELECT id FROM menus
WHERE menu_key = 'lab_menu_principal'
AND status = 'active'
LIMIT 1
");
$stmt->execute();
$menu = $stmt->fetch(PDO::FETCH_ASSOC);
$currentMenuId = $menu ? $menu['id'] : null;
}
if (!$currentMenuId) {
return null;
}
// Buscar la opción en el menú actual
$stmt = $this->db->prepare("
SELECT * FROM menu_options
WHERE menu_id = ?
AND option_key = ?
AND is_active = 1
LIMIT 1
");
$stmt->execute([$currentMenuId, trim($option)]);
$menuOption = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$menuOption) {
return null;
}
return $this->formatMenuOption($menuOption);
}
/**
* Formatea una respuesta automática
* @param array $response
* @return array
*/
private function formatResponse(array $response): array
{
$result = [
'type' => $response['response_type'],
'priority' => $response['priority']
];
switch ($response['response_type']) {
case 'text':
$result['message'] = $response['response_text'];
break;
case 'menu':
$result['menu_id'] = $response['menu_id'];
break;
case 'template':
$result['template_name'] = $response['template_name'] ?? null;
break;
case 'transfer':
$result['message'] = $response['response_text'];
$result['transfer'] = true;
break;
}
return $result;
}
/**
* Formatea una opción de menú
* @param array $menuOption
* @return array
*/
private function formatMenuOption(array $menuOption): array
{
$result = [
'type' => $menuOption['response_type']
];
switch ($menuOption['response_type']) {
case 'text':
$result['message'] = $menuOption['response_text'];
break;
case 'menu':
$result['menu_id'] = $menuOption['next_menu_id'];
break;
case 'transfer':
$result['message'] = $menuOption['response_text'];
$result['transfer'] = true;
break;
}
return $result;
}
/**
* Obtiene la respuesta por defecto
* @return array
*/
private function getDefaultResponse(): array
{
$stmt = $this->db->prepare("
SELECT * FROM autoresponses
WHERE trigger_type = 'default'
AND is_active = 1
ORDER BY priority DESC
LIMIT 1
");
$stmt->execute();
$response = $stmt->fetch(PDO::FETCH_ASSOC);
if ($response) {
return $this->formatResponse($response);
}
// Respuesta fallback hardcoded
return [
'type' => 'text',
'message' => "❓ No he comprendido tu mensaje.\n\nEscribe MENÚ para ver las opciones disponibles."
];
}
/**
* Detecta el sentimiento del mensaje (básico)
* @param string $message
* @return string positive|negative|neutral
*/
public function detectSentiment(string $message): string
{
$normalized = $this->normalizeText($message);
// Palabras positivas
$positiveWords = ['gracias', 'excelente', 'perfecto', 'bien', 'bueno', 'genial', 'ok', 'vale'];
// Palabras negativas / frustración
$negativeWords = ['no funciona', 'problema', 'error', 'mal', 'ayuda', 'no entiendo', 'dificil', 'complicado'];
$positiveCount = 0;
$negativeCount = 0;
foreach ($positiveWords as $word) {
if (strpos($normalized, $word) !== false) {
$positiveCount++;
}
}
foreach ($negativeWords as $word) {
if (strpos($normalized, $word) !== false) {
$negativeCount++;
}
}
if ($negativeCount > $positiveCount) {
return 'negative';
} elseif ($positiveCount > 0) {
return 'positive';
}
return 'neutral';
}
/**
* Determina si el mensaje indica frustración (para escalar a asesor)
* @param string $message
* @return bool
*/
public function detectsFrustration(string $message): bool
{
return $this->detectSentiment($message) === 'negative';
}
}
+315
View File
@@ -0,0 +1,315 @@
<?php
/**
* Validadores de Datos del Paciente
* Valida información para agendamientos y procesos
*/
class PatientDataValidator
{
/**
* Valida todos los datos del paciente para agendamiento
* @param array $data
* @return array ['valid' => bool, 'errors' => array, 'data' => array]
*/
public static function validateAppointmentData(array $data): array
{
$errors = [];
$validData = [];
// Nombre completo
if (empty($data['nombre'])) {
$errors[] = "Falta el nombre completo";
} elseif (!self::validateFullName($data['nombre'])) {
$errors[] = "El nombre debe tener al menos nombre y apellido";
} else {
$validData['nombre'] = self::sanitizeName($data['nombre']);
}
// Documento
if (empty($data['documento'])) {
$errors[] = "Falta el número de documento";
} elseif (!self::validateDocument($data['documento'])) {
$errors[] = "El documento debe tener entre 6 y 11 dígitos";
} else {
$validData['documento'] = self::sanitizeDocument($data['documento']);
}
// Dirección
if (empty($data['direccion'])) {
$errors[] = "Falta la dirección completa";
} elseif (strlen($data['direccion']) < 10) {
$errors[] = "La dirección debe ser más específica (mínimo 10 caracteres)";
} else {
$validData['direccion'] = trim($data['direccion']);
}
// Teléfono
if (empty($data['telefono'])) {
$errors[] = "Falta el número de celular";
} elseif (!self::validatePhone($data['telefono'])) {
$errors[] = "El número de celular debe tener 10 dígitos y comenzar con 3";
} else {
$validData['telefono'] = self::sanitizePhone($data['telefono']);
}
// Email
if (empty($data['email'])) {
$errors[] = "Falta el correo electrónico";
} elseif (!self::validateEmail($data['email'])) {
$errors[] = "El correo electrónico no es válido";
} else {
$validData['email'] = strtolower(trim($data['email']));
}
// Tipo (particular/seguro)
if (empty($data['tipo'])) {
$errors[] = "Falta indicar si es particular o por seguro";
} else {
$tipo = strtolower(trim($data['tipo']));
if (strpos($tipo, 'particular') !== false) {
$validData['tipo'] = 'particular';
} elseif (strpos($tipo, 'seguro') !== false || strpos($tipo, 'eps') !== false) {
$validData['tipo'] = 'seguro';
// Extraer nombre del seguro si lo menciona
$validData['nombre_seguro'] = self::extractInsuranceName($data['tipo']);
} else {
$errors[] = "Debe indicar 'particular' o 'seguro'";
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
'data' => $validData
];
}
/**
* Valida un nombre completo
* @param string $name
* @return bool
*/
public static function validateFullName(string $name): bool
{
$name = trim($name);
// Debe tener al menos 2 palabras
$words = preg_split('/\s+/', $name);
if (count($words) < 2) {
return false;
}
// Solo debe contener letras y espacios
return preg_match('/^[a-záéíóúñA-ZÁÉÍÓÚÑ\s]+$/u', $name);
}
/**
* Valida un número de documento
* @param string $document
* @return bool
*/
public static function validateDocument(string $document): bool
{
$doc = preg_replace('/[^0-9]/', '', $document);
$length = strlen($doc);
return $length >= 6 && $length <= 11;
}
/**
* Valida un número de teléfono colombiano
* @param string $phone
* @return bool
*/
public static function validatePhone(string $phone): bool
{
$phone = preg_replace('/[^0-9]/', '', $phone);
// Debe tener 10 dígitos y comenzar con 3
return strlen($phone) === 10 && $phone[0] === '3';
}
/**
* Valida un correo electrónico
* @param string $email
* @return bool
*/
public static function validateEmail(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Sanitiza un nombre
* @param string $name
* @return string
*/
public static function sanitizeName(string $name): string
{
// Convertir a formato título (Primera Letra Mayúscula)
$name = mb_convert_case(trim($name), MB_CASE_TITLE, 'UTF-8');
// Remover espacios extras
$name = preg_replace('/\s+/', ' ', $name);
return $name;
}
/**
* Sanitiza un documento
* @param string $document
* @return string
*/
public static function sanitizeDocument(string $document): string
{
return preg_replace('/[^0-9]/', '', $document);
}
/**
* Sanitiza un teléfono
* @param string $phone
* @return string
*/
public static function sanitizePhone(string $phone): string
{
$phone = preg_replace('/[^0-9]/', '', $phone);
// Si tiene 13 dígitos y empieza con 57, quitar el 57
if (strlen($phone) === 12 && substr($phone, 0, 2) === '57') {
$phone = substr($phone, 2);
}
return $phone;
}
/**
* Extrae el nombre del seguro del texto
* @param string $text
* @return string|null
*/
private static function extractInsuranceName(string $text): ?string
{
$insurances = [
'colsanitas' => 'Colsanitas',
'sanitas' => 'Sanitas',
'sura' => 'Sura',
'compensar' => 'Compensar',
'salud total' => 'Salud Total',
'famisanar' => 'Famisanar',
'nueva eps' => 'Nueva EPS',
'coomeva' => 'Coomeva'
];
$textLower = strtolower($text);
foreach ($insurances as $key => $name) {
if (strpos($textLower, $key) !== false) {
return $name;
}
}
return null;
}
/**
* Parsea datos del paciente desde un mensaje de texto
* @param string $message
* @return array
*/
public static function parsePatientDataFromMessage(string $message): array
{
$data = [];
// Dividir por líneas
$lines = explode("\n", $message);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
// Detectar nombre (primera línea larga con letras)
if (empty($data['nombre']) && preg_match('/^[a-záéíóúñA-ZÁÉÍÓÚÑ\s]{5,}$/u', $line)) {
$data['nombre'] = $line;
continue;
}
// Detectar documento (números de 6-11 dígitos)
if (empty($data['documento']) && preg_match('/(\d{6,11})/', $line, $matches)) {
$data['documento'] = $matches[1];
continue;
}
// Detectar teléfono (10 dígitos comenzando con 3)
if (empty($data['telefono']) && preg_match('/(3\d{9})/', $line, $matches)) {
$data['telefono'] = $matches[1];
continue;
}
// Detectar email
if (empty($data['email']) && preg_match('/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z]+)/', $line, $matches)) {
$data['email'] = $matches[1];
continue;
}
// Detectar dirección (contiene calle, carrera, etc.)
if (empty($data['direccion']) && preg_match('/(calle|carrera|cra|cl|diagonal|transversal|avenida|av)/i', $line)) {
$data['direccion'] = $line;
continue;
}
// Detectar tipo (particular/seguro)
if (empty($data['tipo']) && preg_match('/(particular|seguro|eps)/i', $line)) {
$data['tipo'] = $line;
continue;
}
}
return $data;
}
/**
* Genera un mensaje con los errores de validación
* @param array $errors
* @return string
*/
public static function formatValidationErrors(array $errors): string
{
$message = "⚠️ Por favor corrige los siguientes datos:\n\n";
foreach ($errors as $error) {
$message .= "$error\n";
}
$message .= "\nEnvía la información completa de nuevo.";
return $message;
}
/**
* Genera un resumen de los datos validados
* @param array $data
* @return string
*/
public static function formatDataSummary(array $data): string
{
$message = "📋 DATOS RECIBIDOS:\n\n";
$message .= "👤 Nombre: {$data['nombre']}\n";
$message .= "🆔 Documento: {$data['documento']}\n";
$message .= "📍 Dirección: {$data['direccion']}\n";
$message .= "📱 Teléfono: {$data['telefono']}\n";
$message .= "📧 Email: {$data['email']}\n";
$message .= "💳 Tipo: {$data['tipo']}";
if (isset($data['nombre_seguro'])) {
$message .= " ({$data['nombre_seguro']})";
}
$message .= "\n\n¿Los datos son correctos?\nResponde: *SÍ* o *NO*";
return $message;
}
}
+732
View File
@@ -0,0 +1,732 @@
-- =====================================================
-- SETUP COMPLETO: LABORATORIO XIMENA CAICEDO
-- Sistema de Chatbot WhatsApp con Flujos Completos
-- Fecha: 13 de enero de 2026
-- =====================================================
-- Limpiar datos previos (opcional)
-- DELETE FROM menu_options WHERE menu_id IN (SELECT id FROM menus WHERE menu_key LIKE 'lab_%');
-- DELETE FROM menus WHERE menu_key LIKE 'lab_%';
-- DELETE FROM autoresponses WHERE trigger_type != 'system';
-- =====================================================
-- 1. MENÚ PRINCIPAL
-- =====================================================
INSERT INTO menus (name, menu_key, description, welcome_message, status, created_at) VALUES
(
'Menú Principal - Lab Ximena Caicedo',
'lab_menu_principal',
'Menú principal del laboratorio con todas las opciones de servicio',
'¡Hola! 👋 Bienvenido(a) a Laboratorio Ximena Caicedo
Por favor, selecciona una opción escribiendo el número:
1️⃣ Agendar toma a domicilio
2️⃣ Consultar resultados
3️⃣ Información sobre exámenes
4️⃣ Ubicación y horarios
5️⃣ Hablar con un asesor
*Horario de atención:*
📅 Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM
📅 Sábados: 8:00 AM - 11:00 AM',
'active',
NOW()
);
-- Obtener el ID del menú principal
SET @menu_principal_id = LAST_INSERT_ID();
-- Opciones del Menú Principal
INSERT INTO menu_options (menu_id, option_key, option_order, option_text, response_type, response_text, next_menu_id, is_active, created_at) VALUES
(
@menu_principal_id,
'1',
1,
'Agendar toma a domicilio',
'text',
'📍 TOMA DE MUESTRAS A DOMICILIO
Para agendar tu domicilio, necesitamos la siguiente información en UN SOLO MENSAJE:
📄 Enviar una foto clara de la orden médica
Datos del paciente:
✓ Nombre completo:
✓ Número de documento:
✓ Foto del documento de identidad:
✓ Dirección completa:
✓ Número de celular:
✓ Correo electrónico:
✓ Indicar si es por algún seguro o particular
*IMPORTANTE:*
• Se atiende por orden de llegada
• Enviar solicitud solo una vez
• Pronto te daremos respuesta
💵 Valor del domicilio: $27.000',
NULL,
1,
NOW()
),
(
@menu_principal_id,
'2',
2,
'Consultar resultados',
'text',
'📊 CONSULTA DE RESULTADOS
Para consultar tus resultados, sigue estos pasos:
1️⃣ Ingresa a la página web:
http://www.resultadoslab.com.co/resultadoslab/vistas/login.php?id=eGltY2FpY2Vkbw==
2️⃣ Credenciales de acceso:
• Usuario: Tu número de documento
• Clave: Los últimos 4 dígitos de tu documento
3️⃣ Da clic en los cuadros rojo y naranja que aparecen en opciones
4️⃣ Descarga los archivos PDF
*NOTA:* Se recomienda descargar desde un computador
⚠️ Estamos actualizando nuestra página web. Gracias por tu comprensión.
¿Tienes problemas para descargar tus resultados?
Responde: *AYUDA RESULTADOS*',
NULL,
1,
NOW()
),
(
@menu_principal_id,
'3',
3,
'Información sobre exámenes',
'menu',
NULL,
NULL, -- Se actualizará después de crear el submenú
1,
NOW()
),
(
@menu_principal_id,
'4',
4,
'Ubicación y horarios',
'text',
'📍 UBICACIÓN Y HORARIOS
*DIRECCIÓN:*
Calle 21 #0-26, Barrio Blanco
Cúcuta, Norte de Santander
📌 Ver en Google Maps:
https://maps.google.com/maps/search/Laboratorio%20Ximena%20Caicedo/@7.877200126647949,-72.49710083007812,17z?hl=es
*HORARIOS DE ATENCIÓN:*
🏥 Toma de muestras en laboratorio:
📅 Lunes a viernes: 6:15 AM - 3:00 PM
📅 Sábados: Por orden de llegada
📞 Atención telefónica/WhatsApp:
📅 Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM
📅 Sábados: 8:00 AM - 11:00 AM
🏠 Tomas a domicilio:
📅 Lunes a viernes: 5:00 AM - 8:30 AM
💵 Valor: $27.000
*FORMAS DE PAGO:*
💵 Efectivo
💳 Tarjeta débito
🏥 Seguros médicos
¿Necesitas agendar? Escribe MENÚ',
NULL,
1,
NOW()
),
(
@menu_principal_id,
'5',
5,
'Hablar con un asesor',
'transfer',
'👤 SOLICITUD DE ASESOR
Un momento por favor, estamos conectando tu consulta con uno de nuestros asesores.
En breve te atenderemos.
*Tu consulta es importante para nosotros* 💙',
NULL,
1,
NOW()
);
-- =====================================================
-- 2. SUBMENÚ DE EXÁMENES
-- =====================================================
INSERT INTO menus (name, menu_key, description, welcome_message, parent_menu_id, status, created_at) VALUES
(
'Información de Exámenes',
'lab_menu_examenes',
'Submenú con información detallada de exámenes',
'🔬 INFORMACIÓN DE EXÁMENES
Selecciona el tipo de información que necesitas:
1️⃣ Pruebas de embarazo
2️⃣ Prueba de paternidad
3️⃣ Helicobacter pylori (en aliento)
4️⃣ Orina de 24 horas
5️⃣ Solicitar cotización
0️⃣ Volver al menú principal',
@menu_principal_id,
'active',
NOW()
);
SET @menu_examenes_id = LAST_INSERT_ID();
-- Actualizar la opción 3 del menú principal para que apunte a este submenú
UPDATE menu_options
SET next_menu_id = @menu_examenes_id
WHERE menu_id = @menu_principal_id AND option_key = '3';
-- Opciones del Submenú de Exámenes
INSERT INTO menu_options (menu_id, option_key, option_order, option_text, response_type, response_text, next_menu_id, is_active, created_at) VALUES
(
@menu_examenes_id,
'1',
1,
'Pruebas de embarazo',
'text',
'🤰 PRUEBAS DE EMBARAZO EN SANGRE
Ofrecemos dos tipos:
1️⃣ *Prueba Cualitativa (Positivo/Negativo)*
💵 Valor: $28.000
📅 Requisito: Mínimo 1 semana de retraso menstrual
2️⃣ *Prueba Cuantitativa (Con valor de HCG)*
💵 Valor: $61.000
📊 Esta prueba da un valor que se interpreta para saber las semanas de embarazo
🕐 Horario: 6:15 AM - 3:00 PM (Lunes a viernes)
⏱️ Tiempo de resultado: 24-72 horas
¿Deseas agendar? Escribe: *AGENDAR EMBARAZO*',
NULL,
1,
NOW()
),
(
@menu_examenes_id,
'2',
2,
'Prueba de paternidad',
'text',
'👨‍👩‍👧 PRUEBA DE PATERNIDAD
📍 Ubicación: Calle 21 #0-26, Barrio Blanco, Cúcuta
https://maps.google.com/maps/search/Laboratorio%20Ximena%20Caicedo/@7.877200126647949,-72.49710083007812,17z?hl=es
*DETALLES DEL EXAMEN:*
🧬 Muestra: Se realiza en saliva
👥 Deben asistir: Presunto padre, madre y el menor
💵 Valor: $760.000
📅 Resultados: 12 a 15 días hábiles
🕐 Horario: Lunes a viernes de 6:15 AM a 3:00 PM
📝 Se toma por orden de llegada (sin cita previa)
⚠️ *NO se realiza a domicilio*
Deben acercarse al laboratorio.
¿Necesitas más información? Escribe *ASESOR*',
NULL,
1,
NOW()
),
(
@menu_examenes_id,
'3',
3,
'Helicobacter pylori (en aliento)',
'text',
'🫁 HELICOBACTER PYLORI EN ALIENTO
*PREPARACIÓN IMPORTANTE:*
⏰ Requisitos previos:
• 2 semanas sin tomar medicamentos para el estómago
• Sin protectores gástricos
• Sin probióticos
• Sin antibióticos
• Sin cualquier medicamento relacionado con el estómago
📍 Día del examen:
• Debe venir en AYUNAS
• NO cepillarse los dientes
• Llegar entre 6:15 AM - 9:30 AM
📅 Disponible de lunes a viernes
¿Deseas cotización? Escribe: *COTIZAR HELICOBACTER*',
NULL,
1,
NOW()
),
(
@menu_examenes_id,
'4',
4,
'Orina de 24 horas',
'text',
'💧 INSTRUCCIONES: ORINA DE 24 HORAS
*PASO A PASO:*
1️⃣ *PRIMERA ORINA DE LA MAÑANA:*
Deséchela (NO la recoja)
2️⃣ *A PARTIR DE LA SEGUNDA ORINA:*
Recoja TODA la orina del día
3️⃣ *DURANTE LA NOCHE:*
Si se levanta a orinar, también debe recogerla
4️⃣ *ALMACENAMIENTO:*
⚠️ IMPORTANTE: Mantener la orina REFRIGERADA todo el tiempo para evitar contaminación
5️⃣ *AL DÍA SIGUIENTE:*
Llevar el recipiente completo al laboratorio
📍 Horario de recepción: 6:15 AM - 3:00 PM
¿Tienes dudas? Escribe *ASESOR*',
NULL,
1,
NOW()
),
(
@menu_examenes_id,
'5',
5,
'Solicitar cotización',
'text',
'💰 SOLICITAR COTIZACIÓN
Para cotizar tus exámenes, envía:
📄 Foto CLARA de la orden médica
✓ Indica si es particular o por seguro
✓ Si es por seguro, indica cuál
Te responderemos con:
• Valor de cada examen
• Valor total
• Indicaciones de preparación
⏱️ Respuesta en horario de atención',
NULL,
1,
NOW()
),
(
@menu_examenes_id,
'0',
0,
'Volver al menú principal',
'menu',
NULL,
@menu_principal_id,
1,
NOW()
);
-- =====================================================
-- 3. RESPUESTAS AUTOMÁTICAS POR PALABRAS CLAVE
-- =====================================================
-- Bienvenida (primer mensaje)
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'welcome',
NULL,
'menu',
NULL,
@menu_principal_id,
100,
1,
NOW()
);
-- Menú / Inicio
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'menu,menú,inicio,start,opciones',
'menu',
NULL,
@menu_principal_id,
90,
1,
NOW()
);
-- Precios
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'precio,valor,costo,cuanto,cuánto,precios,costos',
'text',
'💰 INFORMACIÓN DE PRECIOS
Para darte información exacta, necesitamos saber:
1️⃣ ¿Qué examen necesitas?
2️⃣ ¿Es particular o por seguro?
Puedes:
• Enviar foto de tu orden médica
• Escribir el nombre del examen
• Escribir *COTIZAR* para más información
También puedes ver nuestros servicios frecuentes:
• EMBARAZO - Pruebas de embarazo
• PATERNIDAD - Prueba de paternidad
• DOMICILIO - Toma a domicilio',
NULL,
80,
1,
NOW()
);
-- Resultados
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'resultado,resultados,descargar,reporte,examen',
'text',
'📊 CONSULTA DE RESULTADOS
Ingresa a:
http://www.resultadoslab.com.co/resultadoslab/vistas/login.php?id=eGltY2FpY2Vkbw==
Usuario: Tu documento
Clave: Últimos 4 dígitos
¿Problemas para descargar?
Escribe: *AYUDA RESULTADOS*',
NULL,
85,
1,
NOW()
);
-- Horarios
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'horario,hora,cuando,cuándo,abierto,atienden,abren,cierran',
'text',
'🕐 HORARIOS DE ATENCIÓN
🏥 *Toma de muestras:*
Lunes a viernes: 6:15 AM - 3:00 PM
📞 *Atención WhatsApp:*
Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM
Sábados: 8:00 AM - 11:00 AM
🏠 *Domicilios:*
Lunes a viernes: 5:00 AM - 8:30 AM
Para más información escribe: *UBICACION*',
NULL,
85,
1,
NOW()
);
-- Domicilio
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'domicilio,casa,ir a mi casa,a casa,visita',
'text',
'🏠 SERVICIO A DOMICILIO
💵 Valor: $27.000
⏰ Horario: 5:00 AM - 8:30 AM
Para agendar escribe: 1 o *DOMICILIO*',
NULL,
85,
1,
NOW()
);
-- Embarazo
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'embarazo,embarazada,gestación,gestacion,prueba embarazo',
'text',
'🤰 PRUEBAS DE EMBARAZO
💵 Cualitativa (Sí/No): $28.000
💵 Cuantitativa (con valor): $61.000
📝 Requisito: Mínimo 1 semana de retraso
Para más información escribe: *INFO EMBARAZO*
Para agendar escribe: *AGENDAR EMBARAZO*',
NULL,
85,
1,
NOW()
);
-- Paternidad
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'paternidad,adn,paternity,prueba de paternidad',
'text',
'👨‍👩‍👧 PRUEBA DE PATERNIDAD
💵 Valor: $760.000
📅 Resultado: 12-15 días
🏥 Se realiza en laboratorio (NO a domicilio)
Para más información escribe: *INFO PATERNIDAD*',
NULL,
85,
1,
NOW()
);
-- Ayuda con resultados
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'ayuda resultados,no puedo descargar,ayuda descargar,problema resultados',
'text',
'🆘 AYUDA CON RESULTADOS
Para ayudarte a descargar tus resultados, necesitamos:
📸 Foto del documento de identidad del paciente
*IMPORTANTE:*
Por política de seguridad del paciente, solo podemos enviar los resultados si nos envías la foto del documento.
Si eres familiar del paciente, debes enviar el documento del titular de los exámenes.',
NULL,
95,
1,
NOW()
);
-- Ubicación
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'ubicación,ubicacion,dirección,direccion,donde,dónde,como llego,cómo llego',
'text',
'📍 UBICACIÓN
Calle 21 #0-26, Barrio Blanco
Cúcuta, Norte de Santander
📌 Google Maps:
https://maps.google.com/maps/search/Laboratorio%20Ximena%20Caicedo/@7.877200126647949,-72.49710083007812,17z?hl=es
Para más información escribe: *HORARIOS*',
NULL,
85,
1,
NOW()
);
-- Preparación / Ayunas
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'ayunas,preparación,preparacion,requisitos,como me preparo,cómo me preparo',
'text',
'📋 PREPARACIÓN PARA EXÁMENES
La preparación depende del examen que te van a realizar.
¿Qué examen te van a tomar?
Exámenes con preparación especial:
• PSA - Antígeno prostático
• Frotis vaginal
• Urocultivo
• Helicobacter pylori
• Orina de 24 horas
• Perfil lipídico (ayunas)
Escribe el nombre del examen o envía foto de tu orden.',
NULL,
85,
1,
NOW()
);
-- Asesor / Ayuda / Persona
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'asesor,persona,humano,ayuda,help,operador,hablar con alguien',
'transfer',
'👤 SOLICITUD DE ASESOR
Un momento por favor, estamos conectando tu consulta con uno de nuestros asesores.
En breve te atenderemos.
*Tu consulta es importante para nosotros* 💙',
NULL,
70,
1,
NOW()
);
-- Agradecimientos
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'gracias,thanks,thank you,muchas gracias,ok gracias',
'text',
'😊 ¡De nada! Con gusto.
¿Necesitas algo más?
Escribe *MENÚ* para ver todas las opciones.
Laboratorio Ximena Caicedo
Tu salud, nuestra prioridad 💙',
NULL,
60,
1,
NOW()
);
-- Despedidas
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'adios,adiós,bye,chao,hasta luego,nos vemos',
'text',
'👋 ¡Hasta pronto!
Gracias por confiar en Laboratorio Ximena Caicedo.
Si necesitas algo más, no dudes en escribirnos.
¡Que tengas un excelente día! 😊',
NULL,
60,
1,
NOW()
);
-- Saludos generales
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'keyword',
'hola,buenos dias,buenas tardes,buenas noches,buen dia,hey,hi,hello',
'menu',
NULL,
@menu_principal_id,
75,
1,
NOW()
);
-- Respuesta por defecto (cuando no entiende)
INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES
(
'default',
NULL,
'menu',
'❓ No he comprendido tu mensaje
Por favor selecciona una opción:',
@menu_principal_id,
10,
1,
NOW()
);
-- =====================================================
-- 4. ACTUALIZAR CONFIGURACIÓN DEL SISTEMA
-- =====================================================
-- Actualizar mensaje de bienvenida en system_config
UPDATE system_config
SET config_value = (SELECT welcome_message FROM menus WHERE menu_key = 'lab_menu_principal' LIMIT 1)
WHERE config_key = 'welcome_message';
-- Agregar nombre de empresa si no existe
INSERT INTO system_config (config_key, config_value, description, created_at)
VALUES ('business_name', 'Laboratorio Ximena Caicedo', 'Nombre de la empresa/laboratorio', NOW())
ON DUPLICATE KEY UPDATE config_value = 'Laboratorio Ximena Caicedo';
-- Agregar horarios de atención
INSERT INTO system_config (config_key, config_value, description, created_at)
VALUES
('business_hours_weekday', '7:00-12:00,14:00-17:00', 'Horario de atención lunes a viernes (formato 24h)', NOW()),
('business_hours_saturday', '8:00-11:00', 'Horario de atención sábados (formato 24h)', NOW()),
('business_hours_sunday', 'closed', 'Horario de atención domingos', NOW()),
('out_of_hours_message', 'GRACIAS POR COMUNICARSE CON Laboratorio Ximena Caicedo ⏳\n\nEn este momento NO nos encontramos disponibles.\n\n*Horario de atención:*\n📅 Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM\n📅 Sábados: 8:00 AM - 11:00 AM\n🚫 Domingos y festivos cerrado\n\nPor favor déjanos tu *Nombre* y tu *mensaje*, y con gusto te responderemos al retomar nuestro horario.\n\n💻 Este es un mensaje automático\nGracias por tu comprensión 😊', 'Mensaje automático fuera de horario', NOW())
ON DUPLICATE KEY UPDATE config_value = VALUES(config_value);
-- =====================================================
-- CONFIRMACIÓN
-- =====================================================
SELECT
'✅ SETUP COMPLETADO' as status,
COUNT(DISTINCT m.id) as menus_creados,
COUNT(DISTINCT mo.id) as opciones_creadas,
COUNT(DISTINCT ar.id) as respuestas_automaticas
FROM menus m
LEFT JOIN menu_options mo ON m.id = mo.menu_id
LEFT JOIN autoresponses ar ON ar.is_active = 1
WHERE m.menu_key LIKE 'lab_%';
-- Ver menús creados
SELECT
id,
name,
menu_key,
status,
(SELECT COUNT(*) FROM menu_options WHERE menu_id = m.id) as num_opciones
FROM menus m
WHERE menu_key LIKE 'lab_%'
ORDER BY created_at;
-- Ver respuestas automáticas creadas
SELECT
id,
trigger_type,
trigger_value,
response_type,
priority,
is_active
FROM autoresponses
WHERE is_active = 1
ORDER BY priority DESC, id;
-204
View File
@@ -1,204 +0,0 @@
<?php
/**
* Script de prueba del sistema WhatsApp Bot
* Fecha: 13 de noviembre de 2025
*/
error_reporting(E_ALL);
ini_set('display_errors', 1);
echo "<h1>🤖 Test del Sistema WhatsApp Bot</h1>";
// Verificar archivos principales
$files = [
'config/config.php' => 'Configuración principal',
'classes/Database.php' => 'Clase Database',
'services/WhatsAppService.php' => 'Servicio WhatsApp',
'services/BotService.php' => 'Servicio Bot',
'api/webhook.php' => 'Webhook principal',
'database/schema.sql' => 'Esquema de BD',
'index.php' => 'Página principal'
];
echo "<h2>📂 Verificación de Archivos:</h2>";
echo "<ul>";
foreach ($files as $file => $description) {
$exists = file_exists($file);
$status = $exists ? "✅ Existe" : "❌ Faltante";
echo "<li><strong>{$description}</strong> ({$file}): {$status}</li>";
}
echo "</ul>";
// Probar configuración
echo "<h2>⚙️ Verificación de Configuración:</h2>";
try {
require_once 'config/config.php';
echo "<p>✅ Configuración cargada correctamente</p>";
echo "<ul>";
echo "<li>DB Host: " . DB_HOST . "</li>";
echo "<li>DB Name: " . DB_NAME . "</li>";
echo "<li>WhatsApp Phone ID: " . WHATSAPP_PHONE_NUMBER_ID . "</li>";
echo "<li>App URL: " . APP_URL . "</li>";
echo "</ul>";
} catch (Exception $e) {
echo "<p>❌ Error en configuración: " . $e->getMessage() . "</p>";
}
// Probar conexión a base de datos
echo "<h2>💾 Verificación de Base de Datos:</h2>";
try {
$db = Database::getInstance();
echo "<p>✅ Conexión a base de datos exitosa</p>";
// Verificar tablas principales
$tables = ['users', 'conversations', 'menus', 'menu_options', 'system_config'];
echo "<ul>";
foreach ($tables as $table) {
try {
$count = $db->fetch("SELECT COUNT(*) as count FROM {$table}")['count'];
echo "<li>Tabla <strong>{$table}</strong>: {$count} registros</li>";
} catch (Exception $e) {
echo "<li>Tabla <strong>{$table}</strong>: ❌ Error: " . $e->getMessage() . "</li>";
}
}
echo "</ul>";
} catch (Exception $e) {
echo "<p>❌ Error de conexión: " . $e->getMessage() . "</p>";
}
// Probar servicios
echo "<h2>🔧 Verificación de Servicios:</h2>";
try {
$whatsappService = new WhatsAppService();
echo "<p>✅ WhatsAppService inicializado</p>";
$botService = new BotService();
echo "<p>✅ BotService inicializado</p>";
} catch (Exception $e) {
echo "<p>❌ Error en servicios: " . $e->getMessage() . "</p>";
}
// Probar APIs
echo "<h2>🌐 Verificación de APIs:</h2>";
$apis = [
'api/get_stats.php',
'api/get_users.php',
'api/get_menus.php',
'api/get_conversations.php',
'api/webhook.php'
];
echo "<ul>";
foreach ($apis as $api) {
$exists = file_exists($api);
$readable = $exists ? is_readable($api) : false;
$status = $readable ? "✅ Disponible" : "❌ No disponible";
echo "<li><strong>{$api}</strong>: {$status}</li>";
}
echo "</ul>";
// Probar funcionalidad básica del bot
echo "<h2>🤖 Test Básico del Bot:</h2>";
try {
// Crear usuario de prueba
$testPhone = '573000000000';
// Verificar si el usuario existe
$existingUser = $db->fetch(
"SELECT * FROM users WHERE phone_number = :phone",
['phone' => $testPhone]
);
if (!$existingUser) {
$userId = $db->insert('users', [
'phone_number' => $testPhone,
'name' => 'Usuario de Prueba',
'status' => 'active',
'created_at' => date('Y-m-d H:i:s')
]);
echo "<p>✅ Usuario de prueba creado (ID: {$userId})</p>";
} else {
echo "<p>✅ Usuario de prueba ya existe</p>";
}
// Probar menú principal
$mainMenu = $db->fetch(
"SELECT * FROM menus WHERE is_root = 1 AND is_active = 1 LIMIT 1"
);
if ($mainMenu) {
echo "<p>✅ Menú principal configurado: " . $mainMenu['title'] . "</p>";
// Probar opciones del menú
$options = $db->fetchAll(
"SELECT * FROM menu_options WHERE menu_id = :id ORDER BY option_number",
['id' => $mainMenu['id']]
);
echo "<p>✅ Opciones del menú principal: " . count($options) . " opciones</p>";
} else {
echo "<p>❌ No hay menú principal configurado</p>";
}
} catch (Exception $e) {
echo "<p>❌ Error en test del bot: " . $e->getMessage() . "</p>";
}
// URLs de prueba
echo "<h2>🔗 URLs del Sistema:</h2>";
echo "<ul>";
echo "<li><strong>Panel Principal:</strong> <a href='index.php'>index.php</a></li>";
echo "<li><strong>🧪 SUITE DE TESTS COMPLETA:</strong> <a href='run_tests.php' class='btn btn-primary btn-sm'>run_tests.php</a></li>";
echo "<li><strong>Webhook:</strong> <a href='api/webhook.php?hub_mode=subscribe&hub_verify_token=mi_token_secreto_123&hub_challenge=test'>webhook.php (test)</a></li>";
echo "<li><strong>API Stats:</strong> <a href='api/get_stats.php'>get_stats.php</a></li>";
echo "<li><strong>API Users:</strong> <a href='api/get_users.php'>get_users.php</a></li>";
echo "</ul>";
echo "<div class='alert alert-info mt-4'>";
echo "<h5><i class='fas fa-rocket'></i> Nueva Suite de Tests Implementada</h5>";
echo "<p>Se ha implementado una <strong>suite completa de tests</strong> que cubre:</p>";
echo "<ul>";
echo "<li>✅ <strong>50+ Tests</strong> exhaustivos</li>";
echo "<li>🔌 <strong>17 APIs</strong> validadas</li>";
echo "<li>🔒 <strong>Seguridad</strong> completa</li>";
echo "<li>📝 <strong>Sistema de plantillas</strong></li>";
echo "<li>🔗 <strong>Webhook y servicios</strong></li>";
echo "<li>💾 <strong>Base de datos</strong></li>";
echo "</ul>";
echo "<p class='mb-0'><a href='run_tests.php' class='btn btn-success'><i class='fas fa-play'></i> Ejecutar Tests Completos</a></p>";
echo "</div>";
echo "<h2>📋 Resumen:</h2>";
echo "<p><strong>El sistema WhatsApp Bot está:</strong></p>";
// Calcular estado general
$allGood = file_exists('config/config.php') &&
file_exists('classes/Database.php') &&
file_exists('api/webhook.php') &&
class_exists('Database');
if ($allGood) {
echo "<div style='background: #d4edda; color: #155724; padding: 15px; border-radius: 5px; border-left: 5px solid #28a745;'>";
echo "<h3>✅ SISTEMA FUNCIONANDO</h3>";
echo "<p>Todos los componentes principales están funcionando correctamente.</p>";
echo "<p><strong>Próximos pasos:</strong></p>";
echo "<ul>";
echo "<li>1. Configura el webhook en Facebook Developers</li>";
echo "<li>2. Ajusta la configuración en el panel</li>";
echo "<li>3. Personaliza los menús según tus necesidades</li>";
echo "<li>4. ¡Comienza a recibir mensajes!</li>";
echo "</ul>";
echo "</div>";
} else {
echo "<div style='background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; border-left: 5px solid #dc3545;'>";
echo "<h3>❌ HAY PROBLEMAS</h3>";
echo "<p>Algunos componentes no están funcionando correctamente. Revisa los errores arriba.</p>";
echo "</div>";
}
echo "<hr>";
echo "<p><small>Test ejecutado el: " . date('Y-m-d H:i:s') . "</small></p>";
?>
-175
View File
@@ -1,175 +0,0 @@
<?php
/**
* 🧪 TEST API - WhatsApp Bot
* Prueba específica de endpoints de la API
*/
// Headers para mostrar errores
error_reporting(E_ALL);
ini_set('display_errors', 1);
echo "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
echo "<title>🧪 Test API</title>";
echo "<style>body{font-family:monospace;margin:20px;background:#000;color:#00ff00}";
echo ".ok{color:#00ff00}.error{color:#ff0040}.warning{color:#ffaa00}";
echo ".section{background:#111;padding:15px;margin:10px 0;border:1px solid #333}";
echo "pre{background:#222;padding:10px;color:#ccc;overflow:auto;max-height:200px}";
echo ".endpoint{background:#333;padding:10px;margin:5px 0;border-left:3px solid #ffaa00}";
echo "</style></head><body>";
echo "<h1>🧪 TEST DE LA API</h1>";
// Lista de endpoints para probar
$endpoints = [
'get_settings.php' => 'Obtener configuración del sistema',
'get_stats.php' => 'Obtener estadísticas',
'get_users.php' => 'Obtener usuarios',
'get_menus.php' => 'Obtener menús',
'get_templates.php' => 'Obtener plantillas'
];
echo "<div class='section'>";
echo "<h2>🚀 PROBANDO ENDPOINTS DE LA API...</h2>";
foreach ($endpoints as $endpoint => $description) {
echo "<div class='endpoint'>";
echo "<strong>🔗 $endpoint</strong> - $description<br>";
$api_url = 'api/' . $endpoint;
// Verificar que el archivo existe
if (!file_exists($api_url)) {
echo "<div class='error'>❌ Archivo no encontrado: $api_url</div>";
continue;
}
echo "<div class='ok'>✅ Archivo existe</div>";
// Intentar ejecutar el endpoint usando output buffering
ob_start();
try {
// Simular REQUEST_METHOD para endpoints que lo requieran
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? 'localhost';
// Capturar cualquier error
$error_before = error_get_last();
// Incluir el archivo de la API
include $api_url;
// Obtener la salida
$output = ob_get_contents();
// Verificar errores
$error_after = error_get_last();
if ($error_after && $error_after !== $error_before) {
echo "<div class='error'>❌ Error PHP: " . htmlspecialchars($error_after['message']) . "</div>";
echo "<div class='error'>Línea: " . $error_after['line'] . " en " . basename($error_after['file']) . "</div>";
} else {
// Verificar si la salida parece ser JSON válido
if (!empty($output)) {
$json_data = json_decode($output, true);
if (json_last_error() === JSON_ERROR_NONE) {
echo "<div class='ok'>✅ Respuesta JSON válida (" . strlen($output) . " caracteres)</div>";
// Mostrar estructura básica
if (is_array($json_data)) {
$keys = array_keys($json_data);
$preview_keys = array_slice($keys, 0, 3);
echo "<div class='warning'>📋 Claves: " . implode(', ', $preview_keys);
if (count($keys) > 3) echo " (+" . (count($keys) - 3) . " más)";
echo "</div>";
}
} else {
echo "<div class='warning'>⚠️ Salida no es JSON válido</div>";
if (strlen($output) < 200) {
echo "<div class='warning'>Salida: " . htmlspecialchars(substr($output, 0, 100)) . "</div>";
}
}
} else {
echo "<div class='warning'>⚠️ Sin salida</div>";
}
}
} catch (ParseError $e) {
echo "<div class='error'>❌ Error de sintaxis: " . htmlspecialchars($e->getMessage()) . "</div>";
echo "<div class='error'>Línea: " . $e->getLine() . "</div>";
} catch (Error $e) {
echo "<div class='error'>❌ Error fatal: " . htmlspecialchars($e->getMessage()) . "</div>";
echo "<div class='error'>Archivo: " . basename($e->getFile()) . " línea " . $e->getLine() . "</div>";
} catch (Exception $e) {
echo "<div class='error'>❌ Excepción: " . htmlspecialchars($e->getMessage()) . "</div>";
} finally {
ob_end_clean();
}
echo "</div>";
}
echo "</div>";
// Test directo de carga de configuración desde API
echo "<div class='section'>";
echo "<h2>⚙️ TEST DIRECTO DE CONFIGURACIÓN...</h2>";
echo "<div class='warning'>🔄 Simulando carga desde directorio API...</div>";
// Cambiar al directorio API temporalmente
$original_dir = getcwd();
try {
chdir('api');
echo "<div class='ok'>✅ Cambiado a directorio API</div>";
// Intentar cargar la configuración
if (file_exists('../config/config.php')) {
echo "<div class='ok'>✅ ../config/config.php existe desde API</div>";
try {
require_once '../config/config.php';
echo "<div class='ok'>✅ Configuración cargada exitosamente</div>";
// Verificar algunas constantes
$test_constants = ['DB_HOST', 'DB_NAME', 'DB_USER', 'SECRET_KEY'];
foreach ($test_constants as $const) {
if (defined($const)) {
echo "<div class='ok'>✅ $const definida</div>";
} else {
echo "<div class='error'>❌ $const no definida</div>";
}
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error cargando config: " . htmlspecialchars($e->getMessage()) . "</div>";
}
} else {
echo "<div class='error'>❌ ../config/config.php NO existe desde API</div>";
}
} finally {
chdir($original_dir);
}
echo "</div>";
// Resumen final
echo "<div class='section'>";
echo "<h2>📊 RESUMEN</h2>";
echo "<div class='ok'>✅ Rutas de API corregidas</div>";
echo "<div class='ok'>✅ Todos los archivos usan '../config/config.php'</div>";
echo "<br><div class='warning'><strong>PRÓXIMOS PASOS:</strong></div>";
echo "<div class='warning'>1. Verificar que las tablas existan: <a href='test_config.php' style='color:#ffaa00'>test_config.php</a></div>";
echo "<div class='warning'>2. Si faltan tablas, ejecutar: <a href='instalar_bd.php' style='color:#ffaa00'>instalar_bd.php</a></div>";
echo "<div class='warning'>3. Probar login: <a href='login.php' style='color:#ffaa00'>login.php</a></div>";
echo "<div class='warning'>4. Probar panel: <a href='index.php' style='color:#ffaa00'>index.php</a></div>";
echo "</div>";
echo "</body></html>";
?>
-7
View File
@@ -1,7 +0,0 @@
<?php
session_start();
$_SESSION['admin_logged_in'] = true;
$_SESSION['admin_username'] = 'test_admin';
require_once 'api/manage_config.php';
?>
-57
View File
@@ -1,57 +0,0 @@
<?php
/**
* TEST BÁSICO - Verificar que PHP funciona
* Si ves este mensaje, PHP está funcionando correctamente
*/
echo "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
echo "<title>✅ Test Básico - WhatsApp Bot</title>";
echo "<style>body{font-family:Arial,sans-serif;margin:40px;background:#f0f8ff}";
echo ".success{color:#28a745;background:#d4edda;padding:20px;border-radius:10px;text-align:center}";
echo ".info{color:#0c5460;background:#d1ecf1;padding:15px;border-radius:5px;margin:20px 0}";
echo "h1{color:#333}</style></head><body>";
echo "<div class='success'>";
echo "<h1>✅ ¡PHP FUNCIONANDO CORRECTAMENTE!</h1>";
echo "<p><strong>Versión PHP:</strong> " . phpversion() . "</p>";
echo "<p><strong>Fecha:</strong> " . date('Y-m-d H:i:s') . "</p>";
echo "<p><strong>Servidor:</strong> " . ($_SERVER['SERVER_SOFTWARE'] ?? 'No disponible') . "</p>";
echo "</div>";
echo "<div class='info'>";
echo "<h2>📋 Próximos pasos:</h2>";
echo "<ol>";
echo "<li><strong>Subir archivo diagnóstico:</strong> <a href='diagnostico_500.php'>diagnostico_500.php</a></li>";
echo "<li><strong>Verificar configuración:</strong> Revisar config/config.php</li>";
echo "<li><strong>Probar login:</strong> <a href='login.php'>login.php</a></li>";
echo "<li><strong>Dashboard principal:</strong> <a href='index.php'>index.php</a></li>";
echo "</ol>";
echo "</div>";
echo "<div class='info'>";
echo "<h2>🔧 Si tienes Error 500:</h2>";
echo "<p>1. <strong>Ejecuta diagnóstico:</strong> Ve a diagnostico_500.php</p>";
echo "<p>2. <strong>Desactiva .htaccess:</strong> Renómbralo a .htaccess.bak</p>";
echo "<p>3. <strong>Verifica permisos:</strong> chmod 777 logs/ uploads/</p>";
echo "<p>4. <strong>Revisa logs:</strong> En tu panel de hosting</p>";
echo "</div>";
// Test de escritura
$test_file = 'logs/test_write.txt';
if (is_dir('logs')) {
if (is_writable('logs')) {
if (file_put_contents($test_file, "Test: " . date('Y-m-d H:i:s'))) {
echo "<div class='success'>✅ Directorio logs/ es escribible</div>";
unlink($test_file); // Limpiar archivo de prueba
} else {
echo "<div class='info' style='color:#dc3545;background:#f8d7da'>❌ No se puede escribir en logs/</div>";
}
} else {
echo "<div class='info' style='color:#dc3545;background:#f8d7da'>❌ Directorio logs/ no tiene permisos de escritura</div>";
}
} else {
echo "<div class='info' style='color:#856404;background:#fff3cd'>⚠️ Directorio logs/ no existe</div>";
}
echo "</body></html>";
?>
-214
View File
@@ -1,214 +0,0 @@
<?php
/**
* 🔧 TEST CONFIGURACIÓN - WhatsApp Bot
* Prueba SOLO la configuración sin cargar todo el sistema
*/
// Headers para mostrar errores
error_reporting(E_ALL);
ini_set('display_errors', 1);
echo "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
echo "<title>🔧 Test Configuración</title>";
echo "<style>body{font-family:monospace;margin:20px;background:#000;color:#00ff00}";
echo ".ok{color:#00ff00}.error{color:#ff0040}.warning{color:#ffaa00}";
echo ".section{background:#111;padding:15px;margin:10px 0;border:1px solid #333}";
echo "</style></head><body>";
echo "<h1>🔧 TEST CONFIGURACIÓN</h1>";
// 1. Verificar que el archivo existe
if (!file_exists('config/config.php')) {
echo "<div class='error'>❌ CRÍTICO: config/config.php no encontrado</div>";
echo "<div class='warning'>Asegúrate de que el archivo esté en: config/config.php</div>";
exit;
}
echo "<div class='ok'>✅ config/config.php encontrado</div>";
// 2. Verificar permisos del archivo
if (!is_readable('config/config.php')) {
echo "<div class='error'>❌ CRÍTICO: config/config.php no es legible</div>";
echo "<div class='warning'>Ejecuta: chmod 644 config/config.php</div>";
exit;
}
echo "<div class='ok'>✅ config/config.php es legible</div>";
// 3. Intentar cargar el archivo
echo "<div class='section'>";
echo "<h2>📋 CARGANDO CONFIGURACIÓN...</h2>";
try {
// Capturar cualquier output
ob_start();
// Capturar errores
$error_before = error_get_last();
// Incluir el archivo
require_once 'config/config.php';
// Obtener output
$output = ob_get_clean();
// Verificar si hubo errores
$error_after = error_get_last();
if ($error_after && $error_after !== $error_before) {
echo "<div class='error'>❌ ERROR al cargar config: " . htmlspecialchars($error_after['message']) . "</div>";
echo "<div class='warning'>Línea: " . $error_after['line'] . " en " . $error_after['file'] . "</div>";
} elseif (!empty($output)) {
echo "<div class='warning'>⚠️ ADVERTENCIA: config.php produce output:</div>";
echo "<div class='warning'>" . htmlspecialchars($output) . "</div>";
echo "<div class='warning'>Los archivos de configuración NO deben producir output</div>";
} else {
echo "<div class='ok'>✅ config/config.php cargado sin errores</div>";
}
} catch (ParseError $e) {
echo "<div class='error'>❌ ERROR DE SINTAXIS en config.php:</div>";
echo "<div class='error'>" . htmlspecialchars($e->getMessage()) . "</div>";
echo "<div class='error'>Línea: " . $e->getLine() . "</div>";
exit;
} catch (Error $e) {
echo "<div class='error'>❌ ERROR FATAL en config.php:</div>";
echo "<div class='error'>" . htmlspecialchars($e->getMessage()) . "</div>";
exit;
} catch (Exception $e) {
echo "<div class='error'>❌ EXCEPCIÓN en config.php:</div>";
echo "<div class='error'>" . htmlspecialchars($e->getMessage()) . "</div>";
exit;
}
echo "</div>";
// 4. Verificar constantes requeridas
echo "<div class='section'>";
echo "<h2>🗄️ VERIFICANDO CONSTANTES DE BD...</h2>";
$required_constants = [
'DB_HOST' => 'Servidor de base de datos',
'DB_NAME' => 'Nombre de la base de datos',
'DB_USER' => 'Usuario de la base de datos',
'DB_PASS' => 'Contraseña de la base de datos',
'DB_PORT' => 'Puerto de la base de datos',
'DB_CHARSET' => 'Charset de la base de datos'
];
$all_defined = true;
foreach ($required_constants as $const => $desc) {
if (defined($const)) {
$value = constant($const);
$display_value = ($const === 'DB_PASS') ? str_repeat('*', strlen($value)) : $value;
echo "<div class='ok'>✅ $const = '$display_value' ($desc)</div>";
} else {
echo "<div class='error'>❌ $const no definida ($desc)</div>";
$all_defined = false;
}
}
if (!$all_defined) {
echo "<div class='error'>❌ CRÍTICO: Faltan constantes de base de datos</div>";
echo "<div class='warning'>El sistema no puede funcionar sin estas constantes</div>";
}
echo "</div>";
// 5. Test de conexión a BD (solo si todas las constantes están)
if ($all_defined) {
echo "<div class='section'>";
echo "<h2>🔌 TEST CONEXIÓN BASE DE DATOS...</h2>";
try {
$dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
echo "<div class='ok'>DSN: $dsn</div>";
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_TIMEOUT => 5
]);
echo "<div class='ok'>✅ CONEXIÓN EXITOSA a la base de datos</div>";
// Verificar algunas tablas
$tables_to_check = ['conversations', 'messages', 'settings', 'message_templates'];
foreach ($tables_to_check as $table) {
try {
$stmt = $pdo->query("SHOW TABLES LIKE '$table'");
if ($stmt->rowCount() > 0) {
echo "<div class='ok'>✅ Tabla '$table' existe</div>";
} else {
echo "<div class='warning'>⚠️ Tabla '$table' NO existe</div>";
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error verificando tabla '$table': " . htmlspecialchars($e->getMessage()) . "</div>";
}
}
} catch (PDOException $e) {
echo "<div class='error'>❌ ERROR DE CONEXIÓN:</div>";
echo "<div class='error'>" . htmlspecialchars($e->getMessage()) . "</div>";
echo "<div class='warning'>Verifica:</div>";
echo "<div class='warning'>• Host: " . DB_HOST . "</div>";
echo "<div class='warning'>• Base datos: " . DB_NAME . "</div>";
echo "<div class='warning'>• Usuario: " . DB_USER . "</div>";
echo "<div class='warning'>• Puerto: " . DB_PORT . "</div>";
}
echo "</div>";
}
// 6. Verificar otras constantes importantes
echo "<div class='section'>";
echo "<h2>🔑 OTRAS CONSTANTES DEL SISTEMA...</h2>";
$other_constants = [
'ADMIN_USERNAME' => 'Usuario administrador',
'ADMIN_PASSWORD' => 'Contraseña administrador',
'SECRET_KEY' => 'Clave secreta',
'WHATSAPP_TOKEN' => 'Token de WhatsApp',
'WHATSAPP_PHONE_NUMBER_ID' => 'ID del número de teléfono'
];
foreach ($other_constants as $const => $desc) {
if (defined($const)) {
$value = constant($const);
if (in_array($const, ['ADMIN_PASSWORD', 'SECRET_KEY', 'WHATSAPP_TOKEN'])) {
$display_value = str_repeat('*', min(strlen($value), 20));
} else {
$display_value = $value;
}
echo "<div class='ok'>✅ $const = '$display_value' ($desc)</div>";
} else {
echo "<div class='warning'>⚠️ $const no definida ($desc)</div>";
}
}
echo "</div>";
// 7. Resumen final
echo "<div class='section'>";
echo "<h2>📊 RESUMEN</h2>";
if ($all_defined) {
echo "<div class='ok'>✅ Configuración básica completa</div>";
echo "<div class='ok'>✅ Archivo config.php funcional</div>";
echo "<div class='ok'>✅ Sistema puede intentar funcionar</div>";
echo "<br><div class='warning'><strong>SIGUIENTE PASO:</strong></div>";
echo "<div class='warning'>• Probar: <a href='login.php' style='color:#ffaa00'>login.php</a></div>";
echo "<div class='warning'>• Si funciona login, probar: <a href='index.php' style='color:#ffaa00'>index.php</a></div>";
} else {
echo "<div class='error'>❌ Configuración incompleta</div>";
echo "<div class='error'>❌ Sistema NO puede funcionar</div>";
echo "<div class='warning'>Corrige las constantes faltantes en config/config.php</div>";
}
echo "</div>";
echo "</body></html>";
?>
-452
View File
@@ -1,452 +0,0 @@
<?php
/**
* Script de prueba para el sistema de configuraciones
* Permite probar todas las funciones de manejo de configuraciones
*/
require_once 'config/config.php';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prueba Sistema de Configuraciones</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">🔧 Prueba Sistema de Configuraciones</h1>
<?php if (!isInstallationCompleted()): ?>
<div class="alert alert-warning">
<strong>⚠️ Instalación no completada:</strong> Algunas funciones pueden no estar disponibles.
</div>
<?php endif; ?>
<!-- Información del sistema -->
<div class="card mb-4">
<div class="card-header">
<h5>📊 Estado del Sistema</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Instalación completada:</strong> <?= isInstallationCompleted() ? '✅ Sí' : '❌ No' ?></p>
<p><strong>Sistema migrado:</strong> <?= isAuthSystemMigrated() ? '✅ Sí' : '❌ No' ?></p>
<p><strong>BD conectada:</strong>
<?php
try {
$pdo = createDbConnection();
echo '✅ Conectada';
} catch (Exception $e) {
echo '❌ Error: ' . $e->getMessage();
}
?>
</p>
</div>
<div class="col-md-6">
<p><strong>Tabla system_config:</strong>
<?php
try {
$pdo = createDbConnection();
$exists = $pdo->query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0;
if ($exists) {
$count = $pdo->query("SELECT COUNT(*) as total FROM system_config")->fetch()['total'];
echo "✅ Existe ($count registros)";
} else {
echo '❌ No existe';
}
} catch (Exception $e) {
echo '❌ Error: ' . $e->getMessage();
}
?>
</p>
</div>
</div>
</div>
</div>
<!-- Pruebas de configuración -->
<div class="row">
<div class="col-md-6">
<!-- Probar guardar configuración -->
<div class="card mb-3">
<div class="card-header">
<h6>💾 Probar Guardar Configuración</h6>
</div>
<div class="card-body">
<form id="testSaveForm">
<div class="mb-2">
<input type="text" class="form-control" name="key" placeholder="Clave" value="test_key_<?= time() ?>" required>
</div>
<div class="mb-2">
<input type="text" class="form-control" name="value" placeholder="Valor" value="Valor de prueba" required>
</div>
<div class="mb-2">
<input type="text" class="form-control" name="description" placeholder="Descripción (opcional)" value="Configuración de prueba">
</div>
<button type="submit" class="btn btn-primary btn-sm">Guardar</button>
</form>
<div id="saveResult" class="mt-2"></div>
</div>
</div>
<!-- Probar obtener configuración -->
<div class="card mb-3">
<div class="card-header">
<h6>📖 Probar Obtener Configuración</h6>
</div>
<div class="card-body">
<form id="testGetForm">
<div class="mb-2">
<input type="text" class="form-control" name="key" placeholder="Clave a buscar" required>
</div>
<button type="submit" class="btn btn-info btn-sm">Obtener</button>
</form>
<div id="getResult" class="mt-2"></div>
</div>
</div>
</div>
<div class="col-md-6">
<!-- Probar configuración de WhatsApp -->
<div class="card mb-3">
<div class="card-header">
<h6>📱 Configuración WhatsApp</h6>
</div>
<div class="card-body">
<form id="testWhatsAppForm">
<div class="mb-2">
<input type="text" class="form-control" name="token" placeholder="Token de WhatsApp">
</div>
<div class="mb-2">
<input type="text" class="form-control" name="phone_number_id" placeholder="Phone Number ID">
</div>
<div class="mb-2">
<input type="text" class="form-control" name="webhook_verify_token" placeholder="Webhook Token">
</div>
<button type="submit" class="btn btn-success btn-sm">Guardar WhatsApp</button>
</form>
<div id="whatsappResult" class="mt-2"></div>
</div>
</div>
<!-- Acciones del sistema -->
<div class="card">
<div class="card-header">
<h6>🔧 Acciones del Sistema</h6>
</div>
<div class="card-body">
<button onclick="initializeDefaults()" class="btn btn-warning btn-sm mb-2">Inicializar Defaults</button><br>
<button onclick="migrateConfigs()" class="btn btn-secondary btn-sm mb-2">Migrar a BD</button><br>
<button onclick="clearCache()" class="btn btn-danger btn-sm mb-2">Limpiar Cache</button><br>
<button onclick="getAllConfigs()" class="btn btn-primary btn-sm">Ver Todas</button>
<div id="systemResult" class="mt-2"></div>
</div>
</div>
</div>
</div>
<!-- Configuraciones actuales -->
<div class="card mt-4">
<div class="card-header d-flex justify-content-between">
<h5>📋 Configuraciones Actuales</h5>
<button onclick="refreshConfigs()" class="btn btn-outline-primary btn-sm">🔄 Actualizar</button>
</div>
<div class="card-body">
<div id="currentConfigs">
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status"></div>
Cargando configuraciones...
</div>
</div>
</div>
</div>
<!-- Validación de WhatsApp -->
<div class="card mt-4">
<div class="card-header">
<h5>✅ Validación WhatsApp</h5>
</div>
<div class="card-body">
<div id="whatsappValidation">
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status"></div>
Validando configuración...
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Funciones para probar la API
// Guardar configuración individual
document.getElementById('testSaveForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('api/manage_config.php?action=save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
key: formData.get('key'),
value: formData.get('value'),
description: formData.get('description')
})
});
const result = await response.json();
document.getElementById('saveResult').innerHTML = result.success ?
'<div class="alert alert-success alert-sm">✅ ' + result.message + '</div>' :
'<div class="alert alert-danger alert-sm">❌ ' + result.message + '</div>';
if (result.success) {
refreshConfigs();
}
} catch (error) {
document.getElementById('saveResult').innerHTML =
'<div class="alert alert-danger alert-sm">❌ Error: ' + error.message + '</div>';
}
});
// Obtener configuración
document.getElementById('testGetForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(e.target);
const key = formData.get('key');
try {
const response = await fetch(`api/manage_config.php?action=get&key=${encodeURIComponent(key)}`);
const result = await response.json();
if (result.success) {
const data = result.data;
const html = `
<div class="alert alert-${data.exists ? 'success' : 'warning'} alert-sm">
<strong>Clave:</strong> ${data.key}<br>
<strong>Valor:</strong> ${data.value || '(vacío)'}<br>
<strong>Existe:</strong> ${data.exists ? 'Sí' : 'No'}
</div>
`;
document.getElementById('getResult').innerHTML = html;
} else {
document.getElementById('getResult').innerHTML =
'<div class="alert alert-danger alert-sm">❌ Error: ' + result.error + '</div>';
}
} catch (error) {
document.getElementById('getResult').innerHTML =
'<div class="alert alert-danger alert-sm">❌ Error: ' + error.message + '</div>';
}
});
// Guardar configuración de WhatsApp
document.getElementById('testWhatsAppForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('api/manage_config.php?action=save_whatsapp', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp: {
token: formData.get('token'),
phone_number_id: formData.get('phone_number_id'),
webhook_verify_token: formData.get('webhook_verify_token')
}
})
});
const result = await response.json();
let html = result.success ?
'<div class="alert alert-success alert-sm">✅ ' + result.message + '</div>' :
'<div class="alert alert-danger alert-sm">❌ ' + result.message + '</div>';
if (result.validation) {
html += `
<div class="mt-2">
<small>
<strong>Estado:</strong> ${result.validation.valid ? '✅ Válido' : '❌ Inválido'}<br>
<strong>Errores:</strong> ${result.validation.errors.join(', ') || 'Ninguno'}<br>
<strong>Advertencias:</strong> ${result.validation.warnings.join(', ') || 'Ninguna'}
</small>
</div>
`;
}
document.getElementById('whatsappResult').innerHTML = html;
if (result.success) {
refreshConfigs();
refreshWhatsAppValidation();
}
} catch (error) {
document.getElementById('whatsappResult').innerHTML =
'<div class="alert alert-danger alert-sm">❌ Error: ' + error.message + '</div>';
}
});
// Inicializar configuraciones por defecto
async function initializeDefaults() {
try {
const response = await fetch('api/manage_config.php?action=initialize_defaults', {
method: 'POST'
});
const result = await response.json();
document.getElementById('systemResult').innerHTML = result.success ?
'<div class="alert alert-success alert-sm">✅ ' + result.message + '</div>' :
'<div class="alert alert-danger alert-sm">❌ ' + result.message + '</div>';
if (result.success) {
refreshConfigs();
}
} catch (error) {
document.getElementById('systemResult').innerHTML =
'<div class="alert alert-danger alert-sm">❌ Error: ' + error.message + '</div>';
}
}
// Migrar configuraciones
async function migrateConfigs() {
try {
const response = await fetch('api/manage_config.php?action=migrate_to_database', {
method: 'POST'
});
const result = await response.json();
document.getElementById('systemResult').innerHTML = result.success ?
'<div class="alert alert-success alert-sm">✅ ' + result.message + '</div>' :
'<div class="alert alert-danger alert-sm">❌ ' + result.message + '</div>';
if (result.success) {
refreshConfigs();
}
} catch (error) {
document.getElementById('systemResult').innerHTML =
'<div class="alert alert-danger alert-sm">❌ Error: ' + error.message + '</div>';
}
}
// Limpiar cache
async function clearCache() {
try {
const response = await fetch('api/manage_config.php?action=clear_cache', {
method: 'DELETE'
});
const result = await response.json();
document.getElementById('systemResult').innerHTML = result.success ?
'<div class="alert alert-success alert-sm">✅ ' + result.message + '</div>' :
'<div class="alert alert-danger alert-sm">❌ ' + result.message + '</div>';
} catch (error) {
document.getElementById('systemResult').innerHTML =
'<div class="alert alert-danger alert-sm">❌ Error: ' + error.message + '</div>';
}
}
// Obtener todas las configuraciones
async function getAllConfigs() {
refreshConfigs();
}
// Actualizar lista de configuraciones
async function refreshConfigs() {
try {
const response = await fetch('api/manage_config.php?action=all');
const result = await response.json();
if (result.success) {
const configs = result.data;
let html = '';
if (Object.keys(configs).length === 0) {
html = '<div class="text-muted text-center">No hay configuraciones guardadas</div>';
} else {
html = '<div class="table-responsive"><table class="table table-sm table-striped">';
html += '<thead><tr><th>Clave</th><th>Valor</th><th>Descripción</th><th>Actualizado</th></tr></thead><tbody>';
for (const [key, config] of Object.entries(configs)) {
const value = config.value ? (config.value.length > 50 ? config.value.substring(0, 50) + '...' : config.value) : '(vacío)';
html += `
<tr>
<td><code>${key}</code></td>
<td>${value}</td>
<td><small>${config.description || '—'}</small></td>
<td><small>${config.updated_at || config.created_at}</small></td>
</tr>
`;
}
html += '</tbody></table></div>';
}
document.getElementById('currentConfigs').innerHTML = html;
}
} catch (error) {
document.getElementById('currentConfigs').innerHTML =
'<div class="alert alert-danger">Error cargando configuraciones: ' + error.message + '</div>';
}
}
// Actualizar validación de WhatsApp
async function refreshWhatsAppValidation() {
try {
const response = await fetch('api/manage_config.php?action=validate_whatsapp');
const result = await response.json();
if (result.success) {
const validation = result.data;
let html = `
<div class="alert alert-${validation.valid ? 'success' : 'warning'}">
<h6>${validation.valid ? '✅ Configuración válida' : '⚠️ Configuración incompleta'}</h6>
`;
if (validation.errors.length > 0) {
html += '<p><strong>Errores:</strong></p><ul>';
validation.errors.forEach(error => {
html += `<li>${error}</li>`;
});
html += '</ul>';
}
if (validation.warnings.length > 0) {
html += '<p><strong>Advertencias:</strong></p><ul>';
validation.warnings.forEach(warning => {
html += `<li>${warning}</li>`;
});
html += '</ul>';
}
html += '<p><strong>Campos configurados:</strong></p><ul>';
for (const [field, configured] of Object.entries(validation.configured_fields)) {
html += `<li>${field}: ${configured ? '✅' : '❌'}</li>`;
}
html += '</ul></div>';
document.getElementById('whatsappValidation').innerHTML = html;
}
} catch (error) {
document.getElementById('whatsappValidation').innerHTML =
'<div class="alert alert-danger">Error validando WhatsApp: ' + error.message + '</div>';
}
}
// Cargar datos iniciales
refreshConfigs();
refreshWhatsAppValidation();
</script>
</body>
</html>
-100
View File
@@ -1,100 +0,0 @@
<?php
/**
* Test directo de la API get_menus.php con diagnóstico
*/
echo "<!DOCTYPE html><html><head><title>Test Diagnóstico Menús</title>";
echo "<style>body{font-family:monospace;padding:20px;} .success{color:green;} .error{color:red;} .info{color:blue;} pre{background:#f0f0f0;padding:10px;border:1px solid #ccc;}</style></head><body>";
echo "<h1>🔧 Test Diagnóstico Get Menús API</h1>";
try {
echo "<h2>📞 Llamada directa a get_menus.php</h2>";
// Hacer llamada a la API
$url = 'http://localhost:8000/api/get_menus.php';
echo "<p><strong>URL:</strong> $url</p>";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_COOKIEFILE, ''); // Enable cookies
// Agregar cookie de sesión si existe
if (isset($_COOKIE['PHPSESSID'])) {
curl_setopt($ch, CURLOPT_COOKIE, 'PHPSESSID=' . $_COOKIE['PHPSESSID']);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$headers = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
echo "<h3>🔍 Resultado de la llamada</h3>";
echo "<p><strong>HTTP Code:</strong> $httpCode</p>";
echo "<h4>Headers:</h4>";
echo "<pre>" . htmlspecialchars($headers) . "</pre>";
echo "<h4>Body Response:</h4>";
echo "<pre>" . htmlspecialchars($body) . "</pre>";
// Intentar decodificar JSON
if ($body) {
$jsonData = json_decode($body, true);
if ($jsonData !== null) {
echo "<h4>📋 JSON Decodificado:</h4>";
echo "<pre>" . json_encode($jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "</pre>";
if (isset($jsonData['success'])) {
if ($jsonData['success']) {
echo "<div class='success'>✅ API respondió exitosamente</div>";
if (isset($jsonData['data'])) {
echo "<p><strong>Menús encontrados:</strong> " . count($jsonData['data']) . "</p>";
}
} else {
echo "<div class='error'>❌ API reportó error: " . ($jsonData['error'] ?? 'Error desconocido') . "</div>";
if (isset($jsonData['debug'])) {
echo "<p><strong>Debug info:</strong> " . $jsonData['debug'] . "</p>";
}
}
}
} else {
echo "<div class='error'>❌ Respuesta no es JSON válido</div>";
}
}
// Mostrar logs si existen
echo "<h3>📋 Logs del Sistema</h3>";
$logFile = dirname(__DIR__) . '/logs/system.log';
if (file_exists($logFile)) {
$logs = file_get_contents($logFile);
$recentLogs = array_slice(explode("\n", $logs), -20); // Últimas 20 líneas
echo "<h4>Últimas 20 líneas del log:</h4>";
echo "<pre>" . htmlspecialchars(implode("\n", $recentLogs)) . "</pre>";
} else {
echo "<p>No se encontró archivo de log en: $logFile</p>";
}
// Mostrar error log de PHP si existe
$errorLogFile = dirname(__DIR__) . '/logs/error.log';
if (file_exists($errorLogFile)) {
$errorLogs = file_get_contents($errorLogFile);
$recentErrorLogs = array_slice(explode("\n", $errorLogs), -10); // Últimas 10 líneas
echo "<h4>Últimas 10 líneas del error log:</h4>";
echo "<pre>" . htmlspecialchars(implode("\n", $recentErrorLogs)) . "</pre>";
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error en test: " . $e->getMessage() . "</div>";
}
echo "<hr><p><small>Test ejecutado: " . date('Y-m-d H:i:s') . "</small></p>";
echo "</body></html>";
?>
-209
View File
@@ -1,209 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Funciones Globales - WhatsApp Bot</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-5">
<div class="row">
<div class="col-md-10 mx-auto">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-bug"></i> Test de Funciones Globales</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Modales Disponibles:</h6>
<button class="btn btn-primary mb-2 d-block" onclick="testShowCreateTemplateModal()">
<i class="fas fa-plus"></i> Test Modal Plantilla
</button>
<button class="btn btn-success mb-2 d-block" onclick="testShowCreateMenuModal()">
<i class="fas fa-plus"></i> Test Modal Menú
</button>
<button class="btn btn-info mb-2 d-block" onclick="testBootstrap()">
<i class="fas fa-check"></i> Test Bootstrap
</button>
</div>
<div class="col-md-6">
<h6>Debug Console:</h6>
<div id="console" class="bg-dark text-white p-3 rounded" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
<!-- Console output -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal de Plantilla -->
<div class="modal fade" id="createTemplateModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">📝 Test Modal Plantilla</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Este es el modal de plantillas funcionando correctamente.</p>
<form id="template-form">
<div class="mb-3">
<label class="form-label">Nombre de prueba</label>
<input type="text" class="form-control" placeholder="Plantilla de prueba">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<!-- Modal de Menú -->
<div class="modal fade" id="createMenuModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">📋 Test Modal Menú</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Este es el modal de menús funcionando correctamente.</p>
<form id="menu-form">
<div class="mb-3">
<label class="form-label">Nombre del menú</label>
<input type="text" class="form-control" placeholder="Menú de prueba">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const consoleDiv = document.getElementById('console');
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const colors = {
info: '#17a2b8',
success: '#28a745',
error: '#dc3545',
warning: '#ffc107'
};
consoleDiv.innerHTML += `<div style="color: ${colors[type]};">[${timestamp}] ${message}</div>`;
consoleDiv.scrollTop = consoleDiv.scrollHeight;
console.log(message);
}
function testBootstrap() {
log('🧪 Testing Bootstrap...', 'info');
if (typeof bootstrap !== 'undefined') {
log('✅ Bootstrap is available', 'success');
if (bootstrap.Modal) {
log('✅ Bootstrap.Modal is available', 'success');
} else {
log('❌ Bootstrap.Modal not found', 'error');
}
} else {
log('❌ Bootstrap not available', 'error');
}
}
// Definir las funciones globales aquí directamente
window.showCreateTemplateModal = function() {
log('🔵 showCreateTemplateModal called', 'info');
const modalElement = document.getElementById('createTemplateModal');
if (!modalElement) {
log('❌ Modal element not found', 'error');
return;
}
try {
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
log('✅ Using Bootstrap modal', 'success');
const modal = new bootstrap.Modal(modalElement);
modal.show();
log('✅ Modal opened successfully', 'success');
} else {
log('⚠️ Using manual modal', 'warning');
modalElement.style.display = 'block';
modalElement.classList.add('show');
}
} catch (error) {
log('❌ Error: ' + error.message, 'error');
}
};
window.showCreateMenuModal = function() {
log('🔵 showCreateMenuModal called', 'info');
const modalElement = document.getElementById('createMenuModal');
if (!modalElement) {
log('❌ Menu modal element not found', 'error');
return;
}
try {
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
log('✅ Using Bootstrap modal for menu', 'success');
const modal = new bootstrap.Modal(modalElement);
modal.show();
log('✅ Menu modal opened successfully', 'success');
} else {
log('⚠️ Using manual modal for menu', 'warning');
modalElement.style.display = 'block';
modalElement.classList.add('show');
}
} catch (error) {
log('❌ Error opening menu modal: ' + error.message, 'error');
}
};
function testShowCreateTemplateModal() {
log('🔧 Testing showCreateTemplateModal function...', 'info');
if (typeof showCreateTemplateModal === 'function') {
log('✅ Function exists, calling it...', 'success');
showCreateTemplateModal();
} else {
log('❌ Function not found!', 'error');
}
}
function testShowCreateMenuModal() {
log('🔧 Testing showCreateMenuModal function...', 'info');
if (typeof showCreateMenuModal === 'function') {
log('✅ Function exists, calling it...', 'success');
showCreateMenuModal();
} else {
log('❌ Function not found!', 'error');
}
}
document.addEventListener('DOMContentLoaded', function() {
log('📄 Page loaded', 'info');
testBootstrap();
// Test function availability
log('🔍 Checking function availability...', 'info');
log('showCreateTemplateModal: ' + (typeof showCreateTemplateModal), 'info');
log('showCreateMenuModal: ' + (typeof showCreateMenuModal), 'info');
});
</script>
</body>
</html>
-200
View File
@@ -1,200 +0,0 @@
<?php
/**
* Test de conexión y estructura de menús
* Fecha: 12 de enero de 2026
*/
require_once 'config/config.php';
echo "<!DOCTYPE html>";
echo "<html><head><title>Test Menús - WhatsApp Bot</title>";
echo "<style>body{font-family:Arial;padding:20px;} .success{color:green;} .error{color:red;} .info{color:blue;} pre{background:#f5f5f5;padding:10px;}</style>";
echo "</head><body>";
echo "<h1>🔧 Test de Funcionalidad de Menús</h1>";
try {
$db = Database::getInstance();
echo "<div class='success'>✅ Conexión a base de datos: EXITOSA</div>";
// Verificar tabla menus
echo "<h2>📋 Verificando tabla 'menus'</h2>";
try {
$result = $db->fetchAll("SHOW TABLES LIKE 'menus'");
if (count($result) > 0) {
echo "<div class='success'>✅ Tabla 'menus' existe</div>";
// Mostrar estructura
$structure = $db->fetchAll("DESCRIBE menus");
echo "<h3>Estructura de tabla 'menus':</h3>";
echo "<pre>";
foreach ($structure as $column) {
echo "{$column['Field']} - {$column['Type']} - {$column['Null']} - {$column['Default']}\n";
}
echo "</pre>";
} else {
echo "<div class='error'>❌ Tabla 'menus' NO existe</div>";
echo "<div class='info'>💡 Creando tabla 'menus'...</div>";
$createMenusTable = "
CREATE TABLE IF NOT EXISTS menus (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
menu_key VARCHAR(100) NOT NULL,
description TEXT,
welcome_message TEXT,
status ENUM('active', 'inactive') DEFAULT 'active',
options_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_menu_key (menu_key),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$db->execute($createMenusTable);
echo "<div class='success'>✅ Tabla 'menus' creada</div>";
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error verificando tabla 'menus': " . $e->getMessage() . "</div>";
}
// Verificar tabla menu_options
echo "<h2>📋 Verificando tabla 'menu_options'</h2>";
try {
$result = $db->fetchAll("SHOW TABLES LIKE 'menu_options'");
if (count($result) > 0) {
echo "<div class='success'>✅ Tabla 'menu_options' existe</div>";
// Mostrar estructura
$structure = $db->fetchAll("DESCRIBE menu_options");
echo "<h3>Estructura de tabla 'menu_options':</h3>";
echo "<pre>";
foreach ($structure as $column) {
echo "{$column['Field']} - {$column['Type']} - {$column['Null']} - {$column['Default']}\n";
}
echo "</pre>";
} else {
echo "<div class='error'>❌ Tabla 'menu_options' NO existe</div>";
echo "<div class='info'>💡 Creando tabla 'menu_options'...</div>";
$createMenuOptionsTable = "
CREATE TABLE IF NOT EXISTS menu_options (
id INT AUTO_INCREMENT PRIMARY KEY,
menu_id INT NOT NULL,
option_key VARCHAR(10) NOT NULL,
option_text VARCHAR(255) NOT NULL,
option_action ENUM('message', 'submenu', 'template', 'function') DEFAULT 'message',
option_value TEXT,
order_index INT DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (menu_id) REFERENCES menus(id) ON DELETE CASCADE,
INDEX idx_menu_id (menu_id),
INDEX idx_option_key (option_key),
INDEX idx_order (order_index)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$db->execute($createMenuOptionsTable);
echo "<div class='success'>✅ Tabla 'menu_options' creada</div>";
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error verificando tabla 'menu_options': " . $e->getMessage() . "</div>";
}
// Verificar menús existentes
echo "<h2>📱 Menús existentes</h2>";
try {
$menus = $db->fetchAll("SELECT * FROM menus ORDER BY created_at DESC");
if (count($menus) > 0) {
echo "<div class='info'>📋 Se encontraron " . count($menus) . " menús:</div>";
echo "<pre>";
foreach ($menus as $menu) {
echo "ID: {$menu['id']} | Nombre: {$menu['name']} | Clave: {$menu['menu_key']} | Estado: {$menu['status']}\n";
}
echo "</pre>";
} else {
echo "<div class='info'>📋 No hay menús configurados aún</div>";
// Crear menú de ejemplo
echo "<div class='info'>💡 Creando menú de ejemplo...</div>";
$menuData = [
'name' => 'Menú Principal',
'menu_key' => 'main',
'description' => 'Menú principal del bot',
'welcome_message' => '¡Hola! Bienvenido a nuestro servicio. Por favor selecciona una opción:',
'status' => 'active',
'options_count' => 0
];
$menuId = $db->insert('menus', $menuData);
if ($menuId) {
echo "<div class='success'>✅ Menú de ejemplo creado (ID: {$menuId})</div>";
// Crear opciones de ejemplo
$options = [
['menu_id' => $menuId, 'option_key' => '1', 'option_text' => 'Información', 'option_action' => 'message', 'option_value' => 'Aquí tienes información sobre nuestros servicios', 'order_index' => 1],
['menu_id' => $menuId, 'option_key' => '2', 'option_text' => 'Soporte', 'option_action' => 'message', 'option_value' => 'Nuestro equipo de soporte te ayudará en breve', 'order_index' => 2],
['menu_id' => $menuId, 'option_key' => '0', 'option_text' => 'Salir', 'option_action' => 'message', 'option_value' => 'Gracias por contactarnos', 'order_index' => 3]
];
foreach ($options as $option) {
$db->insert('menu_options', $option);
}
// Actualizar contador de opciones
$db->execute("UPDATE menus SET options_count = 3 WHERE id = :id", ['id' => $menuId]);
echo "<div class='success'>✅ Opciones de menú creadas</div>";
}
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error verificando menús: " . $e->getMessage() . "</div>";
}
// Test de API
echo "<h2>🔌 Test de API</h2>";
try {
$apiUrl = 'http://localhost:8000/api/get_menus.php';
echo "<div class='info'>📞 Probando API: {$apiUrl}</div>";
$context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => 'Content-Type: application/json',
'timeout' => 10
]
]);
$response = @file_get_contents($apiUrl, false, $context);
if ($response !== false) {
$data = json_decode($response, true);
if ($data !== null) {
echo "<div class='success'>✅ API responde correctamente</div>";
echo "<pre>" . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "</pre>";
} else {
echo "<div class='error'>❌ API responde pero JSON inválido</div>";
echo "<pre>" . htmlspecialchars($response) . "</pre>";
}
} else {
echo "<div class='error'>❌ No se pudo conectar a la API</div>";
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error probando API: " . $e->getMessage() . "</div>";
}
} catch (Exception $e) {
echo "<div class='error'>❌ Error de conexión: " . $e->getMessage() . "</div>";
}
echo "<hr>";
echo "<p><strong>Estado del servidor:</strong> El servidor PHP está corriendo en <a href='http://localhost:8000' target='_blank'>http://localhost:8000</a></p>";
echo "<p><strong>Aplicación principal:</strong> <a href='http://localhost:8000/index.php' target='_blank'>WhatsApp Bot Manager</a></p>";
echo "<p><strong>Fecha de prueba:</strong> " . date('Y-m-d H:i:s') . "</p>";
echo "</body></html>";
?>
-153
View File
@@ -1,153 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Menús - WhatsApp Bot Manager</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.success { color: green; }
.error { color: red; }
.info { color: blue; }
button { padding: 10px 15px; margin: 5px; cursor: pointer; }
pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; }
.result { margin: 10px 0; padding: 10px; border-radius: 5px; }
</style>
</head>
<body>
<h1>🔧 Test de Funcionalidad de Menús</h1>
<p>Este test verificará que la funcionalidad de menús esté trabajando correctamente.</p>
<div id="results"></div>
<h2>🧪 Pruebas de API</h2>
<button onclick="testGetMenus()">📋 Probar get_menus.php</button>
<button onclick="testSaveMenu()">💾 Probar save_menu.php</button>
<button onclick="testDeleteMenu()">🗑️ Probar delete_menu.php</button>
<button onclick="clearResults()">🧽 Limpiar Resultados</button>
<div id="test-results"></div>
<script>
function clearResults() {
document.getElementById('test-results').innerHTML = '';
}
function showResult(title, data, isError = false) {
const div = document.createElement('div');
div.className = `result ${isError ? 'error' : 'success'}`;
div.innerHTML = `
<h3>${title}</h3>
<pre>${typeof data === 'object' ? JSON.stringify(data, null, 2) : data}</pre>
`;
document.getElementById('test-results').appendChild(div);
}
async function testGetMenus() {
try {
const response = await fetch('./api/get_menus.php');
const data = await response.json();
showResult('✅ GET Menús - Respuesta', data);
if (data.success && data.data.length > 0) {
showResult('📊 Total de menús encontrados', `${data.data.length} menús`);
} else if (data.success && data.data.length === 0) {
showResult('️ Sin menús', 'No se encontraron menús configurados');
}
} catch (error) {
showResult('❌ Error GET Menús', error.message, true);
}
}
async function testSaveMenu() {
const testMenu = {
name: 'Menú de Prueba',
menu_key: 'test_' + Date.now(),
description: 'Menú creado para prueba automática',
welcome_message: '¡Hola! Este es un menú de prueba. Selecciona una opción:',
status: 'active',
options: [
{
key: '1',
text: 'Opción 1',
action: 'message',
value: 'Respuesta de la opción 1',
order_index: 1
},
{
key: '2',
text: 'Opción 2',
action: 'message',
value: 'Respuesta de la opción 2',
order_index: 2
}
]
};
try {
const response = await fetch('./api/save_menu.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(testMenu)
});
const data = await response.json();
showResult('✅ SAVE Menú - Respuesta', data);
if (data.success) {
// Actualizar lista después de crear
setTimeout(testGetMenus, 1000);
}
} catch (error) {
showResult('❌ Error SAVE Menú', error.message, true);
}
}
async function testDeleteMenu() {
// Primero obtener menús para encontrar uno que eliminar
try {
const getResponse = await fetch('./api/get_menus.php');
const getData = await getResponse.json();
if (getData.success && getData.data.length > 0) {
// Buscar un menú de prueba para eliminar
const testMenu = getData.data.find(menu => menu.menu_key.startsWith('test_'));
if (testMenu) {
const deleteResponse = await fetch('./api/delete_menu.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
menu_id: testMenu.id
})
});
const deleteData = await deleteResponse.json();
showResult(`🗑️ DELETE Menú (ID: ${testMenu.id})`, deleteData);
if (deleteData.success) {
// Actualizar lista después de eliminar
setTimeout(testGetMenus, 1000);
}
} else {
showResult('️ DELETE Menú', 'No se encontró un menú de prueba para eliminar. Crea uno primero.');
}
} else {
showResult('️ DELETE Menú', 'No hay menús para eliminar');
}
} catch (error) {
showResult('❌ Error DELETE Menú', error.message, true);
}
}
// Ejecutar test inicial
document.addEventListener('DOMContentLoaded', function() {
testGetMenus();
});
</script>
</body>
</html>
-203
View File
@@ -1,203 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test JavaScript Menús</title>
<style>
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto; padding: 20px; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f5f5f5; }
.btn { padding: 5px 10px; margin: 2px; border: none; border-radius: 3px; cursor: pointer; }
.btn-outline-primary { border: 1px solid #0066cc; color: #0066cc; background: white; }
.btn-outline-info { border: 1px solid #17a2b8; color: #17a2b8; background: white; }
.btn-outline-danger { border: 1px solid #dc3545; color: #dc3545; background: white; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.bg-success { background-color: #28a745; color: white; }
.bg-secondary { background-color: #6c757d; color: white; }
.me-1 { margin-right: 5px; }
.text-center { text-align: center; }
.text-muted { color: #6c757d; }
#result { margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 5px; }
</style>
</head>
<body>
<h1>🧪 Test JavaScript - Lista de Menús</h1>
<button onclick="testMenusDisplay()">📋 Cargar y Mostrar Menús</button>
<button onclick="clearTable()">🧽 Limpiar Tabla</button>
<div id="result"></div>
<h2>📋 Tabla de Menús</h2>
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Clave</th>
<th>Descripción</th>
<th>Estado</th>
<th>Opciones</th>
<th>Fecha Creación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="menus-table">
<tr><td colspan="7" class="text-center text-muted">Haz clic en "Cargar y Mostrar Menús" para ver los datos</td></tr>
</tbody>
</table>
<script>
// Simular la funcionalidad de WhatsApp Manager
class MenuTester {
constructor() {
this.apiBaseUrl = './api/';
}
log(message, type = 'info') {
console.log(`[${type.toUpperCase()}] ${message}`);
}
truncateText(text, length) {
if (!text) return '';
return text.length > length ? text.substring(0, length) + '...' : text;
}
async apiCall(endpoint, options = {}) {
const url = `${this.apiBaseUrl}${endpoint}`;
try {
const response = await fetch(url, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
throw error;
}
}
updateMenusList(menus) {
const container = document.getElementById('menus-table');
if (!container) {
console.error('Contenedor menus-table no encontrado');
return;
}
if (menus.length === 0) {
container.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No hay menús configurados</td></tr>';
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 += `
<tr>
<td><strong>${menu.menu_name || menu.name || 'Sin nombre'}</strong></td>
<td><code>${menu.menu_key || menu.name || 'sin-clave'}</code></td>
<td>${this.truncateText(menu.description || '', 50)}</td>
<td><span class="badge bg-${menu.status === 'active' ? 'success' : 'secondary'}">${menu.status || 'active'}</span></td>
<td>${menu.options_count || 0} opciones</td>
<td>${formatDate}</td>
<td>
<button class="btn btn-outline-primary me-1" onclick="editMenu(${menu.id})" title="Editar">
📝
</button>
<button class="btn btn-outline-info me-1" onclick="viewMenuOptions(${menu.id})" title="Ver opciones">
📋
</button>
<button class="btn btn-outline-danger" onclick="deleteMenu(${menu.id})" title="Eliminar">
🗑️
</button>
</td>
</tr>
`;
});
container.innerHTML = html;
console.log(`✅ Tabla actualizada con ${menus.length} menús`);
}
async loadMenus() {
try {
const response = await this.apiCall('get_menus.php');
document.getElementById('result').innerHTML = `
<h3>📡 Respuesta de API:</h3>
<pre>${JSON.stringify(response, null, 2)}</pre>
`;
if (response && response.success) {
this.updateMenusList(response.data || []);
return response.data;
} else if (response && Array.isArray(response)) {
this.updateMenusList(response);
return response;
} else {
throw new Error('Formato de respuesta inválido');
}
} catch (error) {
document.getElementById('result').innerHTML = `
<h3>❌ Error:</h3>
<p style="color: red;">${error.message}</p>
`;
console.error('Error cargando menús:', error);
}
}
}
// Instanciar el tester
const menuTester = new MenuTester();
// Funciones globales
function testMenusDisplay() {
console.log('🧪 Iniciando test de carga de menús...');
menuTester.loadMenus();
}
function clearTable() {
const container = document.getElementById('menus-table');
if (container) {
container.innerHTML = '<tr><td colspan="7" class="text-center text-muted">Tabla limpiada</td></tr>';
}
document.getElementById('result').innerHTML = '';
}
function editMenu(id) {
alert(`Editar menú ID: ${id}`);
}
function viewMenuOptions(id) {
alert(`Ver opciones del menú ID: ${id}`);
}
function deleteMenu(id) {
if (confirm(`¿Eliminar menú ID: ${id}?`)) {
alert(`Eliminando menú ID: ${id}`);
}
}
// Auto-cargar al inicio
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ Página cargada, ready para test');
});
</script>
</body>
</html>
-140
View File
@@ -1,140 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Modal Plantillas - WhatsApp Bot</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="./assets/css/styles.css" rel="stylesheet">
</head>
<body>
<div class="container py-5">
<div class="row">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-test-tube"></i> Prueba de Modal de Plantillas</h5>
</div>
<div class="card-body">
<p>Haz clic en el botón para probar el modal de crear plantilla:</p>
<button class="btn btn-primary" onclick="showCreateTemplateModal()">
<i class="fas fa-plus"></i> Nueva Plantilla
</button>
<button class="btn btn-info ms-2" onclick="testBootstrap()">
<i class="fas fa-check"></i> Test Bootstrap
</button>
<div class="mt-3">
<h6>Log de Debug:</h6>
<div id="debug-log" class="bg-light p-3 rounded" style="height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
<!-- Los mensajes aparecerán aquí -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal de Crear Plantilla -->
<div class="modal fade" id="createTemplateModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">📝 Nueva Plantilla de WhatsApp</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="template-form">
<div class="mb-3">
<label class="form-label">Nombre descriptivo *</label>
<input type="text" class="form-control" id="template-name" required placeholder="ej: Bienvenida Cliente">
</div>
<div class="mb-3">
<label class="form-label">Nombre en WhatsApp *</label>
<input type="text" class="form-control" id="template-whatsapp-name" required placeholder="ej: welcome_customer">
</div>
<div class="alert alert-info">
<strong>⚠️ Importante:</strong> Este es un modal de prueba.
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary">Guardar Plantilla</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const debugLog = document.getElementById('debug-log');
function addLog(message) {
const timestamp = new Date().toLocaleTimeString();
debugLog.innerHTML += `[${timestamp}] ${message}\n`;
debugLog.scrollTop = debugLog.scrollHeight;
console.log(message);
}
function testBootstrap() {
addLog('🧪 Probando Bootstrap...');
if (typeof bootstrap !== 'undefined') {
addLog('✅ Bootstrap está disponible');
addLog('📦 Versión detectada: ' + (bootstrap.Modal ? 'Modal disponible' : 'Modal no disponible'));
} else {
addLog('❌ Bootstrap NO está disponible');
}
}
function showCreateTemplateModal() {
addLog('🔵 Intentando abrir modal de crear plantilla...');
const modalElement = document.getElementById('createTemplateModal');
if (!modalElement) {
addLog('❌ No se encontró el modal createTemplateModal');
alert('Error: No se encontró el modal de crear plantilla');
return;
}
addLog('✅ Modal encontrado');
try {
// Limpiar el formulario
const form = document.getElementById('template-form');
if (form) {
form.reset();
addLog('✅ Formulario limpiado');
}
// Verificar si Bootstrap está disponible
if (typeof bootstrap !== 'undefined') {
addLog('✅ Usando Bootstrap modal');
const modal = new bootstrap.Modal(modalElement);
modal.show();
addLog('✅ Modal abierto con Bootstrap');
} else {
addLog('⚠️ Bootstrap no disponible, usando modal manual');
// Fallback manual
modalElement.style.display = 'block';
modalElement.classList.add('show');
addLog('✅ Modal abierto manualmente');
}
} catch (error) {
addLog('❌ Error al abrir modal: ' + error.message);
alert('Error al abrir el modal: ' + error.message);
}
}
document.addEventListener('DOMContentLoaded', function() {
addLog('📄 Página cargada');
testBootstrap();
});
</script>
</body>
</html>
-85
View File
@@ -1,85 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test de Navegación - WhatsApp Bot</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.tab-content { display: none; padding: 20px; }
.tab-content.active { display: block; }
.nav-link.active { background-color: #007bff; color: white; }
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-3 bg-dark text-white" style="height: 100vh;">
<h4 class="p-3">Menú de Prueba</h4>
<ul class="list-unstyled">
<li><a href="#dashboard" class="nav-link active text-white d-block p-2" data-tab="dashboard"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
<li><a href="#users" class="nav-link text-white d-block p-2" data-tab="users"><i class="fas fa-users"></i> Usuarios</a></li>
<li><a href="#logs" class="nav-link text-white d-block p-2" data-tab="logs"><i class="fas fa-file-alt"></i> Logs</a></li>
<li><a href="#menus" class="nav-link text-white d-block p-2" data-tab="menus"><i class="fas fa-list"></i> Menús</a></li>
</ul>
</div>
<div class="col-md-9">
<div id="dashboard" class="tab-content active">
<h2>Dashboard</h2>
<p>Contenido del dashboard aquí.</p>
<div class="alert alert-info">Esta es la pestaña de Dashboard</div>
</div>
<div id="users" class="tab-content">
<h2>Usuarios</h2>
<p>Lista de usuarios aquí.</p>
<div class="alert alert-success">Esta es la pestaña de Usuarios</div>
</div>
<div id="logs" class="tab-content">
<h2>Logs</h2>
<p>Logs del sistema aquí.</p>
<div class="alert alert-warning">Esta es la pestaña de Logs</div>
</div>
<div id="menus" class="tab-content">
<h2>Menús</h2>
<p>Configuración de menús aquí.</p>
<div class="alert alert-primary">Esta es la pestaña de Menús</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ Navegación de prueba inicializada');
document.querySelectorAll('.nav-link[data-tab]').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const tabName = this.getAttribute('data-tab');
console.log('🔄 Cambiando a:', tabName);
// Remover active de todos los enlaces
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
this.classList.add('active');
// Ocultar todos los tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Mostrar el tab seleccionado
const targetTab = document.getElementById(tabName);
if (targetTab) {
targetTab.classList.add('active');
console.log('✅ Tab activado:', tabName);
} else {
console.error('❌ Tab no encontrado:', tabName);
}
});
});
});
</script>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
<?php
echo password_verify('2fa904eee46f', '$2y$10$/ElTmzox8CpwwPaBLOz8JOKPsg7.ml2kn.85XuR7qRl/F4G5fzIxG') ? 'SI FUNCIONA' : 'NO FUNCIONA';
?>
-113
View File
@@ -1,113 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test SES Fix - WhatsApp Bot</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-5">
<div class="row">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header bg-success text-white">
<h5><i class="fas fa-check-circle"></i> Test SES Fix</h5>
</div>
<div class="card-body">
<p>Esta página prueba que las funciones globales funcionen sin errores de SES.</p>
<div class="row">
<div class="col-md-6">
<h6>Funciones disponibles:</h6>
<button class="btn btn-primary d-block mb-2" onclick="testFunction('showCreateTemplateModal')">
<i class="fas fa-file-text"></i> Test showCreateTemplateModal
</button>
<button class="btn btn-success d-block mb-2" onclick="testFunction('showCreateMenuModal')">
<i class="fas fa-list"></i> Test showCreateMenuModal
</button>
<button class="btn btn-info d-block mb-2" onclick="clearLog()">
<i class="fas fa-eraser"></i> Limpiar Log
</button>
</div>
<div class="col-md-6">
<h6>Status Log:</h6>
<div id="status-log" class="bg-light border p-3 rounded" style="height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
<div style="color: green;">✅ Página cargada sin errores de SES</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const logDiv = document.getElementById('status-log');
function addLog(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const colors = {
info: '#17a2b8',
success: '#28a745',
error: '#dc3545',
warning: '#ffc107'
};
logDiv.innerHTML += `<div style="color: ${colors[type]};">[${timestamp}] ${message}</div>`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function clearLog() {
logDiv.innerHTML = '<div style="color: green;">✅ Log limpiado</div>';
}
// Definir funciones de prueba localmente
window.showCreateTemplateModal = function() {
addLog('✅ showCreateTemplateModal llamada exitosamente', 'success');
alert('Modal de plantilla funcionando correctamente');
};
window.showCreateMenuModal = function() {
addLog('✅ showCreateMenuModal llamada exitosamente', 'success');
alert('Modal de menú funcionando correctamente');
};
function testFunction(functionName) {
addLog(`🧪 Probando función: ${functionName}`, 'info');
if (typeof window[functionName] === 'function') {
try {
window[functionName]();
addLog(`✅ Función ${functionName} ejecutada sin errores`, 'success');
} catch (error) {
addLog(`❌ Error ejecutando ${functionName}: ${error.message}`, 'error');
}
} else {
addLog(`❌ Función ${functionName} no encontrada`, 'error');
}
}
document.addEventListener('DOMContentLoaded', function() {
addLog('📄 DOM cargado', 'info');
// Verificar disponibilidad de funciones
addLog('🔍 Verificando funciones globales...', 'info');
const functions = ['showCreateTemplateModal', 'showCreateMenuModal'];
functions.forEach(func => {
if (typeof window[func] === 'function') {
addLog(`${func} disponible`, 'success');
} else {
addLog(`${func} no disponible`, 'error');
}
});
});
</script>
</body>
</html>
-104
View File
@@ -1,104 +0,0 @@
<?php
/**
* Prueba simple del sistema de configuraciones
*/
require_once 'config/config.php';
header('Content-Type: text/plain; charset=utf-8');
try {
// Test básico de conexión a BD
echo "=== PRUEBA SISTEMA DE CONFIGURACIONES ===\n\n";
echo "1. Probando conexión a base de datos...\n";
$pdo = createDbConnection();
echo "✅ Conexión exitosa\n\n";
echo "2. Verificando tabla system_config...\n";
$tableExists = $pdo->query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0;
if (!$tableExists) {
echo "⚠️ Tabla no existe, creándola...\n";
$created = createSystemConfigTable($pdo);
if ($created) {
echo "✅ Tabla creada exitosamente\n\n";
} else {
echo "❌ Error creando tabla\n\n";
exit;
}
} else {
echo "✅ Tabla existe\n\n";
}
echo "3. Probando guardar configuración...\n";
$testKey = 'test_config_' . time();
$testValue = 'Valor de prueba ' . date('Y-m-d H:i:s');
$saved = saveConfigToDB($testKey, $testValue, 'Configuración de prueba');
if ($saved) {
echo "✅ Configuración guardada: $testKey = $testValue\n\n";
} else {
echo "❌ Error guardando configuración\n\n";
}
echo "4. Probando obtener configuración...\n";
$retrieved = getConfigFromDB($testKey, 'no_encontrado');
if ($retrieved === $testValue) {
echo "✅ Configuración recuperada correctamente: $retrieved\n\n";
} else {
echo "❌ Error recuperando configuración. Esperado: $testValue, Obtenido: $retrieved\n\n";
}
echo "5. Probando obtener todas las configuraciones...\n";
$allConfigs = getAllConfigsFromDB();
echo "✅ Total de configuraciones: " . count($allConfigs) . "\n\n";
echo "6. Probando configuración de WhatsApp...\n";
$whatsappTest = [
'token' => 'token_de_prueba_' . time(),
'phone_number_id' => '123456789',
'webhook_verify_token' => 'webhook_test_' . time()
];
$whatsappSaved = saveWhatsAppConfigToDB($whatsappTest);
if ($whatsappSaved) {
echo "✅ Configuración de WhatsApp guardada\n";
$whatsappConfig = getWhatsAppConfigFromDB();
echo "📱 Token: " . $whatsappConfig['token'] . "\n";
echo "📱 Phone ID: " . $whatsappConfig['phone_number_id'] . "\n";
echo "📱 Webhook: " . $whatsappConfig['webhook_verify_token'] . "\n\n";
} else {
echo "❌ Error guardando configuración de WhatsApp\n\n";
}
echo "7. Probando validación de WhatsApp...\n";
$validation = validateWhatsAppConfigFromDB();
echo "✅ Configuración válida: " . ($validation['valid'] ? 'Sí' : 'No') . "\n";
echo " Errores: " . count($validation['errors']) . "\n";
echo " Advertencias: " . count($validation['warnings']) . "\n\n";
echo "8. Limpiando configuración de prueba...\n";
$deleted = deleteConfigFromDB($testKey);
if ($deleted) {
echo "✅ Configuración de prueba eliminada\n\n";
} else {
echo "❌ Error eliminando configuración de prueba\n\n";
}
echo "=== PRUEBAS COMPLETADAS EXITOSAMENTE ===\n";
echo "El sistema de configuraciones funciona correctamente.\n";
echo "Puedes usar las funciones:\n";
echo "- saveConfigToDB(clave, valor, descripción)\n";
echo "- getConfigFromDB(clave, por_defecto)\n";
echo "- getAllConfigsFromDB()\n";
echo "- saveWhatsAppConfigToDB(array_config)\n";
echo "- getWhatsAppConfigFromDB()\n";
echo "- validateWhatsAppConfigFromDB()\n";
} catch (Exception $e) {
echo "❌ ERROR: " . $e->getMessage() . "\n";
echo "Trace: " . $e->getTraceAsString() . "\n";
}
?>
-56
View File
@@ -1,56 +0,0 @@
<?php
/**
* Test directo de plantilla - Debug detallado
*/
require_once 'config/config.php';
require_once 'services/WhatsAppService.php';
header('Content-Type: application/json; charset=utf-8');
// Configurar logging de errores para ver los detalles
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
try {
echo json_encode(['status' => 'Iniciando test de plantilla...'], JSON_PRETTY_PRINT) . "\n";
$whatsapp = new WhatsAppService();
// Usar datos de prueba que sabemos que existen
$recipient = "573168950803"; // Número de prueba
$templateName = "hello_world"; // Esta plantilla FUNCIONA
$language = "en_US"; // Idioma que FUNCIONA
echo json_encode([
'step' => 'Datos de envío',
'recipient' => $recipient,
'template' => $templateName,
'language' => $language
], JSON_PRETTY_PRINT) . "\n";
// Enviar plantilla
echo json_encode(['step' => 'Llamando sendTemplateMessage...'], JSON_PRETTY_PRINT) . "\n";
flush();
$result = $whatsapp->sendTemplateMessage($recipient, $templateName, $language);
echo json_encode([
'step' => 'Resultado',
'success' => true,
'result' => $result
], JSON_PRETTY_PRINT) . "\n";
} catch (Exception $e) {
echo json_encode([
'step' => 'Error capturado',
'success' => false,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
], JSON_PRETTY_PRINT) . "\n";
}
echo json_encode(['status' => 'Test completado. Revisa los logs de error para ver detalles de la petición HTTP.'], JSON_PRETTY_PRINT) . "\n";
?>
-92
View File
@@ -1,92 +0,0 @@
<?php
/**
* Test directo para mensajes de texto
*/
require_once 'config/config.php';
// Simular datos de entrada para mensaje de texto
$testData = [
'recipient' => '+1234567890',
'type' => 'text',
'message' => 'Mensaje de prueba desde test directo'
];
echo "=== TEST MENSAJE DE TEXTO ===\n";
echo "Datos de entrada: " . json_encode($testData) . "\n\n";
try {
// Test 1: Verificar conexión a la base de datos
$db = Database::getInstance();
echo "✅ Conexión a base de datos: OK\n";
// Test 2: Verificar si existe la tabla users
$stmt = $db->query("SHOW TABLES LIKE 'users'");
$usersTable = $stmt->fetch();
echo "✅ Tabla users existe: " . ($usersTable ? 'OK' : 'NO') . "\n";
// Test 3: Verificar si existe la tabla conversations
$stmt = $db->query("SHOW TABLES LIKE 'conversations'");
$conversationsTable = $stmt->fetch();
echo "✅ Tabla conversations existe: " . ($conversationsTable ? 'OK' : 'NO') . "\n";
// Test 4: Simular búsqueda de usuario
$recipient = $testData['recipient'];
$stmt = $db->query("SELECT id FROM users WHERE phone_number = ? LIMIT 1", [$recipient]);
$user = $stmt->fetch();
echo "📱 Usuario existente con teléfono $recipient: " . ($user ? "ID=" . $user['id'] : 'NO EXISTE') . "\n";
// Test 5: Simular creación de usuario si no existe
if (!$user) {
echo "🔄 Intentando crear nuevo usuario...\n";
$stmt = $db->query(
"INSERT INTO users (phone_number, name, created_at) VALUES (?, ?, NOW())",
[$recipient, $recipient]
);
$userId = $db->lastInsertId();
echo "✅ Usuario creado con ID: $userId\n";
} else {
$userId = $user['id'];
echo "✅ Usuario existente con ID: $userId\n";
}
// Test 6: Verificar ventana de 24 horas
if ($userId) {
$stmt = $db->query(
"SELECT MAX(created_at) as last_message FROM conversations
WHERE user_id = ? AND direction = 'incoming'
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
[$userId]
);
$lastMessage = $stmt->fetch();
echo "⏰ Último mensaje entrante en 24h: " . ($lastMessage['last_message'] ?? 'NINGUNO') . "\n";
if (!$lastMessage || !$lastMessage['last_message']) {
echo "⚠️ Usuario fuera de ventana de 24h - Solo plantillas permitidas\n";
} else {
echo "✅ Usuario dentro de ventana de 24h - Mensajes de texto permitidos\n";
}
}
// Test 7: Simular guardado de mensaje
if ($userId) {
echo "💾 Intentando guardar mensaje en conversations...\n";
$messageContent = $testData['message'];
$messageType = $testData['type'];
$stmt = $db->query(
"INSERT INTO conversations (user_id, content, direction, message_type, status, message_id, created_at) VALUES (?, ?, 'outgoing', ?, 'sent', ?, NOW())",
[$userId, $messageContent, $messageType, 'test_message_' . time()]
);
echo "✅ Mensaje guardado correctamente en conversations\n";
}
echo "\n=== RESULTADO ===\n";
echo "✅ Todas las operaciones de base de datos funcionan correctamente\n";
echo "✅ El problema debería estar resuelto\n";
} catch (Exception $e) {
echo "❌ ERROR: " . $e->getMessage() . "\n";
echo "Stack trace: " . $e->getTraceAsString() . "\n";
}
?>
-366
View File
@@ -1,366 +0,0 @@
<?php
/**
* Test de Webhook - Diagnóstico completo
* Fecha: 13 de enero de 2026
*/
require_once 'config/config.php';
// Verificar autenticación
if (!isUserLoggedIn()) {
header('Location: login.php');
exit;
}
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diagnóstico de Webhook - WhatsApp Bot</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body { background: #f8f9fa; padding: 20px; }
.test-card { background: white; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.ok { color: #28a745; font-weight: bold; }
.error { color: #dc3545; font-weight: bold; }
.warning { color: #ffc107; font-weight: bold; }
.info { color: #17a2b8; font-weight: bold; }
.code-box { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; padding: 15px; margin: 10px 0; font-family: 'Courier New', monospace; font-size: 12px; }
.badge-status { font-size: 14px; padding: 8px 15px; }
</style>
</head>
<body>
<div class="container">
<div class="test-card">
<h1 class="mb-4">
<i class="fas fa-plug"></i> Diagnóstico de Webhook WhatsApp
</h1>
<p class="text-muted">Verificación completa del estado del webhook y configuración</p>
</div>
<?php
$db = Database::getInstance();
$allOk = true;
// Test 1: Verificar configuración
echo "<div class='test-card'>";
echo "<h3><i class='fas fa-cog'></i> 1. Configuración del Sistema</h3>";
$config = [
'WHATSAPP_TOKEN' => defined('WHATSAPP_TOKEN') ? WHATSAPP_TOKEN : null,
'WHATSAPP_PHONE_NUMBER_ID' => defined('WHATSAPP_PHONE_NUMBER_ID') ? WHATSAPP_PHONE_NUMBER_ID : null,
'WEBHOOK_VERIFY_TOKEN' => defined('WEBHOOK_VERIFY_TOKEN') ? WEBHOOK_VERIFY_TOKEN : null,
'WHATSAPP_API_URL' => defined('WHATSAPP_API_URL') ? WHATSAPP_API_URL : null,
];
echo "<table class='table table-sm'>";
foreach ($config as $key => $value) {
$status = !empty($value) ? 'ok' : 'error';
$icon = !empty($value) ? '✅' : '❌';
$displayValue = !empty($value) ? (strlen($value) > 30 ? substr($value, 0, 30) . '...' : $value) : 'NO CONFIGURADO';
if ($status === 'error') $allOk = false;
echo "<tr>";
echo "<td class='$status'>$icon $key</td>";
echo "<td><code>$displayValue</code></td>";
echo "</tr>";
}
echo "</table>";
echo "</div>";
// Test 2: URL del Webhook
echo "<div class='test-card'>";
echo "<h3><i class='fas fa-link'></i> 2. URL del Webhook</h3>";
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$webhookUrl = $protocol . '://' . $host . '/api/webhook.php';
echo "<div class='alert alert-info'>";
echo "<strong>URL para configurar en Meta/Facebook:</strong><br>";
echo "<code style='font-size: 14px;'>$webhookUrl</code>";
echo "<button class='btn btn-sm btn-outline-primary ms-2' onclick='navigator.clipboard.writeText(\"$webhookUrl\")'>
<i class='fas fa-copy'></i> Copiar
</button>";
echo "</div>";
// Verificar si el archivo existe
$webhookFile = __DIR__ . '/api/webhook.php';
if (file_exists($webhookFile)) {
echo "<p class='ok'>✅ Archivo webhook.php existe</p>";
} else {
echo "<p class='error'>❌ Archivo webhook.php NO encontrado</p>";
$allOk = false;
}
echo "</div>";
// Test 3: Verificación GET
echo "<div class='test-card'>";
echo "<h3><i class='fas fa-check-circle'></i> 3. Test de Verificación (GET)</h3>";
$verifyUrl = $webhookUrl . '?hub.mode=subscribe&hub.verify_token=' . urlencode(WEBHOOK_VERIFY_TOKEN) . '&hub.challenge=test_challenge_12345';
echo "<p><strong>URL de prueba:</strong></p>";
echo "<div class='code-box'>" . htmlspecialchars($verifyUrl) . "</div>";
echo "<div class='alert alert-info'>";
echo "<strong>💡 Nota:</strong> Este test intenta verificar el webhook desde el servidor mismo. ";
echo "Si estás en localhost, es normal que falle. Lo importante es que el archivo exista y esté configurado correctamente.";
echo "</div>";
// Intentar hacer la verificación solo si no es localhost
$isLocalhost = (strpos($host, 'localhost') !== false || strpos($host, '127.0.0.1') !== false);
if (!$isLocalhost) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $verifyUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($httpCode == 200 && $response === 'test_challenge_12345') {
echo "<p class='ok'>✅ Verificación GET exitosa - El webhook responde correctamente</p>";
echo "<p>Respuesta: <code>$response</code></p>";
} else {
echo "<p class='error'>❌ Error en verificación GET</p>";
echo "<p>Código HTTP: <code>$httpCode</code></p>";
echo "<p>Respuesta: <code>" . htmlspecialchars($response) . "</code></p>";
if ($error) echo "<p>Error cURL: <code>$error</code></p>";
$allOk = false;
}
} else {
// Test manual para localhost
echo "<div class='alert alert-warning'>";
echo "<strong>⚠️ Entorno Local Detectado</strong><br>";
echo "No se puede hacer la verificación automática en localhost. ";
echo "En su lugar, puedes probar manualmente:";
echo "</div>";
echo "<ol>";
echo "<li>Haz clic en el botón de abajo para probar el webhook</li>";
echo "<li>Deberías ver el texto: <code>test_challenge_12345</code></li>";
echo "</ol>";
echo "<a href='$verifyUrl' target='_blank' class='btn btn-primary mb-3'>";
echo "<i class='fas fa-external-link-alt'></i> Probar Webhook Manualmente";
echo "</a>";
// Verificar el archivo directamente
$webhookFilePath = __DIR__ . '/api/webhook.php';
if (file_exists($webhookFilePath)) {
echo "<p class='ok'>✅ El archivo webhook.php existe y es accesible</p>";
// Verificar permisos de lectura
if (is_readable($webhookFilePath)) {
echo "<p class='ok'>✅ El archivo tiene permisos de lectura</p>";
// Verificar que contenga el código necesario
$webhookContent = file_get_contents($webhookFilePath);
if (strpos($webhookContent, 'hub_verify_token') !== false &&
strpos($webhookContent, 'hub_challenge') !== false) {
echo "<p class='ok'>✅ El archivo contiene el código de verificación correcto</p>";
} else {
echo "<p class='warning'>⚠️ El archivo puede no contener el código de verificación</p>";
}
} else {
echo "<p class='error'>❌ El archivo no tiene permisos de lectura</p>";
$allOk = false;
}
}
}
echo "</div>";
// Test 4: Estructura de tablas
echo "<div class='test-card'>";
echo "<h3><i class='fas fa-database'></i> 4. Estructura de Base de Datos</h3>";
$requiredTables = ['users', 'conversations', 'menus', 'menu_options', 'autoresponses', 'webhook_logs'];
echo "<table class='table table-sm'>";
foreach ($requiredTables as $table) {
$exists = $db->fetchAll("SHOW TABLES LIKE '$table'");
$status = !empty($exists) ? 'ok' : 'error';
$icon = !empty($exists) ? '✅' : '❌';
if ($status === 'error') $allOk = false;
echo "<tr>";
echo "<td class='$status'>$icon Tabla: <strong>$table</strong></td>";
if (!empty($exists)) {
// Contar registros
$count = $db->fetch("SELECT COUNT(*) as total FROM $table");
echo "<td><span class='badge bg-info'>{$count['total']} registros</span></td>";
} else {
echo "<td><span class='badge bg-danger'>NO EXISTE</span></td>";
}
echo "</tr>";
}
echo "</table>";
echo "</div>";
// Test 5: Últimos mensajes recibidos
echo "<div class='test-card'>";
echo "<h3><i class='fas fa-inbox'></i> 5. Mensajes Recibidos Recientemente</h3>";
$recentMessages = $db->fetchAll(
"SELECT c.*, u.phone_number, u.name
FROM conversations c
LEFT JOIN users u ON c.user_id = u.id
WHERE c.direction = 'incoming'
ORDER BY c.created_at DESC
LIMIT 5"
);
if (!empty($recentMessages)) {
echo "<p class='ok'>✅ Se encontraron mensajes entrantes</p>";
echo "<table class='table table-sm table-striped'>";
echo "<thead><tr><th>Fecha</th><th>Usuario</th><th>Mensaje</th><th>Estado</th></tr></thead>";
echo "<tbody>";
foreach ($recentMessages as $msg) {
$date = date('d/m/Y H:i', strtotime($msg['created_at']));
$phone = $msg['phone_number'] ?? 'Desconocido';
$content = htmlspecialchars(substr($msg['content'], 0, 50));
$status = $msg['status'];
echo "<tr>";
echo "<td>$date</td>";
echo "<td>$phone</td>";
echo "<td>$content</td>";
echo "<td><span class='badge bg-success'>$status</span></td>";
echo "</tr>";
}
echo "</tbody></table>";
} else {
echo "<p class='warning'>⚠️ No se encontraron mensajes entrantes. El webhook puede no estar recibiendo datos.</p>";
echo "<div class='alert alert-warning'>";
echo "<strong>Posibles causas:</strong><ul>";
echo "<li>El webhook no está configurado en Meta/Facebook</li>";
echo "<li>La URL del webhook es incorrecta</li>";
echo "<li>El servidor no es accesible desde Internet</li>";
echo "<li>Aún no se han enviado mensajes de prueba</li>";
echo "</ul></div>";
}
echo "</div>";
// Test 6: Webhook Logs
echo "<div class='test-card'>";
echo "<h3><i class='fas fa-file-alt'></i> 6. Logs del Webhook</h3>";
$webhookLogs = $db->fetchAll(
"SELECT * FROM webhook_logs
ORDER BY created_at DESC
LIMIT 5"
);
if (!empty($webhookLogs)) {
echo "<p class='ok'>✅ Se encontraron logs del webhook</p>";
echo "<table class='table table-sm'>";
echo "<thead><tr><th>Fecha</th><th>Status</th><th>IP</th><th>Detalles</th></tr></thead>";
echo "<tbody>";
foreach ($webhookLogs as $log) {
$date = date('d/m/Y H:i:s', strtotime($log['created_at']));
$status = $log['status_code'];
$ip = $log['ip_address'];
$statusClass = $status == 200 ? 'success' : 'danger';
echo "<tr>";
echo "<td>$date</td>";
echo "<td><span class='badge bg-$statusClass'>$status</span></td>";
echo "<td>$ip</td>";
echo "<td><button class='btn btn-sm btn-outline-info' onclick='alert(\"Request: " . addslashes(substr($log['request_body'], 0, 200)) . "\")'>Ver</button></td>";
echo "</tr>";
}
echo "</tbody></table>";
} else {
echo "<p class='info'>️ No hay logs del webhook registrados</p>";
}
echo "</div>";
// Test 7: Configuración de Menús y Respuestas
echo "<div class='test-card'>";
echo "<h3><i class='fas fa-robot'></i> 7. Configuración del Bot</h3>";
$menuCount = $db->fetch("SELECT COUNT(*) as total FROM menus WHERE is_active = 1");
$optionCount = $db->fetch("SELECT COUNT(*) as total FROM menu_options WHERE is_active = 1");
$autoResponseCount = $db->fetch("SELECT COUNT(*) as total FROM autoresponses WHERE is_active = 1");
echo "<div class='row'>";
echo "<div class='col-md-4'>";
echo "<div class='alert alert-info text-center'>";
echo "<h4>{$menuCount['total']}</h4>";
echo "<p>Menús Activos</p>";
echo "</div>";
echo "</div>";
echo "<div class='col-md-4'>";
echo "<div class='alert alert-info text-center'>";
echo "<h4>{$optionCount['total']}</h4>";
echo "<p>Opciones de Menú</p>";
echo "</div>";
echo "</div>";
echo "<div class='col-md-4'>";
echo "<div class='alert alert-info text-center'>";
echo "<h4>{$autoResponseCount['total']}</h4>";
echo "<p>Respuestas Automáticas</p>";
echo "</div>";
echo "</div>";
echo "</div>";
echo "</div>";
// Resumen final
echo "<div class='test-card'>";
if ($allOk) {
echo "<div class='alert alert-success'>";
echo "<h4><i class='fas fa-check-circle'></i> ¡Sistema Configurado Correctamente!</h4>";
echo "<p>El webhook está listo para recibir mensajes de WhatsApp.</p>";
echo "</div>";
} else {
echo "<div class='alert alert-danger'>";
echo "<h4><i class='fas fa-exclamation-triangle'></i> Se encontraron problemas</h4>";
echo "<p>Revisa los errores anteriores y corrígelos antes de continuar.</p>";
echo "</div>";
}
echo "<div class='mt-3'>";
echo "<h5>Próximos pasos:</h5>";
echo "<ol>";
echo "<li>Copia la URL del webhook y configúrala en Meta Developer Console</li>";
echo "<li>Usa el token de verificación: <code>" . WEBHOOK_VERIFY_TOKEN . "</code></li>";
echo "<li>Envía un mensaje de prueba desde WhatsApp</li>";
echo "<li>Verifica que aparezca en la sección de mensajes recibidos</li>";
echo "</ol>";
echo "</div>";
echo "</div>";
?>
<div class="text-center mt-4">
<a href="index.php" class="btn btn-primary">
<i class="fas fa-home"></i> Volver al Dashboard
</a>
<button onclick="location.reload()" class="btn btn-secondary">
<i class="fas fa-sync"></i> Recargar Test
</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
-162
View File
@@ -1,162 +0,0 @@
<?php
/**
* Diagnóstico avanzado de WhatsApp API
* Fecha: 4 de enero de 2026
*/
require_once 'config/config.php';
header('Content-Type: text/html; charset=utf-8');
echo "<h2>🔍 Diagnóstico Avanzado de WhatsApp API</h2>";
// 1. Verificar configuración actual
echo "<h3>1. Configuración Actual</h3>";
echo "TOKEN: " . substr(WHATSAPP_TOKEN, 0, 20) . "..." . substr(WHATSAPP_TOKEN, -10) . "<br>";
echo "PHONE_NUMBER_ID: " . WHATSAPP_PHONE_NUMBER_ID . "<br>";
echo "API_URL: " . WHATSAPP_API_URL . "<br>";
// 2. Test de conectividad con WhatsApp API
echo "<h3>2. Test de Conectividad</h3>";
try {
// Test 1: Verificar información del número de teléfono
$ch = curl_init();
$url = WHATSAPP_API_URL . WHATSAPP_PHONE_NUMBER_ID;
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . WHATSAPP_TOKEN,
'Content-Type: application/json'
],
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
echo "❌ Error de conexión: " . $error . "<br>";
} else {
echo "📞 Código HTTP: $httpCode<br>";
if ($httpCode == 200) {
$data = json_decode($response, true);
echo "✅ Conexión exitosa con WhatsApp API<br>";
echo "<pre>" . json_encode($data, JSON_PRETTY_PRINT) . "</pre>";
} else {
echo "❌ Error HTTP $httpCode<br>";
echo "<pre>$response</pre>";
}
}
} catch (Exception $e) {
echo "❌ Excepción: " . $e->getMessage() . "<br>";
}
// 3. Test de mensaje a número de prueba (tu propio número)
echo "<h3>3. Test de Mensaje Real</h3>";
// Crear un botón para probar envío
?>
<form method="post" style="margin: 20px 0;">
<input type="hidden" name="test_message" value="1">
<label>Número de destino:</label>
<input type="text" name="phone" value="+573168950803" style="width: 200px; padding: 5px;">
<br><br>
<label>Mensaje de prueba:</label>
<input type="text" name="message" value="🚀 Test desde sistema - <?php echo date('H:i:s'); ?>" style="width: 400px; padding: 5px;">
<br><br>
<button type="submit" style="padding: 10px 20px; background: #25D366; color: white; border: none; border-radius: 5px;">
📱 Enviar Mensaje de Prueba
</button>
</form>
<?php
if (isset($_POST['test_message'])) {
echo "<h4>🚀 Resultado del Test:</h4>";
try {
$whatsapp = new WhatsAppService();
$phone = $_POST['phone'];
$message = $_POST['message'];
echo "📞 Enviando a: $phone<br>";
echo "💬 Mensaje: $message<br><br>";
$response = $whatsapp->sendTextMessage($phone, $message);
echo "<strong>✅ Respuesta de WhatsApp API:</strong><br>";
echo "<pre style='background: #e8f5e8; padding: 10px; border-radius: 5px;'>";
echo json_encode($response, JSON_PRETTY_PRINT);
echo "</pre>";
if (isset($response['messages'][0]['id'])) {
$messageId = $response['messages'][0]['id'];
echo "<p><strong>📨 Mensaje enviado con ID: $messageId</strong></p>";
echo "<p>⏱️ <strong>Espera 1-2 minutos y verifica tu WhatsApp</strong></p>";
// Mostrar información adicional
echo "<h4>🔍 Información del Envío:</h4>";
if (isset($response['contacts'][0])) {
$contact = $response['contacts'][0];
echo "📱 Número Input: " . $contact['input'] . "<br>";
echo "📱 WhatsApp ID: " . $contact['wa_id'] . "<br>";
}
}
} catch (Exception $e) {
echo "❌ <strong>Error:</strong> " . $e->getMessage() . "<br>";
}
}
// 4. Verificar status del mensaje reciente
echo "<h3>4. Verificar Logs del Sistema</h3>";
$logFile = 'logs/system.log';
if (file_exists($logFile)) {
$logs = file_get_contents($logFile);
$lines = array_filter(array_slice(explode("\n", $logs), -20)); // Últimas 20 líneas
echo "<pre style='background: #f8f9fa; padding: 15px; border: 1px solid #dee2e6; border-radius: 5px; max-height: 400px; overflow-y: auto;'>";
foreach ($lines as $line) {
if (strpos($line, 'ERROR') !== false) {
echo "<span style='color: red;'>$line</span>\n";
} elseif (strpos($line, 'WARNING') !== false) {
echo "<span style='color: orange;'>$line</span>\n";
} else {
echo "$line\n";
}
}
echo "</pre>";
} else {
echo "⚠️ No hay archivo de logs disponible<br>";
}
// 5. Posibles soluciones
echo "<h3>5. ✅ Posibles Soluciones</h3>";
echo "<div style='background: #fff3cd; padding: 15px; border: 1px solid #ffeaa7; border-radius: 5px;'>";
echo "<h4>🔧 Si el mensaje no llega, verifica:</h4>";
echo "<ol>";
echo "<li><strong>Modo de desarrollo:</strong> ¿Tu app está en modo sandbox? Los mensajes solo llegan a números pre-aprobados</li>";
echo "<li><strong>Número verificado:</strong> ¿El número desde el que envías está verificado en Facebook Business?</li>";
echo "<li><strong>Permisos del token:</strong> ¿Tu token tiene permisos 'whatsapp_business_messaging'?</li>";
echo "<li><strong>Número de destino:</strong> ¿El número +573168950803 tiene WhatsApp activo?</li>";
echo "<li><strong>Rate limiting:</strong> ¿Has enviado muchos mensajes recientemente?</li>";
echo "<li><strong>Webhook URL:</strong> ¿Tu webhook está configurado correctamente para recibir respuestas?</li>";
echo "</ol>";
echo "</div>";
echo "<div style='background: #d1ecf1; padding: 15px; border: 1px solid #bee5eb; border-radius: 5px; margin-top: 15px;'>";
echo "<h4>📋 Pasos para resolver:</h4>";
echo "<ol>";
echo "<li>Ve a <a href='https://developers.facebook.com/apps' target='_blank'>Facebook Developers</a></li>";
echo "<li>Selecciona tu app → WhatsApp → Configuración</li>";
echo "<li>Verifica que el número esté <strong>verificado</strong> y <strong>activo</strong></li>";
echo "<li>Revisa la sección 'Phone Numbers' y asegúrate que esté en modo <strong>Producción</strong></li>";
echo "<li>En 'Webhooks', verifica que tu URL esté configurada y validada</li>";
echo "</ol>";
echo "</div>";
?>
-52
View File
@@ -1,52 +0,0 @@
<?php
require_once 'config/config.php';
echo "<h2>Test de Configuración WhatsApp</h2>";
// Obtener configuración desde BD
$config = getWhatsAppConfigFromDB();
echo "<h3>Configuración desde Base de Datos:</h3>";
echo "<pre>";
echo "Token: " . (strlen($config['token']) > 0 ? substr($config['token'], 0, 20) . "... (length: " . strlen($config['token']) . ")" : "EMPTY") . "\n";
echo "Phone Number ID: " . ($config['phone_number_id'] ?: "EMPTY") . "\n";
echo "API URL: " . ($config['api_url'] ?: "EMPTY") . "\n";
echo "</pre>";
// Construir URL como lo haría WhatsAppService
$apiUrl = rtrim($config['api_url'], '/') . '/' . $config['phone_number_id'] . '/messages';
echo "<h3>URL que se construirá:</h3>";
echo "<pre>" . $apiUrl . "</pre>";
// Comparar con tu curl que funciona
$expectedUrl = "https://graph.facebook.com/v22.0/938064202726485/messages";
echo "<h3>URL que debería ser:</h3>";
echo "<pre>" . $expectedUrl . "</pre>";
echo "<h3>¿Coinciden las URLs?</h3>";
echo "<pre>" . ($apiUrl === $expectedUrl ? "✅ SÍ" : "❌ NO") . "</pre>";
// Verificar token
$expectedTokenStart = "EAAcGjPRqunIBQVT6b86O0wWWGLGJ1PANmolIRXRBmEcpqa3P85VlUz0Rrv2c5deXky3gOKZCBw7ZCpdicly4c0G8qFMOH1DfOcSzmV49FlPId3AGPlHEXpqlsPFIE8DF18trW2vglQrFiCHXZCEBSUBQfp1jEASD1NrKKKVHiGVZARrwoxP0J931u0KIr2ZAZCug4fFpnyRZBls9LEgJuomXd4ZCnJtBZA4GvP2zaplmZA52mJiG5H4YiKKWby5GnQq9BgAVaq6sLZBnYI26aR0FgZDZD";
echo "<h3>¿Coincide el token?</h3>";
$tokenMatch = substr($config['token'], 0, 50) === substr($expectedTokenStart, 0, 50);
echo "<pre>" . ($tokenMatch ? "✅ SÍ (primeros 50 caracteres)" : "❌ NO") . "</pre>";
if (!$tokenMatch) {
echo "<h4>Debug Token:</h4>";
echo "<pre>";
echo "Configurado: " . substr($config['token'], 0, 50) . "...\n";
echo "Esperado: " . substr($expectedTokenStart, 0, 50) . "...\n";
echo "</pre>";
}
// Test directo de configuración
echo "<h3>Configuraciones Individuales:</h3>";
echo "<pre>";
foreach(['whatsapp_token', 'whatsapp_phone_number_id', 'whatsapp_api_url'] as $key) {
$value = getConfigFromDB($key, 'NO_ENCONTRADO');
echo "$key: " . ($value === 'NO_ENCONTRADO' ? "❌ NO_ENCONTRADO" : "" . substr($value, 0, 30) . "...") . "\n";
}
echo "</pre>";
?>
Binary file not shown.