delete test
This commit is contained in:
@@ -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.
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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>";
|
||||
?>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
$_SESSION['admin_logged_in'] = true;
|
||||
$_SESSION['admin_username'] = 'test_admin';
|
||||
|
||||
require_once 'api/manage_config.php';
|
||||
?>
|
||||
@@ -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
@@ -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>";
|
||||
?>
|
||||
@@ -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>
|
||||
@@ -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>";
|
||||
?>
|
||||
@@ -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
@@ -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>";
|
||||
?>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<?php
|
||||
echo password_verify('2fa904eee46f', '$2y$10$/ElTmzox8CpwwPaBLOz8JOKPsg7.ml2kn.85XuR7qRl/F4G5fzIxG') ? 'SI FUNCIONA' : 'NO FUNCIONA';
|
||||
?>
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
?>
|
||||
@@ -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";
|
||||
?>
|
||||
@@ -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";
|
||||
}
|
||||
?>
|
||||
@@ -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>
|
||||
@@ -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>";
|
||||
?>
|
||||
@@ -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.
Reference in New Issue
Block a user