up
This commit is contained in:
@@ -1,555 +0,0 @@
|
|||||||
# 🚀 Arquitectura Mejorada - WhatsApp Bot Manager
|
|
||||||
|
|
||||||
## 📋 Tabla de Contenidos
|
|
||||||
|
|
||||||
- [Introducción](#introducción)
|
|
||||||
- [Arquitectura General](#arquitectura-general)
|
|
||||||
- [Componentes Clave](#componentes-clave)
|
|
||||||
- [Flujo de Mensajes](#flujo-de-mensajes)
|
|
||||||
- [Instalación y Configuración](#instalación-y-configuración)
|
|
||||||
- [Deployment](#deployment)
|
|
||||||
- [Monitoreo](#monitoreo)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Introducción
|
|
||||||
|
|
||||||
Esta arquitectura implementa las **mejores prácticas** para chatbots de WhatsApp Business API en PHP, optimizando:
|
|
||||||
|
|
||||||
✅ **Respuesta rápida al webhook** (< 1 segundo)
|
|
||||||
✅ **Procesamiento asíncrono** con colas Redis
|
|
||||||
✅ **Rate limiting automático** (respeta límites de WhatsApp)
|
|
||||||
✅ **Logging estructurado** con Monolog
|
|
||||||
✅ **Gestión de estados** en Redis (conversaciones)
|
|
||||||
✅ **Escalabilidad horizontal** con workers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Arquitectura General
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐
|
|
||||||
│ WhatsApp API │
|
|
||||||
└────────┬────────┘
|
|
||||||
│ POST
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ webhook_optimized │ ← Responde 200 OK < 1 seg
|
|
||||||
│ .php │
|
|
||||||
└────────┬────────────┘
|
|
||||||
│ push
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ Redis Queue │ ← Cola FIFO con prioridad
|
|
||||||
│ (messages, media) │
|
|
||||||
└────────┬────────────┘
|
|
||||||
│ pop (BRPOP)
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ worker.php │ ← Procesa mensajes async
|
|
||||||
│ (3+ procesos) │
|
|
||||||
└────────┬────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ BotService │ ← Lógica del bot
|
|
||||||
│ WhatsAppService │
|
|
||||||
└─────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ventajas sobre arquitectura anterior
|
|
||||||
|
|
||||||
| Antes | Ahora |
|
|
||||||
|-------|-------|
|
|
||||||
| Webhook procesa todo síncronamente | Webhook solo encola (< 100ms) |
|
|
||||||
| Riesgo de timeout si bot tarda > 5s | Worker procesa sin límite de tiempo |
|
|
||||||
| Sin control de límites de envío | Rate limiting automático (80 msg/s) |
|
|
||||||
| Logs dispersos en archivos | Logging centralizado con Monolog |
|
|
||||||
| Estados en BD (lentos) | Estados en Redis (rápido) |
|
|
||||||
| Difícil escalar | Múltiples workers en paralelo |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Componentes Clave
|
|
||||||
|
|
||||||
### 1. **RedisQueue** (`queue/RedisQueue.php`)
|
|
||||||
|
|
||||||
Gestiona colas de mensajes con soporte para:
|
|
||||||
|
|
||||||
- ✅ Prioridades (alta, normal, baja)
|
|
||||||
- ✅ Retry con backoff exponencial
|
|
||||||
- ✅ Dead Letter Queue (DLQ) para mensajes fallidos
|
|
||||||
- ✅ Rate limiting integrado
|
|
||||||
- ✅ Delayed messages (programar envíos)
|
|
||||||
|
|
||||||
**Ejemplo de uso:**
|
|
||||||
|
|
||||||
```php
|
|
||||||
use WhatsApp\Queue\RedisQueue;
|
|
||||||
|
|
||||||
$queue = new RedisQueue();
|
|
||||||
|
|
||||||
// Encolar mensaje
|
|
||||||
$queue->push('messages', [
|
|
||||||
'user' => $user,
|
|
||||||
'messageText' => 'Hola',
|
|
||||||
'messageType' => 'text'
|
|
||||||
], 0); // Prioridad alta
|
|
||||||
|
|
||||||
// Obtener mensaje (bloqueante)
|
|
||||||
$message = $queue->pop(['messages', 'media'], 5); // timeout 5s
|
|
||||||
|
|
||||||
// Re-encolar con retry si falla
|
|
||||||
if (!$success) {
|
|
||||||
$queue->retry('messages', $message, 3); // max 3 intentos
|
|
||||||
}
|
|
||||||
|
|
||||||
// Estadísticas
|
|
||||||
$stats = $queue->getStats();
|
|
||||||
// ['messages' => ['pending' => 5, 'delayed' => 2, 'failed' => 1]]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **ConversationState** (`queue/ConversationState.php`)
|
|
||||||
|
|
||||||
Gestiona estados de conversación en Redis (rápido):
|
|
||||||
|
|
||||||
```php
|
|
||||||
use WhatsApp\Queue\ConversationState;
|
|
||||||
|
|
||||||
$state = new ConversationState();
|
|
||||||
|
|
||||||
// Establecer estado
|
|
||||||
$state->setState($userId, 'waiting_name', [
|
|
||||||
'step' => 1,
|
|
||||||
'retries' => 0
|
|
||||||
], 3600); // Expira en 1 hora
|
|
||||||
|
|
||||||
// Obtener estado
|
|
||||||
$current = $state->getState($userId);
|
|
||||||
// ['state' => 'waiting_name', 'context' => [...], 'updated_at' => ...]
|
|
||||||
|
|
||||||
// Actualizar contexto
|
|
||||||
$state->updateContext($userId, ['name' => 'Juan']);
|
|
||||||
|
|
||||||
// Limpiar al finalizar
|
|
||||||
$state->clearState($userId);
|
|
||||||
|
|
||||||
// Datos temporales (para formularios multi-paso)
|
|
||||||
$state->setTemporaryData($userId, 'email', 'juan@example.com');
|
|
||||||
$email = $state->getTemporaryData($userId, 'email');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Worker** (`worker.php`)
|
|
||||||
|
|
||||||
Procesa mensajes de forma asíncrona:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ejecutar worker manualmente
|
|
||||||
php worker.php messages
|
|
||||||
|
|
||||||
# Worker daemon (loop infinito)
|
|
||||||
php worker.php --daemon
|
|
||||||
|
|
||||||
# Worker específico para media
|
|
||||||
php worker.php media --daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
|
|
||||||
- ✅ Procesa colas: `messages`, `media`, `notifications`
|
|
||||||
- ✅ Shutdown graceful (SIGTERM, SIGINT)
|
|
||||||
- ✅ Auto-restart si excede memoria (128MB)
|
|
||||||
- ✅ Retry automático con backoff
|
|
||||||
- ✅ Logging detallado
|
|
||||||
|
|
||||||
### 4. **Webhook Optimizado** (`api/webhook_optimized.php`)
|
|
||||||
|
|
||||||
Nuevo webhook que **responde inmediatamente**:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Flujo optimizado:
|
|
||||||
1. Leer payload
|
|
||||||
2. Responder 200 OK inmediatamente (< 100ms)
|
|
||||||
3. Forzar envío con fastcgi_finish_request()
|
|
||||||
4. Procesar y encolar sin presión de tiempo
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tiempos medidos:**
|
|
||||||
|
|
||||||
- ⚡ Respuesta al cliente: **50-100ms**
|
|
||||||
- ⏱️ Procesamiento total: **200-500ms**
|
|
||||||
- 📊 vs. webhook anterior: **5x más rápido**
|
|
||||||
|
|
||||||
### 5. **Rate Limiting** (`services/WhatsAppServiceWithRateLimit.php`)
|
|
||||||
|
|
||||||
Respeta límites de WhatsApp automáticamente:
|
|
||||||
|
|
||||||
- 🚦 **80 mensajes/segundo**
|
|
||||||
- 🚦 **1,000 mensajes/hora**
|
|
||||||
- 🚦 **10,000 mensajes/día**
|
|
||||||
|
|
||||||
```php
|
|
||||||
use WhatsAppServiceWithRateLimit;
|
|
||||||
|
|
||||||
$whatsapp = new WhatsAppServiceWithRateLimit();
|
|
||||||
|
|
||||||
// Envía si está dentro del límite, encola si no
|
|
||||||
$result = $whatsapp->sendTextMessage($phone, 'Hola');
|
|
||||||
|
|
||||||
if ($result['queued'] ?? false) {
|
|
||||||
// Mensaje encolado por rate limit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ver estadísticas
|
|
||||||
$stats = $whatsapp->getRateLimitStats();
|
|
||||||
/*
|
|
||||||
[
|
|
||||||
'per_second' => ['current' => 45, 'limit' => 80, 'remaining' => 35],
|
|
||||||
'per_hour' => ['current' => 320, 'limit' => 1000, 'remaining' => 680],
|
|
||||||
'per_day' => ['current' => 5200, 'limit' => 10000, 'remaining' => 4800]
|
|
||||||
]
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. **Logging Estructurado** (`classes/LoggerFactory.php`)
|
|
||||||
|
|
||||||
```php
|
|
||||||
use LoggerFactory;
|
|
||||||
|
|
||||||
// Loggers por canal
|
|
||||||
$logger = LoggerFactory::webhook();
|
|
||||||
$logger->info("Webhook received", ['entries' => 2]);
|
|
||||||
|
|
||||||
// Helper global (compatible con código existente)
|
|
||||||
writeLog('INFO', 'Message processed', ['user_id' => 123], 'bot');
|
|
||||||
|
|
||||||
// Estadísticas
|
|
||||||
$stats = LoggerFactory::getStats();
|
|
||||||
// ['total_files' => 12, 'total_size_mb' => 45.2, 'by_channel' => [...]]
|
|
||||||
|
|
||||||
// Limpieza de logs antiguos (ejecutar con cron)
|
|
||||||
LoggerFactory::cleanup(30); // Elimina logs > 30 días
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Flujo de Mensajes
|
|
||||||
|
|
||||||
### 1. **Mensaje entrante de WhatsApp**
|
|
||||||
|
|
||||||
```
|
|
||||||
Usuario → WhatsApp API → webhook_optimized.php
|
|
||||||
↓ (< 100ms)
|
|
||||||
Responde 200 OK
|
|
||||||
↓
|
|
||||||
Encola en Redis (queue:messages)
|
|
||||||
↓
|
|
||||||
worker.php (BRPOP)
|
|
||||||
↓
|
|
||||||
BotService.processMessage()
|
|
||||||
↓
|
|
||||||
WhatsAppService.sendTextMessage()
|
|
||||||
↓ (con rate limit)
|
|
||||||
WhatsApp API → Usuario
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Envío de mensajes salientes**
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Sin rate limit (envío inmediato)
|
|
||||||
WhatsAppService::sendTextMessage($phone, 'Hola');
|
|
||||||
|
|
||||||
// Con rate limit (puede encolar automáticamente)
|
|
||||||
WhatsAppServiceWithRateLimit::sendTextMessage($phone, 'Hola');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Procesamiento de media**
|
|
||||||
|
|
||||||
```
|
|
||||||
Usuario envía imagen → webhook detecta media
|
|
||||||
↓
|
|
||||||
Encola en queue:media
|
|
||||||
↓
|
|
||||||
worker.php procesa
|
|
||||||
↓
|
|
||||||
MediaService.fetchAndStoreFromGraph()
|
|
||||||
↓
|
|
||||||
Descarga y guarda en /uploads/
|
|
||||||
↓
|
|
||||||
Actualiza conversations.local_file
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Instalación y Configuración
|
|
||||||
|
|
||||||
### 1. **Instalar dependencias**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /ruta/a/tu/proyecto
|
|
||||||
composer install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Configurar .env**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
nano .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Agregar configuración de Redis:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Redis
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=
|
|
||||||
REDIS_DB=0
|
|
||||||
|
|
||||||
# Rate Limiting
|
|
||||||
ENABLE_RATE_LIMIT=true
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_PATH=logs
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Instalar y configurar Redis**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install redis-server -y
|
|
||||||
sudo systemctl enable redis-server
|
|
||||||
sudo systemctl start redis-server
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
redis-cli ping
|
|
||||||
# Respuesta: PONG
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **Configurar Webhook en WhatsApp**
|
|
||||||
|
|
||||||
Actualizar URL del webhook a `webhook_optimized.php`:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://tudominio.com/api/webhook_optimized.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. **Iniciar Workers**
|
|
||||||
|
|
||||||
#### Opción A: Manual (desarrollo)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php worker.php --daemon &
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Opción B: Con Supervisor (producción)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Instalar supervisor
|
|
||||||
sudo apt install supervisor -y
|
|
||||||
|
|
||||||
# Copiar configuración
|
|
||||||
sudo cp supervisor-whatsapp-worker.conf /etc/supervisor/conf.d/
|
|
||||||
|
|
||||||
# Actualizar rutas en el archivo
|
|
||||||
sudo nano /etc/supervisor/conf.d/supervisor-whatsapp-worker.conf
|
|
||||||
|
|
||||||
# Recargar supervisor
|
|
||||||
sudo supervisorctl reread
|
|
||||||
sudo supervisorctl update
|
|
||||||
sudo supervisorctl start whatsapp-worker:*
|
|
||||||
|
|
||||||
# Ver estado
|
|
||||||
sudo supervisorctl status
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Checklist de producción
|
|
||||||
|
|
||||||
- [ ] Redis instalado y funcionando
|
|
||||||
- [ ] Composer dependencies instaladas
|
|
||||||
- [ ] .env configurado correctamente
|
|
||||||
- [ ] Workers iniciados con Supervisor
|
|
||||||
- [ ] Webhook actualizado en WhatsApp
|
|
||||||
- [ ] Logs con permisos correctos (755 en `/logs`)
|
|
||||||
- [ ] Cron configurado para logs cleanup
|
|
||||||
|
|
||||||
### Cron recomendado
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Editar crontab
|
|
||||||
crontab -e
|
|
||||||
|
|
||||||
# Agregar estas líneas:
|
|
||||||
# Procesar mensajes delayed cada minuto
|
|
||||||
* * * * * cd /ruta/proyecto && php -r "require 'vendor/autoload.php'; use WhatsApp\Queue\RedisQueue; \$q = new RedisQueue(); \$q->processDelayed('messages');"
|
|
||||||
|
|
||||||
# Limpiar logs antiguos cada día a las 3 AM
|
|
||||||
0 3 * * * cd /ruta/proyecto && php -r "require 'classes/LoggerFactory.php'; LoggerFactory::cleanup(30);"
|
|
||||||
|
|
||||||
# Monitorear workers (reiniciar si están caídos)
|
|
||||||
*/5 * * * * supervisorctl status whatsapp-worker:* | grep -q RUNNING || supervisorctl restart whatsapp-worker:*
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Monitoreo
|
|
||||||
|
|
||||||
### 1. **Ver estadísticas de colas**
|
|
||||||
|
|
||||||
```php
|
|
||||||
use WhatsApp\Queue\RedisQueue;
|
|
||||||
|
|
||||||
$queue = new RedisQueue();
|
|
||||||
$stats = $queue->getStats();
|
|
||||||
print_r($stats);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Ver logs en tiempo real**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Webhook
|
|
||||||
tail -f logs/webhook.log
|
|
||||||
|
|
||||||
# Worker
|
|
||||||
tail -f logs/worker.log
|
|
||||||
|
|
||||||
# Bot
|
|
||||||
tail -f logs/bot.log
|
|
||||||
|
|
||||||
# Todos los errores
|
|
||||||
tail -f logs/*.log | grep ERROR
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Verificar estado de workers**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo supervisorctl status whatsapp-worker:*
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **Monitorear Redis**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Conexión
|
|
||||||
redis-cli
|
|
||||||
|
|
||||||
# Ver todas las colas
|
|
||||||
KEYS whatsapp:queue:*
|
|
||||||
|
|
||||||
# Ver longitud de cola
|
|
||||||
LLEN whatsapp:queue:messages
|
|
||||||
|
|
||||||
# Ver rate limits
|
|
||||||
KEYS whatsapp:ratelimit:*
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### ❌ Worker no procesa mensajes
|
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar si worker está corriendo
|
|
||||||
ps aux | grep worker.php
|
|
||||||
|
|
||||||
# Ver logs
|
|
||||||
tail -f logs/worker.log
|
|
||||||
|
|
||||||
# Reiniciar workers
|
|
||||||
sudo supervisorctl restart whatsapp-worker:*
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Redis no conecta
|
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar Redis
|
|
||||||
sudo systemctl status redis-server
|
|
||||||
|
|
||||||
# Reiniciar
|
|
||||||
sudo systemctl restart redis-server
|
|
||||||
|
|
||||||
# Verificar puerto
|
|
||||||
netstat -an | grep 6379
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Rate limit excedido constantemente
|
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Ver estadísticas actuales
|
|
||||||
$whatsapp = new WhatsAppServiceWithRateLimit();
|
|
||||||
print_r($whatsapp->getRateLimitStats());
|
|
||||||
|
|
||||||
// Resetear límites (solo desarrollo)
|
|
||||||
$whatsapp->resetRateLimits();
|
|
||||||
|
|
||||||
// Aumentar número de workers
|
|
||||||
// Editar supervisor-whatsapp-worker.conf:
|
|
||||||
// numprocs=5 (en lugar de 3)
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Logs muy grandes
|
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Limpiar manualmente
|
|
||||||
cd logs
|
|
||||||
find . -name "*.log*" -mtime +7 -delete
|
|
||||||
|
|
||||||
# O ejecutar cleanup:
|
|
||||||
php -r "require 'classes/LoggerFactory.php'; echo LoggerFactory::cleanup(7) . ' archivos eliminados';"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Benchmarks
|
|
||||||
|
|
||||||
### Antes vs. Ahora
|
|
||||||
|
|
||||||
| Métrica | Antes | Ahora | Mejora |
|
|
||||||
|---------|-------|-------|--------|
|
|
||||||
| Respuesta webhook | 3-8s | 50-150ms | **50x más rápido** |
|
|
||||||
| Mensajes/segundo | ~10 | 80 (límite API) | **8x más rápido** |
|
|
||||||
| Timeout en webhook | 5-10% | 0% | **100% confiable** |
|
|
||||||
| Memoria worker | N/A | 20-40 MB | **Eficiente** |
|
|
||||||
| Reintentos exitosos | Manual | Automático | **100% automatizado** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Próximos pasos
|
|
||||||
|
|
||||||
1. ✅ **Implementado:** Arquitectura con colas
|
|
||||||
2. ✅ **Implementado:** Rate limiting
|
|
||||||
3. ✅ **Implementado:** Logging estructurado
|
|
||||||
4. 🔜 **Recomendado:** Monitoreo con Prometheus/Grafana
|
|
||||||
5. 🔜 **Recomendado:** Alertas por email/Slack cuando worker cae
|
|
||||||
6. 🔜 **Recomendado:** Dashboard web para ver estadísticas en tiempo real
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Soporte
|
|
||||||
|
|
||||||
Para dudas o problemas:
|
|
||||||
|
|
||||||
1. Revisar logs en `/logs`
|
|
||||||
2. Ver [troubleshooting](#troubleshooting)
|
|
||||||
3. Consultar documentación de WhatsApp API
|
|
||||||
4. Verificar configuración de Redis y workers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Última actualización:** Enero 2026
|
|
||||||
**Versión:** 2.0
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
# 🚀 Guía de Despliegue en Servidor
|
|
||||||
|
|
||||||
## 📋 Requisitos del Servidor
|
|
||||||
|
|
||||||
### Mínimos
|
|
||||||
- **PHP**: 8.0 o superior
|
|
||||||
- **MySQL/MariaDB**: 5.7 o superior
|
|
||||||
- **Servidor Web**: Apache/Nginx
|
|
||||||
- **SSL**: Certificado válido (HTTPS obligatorio para WhatsApp)
|
|
||||||
- **Memoria RAM**: Mínimo 512MB
|
|
||||||
- **Espacio**: 1GB libre
|
|
||||||
|
|
||||||
### Extensiones PHP Requeridas
|
|
||||||
```bash
|
|
||||||
php-pdo
|
|
||||||
php-pdo-mysql
|
|
||||||
php-curl
|
|
||||||
php-json
|
|
||||||
php-mbstring
|
|
||||||
php-openssl
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌍 Configuración por Tipo de Servidor
|
|
||||||
|
|
||||||
### 🔹 **VPS/Servidor Dedicado (Ubuntu/CentOS)**
|
|
||||||
|
|
||||||
#### 1. Instalar Dependencias
|
|
||||||
```bash
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install apache2 mysql-server php8.0 php8.0-mysql php8.0-curl php8.0-json php8.0-mbstring
|
|
||||||
|
|
||||||
# CentOS/RHEL
|
|
||||||
sudo yum install httpd mysql-server php php-mysql php-curl php-json php-mbstring
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Configurar Apache
|
|
||||||
```apache
|
|
||||||
# /etc/apache2/sites-available/whatsapp-bot.conf
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName tudominio.com
|
|
||||||
DocumentRoot /var/www/html/bot
|
|
||||||
|
|
||||||
<Directory /var/www/html/bot>
|
|
||||||
AllowOverride All
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
# Redirigir HTTP a HTTPS
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{HTTPS} off
|
|
||||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
|
||||||
ServerName tudominio.com
|
|
||||||
DocumentRoot /var/www/html/migrador
|
|
||||||
|
|
||||||
SSLEngine on
|
|
||||||
SSLCertificateFile /path/to/your/certificate.crt
|
|
||||||
SSLCertificateKeyFile /path/to/your/private.key
|
|
||||||
|
|
||||||
<Directory /var/www/html/migrador>
|
|
||||||
AllowOverride All
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
</VirtualHost>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Subir Archivos
|
|
||||||
```bash
|
|
||||||
# Crear directorio
|
|
||||||
sudo mkdir -p /var/www/html/migrador
|
|
||||||
|
|
||||||
# Subir archivos (usando SCP/SFTP)
|
|
||||||
scp -r bot/* usuario@servidor:/var/www/html/bot/
|
|
||||||
|
|
||||||
# Configurar permisos
|
|
||||||
sudo chown -R www-data:www-data /var/www/html/bot
|
|
||||||
sudo chmod -R 755 /var/www/html/bot
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔹 **Hosting Compartido (cPanel/Plesk)**
|
|
||||||
|
|
||||||
#### 1. Subir Archivos
|
|
||||||
- Accede a tu **File Manager** en cPanel
|
|
||||||
- Ve a la carpeta `public_html`
|
|
||||||
- Crea carpeta `migrador` (o sube directamente a raíz)
|
|
||||||
- Sube todos los archivos del proyecto
|
|
||||||
|
|
||||||
#### 2. Configurar Base de Datos
|
|
||||||
- Ve a **MySQL Databases** en cPanel
|
|
||||||
- Crea nueva base de datos: `tuusuario_whatsapp`
|
|
||||||
- Crea usuario y asigna privilegios
|
|
||||||
- Anota: host, usuario, contraseña, base de datos
|
|
||||||
|
|
||||||
#### 3. Editar Configuración
|
|
||||||
```php
|
|
||||||
// config/config.php
|
|
||||||
define('DB_HOST', 'localhost'); // O tu host específico
|
|
||||||
define('DB_NAME', 'tuusuario_whatsapp');
|
|
||||||
define('DB_USER', 'tuusuario_dbuser');
|
|
||||||
define('DB_PASS', 'tu_password_db');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔹 **Servicios en la Nube**
|
|
||||||
|
|
||||||
#### **AWS EC2**
|
|
||||||
```bash
|
|
||||||
# 1. Conectar por SSH
|
|
||||||
ssh -i tu-clave.pem ec2-user@tu-ip-publica
|
|
||||||
|
|
||||||
# 2. Instalar LAMP stack
|
|
||||||
sudo yum update -y
|
|
||||||
sudo amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2
|
|
||||||
|
|
||||||
# 3. Configurar y seguir pasos de VPS
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **DigitalOcean Droplet**
|
|
||||||
```bash
|
|
||||||
# 1. Crear droplet con LAMP stack preinstalado
|
|
||||||
# 2. Conectar por SSH
|
|
||||||
# 3. Subir archivos a /var/www/html/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Google Cloud Platform**
|
|
||||||
```bash
|
|
||||||
# 1. Crear VM con Ubuntu
|
|
||||||
# 2. Instalar LAMP stack
|
|
||||||
# 3. Configurar firewall para puertos 80/443
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Configuración Específica del Sistema
|
|
||||||
|
|
||||||
### 1. Editar config/config.php
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
// === CONFIGURACIÓN DE PRODUCCIÓN ===
|
|
||||||
|
|
||||||
// Base de datos de producción
|
|
||||||
define('DB_HOST', 'tu-servidor-mysql');
|
|
||||||
define('DB_NAME', 'whatsapp_production');
|
|
||||||
define('DB_USER', 'tu_usuario_db');
|
|
||||||
define('DB_PASS', 'password_super_seguro');
|
|
||||||
|
|
||||||
// URL de producción
|
|
||||||
define('APP_URL', 'https://tudominio.com/migrador');
|
|
||||||
|
|
||||||
// Deshabilitar errores en producción
|
|
||||||
error_reporting(0);
|
|
||||||
ini_set('display_errors', 0);
|
|
||||||
ini_set('log_errors', 1);
|
|
||||||
|
|
||||||
// Configuración de seguridad
|
|
||||||
define('SECURE_MODE', true);
|
|
||||||
?>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Crear .htaccess (Apache)
|
|
||||||
```apache
|
|
||||||
# .htaccess en raíz del proyecto
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
# Forzar HTTPS
|
|
||||||
RewriteCond %{HTTPS} off
|
|
||||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
|
||||||
|
|
||||||
# Proteger archivos sensibles
|
|
||||||
<Files "config.php">
|
|
||||||
Require all denied
|
|
||||||
</Files>
|
|
||||||
|
|
||||||
<Files "*.sql">
|
|
||||||
Require all denied
|
|
||||||
</Files>
|
|
||||||
|
|
||||||
# Cache headers
|
|
||||||
<IfModule mod_expires.c>
|
|
||||||
ExpiresActive on
|
|
||||||
ExpiresByType text/css "access plus 1 year"
|
|
||||||
ExpiresByType application/javascript "access plus 1 year"
|
|
||||||
ExpiresByType image/png "access plus 1 year"
|
|
||||||
ExpiresByType image/jpg "access plus 1 year"
|
|
||||||
</IfModule>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configurar SSL (Let's Encrypt)
|
|
||||||
```bash
|
|
||||||
# Instalar Certbot
|
|
||||||
sudo apt install certbot python3-certbot-apache
|
|
||||||
|
|
||||||
# Obtener certificado
|
|
||||||
sudo certbot --apache -d tudominio.com
|
|
||||||
|
|
||||||
# Auto-renovación
|
|
||||||
sudo crontab -e
|
|
||||||
# Agregar: 0 12 * * * /usr/bin/certbot renew --quiet
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔗 Configuración de WhatsApp Webhook
|
|
||||||
|
|
||||||
### 1. URL del Webhook
|
|
||||||
```
|
|
||||||
https://tudominio.com/bot/api/webhook.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configurar en Facebook Developers
|
|
||||||
1. Ve a [developers.facebook.com](https://developers.facebook.com)
|
|
||||||
2. Selecciona tu app de WhatsApp Business
|
|
||||||
3. Ve a **WhatsApp > Configuration**
|
|
||||||
4. En **Webhooks**:
|
|
||||||
- **Callback URL**: `https://tudominio.com/migrador/api/webhook.php`
|
|
||||||
- **Verify Token**: `mi_token_secreto_123` (o cambia en config.php)
|
|
||||||
- **Webhook fields**: ✅ `conversations`
|
|
||||||
|
|
||||||
### 3. Verificar Webhook
|
|
||||||
```bash
|
|
||||||
# Probar webhook manualmente
|
|
||||||
curl -X GET "https://tudominio.com/migrador/api/webhook.php?hub.mode=subscribe&hub.challenge=CHALLENGE_ACCEPTED&hub.verify_token=mi_token_secreto_123"
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Configuración Post-Instalación
|
|
||||||
|
|
||||||
### 1. Ejecutar Instalador
|
|
||||||
```
|
|
||||||
https://tudominio.com/migrador/install.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Verificar Sistema
|
|
||||||
```
|
|
||||||
https://tudominio.com/migrador/test.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configurar Panel
|
|
||||||
```
|
|
||||||
https://tudominio.com/migrador/index.php
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Seguridad en Producción
|
|
||||||
|
|
||||||
### 1. Configuraciones PHP
|
|
||||||
```ini
|
|
||||||
# php.ini ajustes
|
|
||||||
expose_php = Off
|
|
||||||
display_errors = Off
|
|
||||||
log_errors = On
|
|
||||||
allow_url_fopen = Off
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Permisos de Archivos
|
|
||||||
```bash
|
|
||||||
# Archivos: 644
|
|
||||||
sudo find /var/www/html/migrador -type f -exec chmod 644 {} \;
|
|
||||||
|
|
||||||
# Directorios: 755
|
|
||||||
sudo find /var/www/html/migrador -type d -exec chmod 755 {} \;
|
|
||||||
|
|
||||||
# Config solo lectura
|
|
||||||
sudo chmod 600 /var/www/html/migrador/config/config.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Backup Automático
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# backup.sh
|
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
mysqldump -u usuario -p password whatsapp_db > /backups/whatsapp_$DATE.sql
|
|
||||||
tar -czf /backups/files_$DATE.tar.gz /var/www/html/migrador
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Monitoreo y Logs
|
|
||||||
|
|
||||||
### 1. Logs de Apache
|
|
||||||
```bash
|
|
||||||
tail -f /var/log/apache2/error.log
|
|
||||||
tail -f /var/log/apache2/access.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Logs de PHP
|
|
||||||
```bash
|
|
||||||
tail -f /var/log/php_errors.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Logs del Sistema
|
|
||||||
```bash
|
|
||||||
# Ver logs desde el panel
|
|
||||||
https://tudominio.com/migrador/index.php#logs
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Optimizaciones de Rendimiento
|
|
||||||
|
|
||||||
### 1. Cache de PHP (OPcache)
|
|
||||||
```ini
|
|
||||||
# php.ini
|
|
||||||
opcache.enable=1
|
|
||||||
opcache.memory_consumption=128
|
|
||||||
opcache.max_accelerated_files=1000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Compresión GZIP
|
|
||||||
```apache
|
|
||||||
# En .htaccess
|
|
||||||
<IfModule mod_deflate.c>
|
|
||||||
AddOutputFilterByType DEFLATE text/plain
|
|
||||||
AddOutputFilterByType DEFLATE text/html
|
|
||||||
AddOutputFilterByType DEFLATE text/css
|
|
||||||
AddOutputFilterByType DEFLATE application/javascript
|
|
||||||
</IfModule>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Optimización MySQL
|
|
||||||
```sql
|
|
||||||
-- my.cnf
|
|
||||||
[mysqld]
|
|
||||||
innodb_buffer_pool_size = 256M
|
|
||||||
query_cache_type = 1
|
|
||||||
query_cache_size = 64M
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 URLs de Producción
|
|
||||||
|
|
||||||
Una vez configurado:
|
|
||||||
|
|
||||||
- **🏠 Panel**: https://tudominio.com/migrador/index.php
|
|
||||||
- **🔗 API**: https://tudominio.com/migrador/api/
|
|
||||||
- **📱 Webhook**: https://tudominio.com/migrador/api/webhook.php
|
|
||||||
- **📊 Stats**: https://tudominio.com/migrador/api/get_stats.php
|
|
||||||
|
|
||||||
## ⚠️ Troubleshooting
|
|
||||||
|
|
||||||
### Error 500
|
|
||||||
- Revisar logs de Apache/PHP
|
|
||||||
- Verificar permisos de archivos
|
|
||||||
- Comprobar configuración de base de datos
|
|
||||||
|
|
||||||
### Webhook no funciona
|
|
||||||
- Verificar SSL válido
|
|
||||||
- Comprobar URL pública
|
|
||||||
- Revisar token de verificación
|
|
||||||
|
|
||||||
### Base de datos no conecta
|
|
||||||
- Verificar credenciales
|
|
||||||
- Comprobar host/puerto
|
|
||||||
- Revisar permisos de usuario MySQL
|
|
||||||
|
|
||||||
## 📝 Checklist Final
|
|
||||||
|
|
||||||
- [ ] ✅ Servidor con PHP 8.0+ y MySQL
|
|
||||||
- [ ] ✅ SSL/HTTPS configurado
|
|
||||||
- [ ] ✅ Archivos subidos con permisos correctos
|
|
||||||
- [ ] ✅ Base de datos creada y configurada
|
|
||||||
- [ ] ✅ config.php editado con datos reales
|
|
||||||
- [ ] ✅ Instalador ejecutado sin errores
|
|
||||||
- [ ] ✅ Webhook configurado en Facebook
|
|
||||||
- [ ] ✅ Sistema probado y funcionando
|
|
||||||
- [ ] ✅ Backup automático configurado
|
|
||||||
|
|
||||||
¡Tu sistema WhatsApp Bot Manager estará listo para producción! 🎉
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
# Guía de Uso - Docker Compose Development
|
|
||||||
|
|
||||||
## 🚀 Inicio Rápido
|
|
||||||
|
|
||||||
### Levantar entorno de desarrollo
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.dev.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ver logs en tiempo real
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.dev.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Detener entorno
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.dev.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reconstruir contenedores
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.dev.yml build --no-cache
|
|
||||||
docker-compose -f docker-compose.dev.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Herramientas Disponibles
|
|
||||||
|
|
||||||
| Servicio | URL | Descripción |
|
|
||||||
|----------|-----|-------------|
|
|
||||||
| **Aplicación** | http://localhost:8080 | WhatsApp Bot principal |
|
|
||||||
| **XDebug** | Puerto 9003 | Debugger para IDE (VSCode, PHPStorm) |
|
|
||||||
| **Redis Commander** | http://localhost:8082 | Interface web para Redis |
|
|
||||||
| **MailHog** | http://localhost:8025 | Captura de emails de prueba |
|
|
||||||
| **Adminer** | http://localhost:8083 | Gestor de base de datos |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Características de Desarrollo
|
|
||||||
|
|
||||||
### ✅ Hot Reload Activado
|
|
||||||
Los cambios en archivos PHP/JS/CSS se reflejan **inmediatamente** sin reiniciar:
|
|
||||||
- ✅ Volumen mount: `./:/var/www/html`
|
|
||||||
- ✅ OPcache desactivado
|
|
||||||
- ✅ No requiere rebuild
|
|
||||||
|
|
||||||
### 🐛 Debugging con XDebug
|
|
||||||
|
|
||||||
#### VSCode Configuration (`.vscode/launch.json`)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Listen for XDebug",
|
|
||||||
"type": "php",
|
|
||||||
"request": "launch",
|
|
||||||
"port": 9003,
|
|
||||||
"pathMappings": {
|
|
||||||
"/var/www/html": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PHPStorm Configuration
|
|
||||||
1. Settings → PHP → Debug → XDebug
|
|
||||||
2. Port: `9003`
|
|
||||||
3. Path mappings: `/var/www/html` → `/Users/lizandro/Documents/GitHub/whatsapp`
|
|
||||||
|
|
||||||
### 📊 Logs en Tiempo Real
|
|
||||||
```bash
|
|
||||||
# Ver logs de todos los servicios
|
|
||||||
docker-compose -f docker-compose.dev.yml logs -f
|
|
||||||
|
|
||||||
# Ver logs de un servicio específico
|
|
||||||
docker-compose -f docker-compose.dev.yml logs -f app
|
|
||||||
|
|
||||||
# Ver logs PHP-FPM
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app tail -f /var/www/html/logs/php-fpm-error.log
|
|
||||||
|
|
||||||
# Ver logs de XDebug
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app tail -f /var/www/html/logs/xdebug.log
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📧 Testing de Emails con MailHog
|
|
||||||
|
|
||||||
MailHog captura todos los emails enviados desde la aplicación:
|
|
||||||
|
|
||||||
1. Configura PHP para usar MailHog:
|
|
||||||
```php
|
|
||||||
ini_set('SMTP', 'mailhog');
|
|
||||||
ini_set('smtp_port', 1025);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Abre http://localhost:8025 para ver emails capturados
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Inspeccionar Redis
|
|
||||||
|
|
||||||
### Opción 1: Redis Commander (Web UI)
|
|
||||||
- URL: http://localhost:8082
|
|
||||||
- Ver claves, valores, TTL
|
|
||||||
- Ejecutar comandos Redis
|
|
||||||
|
|
||||||
### Opción 2: Redis CLI
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.dev.yml exec redis redis-cli
|
|
||||||
|
|
||||||
# Comandos útiles:
|
|
||||||
KEYS * # Ver todas las claves
|
|
||||||
GET clave # Ver valor de una clave
|
|
||||||
TTL clave # Ver tiempo de vida
|
|
||||||
MONITOR # Ver comandos en tiempo real
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗄️ Acceso a Base de Datos
|
|
||||||
|
|
||||||
### Adminer (http://localhost:8083)
|
|
||||||
- **Server**: Tu DB_HOST del .env
|
|
||||||
- **Username**: Tu DB_USER del .env
|
|
||||||
- **Password**: Tu DB_PASS del .env
|
|
||||||
- **Database**: Tu DB_NAME del .env
|
|
||||||
|
|
||||||
### MySQL CLI desde contenedor
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app mysql -h ${DB_HOST} -u ${DB_USER} -p${DB_PASS} ${DB_NAME}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Comandos Útiles
|
|
||||||
|
|
||||||
### Ejecutar comandos en el contenedor
|
|
||||||
```bash
|
|
||||||
# Bash interactivo
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app bash
|
|
||||||
|
|
||||||
# PHP CLI
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app php -v
|
|
||||||
|
|
||||||
# Composer
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app composer install
|
|
||||||
|
|
||||||
# Verificar configuración PHP
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app php -i | grep xdebug
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reiniciar servicios
|
|
||||||
```bash
|
|
||||||
# Reiniciar un servicio específico
|
|
||||||
docker-compose -f docker-compose.dev.yml restart app
|
|
||||||
|
|
||||||
# Reiniciar todos
|
|
||||||
docker-compose -f docker-compose.dev.yml restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### Limpiar todo (cuidado!)
|
|
||||||
```bash
|
|
||||||
# Detener y eliminar contenedores + volúmenes
|
|
||||||
docker-compose -f docker-compose.dev.yml down -v
|
|
||||||
|
|
||||||
# Limpiar imágenes no utilizadas
|
|
||||||
docker system prune -a
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Troubleshooting
|
|
||||||
|
|
||||||
### Problema: Cambios no se reflejan
|
|
||||||
```bash
|
|
||||||
# 1. Verificar que el volumen esté montado
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app ls -la /var/www/html
|
|
||||||
|
|
||||||
# 2. Verificar que OPcache esté desactivado
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app php -i | grep opcache.enable
|
|
||||||
|
|
||||||
# 3. Limpiar caché del navegador (Cmd+Shift+R)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: XDebug no funciona
|
|
||||||
```bash
|
|
||||||
# Verificar que XDebug esté instalado
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app php -m | grep xdebug
|
|
||||||
|
|
||||||
# Ver configuración de XDebug
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app php -i | grep xdebug
|
|
||||||
|
|
||||||
# Ver logs de XDebug
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app tail -f /var/www/html/logs/xdebug.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Redis no conecta
|
|
||||||
```bash
|
|
||||||
# Verificar que Redis esté corriendo
|
|
||||||
docker-compose -f docker-compose.dev.yml ps
|
|
||||||
|
|
||||||
# Test de conexión
|
|
||||||
docker-compose -f docker-compose.dev.yml exec app php -r "try { \$r = new Redis(); \$r->connect('redis', 6379); echo 'OK'; } catch (Exception \$e) { echo \$e->getMessage(); }"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Diferencias con Producción
|
|
||||||
|
|
||||||
| Característica | Desarrollo | Producción |
|
|
||||||
|----------------|-----------|------------|
|
|
||||||
| OPcache | ❌ Desactivado | ✅ Activado |
|
|
||||||
| Error Display | ✅ Activado | ❌ Desactivado |
|
|
||||||
| XDebug | ✅ Activado | ❌ Desactivado |
|
|
||||||
| Log Level | DEBUG | INFO/WARNING |
|
|
||||||
| Workers | 1 | 2+ |
|
|
||||||
| Rate Limiting | ❌ Desactivado | ✅ Activado |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Migrar de Dev a Producción
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Detener entorno de desarrollo
|
|
||||||
docker-compose -f docker-compose.dev.yml down
|
|
||||||
|
|
||||||
# 2. Levantar entorno de producción
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 3. Verificar
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Notas
|
|
||||||
|
|
||||||
- **No commitear** `docker-compose.dev.yml` en producción
|
|
||||||
- Usar `.env` para configuración sensible
|
|
||||||
- Los volúmenes de desarrollo son locales (no se comparten)
|
|
||||||
- MailHog solo atrapa emails locales, no los envía realmente
|
|
||||||
@@ -1,462 +0,0 @@
|
|||||||
# 🐳 WhatsApp Bot Manager - Docker
|
|
||||||
|
|
||||||
## Guía Rápida de Instalación
|
|
||||||
|
|
||||||
Esta es la forma **más fácil y rápida** de desplegar el WhatsApp Bot Manager con todas sus funcionalidades.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 ¿Qué incluye?
|
|
||||||
|
|
||||||
La arquitectura Docker incluye:
|
|
||||||
|
|
||||||
- ✅ **PHP 8.2 + Nginx** (aplicación web)
|
|
||||||
- ✅ **MySQL 8.0** (base de datos)
|
|
||||||
- ✅ **Redis 7** (colas y caché)
|
|
||||||
- ✅ **3 Workers** automáticos (procesamiento asíncrono)
|
|
||||||
- ✅ **Supervisor** (gestor de procesos)
|
|
||||||
- ✅ **PHPMyAdmin** (opcional, para desarrollo)
|
|
||||||
- ✅ **Redis Commander** (opcional, para debugging)
|
|
||||||
- ✅ **Health checks** automáticos
|
|
||||||
- ✅ **Logs centralizados**
|
|
||||||
- ✅ **Auto-restart** en caso de fallos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Instalación en 3 Pasos
|
|
||||||
|
|
||||||
### 1. Clonar y configurar
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /ruta/a/tu/proyecto
|
|
||||||
|
|
||||||
# Copiar variables de entorno
|
|
||||||
cp .env.docker .env
|
|
||||||
|
|
||||||
# Editar .env con tus credenciales de WhatsApp
|
|
||||||
nano .env
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mínimo requerido en `.env`:**
|
|
||||||
|
|
||||||
```env
|
|
||||||
# WhatsApp API
|
|
||||||
WHATSAPP_TOKEN=tu_token_aqui
|
|
||||||
WHATSAPP_PHONE_NUMBER_ID=tu_phone_number_id
|
|
||||||
WEBHOOK_VERIFY_TOKEN=tu_token_verificacion
|
|
||||||
|
|
||||||
# Base de datos (puedes dejar estos valores)
|
|
||||||
DB_PASS=password_seguro
|
|
||||||
DB_ROOT_PASSWORD=root_password_seguro
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Construir contenedores
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Iniciar servicios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
**¡Listo!** 🎉 Tu bot está corriendo en: `http://localhost:8080`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Comandos Útiles
|
|
||||||
|
|
||||||
### Ver logs en tiempo real
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Todos los servicios
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Solo la aplicación
|
|
||||||
docker-compose logs -f app
|
|
||||||
|
|
||||||
# Solo workers
|
|
||||||
docker-compose logs -f worker
|
|
||||||
|
|
||||||
# Solo webhook
|
|
||||||
docker-compose logs -f app | grep webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ver estado de servicios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reiniciar servicios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Reiniciar todo
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Reiniciar solo app
|
|
||||||
docker-compose restart app
|
|
||||||
|
|
||||||
# Reiniciar workers
|
|
||||||
docker-compose restart worker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ejecutar comandos dentro del contenedor
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Shell interactivo
|
|
||||||
docker-compose exec app sh
|
|
||||||
|
|
||||||
# Ejecutar script PHP
|
|
||||||
docker-compose exec app php test_architecture.php
|
|
||||||
|
|
||||||
# Ver cola de Redis
|
|
||||||
docker-compose exec redis redis-cli KEYS "whatsapp:queue:*"
|
|
||||||
|
|
||||||
# Ver estadísticas de workers
|
|
||||||
docker-compose exec app supervisorctl status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Detener y eliminar todo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Detener servicios
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Detener y eliminar volúmenes (⚠️ borra datos)
|
|
||||||
docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Configuración Avanzada
|
|
||||||
|
|
||||||
### Cambiar puerto de la aplicación
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# En .env
|
|
||||||
APP_PORT=3000
|
|
||||||
|
|
||||||
# Reiniciar
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Aumentar número de workers
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# En .env
|
|
||||||
WORKER_REPLICAS=5
|
|
||||||
|
|
||||||
# En docker-compose.yml, en la sección app > environment
|
|
||||||
# Ajustar numprocs en supervisor/programs.conf
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Habilitar herramientas de desarrollo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# PHPMyAdmin en http://localhost:8081
|
|
||||||
# Redis Commander en http://localhost:8082
|
|
||||||
docker-compose --profile dev up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Estructura de Contenedores
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ app (PHP + Nginx + Workers) │
|
|
||||||
│ - Puerto: 8080 → 80 │
|
|
||||||
│ - Supervisor: Nginx + PHP-FPM + 3 Workers│
|
|
||||||
│ - Health check: /health.php │
|
|
||||||
└─────────┬───────────────────────────────┘
|
|
||||||
│
|
|
||||||
├─────────┐
|
|
||||||
│ │
|
|
||||||
┌─────────▼──┐ ┌───▼────────┐
|
|
||||||
│ db │ │ redis │
|
|
||||||
│ MySQL 8.0 │ │ Redis 7 │
|
|
||||||
│ Puerto:3306│ │ Puerto:6379│
|
|
||||||
└────────────┘ └────────────┘
|
|
||||||
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ worker (opcional) │
|
|
||||||
│ 2 workers extra │
|
|
||||||
└─────────────────────┘
|
|
||||||
|
|
||||||
┌──────────────────────────┐
|
|
||||||
│ phpmyadmin (opcional) │
|
|
||||||
│ Puerto: 8081 │
|
|
||||||
└──────────────────────────┘
|
|
||||||
|
|
||||||
┌──────────────────────────┐
|
|
||||||
│ redis-commander (opcional)│
|
|
||||||
│ Puerto: 8082 │
|
|
||||||
└──────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Monitoreo
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8080/health.php
|
|
||||||
```
|
|
||||||
|
|
||||||
Respuesta:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "healthy",
|
|
||||||
"timestamp": "2026-01-27 10:30:00",
|
|
||||||
"checks": {
|
|
||||||
"php": {"status": "ok", "version": "8.2.15"},
|
|
||||||
"database": {"status": "ok", "host": "db"},
|
|
||||||
"redis": {"status": "ok", "host": "redis"},
|
|
||||||
"logs": {"status": "ok", "writable": true},
|
|
||||||
"uploads": {"status": "ok", "writable": true},
|
|
||||||
"queue": {
|
|
||||||
"status": "ok",
|
|
||||||
"stats": {
|
|
||||||
"messages": {"pending": 0, "delayed": 0, "failed": 0},
|
|
||||||
"media": {"pending": 0, "delayed": 0, "failed": 0}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Estado de Workers
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose exec app supervisorctl status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Estadísticas de Redis
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose exec redis redis-cli INFO stats
|
|
||||||
```
|
|
||||||
|
|
||||||
### Uso de recursos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker stats
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Actualizar Webhook en WhatsApp
|
|
||||||
|
|
||||||
Una vez desplegado, actualiza la URL del webhook en WhatsApp Business Dashboard:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://tudominio.com/api/webhook_optimized.php
|
|
||||||
```
|
|
||||||
|
|
||||||
Para desarrollo local con ngrok:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ngrok http 8080
|
|
||||||
|
|
||||||
# Usar la URL generada:
|
|
||||||
# https://xxxxx.ngrok.io/api/webhook_optimized.php
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### ❌ Puerto 8080 ya está en uso
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Cambiar puerto en .env
|
|
||||||
APP_PORT=3000
|
|
||||||
|
|
||||||
# O detener servicio que usa 8080
|
|
||||||
sudo lsof -ti:8080 | xargs kill -9
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Error de permisos en logs/uploads
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose exec app chown -R www:www /var/www/html/logs
|
|
||||||
docker-compose exec app chown -R www:www /var/www/html/uploads
|
|
||||||
docker-compose exec app chmod -R 755 /var/www/html/logs
|
|
||||||
docker-compose exec app chmod -R 755 /var/www/html/uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Workers no procesan mensajes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver logs de workers
|
|
||||||
docker-compose logs worker
|
|
||||||
|
|
||||||
# Reiniciar workers
|
|
||||||
docker-compose exec app supervisorctl restart whatsapp-worker:*
|
|
||||||
|
|
||||||
# Ver estado
|
|
||||||
docker-compose exec app supervisorctl status
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Redis no conecta
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar que Redis está corriendo
|
|
||||||
docker-compose ps redis
|
|
||||||
|
|
||||||
# Ver logs
|
|
||||||
docker-compose logs redis
|
|
||||||
|
|
||||||
# Reiniciar
|
|
||||||
docker-compose restart redis
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ MySQL no inicia
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver logs
|
|
||||||
docker-compose logs db
|
|
||||||
|
|
||||||
# Eliminar volumen y recrear (⚠️ borra datos)
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ "Connection refused" al webhook
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar que Nginx está corriendo
|
|
||||||
docker-compose exec app ps aux | grep nginx
|
|
||||||
|
|
||||||
# Reiniciar Nginx
|
|
||||||
docker-compose exec app supervisorctl restart nginx
|
|
||||||
|
|
||||||
# Ver logs de Nginx
|
|
||||||
docker-compose exec app tail -f /var/log/nginx/error.log
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚢 Deployment en Producción
|
|
||||||
|
|
||||||
### 1. Servidor con Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clonar repositorio
|
|
||||||
git clone https://github.com/tu-repo/whatsapp-bot.git
|
|
||||||
cd whatsapp-bot
|
|
||||||
|
|
||||||
# Configurar .env
|
|
||||||
cp .env.docker .env
|
|
||||||
nano .env
|
|
||||||
|
|
||||||
# Construir y ejecutar
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Ver logs
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Con Docker Swarm (alta disponibilidad)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Inicializar swarm
|
|
||||||
docker swarm init
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
docker stack deploy -c docker-compose.yml whatsapp
|
|
||||||
|
|
||||||
# Ver servicios
|
|
||||||
docker service ls
|
|
||||||
|
|
||||||
# Escalar workers
|
|
||||||
docker service scale whatsapp_worker=5
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Con Kubernetes (opcional)
|
|
||||||
|
|
||||||
Archivos de configuración disponibles en `/k8s` (crear según necesidad).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Performance
|
|
||||||
|
|
||||||
### Configuración optimizada para producción
|
|
||||||
|
|
||||||
La configuración Docker incluye:
|
|
||||||
|
|
||||||
- ✅ **OPcache** activado (PHP)
|
|
||||||
- ✅ **FastCGI cache** en Nginx
|
|
||||||
- ✅ **Redis** para sesiones PHP
|
|
||||||
- ✅ **Connection pooling** MySQL
|
|
||||||
- ✅ **Gzip** compresión
|
|
||||||
- ✅ **pm.dynamic** para PHP-FPM (auto-scaling)
|
|
||||||
|
|
||||||
### Benchmarks
|
|
||||||
|
|
||||||
| Métrica | Valor |
|
|
||||||
|---------|-------|
|
|
||||||
| Respuesta webhook | 50-100ms |
|
|
||||||
| Throughput | 80 msg/s |
|
|
||||||
| Memoria por worker | 30-50 MB |
|
|
||||||
| CPU por worker | 5-10% |
|
|
||||||
| Uptime | 99.9% |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Actualización
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull últimos cambios
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Rebuild contenedores
|
|
||||||
docker-compose build --no-cache
|
|
||||||
|
|
||||||
# Reiniciar con nuevas imágenes
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Ver logs para verificar
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Próximos Pasos
|
|
||||||
|
|
||||||
1. ✅ Configurar SSL/HTTPS (usar reverse proxy como Traefik o Caddy)
|
|
||||||
2. ✅ Configurar backups automáticos de MySQL
|
|
||||||
3. ✅ Implementar monitoreo con Prometheus + Grafana
|
|
||||||
4. ✅ Configurar alertas (email/Slack) para fallos críticos
|
|
||||||
5. ✅ Implementar CI/CD con GitHub Actions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Documentación Adicional
|
|
||||||
|
|
||||||
- [Arquitectura Completa](ARQUITECTURA_MEJORADA.md)
|
|
||||||
- [Documentación Original](README.md)
|
|
||||||
- [WhatsApp API Docs](https://developers.facebook.com/docs/whatsapp)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🆘 Soporte
|
|
||||||
|
|
||||||
Si encuentras problemas:
|
|
||||||
|
|
||||||
1. Revisar logs: `docker-compose logs -f`
|
|
||||||
2. Verificar health: `curl http://localhost:8080/health.php`
|
|
||||||
3. Ejecutar tests: `docker-compose exec app php test_architecture.php`
|
|
||||||
4. Consultar troubleshooting arriba
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**¡Todo listo para producción!** 🚀
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
# Dockerfile para contenedor de mantenimiento
|
||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
# Instalar herramientas necesarias incluyendo PHP
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
php82 \
|
||||||
|
php82-pdo \
|
||||||
|
php82-pdo_mysql \
|
||||||
|
php82-mysqli \
|
||||||
|
php82-curl \
|
||||||
|
php82-json \
|
||||||
|
php82-mbstring \
|
||||||
|
php82-openssl \
|
||||||
|
php82-redis \
|
||||||
|
mariadb-client \
|
||||||
|
curl \
|
||||||
|
tzdata \
|
||||||
|
dcron \
|
||||||
|
logrotate \
|
||||||
|
gzip && \
|
||||||
|
ln -sf /usr/bin/php82 /usr/bin/php
|
||||||
|
|
||||||
|
# Configurar zona horaria
|
||||||
|
ENV TZ=America/Bogota
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
# Crear directorios de trabajo
|
||||||
|
RUN mkdir -p /backups /scripts /var/log/cron
|
||||||
|
|
||||||
|
# Copiar scripts de mantenimiento
|
||||||
|
COPY docker/maintenance/scripts/ /scripts/
|
||||||
|
RUN chmod +x /scripts/*.sh
|
||||||
|
|
||||||
|
# Configurar crontab
|
||||||
|
COPY docker/maintenance/crontab /etc/crontabs/root
|
||||||
|
|
||||||
|
# Script de inicio
|
||||||
|
COPY docker/maintenance/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
WORKDIR /scripts
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
# 🚨 EMERGENCIA: "Table doesn't exist" - SOLUCIÓN INMEDIATA
|
|
||||||
|
|
||||||
## ⚠️ Tu Error Específico
|
|
||||||
```
|
|
||||||
❌ Table 'usite_whatsapp_bot.menus' doesn't exist
|
|
||||||
❌ Table 'usite_whatsapp_bot.users' doesn't exist
|
|
||||||
⚠️ 0 tablas, 0 elementos
|
|
||||||
```
|
|
||||||
|
|
||||||
**CAUSA:** Las tablas NO se están creando. Todos los instaladores anteriores fallan en tu hosting.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 SOLUCIÓN DEFINITIVA
|
|
||||||
|
|
||||||
### 🎯 **OPCIÓN 1: Instalador Ultra Básico (GARANTIZADO)**
|
|
||||||
**👉 [install_ultra_basic.php](install_ultra_basic.php)**
|
|
||||||
|
|
||||||
**¿Por qué funcionará?**
|
|
||||||
- ✅ Crea SOLO 2 tablas esenciales: `users` y `system_config`
|
|
||||||
- ✅ Verifica CADA tabla después de crearla
|
|
||||||
- ✅ NO intenta insertar datos si las tablas fallan
|
|
||||||
- ✅ Proceso paso a paso con verificación completa
|
|
||||||
|
|
||||||
### 🛠️ **OPCIÓN 2: Crear Tablas Manualmente**
|
|
||||||
Si incluso el ultra básico falla, hazlo manual:
|
|
||||||
|
|
||||||
1. **Ve a phpMyAdmin en tu HestiaCP**
|
|
||||||
2. **Selecciona tu BD:** `usite_whatsapp_bot`
|
|
||||||
3. **Ejecuta este SQL:**
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
|
||||||
name VARCHAR(100),
|
|
||||||
status ENUM('active', 'inactive') DEFAULT 'active',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_config (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
config_key VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
config_value TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO system_config (config_key, config_value) VALUES
|
|
||||||
('app_installed', '1'),
|
|
||||||
('install_date', NOW()),
|
|
||||||
('business_name', 'Mi Empresa');
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Luego actualiza config/config.php manualmente:**
|
|
||||||
```php
|
|
||||||
define('DB_HOST', 'localhost');
|
|
||||||
define('DB_NAME', 'usite_whatsapp_bot');
|
|
||||||
define('DB_USER', 'tu_usuario');
|
|
||||||
define('DB_PASS', 'tu_contraseña');
|
|
||||||
define('ADMIN_PASSWORD', password_hash('admin123', PASSWORD_DEFAULT));
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Crear archivo:** `.installation_completed` (contenido cualquiera)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Diferencias de los Instaladores
|
|
||||||
|
|
||||||
| Instalador | Tablas que crea | Tu resultado anterior |
|
|
||||||
|------------|------------------|----------------------|
|
|
||||||
| `install.php` | Intenta crear BD | ❌ No tienes privilegios |
|
|
||||||
| `install_manual.php` | 7+ tablas complejas | ❌ Falla en parsing |
|
|
||||||
| `install_integrated.php` | 7+ tablas, schema interno | ❌ Aún falla creación |
|
|
||||||
| `install_simple.php` | 3 tablas básicas | ❌ Probablemente falla |
|
|
||||||
| **`install_ultra_basic.php`** | **SOLO 2 tablas** | ✅ **DEBERÍA FUNCIONAR** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 ¿Por qué fallan los otros?
|
|
||||||
|
|
||||||
### Problemas identificados en tu hosting:
|
|
||||||
1. **Permisos restrictivos:** Puede que no tengas CREATE TABLE completo
|
|
||||||
2. **Claves foráneas:** Tu MySQL puede rechazar FOREIGN KEY
|
|
||||||
3. **Tipos de datos:** Algunos ENUM o JSON no soportados
|
|
||||||
4. **Transacciones:** Problemas con múltiples operaciones
|
|
||||||
|
|
||||||
### El Ultra Básico evita esto:
|
|
||||||
- ✅ Solo 2 tablas simples, sin dependencias
|
|
||||||
- ✅ Sin claves foráneas complejas
|
|
||||||
- ✅ Sin tipos de datos exóticos
|
|
||||||
- ✅ Verifica cada paso antes de continuar
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 PASOS EXACTOS PARA TI
|
|
||||||
|
|
||||||
### 1. Ir al Instalador Ultra Básico
|
|
||||||
```
|
|
||||||
URL: tudominio.com/install_ultra_basic.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Usar las mismas credenciales
|
|
||||||
- **Host:** localhost
|
|
||||||
- **BD:** usite_whatsapp_bot
|
|
||||||
- **Usuario:** [el que creaste en HestiaCP]
|
|
||||||
- **Contraseña:** [la que configuraste]
|
|
||||||
|
|
||||||
### 3. Si funciona verás:
|
|
||||||
```
|
|
||||||
✅ Conectado a 'usite_whatsapp_bot'
|
|
||||||
✅ Tabla 'users' creada y funcional
|
|
||||||
✅ Tabla 'system_config' creada y funcional
|
|
||||||
✅ Configuración inicial insertada
|
|
||||||
✅ config.php actualizado
|
|
||||||
✅ Verificación final: 0 usuarios, 4 configs
|
|
||||||
🎉 Instalación básica completada
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Después podrás:
|
|
||||||
- Entrar al panel con: `admin` / `[contraseña generada]`
|
|
||||||
- Probar con: `test.php`
|
|
||||||
- Agregar funciones desde el panel
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Si AÚN No Funciona
|
|
||||||
|
|
||||||
### Información para soporte:
|
|
||||||
1. **Resultado exacto del ultra básico**
|
|
||||||
2. **Versión PHP de tu hosting**
|
|
||||||
3. **Versión MySQL de tu hosting**
|
|
||||||
4. **Permisos exactos del usuario de BD**
|
|
||||||
|
|
||||||
### Alternativa extrema:
|
|
||||||
Si ningún instalador funciona, el problema es de permisos de tu hosting. Contacta soporte de HestiaCP para revisar permisos de CREATE TABLE.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Una vez que funcione:
|
|
||||||
|
|
||||||
✅ **Tendrás:** Sistema básico funcional
|
|
||||||
✅ **Podrás:** Acceder al panel de admin
|
|
||||||
✅ **Siguiente:** Configurar WhatsApp desde el panel
|
|
||||||
✅ **Expandir:** Agregar más funciones gradualmente
|
|
||||||
|
|
||||||
**👉 [IR AL INSTALADOR ULTRA BÁSICO AHORA](install_ultra_basic.php)** 🚀
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
## ✅ Problema del Error 500 Corregido
|
|
||||||
|
|
||||||
### 🐛 Causa del Error:
|
|
||||||
Al pegar una imagen desde el portapapeles y enviarla, las peticiones `fetch` a los endpoints de API **NO estaban enviando las cookies de sesión**, causando que:
|
|
||||||
1. `upload_media.php` rechazara la petición → Error 401
|
|
||||||
2. Nginx devolviera error 500 por fallo en PHP
|
|
||||||
|
|
||||||
### 🔧 Solución Aplicada:
|
|
||||||
|
|
||||||
Agregué `credentials: 'same-origin'` a ambos fetch en `sendMediaMessage()`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 1. Upload del archivo
|
|
||||||
const uploadResponse = await fetch('api/upload_media.php', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin', // ✅ Enviar cookies de sesión
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Envío del mensaje
|
|
||||||
const sendResponse = await fetch('api/send_media_message.php', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin', // ✅ Enviar cookies de sesión
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({...})
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📋 Cómo Funciona Ahora:
|
|
||||||
|
|
||||||
1. **Copias una imagen** (Ctrl+C o Cmd+C)
|
|
||||||
2. **Haces click en el input de mensaje**
|
|
||||||
3. **Pegas** (Ctrl+V o Cmd+V)
|
|
||||||
4. ✅ Aparece el preview de la imagen
|
|
||||||
5. ✅ Haces click en "Enviar"
|
|
||||||
6. ✅ Se sube el archivo con autenticación correcta
|
|
||||||
7. ✅ Se envía por WhatsApp
|
|
||||||
8. ✅ Aparece en la conversación
|
|
||||||
|
|
||||||
### 🧪 Probar Ahora:
|
|
||||||
|
|
||||||
1. Recarga la página: http://localhost:8080/conversations.php
|
|
||||||
2. Abre una conversación
|
|
||||||
3. Copia una imagen desde cualquier lugar
|
|
||||||
4. Haz click en el input de texto
|
|
||||||
5. Presiona Ctrl+V (o Cmd+V en Mac)
|
|
||||||
6. Debe aparecer el preview sin errores
|
|
||||||
7. Haz click en enviar
|
|
||||||
8. Debe enviarse correctamente
|
|
||||||
|
|
||||||
### 📝 Nota Técnica:
|
|
||||||
|
|
||||||
Por defecto, `fetch()` **NO envía cookies** a menos que especifiques:
|
|
||||||
- `credentials: 'same-origin'` → Envía cookies al mismo dominio
|
|
||||||
- `credentials: 'include'` → Envía cookies incluso en CORS
|
|
||||||
|
|
||||||
El endpoint `upload_media.php` usa `requireAuthentication()` que verifica la sesión en cookies, por eso fallaba antes.
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
# Guía para Resolver Error de Plantillas WhatsApp
|
|
||||||
|
|
||||||
## Error: "Template name does not exist in the translation" (#132001)
|
|
||||||
|
|
||||||
### ¿Qué significa este error?
|
|
||||||
Este error indica que WhatsApp no encuentra la plantilla especificada, o hay un problema con su configuración.
|
|
||||||
|
|
||||||
### Causas Comunes:
|
|
||||||
1. La plantilla no existe en WhatsApp Business Manager
|
|
||||||
2. La plantilla no está aprobada por Meta
|
|
||||||
3. El idioma especificado no coincide con el de la plantilla
|
|
||||||
4. El nombre de la plantilla está mal escrito
|
|
||||||
|
|
||||||
## Pasos para Resolverlo
|
|
||||||
|
|
||||||
### 1. Verificar en WhatsApp Business Manager
|
|
||||||
1. Ve a [business.facebook.com](https://business.facebook.com)
|
|
||||||
2. Selecciona tu cuenta de negocio
|
|
||||||
3. Ve a **WhatsApp Manager** → **Plantillas de mensaje**
|
|
||||||
4. Verifica que existe la plantilla `jaspers_market_plain_text_v1`
|
|
||||||
5. Confirma que el estado es **"Aprobada"** (no "Pendiente de revisión")
|
|
||||||
|
|
||||||
### 2. Verificar el Idioma
|
|
||||||
- Si la plantilla está en inglés, usa `language: "en"`
|
|
||||||
- Si está en español, usa `language: "es"`
|
|
||||||
- El idioma debe coincidir EXACTAMENTE
|
|
||||||
|
|
||||||
### 3. Verificar el Nombre
|
|
||||||
- Usa el nombre EXACTO de la plantilla
|
|
||||||
- No uses espacios ni caracteres especiales si no están en el nombre original
|
|
||||||
- Verifica mayúsculas y minúsculas
|
|
||||||
|
|
||||||
### 4. Usar el Diagnóstico del Sistema
|
|
||||||
1. Ve a la pestaña **Configuración**
|
|
||||||
2. Haz clic en **"Ejecutar"** en el panel de Diagnóstico
|
|
||||||
3. Revisa el estado de tus plantillas
|
|
||||||
|
|
||||||
## Ejemplos de Envío Correcto
|
|
||||||
|
|
||||||
### Formato Simple:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"phone": "+1234567890",
|
|
||||||
"type": "template",
|
|
||||||
"template_name": "jaspers_market_plain_text_v1",
|
|
||||||
"language": "en",
|
|
||||||
"parameters": ["Juan", "123456"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Formato WhatsApp Business API:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"messaging_product": "whatsapp",
|
|
||||||
"to": "+1234567890",
|
|
||||||
"type": "template",
|
|
||||||
"template": {
|
|
||||||
"name": "jaspers_market_plain_text_v1",
|
|
||||||
"language": {
|
|
||||||
"code": "en"
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"type": "body",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"text": "Juan"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"text": "123456"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Crear una Nueva Plantilla
|
|
||||||
|
|
||||||
### En WhatsApp Business Manager:
|
|
||||||
1. Ve a **Plantillas de mensaje** → **Crear plantilla**
|
|
||||||
2. Escoge la categoría apropiada (UTILITY, MARKETING, AUTHENTICATION)
|
|
||||||
3. Escribe el contenido usando variables como `{{1}}`, `{{2}}`, etc.
|
|
||||||
4. Envía para aprobación
|
|
||||||
5. Espera la aprobación de Meta (puede tomar 24-48 horas)
|
|
||||||
|
|
||||||
### Ejemplo de Plantilla Simple:
|
|
||||||
```
|
|
||||||
Nombre: mi_plantilla_bienvenida
|
|
||||||
Idioma: Español
|
|
||||||
Categoría: UTILITY
|
|
||||||
Contenido: Hola {{1}}, bienvenido a nuestro servicio. Tu código es: {{2}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Registrarla en el Sistema:
|
|
||||||
1. Ve a la pestaña **Plantillas**
|
|
||||||
2. Haz clic en **Nueva Plantilla**
|
|
||||||
3. Completa los datos:
|
|
||||||
- Nombre: `mi_plantilla_bienvenida`
|
|
||||||
- Plantilla WhatsApp: `mi_plantilla_bienvenida` (igual al nombre)
|
|
||||||
- Idioma: `es`
|
|
||||||
- Categoría: `UTILITY`
|
|
||||||
- Estado: `approved` (solo después de que Meta la apruebe)
|
|
||||||
|
|
||||||
## Herramientas de Debug
|
|
||||||
|
|
||||||
### 1. Verificar Plantillas Disponibles
|
|
||||||
```bash
|
|
||||||
curl -X GET "http://tu-dominio.com/api/check_templates.php"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Ejecutar Diagnóstico Completo
|
|
||||||
```bash
|
|
||||||
curl -X GET "http://tu-dominio.com/api/diagnose_whatsapp.php?debug=1"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Probar Envío con Debug
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://tu-dominio.com/api/send_message.php?debug=1" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"phone": "+1234567890",
|
|
||||||
"type": "template",
|
|
||||||
"template_name": "mi_plantilla_test",
|
|
||||||
"language": "es",
|
|
||||||
"parameters": ["Pedro", "ABC123"]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Estados de Plantillas
|
|
||||||
|
|
||||||
| Estado | Descripción | ¿Se puede usar? |
|
|
||||||
|--------|-------------|-----------------|
|
|
||||||
| `pending` | En revisión por Meta | ❌ No |
|
|
||||||
| `approved` | Aprobada por Meta | ✅ Sí |
|
|
||||||
| `rejected` | Rechazada por Meta | ❌ No |
|
|
||||||
| `disabled` | Deshabilitada | ❌ No |
|
|
||||||
|
|
||||||
## Resolución por Pasos
|
|
||||||
|
|
||||||
### ✅ Lista de Verificación:
|
|
||||||
- [ ] La plantilla existe en WhatsApp Business Manager
|
|
||||||
- [ ] La plantilla está aprobada (estado: "Aprobada")
|
|
||||||
- [ ] El nombre de la plantilla es exacto
|
|
||||||
- [ ] El idioma coincide con la configuración en Meta
|
|
||||||
- [ ] Los parámetros coinciden con las variables de la plantilla
|
|
||||||
- [ ] La configuración de WhatsApp Business API es correcta
|
|
||||||
|
|
||||||
### 🔧 Si Sigues Teniendo Problemas:
|
|
||||||
1. Revisa el Access Token (puede haber expirado)
|
|
||||||
2. Confirma los permisos de la aplicación de Facebook
|
|
||||||
3. Verifica que el Phone Number ID es correcto
|
|
||||||
4. Asegúrate de que el número de WhatsApp Business está verificado
|
|
||||||
|
|
||||||
## Contacto y Soporte
|
|
||||||
|
|
||||||
Si después de seguir estos pasos sigues teniendo problemas:
|
|
||||||
1. Usa el diagnóstico del sistema para obtener un reporte detallado
|
|
||||||
2. Verifica los logs en la pestaña "Logs"
|
|
||||||
3. Comprueba la documentación oficial de WhatsApp Business API
|
|
||||||
4. Considera crear una plantilla de prueba simple para verificar conectividad
|
|
||||||
|
|
||||||
---
|
|
||||||
*Guía actualizada: Enero 2026*
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
# 🛠️ Guía de Instalación en HestiaCP
|
|
||||||
|
|
||||||
**Desarrollado por U-Site.app** - [https://u-site.app](https://u-site.app)
|
|
||||||
|
|
||||||
## ⚠️ Problema Común en Hostings Compartidos
|
|
||||||
|
|
||||||
El instalador automático (`install.php`) **NO FUNCIONA** en hostings compartidos como HestiaCP, cPanel, etc., porque requiere privilegios de administrador para crear bases de datos y usuarios.
|
|
||||||
|
|
||||||
## ✅ Solución: Instalación Manual
|
|
||||||
|
|
||||||
Usa el archivo `install_manual.php` que he creado específicamente para hostings compartidos.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Pasos en HestiaCP
|
|
||||||
|
|
||||||
### 1. Crear Base de Datos
|
|
||||||
1. Entra al panel de HestiaCP
|
|
||||||
2. Ve a **"Databases"** o **"Base de datos"**
|
|
||||||
3. Haz clic en **"Add Database"**
|
|
||||||
4. Configura:
|
|
||||||
- **Database Name:** `whatsapp_bot` (o el nombre que prefieras)
|
|
||||||
- **Database User:** Crea un usuario (ej: `whatsapp_user`)
|
|
||||||
- **Password:** Usa una contraseña segura
|
|
||||||
- **Privileges:** Selecciona **ALL** o todos los permisos
|
|
||||||
|
|
||||||
### 2. Anotar Credenciales
|
|
||||||
Guarda esta información que necesitarás:
|
|
||||||
|
|
||||||
```
|
|
||||||
Host: localhost
|
|
||||||
Puerto: 3306 (generalmente)
|
|
||||||
Base de datos: [el nombre que creaste]
|
|
||||||
Usuario: [el usuario que creaste]
|
|
||||||
Contraseña: [la contraseña que configuraste]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Ejecutar Instalación Manual
|
|
||||||
1. Abre tu navegador web
|
|
||||||
2. Ve a: `https://tudominio.com/ruta-del-bot/install_manual.php`
|
|
||||||
3. Completa el formulario con las credenciales del paso anterior
|
|
||||||
4. Haz clic en **"Configurar Sistema"**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Características del Instalador Manual
|
|
||||||
|
|
||||||
### ✅ Lo que SÍ hace:
|
|
||||||
- ✅ Conecta con tu BD existente
|
|
||||||
- ✅ Crea todas las tablas necesarias
|
|
||||||
- ✅ Inserta datos de configuración inicial
|
|
||||||
- ✅ Genera contraseña de admin aleatoria
|
|
||||||
- ✅ Configura el archivo `config.php`
|
|
||||||
- ✅ Detecta automáticamente la URL del servidor
|
|
||||||
- ✅ Protege contra reinstalaciones accidentales
|
|
||||||
- ✅ Crea archivo de credenciales
|
|
||||||
|
|
||||||
### ❌ Lo que NO necesita:
|
|
||||||
- ❌ Privilegios de root/administrador
|
|
||||||
- ❌ Crear la base de datos (ya la creaste)
|
|
||||||
- ❌ Crear el usuario (ya lo creaste)
|
|
||||||
- ❌ Configuración especial del servidor
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Estructura Típica en HestiaCP
|
|
||||||
|
|
||||||
```
|
|
||||||
/public_html/
|
|
||||||
├── tu-bot/
|
|
||||||
│ ├── install_manual.php ← Usa este archivo
|
|
||||||
│ ├── install.php ← NO uses este (no funciona)
|
|
||||||
│ ├── config/
|
|
||||||
│ ├── api/
|
|
||||||
│ └── ...resto de archivos
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Errores Comunes y Soluciones
|
|
||||||
|
|
||||||
### Error: "se queda cargando"
|
|
||||||
**Problema:** El instalador automático intenta crear BD con privilegios que no tienes.
|
|
||||||
**Solución:** Usa `install_manual.php` en lugar de `install.php`
|
|
||||||
|
|
||||||
### Error: "Access denied for user"
|
|
||||||
**Problema:** Credenciales incorrectas o permisos insuficientes.
|
|
||||||
**Solución:**
|
|
||||||
- Verifica que el usuario tenga ALL PRIVILEGES en la BD
|
|
||||||
- Confirma host, usuario y contraseña
|
|
||||||
- Asegúrate de que la BD existe
|
|
||||||
|
|
||||||
### Error: "Connection refused"
|
|
||||||
**Problema:** Host o puerto incorrectos.
|
|
||||||
**Solución:**
|
|
||||||
- Usa `localhost` como host
|
|
||||||
- Confirma que el puerto sea 3306
|
|
||||||
- Verifica que MySQL esté corriendo
|
|
||||||
|
|
||||||
### Error: "Database doesn't exist"
|
|
||||||
**Problema:** Nombre de BD incorrecto o no existe.
|
|
||||||
**Solución:**
|
|
||||||
- Verifica el nombre exacto en el panel de HestiaCP
|
|
||||||
- Asegúrate de que la BD fue creada correctamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Después de la Instalación
|
|
||||||
|
|
||||||
1. **Guarda las credenciales:** Se generará un archivo `CREDENCIALES_SISTEMA.txt`
|
|
||||||
2. **Prueba el sistema:** Ve a `test.php` para verificar
|
|
||||||
3. **Configura WhatsApp:** Ve a `server_setup.php`
|
|
||||||
4. **Accede al panel:** Ve a `index.php` con usuario `admin`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Soporte Técnico
|
|
||||||
|
|
||||||
- **Email:** support@u-site.app
|
|
||||||
- **Web:** https://u-site.app/support
|
|
||||||
- **Documentación:** Este archivo y los comentarios en el código
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Credenciales por Defecto
|
|
||||||
|
|
||||||
### Base de Datos
|
|
||||||
- **Host:** localhost
|
|
||||||
- **Usuario/Contraseña:** Los que TÚ configuraste en HestiaCP
|
|
||||||
|
|
||||||
### Panel Admin
|
|
||||||
- **Usuario:** admin
|
|
||||||
- **Contraseña:** Se genera automáticamente (se muestra durante instalación)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚡ Comandos Útiles para Debugging
|
|
||||||
|
|
||||||
### Verificar conexión desde terminal:
|
|
||||||
```bash
|
|
||||||
mysql -h localhost -u tu_usuario -p tu_basedatos
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ver tablas después de instalar:
|
|
||||||
```sql
|
|
||||||
USE tu_basedatos;
|
|
||||||
SHOW TABLES;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ver configuración de usuario:
|
|
||||||
```sql
|
|
||||||
SELECT User, Host FROM mysql.user WHERE User='tu_usuario';
|
|
||||||
SHOW GRANTS FOR 'tu_usuario'@'localhost';
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**¡Listo! Con esta guía deberías poder instalar sin problemas en HestiaCP.** 🚀
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
# 🚀 Instalación en HestiaCP - WhatsApp Bot Manager
|
|
||||||
|
|
||||||
## 📦 Archivo para HestiaCP
|
|
||||||
- **Archivo:** `whatsapp_HESTIACP_READY_20260104_104546.zip`
|
|
||||||
- **Optimizado para:** HestiaCP + hosting compartido/VPS
|
|
||||||
- **Tamaño:** ~168KB
|
|
||||||
|
|
||||||
## 🔧 .htaccess Reparado para HestiaCP
|
|
||||||
|
|
||||||
### ✅ Mejoras Implementadas:
|
|
||||||
- **Compatibilidad HestiaCP:** Sintaxis moderna (Require all denied)
|
|
||||||
- **Seguridad avanzada:** Protección contra inyecciones SQL/XSS
|
|
||||||
- **Rendimiento:** Compresión GZIP y cache optimizado
|
|
||||||
- **APIs:** Headers específicos para WhatsApp webhook
|
|
||||||
- **CSP:** Content Security Policy configurado
|
|
||||||
|
|
||||||
### 🛡️ Protecciones de Seguridad:
|
|
||||||
- ✅ Archivos de configuración protegidos
|
|
||||||
- ✅ Archivos .env bloqueados
|
|
||||||
- ✅ Logs inaccesibles desde web
|
|
||||||
- ✅ Tests bloqueados en producción
|
|
||||||
- ✅ Headers de seguridad completos
|
|
||||||
|
|
||||||
## 📋 Instalación en HestiaCP
|
|
||||||
|
|
||||||
### 1. Acceso al Panel
|
|
||||||
```bash
|
|
||||||
1. Acceder a HestiaCP: https://tu-servidor:8083
|
|
||||||
2. Login con credenciales de admin
|
|
||||||
3. Ir a "Web" → Seleccionar dominio
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Subir Archivos
|
|
||||||
```bash
|
|
||||||
1. File Manager → public_html/
|
|
||||||
2. Subir whatsapp_HESTIACP_READY_20260104_104546.zip
|
|
||||||
3. Extraer ZIP en public_html/
|
|
||||||
4. Borrar archivo ZIP después
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configurar Permisos en HestiaCP
|
|
||||||
```bash
|
|
||||||
1. En File Manager, seleccionar carpetas:
|
|
||||||
- logs/ → Permisos 777
|
|
||||||
- uploads/ → Permisos 777
|
|
||||||
- config/ → Permisos 755
|
|
||||||
|
|
||||||
2. O por SSH:
|
|
||||||
chmod 777 public_html/logs/
|
|
||||||
chmod 777 public_html/uploads/
|
|
||||||
chmod 644 public_html/.htaccess
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Crear Base de Datos
|
|
||||||
```bash
|
|
||||||
1. En HestiaCP → "DB"
|
|
||||||
2. Add Database:
|
|
||||||
- Name: whatsapp_bot
|
|
||||||
- User: whatsapp_user
|
|
||||||
- Password: [generar segura]
|
|
||||||
3. Importar database/schema.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Configurar config.php
|
|
||||||
```php
|
|
||||||
// Editar public_html/config/config.php
|
|
||||||
define('DB_HOST', 'localhost');
|
|
||||||
define('DB_NAME', 'usuario_whatsapp_bot'); // prefijo_nombre
|
|
||||||
define('DB_USER', 'usuario_whatsapp_user'); // prefijo_usuario
|
|
||||||
define('DB_PASS', 'tu_password_db');
|
|
||||||
define('DB_PORT', 3306);
|
|
||||||
define('DB_CHARSET', 'utf8mb4');
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Configuración Específica HestiaCP
|
|
||||||
|
|
||||||
### SSL/HTTPS (Recomendado)
|
|
||||||
```bash
|
|
||||||
1. En HestiaCP → "Web" → tu dominio
|
|
||||||
2. "Edit" → SSL → "Generate" (Let's Encrypt)
|
|
||||||
3. Activar "Force SSL redirect"
|
|
||||||
4. En .htaccess descomenta las líneas HTTPS
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP Version
|
|
||||||
```bash
|
|
||||||
1. En HestiaCP → "Web" → Edit domain
|
|
||||||
2. "Advanced options" → PHP Version: 8.1+
|
|
||||||
3. "Save"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Subdominio (Opcional)
|
|
||||||
```bash
|
|
||||||
1. En HestiaCP → "Web" → "Add Web Domain"
|
|
||||||
2. Domain: bot.tudominio.com
|
|
||||||
3. Mismo proceso de instalación
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔑 Acceso al Sistema
|
|
||||||
|
|
||||||
### URLs de Acceso:
|
|
||||||
- **Dashboard:** https://tudominio.com/
|
|
||||||
- **Login:** https://tudominio.com/login.php
|
|
||||||
- **Verificador:** https://tudominio.com/verificar_servidor.php
|
|
||||||
- **Webhook:** https://tudominio.com/api/webhook.php
|
|
||||||
|
|
||||||
### Credenciales:
|
|
||||||
- **Usuario:** admin
|
|
||||||
- **Contraseña:** 2fa904eee46f
|
|
||||||
|
|
||||||
## 🧪 Verificación Post-Instalación
|
|
||||||
|
|
||||||
### 1. Test básico
|
|
||||||
```bash
|
|
||||||
1. Ir a: https://tudominio.com/verificar_servidor.php
|
|
||||||
2. Verificar que todo esté en ✅ verde
|
|
||||||
3. Corregir cualquier ❌ error mostrado
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test de login
|
|
||||||
```bash
|
|
||||||
1. Ir a: https://tudominio.com/login.php
|
|
||||||
2. Ingresar credenciales
|
|
||||||
3. Verificar acceso al dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test de webhook
|
|
||||||
```bash
|
|
||||||
1. Verificar: https://tudominio.com/api/webhook.php
|
|
||||||
2. Debe mostrar: "Webhook endpoint activo"
|
|
||||||
3. Configurar URL en WhatsApp Business API
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Configuración WhatsApp API
|
|
||||||
|
|
||||||
### En el Dashboard:
|
|
||||||
```bash
|
|
||||||
1. Login al sistema
|
|
||||||
2. Ir a "Configuraciones"
|
|
||||||
3. Completar:
|
|
||||||
- WhatsApp Access Token
|
|
||||||
- Phone Number ID
|
|
||||||
- Business Account ID
|
|
||||||
- Webhook Verify Token
|
|
||||||
```
|
|
||||||
|
|
||||||
### En WhatsApp Business API:
|
|
||||||
```bash
|
|
||||||
1. Facebook Developers → App → WhatsApp
|
|
||||||
2. Configuration → Webhook URL:
|
|
||||||
https://tudominio.com/api/webhook.php
|
|
||||||
3. Verify Token: (copiar del dashboard)
|
|
||||||
4. Suscribirse a: conversations, message_status
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 Solución de Problemas HestiaCP
|
|
||||||
|
|
||||||
### Error 500 - Internal Server Error
|
|
||||||
```bash
|
|
||||||
1. Revisar error log en HestiaCP → Statistics → Error Log
|
|
||||||
2. Verificar permisos de archivos/carpetas
|
|
||||||
3. Comprobar sintaxis de .htaccess
|
|
||||||
4. Revisar versión PHP (requiere 7.4+)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Base de datos no conecta
|
|
||||||
```bash
|
|
||||||
1. Verificar credenciales en config/config.php
|
|
||||||
2. Comprobar prefijo de usuario/DB en HestiaCP
|
|
||||||
3. Verificar que DB esté en mismo servidor
|
|
||||||
4. Probar conexión desde File Manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Headers de seguridad
|
|
||||||
```bash
|
|
||||||
1. Si hay conflicto con CSP, editar .htaccess
|
|
||||||
2. Ajustar Content-Security-Policy según necesidades
|
|
||||||
3. Para debugging, comentar temporalmente CSP
|
|
||||||
```
|
|
||||||
|
|
||||||
### Webhook no recibe mensajes
|
|
||||||
```bash
|
|
||||||
1. Verificar URL pública accesible
|
|
||||||
2. Comprobar SSL activo
|
|
||||||
3. Revisar logs en HestiaCP
|
|
||||||
4. Test manual: curl -X POST https://tudominio.com/api/webhook.php
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 Soporte Específico HestiaCP
|
|
||||||
|
|
||||||
**Documentación oficial:** https://docs.hestiacp.com/
|
|
||||||
**Panel de control:** Acceso vía puerto 8083
|
|
||||||
**Logs del sistema:** /var/log/hestia/
|
|
||||||
**PHP logs:** Panel → Statistics → Error Log
|
|
||||||
|
|
||||||
## 🎯 Optimizaciones HestiaCP
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
```bash
|
|
||||||
1. Activar OPcache en PHP system_config
|
|
||||||
2. Configurar Redis/Memcached si disponible
|
|
||||||
3. Optimizar MySQL en HestiaCP system_config
|
|
||||||
4. Usar Cloudflare para CDN
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup
|
|
||||||
```bash
|
|
||||||
1. HestiaCP → Backups → Schedule
|
|
||||||
2. Backup automático diario/semanal
|
|
||||||
3. Incluir: Web, DB, Mail
|
|
||||||
4. Almacenar en storage externo
|
|
||||||
```
|
|
||||||
|
|
||||||
¡Sistema listo para funcionar en HestiaCP! 🚀
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
# 🚀 Configuración SSE para HestiaCP
|
|
||||||
|
|
||||||
## ✅ Pasos para Activar SSE en tu Servidor HestiaCP
|
|
||||||
|
|
||||||
### 1. Verificar que el código esté en el servidor
|
|
||||||
```bash
|
|
||||||
# Subir por FTP/SFTP o Git
|
|
||||||
cd /home/tu-usuario/web/tu-dominio.com/public_html/
|
|
||||||
ls -la api/sse_events.php # Debe existir
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configurar Apache para SSE (si usas Apache)
|
|
||||||
|
|
||||||
**Opción A: Editar .htaccess en la raíz**
|
|
||||||
```apache
|
|
||||||
# Agregar al final de .htaccess
|
|
||||||
<IfModule mod_headers.c>
|
|
||||||
# SSE Headers
|
|
||||||
<FilesMatch "sse_events\.php$">
|
|
||||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
|
||||||
Header set X-Accel-Buffering "no"
|
|
||||||
SetEnv no-gzip 1
|
|
||||||
</FilesMatch>
|
|
||||||
</IfModule>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Opción B: Crear .htaccess específico en /api/**
|
|
||||||
```bash
|
|
||||||
cd /home/tu-usuario/web/tu-dominio.com/public_html/api/
|
|
||||||
nano .htaccess
|
|
||||||
```
|
|
||||||
|
|
||||||
Contenido:
|
|
||||||
```apache
|
|
||||||
<IfModule mod_headers.c>
|
|
||||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
|
||||||
Header set X-Accel-Buffering "no"
|
|
||||||
SetEnv no-gzip 1
|
|
||||||
</IfModule>
|
|
||||||
|
|
||||||
# Prevenir timeout en SSE
|
|
||||||
<IfModule mod_fcgid.c>
|
|
||||||
FcgidIOTimeout 300
|
|
||||||
</IfModule>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Ajustar PHP-FPM (Recomendado)
|
|
||||||
|
|
||||||
Editar configuración PHP-FPM en HestiaCP:
|
|
||||||
|
|
||||||
1. **Panel HestiaCP** → **Web** → **tu dominio** → **Edit**
|
|
||||||
2. **Backend Template**: PHP-FPM
|
|
||||||
3. **PHP Version**: 8.x
|
|
||||||
|
|
||||||
Luego editar el archivo de configuración (vía SSH):
|
|
||||||
```bash
|
|
||||||
# Ubicación típica en HestiaCP
|
|
||||||
sudo nano /etc/php/8.2/fpm/pool.d/tu-usuario.conf
|
|
||||||
|
|
||||||
# Agregar o modificar:
|
|
||||||
request_terminate_timeout = 300s
|
|
||||||
pm.max_requests = 500
|
|
||||||
```
|
|
||||||
|
|
||||||
Reiniciar PHP-FPM:
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart php8.2-fpm
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Verificar Nginx (si usas Nginx)
|
|
||||||
|
|
||||||
Si tu dominio usa Nginx como proxy, editar la configuración:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/nginx/conf.d/domains/tu-dominio.com.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
Agregar dentro del bloque `location ~ \.php$`:
|
|
||||||
```nginx
|
|
||||||
# Configuración especial para SSE
|
|
||||||
location ~ ^/api/sse_events\.php$ {
|
|
||||||
fastcgi_buffering off;
|
|
||||||
proxy_buffering off;
|
|
||||||
|
|
||||||
fastcgi_read_timeout 300s;
|
|
||||||
fastcgi_send_timeout 300s;
|
|
||||||
|
|
||||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
|
||||||
include fastcgi_params;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reiniciar Nginx:
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Probar SSE desde el servidor
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Conectar por SSH a tu servidor
|
|
||||||
ssh usuario@tu-dominio.com
|
|
||||||
|
|
||||||
# Probar SSE
|
|
||||||
curl -N -m 5 'https://tu-dominio.com/api/sse_events.php?token=demo_token'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Respuesta esperada:**
|
|
||||||
```
|
|
||||||
event: connected
|
|
||||||
data: {"timestamp":1706300000,"user_id":"token_12345678","mode":"authenticated","status":"ready"}
|
|
||||||
|
|
||||||
: heartbeat
|
|
||||||
```
|
|
||||||
|
|
||||||
Si ves esto, **SSE está funcionando** ✅
|
|
||||||
|
|
||||||
### 6. Probar desde el navegador
|
|
||||||
|
|
||||||
1. Abrir tu sitio: `https://tu-dominio.com/conversations.php`
|
|
||||||
2. Abrir Consola del navegador (F12 → Console)
|
|
||||||
3. Buscar:
|
|
||||||
```
|
|
||||||
Conectando a SSE para eventos en tiempo real...
|
|
||||||
SSE URL: https://tu-dominio.com/api/sse_events.php?token=...
|
|
||||||
✅ SSE conectado (🔐 autenticado): {...}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Solución de Problemas
|
|
||||||
|
|
||||||
#### Error: "Failed to fetch" o timeout
|
|
||||||
|
|
||||||
**Causa**: Firewall o mod_security bloqueando
|
|
||||||
**Solución**:
|
|
||||||
```bash
|
|
||||||
# Desactivar mod_security para SSE (temporal)
|
|
||||||
sudo a2dismod security2
|
|
||||||
sudo systemctl restart apache2
|
|
||||||
|
|
||||||
# O agregar excepción en .htaccess:
|
|
||||||
<IfModule mod_security2.c>
|
|
||||||
SecRuleEngine Off
|
|
||||||
</IfModule>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error: "EventSource's response has a MIME type"
|
|
||||||
|
|
||||||
**Causa**: Headers incorrectos
|
|
||||||
**Solución**: Verificar que sse_events.php tenga:
|
|
||||||
```php
|
|
||||||
header('Content-Type: text/event-stream');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error: Conexión se cierra cada 30 segundos
|
|
||||||
|
|
||||||
**Causa**: Timeout de proxy
|
|
||||||
**Solución**: Aumentar timeouts en nginx.conf o apache2.conf:
|
|
||||||
```
|
|
||||||
# Apache
|
|
||||||
Timeout 300
|
|
||||||
KeepAliveTimeout 300
|
|
||||||
|
|
||||||
# Nginx
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. Configuración de Seguridad (Producción)
|
|
||||||
|
|
||||||
#### A. Token seguro en vez de demo_token
|
|
||||||
|
|
||||||
Editar `api/sse_events.php`:
|
|
||||||
```php
|
|
||||||
// Generar token en login
|
|
||||||
$_SESSION['sse_token'] = bin2hex(random_bytes(32));
|
|
||||||
|
|
||||||
// Validar token en SSE
|
|
||||||
$token = $_GET['token'] ?? null;
|
|
||||||
if ($token && isset($_SESSION['sse_token']) && $token === $_SESSION['sse_token']) {
|
|
||||||
$authenticated = true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
En `conversations.php`:
|
|
||||||
```javascript
|
|
||||||
// Usar token de sesión
|
|
||||||
const token = '<?php echo $_SESSION['sse_token'] ?? 'demo_token'; ?>';
|
|
||||||
const sseUrl = `${baseUrl}/api/sse_events.php?token=${token}&t=${Date.now()}`;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Restringir acceso por IP (opcional)
|
|
||||||
|
|
||||||
En `.htaccess`:
|
|
||||||
```apache
|
|
||||||
<FilesMatch "sse_events\.php$">
|
|
||||||
# Solo permitir desde localhost o IPs específicas
|
|
||||||
Require ip 127.0.0.1
|
|
||||||
Require ip tu.ip.publica
|
|
||||||
</FilesMatch>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. Monitoreo
|
|
||||||
|
|
||||||
Ver conexiones SSE activas:
|
|
||||||
```bash
|
|
||||||
# Ver procesos PHP activos
|
|
||||||
ps aux | grep sse_events
|
|
||||||
|
|
||||||
# Ver logs de errores
|
|
||||||
tail -f /var/log/php8.2-fpm.log
|
|
||||||
tail -f /var/log/apache2/error.log
|
|
||||||
tail -f /var/log/nginx/error.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10. Performance
|
|
||||||
|
|
||||||
Para muchos usuarios simultáneos:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Aumentar límites PHP-FPM
|
|
||||||
sudo nano /etc/php/8.2/fpm/pool.d/tu-usuario.conf
|
|
||||||
|
|
||||||
pm = dynamic
|
|
||||||
pm.max_children = 50
|
|
||||||
pm.start_servers = 10
|
|
||||||
pm.min_spare_servers = 5
|
|
||||||
pm.max_spare_servers = 20
|
|
||||||
pm.max_requests = 500
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Checklist de Verificación
|
|
||||||
|
|
||||||
- [ ] Archivo `api/sse_events.php` subido al servidor
|
|
||||||
- [ ] `.htaccess` configurado con headers SSE
|
|
||||||
- [ ] PHP-FPM timeout aumentado a 300s
|
|
||||||
- [ ] Nginx/Apache configurado (si aplica)
|
|
||||||
- [ ] Test con curl funciona (muestra "event: connected")
|
|
||||||
- [ ] Test desde navegador funciona (console.log muestra "✅ SSE conectado")
|
|
||||||
- [ ] Token de seguridad configurado (no usar demo_token en producción)
|
|
||||||
- [ ] Firewall/mod_security permite conexiones largas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Soporte HestiaCP
|
|
||||||
|
|
||||||
Si tienes problemas:
|
|
||||||
|
|
||||||
1. **Panel HestiaCP**: https://tu-dominio.com:8083
|
|
||||||
2. **Logs**: Panel → Log Files
|
|
||||||
3. **SSH**: Usuario y contraseña en el panel
|
|
||||||
4. **Documentación**: https://docs.hestiacp.com/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Resultado Esperado
|
|
||||||
|
|
||||||
Una vez configurado correctamente:
|
|
||||||
|
|
||||||
```
|
|
||||||
ANTES (desarrollo local):
|
|
||||||
http://localhost:8000/api/sse_events.php
|
|
||||||
❌ Timeout (servidor PHP built-in no soporta SSE)
|
|
||||||
|
|
||||||
AHORA (producción HestiaCP):
|
|
||||||
https://tu-dominio.com/api/sse_events.php
|
|
||||||
✅ Conectado - eventos en tiempo real < 1 segundo
|
|
||||||
```
|
|
||||||
|
|
||||||
**¡Tu sistema de notificaciones en tiempo real estará funcionando!** 🚀
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
# 🔧 Guía de Solución de Errores SSE
|
|
||||||
|
|
||||||
## Error: "❌ Error en SSE" sin detalles
|
|
||||||
|
|
||||||
### Diagnóstico Rápido
|
|
||||||
|
|
||||||
1. **Revisar la consola del navegador**
|
|
||||||
- Buscar el nuevo log con `readyState: X (STATE_NAME)`
|
|
||||||
- Estados posibles:
|
|
||||||
- `0 (CONNECTING)`: El navegador está intentando conectar
|
|
||||||
- `1 (OPEN)`: Conexión establecida correctamente
|
|
||||||
- `2 (CLOSED)`: Conexión cerrada (error)
|
|
||||||
|
|
||||||
2. **Verificar la pestaña Network**
|
|
||||||
- Buscar la petición a `sse_events.php`
|
|
||||||
- Ver el status code:
|
|
||||||
- `200`: OK (pero puede haber error en el stream)
|
|
||||||
- `401`: No autenticado
|
|
||||||
- `500`: Error del servidor
|
|
||||||
- `502/504`: Error de proxy/timeout
|
|
||||||
|
|
||||||
### Solución por Estado
|
|
||||||
|
|
||||||
#### Estado CLOSED (readyState: 2)
|
|
||||||
|
|
||||||
**Causa común**: El servidor cierra la conexión inmediatamente
|
|
||||||
|
|
||||||
**Solución**:
|
|
||||||
```bash
|
|
||||||
# 1. Verificar logs del servidor
|
|
||||||
tail -f /var/log/apache2/error.log
|
|
||||||
# o
|
|
||||||
tail -f /var/log/nginx/error.log
|
|
||||||
|
|
||||||
# 2. Probar el endpoint directamente
|
|
||||||
curl -N http://localhost:8000/api/sse_events.php?token=demo_token
|
|
||||||
|
|
||||||
# 3. Ejecutar el diagnóstico completo
|
|
||||||
php test_sse_complete.php
|
|
||||||
```
|
|
||||||
|
|
||||||
**Posibles problemas**:
|
|
||||||
- PHP cierra la conexión por timeout → Verificar `set_time_limit(0)` en sse_events.php
|
|
||||||
- Buffer de salida activo → Verificar que no hay `ob_start()` antes
|
|
||||||
- Nginx/Apache cortando la conexión → Configurar proxy buffering off
|
|
||||||
|
|
||||||
#### Estado CONNECTING (readyState: 0)
|
|
||||||
|
|
||||||
**Causa común**: El navegador no puede conectar al servidor
|
|
||||||
|
|
||||||
**Solución**:
|
|
||||||
```bash
|
|
||||||
# 1. Verificar que el servidor está corriendo
|
|
||||||
lsof -i :8000
|
|
||||||
|
|
||||||
# 2. Verificar firewall
|
|
||||||
# macOS
|
|
||||||
sudo pfctl -s rules
|
|
||||||
|
|
||||||
# 3. Probar desde curl
|
|
||||||
curl -I http://localhost:8000/api/sse_events.php?token=demo_token
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuración del Servidor
|
|
||||||
|
|
||||||
#### Apache (.htaccess)
|
|
||||||
```apache
|
|
||||||
# Deshabilitar compresión para SSE
|
|
||||||
<IfModule mod_headers.c>
|
|
||||||
<FilesMatch "sse_events\.php$">
|
|
||||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
|
||||||
Header set X-Accel-Buffering "no"
|
|
||||||
SetEnv no-gzip 1
|
|
||||||
</FilesMatch>
|
|
||||||
</IfModule>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Nginx (nginx.conf)
|
|
||||||
```nginx
|
|
||||||
location /api/sse_events.php {
|
|
||||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
|
||||||
include fastcgi_params;
|
|
||||||
|
|
||||||
# Deshabilitar buffering para SSE
|
|
||||||
fastcgi_buffering off;
|
|
||||||
proxy_buffering off;
|
|
||||||
|
|
||||||
# Timeouts largos
|
|
||||||
fastcgi_read_timeout 300s;
|
|
||||||
fastcgi_send_timeout 300s;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PHP-FPM (www.conf)
|
|
||||||
```ini
|
|
||||||
; Aumentar timeout de ejecución
|
|
||||||
request_terminate_timeout = 300s
|
|
||||||
|
|
||||||
; Permitir más procesos para SSE
|
|
||||||
pm.max_children = 50
|
|
||||||
pm.start_servers = 10
|
|
||||||
pm.min_spare_servers = 5
|
|
||||||
pm.max_spare_servers = 20
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pruebas Manuales
|
|
||||||
|
|
||||||
#### Test 1: Verificar headers
|
|
||||||
```bash
|
|
||||||
curl -I http://localhost:8000/api/sse_events.php?token=demo_token
|
|
||||||
```
|
|
||||||
|
|
||||||
**Esperado**:
|
|
||||||
```
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/event-stream
|
|
||||||
Cache-Control: no-cache, no-store, must-revalidate
|
|
||||||
Connection: keep-alive
|
|
||||||
X-Accel-Buffering: no
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Test 2: Recibir eventos
|
|
||||||
```bash
|
|
||||||
curl -N http://localhost:8000/api/sse_events.php?token=demo_token
|
|
||||||
```
|
|
||||||
|
|
||||||
**Esperado**:
|
|
||||||
```
|
|
||||||
event: connected
|
|
||||||
data: {"timestamp":1706300000,"user_id":"token_12345678","mode":"authenticated"}
|
|
||||||
|
|
||||||
: heartbeat
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Test 3: Diagnóstico completo
|
|
||||||
```bash
|
|
||||||
php test_sse_complete.php
|
|
||||||
```
|
|
||||||
|
|
||||||
**Esperado**:
|
|
||||||
```
|
|
||||||
✅ Headers SSE correctos
|
|
||||||
[1] 📨 Evento: connected
|
|
||||||
📦 Data: {
|
|
||||||
"timestamp": 1706300000,
|
|
||||||
"user_id": "token_12345678",
|
|
||||||
"mode": "authenticated"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Solución de Problemas Comunes
|
|
||||||
|
|
||||||
#### Error: "net::ERR_INCOMPLETE_CHUNKED_ENCODING"
|
|
||||||
|
|
||||||
**Causa**: El servidor termina el stream abruptamente
|
|
||||||
|
|
||||||
**Solución**:
|
|
||||||
1. Verificar que no hay errores PHP en el stream
|
|
||||||
2. Aumentar `max_execution_time` en php.ini
|
|
||||||
3. Verificar que no hay `exit()` o `die()` en el loop SSE
|
|
||||||
|
|
||||||
#### Error: "Failed to fetch"
|
|
||||||
|
|
||||||
**Causa**: CORS o credenciales
|
|
||||||
|
|
||||||
**Solución**:
|
|
||||||
```javascript
|
|
||||||
// En conversations.php, verificar que se usa:
|
|
||||||
this.eventSource = new EventSource(sseUrl, {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
```php
|
|
||||||
// En sse_events.php, agregar headers CORS:
|
|
||||||
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
|
|
||||||
header('Access-Control-Allow-Credentials: true');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error: Connection timeout después de 30s
|
|
||||||
|
|
||||||
**Causa**: Servidor proxy cortando conexión
|
|
||||||
|
|
||||||
**Solución Apache**:
|
|
||||||
```apache
|
|
||||||
Timeout 300
|
|
||||||
KeepAliveTimeout 300
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solución Nginx**:
|
|
||||||
```nginx
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_connect_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging Avanzado
|
|
||||||
|
|
||||||
#### Habilitar logs detallados PHP
|
|
||||||
```php
|
|
||||||
// Agregar al inicio de sse_events.php
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 0); // No mostrar en stream
|
|
||||||
ini_set('log_errors', 1);
|
|
||||||
ini_set('error_log', '/tmp/sse_debug.log');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Monitorear eventos en tiempo real
|
|
||||||
```bash
|
|
||||||
# Terminal 1: Ver logs
|
|
||||||
tail -f /tmp/sse_debug.log
|
|
||||||
|
|
||||||
# Terminal 2: Conectar SSE
|
|
||||||
php test_sse_complete.php
|
|
||||||
|
|
||||||
# Terminal 3: Enviar evento de prueba
|
|
||||||
curl -X POST http://localhost:8000/api/push_event.php \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"event":"test","data":{"message":"Prueba"},"target":"all"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checklist de Verificación
|
|
||||||
|
|
||||||
- [ ] El servidor está corriendo en el puerto esperado
|
|
||||||
- [ ] Los headers SSE están correctos (Content-Type: text/event-stream)
|
|
||||||
- [ ] No hay buffering de salida activo
|
|
||||||
- [ ] El endpoint responde con HTTP 200
|
|
||||||
- [ ] Se recibe el evento 'connected' al conectar
|
|
||||||
- [ ] Los eventos tienen el formato correcto (event: XXX\ndata: {}\n\n)
|
|
||||||
- [ ] El archivo de eventos temporal es escribible (/uploads/)
|
|
||||||
- [ ] Los logs no muestran errores PHP
|
|
||||||
- [ ] El navegador no está bloqueando la conexión (CORS)
|
|
||||||
- [ ] La autenticación está funcionando (sesión o token)
|
|
||||||
|
|
||||||
### Contacto y Soporte
|
|
||||||
|
|
||||||
Si después de seguir esta guía el problema persiste:
|
|
||||||
|
|
||||||
1. Ejecutar `php test_sse_complete.php` y copiar la salida completa
|
|
||||||
2. Verificar `/tmp/sse_debug.log` y copiar los últimos 50 líneas
|
|
||||||
3. Revisar la consola del navegador y copiar todos los logs relacionados con SSE
|
|
||||||
4. Verificar Network tab y copiar headers/response de sse_events.php
|
|
||||||
|
|
||||||
## Mejoras Implementadas
|
|
||||||
|
|
||||||
### 1. Mejor Logging en Cliente (conversations.php)
|
|
||||||
- ✅ Log detallado del readyState con nombre del estado
|
|
||||||
- ✅ Limpieza de timeout antes de reconectar
|
|
||||||
- ✅ Try-catch al cerrar EventSource
|
|
||||||
|
|
||||||
### 2. Mejor Manejo en Servidor (sse_events.php)
|
|
||||||
- ✅ Límite de tiempo de conexión (5 minutos)
|
|
||||||
- ✅ Detección de cliente desconectado
|
|
||||||
- ✅ Logs más detallados con trace completo
|
|
||||||
- ✅ Headers optimizados para diferentes servidores
|
|
||||||
|
|
||||||
### 3. Herramientas de Diagnóstico
|
|
||||||
- ✅ test_sse_complete.php: Prueba completa de conexión
|
|
||||||
- ✅ Parseo de eventos en tiempo real
|
|
||||||
- ✅ Medición de tiempos de conexión
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# 🚀 Instrucciones para Subir al Servidor
|
|
||||||
|
|
||||||
## 📁 Archivo Listo para Subir
|
|
||||||
- **Archivo:** `whatsapp_fixed_20260104_103742.zip`
|
|
||||||
- **Tamaño:** ~164KB
|
|
||||||
- **Estado:** ✅ Todas las rutas corregidas para hosting
|
|
||||||
|
|
||||||
## 🔧 Cambios Realizados
|
|
||||||
|
|
||||||
### ✅ Rutas Corregidas
|
|
||||||
- **Antes:** `require_once '../config/config.php';`
|
|
||||||
- **Después:** `require_once 'config/config.php';`
|
|
||||||
|
|
||||||
**Archivos corregidos:**
|
|
||||||
- ✅ api/webhook.php
|
|
||||||
- ✅ api/send_message.php
|
|
||||||
- ✅ api/send_broadcast.php
|
|
||||||
- ✅ Todas las 17 APIs del sistema
|
|
||||||
- ✅ Sistema de autenticación añadido en todas las APIs
|
|
||||||
|
|
||||||
## 📋 Proceso de Instalación
|
|
||||||
|
|
||||||
### 1. Subir Archivos
|
|
||||||
```bash
|
|
||||||
1. Extraer whatsapp_fixed_20260104_103742.zip
|
|
||||||
2. Subir todos los archivos a public_html/
|
|
||||||
3. Mantener la estructura de carpetas
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configurar Permisos
|
|
||||||
```bash
|
|
||||||
chmod 755 api/
|
|
||||||
chmod 755 config/
|
|
||||||
chmod 755 classes/
|
|
||||||
chmod 755 services/
|
|
||||||
chmod 777 logs/
|
|
||||||
chmod 777 uploads/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configurar Base de Datos
|
|
||||||
```bash
|
|
||||||
1. Importar database/schema.sql
|
|
||||||
2. Editar config/config.php con datos de tu hosting:
|
|
||||||
- DB_HOST
|
|
||||||
- DB_NAME
|
|
||||||
- DB_USER
|
|
||||||
- DB_PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Configurar WhatsApp API
|
|
||||||
```bash
|
|
||||||
1. Obtener tokens de WhatsApp Business API
|
|
||||||
2. Actualizar en config/config.php:
|
|
||||||
- WHATSAPP_TOKEN
|
|
||||||
- WHATSAPP_PHONE_NUMBER_ID
|
|
||||||
- WHATSAPP_API_URL
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔑 Credenciales de Acceso
|
|
||||||
- **Usuario:** admin
|
|
||||||
- **Contraseña:** 2fa904eee46f
|
|
||||||
- **URL Login:** https://tudominio.com/login.php
|
|
||||||
|
|
||||||
## 🧪 Verificar Instalación
|
|
||||||
- **Dashboard:** https://tudominio.com/
|
|
||||||
- **Tests:** https://tudominio.com/run_tests.php
|
|
||||||
- **Webhook:** https://tudominio.com/api/webhook.php
|
|
||||||
|
|
||||||
## 🎯 Características Implementadas
|
|
||||||
- ✅ Sistema de autenticación completo
|
|
||||||
- ✅ Protección contra fuerza bruta
|
|
||||||
- ✅ API segura con verificación de sesión
|
|
||||||
- ✅ Sistema de plantillas WhatsApp
|
|
||||||
- ✅ Envío de mensajes con templates
|
|
||||||
- ✅ Dashboard con analytics
|
|
||||||
- ✅ Suite de tests completa
|
|
||||||
- ✅ Gestión de usuarios y conversaciones
|
|
||||||
|
|
||||||
## 🔒 Seguridad
|
|
||||||
- ✅ Todas las APIs requieren autenticación
|
|
||||||
- ✅ Protección contra ataques de fuerza bruta
|
|
||||||
- ✅ Sesiones seguras con timeouts
|
|
||||||
- ✅ Validación de entrada en formularios
|
|
||||||
- ✅ Headers de seguridad implementados
|
|
||||||
|
|
||||||
## 📞 Soporte
|
|
||||||
El sistema está completamente funcional y probado. Si tienes problemas:
|
|
||||||
1. Verificar permisos de archivos
|
|
||||||
2. Revisar configuración de base de datos
|
|
||||||
3. Comprobar tokens de WhatsApp API
|
|
||||||
4. Ejecutar tests desde run_tests.php
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
# ✅ Notificaciones por SSE - Implementación Completa
|
|
||||||
|
|
||||||
## 📋 Resumen de Cambios
|
|
||||||
|
|
||||||
### 🎯 Objetivo
|
|
||||||
Eliminar el polling constante de notificaciones (cada 7-15 segundos) y reemplazarlo con push en tiempo real mediante SSE.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Archivos Modificados
|
|
||||||
|
|
||||||
### 1. **conversations.php**
|
|
||||||
**Líneas 1058-1065**: Polling de notificaciones reducido
|
|
||||||
```javascript
|
|
||||||
// ANTES: setInterval(() => this.loadNotifications(), 7000);
|
|
||||||
// AHORA: setInterval(() => this.loadNotifications(), 60000);
|
|
||||||
```
|
|
||||||
- ✅ Cambio de 7s → 60s (solo como backup)
|
|
||||||
- ✅ Comentario: "SSE se encargará de las nuevas en tiempo real"
|
|
||||||
|
|
||||||
**Líneas 1112-1124**: Nuevo listener SSE para notificaciones
|
|
||||||
```javascript
|
|
||||||
this.eventSource.addEventListener('notification', (e) => {
|
|
||||||
const notification = JSON.parse(e.data);
|
|
||||||
this.showNotificationToast(notification);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
- ✅ Escucha evento 'notification' de SSE
|
|
||||||
- ✅ Muestra el toast automáticamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. **api/sse_events.php**
|
|
||||||
**Líneas 125-142**: Envío de notificaciones en el loop SSE
|
|
||||||
```php
|
|
||||||
$notifications = $db->fetchAll(
|
|
||||||
"SELECT id, user_id, type, message, data, is_read, created_at
|
|
||||||
FROM notifications
|
|
||||||
WHERE is_read = 0
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 10"
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($notifications as $notification) {
|
|
||||||
sendSSEEvent('notification', $notification);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- ✅ Lee notificaciones no leídas cada 3 segundos
|
|
||||||
- ✅ Las envía por SSE a todos los clientes conectados
|
|
||||||
- ✅ Manejo de errores con try-catch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Archivos Nuevos
|
|
||||||
|
|
||||||
### 3. **classes/NotificationHelper.php** ⭐
|
|
||||||
Helper para crear y enviar notificaciones automáticamente por SSE.
|
|
||||||
|
|
||||||
**Método principal: `create()`**
|
|
||||||
```php
|
|
||||||
NotificationHelper::create(
|
|
||||||
$userId, // ID del usuario
|
|
||||||
'document', // Tipo: message, document, status, urgent, etc.
|
|
||||||
'Usuario subió 3 documentos', // Mensaje
|
|
||||||
['count' => 3] // Datos adicionales (opcional)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Método auxiliar: `pushSSE()`**
|
|
||||||
```php
|
|
||||||
NotificationHelper::pushSSE($notification);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- ✅ Inserta en BD (tabla `notifications`)
|
|
||||||
- ✅ Envía automáticamente por SSE vía `push_event.php`
|
|
||||||
- ✅ Broadcast a todos los operadores conectados
|
|
||||||
- ✅ Fire-and-forget (no bloquea ejecución)
|
|
||||||
- ✅ Manejo de errores con logs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. **examples/notification_helper_usage.php**
|
|
||||||
Ejemplos de uso del helper:
|
|
||||||
- Notificación cuando usuario sube documento
|
|
||||||
- Notificación de mensaje importante
|
|
||||||
- Notificación de cambio de estado
|
|
||||||
- Notificación personalizada
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Comparación Antes vs Ahora
|
|
||||||
|
|
||||||
| Aspecto | ANTES | AHORA | Mejora |
|
|
||||||
|---------|-------|-------|--------|
|
|
||||||
| **Notificaciones** | Cada 7s | Cada 60s (backup) | **88% menos** |
|
|
||||||
| **Conversaciones** | Cada 30s | ❌ ELIMINADO (solo SSE) | **100% menos** |
|
|
||||||
| **Latencia** | 3-7 segundos | < 1 segundo | **~85% más rápido** |
|
|
||||||
| **Carga servidor** | ~10 req/min | ~1 req/min | **90% reducción** |
|
|
||||||
| **Método** | HTTP GET repetido | SSE push | Push en tiempo real |
|
|
||||||
|
|
||||||
### Detalles por Endpoint
|
|
||||||
|
|
||||||
| Endpoint | Frecuencia ANTES | Frecuencia AHORA | Reducción |
|
|
||||||
|----------|------------------|------------------|-----------|
|
|
||||||
| `get_notifications.php` | Cada 7s (~8.5/min) | Cada 60s (1/min) | **88%** |
|
|
||||||
| `get_conversations.php` | Cada 30s (2/min) | ❌ ELIMINADO | **100%** |
|
|
||||||
| `get_messages.php` | Cada 10s (6/min) | Cada 10s (solo activa) | 0% (ya optimizado) |
|
|
||||||
|
|
||||||
**Total del sistema: 90% menos peticiones HTTP** 🚀
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Cómo Usar
|
|
||||||
|
|
||||||
### Crear notificación desde cualquier archivo PHP:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// 1. Incluir el helper
|
|
||||||
require_once __DIR__ . '/classes/NotificationHelper.php';
|
|
||||||
|
|
||||||
// 2. Crear y enviar notificación (automáticamente por SSE)
|
|
||||||
NotificationHelper::create(
|
|
||||||
$userId,
|
|
||||||
'document',
|
|
||||||
'Usuario subió documento importante',
|
|
||||||
['file_name' => 'factura.pdf', 'file_size' => 2048000]
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### El cliente NO necesita hacer nada:
|
|
||||||
- ✅ El evento SSE llega automáticamente
|
|
||||||
- ✅ Se muestra el toast de notificación
|
|
||||||
- ✅ Se reproduce sonido (si está habilitado)
|
|
||||||
- ✅ El usuario puede hacer clic para ver detalles
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Flujo Completo
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Evento ocurre (usuario sube archivo, nuevo mensaje, etc.)
|
|
||||||
↓
|
|
||||||
2. Tu código llama: NotificationHelper::create(...)
|
|
||||||
↓
|
|
||||||
3. Helper inserta en BD (tabla notifications)
|
|
||||||
↓
|
|
||||||
4. Helper envía POST a push_event.php
|
|
||||||
↓
|
|
||||||
5. push_event.php escribe evento en archivo temporal
|
|
||||||
↓
|
|
||||||
6. sse_events.php lee eventos pendientes
|
|
||||||
↓
|
|
||||||
7. sse_events.php envía evento 'notification' al cliente
|
|
||||||
↓
|
|
||||||
8. Cliente recibe evento y muestra toast
|
|
||||||
↓
|
|
||||||
9. Usuario ve notificación en < 1 segundo
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Tipos de Notificaciones Soportados
|
|
||||||
|
|
||||||
| Tipo | Descripción | Uso |
|
|
||||||
|------|-------------|-----|
|
|
||||||
| `message` | Mensaje nuevo o importante | Mensajes con palabras clave |
|
|
||||||
| `document` | Usuario subió documento | Upload de archivos |
|
|
||||||
| `status` | Cambio de estado | Estado del ticket/caso |
|
|
||||||
| `urgent` | Notificación urgente | Mensajes prioritarios |
|
|
||||||
| `info` | Información general | Avisos del sistema |
|
|
||||||
| `warning` | Advertencia | Límites, errores menores |
|
|
||||||
| `success` | Acción exitosa | Confirmaciones |
|
|
||||||
| `custom` | Personalizado | Cualquier otro tipo |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Ejemplos Prácticos
|
|
||||||
|
|
||||||
### Ejemplo 1: Notificar cuando usuario sube documentos
|
|
||||||
```php
|
|
||||||
// En api/upload_media.php
|
|
||||||
if ($uploadSuccess && $fileType === 'document') {
|
|
||||||
require_once __DIR__ . '/../classes/NotificationHelper.php';
|
|
||||||
|
|
||||||
NotificationHelper::create(
|
|
||||||
$userId,
|
|
||||||
'document',
|
|
||||||
"Usuario {$userName} subió: {$fileName}",
|
|
||||||
[
|
|
||||||
'file_name' => $fileName,
|
|
||||||
'file_type' => $fileType,
|
|
||||||
'file_size' => $fileSize
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ejemplo 2: Notificar mensaje con palabra clave
|
|
||||||
```php
|
|
||||||
// En api/webhook.php
|
|
||||||
if (preg_match('/\b(urgente|importante|ayuda)\b/i', $messageText)) {
|
|
||||||
require_once __DIR__ . '/../classes/NotificationHelper.php';
|
|
||||||
|
|
||||||
NotificationHelper::create(
|
|
||||||
$userId,
|
|
||||||
'urgent',
|
|
||||||
"Mensaje urgente de {$userName}",
|
|
||||||
[
|
|
||||||
'message_preview' => substr($messageText, 0, 100),
|
|
||||||
'phone' => $userPhone
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ejemplo 3: Notificar cambio de estado
|
|
||||||
```php
|
|
||||||
// En cualquier script de gestión
|
|
||||||
if ($statusChanged) {
|
|
||||||
require_once __DIR__ . '/classes/NotificationHelper.php';
|
|
||||||
|
|
||||||
NotificationHelper::create(
|
|
||||||
$userId,
|
|
||||||
'status',
|
|
||||||
"Estado cambiado: {$oldStatus} → {$newStatus}",
|
|
||||||
[
|
|
||||||
'old_status' => $oldStatus,
|
|
||||||
'new_status' => $newStatus,
|
|
||||||
'changed_by' => $_SESSION['admin_name'] ?? 'Sistema'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Ventajas de Esta Implementación
|
|
||||||
|
|
||||||
1. **Sin Cambios en Cliente**: El frontend sigue igual, solo agregamos un listener
|
|
||||||
2. **Automático**: Solo llamas a `create()` y todo sucede automáticamente
|
|
||||||
3. **Sin Bloqueos**: Fire-and-forget, no afecta el rendimiento
|
|
||||||
4. **Escalable**: Funciona con 1 o 1000 operadores conectados
|
|
||||||
5. **Fallback**: Si SSE falla, el polling de 60s lo cubre
|
|
||||||
6. **Flexible**: Acepta cualquier tipo y datos personalizados
|
|
||||||
7. **Debugging**: Logs detallados en error_log
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Cómo Probar
|
|
||||||
|
|
||||||
### 1. Verificar que SSE está conectado:
|
|
||||||
```bash
|
|
||||||
# Abrir consola del navegador en conversations.php
|
|
||||||
# Deberías ver: "✅ SSE conectado (🔐 autenticado)"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Crear notificación de prueba:
|
|
||||||
```bash
|
|
||||||
php -r "
|
|
||||||
require_once 'classes/NotificationHelper.php';
|
|
||||||
NotificationHelper::create(1, 'test', 'Prueba de notificación SSE', ['test' => true]);
|
|
||||||
echo 'Notificación enviada\n';
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Verificar en el navegador:
|
|
||||||
- Deberías ver el toast aparecer en < 1 segundo
|
|
||||||
- En la consola: "🔔 Nueva notificación (SSE): ..."
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Impacto en Performance
|
|
||||||
|
|
||||||
### Servidor
|
|
||||||
- **Antes**: ~8-9 peticiones GET /api/get_notifications.php por minuto por cliente
|
|
||||||
- **Ahora**: ~1 petición por minuto (solo backup)
|
|
||||||
- **Reducción**: 88% menos carga HTTP
|
|
||||||
|
|
||||||
### Cliente
|
|
||||||
- **Antes**: Latencia de 3-7 segundos para ver notificación
|
|
||||||
- **Ahora**: Latencia < 1 segundo
|
|
||||||
- **Mejora**: 6x más rápido
|
|
||||||
|
|
||||||
### Base de Datos
|
|
||||||
- **Antes**: Query cada 7s → ~8.5 queries/min
|
|
||||||
- **Ahora**: Query cada 60s (backup) + SSE cada 3s (compartido con otros eventos)
|
|
||||||
- **Reducción**: ~85% menos queries independientes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Seguridad
|
|
||||||
|
|
||||||
- ✅ Solo localhost puede llamar a `push_event.php` (protegido por IP)
|
|
||||||
- ✅ Autenticación por sesión o token en SSE
|
|
||||||
- ✅ Notificaciones solo visibles para operadores autenticados
|
|
||||||
- ✅ Datos sensibles en campo `data` (no en mensaje)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Notificaciones no aparecen:
|
|
||||||
1. Verificar que SSE está conectado (consola del navegador)
|
|
||||||
2. Revisar logs: `tail -f /var/log/apache2/error.log`
|
|
||||||
3. Verificar que `push_event.php` es accesible
|
|
||||||
4. Ejecutar: `php test_sse_complete.php`
|
|
||||||
|
|
||||||
### Notificaciones duplicadas:
|
|
||||||
- Normal si hay múltiples operadores conectados
|
|
||||||
- Cada operador recibe su propia notificación
|
|
||||||
|
|
||||||
### Polling aún muy frecuente:
|
|
||||||
- Verificar `setupNotificationPolling()` en conversations.php
|
|
||||||
- Debe ser 60000ms (60 segundos), no 15000ms
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Referencias
|
|
||||||
|
|
||||||
- [SSE_REALTIME_DOCS.md](SSE_REALTIME_DOCS.md) - Documentación completa del sistema SSE
|
|
||||||
- [GUIA_SOLUCION_SSE.md](GUIA_SOLUCION_SSE.md) - Guía de solución de problemas
|
|
||||||
- [examples/notification_helper_usage.php](examples/notification_helper_usage.php) - Ejemplos de uso
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Resumen Final
|
|
||||||
|
|
||||||
Con esta implementación:
|
|
||||||
- ✅ Eliminamos 88% de las peticiones de notificaciones
|
|
||||||
- ✅ Notificaciones llegan 6x más rápido (< 1s vs 3-7s)
|
|
||||||
- ✅ Sistema más escalable y eficiente
|
|
||||||
- ✅ API simple y fácil de usar: `NotificationHelper::create()`
|
|
||||||
- ✅ Sin cambios en el frontend (solo agregamos listener)
|
|
||||||
- ✅ Fallback automático si SSE falla
|
|
||||||
|
|
||||||
**Total de reducción en peticiones HTTP: ~85% en todo el sistema** 🚀
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
# 🎯 Optimización Completa - Eliminación de Polling
|
|
||||||
|
|
||||||
## 📊 Resumen de Cambios
|
|
||||||
|
|
||||||
### ❌ ANTES: Sistema de Polling Constante
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Cliente (Navegador) │
|
|
||||||
├─────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ⏰ Cada 7 segundos: │
|
|
||||||
│ GET /api/get_notifications.php │
|
|
||||||
│ → 8.5 peticiones/minuto │
|
|
||||||
│ │
|
|
||||||
│ ⏰ Cada 30 segundos: │
|
|
||||||
│ GET /api/get_conversations.php │
|
|
||||||
│ → 2 peticiones/minuto │
|
|
||||||
│ │
|
|
||||||
│ ⏰ Cada 10 segundos (si hay conversación abierta): │
|
|
||||||
│ GET /api/get_user_messages.php (delta) │
|
|
||||||
│ → 6 peticiones/minuto │
|
|
||||||
│ │
|
|
||||||
│ 📊 TOTAL: ~16.5 peticiones/minuto por cliente │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problemas:**
|
|
||||||
- 🔴 Alta latencia (3-7 segundos para ver cambios)
|
|
||||||
- 🔴 Carga innecesaria en servidor
|
|
||||||
- 🔴 Peticiones redundantes (aunque no haya cambios)
|
|
||||||
- 🔴 No escala bien con muchos usuarios
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ✅ AHORA: Sistema SSE Puro (Sin Polling)
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Cliente (Navegador) │
|
|
||||||
├─────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 🔌 SSE Conectado (1 conexión persistente): │
|
|
||||||
│ ← Evento 'new_message' (< 1s) │
|
|
||||||
│ → Recarga mensajes de conversación activa │
|
|
||||||
│ ← Evento 'new_conversation' (< 1s) │
|
|
||||||
│ → Agrega conversación a lista │
|
|
||||||
│ ← Evento 'notification' (< 1s) │
|
|
||||||
│ → Muestra toast de notificación │
|
|
||||||
│ ← Evento 'heartbeat' (cada 30s) │
|
|
||||||
│ → Mantiene conexión viva │
|
|
||||||
│ │
|
|
||||||
│ 🔄 Backup (solo si SSE falla): │
|
|
||||||
│ GET /api/get_notifications.php (cada 60s) │
|
|
||||||
│ → 1 petición/minuto │
|
|
||||||
│ │
|
|
||||||
│ 📊 TOTAL: ~1 petición/minuto por cliente │
|
|
||||||
│ (95% reducción) 🎉 │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ventajas:**
|
|
||||||
- 🟢 Latencia < 1 segundo (10x más rápido)
|
|
||||||
- 🟢 95% menos carga en servidor
|
|
||||||
- 🟢 Solo transmite cuando hay cambios reales
|
|
||||||
- 🟢 Escala perfectamente con muchos usuarios
|
|
||||||
- 🟢 Sin polling = Sin queries innecesarias a BD
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Archivos Modificados
|
|
||||||
|
|
||||||
### 1. `conversations.php`
|
|
||||||
|
|
||||||
#### Cambio 1: Notificaciones (Línea 1058-1065)
|
|
||||||
```javascript
|
|
||||||
// ANTES
|
|
||||||
setupNotificationPolling() {
|
|
||||||
this.loadNotifications();
|
|
||||||
setInterval(() => this.loadNotifications(), 7000); // ❌ Cada 7s
|
|
||||||
}
|
|
||||||
|
|
||||||
// AHORA
|
|
||||||
setupNotificationPolling() {
|
|
||||||
// Carga inicial de notificaciones pendientes
|
|
||||||
// SSE se encargará de las nuevas en tiempo real
|
|
||||||
this.loadNotifications();
|
|
||||||
|
|
||||||
// Backup: verificar cada 60 segundos (solo por si SSE falla)
|
|
||||||
setInterval(() => this.loadNotifications(), 60000); // ✅ Cada 60s
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Cambio 2: Conversaciones (Línea 2132-2138)
|
|
||||||
```javascript
|
|
||||||
// ANTES
|
|
||||||
setupAutoRefresh() {
|
|
||||||
// Actualizar conversaciones cada 30 segundos
|
|
||||||
setInterval(() => {
|
|
||||||
this.loadConversations(); // ❌ Cada 30s
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
// ... más código
|
|
||||||
}
|
|
||||||
|
|
||||||
// AHORA
|
|
||||||
setupAutoRefresh() {
|
|
||||||
// NOTA: La recarga automática de conversaciones fue ELIMINADA
|
|
||||||
// SSE se encarga de actualizar en tiempo real con eventos:
|
|
||||||
// - 'new_message': actualiza la conversación existente
|
|
||||||
// - 'new_conversation': agrega nueva conversación a la lista
|
|
||||||
// Esto elimina el polling constante y reduce la carga en ~90%
|
|
||||||
|
|
||||||
// ✅ Sin setInterval de conversaciones
|
|
||||||
|
|
||||||
// ... resto del código (solo polling de mensajes)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Cambio 3: Listeners SSE (Línea 1094-1125)
|
|
||||||
```javascript
|
|
||||||
// Evento: nuevo mensaje entrante
|
|
||||||
this.eventSource.addEventListener('new_message', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
|
|
||||||
// Actualizar la lista de conversaciones (sin recargar)
|
|
||||||
this.updateConversationInList(data); // ✅ Actualización inteligente
|
|
||||||
|
|
||||||
// Si es la conversación activa, recargar mensajes
|
|
||||||
if (this.currentUserId === data.user_id) {
|
|
||||||
this.loadMessages(this.currentUserId, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playNotificationSound();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Evento: nueva conversación detectada
|
|
||||||
this.eventSource.addEventListener('new_conversation', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
|
|
||||||
// Agregar a la lista (sin recargar todo)
|
|
||||||
this.addConversationToList(data); // ✅ Solo agrega una
|
|
||||||
|
|
||||||
this.playNotificationSound();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Evento: notificación del sistema
|
|
||||||
this.eventSource.addEventListener('notification', (e) => {
|
|
||||||
const notification = JSON.parse(e.data);
|
|
||||||
this.showNotificationToast(notification); // ✅ Push instantáneo
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. `api/sse_events.php`
|
|
||||||
|
|
||||||
#### Cambio: Envío de notificaciones (Línea 125-142)
|
|
||||||
```php
|
|
||||||
// Verificar notificaciones no leídas cada 3 segundos
|
|
||||||
try {
|
|
||||||
$notifications = $db->fetchAll(
|
|
||||||
"SELECT id, user_id, type, message, data, is_read, created_at
|
|
||||||
FROM notifications
|
|
||||||
WHERE is_read = 0
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 10"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($notifications && count($notifications) > 0) {
|
|
||||||
foreach ($notifications as $notification) {
|
|
||||||
sendSSEEvent('notification', $notification); // ✅ Push automático
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('SSE: Error leyendo notificaciones: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. `classes/NotificationHelper.php` (NUEVO)
|
|
||||||
|
|
||||||
```php
|
|
||||||
class NotificationHelper {
|
|
||||||
|
|
||||||
// Crear y enviar notificación automáticamente
|
|
||||||
public static function create($userId, $type, $message, $data = []) {
|
|
||||||
// 1. Insertar en BD
|
|
||||||
$notificationId = $db->insert('notifications', [...]);
|
|
||||||
|
|
||||||
// 2. Enviar por SSE automáticamente
|
|
||||||
self::pushSSE($notification);
|
|
||||||
|
|
||||||
return $notificationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enviar notificación por SSE
|
|
||||||
public static function pushSSE($notification) {
|
|
||||||
// Fire-and-forget a push_event.php
|
|
||||||
$ch = curl_init('http://localhost:8000/api/push_event.php');
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|
||||||
'event' => 'notification',
|
|
||||||
'data' => $notification,
|
|
||||||
'target' => 'all'
|
|
||||||
]));
|
|
||||||
curl_exec($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Uso simple:**
|
|
||||||
```php
|
|
||||||
// En cualquier archivo PHP
|
|
||||||
NotificationHelper::create(
|
|
||||||
$userId,
|
|
||||||
'document',
|
|
||||||
'Usuario subió 3 documentos',
|
|
||||||
['count' => 3]
|
|
||||||
);
|
|
||||||
// ¡Eso es todo! Aparece en el navegador en < 1 segundo
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Impacto Medible
|
|
||||||
|
|
||||||
### Por Endpoint
|
|
||||||
user_messages.php` | 6 | 0 | **100%** ⬇️ |
|
|
||||||
| **TOTAL** | **16.5** | **~1** | **~95%** ⬇️ |
|
|
||||||
|
|
||||||
### Por Usuario
|
|
||||||
|
|
||||||
- **1 usuario conectado**: De 16.5 req/min → ~1 req/min
|
|
||||||
- **10 usuarios**: De 165 req/min → ~10 req/min
|
|
||||||
- **100 usuarios**: De 1650 req/min → ~100 req/min
|
|
||||||
|
|
||||||
**Ahorro de recursos**: 15
|
|
||||||
- **1 usuario conectado**: De 16.5 req/min → 1-2 req/min
|
|
||||||
- **10 usuarios**: De 165 req/min → 10-20 req/min
|
|
||||||
- **100 usuarios**: De 1650 req/min → 100-200 req/min
|
|
||||||
|
|
||||||
**Ahorro de recursos**: 1450 peticiones HTTP menos por minuto con 100 usuarios 🚀
|
|
||||||
|
|
||||||
### Latencia
|
|
||||||
|
|
||||||
| Evento | Antes | Ahora | Mejora |
|
|
||||||
|--------|-------|-------|--------|
|
|
||||||
| **Nueva notificación** | 3-7s | < 1s | **6x más rápido** |
|
|
||||||
| **Nueva conversación** | 15-30s | < 1s | **30x más rápido** |
|
|
||||||
| **Nuevo mensaje** | 5-10s | < 1s | **10x más rápido** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Flujo Completo de un Evento
|
|
||||||
|
|
||||||
### Ejemplo: Usuario sube un documento
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ 1. Usuario sube archivo │
|
|
||||||
│ → POST /api/upload_media.php │
|
|
||||||
└─────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ 2. Script PHP llama a NotificationHelper │
|
|
||||||
│ NotificationHelper::create( │
|
|
||||||
│ $userId, 'document', │
|
|
||||||
│ 'Usuario subió documento.pdf', │
|
|
||||||
│ ['file_name' => 'documento.pdf'] │
|
|
||||||
│ ) │
|
|
||||||
└─────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
├─► Inserta en tabla notifications
|
|
||||||
│
|
|
||||||
└─► POST interno a push_event.php
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ 3. push_event.php escribe evento en archivo temporal │
|
|
||||||
│ /uploads/events_all.json │
|
|
||||||
└─────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ 4. sse_events.php lee el archivo (cada 2-3s) │
|
|
||||||
│ → sendSSEEvent('notification', [...]) │
|
|
||||||
└─────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
│ (Conexión SSE persistente)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ 5. Navegador recibe evento en < 1 segundo │
|
|
||||||
│ eventSource.addEventListener('notification', ...) │
|
|
||||||
│ → showNotificationToast(notification) │
|
|
||||||
└─────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ 6. Usuario ve el toast de notificación │
|
|
||||||
│ 🔔 "Usuario subió documento.pdf" │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
⏱️ Tiempo total: < 1 segundo
|
|
||||||
📉 Peticiones HTTP: 0 (solo la subida inicial)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Checklist de Optimización
|
|
||||||
|
|
||||||
- [x] Notificaciones: 7s → 60s (backup)
|
|
||||||
- [x] Conversaciones: 30s → **ELIMINADO**
|
|
||||||
- [x] Mensajes delta: 10s → **ELIMINADO**
|
|
||||||
- [x] SSE listener para `notification`
|
|
||||||
- [x] SSE listener para `new_message` (recarga mensajes automáticamente)
|
|
||||||
- [x] SSE listener para `new_conversation`
|
|
||||||
- [x] SSE envía notificaciones automáticamente
|
|
||||||
- [x] NotificationHelper creado
|
|
||||||
- [x] Ejemplos de uso documentados
|
|
||||||
- [x] Métodos `updateConversationInList()` y `addConversationToList()` funcionando
|
|
||||||
- [x] Documentación actualizada
|
|
||||||
|
|
||||||
**Todo el polling HTTP ha sido eliminado. Sistema 100% push con SSE.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Resultado Final
|
|
||||||
|
|
||||||
### Antes vs Ahora - Visualización
|
|
||||||
|
|
||||||
```
|
|
||||||
ANTES (Polling):
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
GET notifications ⏰⏰⏰⏰⏰⏰⏰⏰ (cada 7s)
|
|
||||||
GET conversations ⏰ ⏰ ⏰ ⏰ (cada 30s)
|
|
||||||
GET messages ⏰⏰⏰⏰⏰⏰ (cada 10s)
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total: ~16 peticiones/minuto 🔴
|
|
||||||
|
|
||||||
AHORA (SSE + Backup):
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
SSE push 🔌 ────────────────── (persistente)
|
|
||||||
new_message ▶ (cuando ocurre, < 1s)
|
|
||||||
new_conversation ▶ (cuando ocurre, < 1s)
|
|
||||||
notification ▶ (cuando ocurre, < 1s)
|
|
||||||
heartbeat ♥ (cada 30s)
|
|
||||||
|
|
||||||
GET notifications ⏰ (cada 60s, backup)
|
|
||||||
GET messages ⏰⏰⏰⏰⏰⏰ (cada 10s, activa)
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total: ~1-2 peticiones/minuto 🟢
|
|
||||||
|
|
||||||
📊 Reducción: 90%
|
|
||||||
⚡ Velocidad: 6-30x más rápido
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Conclusión
|
|
||||||
|
|
||||||
Con esta optimización completa:
|
|
||||||
|
|
||||||
✅ **95% menos peticiones HTTP** (16.5/min → ~1/min)
|
|
||||||
✅ **10x más rápido** en tiempo de respuesta (< 1s vs 3-30s)
|
|
||||||
✅ **Escalabilidad mejorada** exponencialmente
|
|
||||||
✅ **Mejor experiencia de usuario** (tiempo real verdadero)
|
|
||||||
✅ **Menor carga en servidor** (CPU, RAM, BD)
|
|
||||||
✅ **Menor consumo de batería** en dispositivos móviles
|
|
||||||
✅ **API simple** para crear notificaciones: `NotificationHelper::create()`
|
|
||||||
✅ **Sin polling** = Solo eventos cuando hay cambios reales
|
|
||||||
|
|
||||||
### Sistema Verdaderamente En Tiempo Real
|
|
||||||
|
|
||||||
**Antes**: Polling cada X segundos (cliente pregunta: "¿hay algo nuevo?")
|
|
||||||
**Ahora**: SSE push instantáneo (servidor dice: "¡ESTO es nuevo!")
|
|
||||||
|
|
||||||
**El sistema ahora es verdaderamente en tiempo real con 0 polling innecesario.** 🚀
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
# WhatsApp Bot Manager 🚀
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="https://img.shields.io/badge/PHP-8.2+-777BB4?style=for-the-badge&logo=php&logoColor=white" alt="PHP 8.2+">
|
|
||||||
<img src="https://img.shields.io/badge/MariaDB-10.11-003545?style=for-the-badge&logo=mariadb&logoColor=white" alt="MariaDB">
|
|
||||||
<img src="https://img.shields.io/badge/Docker-Ready-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker Ready">
|
|
||||||
<img src="https://img.shields.io/badge/WhatsApp-API-25D366?style=for-the-badge&logo=whatsapp&logoColor=white" alt="WhatsApp API">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<strong>Panel administrativo para gestionar bots y envíos de WhatsApp Business</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
Desarrollado con 💚 por <a href="https://u-site.app"><strong>U-Site.app</strong></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏢 Sobre U-Site.app
|
|
||||||
|
|
||||||
**U-Site.app** es una casa de desarrollo de software especializada en soluciones empresariales, automatización y aplicaciones web modernas. Creamos herramientas que simplifican procesos y potencian negocios.
|
|
||||||
|
|
||||||
🌐 **Website:** [u-site.app](https://u-site.app)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Características
|
|
||||||
|
|
||||||
- 📱 **Gestión de conversaciones** - Interfaz tipo WhatsApp Web
|
|
||||||
- 📨 **Plantillas de mensajes** - Envíos masivos con templates aprobados
|
|
||||||
- 🤖 **Bot automatizado** - Respuestas automáticas configurables
|
|
||||||
- 🔐 **Autenticación segura** - Protección contra fuerza bruta
|
|
||||||
- 📎 **Multimedia** - Soporte para imágenes, documentos, audio y video
|
|
||||||
- 🔔 **Notificaciones SSE** - Tiempo real sin polling
|
|
||||||
- 🐳 **Docker Ready** - Despliegue en contenedores
|
|
||||||
- 🇨🇴 **Zona horaria Colombia** - Configurado para América/Bogotá
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Requisitos
|
|
||||||
|
|
||||||
| Requisito | Versión |
|
|
||||||
|-----------|---------|
|
|
||||||
| PHP | 8.0+ |
|
|
||||||
| MariaDB/MySQL | 10.11+ / 8.0+ |
|
|
||||||
| Redis | 7.0+ |
|
|
||||||
| Docker | 24.0+ (opcional) |
|
|
||||||
|
|
||||||
**Extensiones PHP requeridas:** PDO, MySQL, mbstring, curl, openssl, json, gd
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Instalación
|
|
||||||
|
|
||||||
### Opción 1: Docker (Recomendado)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clonar repositorio
|
|
||||||
git clone https://github.com/tu-usuario/whatsapp.git
|
|
||||||
cd whatsapp
|
|
||||||
|
|
||||||
# Desarrollo local
|
|
||||||
docker-compose -f docker-compose.dev.yml up -d
|
|
||||||
|
|
||||||
# Producción
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
**URLs de acceso (desarrollo):**
|
|
||||||
- 🌐 App: http://localhost:8080
|
|
||||||
- 🗄️ phpMyAdmin: http://localhost:8084
|
|
||||||
|
|
||||||
### Opción 2: Instalación Manual
|
|
||||||
|
|
||||||
1. Clonar y configurar:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/tu-usuario/whatsapp.git
|
|
||||||
cd whatsapp
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Editar `.env` con tus credenciales
|
|
||||||
|
|
||||||
3. Ejecutar instalador: `http://tu-servidor/install.php`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Estructura del Proyecto
|
|
||||||
|
|
||||||
```
|
|
||||||
whatsapp/
|
|
||||||
├── api/ # Endpoints API REST
|
|
||||||
├── assets/ # CSS, JS, imágenes
|
|
||||||
├── classes/ # Clases PHP (Database, Services)
|
|
||||||
├── config/ # Configuración
|
|
||||||
├── database/ # Scripts SQL y backups
|
|
||||||
├── docker/ # Configuración Docker (nginx, php, supervisor)
|
|
||||||
├── services/ # Servicios (WhatsApp, Bot)
|
|
||||||
├── uploads/ # Archivos subidos
|
|
||||||
├── Dockerfile # Imagen principal
|
|
||||||
├── Dockerfile.worker # Imagen para workers
|
|
||||||
├── docker-compose.dev.yml # Desarrollo
|
|
||||||
├── docker-compose.prod.yml # Producción
|
|
||||||
└── deploy-vps.sh # Script de despliegue
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Configuración
|
|
||||||
|
|
||||||
### Variables de Entorno (.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Base de datos
|
|
||||||
DB_HOST=mysql
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_NAME=usite_whatsapp_bot
|
|
||||||
DB_USER=tu_usuario
|
|
||||||
DB_PASS=tu_password
|
|
||||||
|
|
||||||
# WhatsApp Business API
|
|
||||||
WHATSAPP_TOKEN=tu_token_aqui
|
|
||||||
WHATSAPP_PHONE_NUMBER_ID=tu_phone_id
|
|
||||||
WEBHOOK_VERIFY_TOKEN=tu_webhook_token
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
```
|
|
||||||
|
|
||||||
### Webhook de WhatsApp
|
|
||||||
|
|
||||||
Configura el webhook en Meta Developer Console:
|
|
||||||
- **URL:** `https://tu-dominio.com/api/webhook.php`
|
|
||||||
- **Verify Token:** El mismo que `WEBHOOK_VERIFY_TOKEN`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐳 Contenedores Docker
|
|
||||||
|
|
||||||
| Contenedor | Descripción | Puerto |
|
|
||||||
|------------|-------------|--------|
|
|
||||||
| `whatsapp-app` | PHP + Nginx | 80, 443 |
|
|
||||||
| `whatsapp-mysql` | MariaDB 10.11 | 3306 |
|
|
||||||
| `whatsapp-redis` | Redis 7 | 6379 |
|
|
||||||
| `whatsapp-worker` | Procesos background | - |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 Documentación Adicional
|
|
||||||
|
|
||||||
- [Guía de Instalación](INSTALACION.md)
|
|
||||||
- [Guía de Despliegue](DEPLOYMENT_GUIDE.md)
|
|
||||||
- [Configuración SSE](README_SSE.md)
|
|
||||||
- [Solución de Problemas](SOLUCION_HOSTING.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔒 Seguridad
|
|
||||||
|
|
||||||
- Los directorios `dev/`, `tests/`, `scripts/` contienen herramientas de desarrollo
|
|
||||||
- **NO exponer** estos directorios en producción
|
|
||||||
- Proteger con `.htaccess` o configuración de nginx
|
|
||||||
- Rotar credenciales regularmente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Licencia
|
|
||||||
|
|
||||||
Propietario - © 2026 [U-Site.app](https://u-site.app)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<strong>Desarrollado con 💚 por <a href="https://u-site.app">U-site.app</a></strong>
|
|
||||||
</p>
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
# 🏥 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: `conversations`, `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.
|
|
||||||
-230
@@ -1,230 +0,0 @@
|
|||||||
# 🚀 Guía de Configuración Completa - WhatsApp Bot Manager
|
|
||||||
|
|
||||||
## ✅ Estado del Sistema
|
|
||||||
|
|
||||||
**Sistema validado y optimizado** - 2 de enero de 2026
|
|
||||||
|
|
||||||
### 🛠️ Mejoras Implementadas
|
|
||||||
|
|
||||||
- ✅ Estructura de archivos validada y completa
|
|
||||||
- ✅ Configuración mejorada con soporte para archivos .env
|
|
||||||
- ✅ Sistema de logging centralizado implementado
|
|
||||||
- ✅ Permisos de archivos configurados correctamente
|
|
||||||
- ✅ Validación completa del sistema implementada
|
|
||||||
- ✅ Protecciones de seguridad añadidas (.htaccess)
|
|
||||||
- ✅ Classes y Services completamente funcionales
|
|
||||||
- ✅ API endpoints validados y operativos
|
|
||||||
- ✅ Interfaz web optimizada
|
|
||||||
- ✅ Sistema de instaladores mejorado
|
|
||||||
|
|
||||||
## 🔧 Configuración Paso a Paso
|
|
||||||
|
|
||||||
### 1. **Configuración de Variables de Entorno (Recomendado)**
|
|
||||||
|
|
||||||
Crea un archivo `.env` en la raíz del proyecto usando `.env.example` como base:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Edita el archivo `.env` con tus valores reales:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Base de datos
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_NAME=tu_base_datos
|
|
||||||
DB_USER=tu_usuario
|
|
||||||
DB_PASS=tu_contraseña
|
|
||||||
|
|
||||||
# WhatsApp Business API
|
|
||||||
WHATSAPP_TOKEN=tu_token_real_de_facebook
|
|
||||||
WHATSAPP_PHONE_NUMBER_ID=tu_phone_number_id_real
|
|
||||||
WEBHOOK_VERIFY_TOKEN=tu_token_secreto_personalizado
|
|
||||||
|
|
||||||
# Configuración general
|
|
||||||
APP_URL=https://tudominio.com
|
|
||||||
TIMEZONE=America/Bogota
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Instalación de Base de Datos**
|
|
||||||
|
|
||||||
Tienes varias opciones según tu tipo de hosting:
|
|
||||||
|
|
||||||
#### **VPS/Servidor Dedicado:**
|
|
||||||
```
|
|
||||||
http://tudominio.com/install.php
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Hosting Compartido (cPanel):**
|
|
||||||
```
|
|
||||||
http://tudominio.com/install_manual.php
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Instalación Básica:**
|
|
||||||
```
|
|
||||||
http://tudominio.com/install_simple.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Validación del Sistema**
|
|
||||||
|
|
||||||
Verifica que todo esté funcionando correctamente:
|
|
||||||
|
|
||||||
```
|
|
||||||
http://tudominio.com/system_validator.php
|
|
||||||
```
|
|
||||||
|
|
||||||
Este validador te mostrará:
|
|
||||||
- ✅ Estado de PHP y extensiones
|
|
||||||
- ✅ Conexión a base de datos
|
|
||||||
- ✅ Estructura de tablas
|
|
||||||
- ✅ Archivos críticos
|
|
||||||
- ✅ Permisos de escritura
|
|
||||||
- ✅ Configuración de WhatsApp
|
|
||||||
- ✅ Estado general del sistema
|
|
||||||
|
|
||||||
### 4. **Configuración de WhatsApp Business API**
|
|
||||||
|
|
||||||
#### **Obtener Token de Acceso:**
|
|
||||||
1. Ve a [Facebook for Developers](https://developers.facebook.com/)
|
|
||||||
2. Crea una aplicación de WhatsApp Business
|
|
||||||
3. Genera un token de acceso permanente
|
|
||||||
4. Obtén tu Phone Number ID
|
|
||||||
|
|
||||||
#### **Configurar Webhook:**
|
|
||||||
1. URL del webhook: `https://tudominio.com/api/webhook.php`
|
|
||||||
2. Token de verificación: (el que configuraste en .env)
|
|
||||||
3. Eventos a suscribirse: `conversations`
|
|
||||||
|
|
||||||
### 5. **Acceso al Panel de Administración**
|
|
||||||
|
|
||||||
```
|
|
||||||
http://tudominio.com/index.php
|
|
||||||
```
|
|
||||||
|
|
||||||
**Credenciales por defecto:**
|
|
||||||
- Usuario: `admin`
|
|
||||||
- Contraseña: (generada automáticamente durante la instalación)
|
|
||||||
|
|
||||||
## 📁 Archivos y Directorios Importantes
|
|
||||||
|
|
||||||
### **Archivos de Configuración:**
|
|
||||||
- `config/config.php` - Configuración principal mejorada
|
|
||||||
- `.env` - Variables de entorno (crear desde .env.example)
|
|
||||||
- `.htaccess` - Protecciones de seguridad
|
|
||||||
|
|
||||||
### **Archivos de Instalación:**
|
|
||||||
- `install.php` - Instalador automático (VPS)
|
|
||||||
- `install_manual.php` - Para hosting compartido
|
|
||||||
- `system_validator.php` - Validador del sistema
|
|
||||||
|
|
||||||
### **Core del Sistema:**
|
|
||||||
- `classes/Database.php` - Conexión a base de datos
|
|
||||||
- `services/BotService.php` - Lógica del chatbot
|
|
||||||
- `services/WhatsAppService.php` - Integración con WhatsApp
|
|
||||||
- `api/webhook.php` - Receptor de mensajes
|
|
||||||
- `api/` - Endpoints de la API REST
|
|
||||||
|
|
||||||
### **Directorios:**
|
|
||||||
- `logs/` - Archivos de log del sistema
|
|
||||||
- `uploads/` - Archivos subidos por usuarios
|
|
||||||
- `assets/` - CSS y JavaScript del frontend
|
|
||||||
|
|
||||||
## 🔒 Seguridad Implementada
|
|
||||||
|
|
||||||
### **Protecciones de Archivos:**
|
|
||||||
- Archivo `.htaccess` con restricciones de acceso
|
|
||||||
- Protección de archivos de configuración
|
|
||||||
- Headers de seguridad HTTP
|
|
||||||
- Protección contra ataques de fuerza bruta
|
|
||||||
|
|
||||||
### **Validaciones:**
|
|
||||||
- Tokens de verificación en webhooks
|
|
||||||
- Sanitización de entradas
|
|
||||||
- Validación de permisos de usuario
|
|
||||||
- Logs de seguridad
|
|
||||||
|
|
||||||
## 🚀 Funcionalidades Listas
|
|
||||||
|
|
||||||
### **Bot Inteligente:**
|
|
||||||
- ✅ Sistema de menús parametrizables
|
|
||||||
- ✅ Respuestas automáticas por keywords
|
|
||||||
- ✅ Navegación por números (1, 2, 3...)
|
|
||||||
- ✅ Comandos especiales (menu, ayuda, salir)
|
|
||||||
- ✅ Seguimiento de estado por usuario
|
|
||||||
|
|
||||||
### **Panel Web:**
|
|
||||||
- ✅ Dashboard con estadísticas en tiempo real
|
|
||||||
- ✅ Gestión de usuarios y conversaciones
|
|
||||||
- ✅ Configuración de menús dinámicos
|
|
||||||
- ✅ Envío de mensajes individuales y masivos
|
|
||||||
- ✅ Sistema de plantillas
|
|
||||||
- ✅ Logs detallados
|
|
||||||
|
|
||||||
### **API REST:**
|
|
||||||
- ✅ Webhook para recibir mensajes
|
|
||||||
- ✅ Endpoints para envío de mensajes
|
|
||||||
- ✅ APIs para estadísticas y gestión
|
|
||||||
- ✅ Integración con sistemas externos
|
|
||||||
|
|
||||||
## 📱 Flujo de Mensajes
|
|
||||||
|
|
||||||
```
|
|
||||||
Usuario → WhatsApp → Facebook API → webhook.php → BotService → WhatsAppService → Usuario
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Comandos de Mantenimiento
|
|
||||||
|
|
||||||
### **Verificar Logs:**
|
|
||||||
```bash
|
|
||||||
tail -f logs/system.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Limpiar Logs:**
|
|
||||||
```bash
|
|
||||||
> logs/system.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Verificar Permisos:**
|
|
||||||
```bash
|
|
||||||
ls -la logs/ uploads/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🆘 Solución de Problemas
|
|
||||||
|
|
||||||
### **Si el webhook no recibe mensajes:**
|
|
||||||
1. Verifica la URL del webhook en Facebook
|
|
||||||
2. Confirma que el token de verificación coincida
|
|
||||||
3. Revisa los logs: `logs/system.log`
|
|
||||||
4. Valida que el SSL esté funcionando
|
|
||||||
|
|
||||||
### **Si hay errores de base de datos:**
|
|
||||||
1. Verifica las credenciales en `.env` o `config.php`
|
|
||||||
2. Confirma que las tablas existan
|
|
||||||
3. Ejecuta el validador del sistema
|
|
||||||
|
|
||||||
### **Si no se pueden escribir logs:**
|
|
||||||
1. Verifica permisos del directorio `logs/`
|
|
||||||
2. Ejecuta: `chmod 755 logs/`
|
|
||||||
|
|
||||||
## 📞 Soporte
|
|
||||||
|
|
||||||
- **Desarrollador:** U-Site.app
|
|
||||||
- **Email:** support@u-site.app
|
|
||||||
- **Documentación:** Archivos .md en el proyecto
|
|
||||||
- **Validador:** system_validator.php
|
|
||||||
|
|
||||||
## 🎉 ¡Listo para Funcionar!
|
|
||||||
|
|
||||||
Tu sistema WhatsApp Bot Manager está **completamente validado y optimizado**.
|
|
||||||
|
|
||||||
**Próximos pasos:**
|
|
||||||
1. Ejecuta `system_validator.php` para confirmar todo está bien
|
|
||||||
2. Configura tus tokens de WhatsApp
|
|
||||||
3. Accede al panel de administración
|
|
||||||
4. ¡Comienza a usar tu chatbot!
|
|
||||||
|
|
||||||
---
|
|
||||||
**Versión del Sistema:** 1.0.0
|
|
||||||
**Fecha de Validación:** 2 de enero de 2026
|
|
||||||
**Estado:** ✅ Sistema Completamente Operativo
|
|
||||||
-206
@@ -1,206 +0,0 @@
|
|||||||
# 🚀 Sistema de Notificaciones en Tiempo Real - Guía Rápida
|
|
||||||
|
|
||||||
## ✨ ¿Qué se implementó?
|
|
||||||
|
|
||||||
Reemplazamos el **polling constante** (peticiones cada 7-8 segundos) por **Server-Sent Events (SSE)**, que empuja notificaciones en tiempo real cuando llega un mensaje de WhatsApp.
|
|
||||||
|
|
||||||
## 📊 Comparación
|
|
||||||
|
|
||||||
| Aspecto | Antes (Polling) | Ahora (SSE) |
|
|
||||||
|---------|----------------|-------------|
|
|
||||||
| **Latencia** | 7-15 segundos | < 1 segundo |
|
|
||||||
| **Peticiones/min** | 15-20 | 2-3 |
|
|
||||||
| **Carga servidor** | Alta | Baja (85% menos) |
|
|
||||||
| **Actualización UI** | Recarga todo | Solo conversación afectada |
|
|
||||||
| **Experiencia** | Lenta | Instantánea |
|
|
||||||
|
|
||||||
## 📁 Archivos nuevos
|
|
||||||
|
|
||||||
```
|
|
||||||
api/
|
|
||||||
├── sse_events.php # Endpoint SSE (conexión persistente)
|
|
||||||
└── push_event.php # API interna para empujar eventos
|
|
||||||
|
|
||||||
SSE_REALTIME_DOCS.md # Documentación completa
|
|
||||||
test_sse.sh # Script de prueba
|
|
||||||
README_SSE.md # Este archivo
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Archivos modificados
|
|
||||||
|
|
||||||
- ✅ `api/webhook.php` - Empuja evento cuando llega mensaje
|
|
||||||
- ✅ `conversations.php` - Se conecta a SSE y actualiza UI incrementalmente
|
|
||||||
|
|
||||||
## 🎯 Cómo funciona
|
|
||||||
|
|
||||||
```
|
|
||||||
1. WhatsApp envía mensaje
|
|
||||||
↓
|
|
||||||
2. webhook.php recibe y guarda en BD
|
|
||||||
↓
|
|
||||||
3. webhook.php empuja evento → push_event.php
|
|
||||||
↓
|
|
||||||
4. push_event.php escribe en archivo temporal
|
|
||||||
↓
|
|
||||||
5. sse_events.php lee archivo y envía al navegador
|
|
||||||
↓
|
|
||||||
6. conversations.php recibe evento y actualiza UI
|
|
||||||
✅ Solo actualiza la conversación específica (NO recarga todo)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Cómo probar
|
|
||||||
|
|
||||||
### 1. Ejecutar script de prueba
|
|
||||||
```bash
|
|
||||||
cd /Users/lizandro/Documents/GitHub/whatsapp
|
|
||||||
./test_sse.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Deberías ver:
|
|
||||||
```
|
|
||||||
🧪 Testing SSE Real-time System
|
|
||||||
✓ SSE endpoint responde
|
|
||||||
✓ Push endpoint funciona
|
|
||||||
✓ Evento enviado exitosamente
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Probar en navegador
|
|
||||||
|
|
||||||
1. Abre `conversations.php`
|
|
||||||
2. Abre **DevTools → Console** (F12)
|
|
||||||
3. Deberías ver:
|
|
||||||
```
|
|
||||||
Conectando a SSE para eventos en tiempo real...
|
|
||||||
✅ SSE conectado: {timestamp: 1738000000}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Enviar mensaje de prueba desde WhatsApp
|
|
||||||
|
|
||||||
Cuando llegue un mensaje:
|
|
||||||
```javascript
|
|
||||||
📨 Nuevo mensaje (SSE): {user_id: 123, message: "Hola", ...}
|
|
||||||
Recargando mensajes de conversación activa...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resultado:** El mensaje aparece en < 1 segundo, sin recargar toda la página.
|
|
||||||
|
|
||||||
## 🔍 Verificar que funciona
|
|
||||||
|
|
||||||
### Método 1: Ver eventos en tiempo real
|
|
||||||
```bash
|
|
||||||
# Terminal 1: Ver eventos guardados
|
|
||||||
watch -n 1 cat /Users/lizandro/Documents/GitHub/whatsapp/uploads/events_global.json
|
|
||||||
|
|
||||||
# Terminal 2: Conectar manualmente a SSE (con token)
|
|
||||||
curl -N http://localhost/api/sse_events.php?token=demo_token
|
|
||||||
```
|
|
||||||
|
|
||||||
### Método 2: Simular mensaje nuevo
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost/api/push_event.php \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-Push-Token: internal_push_secret_2026" \
|
|
||||||
-d '{
|
|
||||||
"event_type": "new_message",
|
|
||||||
"data": {
|
|
||||||
"user_id": 123,
|
|
||||||
"phone_number": "573168950803",
|
|
||||||
"message": "Hola desde terminal",
|
|
||||||
"timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Si tienes `conversations.php` abierto, verás el mensaje aparecer instantáneamente.
|
|
||||||
|
|
||||||
## 🐛 Solución de problemas
|
|
||||||
|
|
||||||
### ❌ "Usuario no autenticado" en SSE
|
|
||||||
|
|
||||||
**Causa:** EventSource no envía cookies de sesión automáticamente en algunos navegadores
|
|
||||||
|
|
||||||
**Solución aplicada:**
|
|
||||||
- ✅ SSE ahora acepta token en URL: `api/sse_events.php?token=demo_token`
|
|
||||||
- ✅ Modo global: Si no hay autenticación específica, usa eventos broadcast
|
|
||||||
- ✅ Fallback automático: El sistema sigue funcionando
|
|
||||||
|
|
||||||
**No requiere acción manual** - Ya está corregido en el código.
|
|
||||||
|
|
||||||
### ❌ "SSE no conecta"
|
|
||||||
|
|
||||||
**Causa:** Sesión no iniciada o permisos
|
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
```bash
|
|
||||||
# Verificar que estás logueado en conversations.php
|
|
||||||
# Verificar permisos del directorio uploads
|
|
||||||
chmod 755 /Users/lizandro/Documents/GitHub/whatsapp/uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ "Eventos no llegan"
|
|
||||||
|
|
||||||
**Causa:** Archivo temporal no se crea
|
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
```bash
|
|
||||||
# Verificar que se pueden crear archivos
|
|
||||||
touch /Users/lizandro/Documents/GitHub/whatsapp/uploads/test.txt
|
|
||||||
# Si falla, ajustar permisos
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ "Push falla desde webhook"
|
|
||||||
|
|
||||||
**Causa:** Token incorrecto o URL no resuelve
|
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
```php
|
|
||||||
// En webhook.php, verificar que la URL sea correcta:
|
|
||||||
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
|
|
||||||
|
|
||||||
// O cambiar a localhost:
|
|
||||||
$pushUrl = 'http://localhost/api/push_event.php';
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Configuración
|
|
||||||
|
|
||||||
### Cambiar token de seguridad
|
|
||||||
```php
|
|
||||||
// En push_event.php y webhook.php
|
|
||||||
$validToken = 'tu_token_super_secreto_aqui';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ajustar frecuencia de revisión
|
|
||||||
```php
|
|
||||||
// En sse_events.php, línea ~95
|
|
||||||
sleep(2); // Cambiar a 1-5 segundos según necesidad
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deshabilitar polling (opcional)
|
|
||||||
```javascript
|
|
||||||
// En conversations.php, comentar:
|
|
||||||
// setInterval(() => this.loadNotifications(), 15000);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Beneficios inmediatos
|
|
||||||
|
|
||||||
1. ✅ **Notificaciones instantáneas** - Mensajes aparecen en < 1 segundo
|
|
||||||
2. ✅ **Menos carga** - 85% menos peticiones HTTP
|
|
||||||
3. ✅ **UI más fluida** - No recarga toda la página
|
|
||||||
4. ✅ **Mejor UX** - Experiencia similar a WhatsApp Web
|
|
||||||
5. ✅ **Compatible** - Si SSE falla, polling sigue funcionando
|
|
||||||
|
|
||||||
## 🎉 ¡Listo!
|
|
||||||
|
|
||||||
El sistema está funcionando. Cada vez que llegue un mensaje de WhatsApp:
|
|
||||||
- Se guarda en BD (como antes)
|
|
||||||
- Se empuja evento en tiempo real
|
|
||||||
- Aparece instantáneamente en `conversations.php`
|
|
||||||
- **Solo se actualiza la conversación específica**
|
|
||||||
|
|
||||||
No más esperas de 7-15 segundos ni recargas completas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Documentación completa:** Ver [SSE_REALTIME_DOCS.md](SSE_REALTIME_DOCS.md)
|
|
||||||
|
|
||||||
**Soporte:** Si algo no funciona, ejecuta `./test_sse.sh` y revisa el output.
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
# 📋 REPORTE COMPLETO DEL SISTEMA WHATSAPP BOT
|
|
||||||
**Fecha de Análisis:** 13 de enero de 2026
|
|
||||||
**Estado:** ✅ SISTEMA COMPLETO Y FUNCIONAL
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 RESUMEN EJECUTIVO
|
|
||||||
|
|
||||||
El sistema WhatsApp Bot está **100% funcional** con todas las características principales implementadas:
|
|
||||||
|
|
||||||
✅ **Autenticación y Seguridad**
|
|
||||||
✅ **Gestión de Usuarios**
|
|
||||||
✅ **Sistema de Mensajería**
|
|
||||||
✅ **Multimedia (Imágenes, Videos, Audios, Documentos)**
|
|
||||||
✅ **Grabación de Audio en Tiempo Real**
|
|
||||||
✅ **Menús Interactivos**
|
|
||||||
✅ **Plantillas de WhatsApp**
|
|
||||||
✅ **Auto-respuestas**
|
|
||||||
✅ **Logs y Diagnósticos**
|
|
||||||
✅ **APIs REST Completas**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 ESTADÍSTICAS DEL PROYECTO
|
|
||||||
|
|
||||||
- **Total archivos PHP:** 130+
|
|
||||||
- **APIs REST:** 42 endpoints
|
|
||||||
- **Clases:** Database.php (Singleton Pattern)
|
|
||||||
- **Servicios:** WhatsAppService (11 métodos de envío)
|
|
||||||
- **Interfaces:** 3 (index.php, conversations.php, chat_window.php)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔒 1. SEGURIDAD Y AUTENTICACIÓN
|
|
||||||
|
|
||||||
### ✅ IMPLEMENTADO:
|
|
||||||
- **Sistema de Login:** `login.php` con protección brute force
|
|
||||||
- **Gestión de Sesiones:** Timeout 30 minutos
|
|
||||||
- **Autenticación de APIs:** `requireAuthentication()` en todos los endpoints
|
|
||||||
- **Migración a BD:** Sistema `admin_users` tabla
|
|
||||||
- **Password Recovery:** `password_recovery.php`
|
|
||||||
- **Gestión Admins:** `gestion_admin_unificada.php`
|
|
||||||
|
|
||||||
### 📁 Archivos Clave:
|
|
||||||
- ✅ `login.php` - Sistema de autenticación
|
|
||||||
- ✅ `logout.php` - Cierre de sesión
|
|
||||||
- ✅ `config/config.php` - Funciones de seguridad
|
|
||||||
- ✅ `auth_protect.php` - Protección de páginas
|
|
||||||
- ✅ `migrate_to_database_auth.php` - Migración
|
|
||||||
|
|
||||||
### 🔑 Credenciales:
|
|
||||||
```
|
|
||||||
Usuario: admin
|
|
||||||
Password: 2fa904eee46f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💬 2. SISTEMA DE MENSAJERÍA
|
|
||||||
|
|
||||||
### ✅ WhatsAppService.php - Métodos Disponibles:
|
|
||||||
|
|
||||||
#### Mensajes de Texto:
|
|
||||||
```php
|
|
||||||
sendTextMessage($to, $message)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Plantillas:
|
|
||||||
```php
|
|
||||||
sendTemplateMessage($to, $templateName, $language, $bodyParameters, $headerParameters)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Mensajes Interactivos:
|
|
||||||
```php
|
|
||||||
sendInteractiveMessage($to, $bodyText, $buttons, $header, $footer)
|
|
||||||
sendListMessage($to, $bodyText, $buttonText, $sections, $header, $footer)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Multimedia:
|
|
||||||
```php
|
|
||||||
sendImageMessage($to, $imageUrl, $caption)
|
|
||||||
sendVideoMessage($to, $videoUrl, $caption)
|
|
||||||
sendAudioMessage($to, $audioUrl)
|
|
||||||
sendDocumentMessage($to, $documentUrl, $filename, $caption)
|
|
||||||
uploadMedia($filePath, $mimeType)
|
|
||||||
downloadMedia($mediaId)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📂 Formatos Soportados:
|
|
||||||
- **Imágenes:** JPG, PNG, WEBP (máx 5MB)
|
|
||||||
- **Videos:** MP4 (máx 16MB)
|
|
||||||
- **Audios:** MP3, AAC, OGG, WEBM (máx 16MB)
|
|
||||||
- **Documentos:** PDF, DOC, DOCX, XLS, XLSX (máx 100MB)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎤 3. FUNCIONALIDAD MULTIMEDIA
|
|
||||||
|
|
||||||
### ✅ Características Implementadas:
|
|
||||||
|
|
||||||
#### En conversations.php:
|
|
||||||
- ✅ Botón adjuntar archivos (📎)
|
|
||||||
- ✅ Preview de multimedia antes de enviar
|
|
||||||
- ✅ Caption opcional para imágenes/videos/documentos
|
|
||||||
- ✅ Visualización inline de multimedia
|
|
||||||
- ✅ Validación de tamaños y tipos
|
|
||||||
|
|
||||||
#### En chat_window.php:
|
|
||||||
- ✅ Todas las características de conversations.php
|
|
||||||
- ✅ **Grabación de audio en tiempo real**
|
|
||||||
- ✅ Botón de micrófono (🎤)
|
|
||||||
- ✅ Indicador visual de grabación
|
|
||||||
- ✅ Timer de grabación
|
|
||||||
- ✅ Preview de audio antes de enviar
|
|
||||||
|
|
||||||
### 📁 APIs Multimedia:
|
|
||||||
- ✅ `api/upload_media.php` - Subir archivos
|
|
||||||
- ✅ `api/send_media_message.php` - Enviar multimedia
|
|
||||||
- ✅ Tabla `media_files` en base de datos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗄️ 4. BASE DE DATOS
|
|
||||||
|
|
||||||
### ✅ Tablas Principales:
|
|
||||||
|
|
||||||
1. **users** - Gestión de usuarios WhatsApp
|
|
||||||
2. **conversations** - Historial de mensajes
|
|
||||||
3. **menus** - Menús interactivos
|
|
||||||
4. **menu_options** - Opciones de menús
|
|
||||||
5. **autoresponses** - Respuestas automáticas
|
|
||||||
6. **templates** - Plantillas de WhatsApp
|
|
||||||
7. **system_config** - Configuración del sistema
|
|
||||||
8. **admin_users** - Administradores del sistema
|
|
||||||
9. **webhook_logs** - Logs de webhook
|
|
||||||
10. **media_files** - Archivos multimedia ⭐ NUEVA
|
|
||||||
|
|
||||||
### 📊 Clase Database:
|
|
||||||
- ✅ Patrón Singleton
|
|
||||||
- ✅ Métodos: `query()`, `fetch()`, `fetchAll()`, `insert()`, `update()`, `delete()`
|
|
||||||
- ✅ Manejo de errores y transacciones
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔌 5. APIs REST (42 ENDPOINTS)
|
|
||||||
|
|
||||||
### 📤 Mensajería:
|
|
||||||
- ✅ `send_message.php` - Enviar texto
|
|
||||||
- ✅ `send_template.php` - Enviar plantilla
|
|
||||||
- ✅ `send_media_message.php` - Enviar multimedia
|
|
||||||
|
|
||||||
### 👥 Usuarios:
|
|
||||||
- ✅ `get_users.php` - Listar usuarios
|
|
||||||
- ✅ `update_user.php` - Actualizar usuario
|
|
||||||
- ✅ `delete_user.php` - Eliminar usuario
|
|
||||||
- ✅ `export_users.php` - Exportar CSV
|
|
||||||
|
|
||||||
### 💬 Conversaciones:
|
|
||||||
- ✅ `get_conversations.php` - Lista de conversaciones
|
|
||||||
- ✅ `get_conversation_list.php` - Lista simplificada
|
|
||||||
- ✅ `get_conversation_detail.php` - Detalle de conversación
|
|
||||||
- ✅ `get_conversation_stats.php` - Estadísticas
|
|
||||||
- ✅ `get_recent_conversations.php` - Mensajes recientes
|
|
||||||
|
|
||||||
### 🍽️ Menús:
|
|
||||||
- ✅ `get_menus.php` - Listar menús
|
|
||||||
- ✅ `save_menu.php` - Guardar menú
|
|
||||||
- ✅ `delete_menu.php` - Eliminar menú
|
|
||||||
- ✅ `save_menu_option.php` - Guardar opción
|
|
||||||
- ✅ `update_menu_option.php` - Actualizar opción
|
|
||||||
- ✅ `delete_menu_option.php` - Eliminar opción
|
|
||||||
|
|
||||||
### 🤖 Auto-respuestas:
|
|
||||||
- ✅ `get_autoresponses.php` - Listar
|
|
||||||
- ✅ `save_autoresponse.php` - Guardar
|
|
||||||
- ✅ `delete_autoresponse.php` - Eliminar
|
|
||||||
|
|
||||||
### 📋 Plantillas:
|
|
||||||
- ✅ `get_templates.php` - Listar plantillas
|
|
||||||
- ✅ `delete_template.php` - Eliminar plantilla
|
|
||||||
|
|
||||||
### 📊 Dashboard:
|
|
||||||
- ✅ `get_stats.php` - Estadísticas generales
|
|
||||||
- ✅ `get_chart_data.php` - Datos para gráficos
|
|
||||||
|
|
||||||
### 📁 Multimedia:
|
|
||||||
- ✅ `upload_media.php` - Subir archivos
|
|
||||||
- ✅ `send_media_message.php` - Enviar multimedia
|
|
||||||
|
|
||||||
### ⚙️ Configuración:
|
|
||||||
- ✅ `get_system_config.php` - Obtener configuración
|
|
||||||
- ✅ `save_system_config.php` - Guardar configuración
|
|
||||||
- ✅ `manage_config.php` - Gestión completa
|
|
||||||
|
|
||||||
### 📝 Logs:
|
|
||||||
- ✅ `get_logs.php` - Obtener logs
|
|
||||||
- ✅ `clear_logs.php` - Limpiar logs
|
|
||||||
|
|
||||||
### 🔧 Webhook:
|
|
||||||
- ✅ `webhook.php` - Recibir mensajes de WhatsApp
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🖥️ 6. INTERFACES DE USUARIO
|
|
||||||
|
|
||||||
### ✅ Panel Principal - index.php
|
|
||||||
**Características:**
|
|
||||||
- Dashboard con estadísticas en tiempo real
|
|
||||||
- Gestión de usuarios
|
|
||||||
- Plantillas de WhatsApp
|
|
||||||
- Menús interactivos
|
|
||||||
- Auto-respuestas
|
|
||||||
- Logs del sistema
|
|
||||||
- Configuración de WhatsApp
|
|
||||||
- Gráficos de actividad
|
|
||||||
|
|
||||||
### ✅ Conversaciones - conversations.php
|
|
||||||
**Características:**
|
|
||||||
- Lista de conversaciones con búsqueda
|
|
||||||
- Chat en tiempo real
|
|
||||||
- Envío de texto y multimedia
|
|
||||||
- Preview de archivos
|
|
||||||
- Caption para multimedia
|
|
||||||
- Visualización inline
|
|
||||||
- Botón adjuntar (📎)
|
|
||||||
|
|
||||||
### ✅ Chat Window - chat_window.php
|
|
||||||
**Características:**
|
|
||||||
- Ventana de chat individual
|
|
||||||
- Todas las funciones de conversations.php
|
|
||||||
- **Grabación de audio (🎤)**
|
|
||||||
- Indicador de grabación con timer
|
|
||||||
- Preview de audio
|
|
||||||
|
|
||||||
### ✅ Sistema de Login - login.php
|
|
||||||
**Características:**
|
|
||||||
- Diseño moderno con gradientes
|
|
||||||
- Protección brute force
|
|
||||||
- Recuperación de contraseña
|
|
||||||
- Mensajes informativos
|
|
||||||
- Badges de seguridad
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 7. HERRAMIENTAS DE DIAGNÓSTICO
|
|
||||||
|
|
||||||
### ✅ Instalación:
|
|
||||||
- `instalacion.html` - Wizard de instalación
|
|
||||||
- `install.php`, `install_simple.php`, `install_ultra_basic.php`
|
|
||||||
- `instalar_bd.php` - Crear tablas
|
|
||||||
|
|
||||||
### ✅ Diagnóstico:
|
|
||||||
- `test_webhook.php` - Verificar webhook (7 tests)
|
|
||||||
- `diagnostico_whatsapp.php` - Estado de WhatsApp API
|
|
||||||
- `diagnostico_api.php` - Verificar APIs
|
|
||||||
- `diagnostico_login.php` - Verificar autenticación
|
|
||||||
- `verificar_sistema.php` - Estado general
|
|
||||||
- `system_validator.php` - Validación completa
|
|
||||||
|
|
||||||
### ✅ Tests:
|
|
||||||
- `test.php` - Suite de tests
|
|
||||||
- `run_tests.php` - Ejecutar todos los tests
|
|
||||||
- `test_api.php` - Tests de API
|
|
||||||
- `test_whatsapp_completo.php` - Tests WhatsApp
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 8. DOCUMENTACIÓN
|
|
||||||
|
|
||||||
### ✅ Guías Disponibles:
|
|
||||||
- `README.md` - Documentación principal
|
|
||||||
- `DEPLOYMENT_GUIDE.md` - Guía de despliegue
|
|
||||||
- `INSTALACION.md` - Guía de instalación
|
|
||||||
- `INSTRUCCIONES_SERVIDOR.md` - Configuración servidor
|
|
||||||
- `GUIA_HESTIACP.md` - Guía para HestiaCP
|
|
||||||
- `SOLUCION_SEGURIDAD.md` - Soluciones de seguridad
|
|
||||||
- `SOLUCION_TABLA_NO_EXISTE.md` - Soluciones BD
|
|
||||||
- `GUIA_ERROR_PLANTILLAS.md` - Troubleshooting plantillas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 9. ÁREAS QUE NECESITAN ATENCIÓN
|
|
||||||
|
|
||||||
### 🟡 RECOMENDACIONES:
|
|
||||||
|
|
||||||
#### 1. **Limpieza de Archivos de Debug**
|
|
||||||
Eliminar antes de producción:
|
|
||||||
- `debug_*.php` (8 archivos)
|
|
||||||
- `diagnostico_*.php` (7 archivos)
|
|
||||||
- `test_*.php` (20+ archivos)
|
|
||||||
- `verificar_*.php` (4 archivos)
|
|
||||||
- `fix_*.php` (4 archivos)
|
|
||||||
|
|
||||||
#### 2. **Archivos de Instalación**
|
|
||||||
Eliminar después de instalar:
|
|
||||||
- `install*.php` (5 archivos)
|
|
||||||
- `instalacion.html`
|
|
||||||
- `gestion_admin_unificada.php` (solo para setup)
|
|
||||||
- `migrate_to_database_auth.php` (ya migrado)
|
|
||||||
|
|
||||||
#### 3. **Seguridad**
|
|
||||||
- ✅ Cambiar credenciales por defecto
|
|
||||||
- ✅ Eliminar modo debug de APIs
|
|
||||||
- ✅ Activar HTTPS en producción
|
|
||||||
- ✅ Revisar permisos de directorios
|
|
||||||
- ✅ Configurar .htaccess para proteger config/
|
|
||||||
|
|
||||||
#### 4. **Optimizaciones**
|
|
||||||
- Agregar cache para configuración
|
|
||||||
- Implementar rate limiting en APIs
|
|
||||||
- Comprimir archivos CSS/JS
|
|
||||||
- Agregar logging más detallado
|
|
||||||
|
|
||||||
#### 5. **Monitoreo**
|
|
||||||
- Sistema de alertas para errores
|
|
||||||
- Dashboard de métricas en tiempo real
|
|
||||||
- Backup automático de BD
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 10. CHECKLIST DE PRODUCCIÓN
|
|
||||||
|
|
||||||
### Antes de Deploy:
|
|
||||||
|
|
||||||
- [ ] Cambiar credenciales de admin
|
|
||||||
- [ ] Actualizar tokens de WhatsApp
|
|
||||||
- [ ] Configurar webhook URL en Facebook
|
|
||||||
- [ ] Eliminar archivos de debug/test
|
|
||||||
- [ ] Configurar HTTPS
|
|
||||||
- [ ] Establecer permisos correctos (755 directorios, 644 archivos)
|
|
||||||
- [ ] Crear .htaccess de seguridad
|
|
||||||
- [ ] Configurar backups automáticos
|
|
||||||
- [ ] Verificar logs de errores
|
|
||||||
- [ ] Probar envío de mensajes
|
|
||||||
- [ ] Probar recepción de mensajes
|
|
||||||
- [ ] Probar multimedia
|
|
||||||
- [ ] Verificar grabación de audio
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 11. CARACTERÍSTICAS DESTACADAS
|
|
||||||
|
|
||||||
### ⭐ LO MEJOR DEL SISTEMA:
|
|
||||||
|
|
||||||
1. **Grabación de Audio en Tiempo Real** 🎤
|
|
||||||
- Primera implementación en un bot de WhatsApp
|
|
||||||
- Interfaz tipo WhatsApp Web
|
|
||||||
- Preview antes de enviar
|
|
||||||
- Timer de grabación
|
|
||||||
|
|
||||||
2. **Multimedia Completo** 📷
|
|
||||||
- Subida de archivos
|
|
||||||
- Preview integrado
|
|
||||||
- Caption opcional
|
|
||||||
- Visualización inline
|
|
||||||
|
|
||||||
3. **Seguridad Robusta** 🔒
|
|
||||||
- Protección brute force
|
|
||||||
- Sesiones seguras
|
|
||||||
- Autenticación en todas las APIs
|
|
||||||
- Migración a BD para admins
|
|
||||||
|
|
||||||
4. **Interfaces Modernas** 🎨
|
|
||||||
- Diseño tipo WhatsApp
|
|
||||||
- Responsive
|
|
||||||
- Animaciones suaves
|
|
||||||
- UX intuitiva
|
|
||||||
|
|
||||||
5. **APIs REST Completas** 🔌
|
|
||||||
- 42 endpoints
|
|
||||||
- Documentación clara
|
|
||||||
- Manejo de errores
|
|
||||||
- Debug mode
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 12. PRÓXIMOS PASOS RECOMENDADOS
|
|
||||||
|
|
||||||
### Características Opcionales:
|
|
||||||
|
|
||||||
1. **Mensajes Programados**
|
|
||||||
- Envío diferido
|
|
||||||
- Campañas automáticas
|
|
||||||
- Recordatorios
|
|
||||||
|
|
||||||
2. **Analytics Avanzado**
|
|
||||||
- Tasa de respuesta
|
|
||||||
- Tiempos de respuesta
|
|
||||||
- Conversiones
|
|
||||||
|
|
||||||
3. **Integración CRM**
|
|
||||||
- Sincronización con Salesforce
|
|
||||||
- Webhooks salientes
|
|
||||||
- API de terceros
|
|
||||||
|
|
||||||
4. **IA y Chatbots**
|
|
||||||
- Respuestas inteligentes
|
|
||||||
- NLP para intenciones
|
|
||||||
- Aprendizaje automático
|
|
||||||
|
|
||||||
5. **Multi-agente**
|
|
||||||
- Asignación de conversaciones
|
|
||||||
- Chat en grupo interno
|
|
||||||
- Notas privadas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💯 CONCLUSIÓN
|
|
||||||
|
|
||||||
**El sistema está 100% funcional y listo para producción.**
|
|
||||||
|
|
||||||
### Puntuación por Módulo:
|
|
||||||
- ✅ Autenticación: **10/10**
|
|
||||||
- ✅ Mensajería: **10/10**
|
|
||||||
- ✅ Multimedia: **10/10**
|
|
||||||
- ✅ APIs: **10/10**
|
|
||||||
- ✅ Interfaces: **10/10**
|
|
||||||
- ✅ Base de Datos: **10/10**
|
|
||||||
- ✅ Seguridad: **9/10** (eliminar archivos de debug)
|
|
||||||
- ✅ Documentación: **10/10**
|
|
||||||
|
|
||||||
### **PUNTUACIÓN TOTAL: 99/100** ⭐⭐⭐⭐⭐
|
|
||||||
|
|
||||||
**Único pendiente:** Limpieza de archivos de desarrollo antes de producción.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Desarrollado por U-Site.app**
|
|
||||||
**Fecha:** 13 de enero de 2026
|
|
||||||
**Versión:** 2.0 - Sistema Completo con Multimedia
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# 🚨 SOLUCIÓN: "Error probando la nueva configuración"
|
|
||||||
|
|
||||||
## 📋 El Problema
|
|
||||||
Te aparece este error durante la instalación:
|
|
||||||
```
|
|
||||||
❌ Error probando la nueva configuración: Error en la consulta a la base de datos
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Posibles Causas
|
|
||||||
|
|
||||||
### 1. **Tabla 'users' no existe aún**
|
|
||||||
- El schema no se aplicó correctamente
|
|
||||||
- Hay errores en el SQL
|
|
||||||
|
|
||||||
### 2. **Archivo config.php no se actualizó**
|
|
||||||
- Permisos de escritura insuficientes
|
|
||||||
- Error al escribir el archivo
|
|
||||||
|
|
||||||
### 3. **Credenciales incorrectas**
|
|
||||||
- Host, usuario o contraseña incorrectos
|
|
||||||
- Base de datos no existe
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ SOLUCIONES (En orden de dificultad)
|
|
||||||
|
|
||||||
### 🚀 OPCIÓN 1: Usa el Instalador Simplificado
|
|
||||||
**¡La más fácil!** Creé un instalador específicamente para estos problemas:
|
|
||||||
|
|
||||||
1. Ve a: `install_simple.php`
|
|
||||||
2. Completa tus datos de BD
|
|
||||||
3. ¡Listo! Solo instala lo esencial
|
|
||||||
|
|
||||||
### 🔧 OPCIÓN 2: Diagnóstico Completo
|
|
||||||
Identifica exactamente qué está mal:
|
|
||||||
|
|
||||||
1. Ve a: `debug_install.php`
|
|
||||||
2. Ingresa tus credenciales de BD
|
|
||||||
3. Ejecuta el diagnóstico completo
|
|
||||||
4. Sigue las recomendaciones específicas
|
|
||||||
|
|
||||||
### 🛠️ OPCIÓN 3: Instalación Manual Paso a Paso
|
|
||||||
|
|
||||||
#### Paso 1: Verifica tu Base de Datos
|
|
||||||
```sql
|
|
||||||
-- En tu panel MySQL (phpMyAdmin, etc.)
|
|
||||||
USE tu_basedatos;
|
|
||||||
SHOW TABLES;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 2: Crea las tablas manualmente
|
|
||||||
Si no existen, ejecuta este SQL:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
|
||||||
name VARCHAR(100),
|
|
||||||
status ENUM('active', 'inactive') DEFAULT 'active',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_config (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
config_key VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
config_value TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT IGNORE INTO system_config (config_key, config_value) VALUES
|
|
||||||
('app_installed', '1'),
|
|
||||||
('install_date', NOW());
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 3: Actualiza config.php manualmente
|
|
||||||
Edita `config/config.php` y cambia:
|
|
||||||
|
|
||||||
```php
|
|
||||||
define('DB_HOST', 'localhost'); // Tu host
|
|
||||||
define('DB_PORT', '3306'); // Tu puerto
|
|
||||||
define('DB_NAME', 'tu_basedatos'); // Tu BD
|
|
||||||
define('DB_USER', 'tu_usuario'); // Tu usuario
|
|
||||||
define('DB_PASS', 'tu_contraseña'); // Tu contraseña
|
|
||||||
define('ADMIN_PASSWORD', password_hash('admin123', PASSWORD_DEFAULT));
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 4: Crear archivo de instalación completada
|
|
||||||
Crea el archivo `.installation_completed` con cualquier contenido.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 RECOMENDACIÓN ESPECÍFICA
|
|
||||||
|
|
||||||
### Para HestiaCP/cPanel:
|
|
||||||
1. **USA:** `install_simple.php` ← **MÁS FÁCIL**
|
|
||||||
2. **SI FALLA:** `debug_install.php` para diagnóstico
|
|
||||||
3. **ÚLTIMA OPCIÓN:** Instalación manual paso a paso
|
|
||||||
|
|
||||||
### Credenciales Típicas en HestiaCP:
|
|
||||||
- **Host:** `localhost`
|
|
||||||
- **Puerto:** `3306`
|
|
||||||
- **BD:** `tuusuario_nombrebd`
|
|
||||||
- **Usuario:** `tuusuario_nombreusuario`
|
|
||||||
- **Contraseña:** La que configuraste
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Después de Solucionar
|
|
||||||
|
|
||||||
### Credenciales de Admin:
|
|
||||||
- **Usuario:** `admin`
|
|
||||||
- **Contraseña:** `admin123` (o la que configuraste)
|
|
||||||
|
|
||||||
### Próximos pasos:
|
|
||||||
1. Ve a `index.php` para acceder al panel
|
|
||||||
2. Prueba con `test.php` que todo funcione
|
|
||||||
3. Configura WhatsApp en `server_setup.php`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 ¿Sigues con problemas?
|
|
||||||
|
|
||||||
### Información necesaria para soporte:
|
|
||||||
1. **Tipo de hosting:** (HestiaCP, cPanel, etc.)
|
|
||||||
2. **Error exacto:** Copia el mensaje completo
|
|
||||||
3. **Resultado de:** `debug_install.php`
|
|
||||||
4. **Versión PHP:** Desde tu panel de control
|
|
||||||
|
|
||||||
### Contacto:
|
|
||||||
- **Email:** support@u-site.app
|
|
||||||
- **Con:** Toda la información anterior
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Archivos de Ayuda Creados
|
|
||||||
|
|
||||||
1. **`install_simple.php`** - Instalador minimalista ✅
|
|
||||||
2. **`debug_install.php`** - Diagnóstico completo ✅
|
|
||||||
3. **`install_manual.php`** - Instalador manual completo ✅
|
|
||||||
4. **`GUIA_HESTIACP.md`** - Guía específica para HestiaCP ✅
|
|
||||||
|
|
||||||
**¡Con estas herramientas deberías poder instalar sin problemas!** 🚀
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
# ✅ SOLUCIÓN PARA HOSTING COMPARTIDO (HestiaCP)
|
|
||||||
|
|
||||||
**¡Problema solucionado!** He creado una solución completa para tu problema con la instalación en hosting compartido.
|
|
||||||
|
|
||||||
## 🚨 El Problema
|
|
||||||
- El instalador automático (`install.php`) **NO FUNCIONA** en hosting compartido
|
|
||||||
- Se queda "cargando" porque intenta crear BD con privilegios de administrador
|
|
||||||
- En HestiaCP/cPanel no tienes esos permisos
|
|
||||||
|
|
||||||
## ✅ La Solución
|
|
||||||
He creado un **instalador manual** específico para hosting compartido.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 PASOS PARA INSTALAR EN HESTIACP
|
|
||||||
|
|
||||||
### 1. Crear Base de Datos Manualmente
|
|
||||||
En tu panel de HestiaCP:
|
|
||||||
1. Ve a **"Databases"**
|
|
||||||
2. Clic en **"Add Database"**
|
|
||||||
3. Configura:
|
|
||||||
- **Database Name:** (ej: `tuusuario_whatsapp`)
|
|
||||||
- **Database User:** (ej: `tuusuario_bot`)
|
|
||||||
- **Password:** (una contraseña segura)
|
|
||||||
- **Privileges:** **ALL** (todos los permisos)
|
|
||||||
|
|
||||||
### 2. Anotar las Credenciales
|
|
||||||
Guarda esta información:
|
|
||||||
- **Host:** `localhost`
|
|
||||||
- **Base de datos:** El nombre que creaste
|
|
||||||
- **Usuario:** El usuario que creaste
|
|
||||||
- **Contraseña:** La contraseña que configuraste
|
|
||||||
|
|
||||||
### 3. Usar el Instalador Manual
|
|
||||||
1. Ve a: `https://tudominio.com/ruta-del-bot/install_manual.php`
|
|
||||||
2. Completa el formulario con las credenciales del paso anterior
|
|
||||||
3. Haz clic en **"Configurar Sistema"**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Archivos Creados
|
|
||||||
|
|
||||||
He creado estos archivos nuevos para solucionar tu problema:
|
|
||||||
|
|
||||||
1. **`install_manual.php`** - Instalador para hosting compartido ✅
|
|
||||||
2. **`GUIA_HESTIACP.md`** - Guía detallada paso a paso ✅
|
|
||||||
3. **`instalacion.html`** - Página para elegir tipo de instalación ✅
|
|
||||||
4. **Mejorado `install.php`** - Ahora detecta hosting compartido ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 URL de Inicio
|
|
||||||
|
|
||||||
**👉 EMPIEZA AQUÍ:** `https://tudominio.com/ruta-del-bot/instalacion.html`
|
|
||||||
|
|
||||||
Esta página te permite elegir entre:
|
|
||||||
- **Instalación Automática** (para desarrollo local)
|
|
||||||
- **Instalación Manual** (para hosting compartido) ← **USA ESTA**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Credenciales Generadas
|
|
||||||
|
|
||||||
### Para la Base de Datos
|
|
||||||
- Usarás las credenciales que **TÚ** creaste en HestiaCP
|
|
||||||
|
|
||||||
### Para el Panel Admin
|
|
||||||
- **Usuario:** `admin`
|
|
||||||
- **Contraseña:** Se genera automáticamente y se muestra durante la instalación
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Si Tienes Problemas
|
|
||||||
|
|
||||||
### Errores Comunes:
|
|
||||||
- **"Access denied"** → Verifica credenciales de BD
|
|
||||||
- **"Database doesn't exist"** → Confirma el nombre exacto de la BD
|
|
||||||
- **"Connection refused"** → Asegúrate que MySQL esté activo
|
|
||||||
|
|
||||||
### Soporte:
|
|
||||||
- **Email:** support@u-site.app
|
|
||||||
- **Documentación:** `GUIA_HESTIACP.md`
|
|
||||||
- **Pruebas:** `test.php` después de instalar
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Próximos Pasos Después de Instalar
|
|
||||||
|
|
||||||
1. **Prueba el sistema:** Ve a `test.php`
|
|
||||||
2. **Accede al panel:** Ve a `index.php`
|
|
||||||
3. **Configura WhatsApp:** Ve a `server_setup.php`
|
|
||||||
4. **Lee las credenciales:** Archivo `CREDENCIALES_SISTEMA.txt`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**¡Ya no tendrás problemas con la instalación en HestiaCP!** 🎉
|
|
||||||
|
|
||||||
La solución está probada y funciona específicamente para hostings compartidos que no permiten crear bases de datos por código.
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# 🔐 Solución Problema de Seguridad - Acceso sin Autenticación
|
|
||||||
|
|
||||||
## 🚨 Problema Encontrado
|
|
||||||
|
|
||||||
El sistema tenía un **fallo crítico de seguridad** que permitía acceder al dashboard y a las APIs sin necesidad de iniciar sesión.
|
|
||||||
|
|
||||||
### Archivos afectados:
|
|
||||||
- ❌ `index.php` - Dashboard principal sin verificación de login
|
|
||||||
- ❌ `api/*.php` - APIs sin protección de autenticación
|
|
||||||
|
|
||||||
## ✅ Solución Implementada
|
|
||||||
|
|
||||||
### 1. Corregido `index.php`
|
|
||||||
```php
|
|
||||||
// ANTES - Sin verificación
|
|
||||||
require_once 'config/config.php';
|
|
||||||
// Mostraba el dashboard directamente
|
|
||||||
|
|
||||||
// DESPUÉS - Con verificación
|
|
||||||
require_once 'config/config.php';
|
|
||||||
|
|
||||||
// Verificar autenticación - redirigir al login si no está logueado
|
|
||||||
if (!isUserLoggedIn()) {
|
|
||||||
header('Location: login.php');
|
|
||||||
exit('Acceso denegado. <a href="login.php">Iniciar sesión</a>');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Nueva función en `config.php`
|
|
||||||
```php
|
|
||||||
// Función para verificar autenticación en APIs
|
|
||||||
function requireAuthentication() {
|
|
||||||
if (!isUserLoggedIn()) {
|
|
||||||
http_response_code(401);
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
echo json_encode([
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'No autorizado. Inicie sesión.',
|
|
||||||
'redirect' => 'login.php'
|
|
||||||
]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. APIs protegidas
|
|
||||||
Archivos corregidos con `requireAuthentication()`:
|
|
||||||
- ✅ `api/get_stats.php`
|
|
||||||
- ✅ `api/save_system_config.php`
|
|
||||||
- ✅ `api/get_users.php`
|
|
||||||
- ✅ `api/send_message.php`
|
|
||||||
- ✅ `api/get_system_config.php`
|
|
||||||
- ✅ `api/get_menus.php`
|
|
||||||
- ✅ `api/export_users.php`
|
|
||||||
|
|
||||||
## 🔒 Seguridad Actual
|
|
||||||
|
|
||||||
### Flujo de Autenticación:
|
|
||||||
1. Usuario intenta acceder a `index.php`
|
|
||||||
2. Sistema verifica `isUserLoggedIn()`
|
|
||||||
3. Si no está logueado → Redirección a `login.php`
|
|
||||||
4. Si está logueado → Acceso permitido
|
|
||||||
|
|
||||||
### Protección de APIs:
|
|
||||||
1. Cliente hace petición a API
|
|
||||||
2. `requireAuthentication()` verifica sesión
|
|
||||||
3. Si no está autenticado → Error 401 JSON
|
|
||||||
4. Si está autenticado → Procesamiento normal
|
|
||||||
|
|
||||||
## 🎯 Recomendaciones Adicionales
|
|
||||||
|
|
||||||
Para fortalecer aún más la seguridad:
|
|
||||||
|
|
||||||
1. **Revisar APIs restantes** - Aplicar `requireAuthentication()` a todas
|
|
||||||
2. **HTTPS obligatorio** - Configurar SSL en producción
|
|
||||||
3. **Rate Limiting** - Limitar requests por IP
|
|
||||||
4. **Logs de seguridad** - Registrar intentos no autorizados
|
|
||||||
5. **Token CSRF** - Protección contra ataques Cross-Site
|
|
||||||
|
|
||||||
## 📝 Credenciales por Defecto
|
|
||||||
|
|
||||||
- **Usuario**: admin
|
|
||||||
- **Contraseña**: Se configura durante la instalación
|
|
||||||
|
|
||||||
## ⚠️ Importante
|
|
||||||
|
|
||||||
**Antes de esta corrección**, cualquier persona que conociera la URL podía:
|
|
||||||
- Ver el dashboard completo
|
|
||||||
- Acceder a todas las APIs
|
|
||||||
- Ver datos de usuarios
|
|
||||||
- Enviar mensajes
|
|
||||||
- Modificar configuraciones
|
|
||||||
|
|
||||||
**Ahora**, todas estas acciones requieren autenticación previa.
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# 🚨 SOLUCIÓN PARA ERROR: "Table doesn't exist"
|
|
||||||
|
|
||||||
## Tu Situación Específica
|
|
||||||
Has reportado estos errores:
|
|
||||||
```
|
|
||||||
⚠️ Esquema procesado con advertencias: 0 tablas, 0 elementos
|
|
||||||
⚠️ SQL Error: Table 'usite_whatsapp_bot.menus' doesn't exist
|
|
||||||
```
|
|
||||||
|
|
||||||
**CAUSA:** El archivo `schema.sql` no se está procesando correctamente. El instalador intenta insertar datos en tablas que aún no se han creado.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ SOLUCIÓN INMEDIATA
|
|
||||||
|
|
||||||
### 🎯 **OPCIÓN 1: Instalador Ultra Básico (RECOMENDADO)**
|
|
||||||
**👉 Usa:** `install_ultra_basic.php`
|
|
||||||
|
|
||||||
### 🔧 **OPCIÓN 2: Instalador Integrado (COMPLETO)**
|
|
||||||
**👉 Usa:** `install_integrated.php`
|
|
||||||
|
|
||||||
**¿Por qué funcionan?**
|
|
||||||
- ✅ Schema SQL integrado en el código PHP
|
|
||||||
- ✅ No depende del archivo `schema.sql` externo
|
|
||||||
- ✅ Ejecución controlada paso a paso
|
|
||||||
- ✅ Primero crea tablas, luego inserta datos
|
|
||||||
|
|
||||||
**Nota:** La tabla `users` NO incluye campo `password` porque es para usuarios de WhatsApp que se identifican por número de teléfono. La contraseña de admin se guarda en `config.php`.
|
|
||||||
|
|
||||||
### 🔧 **OPCIÓN 2: Reparar Manualmente**
|
|
||||||
Si quieres usar el instalador manual:
|
|
||||||
|
|
||||||
1. **Ejecuta este SQL directamente en phpMyAdmin:**
|
|
||||||
```sql
|
|
||||||
USE usite_whatsapp_bot;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS menus (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
title VARCHAR(200) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
parent_id INT NULL,
|
|
||||||
is_root BOOLEAN DEFAULT FALSE,
|
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
order_position INT DEFAULT 0,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_parent (parent_id),
|
|
||||||
INDEX idx_active (is_active)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Luego usa cualquier instalador**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 ¿Por qué falló el archivo schema.sql?
|
|
||||||
|
|
||||||
### Problemas comunes:
|
|
||||||
1. **Parsing incorrecto:** El archivo se divide mal por `;`
|
|
||||||
2. **Variables no definidas:** `SET @variable` no funciona en algunos contextos
|
|
||||||
3. **Dependencias:** Se intenta insertar en tablas antes de crearlas
|
|
||||||
4. **Encoding:** Caracteres especiales en el archivo
|
|
||||||
|
|
||||||
### El instalador integrado evita estos problemas porque:
|
|
||||||
- ✅ Usa arrays PHP en lugar de parsing de archivo
|
|
||||||
- ✅ Ejecuta CREATE TABLE primero, INSERT después
|
|
||||||
- ✅ Manejo de errores específico para cada paso
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Pasos Específicos Para Ti
|
|
||||||
|
|
||||||
### 1. **Usar el Instalador Integrado:**
|
|
||||||
```
|
|
||||||
1. Ve a: tudominio.com/install_integrated.php
|
|
||||||
2. Ingresa tus credenciales:
|
|
||||||
- Host: localhost
|
|
||||||
- Puerto: 3306
|
|
||||||
- BD: usite_whatsapp_bot
|
|
||||||
- Usuario: [tu usuario]
|
|
||||||
- Contraseña: [tu contraseña]
|
|
||||||
3. Clic en "Instalar con Schema Integrado"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Credenciales que se generarán:**
|
|
||||||
- **Usuario admin:** `admin`
|
|
||||||
- **Contraseña:** Se muestra durante la instalación
|
|
||||||
- **Se guarda en:** `CREDENCIALES.txt`
|
|
||||||
|
|
||||||
### 3. **Verificar que funcionó:**
|
|
||||||
```
|
|
||||||
- Ve a: test.php
|
|
||||||
- Deberías ver: "✅ Sistema funcionando"
|
|
||||||
- Login en: index.php
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Archivos Disponibles
|
|
||||||
|
|
||||||
| Instalador | Recomendado Para | Tu Situación |
|
|
||||||
|------------|------------------|--------------|
|
|
||||||
| `install_integrated.php` | **Problemas con schema.sql** | ✅ **PERFECTO** |
|
|
||||||
| `install_simple.php` | Solo funciones básicas | ✅ Alternativa |
|
|
||||||
| `install_manual.php` | Instalación completa | ❌ Tiene el problema |
|
|
||||||
| `debug_install.php` | Diagnóstico de problemas | ℹ️ Para análisis |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Después de Instalar
|
|
||||||
|
|
||||||
1. **Verifica las tablas creadas:**
|
|
||||||
- Ve a phpMyAdmin
|
|
||||||
- Deberías ver: users, conversations, menus, menu_options, system_config, etc.
|
|
||||||
|
|
||||||
2. **Prueba el sistema:**
|
|
||||||
- `test.php` - Para verificar funcionamiento
|
|
||||||
- `index.php` - Panel de administración
|
|
||||||
- `login.php` - Sistema de login
|
|
||||||
|
|
||||||
3. **Configura WhatsApp:**
|
|
||||||
- Entra al panel con las credenciales generadas
|
|
||||||
- Ve a Configuración
|
|
||||||
- Agrega tu token de WhatsApp
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Si Aún Tienes Problemas
|
|
||||||
|
|
||||||
### Información para soporte:
|
|
||||||
- **Hosting:** HestiaCP (confirmado)
|
|
||||||
- **BD:** usite_whatsapp_bot (confirmado)
|
|
||||||
- **Error anterior:** Schema parsing falló
|
|
||||||
- **Solución aplicada:** Instalador integrado
|
|
||||||
|
|
||||||
### Contacto:
|
|
||||||
- **Email:** support@u-site.app
|
|
||||||
- **Incluye:** Resultado del instalador integrado
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**💡 RESUMEN:** Tu problema se debe a que el archivo `schema.sql` no se procesa correctamente. El instalador integrado resuelve esto completamente al tener todo el SQL dentro del código PHP. ¡Úsalo y debería funcionar perfectamente! 🚀
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
# Sistema de Notificaciones en Tiempo Real (SSE)
|
|
||||||
|
|
||||||
## 📡 Descripción
|
|
||||||
|
|
||||||
Sistema de notificaciones push en tiempo real usando **Server-Sent Events (SSE)** para reemplazar el polling constante y mejorar la eficiencia.
|
|
||||||
|
|
||||||
## 🔄 Flujo de funcionamiento
|
|
||||||
|
|
||||||
### 1. **Cliente se conecta a SSE**
|
|
||||||
```javascript
|
|
||||||
// En conversations.php
|
|
||||||
connectSSE() {
|
|
||||||
this.eventSource = new EventSource('api/sse_events.php');
|
|
||||||
// Escucha eventos: new_message, new_conversation, heartbeat
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Webhook recibe mensaje de WhatsApp**
|
|
||||||
```
|
|
||||||
WhatsApp → webhook.php → Guarda en BD → pushSSEEvent()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Push notifica a operadores conectados**
|
|
||||||
```php
|
|
||||||
webhook.php → push_event.php → Escribe evento en archivo temporal
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **SSE envía evento a clientes**
|
|
||||||
```php
|
|
||||||
sse_events.php → Lee eventos pendientes → Envía a navegador
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. **Cliente recibe y procesa evento**
|
|
||||||
```javascript
|
|
||||||
// Solo actualiza la conversación afectada, NO recarga todo
|
|
||||||
eventSource.addEventListener('new_message', (e) => {
|
|
||||||
// Actualizar conversación en lista
|
|
||||||
// Si es la activa, recargar mensajes
|
|
||||||
// Reproducir sonido
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Archivos creados
|
|
||||||
|
|
||||||
### `api/sse_events.php`
|
|
||||||
- Endpoint SSE que mantiene conexión abierta
|
|
||||||
- Lee eventos de archivos temporales
|
|
||||||
- Envía eventos al navegador en tiempo real
|
|
||||||
- **NUEVO**: Envía notificaciones no leídas automáticamente
|
|
||||||
- Heartbeat cada 30 segundos para mantener conexión viva
|
|
||||||
|
|
||||||
### `api/push_event.php`
|
|
||||||
- API interna para empujar eventos
|
|
||||||
- Solo accesible desde localhost o con token secreto
|
|
||||||
- Escribe eventos en archivos temporales por usuario
|
|
||||||
- Broadcast a todos los operadores conectados
|
|
||||||
|
|
||||||
### `classes/NotificationHelper.php` ⭐ NUEVO
|
|
||||||
- Helper para crear y enviar notificaciones
|
|
||||||
- **Método create()**: Inserta en BD y envía automáticamente por SSE
|
|
||||||
- **Método pushSSE()**: Envía notificación existente por SSE
|
|
||||||
- Uso: `NotificationHelper::create($userId, 'document', 'Usuario subió documento', ['count' => 3])`
|
|
||||||
|
|
||||||
## 🔧 Modificaciones en archivos existentes
|
|
||||||
|
|
||||||
### `api/webhook.php`
|
|
||||||
- ✅ Agregado método `pushSSEEvent()`
|
|
||||||
- ✅ Llama a `push_event.php` cuando llega mensaje nuevo
|
|
||||||
- ✅ Fire-and-forget (no bloquea el webhook)
|
|
||||||
|
|
||||||
### `conversations.php`
|
|
||||||
- ✅ Agregado método `connectSSE()`
|
|
||||||
- ✅ Agregado método `updateConversationInList()`
|
|
||||||
- ✅ Agregado método `addConversationToList()`
|
|
||||||
- ✅ **Agregado listener para evento 'notification'**
|
|
||||||
- ✅ Polling de notificaciones: 7s → 60s (backup)
|
|
||||||
- ✅ Polling de conversaciones: 7s → 30s (backup)
|
|
||||||
- ✅ Auto-refresh reducido de 8s → 20s (backup)
|
|
||||||
|
|
||||||
## 🎯 Ventajas del sistema SSE
|
|
||||||
|
|
||||||
### **Antes (Polling):**
|
|
||||||
```
|
|
||||||
Cliente → GET messages cada 7-8s
|
|
||||||
Cliente → GET conversations cada 15s
|
|
||||||
Cliente → GET notifications cada 7s
|
|
||||||
|
|
||||||
|
|
||||||
### **Ahora (SSE):**
|
|
||||||
```
|
|
||||||
Cliente ← SSE conectado (1 conexión persistente)
|
|
||||||
Servidor → Push cuando hay nuevo mensaje (< 1s)
|
|
||||||
→ Recarga automáticamente: loadMessages(userId, false, true)
|
|
||||||
Servidor → Push cuando hay nueva conversación (< 1s)
|
|
||||||
→ Agrega a lista: addConversationToList(data)
|
|
||||||
Servidor → Push cuando hay notificación (< 1s)
|
|
||||||
→ Muestra toast: showNotificationToast(notification)
|
|
||||||
Servidor → Heartbeat cada 30s
|
|
||||||
|
|
||||||
Backup polling (solo por si falla SSE):
|
|
||||||
Cliente → GET notifications cada 60s
|
|
||||||
|
|
||||||
Total: ~1 petición por minuto por cliente
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reducción: 95% menos peticiones HTTP** 🎉
|
|
||||||
|
|
||||||
### ¿Por qué NO necesitamos ningún polling ahora?
|
|
||||||
|
|
||||||
#### 1. **Mensajes**: SSE evento `new_message`
|
|
||||||
```javascript
|
|
||||||
// En conversations.php línea ~1103
|
|
||||||
this.eventSource.addEventListener('new_message', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
|
|
||||||
// Si es la conversación activa, recarga mensajes
|
|
||||||
if (this.currentUserId === data.user_id) {
|
|
||||||
this.loadMessages(this.currentUserId, false, true); // ✅ Push
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza la conversación en la lista
|
|
||||||
this.updateConversationInList(data);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. **Conversaciones**: SSE eventos `new_message` + `new_conversation`
|
|
||||||
```javascript
|
|
||||||
// Actualización inteligente sin recargar todo
|
|
||||||
this.eventSource.addEventListener('new_conversation', (e) => {
|
|
||||||
this.addConversationToList(JSON.parse(e.data)); // ✅ Solo agrega una
|
|
||||||
});
|
|
||||||
|
|
||||||
this.eventSource.addEventListener('new_message', (e) => {
|
|
||||||
this.updateConversationInList(JSON.parse(e.data)); // ✅ Solo actualiza una
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. **Notificaciones**: SSE evento `notification`
|
|
||||||
```javascript
|
|
||||||
this.eventSource.addEventListener('notification', (e) => {
|
|
||||||
this.showNotificationToast(JSON.parse(e.data)); // ✅ Toast instantáneo
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resultado**: Sistema 100% push, 0 polling innecesario 🚀
|
|
||||||
Total: ~15-20 peticiones por minuto por cliente
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Ahora (SSE):**
|
|
||||||
```
|
|
||||||
Cliente ← SSE conexión permanente
|
|
||||||
Cliente → GET backup cada 15-20s
|
|
||||||
|
|
||||||
Total: ~2-3 peticiones por minuto por cliente
|
|
||||||
Eventos llegan en < 1 segundo después del webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Beneficios:**
|
|
||||||
- ✅ **Latencia ultra-baja:** Mensajes llegan en ~500ms
|
|
||||||
- ✅ **Menos carga servidor:** 85% menos peticiones HTTP
|
|
||||||
- ✅ **Actualizaciones incrementales:** Solo afecta conversación específica
|
|
||||||
- ✅ **No recarga todo:** UI más fluida y rápida
|
|
||||||
- ✅ **Conexión persistente:** Un solo canal para todos los eventos
|
|
||||||
- ✅ **Fallback automático:** Si SSE falla, polling sigue funcionando
|
|
||||||
|
|
||||||
## 🔐 Seguridad
|
|
||||||
|
|
||||||
### **Token secreto interno:**
|
|
||||||
```php
|
|
||||||
// En push_event.php
|
|
||||||
X-Push-Token: internal_push_secret_2026
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Validación de IP:**
|
|
||||||
```php
|
|
||||||
// Solo permite localhost por defecto
|
|
||||||
$allowedIPs = ['127.0.0.1', '::1', 'localhost'];
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Autenticación SSE:**
|
|
||||||
```php
|
|
||||||
// En sse_events.php
|
|
||||||
requireAuthentication(); // Usa sesión PHP
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Eventos disponibles
|
|
||||||
|
|
||||||
### `connected`
|
|
||||||
- Se envía cuando cliente se conecta exitosamente
|
|
||||||
- Data: `{ timestamp: unix_timestamp }`
|
|
||||||
|
|
||||||
### `new_message`
|
|
||||||
- Se envía cuando llega mensaje nuevo de WhatsApp
|
|
||||||
- Data:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": 123,
|
|
||||||
"phone_number": "573168950803",
|
|
||||||
"name": "Juan Pérez",
|
|
||||||
"message": "Hola, necesito ayuda",
|
|
||||||
"message_type": "text",
|
|
||||||
"timestamp": "2026-01-27 10:30:00"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `new_conversation`
|
|
||||||
- Se envía cuando se detecta nueva conversación
|
|
||||||
- Data:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": 124,
|
|
||||||
"phone_number": "573168950804",
|
|
||||||
"name": "María García",
|
|
||||||
"message_count": 1,
|
|
||||||
"timestamp": "2026-01-27 10:35:00"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `heartbeat`
|
|
||||||
- Se envía cada 30 segundos
|
|
||||||
- Data: `{ timestamp: unix_timestamp }`
|
|
||||||
- Mantiene conexión viva
|
|
||||||
|
|
||||||
## 🚀 Cómo probar
|
|
||||||
|
|
||||||
### 1. **Verificar que SSE funciona:**
|
|
||||||
```bash
|
|
||||||
curl -N http://localhost/api/sse_events.php
|
|
||||||
```
|
|
||||||
|
|
||||||
Deberías ver:
|
|
||||||
```
|
|
||||||
event: connected
|
|
||||||
data: {"timestamp":1738000000}
|
|
||||||
|
|
||||||
event: heartbeat
|
|
||||||
data: {"timestamp":1738000030}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Simular un push interno:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost/api/push_event.php \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-Push-Token: internal_push_secret_2026" \
|
|
||||||
-d '{
|
|
||||||
"event_type": "new_message",
|
|
||||||
"data": {
|
|
||||||
"user_id": 123,
|
|
||||||
"phone_number": "573168950803",
|
|
||||||
"message": "Test message"
|
|
||||||
},
|
|
||||||
"target_user_id": "all"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Ver eventos en navegador:**
|
|
||||||
- Abrir `conversations.php`
|
|
||||||
- Abrir DevTools → Console
|
|
||||||
- Deberías ver: `✅ SSE conectado: {...}`
|
|
||||||
- Enviar mensaje de prueba desde WhatsApp
|
|
||||||
- Verás: `📨 Nuevo mensaje (SSE): {...}`
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### **SSE no conecta:**
|
|
||||||
1. Verificar que `api/sse_events.php` es accesible
|
|
||||||
2. Verificar que sesión está activa (login requerido)
|
|
||||||
3. Revisar logs del servidor
|
|
||||||
|
|
||||||
### **Eventos no llegan:**
|
|
||||||
1. Verificar permisos de escritura en `/uploads/`
|
|
||||||
2. Verificar que `push_event.php` es accesible desde localhost
|
|
||||||
3. Revisar logs de webhook
|
|
||||||
|
|
||||||
### **Reconexión constante:**
|
|
||||||
1. Verificar timeout del servidor (debe ser > 60s)
|
|
||||||
2. Verificar que nginx/apache no tiene buffer activo
|
|
||||||
3. Revisar límites de conexiones persistentes
|
|
||||||
|
|
||||||
## ⚙️ Configuración avanzada
|
|
||||||
|
|
||||||
### **Cambiar token de seguridad:**
|
|
||||||
```bash
|
|
||||||
# .env o config
|
|
||||||
PUSH_TOKEN=tu_token_super_secreto_aqui
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Aumentar timeout SSE:**
|
|
||||||
```php
|
|
||||||
// En sse_events.php
|
|
||||||
set_time_limit(300); // 5 minutos
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Cambiar frecuencia de revisión:**
|
|
||||||
```php
|
|
||||||
// En sse_events.php, cambiar:
|
|
||||||
sleep(2); // A valor deseado (1-5 segundos)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Monitoreo
|
|
||||||
|
|
||||||
### **Ver eventos en tiempo real:**
|
|
||||||
```bash
|
|
||||||
tail -f /ruta/uploads/events_global.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Contar conexiones activas:**
|
|
||||||
```bash
|
|
||||||
netstat -an | grep :80 | grep ESTABLISHED | wc -l
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Ver logs de push:**
|
|
||||||
```bash
|
|
||||||
tail -f /var/log/apache2/error.log | grep pushSSEEvent
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Migración desde polling
|
|
||||||
|
|
||||||
El sistema es compatible hacia atrás. Si SSE falla:
|
|
||||||
- Polling sigue funcionando como backup
|
|
||||||
- No se pierden mensajes
|
|
||||||
- Usuario no nota diferencia (solo latencia mayor)
|
|
||||||
|
|
||||||
Para deshabilitar polling completamente:
|
|
||||||
```javascript
|
|
||||||
// En conversations.php, comentar:
|
|
||||||
// setInterval(() => this.loadNotifications(), 15000);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Notas importantes
|
|
||||||
|
|
||||||
1. **Archivos temporales:** Los eventos se guardan en `/uploads/events_{user_id}.json`
|
|
||||||
2. **Limpieza automática:** Solo se mantienen últimos 50 eventos por usuario
|
|
||||||
3. **Broadcast:** Eventos se envían a todos los operadores conectados
|
|
||||||
4. **No duplicación:** Verificación de IDs previene mensajes duplicados
|
|
||||||
5. **Graceful degradation:** Sistema funciona con o sin SSE
|
|
||||||
|
|
||||||
## 🎉 Resultado final
|
|
||||||
|
|
||||||
**Antes:**
|
|
||||||
- Mensajes aparecen después de 7-15 segundos
|
|
||||||
- Múltiples peticiones constantes
|
|
||||||
- Recarga completa de lista y conversaciones
|
|
||||||
|
|
||||||
**Ahora:**
|
|
||||||
- Mensajes aparecen en < 1 segundo
|
|
||||||
- Una conexión persistente + backup ligero
|
|
||||||
- Solo actualiza la conversación afectada
|
|
||||||
- UI más fluida y responsive
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Cambiar contraseña de usuario administrador
|
||||||
|
* Fecha: 3 de febrero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Leer datos del request
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$userId = $input['user_id'] ?? null;
|
||||||
|
$newPassword = $input['new_password'] ?? '';
|
||||||
|
|
||||||
|
if (!$userId) {
|
||||||
|
throw new Exception('ID de usuario requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($newPassword) < 6) {
|
||||||
|
throw new Exception('La contraseña debe tener al menos 6 caracteres');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encriptar contraseña
|
||||||
|
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Actualizar contraseña
|
||||||
|
$db->execute(
|
||||||
|
"UPDATE admin_users SET password_hash = ? WHERE id = ?",
|
||||||
|
[$hashedPassword, $userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Contraseña actualizada correctamente'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Error changing admin password: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Crear nuevo usuario administrador
|
||||||
|
* Fecha: 3 de febrero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Leer datos del request
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$username = trim($input['username'] ?? '');
|
||||||
|
$fullName = trim($input['full_name'] ?? '');
|
||||||
|
$email = trim($input['email'] ?? '');
|
||||||
|
$password = $input['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($username)) {
|
||||||
|
throw new Exception('El nombre de usuario es requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
throw new Exception('La contraseña debe tener al menos 6 caracteres');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el username ya existe
|
||||||
|
$existing = $db->fetch(
|
||||||
|
"SELECT id FROM admin_users WHERE username = ?",
|
||||||
|
[$username]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
throw new Exception('El nombre de usuario ya está en uso');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encriptar contraseña
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Insertar usuario
|
||||||
|
$db->execute(
|
||||||
|
"INSERT INTO admin_users (username, password_hash, full_name, email, is_active, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, 1, NOW())",
|
||||||
|
[$username, $hashedPassword, $fullName, $email]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Usuario creado correctamente'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Error creating admin user: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Eliminar mensaje
|
||||||
|
* Permite eliminar un mensaje de la conversación
|
||||||
|
* Fecha: 28 de enero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Habilitar display de errores para debug
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
// Log para debug
|
||||||
|
error_log("delete_message.php: Iniciando, session_id=" . session_id() . ", admin_logged_in=" . (isset($_SESSION['admin_logged_in']) ? $_SESSION['admin_logged_in'] : 'no definido'));
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, DELETE, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
// Manejar preflight OPTIONS request
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'DELETE') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido. Use POST o DELETE.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
// También soportar query params para DELETE
|
||||||
|
if (!$input && $_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
|
$input = [
|
||||||
|
'message_id' => $_GET['message_id'] ?? $_GET['id'] ?? null,
|
||||||
|
'delete_for_everyone' => $_GET['delete_for_everyone'] ?? false
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'JSON inválido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$messageId = $input['message_id'] ?? $input['id'] ?? null;
|
||||||
|
$deleteForEveryone = $input['delete_for_everyone'] ?? false;
|
||||||
|
$softDelete = $input['soft_delete'] ?? true; // Por defecto, soft delete
|
||||||
|
|
||||||
|
if (!$messageId) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'message_id es requerido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Buscar el mensaje
|
||||||
|
$sql = "SELECT id, user_id, direction, message_type, content, message_id as wa_message_id,
|
||||||
|
local_file, local_thumb, media_url
|
||||||
|
FROM conversations
|
||||||
|
WHERE id = :mid1 OR message_id = :mid2
|
||||||
|
LIMIT 1";
|
||||||
|
$message = $db->fetch($sql, ['mid1' => $messageId, 'mid2' => $messageId]);
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Mensaje no encontrado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo permitir eliminar mensajes salientes (outgoing) a menos que sea admin
|
||||||
|
$isAdmin = isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'];
|
||||||
|
if ($message['direction'] !== 'outgoing' && !$isAdmin) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Solo puedes eliminar tus propios mensajes']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletedMessageId = $message['id'];
|
||||||
|
$userId = $message['user_id'];
|
||||||
|
$waMessageId = $message['wa_message_id'];
|
||||||
|
|
||||||
|
if ($softDelete) {
|
||||||
|
// Soft delete: marcar como eliminado pero mantener registro
|
||||||
|
// Verificar si existen las columnas deleted_at y original_content
|
||||||
|
$hasDeletedAt = false;
|
||||||
|
$hasOriginalContent = false;
|
||||||
|
try {
|
||||||
|
$columns = $db->fetchAll("SHOW COLUMNS FROM conversations WHERE Field IN ('deleted_at', 'original_content')");
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
if ($col['Field'] === 'deleted_at') $hasDeletedAt = true;
|
||||||
|
if ($col['Field'] === 'original_content') $hasOriginalContent = true;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Ignorar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construir la query según las columnas disponibles
|
||||||
|
// IMPORTANTE: Guardar original_content ANTES de modificar content
|
||||||
|
if ($hasDeletedAt && $hasOriginalContent) {
|
||||||
|
// Primero guardamos el contenido original
|
||||||
|
$db->execute("UPDATE conversations SET original_content = content WHERE id = :id AND original_content IS NULL", ['id' => $deletedMessageId]);
|
||||||
|
// Luego marcamos como eliminado
|
||||||
|
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]', deleted_at = NOW() WHERE id = :id";
|
||||||
|
} elseif ($hasDeletedAt) {
|
||||||
|
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]', deleted_at = NOW() WHERE id = :id";
|
||||||
|
} else {
|
||||||
|
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]' WHERE id = :id";
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->execute($updateSql, ['id' => $deletedMessageId]);
|
||||||
|
|
||||||
|
error_log("Message soft-deleted: ID={$deletedMessageId}, User={$userId}");
|
||||||
|
} else {
|
||||||
|
// Hard delete: eliminar completamente
|
||||||
|
|
||||||
|
// Primero, eliminar archivos multimedia asociados si existen
|
||||||
|
if (!empty($message['local_file'])) {
|
||||||
|
$filePath = dirname(__DIR__) . '/' . $message['local_file'];
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
@unlink($filePath);
|
||||||
|
error_log("Deleted media file: {$filePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($message['local_thumb'])) {
|
||||||
|
$thumbPath = dirname(__DIR__) . '/' . $message['local_thumb'];
|
||||||
|
if (file_exists($thumbPath)) {
|
||||||
|
@unlink($thumbPath);
|
||||||
|
error_log("Deleted thumbnail: {$thumbPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar el mensaje de la base de datos
|
||||||
|
$deleteSql = "DELETE FROM conversations WHERE id = :id";
|
||||||
|
$db->execute($deleteSql, ['id' => $deletedMessageId]);
|
||||||
|
|
||||||
|
error_log("Message hard-deleted: ID={$deletedMessageId}, User={$userId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentar notificar por SSE si está disponible
|
||||||
|
try {
|
||||||
|
$ssePayload = [
|
||||||
|
'type' => 'message_deleted',
|
||||||
|
'user_id' => $userId,
|
||||||
|
'message_id' => $deletedMessageId,
|
||||||
|
'wa_message_id' => $waMessageId,
|
||||||
|
'soft_delete' => $softDelete,
|
||||||
|
'deleted_at' => date('Y-m-d H:i:s')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Llamar al endpoint push_event interno
|
||||||
|
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
|
||||||
|
$ch = curl_init($pushUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||||
|
'event_type' => 'message_deleted',
|
||||||
|
'data' => $ssePayload,
|
||||||
|
'target_user_id' => 'all'
|
||||||
|
]));
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
} catch (Exception $sseErr) {
|
||||||
|
error_log("delete_message: SSE notification failed: " . $sseErr->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $softDelete ? 'Mensaje eliminado' : 'Mensaje eliminado permanentemente',
|
||||||
|
'data' => [
|
||||||
|
'id' => $deletedMessageId,
|
||||||
|
'message_id' => $waMessageId,
|
||||||
|
'soft_delete' => $softDelete,
|
||||||
|
'deleted_at' => date('Y-m-d H:i:s')
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('delete_message.php error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error interno del servidor', 'debug' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Eliminar mensaje programado
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: DELETE, POST');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
// Intentar obtener de query params
|
||||||
|
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||||
|
} else {
|
||||||
|
$id = isset($input['id']) ? intval($input['id']) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
throw new Exception('ID de mensaje programado requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Verificar que existe
|
||||||
|
$existing = $db->fetch("SELECT * FROM scheduled_messages WHERE id = ?", [$id]);
|
||||||
|
|
||||||
|
if (!$existing) {
|
||||||
|
throw new Exception('Mensaje programado no encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar
|
||||||
|
$db->execute("DELETE FROM scheduled_messages WHERE id = ?", [$id]);
|
||||||
|
|
||||||
|
writeLog('INFO', "Mensaje programado eliminado: ID={$id}");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Mensaje programado eliminado exitosamente'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error in delete_scheduled_message.php: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Editar mensaje
|
||||||
|
* Permite editar el contenido de un mensaje enviado (solo mensajes propios/outgoing)
|
||||||
|
* Fecha: 28 de enero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Habilitar display de errores para debug
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
// Log para debug
|
||||||
|
error_log("edit_message.php: Iniciando, session_id=" . session_id() . ", admin_logged_in=" . (isset($_SESSION['admin_logged_in']) ? $_SESSION['admin_logged_in'] : 'no definido'));
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
// Manejar preflight OPTIONS request
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido. Use POST.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'JSON inválido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$messageId = $input['message_id'] ?? $input['id'] ?? null;
|
||||||
|
$newContent = $input['content'] ?? $input['new_content'] ?? null;
|
||||||
|
|
||||||
|
if (!$messageId) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'message_id es requerido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newContent === null || trim($newContent) === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'El contenido no puede estar vacío']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Buscar el mensaje
|
||||||
|
$sql = "SELECT id, user_id, direction, message_type, content, message_id as wa_message_id
|
||||||
|
FROM conversations
|
||||||
|
WHERE id = :mid1 OR message_id = :mid2
|
||||||
|
LIMIT 1";
|
||||||
|
$message = $db->fetch($sql, ['mid1' => $messageId, 'mid2' => $messageId]);
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Mensaje no encontrado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo permitir editar mensajes salientes (outgoing)
|
||||||
|
if ($message['direction'] !== 'outgoing') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Solo puedes editar tus propios mensajes']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo permitir editar mensajes de texto
|
||||||
|
if ($message['message_type'] !== 'text' && $message['message_type'] !== null) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Solo se pueden editar mensajes de texto']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar contenido original para historial (opcional)
|
||||||
|
$originalContent = $message['content'];
|
||||||
|
|
||||||
|
// Verificar si existe la columna edited_at
|
||||||
|
$hasEditedAt = false;
|
||||||
|
try {
|
||||||
|
$columns = $db->fetchAll("SHOW COLUMNS FROM conversations WHERE Field = 'edited_at'");
|
||||||
|
$hasEditedAt = !empty($columns);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Ignorar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar el mensaje en la base de datos
|
||||||
|
if ($hasEditedAt) {
|
||||||
|
$updateSql = "UPDATE conversations SET content = :content, edited_at = NOW() WHERE id = :id";
|
||||||
|
} else {
|
||||||
|
$updateSql = "UPDATE conversations SET content = :content WHERE id = :id";
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->execute($updateSql, [
|
||||||
|
'content' => trim($newContent),
|
||||||
|
'id' => $message['id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Registrar la edición en el log
|
||||||
|
error_log("Message edited: ID={$message['id']}, User={$message['user_id']}, Original='{$originalContent}', New='{$newContent}'");
|
||||||
|
|
||||||
|
// Intentar notificar por SSE si está disponible
|
||||||
|
try {
|
||||||
|
$ssePayload = [
|
||||||
|
'type' => 'message_edited',
|
||||||
|
'user_id' => $message['user_id'],
|
||||||
|
'message_id' => $message['id'],
|
||||||
|
'wa_message_id' => $message['wa_message_id'],
|
||||||
|
'new_content' => trim($newContent),
|
||||||
|
'edited_at' => date('Y-m-d H:i:s')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Llamar al endpoint push_event interno
|
||||||
|
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
|
||||||
|
$ch = curl_init($pushUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||||
|
'event_type' => 'message_edited',
|
||||||
|
'data' => $ssePayload,
|
||||||
|
'target_user_id' => 'all'
|
||||||
|
]));
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
} catch (Exception $sseErr) {
|
||||||
|
error_log("edit_message: SSE notification failed: " . $sseErr->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Mensaje editado correctamente',
|
||||||
|
'data' => [
|
||||||
|
'id' => $message['id'],
|
||||||
|
'message_id' => $message['wa_message_id'],
|
||||||
|
'new_content' => trim($newContent),
|
||||||
|
'edited_at' => date('Y-m-d H:i:s')
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('edit_message.php error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error interno del servidor', 'debug' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@ try {
|
|||||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||||
$offset = ($page - 1) * $limit;
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
// Parámetro de búsqueda
|
||||||
|
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||||
|
|
||||||
// Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar
|
// Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar
|
||||||
$filter = isset($_GET['filter']) ? strtolower(trim($_GET['filter'])) : 'all';
|
$filter = isset($_GET['filter']) ? strtolower(trim($_GET['filter'])) : 'all';
|
||||||
|
|
||||||
@@ -47,7 +50,19 @@ try {
|
|||||||
lm.local_file AS last_local_file,
|
lm.local_file AS last_local_file,
|
||||||
lm.local_thumb AS last_local_thumb,
|
lm.local_thumb AS last_local_thumb,
|
||||||
lm.created_at AS last_time,
|
lm.created_at AS last_time,
|
||||||
IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count
|
IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count";
|
||||||
|
|
||||||
|
// Si hay búsqueda, agregar campo para ordenar por relevancia
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= ",
|
||||||
|
(CASE
|
||||||
|
WHEN u.name LIKE ? THEN 3
|
||||||
|
WHEN u.phone_number LIKE ? THEN 2
|
||||||
|
ELSE 1
|
||||||
|
END) AS relevance";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= "
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN conversations c ON c.user_id = u.id
|
LEFT JOIN conversations c ON c.user_id = u.id
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
@@ -55,7 +70,23 @@ try {
|
|||||||
JOIN (
|
JOIN (
|
||||||
SELECT user_id, MAX(created_at) AS last_time FROM conversations GROUP BY user_id
|
SELECT user_id, MAX(created_at) AS last_time FROM conversations GROUP BY user_id
|
||||||
) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time
|
) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time
|
||||||
) lm ON lm.user_id = u.id
|
) lm ON lm.user_id = u.id";
|
||||||
|
|
||||||
|
// Agregar condición de búsqueda si existe
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= "
|
||||||
|
WHERE (
|
||||||
|
u.name LIKE ?
|
||||||
|
OR u.phone_number LIKE ?
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM conversations c2
|
||||||
|
WHERE c2.user_id = u.id
|
||||||
|
AND c2.content LIKE ?
|
||||||
|
)
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= "
|
||||||
GROUP BY u.id";
|
GROUP BY u.id";
|
||||||
|
|
||||||
// Aplicar filtro 'unread' si se solicita
|
// Aplicar filtro 'unread' si se solicita
|
||||||
@@ -63,15 +94,63 @@ try {
|
|||||||
$sql .= " HAVING IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) > 0";
|
$sql .= " HAVING IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) > 0";
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql .= "\n ORDER BY lm.created_at DESC\n LIMIT %d OFFSET %d";
|
// Ordenar por relevancia si hay búsqueda, sino por fecha
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= "\n ORDER BY relevance DESC, lm.created_at DESC";
|
||||||
|
} else {
|
||||||
|
$sql .= "\n ORDER BY lm.created_at DESC";
|
||||||
|
}
|
||||||
|
|
||||||
$conversations = $db->fetchAll(sprintf($sql, $limit, $offset));
|
$sql .= "\n LIMIT %d OFFSET %d";
|
||||||
|
|
||||||
|
// Preparar parámetros para la consulta
|
||||||
|
$params = [];
|
||||||
|
if (!empty($search)) {
|
||||||
|
$searchParam = '%' . $search . '%';
|
||||||
|
// Para el campo de relevancia
|
||||||
|
$params[] = $searchParam; // name LIKE
|
||||||
|
$params[] = $searchParam; // phone_number LIKE
|
||||||
|
// Para el WHERE
|
||||||
|
$params[] = $searchParam; // name LIKE
|
||||||
|
$params[] = $searchParam; // phone_number LIKE
|
||||||
|
$params[] = $searchParam; // content LIKE
|
||||||
|
}
|
||||||
|
|
||||||
|
$finalSql = sprintf($sql, $limit, $offset);
|
||||||
|
|
||||||
|
if (!empty($params)) {
|
||||||
|
$conversations = $db->fetchAll($finalSql, $params);
|
||||||
|
} else {
|
||||||
|
$conversations = $db->fetchAll($finalSql);
|
||||||
|
}
|
||||||
|
|
||||||
// Conteo total de usuarios con al menos una conversación (útil para paginar)
|
// Conteo total de usuarios con al menos una conversación (útil para paginar)
|
||||||
if ($filter === 'unread') {
|
if (!empty($search)) {
|
||||||
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations c WHERE c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)");
|
// Conteo con búsqueda
|
||||||
|
$searchParam = '%' . $search . '%';
|
||||||
|
$countSql = "SELECT COUNT(DISTINCT u.id) AS count
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN conversations c ON c.user_id = u.id
|
||||||
|
WHERE (
|
||||||
|
u.name LIKE ?
|
||||||
|
OR u.phone_number LIKE ?
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM conversations c2
|
||||||
|
WHERE c2.user_id = u.id
|
||||||
|
AND c2.content LIKE ?
|
||||||
|
)
|
||||||
|
)";
|
||||||
|
if ($filter === 'unread') {
|
||||||
|
$countSql .= " AND c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)";
|
||||||
|
}
|
||||||
|
$totalRow = $db->fetch($countSql, [$searchParam, $searchParam, $searchParam]);
|
||||||
} else {
|
} else {
|
||||||
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations");
|
// Conteo normal sin búsqueda
|
||||||
|
if ($filter === 'unread') {
|
||||||
|
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations c WHERE c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)");
|
||||||
|
} else {
|
||||||
|
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$total = isset($totalRow['count']) ? intval($totalRow['count']) : 0;
|
$total = isset($totalRow['count']) ? intval($totalRow['count']) : 0;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Obtener mensajes programados
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Parámetros de filtro
|
||||||
|
$userId = isset($_GET['user_id']) ? intval($_GET['user_id']) : null;
|
||||||
|
$status = $_GET['status'] ?? null;
|
||||||
|
$dateFrom = $_GET['date_from'] ?? null;
|
||||||
|
$dateTo = $_GET['date_to'] ?? null;
|
||||||
|
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : 100;
|
||||||
|
$offset = isset($_GET['offset']) ? intval($_GET['offset']) : 0;
|
||||||
|
|
||||||
|
// Construir query
|
||||||
|
$sql = "SELECT * FROM scheduled_messages_view WHERE 1=1";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($userId) {
|
||||||
|
$sql .= " AND user_id = ?";
|
||||||
|
$params[] = $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status) {
|
||||||
|
$sql .= " AND status = ?";
|
||||||
|
$params[] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dateFrom) {
|
||||||
|
$sql .= " AND scheduled_date >= ?";
|
||||||
|
$params[] = $dateFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dateTo) {
|
||||||
|
$sql .= " AND scheduled_date <= ?";
|
||||||
|
$params[] = $dateTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar por fecha más cercana primero
|
||||||
|
$sql .= " ORDER BY scheduled_date ASC, scheduled_time ASC";
|
||||||
|
|
||||||
|
// Paginación
|
||||||
|
$sql .= " LIMIT ? OFFSET ?";
|
||||||
|
$params[] = $limit;
|
||||||
|
$params[] = $offset;
|
||||||
|
|
||||||
|
$messages = $db->fetchAll($sql, $params);
|
||||||
|
|
||||||
|
error_log('📋 get_scheduled_messages.php - Encontrados: ' . count($messages) . ' mensajes');
|
||||||
|
error_log('🔍 SQL ejecutado: ' . $sql);
|
||||||
|
error_log('📊 Parámetros: ' . json_encode($params));
|
||||||
|
|
||||||
|
// Decodificar parámetros JSON
|
||||||
|
foreach ($messages as &$message) {
|
||||||
|
if ($message['template_parameters']) {
|
||||||
|
$message['template_parameters'] = json_decode($message['template_parameters'], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contar total
|
||||||
|
$countSql = "SELECT COUNT(*) as total FROM scheduled_messages WHERE 1=1";
|
||||||
|
$countParams = [];
|
||||||
|
|
||||||
|
if ($userId) {
|
||||||
|
$countSql .= " AND user_id = ?";
|
||||||
|
$countParams[] = $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status) {
|
||||||
|
$countSql .= " AND status = ?";
|
||||||
|
$countParams[] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dateFrom) {
|
||||||
|
$countSql .= " AND scheduled_date >= ?";
|
||||||
|
$countParams[] = $dateFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dateTo) {
|
||||||
|
$countSql .= " AND scheduled_date <= ?";
|
||||||
|
$countParams[] = $dateTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
$countResult = $db->fetch($countSql, $countParams);
|
||||||
|
$total = $countResult['total'] ?? 0;
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $messages,
|
||||||
|
'total' => $total,
|
||||||
|
'limit' => $limit,
|
||||||
|
'offset' => $offset
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error in get_scheduled_messages.php: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Error obteniendo mensajes programados'
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Obtener detalles completos de una plantilla con variables
|
||||||
|
* Devuelve los parámetros que se deben llenar
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$templateId = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||||
|
$templateName = isset($_GET['name']) ? trim($_GET['name']) : null;
|
||||||
|
$language = isset($_GET['language']) ? trim($_GET['language']) : 'es';
|
||||||
|
|
||||||
|
if (!$templateId && !$templateName) {
|
||||||
|
throw new Exception('Se requiere ID o nombre de plantilla');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
if ($templateId) {
|
||||||
|
$template = $db->fetch(
|
||||||
|
"SELECT * FROM message_templates WHERE id = ?",
|
||||||
|
[$templateId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$template = $db->fetch(
|
||||||
|
"SELECT * FROM message_templates WHERE template_name = ? AND language_code = ?",
|
||||||
|
[$templateName, $language]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
|
throw new Exception('Plantilla no encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraer variables del body_text o de example_parameters
|
||||||
|
$bodyText = $template['body_text'] ?? '';
|
||||||
|
$variables = [];
|
||||||
|
|
||||||
|
// Primero intentar usar las variables guardadas en example_parameters
|
||||||
|
if (!empty($template['example_parameters'])) {
|
||||||
|
$examples = json_decode($template['example_parameters'], true);
|
||||||
|
if (isset($examples['variables']) && is_array($examples['variables'])) {
|
||||||
|
// Usar las variables ya procesadas durante la sincronización
|
||||||
|
foreach ($examples['variables'] as $var) {
|
||||||
|
$varName = $var['name'] ?? $var['index'];
|
||||||
|
$variables[] = [
|
||||||
|
'index' => $var['index'],
|
||||||
|
'placeholder' => $var['placeholder'],
|
||||||
|
'label' => is_numeric($varName) ? 'Variable ' . $varName : ucfirst(str_replace('_', ' ', $varName)),
|
||||||
|
'required' => true,
|
||||||
|
'example' => $var['example'] ?? null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no hay variables guardadas, extraerlas del body_text (fallback)
|
||||||
|
if (empty($variables) && preg_match_all('/\{\{([^\}]+)\}\}/', $bodyText, $matches)) {
|
||||||
|
$uniqueVars = array_unique($matches[1]);
|
||||||
|
|
||||||
|
$index = 1;
|
||||||
|
foreach ($uniqueVars as $varName) {
|
||||||
|
$varIndex = is_numeric($varName) ? intval($varName) : $index++;
|
||||||
|
$variables[] = [
|
||||||
|
'index' => $varIndex,
|
||||||
|
'placeholder' => '{{' . $varName . '}}',
|
||||||
|
'label' => is_numeric($varName) ? 'Variable ' . $varName : ucfirst(str_replace('_', ' ', $varName)),
|
||||||
|
'required' => true,
|
||||||
|
'example' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar respuesta
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'template' => [
|
||||||
|
'id' => $template['id'],
|
||||||
|
'name' => $template['name'],
|
||||||
|
'template_name' => $template['template_name'],
|
||||||
|
'language_code' => $template['language_code'],
|
||||||
|
'category' => $template['category'],
|
||||||
|
'status' => $template['status'],
|
||||||
|
'body_text' => $bodyText,
|
||||||
|
'header_text' => $template['header_text'] ?? null,
|
||||||
|
'header_type' => $template['header_type'] ?? null,
|
||||||
|
'footer_text' => $template['footer_text'] ?? null,
|
||||||
|
'has_variables' => count($variables) > 0,
|
||||||
|
'variables_count' => count($variables),
|
||||||
|
'variables' => $variables
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error in get_template_details.php: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
+16
-2
@@ -27,8 +27,22 @@ try {
|
|||||||
$approvedOnly = isset($_GET['approved_only']) && $_GET['approved_only'] == '1';
|
$approvedOnly = isset($_GET['approved_only']) && $_GET['approved_only'] == '1';
|
||||||
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : null;
|
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : null;
|
||||||
|
|
||||||
// Ajuste: some DBs may not have body_text column; omit it to avoid SQL errors
|
// Incluir todos los campos necesarios para la UI
|
||||||
$sql = "SELECT id, name, template_name, language_code, category, status, created_at FROM message_templates";
|
$sql = "SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
template_name,
|
||||||
|
language_code,
|
||||||
|
category,
|
||||||
|
status,
|
||||||
|
body_text,
|
||||||
|
header_text,
|
||||||
|
header_type,
|
||||||
|
footer_text,
|
||||||
|
components,
|
||||||
|
example_parameters,
|
||||||
|
created_at
|
||||||
|
FROM message_templates";
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($approvedOnly) {
|
if ($approvedOnly) {
|
||||||
|
|||||||
+41
-5
@@ -24,8 +24,30 @@ header('Access-Control-Allow-Headers: Content-Type');
|
|||||||
try {
|
try {
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
|
|
||||||
$users = $db->fetchAll(
|
// Parámetros de paginación
|
||||||
"SELECT
|
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||||
|
$limit = isset($_GET['limit']) ? max(1, min(100, intval($_GET['limit']))) : 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
// Búsqueda opcional
|
||||||
|
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||||
|
$whereClause = '';
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($search)) {
|
||||||
|
$whereClause = "WHERE u.phone_number LIKE ? OR u.name LIKE ? OR u.email LIKE ?";
|
||||||
|
$searchParam = "%{$search}%";
|
||||||
|
$params = [$searchParam, $searchParam, $searchParam];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contar total de registros
|
||||||
|
$countQuery = "SELECT COUNT(*) as total FROM users u {$whereClause}";
|
||||||
|
$totalResult = $db->fetch($countQuery, $params);
|
||||||
|
$total = $totalResult['total'];
|
||||||
|
$totalPages = ceil($total / $limit);
|
||||||
|
|
||||||
|
// Obtener usuarios paginados
|
||||||
|
$query = "SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.phone_number,
|
u.phone_number,
|
||||||
u.name,
|
u.name,
|
||||||
@@ -38,12 +60,26 @@ try {
|
|||||||
m.name as current_menu
|
m.name as current_menu
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN menus m ON u.current_menu_id = m.id
|
LEFT JOIN menus m ON u.current_menu_id = m.id
|
||||||
ORDER BY u.created_at DESC"
|
{$whereClause}
|
||||||
);
|
ORDER BY u.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
$params[] = $limit;
|
||||||
|
$params[] = $offset;
|
||||||
|
|
||||||
|
$users = $db->fetchAll($query, $params);
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => $users
|
'data' => $users,
|
||||||
|
'pagination' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'totalPages' => $totalPages,
|
||||||
|
'hasNext' => $page < $totalPages,
|
||||||
|
'hasPrev' => $page > 1
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Listar usuarios administradores del sistema
|
||||||
|
* Fecha: 3 de febrero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Obtener todos los usuarios administradores
|
||||||
|
$users = $db->fetchAll("
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
is_active,
|
||||||
|
last_login,
|
||||||
|
created_at
|
||||||
|
FROM admin_users
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $users
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Error listing admin users: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Error al obtener usuarios: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Generar vista previa de plantilla con parámetros
|
||||||
|
* Reemplaza las variables y muestra cómo se verá el mensaje final
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
throw new Exception('Datos inválidos');
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateId = $input['template_id'] ?? null;
|
||||||
|
$parameters = $input['parameters'] ?? [];
|
||||||
|
|
||||||
|
if (!$templateId) {
|
||||||
|
throw new Exception('Se requiere ID de plantilla');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$template = $db->fetch(
|
||||||
|
"SELECT * FROM message_templates WHERE id = ?",
|
||||||
|
[$templateId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
|
throw new Exception('Plantilla no encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener textos de la plantilla
|
||||||
|
$bodyText = $template['body_text'] ?? '';
|
||||||
|
$headerText = $template['header_text'] ?? null;
|
||||||
|
$footerText = $template['footer_text'] ?? null;
|
||||||
|
|
||||||
|
// Obtener variables guardadas para saber sus placeholders
|
||||||
|
$exampleParams = json_decode($template['example_parameters'] ?? '{}', true);
|
||||||
|
$savedVariables = $exampleParams['variables'] ?? [];
|
||||||
|
|
||||||
|
// Reemplazar variables en el body
|
||||||
|
$bodyPreview = $bodyText;
|
||||||
|
if (is_array($parameters)) {
|
||||||
|
// Si tenemos variables guardadas, usar sus placeholders
|
||||||
|
if (!empty($savedVariables)) {
|
||||||
|
foreach ($savedVariables as $var) {
|
||||||
|
$index = $var['index'] - 1; // Convertir a índice de array (0-based)
|
||||||
|
if (isset($parameters[$index])) {
|
||||||
|
$placeholder = $var['placeholder']; // Ejemplo: {{nombre_tema}} o {{1}}
|
||||||
|
$bodyPreview = str_replace($placeholder, $parameters[$index], $bodyPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: reemplazar variables numéricas estándar
|
||||||
|
foreach ($parameters as $index => $value) {
|
||||||
|
$placeholder = '{{' . ($index + 1) . '}}';
|
||||||
|
$bodyPreview = str_replace($placeholder, $value, $bodyPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reemplazar variables en el header si existen
|
||||||
|
$headerPreview = $headerText;
|
||||||
|
if ($headerText && is_array($parameters)) {
|
||||||
|
if (!empty($savedVariables)) {
|
||||||
|
foreach ($savedVariables as $var) {
|
||||||
|
$index = $var['index'] - 1;
|
||||||
|
if (isset($parameters[$index])) {
|
||||||
|
$placeholder = $var['placeholder'];
|
||||||
|
$headerPreview = str_replace($placeholder, $parameters[$index], $headerPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($parameters as $index => $value) {
|
||||||
|
$placeholder = '{{' . ($index + 1) . '}}';
|
||||||
|
$headerPreview = str_replace($placeholder, $value, $headerPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si quedan variables sin reemplazar (tanto numéricas como con nombres)
|
||||||
|
$hasUnreplacedVars = preg_match('/\{\{[^\}]+\}\}/', $bodyPreview);
|
||||||
|
|
||||||
|
// Generar vista previa HTML
|
||||||
|
$htmlPreview = '';
|
||||||
|
|
||||||
|
if ($headerPreview) {
|
||||||
|
$htmlPreview .= '<div class="template-header" style="font-weight: bold; margin-bottom: 8px; color: #075e54;">';
|
||||||
|
$htmlPreview .= htmlspecialchars($headerPreview);
|
||||||
|
$htmlPreview .= '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$htmlPreview .= '<div class="template-body" style="margin-bottom: 8px;">';
|
||||||
|
$htmlPreview .= nl2br(htmlspecialchars($bodyPreview));
|
||||||
|
$htmlPreview .= '</div>';
|
||||||
|
|
||||||
|
if ($footerText) {
|
||||||
|
$htmlPreview .= '<div class="template-footer" style="font-size: 0.85em; color: #667781; margin-top: 8px;">';
|
||||||
|
$htmlPreview .= htmlspecialchars($footerText);
|
||||||
|
$htmlPreview .= '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'preview' => [
|
||||||
|
'body' => $bodyPreview,
|
||||||
|
'header' => $headerPreview,
|
||||||
|
'footer' => $footerText,
|
||||||
|
'html' => $htmlPreview,
|
||||||
|
'has_unreplaced_variables' => $hasUnreplacedVars,
|
||||||
|
'is_complete' => !$hasUnreplacedVars
|
||||||
|
]
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error in preview_template.php: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Crear mensaje programado / recordatorio
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
error_log('📥 schedule_message.php - Input recibido: ' . json_encode($input));
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
throw new Exception('Datos inválidos');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar campos requeridos
|
||||||
|
$userId = isset($input['user_id']) ? intval($input['user_id']) : null;
|
||||||
|
$messageType = $input['message_type'] ?? 'template';
|
||||||
|
$scheduledDate = $input['scheduled_date'] ?? null;
|
||||||
|
$scheduledTime = $input['scheduled_time'] ?? '09:00';
|
||||||
|
|
||||||
|
if (!$userId) {
|
||||||
|
throw new Exception('ID de usuario requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scheduledDate) {
|
||||||
|
throw new Exception('Fecha programada requerida');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que la fecha no sea en el pasado
|
||||||
|
$scheduledDateTime = new DateTime($scheduledDate . ' ' . $scheduledTime);
|
||||||
|
$now = new DateTime();
|
||||||
|
|
||||||
|
if ($scheduledDateTime < $now) {
|
||||||
|
throw new Exception('La fecha programada no puede ser en el pasado');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Verificar que el usuario existe
|
||||||
|
$user = $db->fetch("SELECT * FROM users WHERE id = ?", [$userId]);
|
||||||
|
if (!$user) {
|
||||||
|
throw new Exception('Usuario no encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar datos según el tipo de mensaje
|
||||||
|
$templateId = null;
|
||||||
|
$templateName = null;
|
||||||
|
$templateLanguage = 'es';
|
||||||
|
$templateParameters = null;
|
||||||
|
$messageContent = null;
|
||||||
|
|
||||||
|
if ($messageType === 'template') {
|
||||||
|
$templateId = isset($input['template_id']) ? intval($input['template_id']) : null;
|
||||||
|
$templateName = $input['template_name'] ?? null;
|
||||||
|
$templateLanguage = $input['template_language'] ?? 'es';
|
||||||
|
$templateParameters = $input['template_parameters'] ?? null;
|
||||||
|
|
||||||
|
error_log('🔍 template_parameters recibido: ' . json_encode($templateParameters));
|
||||||
|
error_log('🔍 Es array? ' . (is_array($templateParameters) ? 'Sí' : 'No'));
|
||||||
|
|
||||||
|
if (!$templateId && !$templateName) {
|
||||||
|
throw new Exception('Se requiere ID o nombre de plantilla');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si se proporcionaron parámetros, convertir a JSON
|
||||||
|
if ($templateParameters && is_array($templateParameters)) {
|
||||||
|
$templateParameters = json_encode($templateParameters, JSON_UNESCAPED_UNICODE);
|
||||||
|
error_log('✅ Convertido a JSON: ' . $templateParameters);
|
||||||
|
} else {
|
||||||
|
error_log('⚠️ No se convirtió - es null o no es array');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if ($messageType === 'text') {
|
||||||
|
$messageContent = $input['message_content'] ?? null;
|
||||||
|
|
||||||
|
if (empty($messageContent)) {
|
||||||
|
throw new Exception('Contenido del mensaje requerido');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Exception('Tipo de mensaje no soportado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener ID del admin actual
|
||||||
|
$createdBy = $_SESSION['admin_user']['id'] ?? $_SESSION['user_id'] ?? null;
|
||||||
|
|
||||||
|
// Insertar mensaje programado
|
||||||
|
$db->execute(
|
||||||
|
"INSERT INTO scheduled_messages (
|
||||||
|
user_id,
|
||||||
|
template_id,
|
||||||
|
template_name,
|
||||||
|
template_language,
|
||||||
|
template_parameters,
|
||||||
|
message_type,
|
||||||
|
message_content,
|
||||||
|
scheduled_date,
|
||||||
|
scheduled_time,
|
||||||
|
created_by,
|
||||||
|
status
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')",
|
||||||
|
[
|
||||||
|
$userId,
|
||||||
|
$templateId,
|
||||||
|
$templateName,
|
||||||
|
$templateLanguage,
|
||||||
|
$templateParameters,
|
||||||
|
$messageType,
|
||||||
|
$messageContent,
|
||||||
|
$scheduledDate,
|
||||||
|
$scheduledTime,
|
||||||
|
$createdBy
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$scheduledId = $db->lastInsertId();
|
||||||
|
|
||||||
|
error_log('✅ Mensaje programado guardado con ID: ' . $scheduledId);
|
||||||
|
|
||||||
|
// Log
|
||||||
|
writeLog('INFO', "Mensaje programado creado: ID={$scheduledId}, Usuario={$userId}, Fecha={$scheduledDate} {$scheduledTime}");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Recordatorio programado exitosamente',
|
||||||
|
'data' => [
|
||||||
|
'id' => $scheduledId,
|
||||||
|
'scheduled_date' => $scheduledDate,
|
||||||
|
'scheduled_time' => $scheduledTime,
|
||||||
|
'user_name' => $user['name'],
|
||||||
|
'phone_number' => $user['phone_number']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error in schedule_message.php: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
+189
-27
@@ -23,55 +23,214 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|||||||
try {
|
try {
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
if (!$input || !isset($input['message'])) {
|
if (!$input) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'Mensaje requerido']);
|
echo json_encode(['error' => 'Datos requeridos']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filter = $input['filter'] ?? 'all';
|
$selectionType = $input['selection_type'] ?? 'filter';
|
||||||
$message = $input['message'];
|
$messageType = $input['message_type'] ?? 'text';
|
||||||
|
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
$whatsappService = new WhatsAppService();
|
$whatsappService = new WhatsAppService();
|
||||||
|
|
||||||
// Construir query según filtro
|
// Obtener usuarios según tipo de selección
|
||||||
$whereClause = "WHERE u.status = 'active'";
|
$users = [];
|
||||||
|
|
||||||
switch ($filter) {
|
if ($selectionType === 'specific') {
|
||||||
case 'active':
|
// Usuarios específicos seleccionados
|
||||||
$whereClause .= " AND EXISTS (
|
if (!isset($input['user_ids']) || !is_array($input['user_ids']) || empty($input['user_ids'])) {
|
||||||
SELECT 1 FROM conversations c
|
http_response_code(400);
|
||||||
WHERE c.user_id = u.id
|
echo json_encode(['error' => 'Debes seleccionar al menos un usuario']);
|
||||||
AND c.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
exit;
|
||||||
)";
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case 'recent':
|
$userIds = array_map('intval', $input['user_ids']);
|
||||||
$whereClause .= " AND u.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
|
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
|
||||||
break;
|
$query = "SELECT id, phone_number, name FROM users WHERE id IN ($placeholders) ORDER BY id ASC";
|
||||||
|
$users = $db->fetchAll($query, $userIds);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Por filtro
|
||||||
|
$filter = $input['filter'] ?? 'all';
|
||||||
|
$whereClause = "1=1";
|
||||||
|
|
||||||
|
switch ($filter) {
|
||||||
|
case 'active':
|
||||||
|
$whereClause .= " AND EXISTS (
|
||||||
|
SELECT 1 FROM conversations c
|
||||||
|
WHERE c.user_id = u.id
|
||||||
|
AND c.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||||
|
)";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'recent':
|
||||||
|
$whereClause .= " AND u.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'all':
|
||||||
|
// Incluir todos los usuarios
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = "SELECT id, phone_number, name FROM users u WHERE " . $whereClause . " ORDER BY id ASC";
|
||||||
|
$users = $db->fetchAll($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener usuarios según filtro
|
if (empty($users)) {
|
||||||
$users = $db->fetchAll(
|
http_response_code(404);
|
||||||
"SELECT phone_number FROM users u " . $whereClause
|
echo json_encode([
|
||||||
);
|
'error' => 'No se encontraron usuarios para enviar',
|
||||||
|
'selection_type' => $selectionType
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$sentCount = 0;
|
$sentCount = 0;
|
||||||
$errorCount = 0;
|
$errorCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
try {
|
try {
|
||||||
$response = $whatsappService->sendTextMessage($user['phone_number'], $message);
|
$response = null;
|
||||||
if ($response) {
|
$messageContent = '';
|
||||||
$sentCount++;
|
|
||||||
|
// Enviar según tipo de mensaje
|
||||||
|
if ($messageType === 'template') {
|
||||||
|
// Enviar plantilla
|
||||||
|
if (!isset($input['template_id'])) {
|
||||||
|
throw new Exception('ID de plantilla requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateId = intval($input['template_id']);
|
||||||
|
$template = $db->fetch("SELECT * FROM message_templates WHERE id = ?", [$templateId]);
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
|
throw new Exception('Plantilla no encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = strtoupper($template['status']);
|
||||||
|
if ($status !== 'APPROVED') {
|
||||||
|
throw new Exception('La plantilla no está aprobada. Status: ' . $template['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar variables
|
||||||
|
$templateVariables = $input['template_variables'] ?? [];
|
||||||
|
|
||||||
|
error_log('📝 Variables recibidas: ' . json_encode($templateVariables));
|
||||||
|
error_log('🔍 Tipo: ' . gettype($templateVariables));
|
||||||
|
|
||||||
|
// Detectar si son variables numéricas o con nombres
|
||||||
|
$isNumericVars = false;
|
||||||
|
$isNamedVars = false;
|
||||||
|
|
||||||
|
if (is_array($templateVariables) && !empty($templateVariables)) {
|
||||||
|
$firstKey = array_key_first($templateVariables);
|
||||||
|
if (is_int($firstKey) && isset($templateVariables[0])) {
|
||||||
|
// Array indexado numéricamente [0 => "val1", 1 => "val2"]
|
||||||
|
$isNumericVars = true;
|
||||||
|
error_log('✅ Variables tipo: Array numérico indexado');
|
||||||
|
} elseif (is_string($firstKey) && !is_numeric($firstKey)) {
|
||||||
|
// Array asociativo con nombres ["nombre_tema" => "val1", "fecha" => "val2"]
|
||||||
|
$isNamedVars = true;
|
||||||
|
error_log('✅ Variables tipo: Objeto con nombres (asociativo)');
|
||||||
|
} elseif (is_numeric($firstKey)) {
|
||||||
|
// Array con claves numéricas [1 => "val1", 2 => "val2"]
|
||||||
|
$isNumericVars = true;
|
||||||
|
error_log('✅ Variables tipo: Array con claves numéricas');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesar según el tipo
|
||||||
|
$processedVariables = [];
|
||||||
|
|
||||||
|
if ($isNamedVars) {
|
||||||
|
// Variables con nombres: pasar como objeto asociativo
|
||||||
|
$processedVariables = $templateVariables;
|
||||||
|
error_log('📦 Enviando como objeto con nombres: ' . json_encode($processedVariables));
|
||||||
|
} elseif ($isNumericVars) {
|
||||||
|
// Variables numéricas: ordenar y convertir a array indexado
|
||||||
|
ksort($templateVariables, SORT_NUMERIC);
|
||||||
|
foreach ($templateVariables as $value) {
|
||||||
|
$processedVariables[] = $value;
|
||||||
|
}
|
||||||
|
error_log('📊 Enviando como array ordenado: ' . json_encode($processedVariables));
|
||||||
|
} else {
|
||||||
|
error_log('⚠️ Sin variables o formato desconocido');
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('📤 Variables procesadas: ' . json_encode($processedVariables));
|
||||||
|
|
||||||
|
// Verificar variables si la plantilla usa formato numérico
|
||||||
|
$expectedVars = preg_match_all('/\{\{(\d+)\}\}/', $template['body_text'] ?? '', $matches);
|
||||||
|
if ($expectedVars > 0 && $isNumericVars && count($processedVariables) !== $expectedVars) {
|
||||||
|
throw new Exception("La plantilla requiere {$expectedVars} variable(s) numéricas, pero se enviaron " . count($processedVariables));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $whatsappService->sendTemplateMessage(
|
||||||
|
$user['phone_number'],
|
||||||
|
$template['template_name'] ?? $template['name'],
|
||||||
|
$template['language_code'] ?? 'es',
|
||||||
|
$processedVariables
|
||||||
|
);
|
||||||
|
|
||||||
|
// Construir contenido para BD (reemplazar variables en body_text)
|
||||||
|
$messageContent = $template['body_text'] ?? $template['name'];
|
||||||
|
if ($isNamedVars) {
|
||||||
|
// Reemplazar por nombres
|
||||||
|
foreach ($templateVariables as $key => $value) {
|
||||||
|
$messageContent = str_replace('{{' . $key . '}}', $value, $messageContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reemplazar por índice numérico
|
||||||
|
foreach ($processedVariables as $index => $value) {
|
||||||
|
$messageContent = str_replace('{{' . ($index + 1) . '}}', $value, $messageContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Enviar texto simple
|
||||||
|
$message = trim($input['message'] ?? '');
|
||||||
|
if (empty($message)) {
|
||||||
|
throw new Exception('Mensaje vacío');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $whatsappService->sendTextMessage($user['phone_number'], $message);
|
||||||
|
$messageContent = $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pequeña pausa para no saturar la API
|
if ($response && isset($response['success']) && $response['success']) {
|
||||||
usleep(100000); // 0.1 segundo
|
$sentCount++;
|
||||||
|
|
||||||
|
// Guardar mensaje en BD
|
||||||
|
$db->insert('conversations', [
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'message_id' => $response['message_id'] ?? null,
|
||||||
|
'content' => $messageContent,
|
||||||
|
'direction' => 'outgoing',
|
||||||
|
'message_type' => $messageType === 'template' ? 'template' : 'text',
|
||||||
|
'status' => 'sent',
|
||||||
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$errorCount++;
|
||||||
|
$errors[] = [
|
||||||
|
'phone' => $user['phone_number'],
|
||||||
|
'error' => 'Respuesta inválida de WhatsApp'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pausa para no saturar la API (250ms entre mensajes)
|
||||||
|
usleep(250000);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$errorCount++;
|
$errorCount++;
|
||||||
|
$errors[] = [
|
||||||
|
'phone' => $user['phone_number'],
|
||||||
|
'name' => $user['name'] ?? 'Sin nombre',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
error_log("Error sending broadcast to {$user['phone_number']}: " . $e->getMessage());
|
error_log("Error sending broadcast to {$user['phone_number']}: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +239,10 @@ try {
|
|||||||
'success' => true,
|
'success' => true,
|
||||||
'sent_count' => $sentCount,
|
'sent_count' => $sentCount,
|
||||||
'error_count' => $errorCount,
|
'error_count' => $errorCount,
|
||||||
'total_users' => count($users)
|
'total_users' => count($users),
|
||||||
|
'selection_type' => $selectionType,
|
||||||
|
'message_type' => $messageType,
|
||||||
|
'errors' => $errorCount > 0 ? $errors : null
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
@@ -239,3 +239,27 @@
|
|||||||
[2026-01-29 11:39:41] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
|
[2026-01-29 11:39:41] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
|
||||||
[2026-01-29 11:39:41] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
|
[2026-01-29 11:39:41] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
|
||||||
[2026-01-29 11:39:41] Saving to DB - content: record_1769704778262.webm, media_url: 906365935662402, local_file: uploads/media_697b8d4b88a6c2.66006027.ogg, local_thumb: null
|
[2026-01-29 11:39:41] Saving to DB - content: record_1769704778262.webm, media_url: 906365935662402, local_file: uploads/media_697b8d4b88a6c2.66006027.ogg, local_thumb: null
|
||||||
|
[2026-02-02 21:43:43] Raw input: {"recipient":"573168950803","media_url":"https://bot.u-s.app/uploads/media_698160df42ea78.64430762.ogg","media_type":"audio","caption":null,"filename":"record_1770086620567.webm","is_voice":true}
|
||||||
|
[2026-02-02 21:43:43] Input decoded: {"recipient":"573168950803","media_url":"https:\/\/bot.u-s.app\/uploads\/media_698160df42ea78.64430762.ogg","media_type":"audio","caption":null,"filename":"record_1770086620567.webm","is_voice":true}
|
||||||
|
[2026-02-02 21:43:43] is_voice: true
|
||||||
|
[2026-02-02 21:43:44] Upload result: {"id":"1591364868775705"}
|
||||||
|
[2026-02-02 21:43:44] Media ID obtained: 1591364868775705
|
||||||
|
[2026-02-02 21:43:45] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMDhGRTNDMUQ5NEFGQTRBQ0FDAA=="}]}
|
||||||
|
[2026-02-02 21:43:45] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMDhGRTNDMUQ5NEFGQTRBQ0FDAA=="}]}
|
||||||
|
[2026-02-02 21:43:45] Saving to DB - content: record_1770086620567.webm, media_url: 1591364868775705, local_file: uploads/media_698160df42ea78.64430762.ogg, local_thumb: null
|
||||||
|
[2026-02-03 08:52:11] Raw input: {"recipient":"573168950803","media_url":"https://bot.u-s.app/uploads/media_6981fd8bb34da0.05106104.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
|
||||||
|
[2026-02-03 08:52:11] Input decoded: {"recipient":"573168950803","media_url":"https:\/\/bot.u-s.app\/uploads\/media_6981fd8bb34da0.05106104.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
|
||||||
|
[2026-02-03 08:52:11] is_voice: false
|
||||||
|
[2026-02-03 08:52:12] Upload result: {"id":"817602961329813"}
|
||||||
|
[2026-02-03 08:52:12] Media ID obtained: 817602961329813
|
||||||
|
[2026-02-03 08:52:13] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMUVGMkUzOTNCNjBENTE4OEI1AA=="}]}
|
||||||
|
[2026-02-03 08:52:13] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMUVGMkUzOTNCNjBENTE4OEI1AA=="}]}
|
||||||
|
[2026-02-03 08:52:13] Saving to DB - content: 20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx, media_url: 817602961329813, local_file: uploads/media_6981fd8bb34da0.05106104.xlsx, local_thumb: null
|
||||||
|
[2026-02-03 16:20:01] Raw input: {"recipient":"573022548060","media_url":"https://bot.u-s.app/uploads/media_69826681178109.48762271.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
|
||||||
|
[2026-02-03 16:20:01] Input decoded: {"recipient":"573022548060","media_url":"https:\/\/bot.u-s.app\/uploads\/media_69826681178109.48762271.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
|
||||||
|
[2026-02-03 16:20:01] is_voice: false
|
||||||
|
[2026-02-03 16:20:02] Upload result: {"id":"876532491851564"}
|
||||||
|
[2026-02-03 16:20:02] Media ID obtained: 876532491851564
|
||||||
|
[2026-02-03 16:20:02] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573022548060","wa_id":"573022548060"}],"messages":[{"id":"wamid.HBgMNTczMDIyNTQ4MDYwFQIAERgSMjAzNDlFQTVENzA2OEQ5QzkwAA=="}]}
|
||||||
|
[2026-02-03 16:20:02] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573022548060","wa_id":"573022548060"}],"messages":[{"id":"wamid.HBgMNTczMDIyNTQ4MDYwFQIAERgSMjAzNDlFQTVENzA2OEQ5QzkwAA=="}]}
|
||||||
|
[2026-02-03 16:20:02] Saving to DB - content: 20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx, media_url: 876532491851564, local_file: uploads/media_69826681178109.48762271.xlsx, local_thumb: null
|
||||||
|
|||||||
+10
-3
@@ -163,10 +163,17 @@ try {
|
|||||||
echo json_encode(['error' => 'Mensaje requerido']);
|
echo json_encode(['error' => 'Mensaje requerido']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Procesar saltos de línea escapados (\\n o \n) -> convertir a saltos reales
|
||||||
|
$message = $input['message'];
|
||||||
|
$message = str_replace('\\n', "\n", $message); // \n escapado literal
|
||||||
|
$message = preg_replace('/(?<!\\\\)\\\\n/', "\n", $message); // \n en string
|
||||||
|
|
||||||
// DEBUG: Mostrar datos de envío de mensaje de texto
|
// DEBUG: Mostrar datos de envío de mensaje de texto
|
||||||
error_log("=== DEBUG WHATSAPP TEXT MESSAGE ===");
|
error_log("=== DEBUG WHATSAPP TEXT MESSAGE ===");
|
||||||
error_log("Recipient: " . $recipient);
|
error_log("Recipient: " . $recipient);
|
||||||
error_log("Message: " . $input['message']);
|
error_log("Message original: " . $input['message']);
|
||||||
|
error_log("Message processed: " . $message);
|
||||||
error_log("Method: sendTextMessage()/sendTextReply()");
|
error_log("Method: sendTextMessage()/sendTextReply()");
|
||||||
error_log("====================================");
|
error_log("====================================");
|
||||||
|
|
||||||
@@ -175,9 +182,9 @@ try {
|
|||||||
|
|
||||||
if (!empty($input['reply_to'])) {
|
if (!empty($input['reply_to'])) {
|
||||||
$replyTo = $input['reply_to'];
|
$replyTo = $input['reply_to'];
|
||||||
$response = $whatsappService->sendTextReply($recipient, $replyTo, $input['message'], $operatorId ? ['operator_id' => $operatorId] : null);
|
$response = $whatsappService->sendTextReply($recipient, $replyTo, $message, $operatorId ? ['operator_id' => $operatorId] : null);
|
||||||
} else {
|
} else {
|
||||||
$response = $whatsappService->sendTextMessage($recipient, $input['message'], $operatorId ? ['operator_id' => $operatorId] : null);
|
$response = $whatsappService->sendTextMessage($recipient, $message, $operatorId ? ['operator_id' => $operatorId] : null);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Sincronizar plantillas desde Facebook/WhatsApp Business API
|
||||||
|
* Obtiene las plantillas desde la API de WhatsApp y las guarda en la base de datos
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Obtener configuración de WhatsApp
|
||||||
|
$token = getConfigFromDB('whatsapp_token', '');
|
||||||
|
$wabaId = getConfigFromDB('whatsapp_business_account_id', '');
|
||||||
|
|
||||||
|
if (empty($token)) {
|
||||||
|
throw new Exception('Token de WhatsApp no configurado');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($wabaId)) {
|
||||||
|
throw new Exception('Business Account ID no configurado. Configure el WABA ID en la configuración del sistema.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hacer petición a la API de WhatsApp para obtener plantillas
|
||||||
|
$url = "https://graph.facebook.com/v21.0/{$wabaId}/message_templates";
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
"Authorization: Bearer {$token}",
|
||||||
|
"Content-Type: application/json"
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
throw new Exception("Error de conexión: {$error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
$errorData = json_decode($response, true);
|
||||||
|
$errorMsg = $errorData['error']['message'] ?? 'Error desconocido';
|
||||||
|
throw new Exception("Error de API ({$httpCode}): {$errorMsg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
|
||||||
|
// LOG: Ver respuesta completa de WhatsApp
|
||||||
|
error_log('📥 Respuesta de WhatsApp API: ' . substr($response, 0, 2000));
|
||||||
|
error_log('📊 Total de plantillas recibidas: ' . count($data['data'] ?? []));
|
||||||
|
|
||||||
|
if (!isset($data['data']) || !is_array($data['data'])) {
|
||||||
|
throw new Exception('Respuesta inválida de la API de WhatsApp');
|
||||||
|
}
|
||||||
|
|
||||||
|
$templates = $data['data'];
|
||||||
|
|
||||||
|
// LOG: Ver primera plantilla como ejemplo
|
||||||
|
if (!empty($templates)) {
|
||||||
|
error_log('📋 Ejemplo de plantilla recibida: ' . json_encode($templates[0], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
$syncedCount = 0;
|
||||||
|
$updatedCount = 0;
|
||||||
|
$skippedCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($templates as $template) {
|
||||||
|
try {
|
||||||
|
$templateName = $template['name'] ?? '';
|
||||||
|
$language = $template['language'] ?? 'es';
|
||||||
|
$status = $template['status'] ?? 'pending';
|
||||||
|
$category = $template['category'] ?? 'UTILITY';
|
||||||
|
$components = $template['components'] ?? [];
|
||||||
|
|
||||||
|
if (empty($templateName)) {
|
||||||
|
$skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraer información de los componentes
|
||||||
|
$bodyText = null;
|
||||||
|
$headerText = null;
|
||||||
|
$headerType = null;
|
||||||
|
$footerText = null;
|
||||||
|
$exampleParameters = [];
|
||||||
|
|
||||||
|
foreach ($components as $component) {
|
||||||
|
$type = $component['type'] ?? '';
|
||||||
|
|
||||||
|
if ($type === 'BODY') {
|
||||||
|
$bodyText = $component['text'] ?? null;
|
||||||
|
// Extraer ejemplos de parámetros del body
|
||||||
|
if (isset($component['example']['body_text'])) {
|
||||||
|
$exampleParameters['body'] = $component['example']['body_text'];
|
||||||
|
}
|
||||||
|
} elseif ($type === 'HEADER') {
|
||||||
|
$headerText = $component['text'] ?? null;
|
||||||
|
$headerType = strtolower($component['format'] ?? 'text');
|
||||||
|
// Extraer ejemplos del header
|
||||||
|
if (isset($component['example']['header_text'])) {
|
||||||
|
$exampleParameters['header'] = $component['example']['header_text'];
|
||||||
|
}
|
||||||
|
} elseif ($type === 'FOOTER') {
|
||||||
|
$footerText = $component['text'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NUEVO: Extraer variables automáticamente del body_text
|
||||||
|
$variables = [];
|
||||||
|
if ($bodyText) {
|
||||||
|
// Buscar tanto variables numéricas {{1}} como con nombres {{nombre_tema}}
|
||||||
|
preg_match_all('/\{\{([^\}]+)\}\}/', $bodyText, $matches);
|
||||||
|
if (!empty($matches[1])) {
|
||||||
|
$uniqueVars = array_unique($matches[1]);
|
||||||
|
|
||||||
|
error_log("🔍 Template '{$templateName}' - Variables encontradas: " . json_encode($matches[1]));
|
||||||
|
|
||||||
|
$index = 1;
|
||||||
|
foreach ($uniqueVars as $varName) {
|
||||||
|
$example = null;
|
||||||
|
|
||||||
|
// Si la variable es numérica, usar su índice
|
||||||
|
if (is_numeric($varName)) {
|
||||||
|
$varIndex = (int)$varName;
|
||||||
|
// Los ejemplos de WhatsApp vienen como array de arrays: [["valor"]]
|
||||||
|
if (isset($exampleParameters['body'][$varIndex - 1])) {
|
||||||
|
$exampleData = $exampleParameters['body'][$varIndex - 1];
|
||||||
|
$example = is_array($exampleData) ? $exampleData[0] : $exampleData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Para variables con nombre, usar índice secuencial
|
||||||
|
$varIndex = $index;
|
||||||
|
if (isset($exampleParameters['body'][$index - 1])) {
|
||||||
|
$exampleData = $exampleParameters['body'][$index - 1];
|
||||||
|
$example = is_array($exampleData) ? $exampleData[0] : $exampleData;
|
||||||
|
}
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$variables[] = [
|
||||||
|
'index' => $varIndex,
|
||||||
|
'placeholder' => "{{" . $varName . "}}",
|
||||||
|
'name' => $varName,
|
||||||
|
'example' => $example
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("✅ Variables procesadas para '{$templateName}': " . json_encode($variables, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si encontramos variables, agregarlas a example_parameters
|
||||||
|
if (!empty($variables)) {
|
||||||
|
$exampleParameters['variables'] = $variables;
|
||||||
|
error_log("💾 example_parameters final para '{$templateName}': " . json_encode($exampleParameters, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar datos para guardar
|
||||||
|
$componentsJson = !empty($components) ? json_encode($components, JSON_UNESCAPED_UNICODE) : null;
|
||||||
|
$exampleJson = !empty($exampleParameters) ? json_encode($exampleParameters, JSON_UNESCAPED_UNICODE) : null;
|
||||||
|
|
||||||
|
// Verificar si la plantilla ya existe
|
||||||
|
$existing = $db->fetch(
|
||||||
|
"SELECT id, status FROM message_templates WHERE template_name = ? AND language_code = ?",
|
||||||
|
[$templateName, $language]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Actualizar plantilla existente con nuevos campos
|
||||||
|
$db->execute(
|
||||||
|
"UPDATE message_templates SET
|
||||||
|
status = ?,
|
||||||
|
category = ?,
|
||||||
|
body_text = ?,
|
||||||
|
header_text = ?,
|
||||||
|
header_type = ?,
|
||||||
|
footer_text = ?,
|
||||||
|
components = ?,
|
||||||
|
example_parameters = ?,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?",
|
||||||
|
[
|
||||||
|
strtolower($status),
|
||||||
|
strtolower($category),
|
||||||
|
$bodyText,
|
||||||
|
$headerText,
|
||||||
|
$headerType,
|
||||||
|
$footerText,
|
||||||
|
$componentsJson,
|
||||||
|
$exampleJson,
|
||||||
|
$existing['id']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$updatedCount++;
|
||||||
|
} else {
|
||||||
|
// Insertar nueva plantilla con componentes
|
||||||
|
$db->execute(
|
||||||
|
"INSERT INTO message_templates (
|
||||||
|
name,
|
||||||
|
template_name,
|
||||||
|
language_code,
|
||||||
|
category,
|
||||||
|
status,
|
||||||
|
body_text,
|
||||||
|
header_text,
|
||||||
|
header_type,
|
||||||
|
footer_text,
|
||||||
|
components,
|
||||||
|
example_parameters,
|
||||||
|
created_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())",
|
||||||
|
[
|
||||||
|
$templateName, // nombre descriptivo igual al nombre técnico
|
||||||
|
$templateName,
|
||||||
|
$language,
|
||||||
|
strtolower($category),
|
||||||
|
strtolower($status),
|
||||||
|
$bodyText,
|
||||||
|
$headerText,
|
||||||
|
$headerType,
|
||||||
|
$footerText,
|
||||||
|
$componentsJson,
|
||||||
|
$exampleJson
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$syncedCount++;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors[] = "Error procesando plantilla '{$templateName}': " . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log de la sincronización
|
||||||
|
writeLog('INFO', "Plantillas sincronizadas desde Facebook: {$syncedCount} nuevas, {$updatedCount} actualizadas, {$skippedCount} sin cambios");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Plantillas sincronizadas correctamente',
|
||||||
|
'data' => [
|
||||||
|
'total' => count($templates),
|
||||||
|
'synced' => $syncedCount,
|
||||||
|
'updated' => $updatedCount,
|
||||||
|
'skipped' => $skippedCount,
|
||||||
|
'errors' => $errors
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error in sync_templates_from_facebook.php: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Activar/Desactivar usuario administrador
|
||||||
|
* Fecha: 3 de febrero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Leer datos del request
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$userId = $input['user_id'] ?? null;
|
||||||
|
$isActive = $input['is_active'] ?? null;
|
||||||
|
|
||||||
|
if (!$userId) {
|
||||||
|
throw new Exception('ID de usuario requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
// No permitir desactivar el propio usuario
|
||||||
|
if ($userId == $_SESSION['user_id']) {
|
||||||
|
throw new Exception('No puedes desactivar tu propio usuario');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar estado
|
||||||
|
$db->execute(
|
||||||
|
"UPDATE admin_users SET is_active = ? WHERE id = ?",
|
||||||
|
[$isActive ? 1 : 0, $userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Estado actualizado correctamente'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Error toggling admin user status: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Actualizar usuario administrador
|
||||||
|
* Fecha: 3 de febrero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Leer datos del request
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$userId = $input['user_id'] ?? null;
|
||||||
|
$username = trim($input['username'] ?? '');
|
||||||
|
$fullName = trim($input['full_name'] ?? '');
|
||||||
|
$email = trim($input['email'] ?? '');
|
||||||
|
|
||||||
|
if (!$userId) {
|
||||||
|
throw new Exception('ID de usuario requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($username)) {
|
||||||
|
throw new Exception('El nombre de usuario es requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el username ya existe (excepto para el usuario actual)
|
||||||
|
$existing = $db->fetch(
|
||||||
|
"SELECT id FROM admin_users WHERE username = ? AND id != ?",
|
||||||
|
[$username, $userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
throw new Exception('El nombre de usuario ya está en uso');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar usuario
|
||||||
|
$db->execute(
|
||||||
|
"UPDATE admin_users SET username = ?, full_name = ?, email = ? WHERE id = ?",
|
||||||
|
[$username, $fullName, $email, $userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Usuario actualizado correctamente'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Error updating admin user: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Actualizar mensaje programado
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../config/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: PUT, POST');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
throw new Exception('Datos inválidos');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($input['id']) ? intval($input['id']) : null;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
throw new Exception('ID de mensaje programado requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Verificar que existe
|
||||||
|
$existing = $db->fetch("SELECT * FROM scheduled_messages WHERE id = ?", [$id]);
|
||||||
|
|
||||||
|
if (!$existing) {
|
||||||
|
throw new Exception('Mensaje programado no encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// No permitir editar mensajes ya enviados
|
||||||
|
if ($existing['status'] === 'sent') {
|
||||||
|
throw new Exception('No se puede editar un mensaje ya enviado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar campos a actualizar
|
||||||
|
$updates = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (isset($input['scheduled_date'])) {
|
||||||
|
$updates[] = "scheduled_date = ?";
|
||||||
|
$params[] = $input['scheduled_date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($input['scheduled_time'])) {
|
||||||
|
$updates[] = "scheduled_time = ?";
|
||||||
|
$params[] = $input['scheduled_time'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($input['template_parameters'])) {
|
||||||
|
$updates[] = "template_parameters = ?";
|
||||||
|
$params[] = is_array($input['template_parameters'])
|
||||||
|
? json_encode($input['template_parameters'], JSON_UNESCAPED_UNICODE)
|
||||||
|
: $input['template_parameters'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($input['message_content'])) {
|
||||||
|
$updates[] = "message_content = ?";
|
||||||
|
$params[] = $input['message_content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($input['status'])) {
|
||||||
|
$updates[] = "status = ?";
|
||||||
|
$params[] = $input['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($updates)) {
|
||||||
|
throw new Exception('No hay campos para actualizar');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar updated_at
|
||||||
|
$updates[] = "updated_at = NOW()";
|
||||||
|
|
||||||
|
// Agregar ID al final de params
|
||||||
|
$params[] = $id;
|
||||||
|
|
||||||
|
// Ejecutar actualización
|
||||||
|
$sql = "UPDATE scheduled_messages SET " . implode(', ', $updates) . " WHERE id = ?";
|
||||||
|
$db->execute($sql, $params);
|
||||||
|
|
||||||
|
writeLog('INFO', "Mensaje programado actualizado: ID={$id}");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Mensaje programado actualizado exitosamente'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error in update_scheduled_message.php: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
+12
-4
@@ -163,10 +163,18 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construir URL pública
|
// Construir URL pública usando APP_URL del .env si está disponible
|
||||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
// Esto asegura que los archivos sean accesibles públicamente desde WhatsApp (datos móviles)
|
||||||
$host = $_SERVER['HTTP_HOST'];
|
$appUrl = getenv('APP_URL');
|
||||||
$publicUrl = $protocol . '://' . $host . '/uploads/' . $uniqueName;
|
if ($appUrl && $appUrl !== '') {
|
||||||
|
// Usar APP_URL del .env (ej: https://bot.u-s.app/)
|
||||||
|
$publicUrl = rtrim($appUrl, '/') . '/uploads/' . $uniqueName;
|
||||||
|
} else {
|
||||||
|
// Fallback: auto-detectar desde HTTP_HOST (desarrollo local)
|
||||||
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'];
|
||||||
|
$publicUrl = $protocol . '://' . $host . '/uploads/' . $uniqueName;
|
||||||
|
}
|
||||||
|
|
||||||
// Determinar tipo de medio
|
// Determinar tipo de medio
|
||||||
$mediaType = 'document';
|
$mediaType = 'document';
|
||||||
|
|||||||
@@ -279,3 +279,138 @@
|
|||||||
[2026-01-28 03:19:02] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
[2026-01-28 03:19:02] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
[2026-01-28 03:19:02] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1769588642&hash=ARkCJbkmByU2tdLOv2EHpSH0F12MJjM_Oqj-hiGgNVSYAg
|
[2026-01-28 03:19:02] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1769588642&hash=ARkCJbkmByU2tdLOv2EHpSH0F12MJjM_Oqj-hiGgNVSYAg
|
||||||
[2026-01-28 03:19:02] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1769588642&hash=ARl75SVpu4vAnsH1AXoBGSv5sjftMr7JPocagoN5kIdMWQ
|
[2026-01-28 03:19:02] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1769588642&hash=ARl75SVpu4vAnsH1AXoBGSv5sjftMr7JPocagoN5kIdMWQ
|
||||||
|
[2026-02-03 08:52:36] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 08:52:36] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 08:52:36] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 08:52:36] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127056&hash=ARmn_DQsFhxynN1KXT-Ge4Xf8WLnjXtDlWoDsUomB4lLKg
|
||||||
|
[2026-02-03 08:52:36] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127056&hash=ARnKjD4rWdn1Lf97robC2d7AyEHpxH6a2_oPLhmgZ0I9pA
|
||||||
|
[2026-02-03 08:52:57] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 08:52:57] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 08:52:57] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 08:52:57] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127077&hash=ARldzoeUm09JzYXDKy0ypZS4VLSt5A541gqPHgUu-u9geA
|
||||||
|
[2026-02-03 08:52:57] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127077&hash=ARmAMwa-nPL80hEmwrHHiSHiMhllm6JUl3UG413ZbMkpnA
|
||||||
|
[2026-02-03 08:52:59] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 08:52:59] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 08:52:59] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 08:52:59] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127079&hash=ARmB9s1otyY0Zp-uU577Ezen_iMY_waheMG8EMOMTf4yvA
|
||||||
|
[2026-02-03 08:52:59] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127079&hash=ARlMcDHlgltZRpheK41ar-GLM_sSPXV_8yudSsLXa4IOJw
|
||||||
|
[2026-02-03 08:53:52] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 08:53:52] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 08:53:52] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 08:53:53] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127133&hash=ARmVgexderjai88OImGBkEGRonyefGBrm1-ow633oVjF5w
|
||||||
|
[2026-02-03 08:53:53] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127133&hash=ARleR9CvdH_ZXgcEKan0BggNv6osT2pUwtC3qumta4gttA
|
||||||
|
[2026-02-03 08:53:53] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 08:53:53] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 08:53:53] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 08:53:53] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127133&hash=ARmVgexderjai88OImGBkEGRonyefGBrm1-ow633oVjF5w
|
||||||
|
[2026-02-03 08:53:53] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127133&hash=ARleR9CvdH_ZXgcEKan0BggNv6osT2pUwtC3qumta4gttA
|
||||||
|
[2026-02-03 11:15:58] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 11:15:58] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 11:15:58] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 11:15:59] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770135659&hash=ARlKSkH_tfyiy1Ctx7mwxi3aZ2JrsWdWcK9I73EldYmKeg
|
||||||
|
[2026-02-03 11:15:59] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770135659&hash=ARmOinKL4W0exQVUyOxwpLCkmPghiI0rzBv22kDEYwbePA
|
||||||
|
[2026-02-03 12:00:15] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 12:00:15] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 12:00:15] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 12:00:15] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770138315&hash=ARnn0qsPekd9pOK3dw85bzArN5oMXws99mc_zs6cdK0wtQ
|
||||||
|
[2026-02-03 12:00:15] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770138315&hash=ARlAM-vxY5Y6ST1Lwp8ox6SBOAefCdZ9yWVZKM-04kcgqQ
|
||||||
|
[2026-02-03 12:45:14] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 12:45:14] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 12:45:14] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 12:45:14] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770141014&hash=ARn79fTfe65Rz38MXO0hhmyLSuVJsGuIzM30zAngMKVSeA
|
||||||
|
[2026-02-03 12:45:14] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770141014&hash=ARknEbp3Lf9RYQ2daDdcExwkmvWlMb1F_DLBhGQYJhqXtQ
|
||||||
|
[2026-02-03 14:22:03] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 14:22:03] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 14:22:03] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 14:22:03] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770146823&hash=ARmkpGGK9nwQCAqwRqTNu8hZdxaLvFe_QqKCZDa60vjQMw
|
||||||
|
[2026-02-03 14:22:03] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770146823&hash=ARkmJ5x9KoRtE0e7E2j3ao1uDWPpEzVsJ3mSCUza8UjbKQ
|
||||||
|
[2026-02-03 14:23:56] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 14:23:56] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 14:23:56] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 14:23:57] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770146937&hash=ARlFlA7KVAre979qNou-DdifbKg_QVenRMQ-ocl3p7vqjg
|
||||||
|
[2026-02-03 14:23:57] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770146937&hash=ARnJ0xX1NUbsvypvvoZA6UwXnFioUbFugAFUqwrBpQh23Q
|
||||||
|
[2026-02-03 14:28:39] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 14:28:39] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 14:28:39] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 14:28:39] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147219&hash=ARkHFZc5AuDMwNI48w_-THsJM1CoGx3G8CtP-N7fcdBBtQ
|
||||||
|
[2026-02-03 14:28:39] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147219&hash=ARmjcb4sF70-VmiQiXeLl3p8XSaAjjGF9zbh339aUlnFOg
|
||||||
|
[2026-02-03 14:34:25] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 14:34:25] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 14:34:25] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 14:34:25] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147565&hash=ARk8Q6oLaHEvxv7WNOGtOwkgB8RDWPs7xk672ACpSuC-3A
|
||||||
|
[2026-02-03 14:34:25] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147565&hash=ARkNv8rIgcXUHoj2i9ZwZQBW88W7p1yab6a_arnqGzbJtw
|
||||||
|
[2026-02-03 14:39:57] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 14:39:57] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 14:39:57] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 14:39:58] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147898&hash=ARnRWSW5FuBObUTXyIRf_5mEV90eLmf4Kx4hft5MAQH7fw
|
||||||
|
[2026-02-03 14:39:58] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147898&hash=ARlVjspTqGVhZzOq1D-cMbpj7AXP3bNSacmtV3ZIg_ALqA
|
||||||
|
[2026-02-03 14:49:54] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 14:49:54] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 14:49:54] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 14:49:55] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770148494&hash=ARka8CBIIFFl2uNVBdAnHVJldB8vBjgU0NbvGVDabWBfsw
|
||||||
|
[2026-02-03 14:49:55] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770148494&hash=ARnz6olwrZgd3SpTw-AbpdOo1lcbFr72hX3T6CcVDX9yzw
|
||||||
|
[2026-02-03 14:50:00] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 14:50:00] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 14:50:00] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 14:50:00] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770148500&hash=ARkBigqY86ltDKoAFz_5B1g5uDgpegUSHtL4OGJeQME8rw
|
||||||
|
[2026-02-03 14:50:00] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770148500&hash=ARmtsjCTyftZSmwlVDnK0QCoYEzA8-NtYX6avOCtr-belQ
|
||||||
|
[2026-02-03 15:43:01] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 15:43:01] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 15:43:01] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 15:43:02] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151682&hash=ARnTTvbQWWCa7LYWf9S55Dg6MSLjjFKtI7nWR0bN2zvDQQ
|
||||||
|
[2026-02-03 15:43:02] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151682&hash=ARlo5HOK4ztNMJITAnXP2SkjsasuE9VDkgnxXYFUC-t4ng
|
||||||
|
[2026-02-03 15:46:16] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 15:46:16] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 15:46:16] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 15:46:16] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151876&hash=ARkTtYYdsUFAiZwrYv2rgdGGiEc-D59t4NMElrHDHtA_Vw
|
||||||
|
[2026-02-03 15:46:16] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151876&hash=ARlQUE5zvswz5tBhKIWtAYtD-E7qr9nTCcpuEoYi3nzDLA
|
||||||
|
[2026-02-03 15:46:33] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 15:46:33] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 15:46:33] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 15:46:34] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151894&hash=ARm8Ao4ZAndoLc5sPn6u44DIOtYTW9ck-en0zxlbgOCIRA
|
||||||
|
[2026-02-03 15:46:34] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151894&hash=ARlLvqyHLBLf1wJwmDZZTlqQT0BpMvqj_hqmhijSVdCbxw
|
||||||
|
[2026-02-03 15:47:11] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 15:47:11] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 15:47:11] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 15:47:11] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151931&hash=ARmC5M11laF_p8bRXwSW0aUa6BZ5p6HLJ4xFt5i_rhGelg
|
||||||
|
[2026-02-03 15:47:11] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151931&hash=ARklCUN4q5XJ2DcsvgXtl1clURKnFJGZ9FB1VMXZ4JUTlA
|
||||||
|
[2026-02-03 15:52:33] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 15:52:33] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 15:52:33] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 15:52:34] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152254&hash=ARkc8acjikfeqPK2EEA-lsA6To-kLYCbC0G1N5I-sgDklw
|
||||||
|
[2026-02-03 15:52:34] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152254&hash=ARkq5Bv1Yx0I8YZVnRPl1GY3mRSjiTZsTeKecOm3WWP6fA
|
||||||
|
[2026-02-03 15:54:38] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 15:54:38] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 15:54:38] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 15:54:38] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152378&hash=ARkd0Smk6IQrA7Z-TtMBMGK080-7tiill2lA697bnJdzQQ
|
||||||
|
[2026-02-03 15:54:38] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152378&hash=ARmCvtVqy3cbEBhreSiBlGMz_k0H0C1q3FglE1cAzOMtsA
|
||||||
|
[2026-02-03 15:54:55] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 15:54:55] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 15:54:56] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 15:54:56] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152396&hash=ARm4I-TH2FgoFVdYU6vyaEldYprMRcQMSUZD7-mteyE2bQ
|
||||||
|
[2026-02-03 15:54:56] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152396&hash=ARmySCPJWQXfIqv5WakKH2es6o2rlrkmd_QeRWbDRDhMjA
|
||||||
|
[2026-02-03 16:17:56] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 16:17:56] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 16:17:56] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 16:17:56] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153776&hash=ARkpL17QRsFrIZ9gL6jP87dZLx9WL80OCkUG2QOBFeE6Iw
|
||||||
|
[2026-02-03 16:17:56] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153776&hash=ARnVTurshL8o0fva9f7ThLQBCNVvOwYex9n-d0Guelq_3w
|
||||||
|
[2026-02-03 16:18:30] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 16:18:30] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 16:18:30] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 16:18:30] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153810&hash=ARkFailq-_F7K1gQIj0Tyqh1KJ_mD8HHlC79mPeupj6g7A
|
||||||
|
[2026-02-03 16:18:30] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153810&hash=ARmcNab_e2NsTTxNcuj_VUmj1_G1h2pdSPXQmEF_T7IZ7A
|
||||||
|
[2026-02-03 16:20:02] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 16:20:02] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 16:20:02] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 16:20:03] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153902&hash=ARmkQUNq3ncSrBHcO337pHLH9_Mrv89BMga1M52iHJjAAA
|
||||||
|
[2026-02-03 16:20:03] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153903&hash=ARkcsEFiF-VZtOHz8UIqPbUlftOVB3pxOmlyOhHWvHwNUA
|
||||||
|
[2026-02-03 16:20:10] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 16:20:10] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 16:20:10] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 16:20:11] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153911&hash=ARlsB6UeRRokyVUn_K3GY56WCvw4M7IiTI-0YYDgWKtDXA
|
||||||
|
[2026-02-03 16:20:11] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153911&hash=ARnhrJuYCnG-1FpfxhUU638YidJDdCp_9Pusp4nmWCCepQ
|
||||||
|
[2026-02-03 20:31:47] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||||
|
[2026-02-03 20:31:47] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||||
|
[2026-02-03 20:31:47] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||||
|
[2026-02-03 20:31:48] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770169008&hash=ARmSF2NK4GqmW53yaJd3NCLYub_AGbau7EHf5WOOETVxbw
|
||||||
|
[2026-02-03 20:31:48] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770169008&hash=ARlAZuDk9UsmFsMvn4qiAOOAk45RwETVe04h9vkJdDruXg
|
||||||
|
|||||||
+82
-3
@@ -87,6 +87,15 @@ class WhatsAppBotManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contador de caracteres para broadcast
|
||||||
|
const broadcastMessage = document.getElementById('broadcast-message');
|
||||||
|
const charCount = document.getElementById('char-count');
|
||||||
|
if (broadcastMessage && charCount) {
|
||||||
|
broadcastMessage.addEventListener('input', (e) => {
|
||||||
|
charCount.textContent = e.target.value.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Cambio de tipo de mensaje
|
// Cambio de tipo de mensaje
|
||||||
const messageType = document.getElementById('message-type');
|
const messageType = document.getElementById('message-type');
|
||||||
if (messageType) {
|
if (messageType) {
|
||||||
@@ -695,9 +704,79 @@ class WhatsAppBotManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendBroadcast() {
|
async sendBroadcast() {
|
||||||
// Funcionalidad de envío masivo deshabilitada temporalmente desde UI
|
const selectionType = document.getElementById('broadcast-selection-type').value;
|
||||||
this.showError('Envío masivo deshabilitado temporalmente');
|
const messageType = document.getElementById('broadcast-message-type').value;
|
||||||
return;
|
|
||||||
|
let payload = {
|
||||||
|
selection_type: selectionType,
|
||||||
|
message_type: messageType
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validar según tipo de selección
|
||||||
|
if (selectionType === 'filter') {
|
||||||
|
payload.filter = document.getElementById('broadcast-filter').value;
|
||||||
|
} else {
|
||||||
|
const selectedUsers = $('#broadcast-users').val();
|
||||||
|
if (!selectedUsers || selectedUsers.length === 0) {
|
||||||
|
this.showError('Debes seleccionar al menos un usuario');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.user_ids = selectedUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar según tipo de mensaje
|
||||||
|
if (messageType === 'text') {
|
||||||
|
const message = document.getElementById('broadcast-message').value;
|
||||||
|
if (!message.trim()) {
|
||||||
|
this.showError('El mensaje no puede estar vacío');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.message = message;
|
||||||
|
} else {
|
||||||
|
const templateId = document.getElementById('broadcast-template').value;
|
||||||
|
if (!templateId) {
|
||||||
|
this.showError('Debes seleccionar una plantilla');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.template_id = templateId;
|
||||||
|
|
||||||
|
// Recoger variables
|
||||||
|
const variables = {};
|
||||||
|
document.querySelectorAll('.broadcast-template-var').forEach(input => {
|
||||||
|
variables[input.dataset.var] = input.value;
|
||||||
|
});
|
||||||
|
payload.template_variables = variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmar envío
|
||||||
|
const targetText = selectionType === 'filter'
|
||||||
|
? `usuarios filtrados por "${payload.filter}"`
|
||||||
|
: `${payload.user_ids.length} usuario(s) seleccionado(s)`;
|
||||||
|
const messageText = messageType === 'text' ? 'mensaje de texto' : 'plantilla';
|
||||||
|
|
||||||
|
if (!confirm(`¿Enviar este ${messageText} a ${targetText}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.showLoading('Enviando mensajes masivos...');
|
||||||
|
const result = await this.apiCall('send_broadcast.php', 'POST', payload);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showSuccess(`✅ Mensajes enviados exitosamente!\nEnviados: ${result.sent_count}\nErrores: ${result.error_count}\nTotal usuarios: ${result.total_users}`);
|
||||||
|
document.getElementById('broadcast-form').reset();
|
||||||
|
$('#broadcast-users').val(null).trigger('change');
|
||||||
|
document.getElementById('broadcast-template-variables').style.display = 'none';
|
||||||
|
document.getElementById('broadcast-template-preview').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
this.showError(result.error || 'Error enviando broadcast');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Error enviando broadcast: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
this.hideLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveMenu() {
|
async saveMenu() {
|
||||||
|
|||||||
+754
-34
@@ -112,12 +112,21 @@ class SimpleWhatsAppManager {
|
|||||||
case 'users':
|
case 'users':
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
break;
|
break;
|
||||||
|
case 'admin_users':
|
||||||
|
this.loadAdminUsers();
|
||||||
|
break;
|
||||||
case 'send_message':
|
case 'send_message':
|
||||||
this.loadconversations();
|
this.loadconversations();
|
||||||
break;
|
break;
|
||||||
case 'templates':
|
case 'templates':
|
||||||
this.loadTemplates();
|
this.loadTemplates();
|
||||||
break;
|
break;
|
||||||
|
case 'broadcast':
|
||||||
|
// Recargar plantillas cuando se abre el tab de broadcast
|
||||||
|
if (typeof loadBroadcastTemplates === 'function') {
|
||||||
|
loadBroadcastTemplates();
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'menus':
|
case 'menus':
|
||||||
this.loadMenus();
|
this.loadMenus();
|
||||||
break;
|
break;
|
||||||
@@ -403,14 +412,19 @@ class SimpleWhatsAppManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadUsers() {
|
async loadUsers(page = 1, search = '') {
|
||||||
this.log('Cargando usuarios');
|
this.log('Cargando usuarios - página ' + page);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.apiCall('get_users.php');
|
let url = `get_users.php?page=${page}&limit=20`;
|
||||||
|
if (search) {
|
||||||
|
url += `&search=${encodeURIComponent(search)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.apiCall(url);
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
this.updateUsersList(response.data || []);
|
this.updateUsersList(response.data || [], response.pagination || {});
|
||||||
} else {
|
} else {
|
||||||
this.showError('No se pudieron cargar los usuarios');
|
this.showError('No se pudieron cargar los usuarios');
|
||||||
}
|
}
|
||||||
@@ -420,15 +434,17 @@ class SimpleWhatsAppManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUsersList(users) {
|
updateUsersList(users, pagination = {}) {
|
||||||
const container = document.getElementById('users-table');
|
const container = document.getElementById('users-table');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
// Guardar datos para uso posterior
|
// Guardar datos para uso posterior
|
||||||
this.usersData = users;
|
this.usersData = users;
|
||||||
|
this.usersPagination = pagination;
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
container.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No hay usuarios registrados</td></tr>';
|
container.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No hay usuarios registrados</td></tr>';
|
||||||
|
this.updateUsersPagination({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +475,75 @@ class SimpleWhatsAppManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
this.updateUsersPagination(pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUsersPagination(pagination) {
|
||||||
|
const paginationContainer = document.getElementById('users-pagination');
|
||||||
|
if (!paginationContainer) return;
|
||||||
|
|
||||||
|
if (!pagination.totalPages || pagination.totalPages <= 1) {
|
||||||
|
paginationContainer.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPage = pagination.page || 1;
|
||||||
|
const totalPages = pagination.totalPages;
|
||||||
|
const total = pagination.total || 0;
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<div class="text-muted">
|
||||||
|
Mostrando página ${currentPage} de ${totalPages} (${total} usuarios en total)
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item ${!pagination.hasPrev ? 'disabled' : ''}">
|
||||||
|
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(1); return false;">
|
||||||
|
<i class="fas fa-angle-double-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item ${!pagination.hasPrev ? 'disabled' : ''}">
|
||||||
|
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${currentPage - 1}); return false;">
|
||||||
|
<i class="fas fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mostrar páginas alrededor de la actual
|
||||||
|
const maxPages = 5;
|
||||||
|
let startPage = Math.max(1, currentPage - Math.floor(maxPages / 2));
|
||||||
|
let endPage = Math.min(totalPages, startPage + maxPages - 1);
|
||||||
|
|
||||||
|
if (endPage - startPage < maxPages - 1) {
|
||||||
|
startPage = Math.max(1, endPage - maxPages + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
html += `
|
||||||
|
<li class="page-item ${i === currentPage ? 'active' : ''}">
|
||||||
|
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${i}); return false;">${i}</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<li class="page-item ${!pagination.hasNext ? 'disabled' : ''}">
|
||||||
|
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${currentPage + 1}); return false;">
|
||||||
|
<i class="fas fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item ${!pagination.hasNext ? 'disabled' : ''}">
|
||||||
|
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${totalPages}); return false;">
|
||||||
|
<i class="fas fa-angle-double-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
paginationContainer.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadConversations() {
|
async loadConversations() {
|
||||||
@@ -716,6 +801,7 @@ class SimpleWhatsAppManager {
|
|||||||
const fields = [
|
const fields = [
|
||||||
{ id: 'whatsapp-token', value: data.whatsapp_token },
|
{ id: 'whatsapp-token', value: data.whatsapp_token },
|
||||||
{ id: 'phone-number-id', value: data.phone_number_id },
|
{ id: 'phone-number-id', value: data.phone_number_id },
|
||||||
|
{ id: 'business-account-id', value: data.business_account_id },
|
||||||
{ id: 'webhook-token', value: data.webhook_verify_token },
|
{ id: 'webhook-token', value: data.webhook_verify_token },
|
||||||
{ id: 'api-url', value: data.api_url },
|
{ id: 'api-url', value: data.api_url },
|
||||||
{ id: 'business-name', value: data.business_name },
|
{ id: 'business-name', value: data.business_name },
|
||||||
@@ -739,6 +825,7 @@ class SimpleWhatsAppManager {
|
|||||||
const system_config = {
|
const system_config = {
|
||||||
token: document.getElementById('whatsapp-token').value,
|
token: document.getElementById('whatsapp-token').value,
|
||||||
phone_number_id: document.getElementById('phone-number-id').value,
|
phone_number_id: document.getElementById('phone-number-id').value,
|
||||||
|
business_account_id: document.getElementById('business-account-id').value,
|
||||||
webhook_verify_token: document.getElementById('webhook-token').value,
|
webhook_verify_token: document.getElementById('webhook-token').value,
|
||||||
api_url: document.getElementById('api-url').value,
|
api_url: document.getElementById('api-url').value,
|
||||||
business_name: document.getElementById('business-name').value,
|
business_name: document.getElementById('business-name').value,
|
||||||
@@ -832,6 +919,9 @@ class SimpleWhatsAppManager {
|
|||||||
|
|
||||||
this.log(`Usuarios cargados: ${users.length}, Plantillas cargadas: ${templates.length}`);
|
this.log(`Usuarios cargados: ${users.length}, Plantillas cargadas: ${templates.length}`);
|
||||||
|
|
||||||
|
// Guardar plantillas para uso posterior
|
||||||
|
this.templatesData = templates;
|
||||||
|
|
||||||
if (users.length > 0) {
|
if (users.length > 0) {
|
||||||
this.populateUsersSelect(users);
|
this.populateUsersSelect(users);
|
||||||
} else if (userSelect) {
|
} else if (userSelect) {
|
||||||
@@ -875,13 +965,15 @@ class SimpleWhatsAppManager {
|
|||||||
// Limpiar opciones existentes (excepto la primera)
|
// Limpiar opciones existentes (excepto la primera)
|
||||||
select.innerHTML = '<option value="">Seleccionar plantilla...</option>';
|
select.innerHTML = '<option value="">Seleccionar plantilla...</option>';
|
||||||
|
|
||||||
// Agregar plantillas
|
// Agregar plantillas (solo las aprobadas)
|
||||||
templates.forEach(template => {
|
templates
|
||||||
const option = document.createElement('option');
|
.filter(template => template.status && template.status.toUpperCase() === 'APPROVED')
|
||||||
option.value = template.template_name;
|
.forEach(template => {
|
||||||
option.textContent = `${template.name} - ${template.category || 'Sin descripción'}`;
|
const option = document.createElement('option');
|
||||||
select.appendChild(option);
|
option.value = template.id; // Usar ID en lugar de template_name
|
||||||
});
|
option.textContent = `${template.name} - ${template.category || 'Sin descripción'}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTemplates() {
|
async loadTemplates() {
|
||||||
@@ -1367,38 +1459,90 @@ class SimpleWhatsAppManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare template payload; support named parameters (e.g. { name: 'Juan' })
|
// Obtener ID y nombre de la plantilla
|
||||||
const tplName = templateSelect.value;
|
const templateId = templateSelect.value;
|
||||||
let tplParams = null;
|
const selectedOption = templateSelect.options[templateSelect.selectedIndex];
|
||||||
|
|
||||||
// If there's a hidden input with JSON parameters (index.php), use it
|
// Buscar la plantilla en templatesData para obtener el template_name
|
||||||
const paramsInput = document.getElementById('template-params');
|
let templateName = null;
|
||||||
if (paramsInput && paramsInput.value) {
|
if (this.templatesData) {
|
||||||
try {
|
const template = this.templatesData.find(t => t.id == templateId);
|
||||||
tplParams = JSON.parse(paramsInput.value);
|
if (template) {
|
||||||
} catch (e) {
|
templateName = template.template_name;
|
||||||
console.warn('Invalid JSON in #template-params, ignoring', e);
|
|
||||||
tplParams = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no params provided and template is contacto_nuevo, prompt operator for name
|
if (!templateName) {
|
||||||
if ((!tplParams || (typeof tplParams === 'object' && Object.keys(tplParams).length === 0)) && tplName === 'contacto_nuevo') {
|
this.showError('No se pudo obtener el nombre de la plantilla');
|
||||||
const defaultName = '';
|
return;
|
||||||
const inputName = prompt('Ingrese el nombre para la plantilla contacto_nuevo (dejar vacío para usar el número):', defaultName);
|
}
|
||||||
if (inputName === null) {
|
|
||||||
// user cancelled
|
// Recopilar variables de los inputs
|
||||||
return;
|
let tplParams = {};
|
||||||
|
const varInputs = document.querySelectorAll('.message-template-var');
|
||||||
|
|
||||||
|
if (varInputs.length > 0) {
|
||||||
|
console.log(`🔍 Recopilando ${varInputs.length} variables de la plantilla`);
|
||||||
|
|
||||||
|
// Detectar si las variables son numéricas o con nombres
|
||||||
|
let hasNumericVars = false;
|
||||||
|
let hasNamedVars = false;
|
||||||
|
|
||||||
|
varInputs.forEach(input => {
|
||||||
|
const placeholder = input.dataset.placeholder || '';
|
||||||
|
if (/^\{\{\d+\}\}$/.test(placeholder)) {
|
||||||
|
hasNumericVars = true;
|
||||||
|
} else {
|
||||||
|
hasNamedVars = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📊 Tipo de variables detectado: ${hasNumericVars ? 'numéricas' : 'con nombres'}`);
|
||||||
|
|
||||||
|
if (hasNumericVars && !hasNamedVars) {
|
||||||
|
// Variables numéricas: crear array ordenado por índice
|
||||||
|
const tempObj = {};
|
||||||
|
varInputs.forEach(input => {
|
||||||
|
const index = parseInt(input.dataset.var);
|
||||||
|
tempObj[index] = input.value || '';
|
||||||
|
});
|
||||||
|
// Convertir a array ordenado
|
||||||
|
const sortedKeys = Object.keys(tempObj).map(Number).sort((a, b) => a - b);
|
||||||
|
tplParams = sortedKeys.map(key => tempObj[key]);
|
||||||
|
console.log('📊 Enviando como array ordenado:', tplParams);
|
||||||
|
} else {
|
||||||
|
// Variables con nombres: crear objeto
|
||||||
|
varInputs.forEach(input => {
|
||||||
|
const placeholder = input.dataset.placeholder || '';
|
||||||
|
// Extraer nombre de la variable: {{nombre_tema}} -> nombre_tema
|
||||||
|
const varName = placeholder.replace(/\{\{|\}\}/g, '');
|
||||||
|
tplParams[varName] = input.value || '';
|
||||||
|
});
|
||||||
|
console.log('📦 Enviando como objeto con nombres:', tplParams);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Plantilla sin variables');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si hay input oculto con parámetros (para compatibilidad), usarlo
|
||||||
|
const paramsInput = document.getElementById('template-params');
|
||||||
|
if (paramsInput && paramsInput.value) {
|
||||||
|
try {
|
||||||
|
const hiddenParams = JSON.parse(paramsInput.value);
|
||||||
|
if (Object.keys(hiddenParams).length > 0) {
|
||||||
|
tplParams = hiddenParams;
|
||||||
|
console.log('📝 Usando parámetros del input oculto:', tplParams);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Invalid JSON in #template-params, ignoring', e);
|
||||||
}
|
}
|
||||||
const clean = (typeof inputName === 'string') ? inputName.trim() : '';
|
|
||||||
tplParams = { name: clean || recipient };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messageData = {
|
messageData = {
|
||||||
recipient: recipient,
|
recipient: recipient,
|
||||||
type: 'template',
|
type: 'template',
|
||||||
template_name: tplName,
|
template_name: templateName, // Usar template_name en lugar de ID
|
||||||
parameters: tplParams || {}
|
parameters: tplParams
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1651,6 +1795,164 @@ class SimpleWhatsAppManager {
|
|||||||
this.showError('Error eliminando plantilla: ' + error.message);
|
this.showError('Error eliminando plantilla: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendBroadcast() {
|
||||||
|
const selectionType = document.getElementById('broadcast-selection-type').value;
|
||||||
|
const messageType = document.getElementById('broadcast-message-type').value;
|
||||||
|
|
||||||
|
let payload = {
|
||||||
|
selection_type: selectionType,
|
||||||
|
message_type: messageType
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validar según tipo de selección
|
||||||
|
if (selectionType === 'filter') {
|
||||||
|
payload.filter = document.getElementById('broadcast-filter').value;
|
||||||
|
} else {
|
||||||
|
const selectedUsers = $('#broadcast-users').val();
|
||||||
|
if (!selectedUsers || selectedUsers.length === 0) {
|
||||||
|
this.showError('Debes seleccionar al menos un usuario');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.user_ids = selectedUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar según tipo de mensaje
|
||||||
|
if (messageType === 'text') {
|
||||||
|
const message = document.getElementById('broadcast-message').value;
|
||||||
|
if (!message.trim()) {
|
||||||
|
this.showError('El mensaje no puede estar vacío');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.message = message;
|
||||||
|
} else {
|
||||||
|
const templateId = document.getElementById('broadcast-template').value;
|
||||||
|
if (!templateId) {
|
||||||
|
this.showError('Debes seleccionar una plantilla');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.template_id = templateId;
|
||||||
|
|
||||||
|
// Recoger y validar variables
|
||||||
|
const variablesInputs = document.querySelectorAll('.broadcast-template-var');
|
||||||
|
let hasEmpty = false;
|
||||||
|
|
||||||
|
// Detectar si las variables son numéricas o con nombres usando el placeholder
|
||||||
|
const firstInput = variablesInputs[0];
|
||||||
|
const placeholder = firstInput?.dataset?.placeholder || '';
|
||||||
|
// Si el placeholder es {{1}}, {{2}}, etc. -> numéricas
|
||||||
|
// Si es {{nombre_tema}}, {{fecha}}, etc. -> con nombres
|
||||||
|
const isNumericVar = /^\{\{\d+\}\}$/.test(placeholder);
|
||||||
|
|
||||||
|
console.log('🔍 Detectando tipo de variables:');
|
||||||
|
console.log(' - Placeholder ejemplo:', placeholder);
|
||||||
|
console.log(' - Es numérica?', isNumericVar);
|
||||||
|
|
||||||
|
let variables;
|
||||||
|
if (isNumericVar) {
|
||||||
|
// Variables numéricas {{1}}, {{2}} - usar array ordenado
|
||||||
|
const tempVars = {};
|
||||||
|
variablesInputs.forEach(input => {
|
||||||
|
const varIndex = parseInt(input.dataset.var);
|
||||||
|
if (!input.value.trim()) {
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
hasEmpty = true;
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
tempVars[varIndex] = input.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmpty) {
|
||||||
|
this.showError('Por favor completa todos los campos de variables de la plantilla');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir a array ordenado por índice
|
||||||
|
variables = [];
|
||||||
|
const sortedKeys = Object.keys(tempVars).map(k => parseInt(k)).sort((a, b) => a - b);
|
||||||
|
sortedKeys.forEach(key => {
|
||||||
|
variables.push(tempVars[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📝 Variables numéricas recopiladas:', tempVars);
|
||||||
|
console.log('📊 Array ordenado:', variables);
|
||||||
|
} else {
|
||||||
|
// Variables con nombres {{fecha}}, {{motivo}} - usar objeto
|
||||||
|
variables = {};
|
||||||
|
variablesInputs.forEach(input => {
|
||||||
|
const placeholder = input.dataset.placeholder || '';
|
||||||
|
const varName = placeholder.replace(/\{\{|\}\}/g, ''); // Extraer nombre de {{fecha}}
|
||||||
|
|
||||||
|
if (!input.value.trim()) {
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
hasEmpty = true;
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
variables[varName] = input.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmpty) {
|
||||||
|
this.showError('Por favor completa todos los campos de variables de la plantilla');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📝 Variables con nombres recopiladas:', variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.template_variables = variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmar envío
|
||||||
|
const targetText = selectionType === 'filter'
|
||||||
|
? `usuarios filtrados por "${payload.filter}"`
|
||||||
|
: `${payload.user_ids.length} usuario(s) seleccionado(s)`;
|
||||||
|
const messageText = messageType === 'text' ? 'mensaje de texto' : 'plantilla';
|
||||||
|
|
||||||
|
if (!confirm(`¿Enviar este ${messageText} a ${targetText}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.showInfo('Enviando mensajes masivos...');
|
||||||
|
const result = await this.apiCall('send_broadcast.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: payload
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showSuccess(`✅ Mensajes enviados exitosamente!\nEnviados: ${result.sent_count}\nErrores: ${result.error_count}\nTotal usuarios: ${result.total_users}`);
|
||||||
|
document.getElementById('broadcast-form').reset();
|
||||||
|
$('#broadcast-users').val(null).trigger('change');
|
||||||
|
document.getElementById('broadcast-template-variables').style.display = 'none';
|
||||||
|
document.getElementById('broadcast-template-preview').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
this.showError(result.error || 'Error enviando broadcast');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Error enviando broadcast: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar usuarios administradores del sistema
|
||||||
|
async loadAdminUsers() {
|
||||||
|
this.log('Cargando usuarios administradores...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('list_admin_users.php');
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
renderAdminUsersTable(response.data);
|
||||||
|
} else {
|
||||||
|
this.showError('Error cargando usuarios administradores');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log('Error cargando usuarios administradores: ' + error.message, 'error');
|
||||||
|
this.showError('Error cargando usuarios: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inicializar cuando el DOM esté listo
|
// Inicializar cuando el DOM esté listo
|
||||||
@@ -2839,9 +3141,121 @@ window.toggleMessageType = function () {
|
|||||||
} else {
|
} else {
|
||||||
textGroup.style.display = 'block';
|
textGroup.style.display = 'block';
|
||||||
templateGroup.style.display = 'none';
|
templateGroup.style.display = 'none';
|
||||||
|
// Ocultar variables y preview cuando no es plantilla
|
||||||
|
const varsContainer = document.getElementById('message-template-variables');
|
||||||
|
const previewContainer = document.getElementById('message-template-preview');
|
||||||
|
if (varsContainer) varsContainer.style.display = 'none';
|
||||||
|
if (previewContainer) previewContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cargar detalles de plantilla para send_message
|
||||||
|
window.loadMessageTemplateDetails = async function() {
|
||||||
|
console.log('🔵 loadMessageTemplateDetails llamado');
|
||||||
|
const select = document.getElementById('message-template');
|
||||||
|
const templateId = select.value;
|
||||||
|
console.log('Template ID seleccionado:', templateId);
|
||||||
|
|
||||||
|
if (!templateId) {
|
||||||
|
document.getElementById('message-template-variables').style.display = 'none';
|
||||||
|
document.getElementById('message-template-preview').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.whatsappManager.apiCall(`get_template_details.php?id=${templateId}`);
|
||||||
|
console.log('✅ Detalles de plantilla:', response);
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
const template = response.template || response.data;
|
||||||
|
if (!template) {
|
||||||
|
console.error('❌ No se encontró template en la respuesta');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Template data:', template);
|
||||||
|
console.log('📋 Variables:', template.variables);
|
||||||
|
|
||||||
|
const variablesContainer = document.getElementById('message-template-variables');
|
||||||
|
|
||||||
|
if (template.variables && template.variables.length > 0) {
|
||||||
|
console.log(`✅ Se encontraron ${template.variables.length} variables`);
|
||||||
|
let html = '<label class="form-label fw-bold"><i class="fas fa-edit"></i> Variables de la Plantilla</label>';
|
||||||
|
|
||||||
|
template.variables.forEach((variable, index) => {
|
||||||
|
const varLabel = variable.label || variable.name || `Variable ${variable.index || index + 1}`;
|
||||||
|
const placeholder = variable.placeholder || `{{${variable.index || index + 1}}}`;
|
||||||
|
const example = variable.example || '';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small text-muted">${varLabel}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-sm message-template-var"
|
||||||
|
data-var="${variable.index || index + 1}"
|
||||||
|
data-placeholder="${placeholder}"
|
||||||
|
placeholder="${example || 'Ingrese ' + varLabel.toLowerCase()}"
|
||||||
|
oninput="updateMessagePreview()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📝 HTML generado para variables:', html);
|
||||||
|
variablesContainer.innerHTML = html;
|
||||||
|
variablesContainer.style.display = 'block';
|
||||||
|
console.log('✅ Variables container mostrado');
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Template sin variables');
|
||||||
|
variablesContainer.innerHTML = '<p class="text-muted small">Esta plantilla no tiene variables</p>';
|
||||||
|
variablesContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessagePreview();
|
||||||
|
} else {
|
||||||
|
console.error('❌ Respuesta sin success:', response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error cargando detalles de plantilla:', error);
|
||||||
|
window.whatsappManager.showError('Error cargando plantilla');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actualizar preview de plantilla para send_message
|
||||||
|
window.updateMessagePreview = async function() {
|
||||||
|
const templateId = document.getElementById('message-template').value;
|
||||||
|
if (!templateId) return;
|
||||||
|
|
||||||
|
// Recolectar variables como array (por índice)
|
||||||
|
const variablesArray = [];
|
||||||
|
document.querySelectorAll('.message-template-var').forEach(input => {
|
||||||
|
const index = parseInt(input.dataset.var) - 1; // Convertir a índice de array (0-based)
|
||||||
|
variablesArray[index] = input.value || `{{${input.dataset.var}}}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.whatsappManager.apiCall('preview_template.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { template_id: templateId, parameters: variablesArray }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success && response.preview) {
|
||||||
|
const previewContainer = document.getElementById('message-template-preview');
|
||||||
|
const previewContent = document.getElementById('message-preview-content');
|
||||||
|
|
||||||
|
// Usar el HTML pre-formateado del servidor
|
||||||
|
previewContent.innerHTML = response.preview.html || response.preview.body.replace(/\\n/g, '<br>');
|
||||||
|
previewContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
console.error('Error en preview:', response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generando vista previa:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Función de diagnóstico
|
// Función de diagnóstico
|
||||||
window.runDiagnostic = function () {
|
window.runDiagnostic = function () {
|
||||||
const resultsDiv = document.getElementById('diagnostic-results');
|
const resultsDiv = document.getElementById('diagnostic-results');
|
||||||
@@ -4113,6 +4527,90 @@ window.exportUsers = function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Función para buscar usuarios con debounce
|
||||||
|
let searchUsersTimeout;
|
||||||
|
window.searchUsers = function() {
|
||||||
|
clearTimeout(searchUsersTimeout);
|
||||||
|
searchUsersTimeout = setTimeout(() => {
|
||||||
|
const searchInput = document.getElementById('users-search');
|
||||||
|
const searchTerm = searchInput ? searchInput.value.trim() : '';
|
||||||
|
|
||||||
|
if (window.whatsappManager && window.whatsappManager.loadUsers) {
|
||||||
|
window.whatsappManager.loadUsers(1, searchTerm);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Función para sincronizar plantillas desde Facebook
|
||||||
|
window.syncTemplatesFromFacebook = async function() {
|
||||||
|
console.log('Sincronizando plantillas desde Facebook...');
|
||||||
|
|
||||||
|
if (!window.whatsappManager) {
|
||||||
|
alert('Error: Sistema no inicializado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('btn-sync-templates');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sincronizando...';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.whatsappManager.showInfo('Sincronizando plantillas desde Facebook/WhatsApp Business...');
|
||||||
|
|
||||||
|
const response = await window.whatsappManager.apiCall('sync_templates_from_facebook.php', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
const data = response.data || {};
|
||||||
|
const message = `
|
||||||
|
✅ Sincronización completada exitosamente:
|
||||||
|
• ${data.synced || 0} plantillas nuevas
|
||||||
|
• ${data.updated || 0} plantillas actualizadas
|
||||||
|
• ${data.skipped || 0} sin cambios
|
||||||
|
${data.errors && data.errors.length > 0 ? '\n⚠️ ' + data.errors.length + ' errores encontrados' : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
window.whatsappManager.showSuccess(message);
|
||||||
|
|
||||||
|
// Recargar la lista de plantillas
|
||||||
|
if (window.whatsappManager.loadTemplates) {
|
||||||
|
window.whatsappManager.loadTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar errores si existen
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
console.warn('Errores durante la sincronización:', data.errors);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = response?.error || 'Error desconocido al sincronizar';
|
||||||
|
window.whatsappManager.showError(`Error al sincronizar plantillas: ${errorMsg}`);
|
||||||
|
|
||||||
|
// Si el error es por falta de WABA ID, dar instrucciones
|
||||||
|
if (errorMsg.includes('Business Account ID')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
alert('💡 Para sincronizar plantillas necesitas configurar el Business Account ID (WABA ID).\n\n' +
|
||||||
|
'1. Ve a la pestaña "Configuración"\n' +
|
||||||
|
'2. Ingresa tu WABA ID en el campo correspondiente\n' +
|
||||||
|
'3. Guarda la configuración\n' +
|
||||||
|
'4. Vuelve a intentar la sincronización\n\n' +
|
||||||
|
'Puedes encontrar tu WABA ID en Meta Business Manager.');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sincronizando plantillas:', error);
|
||||||
|
window.whatsappManager.showError('Error de conexión al sincronizar plantillas: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-sync-alt"></i> Sincronizar desde Facebook';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Función para refrescar logs
|
// Función para refrescar logs
|
||||||
window.refreshLogs = function() {
|
window.refreshLogs = function() {
|
||||||
console.log('Refrescando logs...');
|
console.log('Refrescando logs...');
|
||||||
@@ -4190,3 +4688,225 @@ window.clearLogs = async function() {
|
|||||||
alert('Error: Sistema no inicializado');
|
alert('Error: Sistema no inicializado');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// GESTIÓN DE USUARIOS ADMINISTRADORES
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Renderizar tabla de usuarios administradores
|
||||||
|
function renderAdminUsersTable(users) {
|
||||||
|
const tbody = document.getElementById('admin-users-table');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center text-muted">
|
||||||
|
<i class="fas fa-inbox"></i> No hay usuarios registrados
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = users.map(user => {
|
||||||
|
const isActive = parseInt(user.is_active) === 1;
|
||||||
|
const lastLogin = user.last_login ?
|
||||||
|
new Date(user.last_login).toLocaleString('es-CO') :
|
||||||
|
'<span class="text-muted">Nunca</span>';
|
||||||
|
const createdAt = new Date(user.created_at).toLocaleDateString('es-CO');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${escapeHtml(user.username)}</strong></td>
|
||||||
|
<td>${escapeHtml(user.full_name || '-')}</td>
|
||||||
|
<td>${escapeHtml(user.email || '-')}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-${isActive ? 'success' : 'secondary'}">
|
||||||
|
${isActive ? 'Activo' : 'Inactivo'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><small>${lastLogin}</small></td>
|
||||||
|
<td><small>${createdAt}</small></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-primary"
|
||||||
|
onclick="editAdminUser(${user.id})"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-warning"
|
||||||
|
onclick="changeAdminPassword(${user.id}, '${escapeHtml(user.username)}')"
|
||||||
|
title="Cambiar contraseña">
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-${isActive ? 'secondary' : 'success'}"
|
||||||
|
onclick="toggleAdminUserStatus(${user.id}, ${!isActive})"
|
||||||
|
title="${isActive ? 'Desactivar' : 'Activar'}">
|
||||||
|
<i class="fas fa-${isActive ? 'ban' : 'check'}"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar modal para crear usuario
|
||||||
|
window.showCreateAdminUserModal = function() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('adminUserModal'));
|
||||||
|
document.getElementById('admin-user-modal-title').textContent = 'Nuevo Usuario';
|
||||||
|
document.getElementById('admin-user-form').reset();
|
||||||
|
document.getElementById('admin-user-id').value = '';
|
||||||
|
document.getElementById('password-group').style.display = 'block';
|
||||||
|
modal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Editar usuario existente
|
||||||
|
window.editAdminUser = async function(userId) {
|
||||||
|
try {
|
||||||
|
const response = await window.whatsappManager.apiCall('list_admin_users.php');
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
const user = response.data.find(u => parseInt(u.id) === parseInt(userId));
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
document.getElementById('admin-user-modal-title').textContent = 'Editar Usuario';
|
||||||
|
document.getElementById('admin-user-id').value = user.id;
|
||||||
|
document.getElementById('admin-username').value = user.username;
|
||||||
|
document.getElementById('admin-fullname').value = user.full_name || '';
|
||||||
|
document.getElementById('admin-email').value = user.email || '';
|
||||||
|
document.getElementById('password-group').style.display = 'none';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('adminUserModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cargando usuario:', error);
|
||||||
|
alert('Error al cargar datos del usuario');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Guardar usuario (crear o actualizar)
|
||||||
|
window.saveAdminUser = async function() {
|
||||||
|
const userId = document.getElementById('admin-user-id').value;
|
||||||
|
const username = document.getElementById('admin-username').value.trim();
|
||||||
|
const fullName = document.getElementById('admin-fullname').value.trim();
|
||||||
|
const email = document.getElementById('admin-email').value.trim();
|
||||||
|
const password = document.getElementById('admin-password').value;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
alert('El nombre de usuario es requerido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es nuevo usuario, validar contraseña
|
||||||
|
if (!userId && !password) {
|
||||||
|
alert('La contraseña es requerida para usuarios nuevos');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId && password.length < 6) {
|
||||||
|
alert('La contraseña debe tener al menos 6 caracteres');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
// Actualizar usuario existente
|
||||||
|
response = await window.whatsappManager.apiCall('update_admin_user.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { user_id: userId, username, full_name: fullName, email }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Crear nuevo usuario
|
||||||
|
response = await window.whatsappManager.apiCall('create_admin_user.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { username, full_name: fullName, email, password }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
window.whatsappManager.showSuccess(response.message || 'Usuario guardado correctamente');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('adminUserModal')).hide();
|
||||||
|
window.whatsappManager.loadAdminUsers();
|
||||||
|
} else {
|
||||||
|
alert(response?.error || 'Error al guardar usuario');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error guardando usuario:', error);
|
||||||
|
alert('Error al guardar usuario: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cambiar contraseña de usuario
|
||||||
|
window.changeAdminPassword = function(userId, username) {
|
||||||
|
document.getElementById('change-password-user-id').value = userId;
|
||||||
|
document.getElementById('change-password-username').textContent = username;
|
||||||
|
document.getElementById('new-password').value = '';
|
||||||
|
document.getElementById('confirm-password').value = '';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('changePasswordModal'));
|
||||||
|
modal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Guardar nueva contraseña
|
||||||
|
window.saveNewPassword = async function() {
|
||||||
|
const userId = document.getElementById('change-password-user-id').value;
|
||||||
|
const newPassword = document.getElementById('new-password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm-password').value;
|
||||||
|
|
||||||
|
if (!newPassword || newPassword.length < 6) {
|
||||||
|
alert('La contraseña debe tener al menos 6 caracteres');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
alert('Las contraseñas no coinciden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.whatsappManager.apiCall('change_admin_password.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { user_id: userId, new_password: newPassword }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
window.whatsappManager.showSuccess('Contraseña actualizada correctamente');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('changePasswordModal')).hide();
|
||||||
|
} else {
|
||||||
|
alert(response?.error || 'Error al cambiar contraseña');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cambiando contraseña:', error);
|
||||||
|
alert('Error al cambiar contraseña: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Activar/Desactivar usuario
|
||||||
|
window.toggleAdminUserStatus = async function(userId, isActive) {
|
||||||
|
const action = isActive ? 'activar' : 'desactivar';
|
||||||
|
|
||||||
|
if (!confirm(`¿Está seguro de ${action} este usuario?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.whatsappManager.apiCall('toggle_admin_user_status.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { user_id: userId, is_active: isActive }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
window.whatsappManager.showSuccess(`Usuario ${isActive ? 'activado' : 'desactivado'} correctamente`);
|
||||||
|
window.whatsappManager.loadAdminUsers();
|
||||||
|
} else {
|
||||||
|
alert(response?.error || `Error al ${action} usuario`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error ${action}ando usuario:`, error);
|
||||||
|
alert(`Error al ${action} usuario: ` + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
+219
-4
@@ -1119,21 +1119,25 @@ if (empty($user_id)) {
|
|||||||
option.value = template.name;
|
option.value = template.name;
|
||||||
option.textContent = `${display} (${lang})`;
|
option.textContent = `${display} (${lang})`;
|
||||||
option.dataset.language = lang;
|
option.dataset.language = lang;
|
||||||
|
option.dataset.templateId = template.id || '';
|
||||||
templateSelect.appendChild(option);
|
templateSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Valor por defecto global para la plantilla seleccionada
|
// Valor por defecto global para la plantilla seleccionada
|
||||||
window.language = 'en_US';
|
window.language = 'en_US';
|
||||||
|
window.currentTemplateId = null;
|
||||||
|
|
||||||
// Actualizar el language global cuando cambie la plantilla seleccionada
|
// Actualizar el language global cuando cambie la plantilla seleccionada
|
||||||
templateSelect.addEventListener('change', function() {
|
templateSelect.addEventListener('change', function() {
|
||||||
const sel = this.selectedOptions[0];
|
const sel = this.selectedOptions[0];
|
||||||
window.language = sel && sel.dataset && sel.dataset.language ? sel.dataset.language : 'en_US';
|
window.language = sel && sel.dataset && sel.dataset.language ? sel.dataset.language : 'en_US';
|
||||||
|
window.currentTemplateId = sel && sel.dataset && sel.dataset.templateId ? sel.dataset.templateId : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Si hay una plantilla seleccionada por defecto, establecer language acorde
|
// Si hay una plantilla seleccionada por defecto, establecer language acorde
|
||||||
if (templateSelect.selectedOptions.length && templateSelect.selectedOptions[0].dataset.language) {
|
if (templateSelect.selectedOptions.length && templateSelect.selectedOptions[0].dataset.language) {
|
||||||
window.language = templateSelect.selectedOptions[0].dataset.language;
|
window.language = templateSelect.selectedOptions[0].dataset.language;
|
||||||
|
window.currentTemplateId = templateSelect.selectedOptions[0].dataset.templateId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1167,7 +1171,13 @@ if (empty($user_id)) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendTemplateMessage(template,language, message);
|
// Verificar si la plantilla tiene variables
|
||||||
|
if (window.currentTemplateId) {
|
||||||
|
await checkTemplateVariables(window.currentTemplateId, template, language);
|
||||||
|
} else {
|
||||||
|
// Plantilla sin variables, enviar directo
|
||||||
|
await sendTemplateMessage(template, language, message);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await sendTextMessage(message);
|
await sendTextMessage(message);
|
||||||
}
|
}
|
||||||
@@ -1216,22 +1226,33 @@ if (empty($user_id)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enviar mensaje de plantilla
|
// Enviar mensaje de plantilla
|
||||||
async function sendTemplateMessage(template,language, parameters) {
|
async function sendTemplateMessage(template, language, parameters) {
|
||||||
try {
|
try {
|
||||||
showTyping();
|
showTyping();
|
||||||
|
|
||||||
console.log('Enviando plantilla:', {
|
console.log('Enviando plantilla:', {
|
||||||
template,
|
template,
|
||||||
|
language,
|
||||||
parameters,
|
parameters,
|
||||||
user: currentUser
|
user: currentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Preparar parámetros según el formato
|
||||||
|
let paramArray = [];
|
||||||
|
if (Array.isArray(parameters)) {
|
||||||
|
paramArray = parameters;
|
||||||
|
} else if (typeof parameters === 'string' && parameters) {
|
||||||
|
paramArray = parameters.split(',').map(p => p.trim());
|
||||||
|
} else {
|
||||||
|
paramArray = [];
|
||||||
|
}
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
recipient: currentUser.phone_number,
|
recipient: currentUser.phone_number,
|
||||||
type: 'template',
|
type: 'template',
|
||||||
template: template,
|
template: template,
|
||||||
language: language || 'en_US',
|
language: language || 'en_US',
|
||||||
parameters: parameters ? parameters.split(',') : []
|
parameters: paramArray
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Request body:', requestBody);
|
console.log('Request body:', requestBody);
|
||||||
@@ -1245,7 +1266,10 @@ if (empty($user_id)) {
|
|||||||
hideTyping();
|
hideTyping();
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
addMessageToView(`Plantilla: ${template}`, 'outgoing');
|
const displayMsg = paramArray.length > 0 ?
|
||||||
|
`Plantilla: ${template} (con ${paramArray.length} variable(s))` :
|
||||||
|
`Plantilla: ${template}`;
|
||||||
|
addMessageToView(displayMsg, 'outgoing');
|
||||||
showAlert('Mensaje de plantilla enviado correctamente', 'success');
|
showAlert('Mensaje de plantilla enviado correctamente', 'success');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.error || 'Error enviando plantilla');
|
throw new Error(response.error || 'Error enviando plantilla');
|
||||||
@@ -1925,6 +1949,197 @@ if (empty($user_id)) {
|
|||||||
const bsModal = new bootstrap.Modal(modal);
|
const bsModal = new bootstrap.Modal(modal);
|
||||||
bsModal.show();
|
bsModal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Variables globales para el modal de plantillas
|
||||||
|
let currentTemplateData = null;
|
||||||
|
|
||||||
|
// Verificar si la plantilla tiene variables
|
||||||
|
async function checkTemplateVariables(templateId, templateName, language) {
|
||||||
|
try {
|
||||||
|
const response = await whatsappManager.apiCall(`get_template_details.php?id=${templateId}`);
|
||||||
|
|
||||||
|
if (response && response.success && response.template) {
|
||||||
|
currentTemplateData = response.template;
|
||||||
|
|
||||||
|
if (response.template.has_variables) {
|
||||||
|
// Mostrar modal para llenar variables
|
||||||
|
showTemplateVariablesModal(response.template);
|
||||||
|
} else {
|
||||||
|
// Enviar directo sin variables
|
||||||
|
await sendTemplateMessage(templateName, language, []);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No se pudo obtener detalles de la plantilla');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking template variables:', error);
|
||||||
|
showAlert('Error al cargar detalles de plantilla: ' + error.message, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar modal con campos para variables
|
||||||
|
function showTemplateVariablesModal(template) {
|
||||||
|
// Actualizar información del encabezado
|
||||||
|
document.getElementById('templateNameDisplay').textContent = template.name;
|
||||||
|
document.getElementById('templateLanguageDisplay').textContent = `Idioma: ${template.language_code}`;
|
||||||
|
document.getElementById('templateVariablesCount').textContent = `${template.variables_count} variable(s) requerida(s)`;
|
||||||
|
|
||||||
|
// Generar formulario de variables
|
||||||
|
const formContainer = document.getElementById('variablesForm');
|
||||||
|
formContainer.innerHTML = '';
|
||||||
|
|
||||||
|
template.variables.forEach((variable, index) => {
|
||||||
|
const fieldHtml = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
<strong>${variable.label}</strong>
|
||||||
|
${variable.example ? `<small class="text-muted">(ej: ${variable.example})</small>` : ''}
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control template-variable-input"
|
||||||
|
data-index="${index}"
|
||||||
|
data-var-number="${variable.index}"
|
||||||
|
placeholder="${variable.example || 'Ingrese valor...'}"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
formContainer.insertAdjacentHTML('beforeend', fieldHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listeners para actualizar preview en tiempo real
|
||||||
|
const inputs = formContainer.querySelectorAll('.template-variable-input');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
input.addEventListener('input', updateTemplatePreview);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicializar preview vacío
|
||||||
|
updateTemplatePreview();
|
||||||
|
|
||||||
|
// Mostrar modal
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('templateVariablesModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Event listener para botón de envío
|
||||||
|
document.getElementById('btnSendTemplateWithVariables').onclick = sendTemplateWithVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar vista previa de la plantilla
|
||||||
|
async function updateTemplatePreview() {
|
||||||
|
if (!currentTemplateData) return;
|
||||||
|
|
||||||
|
const inputs = document.querySelectorAll('.template-variable-input');
|
||||||
|
const parameters = [];
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
parameters.push(input.value || '');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await whatsappManager.apiCall('preview_template.php', {
|
||||||
|
body: {
|
||||||
|
template_id: currentTemplateData.id,
|
||||||
|
parameters: parameters
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success && response.preview) {
|
||||||
|
const previewContainer = document.getElementById('templatePreview');
|
||||||
|
|
||||||
|
if (response.preview.is_complete) {
|
||||||
|
previewContainer.innerHTML = response.preview.html;
|
||||||
|
previewContainer.className = 'p-3 bg-light rounded border border-success';
|
||||||
|
} else {
|
||||||
|
previewContainer.innerHTML = response.preview.html +
|
||||||
|
'<div class="text-warning mt-2"><small><i class="fas fa-exclamation-triangle"></i> Faltan variables por completar</small></div>';
|
||||||
|
previewContainer.className = 'p-3 bg-light rounded border border-warning';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating preview:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar plantilla con variables
|
||||||
|
async function sendTemplateWithVariables() {
|
||||||
|
if (!currentTemplateData) return;
|
||||||
|
|
||||||
|
const inputs = document.querySelectorAll('.template-variable-input');
|
||||||
|
const parameters = [];
|
||||||
|
let allFilled = true;
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (!value) {
|
||||||
|
allFilled = false;
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
parameters.push(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allFilled) {
|
||||||
|
showAlert('Por favor complete todas las variables requeridas', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cerrar modal
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('templateVariablesModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
// Enviar plantilla con parámetros
|
||||||
|
await sendTemplateMessage(
|
||||||
|
currentTemplateData.template_name,
|
||||||
|
currentTemplateData.language_code,
|
||||||
|
parameters
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limpiar datos
|
||||||
|
currentTemplateData = null;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Modal para Variables de Plantilla -->
|
||||||
|
<div class="modal fade" id="templateVariablesModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-edit"></i> Completar Variables de Plantilla
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="templateInfo" class="alert alert-info mb-3">
|
||||||
|
<strong id="templateNameDisplay"></strong>
|
||||||
|
<div class="small mt-1">
|
||||||
|
<span id="templateLanguageDisplay"></span> |
|
||||||
|
<span id="templateVariablesCount"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="variablesForm"></div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6 class="border-bottom pb-2">
|
||||||
|
<i class="fas fa-eye"></i> Vista Previa
|
||||||
|
</h6>
|
||||||
|
<div id="templatePreview" class="p-3 bg-light rounded" style="min-height: 80px;">
|
||||||
|
<em class="text-muted">Complete los campos para ver la vista previa...</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success" id="btnSendTemplateWithVariables">
|
||||||
|
<i class="fas fa-paper-plane"></i> Enviar Mensaje
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
-153
@@ -1,153 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script de verificación SSE para HestiaCP
|
|
||||||
# Ejecutar en el servidor: bash check_sse.sh
|
|
||||||
|
|
||||||
echo "🔍 Verificación de SSE para HestiaCP"
|
|
||||||
echo "===================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 1. Verificar que el archivo existe
|
|
||||||
echo "1️⃣ Verificando archivos..."
|
|
||||||
if [ -f "api/sse_events.php" ]; then
|
|
||||||
echo " ✅ api/sse_events.php existe"
|
|
||||||
else
|
|
||||||
echo " ❌ api/sse_events.php NO encontrado"
|
|
||||||
echo " → Asegúrate de subir el archivo al servidor"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. Verificar permisos
|
|
||||||
echo ""
|
|
||||||
echo "2️⃣ Verificando permisos..."
|
|
||||||
PERMS=$(stat -c "%a" api/sse_events.php 2>/dev/null || stat -f "%A" api/sse_events.php 2>/dev/null)
|
|
||||||
if [ "$PERMS" -ge "644" ]; then
|
|
||||||
echo " ✅ Permisos correctos ($PERMS)"
|
|
||||||
else
|
|
||||||
echo " ⚠️ Permisos: $PERMS (se recomienda 644)"
|
|
||||||
echo " Ejecutar: chmod 644 api/sse_events.php"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Verificar versión PHP
|
|
||||||
echo ""
|
|
||||||
echo "3️⃣ Verificando PHP..."
|
|
||||||
PHP_VERSION=$(php -v | head -n 1)
|
|
||||||
echo " $PHP_VERSION"
|
|
||||||
if php -v | grep -q "PHP 8"; then
|
|
||||||
echo " ✅ PHP 8.x detectado"
|
|
||||||
elif php -v | grep -q "PHP 7"; then
|
|
||||||
echo " ⚠️ PHP 7.x (recomendado PHP 8.x)"
|
|
||||||
else
|
|
||||||
echo " ❌ Versión PHP desconocida"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Verificar sintaxis PHP
|
|
||||||
echo ""
|
|
||||||
echo "4️⃣ Verificando sintaxis PHP..."
|
|
||||||
if php -l api/sse_events.php > /dev/null 2>&1; then
|
|
||||||
echo " ✅ Sin errores de sintaxis"
|
|
||||||
else
|
|
||||||
echo " ❌ Error de sintaxis en sse_events.php"
|
|
||||||
php -l api/sse_events.php
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 5. Verificar configuración PHP-FPM
|
|
||||||
echo ""
|
|
||||||
echo "5️⃣ Verificando PHP-FPM..."
|
|
||||||
if systemctl is-active --quiet php8.2-fpm || systemctl is-active --quiet php8.1-fpm || systemctl is-active --quiet php-fpm; then
|
|
||||||
echo " ✅ PHP-FPM está corriendo"
|
|
||||||
else
|
|
||||||
echo " ⚠️ PHP-FPM no detectado o no corriendo"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 6. Probar conexión SSE local
|
|
||||||
echo ""
|
|
||||||
echo "6️⃣ Probando conexión SSE..."
|
|
||||||
echo " Conectando (3 segundos)..."
|
|
||||||
|
|
||||||
RESPONSE=$(timeout 3 curl -N -s 'http://localhost/api/sse_events.php?token=demo_token' 2>&1 || echo "timeout")
|
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q "event: connected"; then
|
|
||||||
echo " ✅ SSE funciona correctamente"
|
|
||||||
echo " Respuesta:"
|
|
||||||
echo "$RESPONSE" | head -5 | sed 's/^/ /'
|
|
||||||
elif echo "$RESPONSE" | grep -q "timeout"; then
|
|
||||||
echo " ⚠️ Timeout - SSE puede no estar configurado correctamente"
|
|
||||||
echo " → Revisar configuración Apache/Nginx"
|
|
||||||
elif echo "$RESPONSE" | grep -q "401"; then
|
|
||||||
echo " ⚠️ HTTP 401 - Problema de autenticación"
|
|
||||||
echo " → Revisar config/auth.php"
|
|
||||||
else
|
|
||||||
echo " ❌ Error inesperado"
|
|
||||||
echo " Respuesta:"
|
|
||||||
echo "$RESPONSE" | head -10 | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 7. Verificar .htaccess
|
|
||||||
echo ""
|
|
||||||
echo "7️⃣ Verificando .htaccess..."
|
|
||||||
if [ -f "api/.htaccess" ]; then
|
|
||||||
echo " ✅ api/.htaccess existe"
|
|
||||||
if grep -q "X-Accel-Buffering" api/.htaccess; then
|
|
||||||
echo " ✅ Configuración SSE encontrada"
|
|
||||||
else
|
|
||||||
echo " ⚠️ Falta configuración SSE en .htaccess"
|
|
||||||
echo " → Ver GUIA_HESTIA_SSE.md"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ⚠️ api/.htaccess no encontrado"
|
|
||||||
echo " → Crear según GUIA_HESTIA_SSE.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 8. Verificar configuración base de datos
|
|
||||||
echo ""
|
|
||||||
echo "8️⃣ Verificando configuración..."
|
|
||||||
if [ -f "config/config.php" ]; then
|
|
||||||
echo " ✅ config/config.php existe"
|
|
||||||
if php -r "require_once 'config/config.php'; echo 'OK';" 2>/dev/null | grep -q "OK"; then
|
|
||||||
echo " ✅ config.php carga correctamente"
|
|
||||||
else
|
|
||||||
echo " ⚠️ Error al cargar config.php"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ❌ config/config.php NO encontrado"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 9. Verificar tabla notifications
|
|
||||||
echo ""
|
|
||||||
echo "9️⃣ Verificando tabla notifications..."
|
|
||||||
if php -r "require_once 'config/config.php'; \$db = Database::getInstance(); \$r = \$db->fetchAll('SHOW TABLES LIKE \"notifications\"'); echo (count(\$r) > 0 ? 'OK' : 'NO');" 2>/dev/null | grep -q "OK"; then
|
|
||||||
echo " ✅ Tabla notifications existe"
|
|
||||||
else
|
|
||||||
echo " ⚠️ Tabla notifications no encontrada"
|
|
||||||
echo " → Ejecutar migration para crear tabla"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Resumen final
|
|
||||||
echo ""
|
|
||||||
echo "===================================="
|
|
||||||
echo "📊 Resumen"
|
|
||||||
echo "===================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q "event: connected"; then
|
|
||||||
echo "✅ SSE está funcionando correctamente"
|
|
||||||
echo ""
|
|
||||||
echo "Próximos pasos:"
|
|
||||||
echo "1. Abrir https://tu-dominio.com/conversations.php"
|
|
||||||
echo "2. Abrir consola del navegador (F12)"
|
|
||||||
echo "3. Verificar: '✅ SSE conectado'"
|
|
||||||
echo ""
|
|
||||||
echo "🚀 Sistema de notificaciones en tiempo real activo!"
|
|
||||||
else
|
|
||||||
echo "⚠️ SSE necesita configuración adicional"
|
|
||||||
echo ""
|
|
||||||
echo "Próximos pasos:"
|
|
||||||
echo "1. Revisar GUIA_HESTIA_SSE.md"
|
|
||||||
echo "2. Configurar .htaccess para SSE"
|
|
||||||
echo "3. Ajustar PHP-FPM timeout (300s)"
|
|
||||||
echo "4. Ejecutar: bash check_sse.sh"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Ver estructura de la tabla message_templates
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once 'config/config.php';
|
|
||||||
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$db = Database::getInstance();
|
|
||||||
|
|
||||||
echo "=== ESTRUCTURA DE TABLA message_templates ===\n\n";
|
|
||||||
|
|
||||||
// Describir la tabla
|
|
||||||
$stmt = $db->query("DESCRIBE message_templates");
|
|
||||||
$columns = $stmt->fetchAll();
|
|
||||||
|
|
||||||
echo "Columnas disponibles:\n";
|
|
||||||
foreach ($columns as $column) {
|
|
||||||
echo "• {$column['Field']} ({$column['Type']}) - {$column['Null']} - {$column['Default']}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n=== INTENTAR INSERCIÓN SIMPLE ===\n";
|
|
||||||
|
|
||||||
// Intentar inserción con campos mínimos
|
|
||||||
$stmt = $db->query(
|
|
||||||
"INSERT INTO message_templates (template_name, language_code, status) VALUES (?, ?, ?)",
|
|
||||||
['hello_world', 'en_US', 'approved']
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($stmt) {
|
|
||||||
echo "✅ Plantilla 'hello_world' agregada exitosamente\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar que se agregó
|
|
||||||
echo "\n📋 Plantillas en la BD:\n";
|
|
||||||
$stmt = $db->query("SELECT * FROM message_templates");
|
|
||||||
$templates = $stmt->fetchAll();
|
|
||||||
|
|
||||||
foreach ($templates as $template) {
|
|
||||||
echo "• {$template['template_name']} ({$template['language_code']}) - {$template['status']}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "❌ Error: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -628,6 +628,10 @@ function saveWhatsAppConfigToDB($whatsappConfig) {
|
|||||||
$success &= saveConfigToDB('whatsapp_phone_number_id', $whatsappConfig['phone_number_id']);
|
$success &= saveConfigToDB('whatsapp_phone_number_id', $whatsappConfig['phone_number_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($whatsappConfig['business_account_id'])) {
|
||||||
|
$success &= saveConfigToDB('whatsapp_business_account_id', $whatsappConfig['business_account_id']);
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($whatsappConfig['api_url'])) {
|
if (isset($whatsappConfig['api_url'])) {
|
||||||
$success &= saveConfigToDB('whatsapp_api_url', $whatsappConfig['api_url']);
|
$success &= saveConfigToDB('whatsapp_api_url', $whatsappConfig['api_url']);
|
||||||
}
|
}
|
||||||
@@ -657,6 +661,7 @@ function getWhatsAppConfigFromDB() {
|
|||||||
'token' => getConfigFromDB('whatsapp_token', ''),
|
'token' => getConfigFromDB('whatsapp_token', ''),
|
||||||
'whatsapp_token' => getConfigFromDB('whatsapp_token', ''),
|
'whatsapp_token' => getConfigFromDB('whatsapp_token', ''),
|
||||||
'phone_number_id' => getConfigFromDB('whatsapp_phone_number_id', ''),
|
'phone_number_id' => getConfigFromDB('whatsapp_phone_number_id', ''),
|
||||||
|
'business_account_id' => getConfigFromDB('whatsapp_business_account_id', ''),
|
||||||
'api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
|
'api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
|
||||||
'whatsapp_api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
|
'whatsapp_api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
|
||||||
'webhook_verify_token' => getConfigFromDB('webhook_verify_token', ''),
|
'webhook_verify_token' => getConfigFromDB('webhook_verify_token', ''),
|
||||||
|
|||||||
+759
-25
@@ -904,6 +904,10 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
<div id="bot-toggle-container" style="display:flex;align-items:center;gap:6px;margin-right:8px;">
|
<div id="bot-toggle-container" style="display:flex;align-items:center;gap:6px;margin-right:8px;">
|
||||||
<button class="btn btn-sm btn-outline-secondary" id="bot-toggle">Bot: On</button>
|
<button class="btn btn-sm btn-outline-secondary" id="bot-toggle">Bot: On</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Botón programar recordatorio -->
|
||||||
|
<button class="btn btn-sm btn-success" id="schedule-reminder-btn" title="Programar recordatorio">
|
||||||
|
<i class="fas fa-calendar-plus"></i>
|
||||||
|
</button>
|
||||||
<!-- Botón actualizar mensajes -->
|
<!-- Botón actualizar mensajes -->
|
||||||
<button class="btn btn-sm btn-outline-light" id="refresh-messages-btn" title="Actualizar mensajes"><i class="fas fa-sync-alt"></i></button>
|
<button class="btn btn-sm btn-outline-light" id="refresh-messages-btn" title="Actualizar mensajes"><i class="fas fa-sync-alt"></i></button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" id="mark-unread-btn" title="Marcar conversación como no leída" style="display:none;">Marcar no leído</button>
|
<button class="btn btn-sm btn-outline-secondary" id="mark-unread-btn" title="Marcar conversación como no leída" style="display:none;">Marcar no leído</button>
|
||||||
@@ -1010,9 +1014,9 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
// ESTE LOG DEBE APARECER PRIMERO
|
// ESTE LOG DEBE APARECER PRIMERO
|
||||||
console.clear();
|
console.clear();
|
||||||
console.log('%c═══════════════════════════════════════════', 'color: #25d366; font-weight: bold;');
|
console.log('%c═══════════════════════════════════════════', 'color: #25d366; font-weight: bold;');
|
||||||
console.log('%c🚀 WHATSAPP BOT v1.2.0 - Desarrollado por U-Site.app', 'background: #25d366; color: white; padding: 10px 20px; font-size: 18px; font-weight: bold; border-radius: 5px;');
|
console.log('%c🚀 WHATSAPP BOT v1.3.0 - Desarrollado por U-Site.app', 'background: #25d366; color: white; padding: 10px 20px; font-size: 18px; font-weight: bold; border-radius: 5px;');
|
||||||
console.log('%c═══════════════════════════════════════════', 'color: #25d366; font-weight: bold;');
|
console.log('%c═══════════════════════════════════════════', 'color: #25d366; font-weight: bold;');
|
||||||
window.__APP_VERSION__ = '1.2.0';
|
window.__APP_VERSION__ = '1.3.0';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
@@ -1030,10 +1034,10 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
<script src="assets/js/chat-common.js?v=<?php echo $asset_v; ?>"></script>
|
<script src="assets/js/chat-common.js?v=<?php echo $asset_v; ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
// ============================================
|
// ============================================
|
||||||
// 🚀 VERSIÓN ACTUALIZADA - 27 ENERO 2026
|
// 🚀 VERSIÓN ACTUALIZADA - 3 FEBRERO 2026
|
||||||
// ============================================
|
// ============================================
|
||||||
console.log('%c📦 Asset Version:', 'font-weight: bold; color: #2575fc;', '<?php echo $asset_v; ?>');
|
console.log('%c📦 Asset Version:', 'font-weight: bold; color: #2575fc;', '<?php echo $asset_v; ?>');
|
||||||
console.log('%c✨ Cambios: SSE mejorado, updateConversationInList con logging completo', 'color: #666;');
|
console.log('%c✨ Cambios: Gestión de usuarios del sistema, búsqueda mejorada en BD, plantillas optimizadas', 'color: #666;');
|
||||||
console.log('============================================');
|
console.log('============================================');
|
||||||
|
|
||||||
// Fallback ligero para showAlert (si no existe una implementación global)
|
// Fallback ligero para showAlert (si no existe una implementación global)
|
||||||
@@ -1783,9 +1787,13 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Búsqueda de conversaciones
|
// Búsqueda de conversaciones con debounce
|
||||||
|
let searchTimeout;
|
||||||
document.getElementById('search-input').addEventListener('input', (e) => {
|
document.getElementById('search-input').addEventListener('input', (e) => {
|
||||||
this.searchConversations(e.target.value);
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
this.searchConversations(e.target.value);
|
||||||
|
}, 500); // Esperar 500ms después de que el usuario deje de escribir
|
||||||
});
|
});
|
||||||
|
|
||||||
// Envío de mensajes
|
// Envío de mensajes
|
||||||
@@ -2257,6 +2265,30 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Botón de programar recordatorio
|
||||||
|
const reminderBtn = document.getElementById('schedule-reminder-btn');
|
||||||
|
if (reminderBtn) {
|
||||||
|
reminderBtn.addEventListener('click', () => this.showReminderModal());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botón de guardar recordatorio
|
||||||
|
const saveReminderBtn = document.getElementById('save-reminder-btn');
|
||||||
|
if (saveReminderBtn) {
|
||||||
|
saveReminderBtn.addEventListener('click', () => this.saveReminder());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selector de plantilla en recordatorio
|
||||||
|
const reminderTemplate = document.getElementById('reminder-template');
|
||||||
|
if (reminderTemplate) {
|
||||||
|
reminderTemplate.addEventListener('change', () => this.handleReminderTemplateChange());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botón de enviar plantilla con variables
|
||||||
|
const sendTemplateWithVarsBtn = document.getElementById('send-template-with-vars-btn');
|
||||||
|
if (sendTemplateWithVarsBtn) {
|
||||||
|
sendTemplateWithVarsBtn.addEventListener('click', () => this.sendTemplateWithVariables());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAutoRefresh() {
|
setupAutoRefresh() {
|
||||||
@@ -4398,6 +4430,309 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== FUNCIONES DE RECORDATORIO ==========
|
||||||
|
|
||||||
|
async showReminderModal() {
|
||||||
|
console.log('🔵 showReminderModal - currentUserId:', this.currentUserId);
|
||||||
|
console.log('📋 conversations:', this.conversations);
|
||||||
|
|
||||||
|
if (!this.currentUserId) {
|
||||||
|
alert('Selecciona una conversación primero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar usuario en conversaciones - probar múltiples formas
|
||||||
|
let user = this.conversations.find(c => c.id == this.currentUserId);
|
||||||
|
if (!user) {
|
||||||
|
user = this.conversations.find(c => c.user_id == this.currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('👤 Usuario encontrado:', user);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const userName = user.name || user.user_name || 'Sin nombre';
|
||||||
|
const phoneNumber = user.phone_number || user.phone || 'Sin teléfono';
|
||||||
|
document.getElementById('reminder-user-name').value = `${userName} (${phoneNumber})`;
|
||||||
|
console.log('✅ Usuario establecido:', userName, phoneNumber);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Usuario no encontrado en conversaciones');
|
||||||
|
// Intentar obtener desde el DOM
|
||||||
|
const nameEl = document.getElementById('chat-name-text') || document.getElementById('chat-name');
|
||||||
|
const phoneEl = document.getElementById('chat-phone');
|
||||||
|
if (nameEl && phoneEl) {
|
||||||
|
document.getElementById('reminder-user-name').value = `${nameEl.textContent} (${phoneEl.textContent})`;
|
||||||
|
console.log('✅ Usuario obtenido del DOM');
|
||||||
|
} else {
|
||||||
|
document.getElementById('reminder-user-name').value = 'Usuario actual';
|
||||||
|
console.warn('⚠️ No se pudo obtener nombre del usuario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establecer fecha mínima (hoy)
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('reminder-date').setAttribute('min', today);
|
||||||
|
document.getElementById('reminder-date').value = today;
|
||||||
|
|
||||||
|
// Cargar plantillas aprobadas
|
||||||
|
await this.loadReminderTemplates();
|
||||||
|
|
||||||
|
// Mostrar modal
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('reminderModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadReminderTemplates() {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('get_templates.php?approved_only=1');
|
||||||
|
const select = document.getElementById('reminder-template');
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
select.innerHTML = '<option value="">Seleccionar plantilla...</option>';
|
||||||
|
response.data.forEach(template => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = template.id;
|
||||||
|
option.textContent = `${template.name} (${template.language_code})`;
|
||||||
|
option.dataset.templateId = template.id;
|
||||||
|
option.dataset.templateName = template.template_name;
|
||||||
|
option.dataset.language = template.language_code;
|
||||||
|
option.dataset.bodyText = template.body_text || '';
|
||||||
|
// Detectar variables tanto numéricas como con nombres
|
||||||
|
option.dataset.hasVariables = template.body_text && /\{\{[^\}]+\}\}/.test(template.body_text);
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading templates:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleReminderTemplateChange() {
|
||||||
|
const select = document.getElementById('reminder-template');
|
||||||
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
|
|
||||||
|
console.log('🔵 handleReminderTemplateChange llamado');
|
||||||
|
console.log('📋 Option seleccionada:', selectedOption);
|
||||||
|
|
||||||
|
if (!selectedOption || !selectedOption.value) {
|
||||||
|
document.getElementById('reminder-variables-container').style.display = 'none';
|
||||||
|
document.getElementById('reminder-preview').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasVariables = selectedOption.dataset.hasVariables === 'true';
|
||||||
|
const bodyText = selectedOption.dataset.bodyText || '';
|
||||||
|
|
||||||
|
console.log('🔍 hasVariables:', hasVariables);
|
||||||
|
console.log('📝 bodyText:', bodyText);
|
||||||
|
|
||||||
|
if (hasVariables) {
|
||||||
|
// Usar get_template_details.php para obtener variables con sus metadatos
|
||||||
|
try {
|
||||||
|
const templateId = selectedOption.dataset.templateId;
|
||||||
|
const detailsResp = await this.apiCall(`get_template_details.php?id=${templateId}`);
|
||||||
|
|
||||||
|
if (detailsResp && detailsResp.success && detailsResp.template) {
|
||||||
|
const variables = detailsResp.template.variables || [];
|
||||||
|
console.log('✅ Variables obtenidas de API:', variables);
|
||||||
|
|
||||||
|
// Generar campos
|
||||||
|
const fieldsContainer = document.getElementById('reminder-variables-fields');
|
||||||
|
fieldsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
variables.forEach(v => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'mb-2';
|
||||||
|
div.innerHTML = `
|
||||||
|
<label class="form-label small">${v.label}</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-sm reminder-variable-input"
|
||||||
|
data-index="${v.index}"
|
||||||
|
data-placeholder="${v.placeholder || ''}"
|
||||||
|
placeholder="${v.example || 'Ingrese valor...'}"
|
||||||
|
required>
|
||||||
|
`;
|
||||||
|
fieldsContainer.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listeners para actualizar preview
|
||||||
|
fieldsContainer.querySelectorAll('.reminder-variable-input').forEach(input => {
|
||||||
|
input.addEventListener('input', () => this.updateReminderPreview());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error obteniendo detalles de plantilla:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('reminder-variables-container').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('reminder-variables-container').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar preview
|
||||||
|
this.updateReminderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReminderPreview() {
|
||||||
|
const select = document.getElementById('reminder-template');
|
||||||
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
|
|
||||||
|
if (!selectedOption || !selectedOption.value) {
|
||||||
|
document.getElementById('reminder-preview').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyText = selectedOption.dataset.bodyText || '';
|
||||||
|
const hasVariables = selectedOption.dataset.hasVariables === 'true';
|
||||||
|
|
||||||
|
if (hasVariables) {
|
||||||
|
const inputs = document.querySelectorAll('.reminder-variable-input');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const index = input.dataset.index;
|
||||||
|
const value = input.value || `{{${index}}}`;
|
||||||
|
bodyText = bodyText.replace(new RegExp(`\\{\\{${index}\\}\\}`, 'g'), value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewContainer = document.getElementById('reminder-preview-content');
|
||||||
|
previewContainer.innerHTML = bodyText.replace(/\n/g, '<br>');
|
||||||
|
document.getElementById('reminder-preview').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveReminder() {
|
||||||
|
if (!this.currentUserId) return;
|
||||||
|
|
||||||
|
const date = document.getElementById('reminder-date').value;
|
||||||
|
const time = document.getElementById('reminder-time').value;
|
||||||
|
const select = document.getElementById('reminder-template');
|
||||||
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
|
|
||||||
|
if (!date || !time) {
|
||||||
|
alert('Por favor completa fecha y hora');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedOption || !selectedOption.value) {
|
||||||
|
alert('Por favor selecciona una plantilla');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recopilar parámetros si hay variables
|
||||||
|
const hasVariables = selectedOption.dataset.hasVariables === 'true' || selectedOption.dataset.hasVariables === true;
|
||||||
|
console.log('🔍 hasVariables:', hasVariables, 'tipo:', typeof selectedOption.dataset.hasVariables);
|
||||||
|
let parameters = null;
|
||||||
|
|
||||||
|
if (hasVariables) {
|
||||||
|
const inputs = document.querySelectorAll('.reminder-variable-input');
|
||||||
|
console.log('📝 Inputs encontrados:', inputs.length);
|
||||||
|
|
||||||
|
if (inputs.length === 0) {
|
||||||
|
console.log('⚠️ No hay inputs de variables');
|
||||||
|
} else {
|
||||||
|
// Detectar tipo de variables usando el placeholder del primer input
|
||||||
|
const firstInput = inputs[0];
|
||||||
|
const placeholder = firstInput?.dataset?.placeholder || '';
|
||||||
|
const isNumericVar = /^\{\{\d+\}\}$/.test(placeholder);
|
||||||
|
|
||||||
|
console.log('🔍 Detectando tipo de variables en recordatorio:');
|
||||||
|
console.log(' - Placeholder ejemplo:', placeholder);
|
||||||
|
console.log(' - Es numérica?', isNumericVar);
|
||||||
|
|
||||||
|
let allFilled = true;
|
||||||
|
|
||||||
|
if (isNumericVar) {
|
||||||
|
// Variables numéricas: crear array ordenado
|
||||||
|
const tempObj = {};
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const value = input.value.trim();
|
||||||
|
const index = parseInt(input.dataset.index || input.dataset.var);
|
||||||
|
if (!value) {
|
||||||
|
allFilled = false;
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
tempObj[index] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convertir a array ordenado
|
||||||
|
const sortedKeys = Object.keys(tempObj).map(Number).sort((a, b) => a - b);
|
||||||
|
parameters = sortedKeys.map(key => tempObj[key]);
|
||||||
|
console.log('📊 Enviando como array ordenado:', parameters);
|
||||||
|
} else {
|
||||||
|
// Variables con nombres: crear objeto
|
||||||
|
parameters = {};
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const value = input.value.trim();
|
||||||
|
const placeholder = input.dataset.placeholder || '';
|
||||||
|
const varName = placeholder.replace(/\{\{|\}\}/g, '');
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
allFilled = false;
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
parameters[varName] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('📦 Enviando como objeto con nombres:', parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allFilled) {
|
||||||
|
alert('Por favor completa todas las variables');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Plantilla sin variables');
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateId = selectedOption.dataset.templateId;
|
||||||
|
const templateName = selectedOption.dataset.templateName;
|
||||||
|
const language = selectedOption.dataset.language;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user_id: parseInt(this.currentUserId),
|
||||||
|
message_type: 'template',
|
||||||
|
template_id: parseInt(templateId),
|
||||||
|
template_name: templateName,
|
||||||
|
template_language: language,
|
||||||
|
template_parameters: parameters,
|
||||||
|
scheduled_date: date,
|
||||||
|
scheduled_time: time
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('💾 Guardando recordatorio:', payload);
|
||||||
|
console.log('📊 Parámetros detalle:', {
|
||||||
|
esNull: parameters === null,
|
||||||
|
esArray: Array.isArray(parameters),
|
||||||
|
longitud: parameters ? parameters.length : 0,
|
||||||
|
valores: parameters
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('schedule_message.php', {
|
||||||
|
body: payload
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Respuesta del servidor:', response);
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
alert('✅ Recordatorio programado exitosamente');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('reminderModal')).hide();
|
||||||
|
|
||||||
|
// Limpiar formulario
|
||||||
|
document.getElementById('reminder-template').value = '';
|
||||||
|
document.getElementById('reminder-variables-container').style.display = 'none';
|
||||||
|
document.getElementById('reminder-preview').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (response.error || 'Error desconocido'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving reminder:', error);
|
||||||
|
alert('Error al programar recordatorio: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadQuickReplies() {
|
async loadQuickReplies() {
|
||||||
const panel = document.getElementById('quick-replies-panel');
|
const panel = document.getElementById('quick-replies-panel');
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
@@ -4474,8 +4809,210 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendTemplateQuick(templateName, language) {
|
async sendTemplateQuick(templateName, language) {
|
||||||
// delegate to unified sendTemplateMessage helper
|
console.log('🔵 sendTemplateQuick:', templateName, language);
|
||||||
await this.sendTemplateMessage(templateName, language, null);
|
// Primero, cargar detalles de la plantilla para verificar si tiene variables
|
||||||
|
try {
|
||||||
|
// Buscar el template_id desde la caché o hacer una petición
|
||||||
|
const templatesResp = await this.apiCall('get_templates.php?approved_only=1');
|
||||||
|
const template = templatesResp.data.find(t => t.template_name === templateName);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
throw new Error('Plantilla no encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Template encontrado:', template);
|
||||||
|
|
||||||
|
// Verificar si tiene variables (tanto numéricas como con nombres)
|
||||||
|
const hasVariables = template.body_text && /\{\{[^\}]+\}\}/.test(template.body_text);
|
||||||
|
console.log('🔍 Tiene variables?', hasVariables);
|
||||||
|
|
||||||
|
if (hasVariables) {
|
||||||
|
// Mostrar modal para pedir variables
|
||||||
|
await this.showTemplateVariablesModal(template);
|
||||||
|
} else {
|
||||||
|
// Enviar directamente sin variables
|
||||||
|
await this.sendTemplateMessage(templateName, language, null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error en sendTemplateQuick:', error);
|
||||||
|
alert('Error al cargar plantilla: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showTemplateVariablesModal(template) {
|
||||||
|
console.log('🔵 showTemplateVariablesModal:', template);
|
||||||
|
|
||||||
|
// Guardar template actual para usarlo al enviar
|
||||||
|
this.currentTemplate = template;
|
||||||
|
|
||||||
|
// Establecer nombre de plantilla en modal
|
||||||
|
document.getElementById('template-modal-name').textContent = template.name || template.template_name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Usar get_template_details.php para obtener variables con metadatos
|
||||||
|
const detailsResp = await this.apiCall(`get_template_details.php?id=${template.id}`);
|
||||||
|
|
||||||
|
if (!detailsResp || !detailsResp.success) {
|
||||||
|
throw new Error('No se pudieron cargar los detalles de la plantilla');
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = detailsResp.template.variables || [];
|
||||||
|
console.log('📝 Variables obtenidas:', variables);
|
||||||
|
|
||||||
|
// Guardar template completo con variables para usarlo después
|
||||||
|
this.currentTemplate = { ...template, variables: variables };
|
||||||
|
|
||||||
|
// Generar campos de entrada
|
||||||
|
const container = document.getElementById('template-variables-container');
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
variables.forEach((variable) => {
|
||||||
|
html += `
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><strong>${variable.label}</strong>
|
||||||
|
${variable.example ? `<small class="text-muted">(ej: ${variable.example})</small>` : ''}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control template-var-input"
|
||||||
|
data-var="${variable.index}"
|
||||||
|
data-placeholder="${variable.placeholder}"
|
||||||
|
placeholder="${variable.example || 'Ingrese valor...'}"
|
||||||
|
onkeyup="window.chatApp.updateTemplatePreview()"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Generar vista previa inicial
|
||||||
|
this.updateTemplatePreview();
|
||||||
|
|
||||||
|
// Mostrar modal
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('templateVariablesModal'));
|
||||||
|
modal.show();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error cargando detalles de plantilla:', error);
|
||||||
|
alert('Error al cargar plantilla: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTemplatePreview() {
|
||||||
|
if (!this.currentTemplate) return;
|
||||||
|
|
||||||
|
const previewContainer = document.getElementById('template-preview');
|
||||||
|
const previewContent = document.getElementById('template-preview-content');
|
||||||
|
|
||||||
|
// Recopilar parámetros
|
||||||
|
const inputs = document.querySelectorAll('.template-var-input');
|
||||||
|
const parameters = [];
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const varIndex = parseInt(input.dataset.var) - 1;
|
||||||
|
parameters[varIndex] = input.value || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Usar preview_template.php para renderizar correctamente
|
||||||
|
const response = await this.apiCall('preview_template.php', {
|
||||||
|
body: {
|
||||||
|
template_id: this.currentTemplate.id,
|
||||||
|
parameters: parameters
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success && response.preview) {
|
||||||
|
previewContent.innerHTML = response.preview.html || response.preview.body.replace(/\n/g, '<br>');
|
||||||
|
previewContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generando preview:', error);
|
||||||
|
// Fallback: mostrar body_text sin procesar
|
||||||
|
previewContent.innerHTML = this.currentTemplate.body_text.replace(/\n/g, '<br>');
|
||||||
|
previewContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTemplateWithVariables() {
|
||||||
|
console.log('🔵 sendTemplateWithVariables');
|
||||||
|
|
||||||
|
if (!this.currentTemplate) {
|
||||||
|
alert('No hay plantilla seleccionada');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recopilar variables
|
||||||
|
const inputs = document.querySelectorAll('.template-var-input');
|
||||||
|
let allFilled = true;
|
||||||
|
|
||||||
|
// Detectar si son variables numéricas o con nombres usando el placeholder
|
||||||
|
const firstInput = inputs[0];
|
||||||
|
const placeholder = firstInput?.dataset?.placeholder || '';
|
||||||
|
// Si el placeholder es {{1}}, {{2}}, etc. -> numéricas
|
||||||
|
// Si es {{nombre_tema}}, {{fecha}}, etc. -> con nombres
|
||||||
|
const isNumericVar = /^\{\{\d+\}\}$/.test(placeholder);
|
||||||
|
|
||||||
|
console.log('🔍 Detectando tipo de variables:');
|
||||||
|
console.log(' - Placeholder ejemplo:', placeholder);
|
||||||
|
console.log(' - Es numérica?', isNumericVar);
|
||||||
|
|
||||||
|
let parameters;
|
||||||
|
if (isNumericVar) {
|
||||||
|
// Variables numéricas {{1}}, {{2}} - usar array
|
||||||
|
parameters = [];
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (!value) {
|
||||||
|
allFilled = false;
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
parameters.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('📊 Enviando como array:', parameters);
|
||||||
|
} else {
|
||||||
|
// Variables con nombres {{fecha}}, {{motivo}} - usar objeto
|
||||||
|
parameters = {};
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const value = input.value.trim();
|
||||||
|
const placeholder = input.dataset.placeholder || '';
|
||||||
|
// Extraer nombre de {{fecha}} -> "fecha"
|
||||||
|
const varName = placeholder.replace(/\{\{|\}\}/g, '');
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
allFilled = false;
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
parameters[varName] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('📦 Enviando como objeto:', parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allFilled) {
|
||||||
|
alert('Por favor completa todas las variables');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📝 Parámetros finales:', parameters);
|
||||||
|
|
||||||
|
// Cerrar modal
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('templateVariablesModal'));
|
||||||
|
if (modal) modal.hide();
|
||||||
|
|
||||||
|
// Enviar plantilla con parámetros
|
||||||
|
await this.sendTemplateMessage(
|
||||||
|
this.currentTemplate.template_name,
|
||||||
|
this.currentTemplate.language_code || 'es',
|
||||||
|
parameters
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limpiar template actual
|
||||||
|
this.currentTemplate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTemplateMessage(templateName, language = 'es', parameters = null) {
|
async sendTemplateMessage(templateName, language = 'es', parameters = null) {
|
||||||
@@ -4547,10 +5084,49 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
|
|
||||||
async sendQuickReply(text) {
|
async sendQuickReply(text) {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
if (!this.currentUserId) return;
|
||||||
|
|
||||||
const input = document.getElementById('message-input');
|
const input = document.getElementById('message-input');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
input.value = text;
|
|
||||||
await this.sendMessage();
|
// No copiar al input para preservar saltos de línea
|
||||||
|
// Enviar directamente usando la API
|
||||||
|
input.disabled = true;
|
||||||
|
const sendBtn = document.getElementById('send-btn');
|
||||||
|
if (sendBtn) sendBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const recipient = this.getConversationPhone(this.currentUserId);
|
||||||
|
if (!recipient) {
|
||||||
|
alert('No se pudo determinar el número de destino');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
recipient: recipient,
|
||||||
|
type: 'text',
|
||||||
|
message: text
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.apiCall('send_message.php', { body: requestBody });
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
// Mostrar inmediatamente en la vista
|
||||||
|
this.addMessageToView(text, 'outgoing');
|
||||||
|
typeof showAlert !== 'undefined' && showAlert('Mensaje enviado correctamente', 'success');
|
||||||
|
// Recargar mensajes en background
|
||||||
|
this.loadconversations(this.currentUserId, false).catch(e=>console.warn(e));
|
||||||
|
} else {
|
||||||
|
throw new Error(result && result.error ? result.error : 'Error desconocido');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending quick reply:', error);
|
||||||
|
alert('Error al enviar respuesta rápida: ' + (error.message || error));
|
||||||
|
} finally {
|
||||||
|
input.disabled = false;
|
||||||
|
if (sendBtn) sendBtn.disabled = false;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
@@ -4784,7 +5360,52 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchConversations(query) {
|
async searchConversations(query) {
|
||||||
|
console.log('🔍 Buscando conversaciones:', query);
|
||||||
|
|
||||||
|
// Limpiar búsqueda si el query está vacío
|
||||||
|
if (!query || query.trim() === '') {
|
||||||
|
// Recargar conversaciones normales
|
||||||
|
await this.loadConversations(1, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar en la base de datos
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall(`get_conversations.php?search=${encodeURIComponent(query.trim())}`);
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
this.conversations = response.data || [];
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.hasMore = false; // Desactivar paginación en búsqueda
|
||||||
|
this.renderConversations();
|
||||||
|
|
||||||
|
console.log(`✅ Búsqueda completada: ${this.conversations.length} resultados`);
|
||||||
|
|
||||||
|
if (this.conversations.length === 0) {
|
||||||
|
const container = document.getElementById('conversation-list');
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="padding: 20px; text-align: center; color: #999;">
|
||||||
|
<i class="fas fa-search" style="font-size: 48px; margin-bottom: 10px; opacity: 0.3;"></i>
|
||||||
|
<p>No se encontraron conversaciones con "${query}"</p>
|
||||||
|
<small>Intenta buscar por nombre, teléfono o contenido del mensaje</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error en búsqueda:', response);
|
||||||
|
// Fallback a búsqueda local si falla el servidor
|
||||||
|
this.searchConversationsLocal(query);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error buscando conversaciones:', error);
|
||||||
|
// Fallback a búsqueda local
|
||||||
|
this.searchConversationsLocal(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Búsqueda local (fallback) - solo filtra las conversaciones ya cargadas
|
||||||
|
searchConversationsLocal(query) {
|
||||||
const items = document.querySelectorAll('.conversation-item');
|
const items = document.querySelectorAll('.conversation-item');
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const name = item.querySelector('.conversation-name').textContent.toLowerCase();
|
const name = item.querySelector('.conversation-name').textContent.toLowerCase();
|
||||||
@@ -5041,6 +5662,9 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
const localThumb = message.local_thumb || '';
|
const localThumb = message.local_thumb || '';
|
||||||
const messageId = message.id || message.message_id || '';
|
const messageId = message.id || message.message_id || '';
|
||||||
|
|
||||||
|
// Obtener la URL base del sitio para URLs absolutas
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
|
||||||
// Debug completo del mensaje
|
// Debug completo del mensaje
|
||||||
if (mediaType === 'audio' || mediaType === 'image' || mediaType === 'video') {
|
if (mediaType === 'audio' || mediaType === 'image' || mediaType === 'video') {
|
||||||
console.log(`🎵 Media message (${mediaType}) - Datos completos:`, {
|
console.log(`🎵 Media message (${mediaType}) - Datos completos:`, {
|
||||||
@@ -5050,7 +5674,8 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
mediaUrl: mediaUrl,
|
mediaUrl: mediaUrl,
|
||||||
mediaUrlExternal: mediaUrlExternal,
|
mediaUrlExternal: mediaUrlExternal,
|
||||||
hasLocalFile: !!localFile,
|
hasLocalFile: !!localFile,
|
||||||
messageType: mediaType
|
messageType: mediaType,
|
||||||
|
baseUrl: baseUrl
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5059,23 +5684,27 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
let thumb = '';
|
let thumb = '';
|
||||||
|
|
||||||
if (localFile) {
|
if (localFile) {
|
||||||
// ✅ PRIORIDAD 1: Archivo local guardado
|
// ✅ PRIORIDAD 1: Archivo local guardado (usar URL absoluta)
|
||||||
full = `/${localFile}`;
|
full = localFile.startsWith('http') ? localFile : `${baseUrl}/${localFile.replace(/^\//, '')}`;
|
||||||
thumb = localThumb ? `/${localThumb}` : `/${localFile}`;
|
thumb = localThumb
|
||||||
console.log('✅ Usando archivo local:', full);
|
? (localThumb.startsWith('http') ? localThumb : `${baseUrl}/${localThumb.replace(/^\//, '')}`)
|
||||||
|
: full;
|
||||||
|
console.log('✅ Usando archivo local (URL absoluta):', full);
|
||||||
} else if (messageId && !mediaUrlExternal) {
|
} else if (messageId && !mediaUrlExternal) {
|
||||||
// ⚠️ PRIORIDAD 2: Usar proxy con el ID de la base de datos solo si no hay media_url_external
|
// ⚠️ PRIORIDAD 2: Usar proxy con el ID de la base de datos (URL absoluta)
|
||||||
full = `/api/version/media-url.php?id=${encodeURIComponent(messageId)}`;
|
full = `${baseUrl}/api/version/media-url.php?id=${encodeURIComponent(messageId)}`;
|
||||||
thumb = full;
|
thumb = full;
|
||||||
console.log('⚠️ No hay local_file, usando proxy con messageId:', messageId);
|
console.log('⚠️ No hay local_file, usando proxy con messageId:', messageId);
|
||||||
} else if (mediaUrlExternal) {
|
} else if (mediaUrlExternal) {
|
||||||
// 🔄 PRIORIDAD 3: Usar media_url_external (puede ser api/get_media.php o URL directa)
|
// 🔄 PRIORIDAD 3: Usar media_url_external (convertir a URL absoluta si es relativa)
|
||||||
full = mediaUrlExternal.startsWith('/') ? mediaUrlExternal : `/${mediaUrlExternal}`;
|
full = mediaUrlExternal.startsWith('http')
|
||||||
|
? mediaUrlExternal
|
||||||
|
: `${baseUrl}/${mediaUrlExternal.replace(/^\//, '')}`;
|
||||||
thumb = full;
|
thumb = full;
|
||||||
console.log('🔄 Usando media_url_external:', full);
|
console.log('🔄 Usando media_url_external (URL absoluta):', full);
|
||||||
} else if (mediaUrl && /^\d+$/.test(mediaUrl)) {
|
} else if (mediaUrl && /^\d+$/.test(mediaUrl)) {
|
||||||
// 📱 PRIORIDAD 4: Es un ID de WhatsApp (solo números)
|
// 📱 PRIORIDAD 4: Es un ID de WhatsApp (solo números) - usar URL absoluta
|
||||||
full = `/api/get_media.php?id=${encodeURIComponent(mediaUrl)}`;
|
full = `${baseUrl}/api/get_media.php?id=${encodeURIComponent(mediaUrl)}`;
|
||||||
thumb = full;
|
thumb = full;
|
||||||
console.log('📱 Usando whatsapp_media_id con get_media.php:', mediaUrl);
|
console.log('📱 Usando whatsapp_media_id con get_media.php:', mediaUrl);
|
||||||
} else if (mediaUrl && /^https?:\/\//i.test(mediaUrl)) {
|
} else if (mediaUrl && /^https?:\/\//i.test(mediaUrl)) {
|
||||||
@@ -5115,11 +5744,25 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
case 'audio': {
|
case 'audio': {
|
||||||
// Para audio, usar la misma lógica de full
|
// Para audio, usar la misma lógica de full
|
||||||
let audioSrc = full;
|
let audioSrc = full;
|
||||||
|
|
||||||
|
if (!audioSrc) {
|
||||||
|
return `
|
||||||
|
<div class="message-media">
|
||||||
|
<div class="alert alert-warning mb-0" style="font-size: 13px;">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Audio no disponible
|
||||||
|
<br><small>El audio puede haber expirado o no está disponible</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="message-media">
|
<div class="message-media">
|
||||||
<audio controls>
|
<audio controls controlsList="nodownload" preload="metadata" onerror="this.parentElement.innerHTML='<div class=\\'alert alert-danger mb-0\\' style=\\'font-size:13px;\\'><i class=\\'fas fa-times-circle\\'></i> Error al cargar audio<br><small>Archivo no encontrado o no disponible</small></div>'">
|
||||||
<source src="${escapeHtml(audioSrc)}" type="audio/mpeg">
|
<source src="${escapeHtml(audioSrc)}" type="audio/mpeg">
|
||||||
Tu navegador no soporta audio.
|
<source src="${escapeHtml(audioSrc)}" type="audio/ogg">
|
||||||
|
<source src="${escapeHtml(audioSrc)}" type="audio/wav">
|
||||||
|
Tu navegador no soporta reproducción de audio.
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -5364,5 +6007,96 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal para programar recordatorio -->
|
||||||
|
<div class="modal fade" id="reminderModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-calendar-plus"></i> Programar Recordatorio
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Cliente</label>
|
||||||
|
<input type="text" class="form-control" id="reminder-user-name" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Fecha</label>
|
||||||
|
<input type="date" class="form-control" id="reminder-date" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Hora</label>
|
||||||
|
<input type="time" class="form-control" id="reminder-time" value="09:00" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Seleccionar Plantilla</label>
|
||||||
|
<select class="form-select" id="reminder-template" required>
|
||||||
|
<option value="">Cargando plantillas...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="reminder-variables-container" style="display:none;">
|
||||||
|
<label class="form-label">Variables de la Plantilla</label>
|
||||||
|
<div id="reminder-variables-fields"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="reminder-preview" class="mt-3" style="display:none;">
|
||||||
|
<h6 class="border-bottom pb-2">Vista Previa</h6>
|
||||||
|
<div id="reminder-preview-content" class="p-3 bg-light rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success" id="save-reminder-btn">
|
||||||
|
<i class="fas fa-save"></i> Programar Recordatorio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal para variables de plantilla (envío normal) -->
|
||||||
|
<div class="modal fade" id="templateVariablesModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-primary text-white">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-edit"></i> Variables de la Plantilla
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong id="template-modal-name"></strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="template-variables-container">
|
||||||
|
<!-- Variables dinámicas -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="template-preview" class="mt-3" style="display:none;">
|
||||||
|
<h6 class="border-bottom pb-2">Vista Previa</h6>
|
||||||
|
<div id="template-preview-content" class="p-3 bg-light rounded"></div>
|
||||||
|
</div>
|
||||||
|
</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" id="send-template-with-vars-btn">
|
||||||
|
<i class="fas fa-paper-plane"></i> Enviar Plantilla
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRACIÓN: 02_add_template_components
|
||||||
|
-- Fecha: 2026-02-03
|
||||||
|
-- Descripción: Agregar columnas para almacenar componentes de plantillas de WhatsApp
|
||||||
|
-- Esto permite capturar variables/placeholders y generar vistas previas
|
||||||
|
-- Nota: Usa IF NOT EXISTS - Es seguro ejecutar múltiples veces
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Agregar columna para el texto del body de la plantilla
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS body_text TEXT DEFAULT NULL
|
||||||
|
COMMENT 'Texto del cuerpo de la plantilla con variables {{1}}, {{2}}, etc.';
|
||||||
|
|
||||||
|
-- Agregar columna para almacenar componentes completos en formato JSON
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS components JSON DEFAULT NULL
|
||||||
|
COMMENT 'Componentes completos de la plantilla desde WhatsApp API (header, body, footer, buttons)';
|
||||||
|
|
||||||
|
-- Agregar columna para almacenar ejemplo de parámetros
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS example_parameters JSON DEFAULT NULL
|
||||||
|
COMMENT 'Ejemplos de valores para los parámetros de la plantilla';
|
||||||
|
|
||||||
|
-- Agregar columna para el texto del header si existe
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS header_text VARCHAR(255) DEFAULT NULL
|
||||||
|
COMMENT 'Texto del header de la plantilla';
|
||||||
|
|
||||||
|
-- Agregar columna para el tipo de header
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS header_type ENUM('text', 'image', 'video', 'document') DEFAULT NULL
|
||||||
|
COMMENT 'Tipo de header de la plantilla';
|
||||||
|
|
||||||
|
-- Agregar columna para el texto del footer
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS footer_text VARCHAR(255) DEFAULT NULL
|
||||||
|
COMMENT 'Texto del footer de la plantilla';
|
||||||
|
|
||||||
|
-- Agregar índice para búsquedas por plantilla y lenguaje
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD INDEX IF NOT EXISTS idx_template_language (template_name, language_code);
|
||||||
|
|
||||||
|
-- Actualizar timestamp
|
||||||
|
ALTER TABLE message_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRACIÓN: 03_create_scheduled_messages
|
||||||
|
-- Fecha: 2026-02-03
|
||||||
|
-- Descripción: Crear tabla y vista para sistema de mensajes programados/recordatorios
|
||||||
|
-- Permite programar envío de plantillas WhatsApp en fechas/horas específicas
|
||||||
|
-- Nota: Usa IF NOT EXISTS / OR REPLACE - Es seguro ejecutar múltiples veces
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Tabla para mensajes programados y recordatorios
|
||||||
|
CREATE TABLE IF NOT EXISTS scheduled_messages (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
template_id INT NULL,
|
||||||
|
template_name VARCHAR(100) NULL,
|
||||||
|
template_language VARCHAR(10) DEFAULT 'es',
|
||||||
|
template_parameters JSON NULL COMMENT 'Parámetros para las variables de la plantilla',
|
||||||
|
message_type ENUM('text', 'template') DEFAULT 'template',
|
||||||
|
message_content TEXT NULL COMMENT 'Contenido del mensaje si es tipo text',
|
||||||
|
scheduled_date DATE NOT NULL,
|
||||||
|
scheduled_time TIME NOT NULL,
|
||||||
|
status ENUM('pending', 'sent', 'failed', 'cancelled') DEFAULT 'pending',
|
||||||
|
sent_at DATETIME NULL,
|
||||||
|
error_message TEXT NULL,
|
||||||
|
created_by INT NULL COMMENT 'ID del admin que creó el recordatorio',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_scheduled_date (scheduled_date),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_scheduled_datetime (scheduled_date, scheduled_time, status),
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Vista para facilitar consultas con información del usuario
|
||||||
|
CREATE OR REPLACE VIEW scheduled_messages_view AS
|
||||||
|
SELECT
|
||||||
|
sm.*,
|
||||||
|
u.name as user_name,
|
||||||
|
u.phone_number,
|
||||||
|
mt.name as template_display_name,
|
||||||
|
mt.body_text as template_body,
|
||||||
|
mt.status as template_status
|
||||||
|
FROM scheduled_messages sm
|
||||||
|
LEFT JOIN users u ON sm.user_id = u.id
|
||||||
|
LEFT JOIN message_templates mt ON sm.template_id = mt.id
|
||||||
|
ORDER BY sm.scheduled_date DESC, sm.scheduled_time DESC;
|
||||||
+1
-1
@@ -218,7 +218,7 @@ deploy() {
|
|||||||
log_info "Construyendo imágenes Docker..."
|
log_info "Construyendo imágenes Docker..."
|
||||||
docker compose -f "$COMPOSE_FILE" build --no-cache
|
docker compose -f "$COMPOSE_FILE" build --no-cache
|
||||||
|
|
||||||
# Detener contenedores existentes
|
# Detener contenedores existentes (NO borra volúmenes, los datos persisten)
|
||||||
log_info "Deteniendo contenedores anteriores..."
|
log_info "Deteniendo contenedores anteriores..."
|
||||||
docker compose -f "$COMPOSE_FILE" down --remove-orphans 2>/dev/null || true
|
docker compose -f "$COMPOSE_FILE" down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script de diagnóstico para errores de fetch
|
|
||||||
# Fecha: 27 de enero de 2026
|
|
||||||
|
|
||||||
echo "🔍 Diagnóstico de errores 'Failed to fetch'"
|
|
||||||
echo "============================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
BASE_URL="http://localhost:8000"
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
echo "📍 Base URL: $BASE_URL"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 1: Verificar que los endpoints existan
|
|
||||||
echo "1️⃣ Verificando endpoints..."
|
|
||||||
|
|
||||||
if [ -f "api/get_conversations.php" ]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} api/get_conversations.php existe"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} api/get_conversations.php NO existe"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "api/get_notifications.php" ]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} api/get_notifications.php existe"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} api/get_notifications.php NO existe"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 2: Verificar sintaxis PHP
|
|
||||||
echo "2️⃣ Verificando sintaxis PHP..."
|
|
||||||
if php -l api/get_conversations.php > /dev/null 2>&1; then
|
|
||||||
echo -e " ${GREEN}✓${NC} get_conversations.php sintaxis correcta"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} get_conversations.php tiene errores de sintaxis"
|
|
||||||
php -l api/get_conversations.php
|
|
||||||
fi
|
|
||||||
|
|
||||||
if php -l api/get_notifications.php > /dev/null 2>&1; then
|
|
||||||
echo -e " ${GREEN}✓${NC} get_notifications.php sintaxis correcta"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} get_notifications.php tiene errores de sintaxis"
|
|
||||||
php -l api/get_notifications.php
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 3: Verificar servidor PHP
|
|
||||||
echo "3️⃣ Verificando servidor PHP..."
|
|
||||||
if curl -s "$BASE_URL" > /dev/null 2>&1; then
|
|
||||||
echo -e " ${GREEN}✓${NC} Servidor PHP respondiendo en $BASE_URL"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Servidor PHP NO responde en $BASE_URL"
|
|
||||||
echo " Inicia el servidor con: php -S localhost:8000"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 4: Verificar que config.php exista
|
|
||||||
echo "4️⃣ Verificando configuración..."
|
|
||||||
if [ -f "config/config.php" ]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} config/config.php existe"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} config/config.php NO existe"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 5: Intentar acceder a los endpoints (sin autenticación)
|
|
||||||
echo "5️⃣ Probando acceso a endpoints..."
|
|
||||||
|
|
||||||
RESP=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/get_conversations.php")
|
|
||||||
if [ "$RESP" = "200" ]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} get_conversations.php responde 200"
|
|
||||||
elif [ "$RESP" = "401" ]; then
|
|
||||||
echo -e " ${YELLOW}⚠${NC} get_conversations.php requiere autenticación (401)"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} get_conversations.php responde $RESP"
|
|
||||||
fi
|
|
||||||
|
|
||||||
RESP=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/get_notifications.php")
|
|
||||||
if [ "$RESP" = "200" ]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} get_notifications.php responde 200"
|
|
||||||
elif [ "$RESP" = "401" ]; then
|
|
||||||
echo -e " ${YELLOW}⚠${NC} get_notifications.php requiere autenticación (401)"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} get_notifications.php responde $RESP"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 6: Verificar logs de error
|
|
||||||
echo "6️⃣ Revisando logs de error recientes..."
|
|
||||||
if [ -f "/tmp/php_errors.log" ]; then
|
|
||||||
echo " Últimas 5 líneas del log:"
|
|
||||||
tail -5 /tmp/php_errors.log 2>/dev/null || echo " No se pudieron leer los logs"
|
|
||||||
else
|
|
||||||
echo " No se encontró archivo de log en /tmp/php_errors.log"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "============================================"
|
|
||||||
echo "✅ Diagnóstico completo"
|
|
||||||
echo ""
|
|
||||||
echo "📌 Soluciones comunes:"
|
|
||||||
echo " 1. Si los endpoints requieren autenticación (401):"
|
|
||||||
echo " - Asegúrate de estar logueado en conversations.php"
|
|
||||||
echo " - Verifica que las cookies de sesión se envíen"
|
|
||||||
echo ""
|
|
||||||
echo " 2. Si el servidor no responde:"
|
|
||||||
echo " - Inicia: cd /Users/lizandro/Documents/GitHub/whatsapp && php -S localhost:8000"
|
|
||||||
echo ""
|
|
||||||
echo " 3. Si hay errores de sintaxis:"
|
|
||||||
echo " - Revisa el archivo indicado y corrige los errores"
|
|
||||||
echo ""
|
|
||||||
echo " 4. Para ver errores en tiempo real:"
|
|
||||||
echo " - tail -f /tmp/php_errors.log"
|
|
||||||
echo ""
|
|
||||||
@@ -58,6 +58,9 @@ services:
|
|||||||
- APP_ENV=development
|
- APP_ENV=development
|
||||||
- APP_DEBUG=true
|
- APP_DEBUG=true
|
||||||
|
|
||||||
|
# URL pública de la aplicación (para generar URLs absolutas en archivos multimedia)
|
||||||
|
- APP_URL=${APP_URL:-https://bot.u-s.app/}
|
||||||
|
|
||||||
# Base de datos - apuntar a MySQL local
|
# Base de datos - apuntar a MySQL local
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_PORT=${DB_PORT:-3306}
|
- DB_PORT=${DB_PORT:-3306}
|
||||||
@@ -224,6 +227,39 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- whatsapp-dev-network
|
- whatsapp-dev-network
|
||||||
|
|
||||||
|
# Contenedor de mantenimiento automatizado
|
||||||
|
maintenance:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.maintenance
|
||||||
|
container_name: whatsapp-dev-maintenance
|
||||||
|
restart: unless-stopped
|
||||||
|
cap_add:
|
||||||
|
- SYS_NICE
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
- ./logs:/var/www/html/logs
|
||||||
|
- ./backups:/backups
|
||||||
|
- maintenance-cron-logs:/var/log/cron
|
||||||
|
environment:
|
||||||
|
- TZ=America/Bogota
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=${DB_NAME:-usite_whatsapp_bot}
|
||||||
|
- DB_USER=${DB_USER:-usite_whatsapp_user}
|
||||||
|
- DB_PASS=${DB_PASS:-6q!Vio0fn@PFmsLyHjH5}
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- APP_URL=http://app/health.php
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- whatsapp-dev-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-dev-data:
|
mysql-dev-data:
|
||||||
driver: local
|
driver: local
|
||||||
@@ -233,6 +269,8 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
php-socket:
|
php-socket:
|
||||||
driver: local
|
driver: local
|
||||||
|
maintenance-cron-logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
whatsapp-dev-network:
|
whatsapp-dev-network:
|
||||||
|
|||||||
@@ -145,6 +145,41 @@ services:
|
|||||||
- whatsapp-network
|
- whatsapp-network
|
||||||
command: php worker.php --daemon
|
command: php worker.php --daemon
|
||||||
|
|
||||||
|
# Mantenimiento automatizado - PRODUCCIÓN
|
||||||
|
maintenance:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.maintenance
|
||||||
|
container_name: whatsapp-maintenance-prod
|
||||||
|
restart: always
|
||||||
|
cap_add:
|
||||||
|
- SYS_NICE
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
- ./logs:/var/www/html/logs
|
||||||
|
- ./backups:/backups
|
||||||
|
- maintenance-cron-logs:/var/log/cron
|
||||||
|
environment:
|
||||||
|
- TZ=America/Bogota
|
||||||
|
- APP_ENV=production
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_NAME=${DB_NAME:-usite_whatsapp_bot}
|
||||||
|
- DB_USER=${DB_USER:-usite_whatsapp_user}
|
||||||
|
- DB_PASS=${DB_PASS:-6q!Vio0fn@PFmsLyHjH5}
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- APP_URL=http://app
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- whatsapp-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
driver: local
|
driver: local
|
||||||
@@ -152,6 +187,8 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
ssl-certs:
|
ssl-certs:
|
||||||
driver: local
|
driver: local
|
||||||
|
maintenance-cron-logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
whatsapp-network:
|
whatsapp-network:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Crontab para tareas de mantenimiento
|
||||||
|
# Formato: minute hour day month weekday command
|
||||||
|
|
||||||
|
# Backup diario de base de datos a las 2:00 AM
|
||||||
|
0 2 * * * /scripts/backup-database.sh >> /var/log/cron/backup.log 2>&1
|
||||||
|
|
||||||
|
# Limpieza de logs a las 3:00 AM
|
||||||
|
0 3 * * * /scripts/clean-logs.sh >> /var/log/cron/clean.log 2>&1
|
||||||
|
|
||||||
|
# Verificación de salud cada hora
|
||||||
|
0 * * * * /scripts/health-check.sh >> /var/log/cron/health.log 2>&1
|
||||||
|
|
||||||
|
# Envío de mensajes programados cada minuto
|
||||||
|
* * * * * cd /var/www/html && php send_scheduled_messages.php >> /var/log/cron/scheduled.log 2>&1
|
||||||
|
|
||||||
|
# Limpiar logs de cron cada semana (domingo a las 4:00 AM)
|
||||||
|
0 4 * * 0 find /var/log/cron -name "*.log" -mtime +7 -delete
|
||||||
|
|
||||||
|
# Log de inicio de cron
|
||||||
|
@reboot echo "[$(date)] Cron iniciado" >> /var/log/cron/system.log
|
||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Entrypoint para contenedor de mantenimiento
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "WhatsApp Bot - Contenedor de Mantenimiento"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Fecha: $(date)"
|
||||||
|
echo "Zona Horaria: $TZ"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar conexión a base de datos
|
||||||
|
echo "Verificando conexión a base de datos..."
|
||||||
|
if mariadb-admin ping -h "${DB_HOST}" -u "${DB_USER}" --password="${DB_PASS}" --silent; then
|
||||||
|
echo "✅ Conexión a base de datos OK"
|
||||||
|
else
|
||||||
|
echo "❌ No se pudo conectar a la base de datos"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar Redis
|
||||||
|
echo "Verificando conexión a Redis..."
|
||||||
|
if echo "PING" | nc -w 2 "${REDIS_HOST}" 6379 | grep -q PONG; then
|
||||||
|
echo "✅ Conexión a Redis OK"
|
||||||
|
else
|
||||||
|
echo "⚠️ Redis no disponible (no crítico)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Crear directorios necesarios
|
||||||
|
mkdir -p /backups/database
|
||||||
|
mkdir -p /var/log/cron
|
||||||
|
|
||||||
|
# Ejecutar migraciones de base de datos
|
||||||
|
echo ""
|
||||||
|
echo "Ejecutando migraciones de base de datos..."
|
||||||
|
/scripts/run-migrations.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Tareas programadas:"
|
||||||
|
echo "- 02:00 AM: Backup de base de datos"
|
||||||
|
echo "- 03:00 AM: Limpieza de logs"
|
||||||
|
echo "- Cada hora: Verificación de salud"
|
||||||
|
echo "- Cada minuto: Envío de mensajes programados"
|
||||||
|
echo ""
|
||||||
|
echo "Logs disponibles en /var/log/cron/"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Iniciar cron en segundo plano y seguir logs
|
||||||
|
crond -b -d 8
|
||||||
|
touch /var/log/cron/scheduled.log
|
||||||
|
tail -f /var/log/cron/*.log
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script de backup diario de base de datos
|
||||||
|
# Ejecuta a las 2:00 AM
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Iniciando backup de base de datos..."
|
||||||
|
|
||||||
|
# Variables de entorno
|
||||||
|
DB_HOST=${DB_HOST:-mysql}
|
||||||
|
DB_NAME=${DB_NAME:-usite_whatsapp_bot}
|
||||||
|
DB_USER=${DB_USER:-usite_whatsapp_user}
|
||||||
|
DB_PASS=${DB_PASS}
|
||||||
|
|
||||||
|
# Directorio de backups
|
||||||
|
BACKUP_DIR="/backups/database"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# Nombre del archivo con timestamp
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql.gz"
|
||||||
|
|
||||||
|
# Ejecutar backup comprimido
|
||||||
|
if mariadb-dump \
|
||||||
|
--host="$DB_HOST" \
|
||||||
|
--user="$DB_USER" \
|
||||||
|
--password="$DB_PASS" \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--events \
|
||||||
|
"$DB_NAME" | gzip > "$BACKUP_FILE"; then
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Backup creado exitosamente: $BACKUP_FILE"
|
||||||
|
|
||||||
|
# Obtener tamaño del backup
|
||||||
|
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tamaño del backup: $SIZE"
|
||||||
|
|
||||||
|
# Eliminar backups antiguos (más de 7 días)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Limpiando backups antiguos..."
|
||||||
|
find "$BACKUP_DIR" -name "backup_*.sql.gz" -type f -mtime +7 -delete
|
||||||
|
|
||||||
|
# Contar backups restantes
|
||||||
|
BACKUP_COUNT=$(find "$BACKUP_DIR" -name "backup_*.sql.gz" -type f | wc -l)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backups disponibles: $BACKUP_COUNT"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ Error al crear backup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completado"
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script de limpieza de logs
|
||||||
|
# Ejecuta a las 3:00 AM
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Iniciando limpieza de logs..."
|
||||||
|
|
||||||
|
# Directorio de logs
|
||||||
|
LOGS_DIR="/var/www/html/logs"
|
||||||
|
|
||||||
|
if [ ! -d "$LOGS_DIR" ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ Directorio de logs no encontrado: $LOGS_DIR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Contar archivos antes de limpiar
|
||||||
|
BEFORE_COUNT=$(find "$LOGS_DIR" -name "*.log" -type f | wc -l)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Archivos de log encontrados: $BEFORE_COUNT"
|
||||||
|
|
||||||
|
# Eliminar logs más antiguos de 30 días
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Eliminando logs antiguos (>30 días)..."
|
||||||
|
find "$LOGS_DIR" -name "*.log" -type f -mtime +30 -delete
|
||||||
|
|
||||||
|
# Comprimir logs entre 7-30 días
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Comprimiendo logs antiguos (7-30 días)..."
|
||||||
|
find "$LOGS_DIR" -name "*.log" -type f -mtime +7 -mtime -30 -exec gzip {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
# Limpiar logs .gz más antiguos de 30 días
|
||||||
|
find "$LOGS_DIR" -name "*.log.gz" -type f -mtime +30 -delete
|
||||||
|
|
||||||
|
# Contar archivos después de limpiar
|
||||||
|
AFTER_COUNT=$(find "$LOGS_DIR" -name "*.log" -o -name "*.log.gz" -type f | wc -l)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Archivos de log restantes: $AFTER_COUNT"
|
||||||
|
|
||||||
|
# Calcular espacio liberado
|
||||||
|
SPACE_USED=$(du -sh "$LOGS_DIR" | cut -f1)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Espacio usado por logs: $SPACE_USED"
|
||||||
|
|
||||||
|
# Truncar logs muy grandes (>100MB)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Truncando logs muy grandes..."
|
||||||
|
find "$LOGS_DIR" -name "*.log" -type f -size +100M -exec sh -c 'echo "Log truncado el $(date)" > "$1"' _ {} \;
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Limpieza de logs completada"
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script de verificación de salud del sistema
|
||||||
|
# Ejecuta cada hora
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando salud del sistema..."
|
||||||
|
|
||||||
|
# URLs a verificar
|
||||||
|
APP_URL=${APP_URL:-http://app/health.php}
|
||||||
|
REDIS_HOST=${REDIS_HOST:-redis}
|
||||||
|
|
||||||
|
# 1. Verificar aplicación web
|
||||||
|
echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando aplicación web... "
|
||||||
|
if curl -s -f "$APP_URL" > /dev/null 2>&1; then
|
||||||
|
echo "✅ OK"
|
||||||
|
else
|
||||||
|
echo "❌ FALLO"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ La aplicación web no responde"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Verificar base de datos
|
||||||
|
echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando base de datos... "
|
||||||
|
if mariadb-admin ping -h "${DB_HOST:-mysql}" -u "${DB_USER}" --password="${DB_PASS}" --silent > /dev/null 2>&1; then
|
||||||
|
echo "✅ OK"
|
||||||
|
else
|
||||||
|
echo "❌ FALLO"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ La base de datos no responde"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Verificar Redis
|
||||||
|
echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando Redis... "
|
||||||
|
if echo "PING" | nc -w 2 "$REDIS_HOST" 6379 | grep -q PONG 2>/dev/null; then
|
||||||
|
echo "✅ OK"
|
||||||
|
else
|
||||||
|
echo "❌ FALLO"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ Redis no responde"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Verificar espacio en disco
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando espacio en disco..."
|
||||||
|
df -h / | tail -1 | awk '{print "[" strftime("%Y-%m-%d %H:%M:%S") "] Espacio usado: " $5 " (" $3 " de " $2 ")"}'
|
||||||
|
|
||||||
|
# Advertencia si el espacio usado es mayor al 85%
|
||||||
|
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||||
|
if [ "$DISK_USAGE" -gt 85 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ ADVERTENCIA: Espacio en disco mayor al 85%"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Verificar memoria
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Uso de memoria:"
|
||||||
|
free -h | grep -E "Mem|Swap" || echo "No disponible en este contenedor"
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificación completada"
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script para ejecutar migraciones de base de datos
|
||||||
|
# Se ejecuta automáticamente al iniciar el contenedor de mantenimiento
|
||||||
|
|
||||||
|
LOG_FILE="/var/log/cron/migrations.log"
|
||||||
|
MIGRATIONS_DIR="/var/www/html/database"
|
||||||
|
MIGRATIONS_APPLIED="/var/www/html/logs/.migrations_applied"
|
||||||
|
|
||||||
|
echo "========================================" | tee -a "$LOG_FILE"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando migraciones..." | tee -a "$LOG_FILE"
|
||||||
|
echo "========================================" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
# Crear archivo de control si no existe
|
||||||
|
touch "$MIGRATIONS_APPLIED"
|
||||||
|
|
||||||
|
# Verificar conexión a BD
|
||||||
|
if ! mariadb-admin ping -h "${DB_HOST}" -u "${DB_USER}" --password="${DB_PASS}" --silent 2>/dev/null; then
|
||||||
|
echo "[ERROR] No se pudo conectar a la base de datos" | tee -a "$LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ejecutar cada archivo .sql en orden alfabético
|
||||||
|
for migration_file in $(ls -1 "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort); do
|
||||||
|
filename=$(basename "$migration_file")
|
||||||
|
|
||||||
|
# Verificar si ya fue aplicada
|
||||||
|
if grep -q "^${filename}$" "$MIGRATIONS_APPLIED" 2>/dev/null; then
|
||||||
|
echo "[SKIP] $filename - Ya aplicada" | tee -a "$LOG_FILE"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[RUN] Ejecutando migración: $filename" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
# Ejecutar la migración
|
||||||
|
if mysql -h "${DB_HOST}" -u "${DB_USER}" --password="${DB_PASS}" "${DB_NAME}" < "$migration_file" 2>&1 | tee -a "$LOG_FILE"; then
|
||||||
|
echo "[OK] $filename - Aplicada correctamente" | tee -a "$LOG_FILE"
|
||||||
|
# Marcar como aplicada
|
||||||
|
echo "$filename" >> "$MIGRATIONS_APPLIED"
|
||||||
|
else
|
||||||
|
echo "[ERROR] $filename - Falló la migración" | tee -a "$LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "========================================" | tee -a "$LOG_FILE"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Migraciones completadas" | tee -a "$LOG_FILE"
|
||||||
|
echo "========================================" | tee -a "$LOG_FILE"
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Descripción del Uso del Permiso whatsapp_business_management
|
|
||||||
|
|
||||||
## WhatsApp Bot Manager by U-Site.app
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Descripción detallada del uso del permiso:**
|
|
||||||
|
|
||||||
Nuestra aplicación "WhatsApp Bot Manager" utiliza el permiso whatsapp_business_management para proporcionar una plataforma completa de administración y gestión de activos comerciales de WhatsApp Business. La aplicación permite a las empresas configurar, administrar y optimizar todos sus recursos de WhatsApp Business desde un panel centralizado, facilitando la gestión operativa y el análisis de rendimiento de sus comunicaciones empresariales.
|
|
||||||
|
|
||||||
**Uso específico del permiso:**
|
|
||||||
|
|
||||||
1. **Administración de Cuentas WhatsApp Business:** La app gestiona la configuración y administración de múltiples cuentas de WhatsApp Business, permitiendo a empresas con varios puntos de contacto centralizar su gestión desde una sola plataforma.
|
|
||||||
|
|
||||||
2. **Gestión de Números de Teléfono:** Administra la configuración, verificación y asignación de números de teléfono comerciales, incluyendo la configuración de perfiles comerciales, horarios de atención y información de contacto empresarial.
|
|
||||||
|
|
||||||
3. **Administración de Plantillas de Mensajes:** Crea, edita, organiza y gestiona plantillas de mensajes pre-aprobadas por WhatsApp, permitiendo a las empresas mantener comunicaciones consistentes y cumplir con las políticas de mensajería masiva.
|
|
||||||
|
|
||||||
4. **Configuración de Webhooks:** Gestiona las suscripciones a webhooks para recibir notificaciones en tiempo real de mensajes entrantes, cambios de estado de mensajes y eventos importantes, asegurando comunicación bidireccional efectiva.
|
|
||||||
|
|
||||||
5. **Gestión de Códigos QR Comerciales:** Genera, personaliza y administra códigos QR para que los clientes puedan iniciar conversaciones comerciales fácilmente, facilitando la captación de nuevos contactos y leads.
|
|
||||||
|
|
||||||
6. **Panel de Análisis y Estadísticas:** Recopila y presenta métricas agregadas y anónimas sobre el rendimiento de las comunicaciones empresariales, incluyendo tasas de respuesta, horarios de mayor actividad y efectividad de plantillas.
|
|
||||||
|
|
||||||
**Valor para los usuarios finales:**
|
|
||||||
|
|
||||||
- **Gestión Centralizada:** Las empresas pueden administrar todos sus activos de WhatsApp Business desde un solo panel, eliminando la necesidad de gestionar múltiples plataformas por separado.
|
|
||||||
- **Configuración Simplificada:** Interface intuitiva para configurar números comerciales, plantillas y webhooks sin conocimientos técnicos avanzados.
|
|
||||||
- **Optimización Basada en Datos:** Acceso a análisis detallados que ayudan a las empresas a optimizar sus estrategias de comunicación y mejorar la satisfacción del cliente.
|
|
||||||
- **Escalabilidad Empresarial:** Capacidad de gestionar múltiples números y cuentas comerciales desde una sola plataforma, ideal para empresas en crecimiento.
|
|
||||||
- **Cumplimiento Automatizado:** Gestión automática de plantillas y configuraciones que aseguran cumplimiento con las políticas de WhatsApp Business.
|
|
||||||
|
|
||||||
**Necesidad para el funcionamiento de la app:**
|
|
||||||
|
|
||||||
Este permiso es fundamental para el funcionamiento de nuestra aplicación porque:
|
|
||||||
|
|
||||||
1. **Configuración Inicial:** Sin acceso a la gestión de activos comerciales, las empresas no pueden configurar adecuadamente sus cuentas de WhatsApp Business para usar la plataforma de automatización.
|
|
||||||
|
|
||||||
2. **Administración de Plantillas:** La gestión de plantillas es esencial para enviar mensajes estructurados y cumplir con las regulaciones de WhatsApp sobre mensajes comerciales.
|
|
||||||
|
|
||||||
3. **Gestión de Webhooks:** El acceso a la configuración de webhooks es crítico para recibir mensajes entrantes y mantener conversaciones bidireccionales en tiempo real.
|
|
||||||
|
|
||||||
4. **Análisis de Rendimiento:** Los datos de análisis son necesarios para proporcionar insights valiosos a las empresas sobre la efectividad de sus comunicaciones y identificar áreas de mejora.
|
|
||||||
|
|
||||||
5. **Escalabilidad:** La capacidad de gestionar múltiples números y cuentas es esencial para empresas que operan en diferentes mercados o tienen múltiples líneas de negocio.
|
|
||||||
|
|
||||||
**Casos de uso específicos:**
|
|
||||||
|
|
||||||
- **Cadena de Restaurantes:** Gestiona números de WhatsApp para cada sucursal, plantillas de menús y promociones, y analiza el rendimiento de cada ubicación.
|
|
||||||
- **Empresa de E-commerce:** Administra múltiples números para diferentes categorías de productos, plantillas de confirmación de pedidos y seguimiento de entregas.
|
|
||||||
- **Centro Médico:** Gestiona números especializados por departamento, plantillas para recordatorios de citas y análisis de comunicación por especialidad.
|
|
||||||
- **Agencia de Marketing:** Administra cuentas de WhatsApp Business de múltiples clientes desde una plataforma centralizada.
|
|
||||||
|
|
||||||
**Beneficios de análisis agregado:**
|
|
||||||
|
|
||||||
La aplicación utiliza datos agregados y anónimos para:
|
|
||||||
- Identificar patrones de comunicación más efectivos
|
|
||||||
- Optimizar horarios de envío de mensajes
|
|
||||||
- Mejorar tasas de respuesta mediante análisis de plantillas
|
|
||||||
- Proporcionar benchmarks de industria para mejorar estrategias de comunicación
|
|
||||||
|
|
||||||
La aplicación está diseñada para ser una solución integral de gestión de WhatsApp Business, proporcionando a las empresas las herramientas necesarias para administrar eficientemente sus activos comerciales y optimizar sus comunicaciones con clientes, cumpliendo siempre con las políticas y regulaciones de WhatsApp Business.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Información de contacto:**
|
|
||||||
- Desarrollador: U-Site.app
|
|
||||||
- Email: support@u-site.app
|
|
||||||
- Sitio web: https://u-site.app
|
|
||||||
- Política de privacidad: https://tudominio.com/privacy-policy.html
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# Descripción del Uso del Permiso whatsapp_business_messaging
|
|
||||||
|
|
||||||
## WhatsApp Bot Manager by U-Site.app
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Descripción detallada del uso del permiso:**
|
|
||||||
|
|
||||||
Nuestra aplicación "WhatsApp Bot Manager" utiliza el permiso whatsapp_business_messaging para proporcionar una plataforma integral de automatización y gestión de comunicaciones empresariales a través de WhatsApp Business API. La aplicación funciona como un sistema de gestión de conversaciones que permite a las empresas automatizar respuestas, gestionar múltiples conversaciones de clientes y mejorar su atención al cliente.
|
|
||||||
|
|
||||||
**Uso específico del permiso:**
|
|
||||||
|
|
||||||
1. **Envío de Mensajes Automatizados:** La app envía mensajes de texto automáticos a clientes basados en triggers predefinidos, respuestas a preguntas frecuentes, notificaciones de servicios, confirmaciones de citas y seguimiento post-venta.
|
|
||||||
|
|
||||||
2. **Gestión de Conversaciones:** Administra y almacena historial completo de conversaciones entre empresas y clientes para proporcionar contexto continuo y seguimiento personalizado de cada interacción.
|
|
||||||
|
|
||||||
3. **Respuestas Inteligentes:** Utiliza un sistema de bot automatizado que responde instantáneamente a consultas comunes de clientes, reduciendo tiempos de respuesta y mejorando la experiencia del usuario.
|
|
||||||
|
|
||||||
4. **Notificaciones Empresariales:** Envía alertas importantes como confirmaciones de pedidos, recordatorios de citas, actualizaciones de estado de servicios y comunicaciones transaccionales.
|
|
||||||
|
|
||||||
5. **Gestión de Multimedia:** Procesa, almacena y responde con contenido multimedia (imágenes, documentos, audios) enviado por clientes para brindar soporte técnico y atención personalizada.
|
|
||||||
|
|
||||||
**Valor para los usuarios finales:**
|
|
||||||
|
|
||||||
- **Respuestas Inmediatas:** Los clientes reciben respuestas instantáneas las 24/7, mejorando significativamente la experiencia de atención al cliente.
|
|
||||||
- **Comunicación Personalizada:** Cada conversación mantiene contexto histórico, permitiendo interacciones más relevantes y personalizadas.
|
|
||||||
- **Eficiencia Operativa:** Las empresas pueden manejar múltiples conversaciones simultáneamente sin pérdida de calidad en el servicio.
|
|
||||||
- **Disponibilidad Continua:** El sistema proporciona atención automatizada fuera del horario comercial, capturando leads y resolviendo consultas básicas.
|
|
||||||
|
|
||||||
**Necesidad para el funcionamiento de la app:**
|
|
||||||
|
|
||||||
Este permiso es absolutamente esencial para el funcionamiento de nuestra aplicación porque:
|
|
||||||
|
|
||||||
1. **Funcionalidad Principal:** Sin la capacidad de enviar mensajes, la app no puede cumplir su propósito fundamental de automatización de comunicaciones empresariales.
|
|
||||||
|
|
||||||
2. **Gestión de Conversaciones:** Necesitamos acceder al historial y metadata de mensajes para mantener contexto conversacional y proporcionar respuestas relevantes.
|
|
||||||
|
|
||||||
3. **Experiencia de Usuario Continua:** La gestión de multimedia es crucial para resolver consultas que requieren intercambio de imágenes, documentos o archivos de audio.
|
|
||||||
|
|
||||||
4. **Análisis y Mejora:** Los datos agregados y anónimos nos permiten optimizar las respuestas automáticas y mejorar la precisión del bot, beneficiando directamente a todos los usuarios.
|
|
||||||
|
|
||||||
**Casos de uso específicos:**
|
|
||||||
|
|
||||||
- Empresa de delivery que envía confirmaciones automáticas de pedidos y tracking de entregas
|
|
||||||
- Clínica médica que envía recordatorios de citas y confirmaciones de disponibilidad
|
|
||||||
- Tienda online que responde consultas sobre productos y procesa órdenes de compra
|
|
||||||
- Empresa de servicios que proporciona soporte técnico 24/7 con respuestas automatizadas
|
|
||||||
|
|
||||||
La aplicación está diseñada exclusivamente para mejorar la comunicación empresa-cliente, cumpliendo con todas las políticas de WhatsApp Business y proporcionando valor real tanto a empresas como a sus clientes finales a través de comunicaciones más eficientes y efectivas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Información de contacto:**
|
|
||||||
- Desarrollador: U-Site.app
|
|
||||||
- Email: support@u-site.app
|
|
||||||
- Sitio web: https://u-site.app
|
|
||||||
- Política de privacidad: https://tudominio.com/privacy-policy.html
|
|
||||||
Executable
+191
@@ -0,0 +1,191 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script de mantenimiento para HOST (fuera de Docker)
|
||||||
|
# Instalación: sudo cp host-maintenance-cron.sh /usr/local/bin/
|
||||||
|
# Crontab: sudo crontab -e
|
||||||
|
# 0 2 * * * /usr/local/bin/host-maintenance-cron.sh backup
|
||||||
|
# 0 3 * * * /usr/local/bin/host-maintenance-cron.sh clean-logs
|
||||||
|
# 0 * * * * /usr/local/bin/host-maintenance-cron.sh health-check
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuración
|
||||||
|
DOCKER_COMPOSE_FILE="/ruta/a/tu/proyecto/docker-compose.dev.yml"
|
||||||
|
PROJECT_DIR="/ruta/a/tu/proyecto"
|
||||||
|
BACKUP_DIR="$PROJECT_DIR/backups"
|
||||||
|
LOGS_DIR="$PROJECT_DIR/logs"
|
||||||
|
|
||||||
|
# Colores para logs
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función: Backup de base de datos
|
||||||
|
backup_database() {
|
||||||
|
log "Iniciando backup de base de datos..."
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR/database"
|
||||||
|
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/database/backup_$(date +%Y%m%d_%H%M%S).sql.gz"
|
||||||
|
|
||||||
|
# Ejecutar backup desde el contenedor MySQL
|
||||||
|
if docker compose -f "$DOCKER_COMPOSE_FILE" exec -T mysql \
|
||||||
|
mariadb-dump \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--events \
|
||||||
|
usite_whatsapp_bot | gzip > "$BACKUP_FILE"; then
|
||||||
|
|
||||||
|
log "✅ Backup creado: $BACKUP_FILE"
|
||||||
|
|
||||||
|
# Mostrar tamaño
|
||||||
|
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||||
|
log "Tamaño: $SIZE"
|
||||||
|
|
||||||
|
# Eliminar backups antiguos (>7 días)
|
||||||
|
find "$BACKUP_DIR/database" -name "backup_*.sql.gz" -mtime +7 -delete
|
||||||
|
log "Backups antiguos eliminados"
|
||||||
|
|
||||||
|
# Contar backups
|
||||||
|
COUNT=$(find "$BACKUP_DIR/database" -name "backup_*.sql.gz" | wc -l)
|
||||||
|
log "Backups disponibles: $COUNT"
|
||||||
|
|
||||||
|
else
|
||||||
|
error "Fallo al crear backup"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función: Limpieza de logs
|
||||||
|
clean_logs() {
|
||||||
|
log "Iniciando limpieza de logs..."
|
||||||
|
|
||||||
|
if [ ! -d "$LOGS_DIR" ]; then
|
||||||
|
warning "Directorio de logs no encontrado: $LOGS_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Contar archivos antes
|
||||||
|
BEFORE=$(find "$LOGS_DIR" -name "*.log" | wc -l)
|
||||||
|
log "Archivos de log: $BEFORE"
|
||||||
|
|
||||||
|
# Eliminar logs >30 días
|
||||||
|
find "$LOGS_DIR" -name "*.log" -mtime +30 -delete
|
||||||
|
|
||||||
|
# Comprimir logs 7-30 días
|
||||||
|
find "$LOGS_DIR" -name "*.log" -mtime +7 -mtime -30 -exec gzip {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
# Eliminar .gz >30 días
|
||||||
|
find "$LOGS_DIR" -name "*.log.gz" -mtime +30 -delete
|
||||||
|
|
||||||
|
# Truncar logs muy grandes
|
||||||
|
find "$LOGS_DIR" -name "*.log" -size +100M -exec sh -c 'echo "Log truncado el $(date)" > "$1"' _ {} \;
|
||||||
|
|
||||||
|
AFTER=$(find "$LOGS_DIR" -name "*.log" -o -name "*.log.gz" | wc -l)
|
||||||
|
SPACE=$(du -sh "$LOGS_DIR" | cut -f1)
|
||||||
|
|
||||||
|
log "Archivos restantes: $AFTER"
|
||||||
|
log "Espacio usado: $SPACE"
|
||||||
|
log "✅ Limpieza completada"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función: Health Check
|
||||||
|
health_check() {
|
||||||
|
log "Verificando salud del sistema..."
|
||||||
|
|
||||||
|
# Verificar contenedores
|
||||||
|
CONTAINERS=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps --services --filter "status=running" | wc -l)
|
||||||
|
EXPECTED=7 # Ajustar según tus servicios
|
||||||
|
|
||||||
|
if [ "$CONTAINERS" -ge "$EXPECTED" ]; then
|
||||||
|
log "✅ Contenedores: $CONTAINERS activos"
|
||||||
|
else
|
||||||
|
warning "Solo $CONTAINERS/$EXPECTED contenedores activos"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar espacio en disco
|
||||||
|
DISK_USAGE=$(df -h "$PROJECT_DIR" | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||||
|
DISK_SPACE=$(df -h "$PROJECT_DIR" | tail -1 | awk '{print $4}')
|
||||||
|
|
||||||
|
log "Espacio en disco: $DISK_USAGE% usado, $DISK_SPACE disponible"
|
||||||
|
|
||||||
|
if [ "$DISK_USAGE" -gt 85 ]; then
|
||||||
|
warning "⚠️ Espacio en disco mayor al 85%"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar logs de Docker
|
||||||
|
ERROR_COUNT=$(docker compose -f "$DOCKER_COMPOSE_FILE" logs --tail=100 2>&1 | grep -i "error" | wc -l)
|
||||||
|
|
||||||
|
if [ "$ERROR_COUNT" -gt 0 ]; then
|
||||||
|
warning "Se encontraron $ERROR_COUNT errores en logs recientes"
|
||||||
|
else
|
||||||
|
log "✅ No hay errores recientes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Verificación completada"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función: Restart services si es necesario
|
||||||
|
auto_restart() {
|
||||||
|
log "Verificando servicios para auto-restart..."
|
||||||
|
|
||||||
|
# Verificar contenedores con reintentos excesivos
|
||||||
|
UNHEALTHY=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps --format json | \
|
||||||
|
jq -r 'select(.Health == "unhealthy") | .Service' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$UNHEALTHY" ]; then
|
||||||
|
warning "Contenedores no saludables: $UNHEALTHY"
|
||||||
|
log "Reiniciando servicios no saludables..."
|
||||||
|
docker compose -f "$DOCKER_COMPOSE_FILE" restart $UNHEALTHY
|
||||||
|
log "✅ Servicios reiniciados"
|
||||||
|
else
|
||||||
|
log "✅ Todos los servicios saludables"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Menú principal
|
||||||
|
case "$1" in
|
||||||
|
backup)
|
||||||
|
backup_database
|
||||||
|
;;
|
||||||
|
clean-logs)
|
||||||
|
clean_logs
|
||||||
|
;;
|
||||||
|
health-check)
|
||||||
|
health_check
|
||||||
|
;;
|
||||||
|
auto-restart)
|
||||||
|
auto_restart
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
backup_database
|
||||||
|
clean_logs
|
||||||
|
health_check
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Uso: $0 {backup|clean-logs|health-check|auto-restart|all}"
|
||||||
|
echo ""
|
||||||
|
echo "Comandos:"
|
||||||
|
echo " backup - Backup de base de datos"
|
||||||
|
echo " clean-logs - Limpieza de logs"
|
||||||
|
echo " health-check - Verificación de salud"
|
||||||
|
echo " auto-restart - Reiniciar servicios no saludables"
|
||||||
|
echo " all - Ejecutar todas las tareas"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -30,8 +30,10 @@ try {
|
|||||||
<title>WhatsApp Bot Manager</title>
|
<title>WhatsApp Bot Manager</title>
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||||
<link href="./assets/css/styles.css?v=11" rel="stylesheet">
|
<link href="./assets/css/styles.css?v=11" rel="stylesheet">
|
||||||
<link href="./assets/css/diagnostic.css?v=10" rel="stylesheet">
|
<link href="./assets/css/diagnostic.css?v=11" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -45,7 +47,10 @@ try {
|
|||||||
<li><a href="#conversations" class="nav-link" data-tab="conversations"><i class="fas fa-comments"></i> Conversaciones <span id="conversations-count" class="sidebar-badge badge bg-danger" style="display:none; font-size:0.72rem; padding:0.15rem 0.4rem;">0</span></a></li>
|
<li><a href="#conversations" class="nav-link" data-tab="conversations"><i class="fas fa-comments"></i> Conversaciones <span id="conversations-count" class="sidebar-badge badge bg-danger" style="display:none; font-size:0.72rem; padding:0.15rem 0.4rem;">0</span></a></li>
|
||||||
<li><a href="#users" class="nav-link" data-tab="users"><i class="fas fa-users"></i> Usuarios</a></li>
|
<li><a href="#users" class="nav-link" data-tab="users"><i class="fas fa-users"></i> Usuarios</a></li>
|
||||||
<li><a href="#menus" class="nav-link" data-tab="menus"><i class="fas fa-list"></i> Menús</a></li>
|
<li><a href="#menus" class="nav-link" data-tab="menus"><i class="fas fa-list"></i> Menús</a></li>
|
||||||
|
<li><a href="#admin_users" class="nav-link" data-tab="admin_users"><i class="fas fa-user-shield"></i> Usuarios del Sistema</a></li>
|
||||||
<li><a href="#send_message" class="nav-link" data-tab="send_message"><i class="fas fa-paper-plane"></i> Enviar Mensaje</a></li>
|
<li><a href="#send_message" class="nav-link" data-tab="send_message"><i class="fas fa-paper-plane"></i> Enviar Mensaje</a></li>
|
||||||
|
<li><a href="#broadcast" class="nav-link" data-tab="broadcast"><i class="fas fa-broadcast-tower"></i> Envío Masivo <span class="badge bg-success ms-1">NUEVO</span></a></li>
|
||||||
|
<li><a href="scheduled_messages.php" class="nav-link"><i class="fas fa-clock"></i> Mensajes Programados</a></li>
|
||||||
<li><a href="#templates" class="nav-link" data-tab="templates"><i class="fas fa-file-text"></i> Plantillas</a></li>
|
<li><a href="#templates" class="nav-link" data-tab="templates"><i class="fas fa-file-text"></i> Plantillas</a></li>
|
||||||
<li><a href="#autoresponses" class="nav-link" data-tab="autoresponses"><i class="fas fa-robot"></i> Respuestas Auto</a></li>
|
<li><a href="#autoresponses" class="nav-link" data-tab="autoresponses"><i class="fas fa-robot"></i> Respuestas Auto</a></li>
|
||||||
<li><a href="#system_config" class="nav-link" data-tab="system_config"><i class="fas fa-cog"></i> Configuración</a></li>
|
<li><a href="#system_config" class="nav-link" data-tab="system_config"><i class="fas fa-cog"></i> Configuración</a></li>
|
||||||
@@ -236,6 +241,22 @@ try {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<!-- Búsqueda de usuarios -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="users-search"
|
||||||
|
placeholder="Buscar por teléfono, nombre o email..."
|
||||||
|
onkeyup="searchUsers()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -254,6 +275,9 @@ try {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginación -->
|
||||||
|
<div id="users-pagination"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,6 +314,46 @@ try {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Users Tab -->
|
||||||
|
<div id="admin_users" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5><i class="fas fa-user-shield"></i> Usuarios del Sistema</h5>
|
||||||
|
<button class="btn btn-primary" onclick="showCreateAdminUserModal()">
|
||||||
|
<i class="fas fa-plus"></i> Nuevo Usuario
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Gestiona los usuarios administradores que tienen acceso al sistema.
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Usuario</th>
|
||||||
|
<th>Nombre Completo</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Último Acceso</th>
|
||||||
|
<th>Fecha Creación</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="admin-users-table">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> Cargando usuarios...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- conversations Tab -->
|
<!-- conversations Tab -->
|
||||||
<div id="send_message" class="tab-content">
|
<div id="send_message" class="tab-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -336,10 +400,26 @@ try {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3" id="template-group" style="display: none;">
|
<div class="mb-3" id="template-group" style="display: none;">
|
||||||
<label class="form-label">Plantilla</label>
|
<label class="form-label">Plantilla</label>
|
||||||
<select class="form-control" id="message-template">
|
<select class="form-control" id="message-template" onchange="loadMessageTemplateDetails()">
|
||||||
<option value="">Seleccionar plantilla...</option>
|
<option value="">Seleccionar plantilla...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Variables de la plantilla -->
|
||||||
|
<div id="message-template-variables" class="mb-3" style="display: none;">
|
||||||
|
<!-- Se llenarán dinámicamente -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview de la plantilla -->
|
||||||
|
<div id="message-template-preview" class="mb-3" style="display: none;">
|
||||||
|
<label class="form-label"><i class="fas fa-eye"></i> Vista Previa</label>
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="message-preview-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-paper-plane"></i> Enviar Mensaje
|
<i class="fas fa-paper-plane"></i> Enviar Mensaje
|
||||||
</button>
|
</button>
|
||||||
@@ -348,33 +428,154 @@ try {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6" id="broadcast-section" style="display:none;">
|
</div>
|
||||||
<div class="card">
|
</div>
|
||||||
<div class="card-header">
|
|
||||||
<h5><i class="fas fa-broadcast-tower"></i> Mensaje Masivo (oculto)</h5>
|
<!-- Broadcast Tab -->
|
||||||
|
<div id="broadcast" class="tab-content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="card shadow-lg">
|
||||||
|
<div class="card-header bg-warning text-white">
|
||||||
|
<h4 class="mb-0"><i class="fas fa-broadcast-tower"></i> Envío Masivo de Mensajes</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="broadcast-form">
|
<div class="alert alert-success mb-4">
|
||||||
<div class="mb-3">
|
<i class="fas fa-check-circle"></i>
|
||||||
<label class="form-label">Filtro de Usuarios</label>
|
<strong>¡Envío Masivo Activo!</strong> Ahora puedes enviar mensajes a múltiples usuarios simultáneamente.
|
||||||
<select class="form-control" id="broadcast-filter">
|
</div>
|
||||||
<option value="all">Todos los usuarios</option>
|
|
||||||
<option value="active">Solo usuarios activos</option>
|
<form id="broadcast-form" onsubmit="return handleBroadcastSubmit(event)">
|
||||||
<option value="recent">Usuarios recientes (30 días)</option>
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-filter"></i> Tipo de Selección
|
||||||
|
</label>
|
||||||
|
<select class="form-control form-control-lg" id="broadcast-selection-type" onchange="toggleBroadcastSelectionType()">
|
||||||
|
<option value="filter">📋 Por filtro de usuarios</option>
|
||||||
|
<option value="specific">👥 Usuarios específicos</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Mensaje</label>
|
<!-- Filtro de usuarios -->
|
||||||
<textarea class="form-control" id="broadcast-message" rows="4" placeholder="Mensaje para envío masivo..."></textarea>
|
<div class="mb-4" id="broadcast-filter-container">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-filter"></i> Filtro de Usuarios
|
||||||
|
</label>
|
||||||
|
<select class="form-control form-control-lg" id="broadcast-filter">
|
||||||
|
<option value="all">📋 Todos los usuarios activos</option>
|
||||||
|
<option value="active">🔥 Solo usuarios con conversaciones recientes (7 días)</option>
|
||||||
|
<option value="recent">✨ Usuarios recientes (30 días)</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
<i class="fas fa-info-circle"></i> Selecciona qué usuarios recibirán el mensaje
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-warning">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<!-- Selección específica de usuarios -->
|
||||||
<strong>Cuidado:</strong> Este mensaje se enviará a múltiples usuarios.
|
<div class="mb-4" id="broadcast-users-container" style="display: none;">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-users"></i> Seleccionar Usuarios
|
||||||
|
</label>
|
||||||
|
<select class="form-control" id="broadcast-users" multiple="multiple">
|
||||||
|
<!-- Opciones cargadas dinámicamente -->
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
<i class="fas fa-info-circle"></i> Selecciona uno o más usuarios para enviar el mensaje
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-envelope"></i> Tipo de Mensaje
|
||||||
|
</label>
|
||||||
|
<select class="form-control form-control-lg" id="broadcast-message-type" onchange="toggleBroadcastMessageType()">
|
||||||
|
<option value="text">📝 Mensaje de texto</option>
|
||||||
|
<option value="template">📋 Plantilla de WhatsApp</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensaje de texto -->
|
||||||
|
<div class="mb-4" id="broadcast-text-container">
|
||||||
|
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-comment-dots"></i> Mensaje
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="broadcast-message"
|
||||||
|
rows="6"
|
||||||
|
placeholder="Escribe el mensaje que deseas enviar a todos los usuarios...
|
||||||
|
|
||||||
|
Ejemplo:
|
||||||
|
¡Hola! 👋 Tenemos una promoción especial para ti..."
|
||||||
|
maxlength="4096"></textarea>
|
||||||
|
<div class="d-flex justify-content-between mt-2">
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
<i class="fas fa-keyboard"></i> Máximo 4096 caracteres
|
||||||
|
</small>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
<span id="char-count">0</span> / 4096
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selección de plantilla -->
|
||||||
|
<div class="mb-4" id="broadcast-template-container" style="display: none;">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-file-text"></i> Seleccionar Plantilla
|
||||||
|
</label>
|
||||||
|
<select class="form-control form-control-lg" id="broadcast-template" onchange="loadBroadcastTemplateDetails()">
|
||||||
|
<option value="">-- Selecciona una plantilla --</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
<i class="fas fa-info-circle"></i> Solo plantillas aprobadas
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Variables de plantilla -->
|
||||||
|
<div id="broadcast-template-variables" class="mb-4" style="display: none;">
|
||||||
|
<!-- Variables dinámicas -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vista previa de plantilla -->
|
||||||
|
<div id="broadcast-template-preview" class="mb-4" style="display: none;">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-eye"></i> Vista Previa
|
||||||
|
</label>
|
||||||
|
<div class="alert alert-info" id="broadcast-preview-content">
|
||||||
|
<!-- Contenido de vista previa -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning d-flex align-items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
|
||||||
|
<div>
|
||||||
|
<strong>Importante:</strong> Este mensaje se enviará a múltiples usuarios.
|
||||||
|
<br>Verifica cuidadosamente el contenido antes de enviar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-warning btn-lg">
|
||||||
|
<i class="fas fa-broadcast-tower"></i> Enviar Mensaje Masivo
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-warning">
|
|
||||||
<i class="fas fa-broadcast-tower"></i> Envío Masivo
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="fw-bold"><i class="fas fa-lightbulb text-warning"></i> Consejos para envío masivo:</h6>
|
||||||
|
<ul class="mb-0 small">
|
||||||
|
<li>Personaliza tu mensaje para que sea relevante</li>
|
||||||
|
<li>Evita enviar mensajes muy frecuentes</li>
|
||||||
|
<li>Respeta la ventana de 24 horas de WhatsApp</li>
|
||||||
|
<li>Usa plantillas aprobadas para nuevos contactos</li>
|
||||||
|
<li>Prueba con un grupo pequeño primero</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,11 +587,24 @@ try {
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5><i class="fas fa-file-text"></i> Plantillas de Mensaje</h5>
|
<h5><i class="fas fa-file-text"></i> Plantillas de Mensaje</h5>
|
||||||
<button class="btn btn-primary" id="btn-new-template" onclick="showCreateTemplateModal()">
|
<div>
|
||||||
<i class="fas fa-plus"></i> Nueva Plantilla
|
<button class="btn btn-success" id="btn-sync-templates" onclick="syncTemplatesFromFacebook()">
|
||||||
</button>
|
<i class="fas fa-sync-alt"></i> Sincronizar desde Facebook
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<!-- Mensaje informativo sobre creación de plantillas -->
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<h6 class="alert-heading"><i class="fas fa-info-circle"></i> ¿Cómo crear nuevas plantillas?</h6>
|
||||||
|
<p class="mb-2">Las plantillas de WhatsApp deben crearse directamente en el Administrador de Meta Business.</p>
|
||||||
|
<p class="mb-3">Una vez creadas y aprobadas por Meta, puedes sincronizarlas aquí usando el botón "Sincronizar desde Facebook".</p>
|
||||||
|
<a href="https://business.facebook.com/latest/whatsapp_manager/message_templates?business_id=2144688622601603"
|
||||||
|
class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="fas fa-external-link-alt"></i> Ir al Administrador de Plantillas de Meta
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -462,6 +676,11 @@ try {
|
|||||||
<label class="form-label">Phone Number ID</label>
|
<label class="form-label">Phone Number ID</label>
|
||||||
<input type="text" class="form-control" id="phone-number-id" placeholder="ID del número de teléfono">
|
<input type="text" class="form-control" id="phone-number-id" placeholder="ID del número de teléfono">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Business Account ID (WABA ID)</label>
|
||||||
|
<input type="text" class="form-control" id="business-account-id" placeholder="ID de la cuenta de WhatsApp Business">
|
||||||
|
<small class="form-text text-muted">Necesario para sincronizar plantillas desde Facebook</small>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Webhook Verify Token</label>
|
<label class="form-label">Webhook Verify Token</label>
|
||||||
<input type="text" class="form-control" id="webhook-token" placeholder="Token de verificación del webhook">
|
<input type="text" class="form-control" id="webhook-token" placeholder="Token de verificación del webhook">
|
||||||
@@ -667,10 +886,104 @@ try {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal para crear/editar usuario administrador -->
|
||||||
|
<div class="modal fade" id="adminUserModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="adminUserModalLabel">
|
||||||
|
<i class="fas fa-user-shield"></i> <span id="admin-user-modal-title">Nuevo Usuario</span>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="admin-user-form">
|
||||||
|
<input type="hidden" id="admin-user-id">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nombre de Usuario *</label>
|
||||||
|
<input type="text" class="form-control" id="admin-username" required
|
||||||
|
placeholder="usuario123">
|
||||||
|
<small class="text-muted">Usado para iniciar sesión</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nombre Completo</label>
|
||||||
|
<input type="text" class="form-control" id="admin-fullname"
|
||||||
|
placeholder="Juan Pérez">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-control" id="admin-email"
|
||||||
|
placeholder="usuario@ejemplo.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="password-group">
|
||||||
|
<label class="form-label">Contraseña *</label>
|
||||||
|
<input type="password" class="form-control" id="admin-password"
|
||||||
|
placeholder="Mínimo 6 caracteres">
|
||||||
|
<small class="text-muted">Mínimo 6 caracteres</small>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="fas fa-times"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveAdminUser()">
|
||||||
|
<i class="fas fa-save"></i> Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal para cambiar contraseña -->
|
||||||
|
<div class="modal fade" id="changePasswordModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-key"></i> Cambiar Contraseña
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="change-password-user-id">
|
||||||
|
<p class="mb-3">Usuario: <strong id="change-password-username"></strong></p>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nueva Contraseña *</label>
|
||||||
|
<input type="password" class="form-control" id="new-password"
|
||||||
|
placeholder="Mínimo 6 caracteres" required>
|
||||||
|
<small class="text-muted">Mínimo 6 caracteres</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Confirmar Contraseña *</label>
|
||||||
|
<input type="password" class="form-control" id="confirm-password"
|
||||||
|
placeholder="Repite la contraseña" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="fas fa-times"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveNewPassword()">
|
||||||
|
<i class="fas fa-key"></i> Cambiar Contraseña
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script src="./assets/js/app_simple.js?v=14"></script>
|
<script src="./assets/js/app_simple.js?v=18"></script>
|
||||||
<script>
|
<script>
|
||||||
// Función de diagnóstico mejorada
|
// Función de diagnóstico mejorada
|
||||||
function runDiagnostic() {
|
function runDiagnostic() {
|
||||||
@@ -1192,9 +1505,310 @@ try {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ====== Funciones para Broadcast con Plantillas y Múltiples Usuarios ======
|
||||||
|
|
||||||
|
// Manejador del formulario de broadcast
|
||||||
|
async function handleBroadcastSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
console.log('Formulario de broadcast enviado');
|
||||||
|
|
||||||
|
// Validar que existe el manager
|
||||||
|
if (!window.whatsappManager || !window.whatsappManager.sendBroadcast) {
|
||||||
|
console.error('whatsappManager no disponible');
|
||||||
|
alert('Error: Sistema no inicializado. Recarga la página.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar variables de plantilla si se seleccionó plantilla
|
||||||
|
const messageType = document.getElementById('broadcast-message-type').value;
|
||||||
|
if (messageType === 'template') {
|
||||||
|
const templateId = document.getElementById('broadcast-template').value;
|
||||||
|
if (!templateId) {
|
||||||
|
alert('Por favor selecciona una plantilla');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que las variables requeridas estén llenas
|
||||||
|
const varInputs = document.querySelectorAll('.broadcast-template-var');
|
||||||
|
let hasEmpty = false;
|
||||||
|
varInputs.forEach(input => {
|
||||||
|
if (!input.value.trim()) {
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
hasEmpty = true;
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmpty) {
|
||||||
|
alert('Por favor completa todos los campos de variables de la plantilla');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llamar al método del manager
|
||||||
|
await window.whatsappManager.sendBroadcast();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeBroadcastSelect2() {
|
||||||
|
// Inicializar Select2 para usuarios con transport personalizado
|
||||||
|
$('#broadcast-users').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
placeholder: 'Buscar y seleccionar usuarios...',
|
||||||
|
allowClear: true,
|
||||||
|
ajax: {
|
||||||
|
transport: function (params, success, failure) {
|
||||||
|
const searchTerm = params.data.term || '';
|
||||||
|
const page = params.data.page || 1;
|
||||||
|
|
||||||
|
fetch(`api/get_users.php?search=${encodeURIComponent(searchTerm)}&page=${page}&limit=50`, {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
success(data);
|
||||||
|
} else {
|
||||||
|
failure();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error cargando usuarios:', error);
|
||||||
|
failure();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
delay: 250,
|
||||||
|
processResults: function (data, params) {
|
||||||
|
params.page = params.page || 1;
|
||||||
|
if (!data || !data.data) {
|
||||||
|
return { results: [] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
results: data.data.map(user => ({
|
||||||
|
id: user.id,
|
||||||
|
text: `${user.name || 'Sin nombre'} (${user.phone_number})`
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
more: data.pagination && data.pagination.hasNext
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cache: true
|
||||||
|
},
|
||||||
|
minimumInputLength: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cargar plantillas aprobadas
|
||||||
|
loadBroadcastTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBroadcastSelectionType() {
|
||||||
|
const selectionType = document.getElementById('broadcast-selection-type').value;
|
||||||
|
const filterContainer = document.getElementById('broadcast-filter-container');
|
||||||
|
const usersContainer = document.getElementById('broadcast-users-container');
|
||||||
|
|
||||||
|
if (selectionType === 'filter') {
|
||||||
|
filterContainer.style.display = 'block';
|
||||||
|
usersContainer.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
filterContainer.style.display = 'none';
|
||||||
|
usersContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBroadcastMessageType() {
|
||||||
|
const messageType = document.getElementById('broadcast-message-type').value;
|
||||||
|
const textContainer = document.getElementById('broadcast-text-container');
|
||||||
|
const templateContainer = document.getElementById('broadcast-template-container');
|
||||||
|
|
||||||
|
if (messageType === 'text') {
|
||||||
|
textContainer.style.display = 'block';
|
||||||
|
templateContainer.style.display = 'none';
|
||||||
|
document.getElementById('broadcast-template-variables').style.display = 'none';
|
||||||
|
document.getElementById('broadcast-template-preview').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
textContainer.style.display = 'none';
|
||||||
|
templateContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBroadcastTemplates() {
|
||||||
|
try {
|
||||||
|
console.log('🔵 Cargando plantillas para broadcast...');
|
||||||
|
|
||||||
|
if (!window.whatsappManager) {
|
||||||
|
console.error('❌ whatsappManager no está disponible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await window.whatsappManager.apiCall('get_templates.php');
|
||||||
|
console.log('Respuesta de get_templates:', response);
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
const select = document.getElementById('broadcast-template');
|
||||||
|
if (!select) {
|
||||||
|
console.error('Elemento broadcast-template no encontrado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.innerHTML = '<option value="">-- Selecciona una plantilla --</option>';
|
||||||
|
|
||||||
|
const approvedTemplates = response.data.filter(t =>
|
||||||
|
t.status && (t.status.toUpperCase() === 'APPROVED' || t.status.toLowerCase() === 'approved')
|
||||||
|
);
|
||||||
|
console.log('Plantillas aprobadas encontradas:', approvedTemplates.length);
|
||||||
|
|
||||||
|
approvedTemplates.forEach(template => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = template.id;
|
||||||
|
option.textContent = `${template.name} (${template.language_code || template.language || 'es'})`;
|
||||||
|
option.dataset.templateId = template.id;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (approvedTemplates.length === 0) {
|
||||||
|
console.warn('No hay plantillas aprobadas disponibles');
|
||||||
|
select.innerHTML = '<option value="">No hay plantillas aprobadas</option>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Respuesta inválida del API:', response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cargando plantillas:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBroadcastTemplateDetails() {
|
||||||
|
console.log('🔵 loadBroadcastTemplateDetails llamado');
|
||||||
|
const select = document.getElementById('broadcast-template');
|
||||||
|
const templateId = select.value;
|
||||||
|
console.log('Template ID seleccionado:', templateId);
|
||||||
|
|
||||||
|
if (!templateId) {
|
||||||
|
document.getElementById('broadcast-template-variables').style.display = 'none';
|
||||||
|
document.getElementById('broadcast-template-preview').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.whatsappManager.apiCall(`get_template_details.php?id=${templateId}`);
|
||||||
|
console.log('✅ Detalles de plantilla:', response);
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
const template = response.template || response.data;
|
||||||
|
if (!template) {
|
||||||
|
console.error('❌ No se encontró template en la respuesta');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Template data:', template);
|
||||||
|
console.log('📋 Variables:', template.variables);
|
||||||
|
|
||||||
|
const variablesContainer = document.getElementById('broadcast-template-variables');
|
||||||
|
|
||||||
|
if (template.variables && template.variables.length > 0) {
|
||||||
|
console.log(`✅ Se encontraron ${template.variables.length} variables`);
|
||||||
|
let html = '<label class="form-label fw-bold"><i class="fas fa-edit"></i> Variables de la Plantilla</label>';
|
||||||
|
|
||||||
|
template.variables.forEach((variable, index) => {
|
||||||
|
console.log(`Variable ${index}:`, variable);
|
||||||
|
const placeholder = variable.example || `Valor para Variable ${variable.index}`;
|
||||||
|
const label = variable.label || `Variable ${variable.index}`;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">${label}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control broadcast-template-var"
|
||||||
|
data-var="${variable.index}"
|
||||||
|
data-placeholder="${variable.placeholder}"
|
||||||
|
placeholder="${placeholder}"
|
||||||
|
onkeyup="updateBroadcastPreview()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📝 HTML generado para variables:', html);
|
||||||
|
variablesContainer.innerHTML = html;
|
||||||
|
variablesContainer.style.display = 'block';
|
||||||
|
console.log('✅ Variables container mostrado');
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Template sin variables');
|
||||||
|
variablesContainer.innerHTML = '<p class="text-muted small">Esta plantilla no tiene variables</p>';
|
||||||
|
variablesContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBroadcastPreview();
|
||||||
|
} else {
|
||||||
|
console.error('❌ Respuesta sin success:', response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error cargando detalles de plantilla:', error);
|
||||||
|
window.whatsappManager.showError('Error cargando plantilla');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBroadcastPreview() {
|
||||||
|
const templateId = document.getElementById('broadcast-template').value;
|
||||||
|
if (!templateId) return;
|
||||||
|
|
||||||
|
// Recolectar variables como array (por índice)
|
||||||
|
const variablesArray = [];
|
||||||
|
document.querySelectorAll('.broadcast-template-var').forEach(input => {
|
||||||
|
const index = parseInt(input.dataset.var) - 1; // Convertir a índice de array (0-based)
|
||||||
|
variablesArray[index] = input.value || `{{${input.dataset.var}}}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.whatsappManager.apiCall('preview_template.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { template_id: templateId, parameters: variablesArray }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.success && response.preview) {
|
||||||
|
const previewContainer = document.getElementById('broadcast-template-preview');
|
||||||
|
const previewContent = document.getElementById('broadcast-preview-content');
|
||||||
|
|
||||||
|
// Usar el HTML pre-formateado del servidor
|
||||||
|
previewContent.innerHTML = response.preview.html || response.preview.body.replace(/\n/g, '<br>');
|
||||||
|
previewContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
console.error('Error en preview:', response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generando vista previa:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Fin de funciones de Broadcast ======
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
try { NotificationManager.init(); } catch(e) { console.error('NotificationManager init failed', e); }
|
try { NotificationManager.init(); } catch(e) { console.error('NotificationManager init failed', e); }
|
||||||
|
|
||||||
|
// Inicializar Select2 para broadcast cuando el manager esté listo
|
||||||
|
const waitForManager = setInterval(() => {
|
||||||
|
if (window.whatsappManager) {
|
||||||
|
clearInterval(waitForManager);
|
||||||
|
console.log('Inicializando broadcast Select2...');
|
||||||
|
try {
|
||||||
|
initializeBroadcastSelect2();
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Error inicializando broadcast Select2:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Timeout de seguridad
|
||||||
|
setTimeout(() => clearInterval(waitForManager), 5000);
|
||||||
|
|
||||||
// Sidebar toggle logic for responsive/mobile
|
// Sidebar toggle logic for responsive/mobile
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||||
|
|||||||
@@ -318,9 +318,6 @@ if ($_POST && !$loginBlocked) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Footer U-Site -->
|
|
||||||
<footer style="position: fixed; bottom: 0; left: 0; right: 0; background: transparent; padding: 15px; text-align: center; font-size: 12px; color: rgba(255,255,255,0.6);">
|
|
||||||
Desarrollado con 💚 por <a href="https://u-site.app" target="_blank" style="color: #25d366; text-decoration: none; font-weight: 600;">U-Site.app</a>
|
|
||||||
</footer>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
01_backup_full.sql
|
||||||
|
02_add_template_components.sql
|
||||||
|
03_create_scheduled_messages.sql
|
||||||
+153
@@ -78,3 +78,156 @@ Stack trace:
|
|||||||
[2026-01-27 14:41:33] [INFO] Cleared advisor_requested for user 3250 after outgoing message by operator
|
[2026-01-27 14:41:33] [INFO] Cleared advisor_requested for user 3250 after outgoing message by operator
|
||||||
[2026-01-27 23:37:33] [INFO] Cleared advisor_requested for user 3277 after outgoing message by operator
|
[2026-01-27 23:37:33] [INFO] Cleared advisor_requested for user 3277 after outgoing message by operator
|
||||||
[2026-01-27 23:42:21] [INFO] Cleared advisor_requested for user 3277 after outgoing message by operator
|
[2026-01-27 23:42:21] [INFO] Cleared advisor_requested for user 3277 after outgoing message by operator
|
||||||
|
[2026-01-29 09:02:07] [INFO] Cleared advisor_requested for user 3250 after outgoing message by operator
|
||||||
|
[2026-01-29 09:05:30] [INFO] Cleared advisor_requested for user 3250 after outgoing message by operator
|
||||||
|
[2026-02-02 21:27:38] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
|
||||||
|
[2026-02-02 21:46:53] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.112.5","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-02 21:46:53] [INFO] delete_template.php start {"raw_input":{"id":14},"query":{"debug":"true"}}
|
||||||
|
[2026-02-02 21:46:53] [INFO] delete_template.php parsed ids {"ids":[14]}
|
||||||
|
[2026-02-02 21:46:53] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-02 21:46:53] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-02 21:46:58] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.112.5","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-02 21:46:58] [INFO] delete_template.php start {"raw_input":{"id":10},"query":{"debug":"true"}}
|
||||||
|
[2026-02-02 21:46:58] [INFO] delete_template.php parsed ids {"ids":[10]}
|
||||||
|
[2026-02-02 21:46:58] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-02 21:46:58] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-02 21:47:01] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 0 actualizadas, 3 sin cambios
|
||||||
|
[2026-02-03 11:07:32] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
|
||||||
|
[2026-02-03 11:07:41] [INFO] Plantillas sincronizadas desde Facebook: 6 nuevas, 1 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 11:21:31] [INFO] Mensaje programado eliminado: ID=1
|
||||||
|
[2026-02-03 12:42:05] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 14:22:57] [INFO] Mensaje programado creado: ID=2, Usuario=3277, Fecha=2026-02-03 14:23
|
||||||
|
[2026-02-03 14:29:08] [INFO] Mensaje programado creado: ID=3, Usuario=3277, Fecha=2026-02-03 14:30
|
||||||
|
[2026-02-03 14:30:22] [INFO] Mensaje programado eliminado: ID=3
|
||||||
|
[2026-02-03 14:35:06] [INFO] Mensaje programado creado: ID=4, Usuario=3277, Fecha=2026-02-03 14:36
|
||||||
|
[2026-02-03 14:38:15] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 14:39:05] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:05] [INFO] delete_template.php start {"raw_input":{"id":16},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:05] [INFO] delete_template.php parsed ids {"ids":[16]}
|
||||||
|
[2026-02-03 14:39:05] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:05] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:09] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:09] [INFO] delete_template.php start {"raw_input":{"id":13},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:09] [INFO] delete_template.php parsed ids {"ids":[13]}
|
||||||
|
[2026-02-03 14:39:09] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:09] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:12] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:12] [INFO] delete_template.php start {"raw_input":{"id":12},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:12] [INFO] delete_template.php parsed ids {"ids":[12]}
|
||||||
|
[2026-02-03 14:39:12] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:12] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:16] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:16] [INFO] delete_template.php start {"raw_input":{"id":17},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:16] [INFO] delete_template.php parsed ids {"ids":[17]}
|
||||||
|
[2026-02-03 14:39:16] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:16] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:18] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:18] [INFO] delete_template.php start {"raw_input":{"id":10},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:18] [INFO] delete_template.php parsed ids {"ids":[10]}
|
||||||
|
[2026-02-03 14:39:18] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:18] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:21] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:21] [INFO] delete_template.php start {"raw_input":{"id":18},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:21] [INFO] delete_template.php parsed ids {"ids":[18]}
|
||||||
|
[2026-02-03 14:39:21] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:21] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:39] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:39] [INFO] delete_template.php start {"raw_input":{"id":14},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:39] [INFO] delete_template.php parsed ids {"ids":[14]}
|
||||||
|
[2026-02-03 14:39:39] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:39] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:42] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:39:42] [INFO] delete_template.php start {"raw_input":{"id":15},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:39:42] [INFO] delete_template.php parsed ids {"ids":[15]}
|
||||||
|
[2026-02-03 14:39:42] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:39:42] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:39:43] [INFO] Plantillas sincronizadas desde Facebook: 7 nuevas, 0 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 14:40:15] [INFO] Mensaje programado creado: ID=5, Usuario=3277, Fecha=2026-02-04 09:00
|
||||||
|
[2026-02-03 14:51:39] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 14:51:48] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:51:48] [INFO] delete_template.php start {"raw_input":{"id":22},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:51:48] [INFO] delete_template.php parsed ids {"ids":[22]}
|
||||||
|
[2026-02-03 14:51:48] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:51:48] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:51:56] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:51:56] [INFO] delete_template.php start {"raw_input":{"id":19},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:51:56] [INFO] delete_template.php parsed ids {"ids":[19]}
|
||||||
|
[2026-02-03 14:51:56] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:51:56] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:51:59] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:51:59] [INFO] delete_template.php start {"raw_input":{"id":23},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:51:59] [INFO] delete_template.php parsed ids {"ids":[23]}
|
||||||
|
[2026-02-03 14:51:59] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:51:59] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:52:02] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:52:02] [INFO] delete_template.php start {"raw_input":{"id":24},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:52:02] [INFO] delete_template.php parsed ids {"ids":[24]}
|
||||||
|
[2026-02-03 14:52:02] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:52:02] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:52:06] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:52:06] [INFO] delete_template.php start {"raw_input":{"id":25},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:52:06] [INFO] delete_template.php parsed ids {"ids":[25]}
|
||||||
|
[2026-02-03 14:52:06] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:52:06] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:52:09] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:52:09] [INFO] delete_template.php start {"raw_input":{"id":20},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:52:09] [INFO] delete_template.php parsed ids {"ids":[20]}
|
||||||
|
[2026-02-03 14:52:09] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:52:09] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:52:11] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 14:52:11] [INFO] delete_template.php start {"raw_input":{"id":21},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 14:52:11] [INFO] delete_template.php parsed ids {"ids":[21]}
|
||||||
|
[2026-02-03 14:52:11] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 14:52:11] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 14:52:15] [INFO] Plantillas sincronizadas desde Facebook: 7 nuevas, 0 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 14:59:06] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 15:09:36] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 15:09:55] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 15:10:09] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:10:09] [INFO] delete_template.php start {"raw_input":{"id":29},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:10:09] [INFO] delete_template.php parsed ids {"ids":[29]}
|
||||||
|
[2026-02-03 15:10:09] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:10:09] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:10:12] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:10:12] [INFO] delete_template.php start {"raw_input":{"id":26},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:10:12] [INFO] delete_template.php parsed ids {"ids":[26]}
|
||||||
|
[2026-02-03 15:10:12] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:10:12] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:10:16] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:10:16] [INFO] delete_template.php start {"raw_input":{"id":30},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:10:16] [INFO] delete_template.php parsed ids {"ids":[30]}
|
||||||
|
[2026-02-03 15:10:16] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:10:16] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:10:20] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:10:20] [INFO] delete_template.php start {"raw_input":{"id":31},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:10:20] [INFO] delete_template.php parsed ids {"ids":[31]}
|
||||||
|
[2026-02-03 15:10:20] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:10:20] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:10:23] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:10:23] [INFO] delete_template.php start {"raw_input":{"id":32},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:10:23] [INFO] delete_template.php parsed ids {"ids":[32]}
|
||||||
|
[2026-02-03 15:10:23] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:10:23] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:10:27] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:10:27] [INFO] delete_template.php start {"raw_input":{"id":27},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:10:27] [INFO] delete_template.php parsed ids {"ids":[27]}
|
||||||
|
[2026-02-03 15:10:27] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:10:27] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:10:31] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:10:31] [INFO] delete_template.php start {"raw_input":{"id":28},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:10:31] [INFO] delete_template.php parsed ids {"ids":[28]}
|
||||||
|
[2026-02-03 15:10:31] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:10:31] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:10:33] [INFO] Plantillas sincronizadas desde Facebook: 7 nuevas, 0 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 15:27:49] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 15:27:51] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 15:29:55] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
|
||||||
|
[2026-02-03 15:29:55] [INFO] delete_template.php start {"raw_input":{"id":33},"query":{"debug":"true"}}
|
||||||
|
[2026-02-03 15:29:55] [INFO] delete_template.php parsed ids {"ids":[33]}
|
||||||
|
[2026-02-03 15:29:55] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
|
||||||
|
[2026-02-03 15:29:55] [ERROR] Class not found {"class":"Pdo\\Mysql"}
|
||||||
|
[2026-02-03 15:29:59] [INFO] Plantillas sincronizadas desde Facebook: 1 nuevas, 6 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 15:34:25] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
|
||||||
|
[2026-02-03 16:19:48] [INFO] Mensaje programado creado: ID=7, Usuario=3277, Fecha=2026-02-03 16:20
|
||||||
|
[2026-02-03 20:47:36] [INFO] Mensaje programado creado: ID=8, Usuario=3250, Fecha=2026-02-03 20:48
|
||||||
|
[2026-02-03 20:54:33] [INFO] Mensaje programado creado: ID=9, Usuario=3250, Fecha=2026-02-03 20:55
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Migración - Agregar columnas para edición y eliminación de mensajes
|
||||||
|
* Fecha: 28 de enero de 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once 'config/config.php';
|
||||||
|
|
||||||
|
echo "<h2>Migración: Columnas para editar/eliminar mensajes</h2>\n";
|
||||||
|
echo "<pre>\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Verificar si las columnas ya existen
|
||||||
|
$columns = [];
|
||||||
|
$result = $db->fetchAll("SHOW COLUMNS FROM conversations");
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$columns[] = $row['Field'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrationsNeeded = [];
|
||||||
|
|
||||||
|
// Columna edited_at
|
||||||
|
if (!in_array('edited_at', $columns)) {
|
||||||
|
$migrationsNeeded[] = [
|
||||||
|
'column' => 'edited_at',
|
||||||
|
'sql' => "ALTER TABLE conversations ADD COLUMN edited_at DATETIME NULL DEFAULT NULL COMMENT 'Fecha de última edición'"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
echo "✅ Columna 'edited_at' ya existe\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columna deleted_at (para soft delete)
|
||||||
|
if (!in_array('deleted_at', $columns)) {
|
||||||
|
$migrationsNeeded[] = [
|
||||||
|
'column' => 'deleted_at',
|
||||||
|
'sql' => "ALTER TABLE conversations ADD COLUMN deleted_at DATETIME NULL DEFAULT NULL COMMENT 'Fecha de eliminación (soft delete)'"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
echo "✅ Columna 'deleted_at' ya existe\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columna original_content (para guardar contenido original antes de eliminar)
|
||||||
|
if (!in_array('original_content', $columns)) {
|
||||||
|
$migrationsNeeded[] = [
|
||||||
|
'column' => 'original_content',
|
||||||
|
'sql' => "ALTER TABLE conversations ADD COLUMN original_content TEXT NULL DEFAULT NULL COMMENT 'Contenido original (antes de eliminar)'"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
echo "✅ Columna 'original_content' ya existe\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecutar migraciones pendientes
|
||||||
|
if (count($migrationsNeeded) > 0) {
|
||||||
|
echo "\n📦 Ejecutando " . count($migrationsNeeded) . " migración(es)...\n\n";
|
||||||
|
|
||||||
|
foreach ($migrationsNeeded as $migration) {
|
||||||
|
echo "➡️ Agregando columna '{$migration['column']}'...\n";
|
||||||
|
try {
|
||||||
|
$db->execute($migration['sql']);
|
||||||
|
echo " ✅ Columna '{$migration['column']}' agregada correctamente\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar índice para optimizar búsquedas de mensajes no eliminados
|
||||||
|
echo "\n➡️ Verificando índices...\n";
|
||||||
|
try {
|
||||||
|
$indexes = $db->fetchAll("SHOW INDEX FROM conversations WHERE Key_name = 'idx_deleted_at'");
|
||||||
|
if (empty($indexes)) {
|
||||||
|
$db->execute("ALTER TABLE conversations ADD INDEX idx_deleted_at (deleted_at)");
|
||||||
|
echo " ✅ Índice 'idx_deleted_at' creado\n";
|
||||||
|
} else {
|
||||||
|
echo " ✅ Índice 'idx_deleted_at' ya existe\n";
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ⚠️ Índice no pudo crearse: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n✅ Migración completada!\n";
|
||||||
|
} else {
|
||||||
|
echo "\n✅ No hay migraciones pendientes. La base de datos está actualizada.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar estructura actual de la tabla
|
||||||
|
echo "\n📋 Estructura actual de la tabla 'conversations':\n";
|
||||||
|
echo str_repeat('-', 60) . "\n";
|
||||||
|
$result = $db->fetchAll("SHOW COLUMNS FROM conversations");
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$null = $row['Null'] === 'YES' ? 'NULL' : 'NOT NULL';
|
||||||
|
$default = $row['Default'] !== null ? "DEFAULT '{$row['Default']}'" : '';
|
||||||
|
echo sprintf(" %-25s %-20s %s %s\n", $row['Field'], $row['Type'], $null, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "❌ Error fatal: " . $e->getMessage() . "\n";
|
||||||
|
echo "Stack trace: " . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "</pre>\n";
|
||||||
|
echo "<p><a href='conversations.php'>← Volver a conversaciones</a></p>\n";
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'config/config.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>📅 Mensajes Programados - 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>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheduled-message-card {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheduled-message-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending { background: #ffc107; color: #000; }
|
||||||
|
.status-sent { background: #28a745; color: #fff; }
|
||||||
|
.status-failed { background: #dc3545; color: #fff; }
|
||||||
|
.status-cancelled { background: #6c757d; color: #fff; }
|
||||||
|
|
||||||
|
.message-preview {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="text-white">
|
||||||
|
<i class="fas fa-calendar-alt"></i> Mensajes Programados
|
||||||
|
</h1>
|
||||||
|
<a href="index.php" class="btn btn-light">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Cliente</label>
|
||||||
|
<select class="form-select" id="filter-user">
|
||||||
|
<option value="">Todos los clientes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Estado</label>
|
||||||
|
<select class="form-select" id="filter-status">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="pending" selected>Pendientes</option>
|
||||||
|
<option value="sent">Enviados</option>
|
||||||
|
<option value="failed">Fallidos</option>
|
||||||
|
<option value="cancelled">Cancelados</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Desde</label>
|
||||||
|
<input type="date" class="form-control" id="filter-date-from">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Hasta</label>
|
||||||
|
<input type="date" class="form-control" id="filter-date-to">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label class="form-label"> </label>
|
||||||
|
<button class="btn btn-primary w-100" onclick="loadScheduledMessages()">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lista de mensajes -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-list"></i> Mensajes Programados
|
||||||
|
</h5>
|
||||||
|
<span class="badge bg-light text-dark" id="total-count">0 mensajes</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="messages-container">
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-3x text-muted"></i>
|
||||||
|
<p class="mt-3 text-muted">Cargando mensajes...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal para editar mensaje -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-primary text-white">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-edit"></i> Editar Mensaje Programado
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="edit-message-id">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Fecha</label>
|
||||||
|
<input type="date" class="form-control" id="edit-date">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Hora</label>
|
||||||
|
<input type="time" class="form-control" id="edit-time">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="edit-parameters-container"></div>
|
||||||
|
</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" onclick="saveEdit()">
|
||||||
|
<i class="fas fa-save"></i> Guardar Cambios
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let currentMessages = [];
|
||||||
|
|
||||||
|
// Cargar usuarios para el filtro
|
||||||
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('api/get_users.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const select = document.getElementById('filter-user');
|
||||||
|
data.data.forEach(user => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = user.id;
|
||||||
|
option.textContent = `${user.name} (${user.phone_number})`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading users:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar mensajes programados
|
||||||
|
async function loadScheduledMessages() {
|
||||||
|
const container = document.getElementById('messages-container');
|
||||||
|
container.innerHTML = '<div class="text-center py-5"><i class="fas fa-spinner fa-spin fa-3x text-muted"></i></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = document.getElementById('filter-user').value;
|
||||||
|
const status = document.getElementById('filter-status').value;
|
||||||
|
const dateFrom = document.getElementById('filter-date-from').value;
|
||||||
|
const dateTo = document.getElementById('filter-date-to').value;
|
||||||
|
|
||||||
|
let url = 'api/get_scheduled_messages.php?limit=100';
|
||||||
|
if (userId) url += `&user_id=${userId}`;
|
||||||
|
if (status) url += `&status=${status}`;
|
||||||
|
if (dateFrom) url += `&date_from=${dateFrom}`;
|
||||||
|
if (dateTo) url += `&date_to=${dateTo}`;
|
||||||
|
|
||||||
|
console.log('Fetching:', url);
|
||||||
|
const response = await fetch(url);
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Response data:', data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
currentMessages = data.data;
|
||||||
|
renderMessages(data.data);
|
||||||
|
document.getElementById('total-count').textContent = `${data.total} mensaje${data.total !== 1 ? 's' : ''}`;
|
||||||
|
} else {
|
||||||
|
container.innerHTML = `<div class="alert alert-danger">Error: ${data.error || 'Error desconocido'}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error completo:', error);
|
||||||
|
container.innerHTML = `<div class="alert alert-danger">Error de conexión: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizar mensajes
|
||||||
|
function renderMessages(messages) {
|
||||||
|
const container = document.getElementById('messages-container');
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-calendar-times fa-3x text-muted"></i>
|
||||||
|
<p class="mt-3 text-muted">No hay mensajes programados</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="row">';
|
||||||
|
|
||||||
|
messages.forEach(msg => {
|
||||||
|
const statusClass = `status-${msg.status}`;
|
||||||
|
const date = new Date(msg.scheduled_date + ' ' + msg.scheduled_time);
|
||||||
|
const dateStr = date.toLocaleDateString('es-ES', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
const timeStr = date.toLocaleTimeString('es-ES', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
let preview = '';
|
||||||
|
if (msg.message_type === 'template') {
|
||||||
|
preview = `<strong>Plantilla:</strong> ${msg.template_display_name || msg.template_name}`;
|
||||||
|
if (msg.template_body) {
|
||||||
|
let bodyPreview = msg.template_body;
|
||||||
|
if (msg.template_parameters && Array.isArray(msg.template_parameters)) {
|
||||||
|
msg.template_parameters.forEach((param, index) => {
|
||||||
|
bodyPreview = bodyPreview.replace(`{{${index + 1}}}`, param);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
preview += `<div class="message-preview">${bodyPreview}</div>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preview = `<div class="message-preview">${msg.message_content}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card scheduled-message-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-user"></i> ${msg.user_name}
|
||||||
|
<small class="text-muted">(${msg.phone_number})</small>
|
||||||
|
</h6>
|
||||||
|
<span class="badge ${statusClass}">${getStatusText(msg.status)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<i class="fas fa-calendar"></i> ${dateStr}
|
||||||
|
<br>
|
||||||
|
<i class="fas fa-clock"></i> ${timeStr}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${preview}
|
||||||
|
|
||||||
|
${msg.error_message ? `<div class="alert alert-danger mt-2 mb-0"><small>${msg.error_message}</small></div>` : ''}
|
||||||
|
|
||||||
|
<div class="mt-3 d-flex gap-2">
|
||||||
|
${msg.status === 'pending' ? `
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="editMessage(${msg.id})">
|
||||||
|
<i class="fas fa-edit"></i> Editar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-warning" onclick="cancelMessage(${msg.id})">
|
||||||
|
<i class="fas fa-ban"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteMessage(${msg.id})">
|
||||||
|
<i class="fas fa-trash"></i> Eliminar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status) {
|
||||||
|
const texts = {
|
||||||
|
'pending': 'Pendiente',
|
||||||
|
'sent': 'Enviado',
|
||||||
|
'failed': 'Fallido',
|
||||||
|
'cancelled': 'Cancelado'
|
||||||
|
};
|
||||||
|
return texts[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editar mensaje
|
||||||
|
function editMessage(id) {
|
||||||
|
const msg = currentMessages.find(m => m.id == id);
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
document.getElementById('edit-message-id').value = msg.id;
|
||||||
|
document.getElementById('edit-date').value = msg.scheduled_date;
|
||||||
|
document.getElementById('edit-time').value = msg.scheduled_time;
|
||||||
|
|
||||||
|
// Si tiene parámetros de plantilla, mostrar campos para editarlos
|
||||||
|
const paramsContainer = document.getElementById('edit-parameters-container');
|
||||||
|
paramsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (msg.template_parameters && Array.isArray(msg.template_parameters)) {
|
||||||
|
paramsContainer.innerHTML = '<label class="form-label">Parámetros de la Plantilla</label>';
|
||||||
|
msg.template_parameters.forEach((param, index) => {
|
||||||
|
paramsContainer.innerHTML += `
|
||||||
|
<div class="mb-2">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
id="edit-param-${index}"
|
||||||
|
value="${param}"
|
||||||
|
placeholder="Variable ${index + 1}">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar cambios
|
||||||
|
async function saveEdit() {
|
||||||
|
const id = document.getElementById('edit-message-id').value;
|
||||||
|
const date = document.getElementById('edit-date').value;
|
||||||
|
const time = document.getElementById('edit-time').value;
|
||||||
|
|
||||||
|
// Recopilar parámetros editados
|
||||||
|
const msg = currentMessages.find(m => m.id == id);
|
||||||
|
let parameters = null;
|
||||||
|
|
||||||
|
if (msg && msg.template_parameters && Array.isArray(msg.template_parameters)) {
|
||||||
|
parameters = [];
|
||||||
|
msg.template_parameters.forEach((_, index) => {
|
||||||
|
const input = document.getElementById(`edit-param-${index}`);
|
||||||
|
if (input) {
|
||||||
|
parameters.push(input.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: parseInt(id),
|
||||||
|
scheduled_date: date,
|
||||||
|
scheduled_time: time
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parameters) {
|
||||||
|
payload.template_parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('api/update_scheduled_message.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert('Mensaje actualizado exitosamente');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||||
|
loadScheduledMessages();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error al guardar cambios');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancelar mensaje
|
||||||
|
async function cancelMessage(id) {
|
||||||
|
if (!confirm('¿Cancelar este mensaje programado?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('api/update_scheduled_message.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id, status: 'cancelled' })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
loadScheduledMessages();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error al cancelar mensaje');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar mensaje
|
||||||
|
async function deleteMessage(id) {
|
||||||
|
if (!confirm('¿Eliminar este mensaje programado? Esta acción no se puede deshacer.')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('api/delete_scheduled_message.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
loadScheduledMessages();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error al eliminar mensaje');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadUsers();
|
||||||
|
loadScheduledMessages();
|
||||||
|
|
||||||
|
// Establecer fecha de hoy como mínimo
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('filter-date-from').value = today;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 🔍 Script de Verificación de Cache
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo "🔍 VERIFICANDO CONFIGURACIÓN ANTI-CACHÉ"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Colores
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# URL a verificar (cambiar según tu instalación)
|
||||||
|
URL="http://localhost:8080/conversations.php"
|
||||||
|
|
||||||
|
# Si se pasa un argumento, usarlo como URL
|
||||||
|
if [ ! -z "$1" ]; then
|
||||||
|
URL="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📍 URL a verificar: $URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar headers HTTP
|
||||||
|
echo "🔍 Verificando headers HTTP..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
HEADERS=$(curl -I -s "$URL")
|
||||||
|
|
||||||
|
# Función para verificar header
|
||||||
|
check_header() {
|
||||||
|
local header_name="$1"
|
||||||
|
local expected_value="$2"
|
||||||
|
|
||||||
|
if echo "$HEADERS" | grep -qi "$header_name"; then
|
||||||
|
local actual_value=$(echo "$HEADERS" | grep -i "$header_name" | cut -d: -f2- | tr -d '\r\n' | sed 's/^[[:space:]]*//')
|
||||||
|
echo -e "${GREEN}✅ $header_name:${NC} $actual_value"
|
||||||
|
|
||||||
|
if [ ! -z "$expected_value" ]; then
|
||||||
|
if echo "$actual_value" | grep -qi "$expected_value"; then
|
||||||
|
echo -e " ${GREEN}→ Contiene '$expected_value' ✓${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠️ No contiene '$expected_value' esperado${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ $header_name: NO ENCONTRADO${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "1️⃣ Cache-Control"
|
||||||
|
check_header "Cache-Control" "no-store"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2️⃣ Pragma"
|
||||||
|
check_header "Pragma" "no-cache"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3️⃣ Expires"
|
||||||
|
check_header "Expires" "1970"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "4️⃣ Content-Type"
|
||||||
|
check_header "Content-Type" "text/html"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar contenido HTML
|
||||||
|
echo "🔍 Verificando meta tags en HTML..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
CONTENT=$(curl -s "$URL")
|
||||||
|
|
||||||
|
check_meta() {
|
||||||
|
local meta_name="$1"
|
||||||
|
local search_pattern="$2"
|
||||||
|
|
||||||
|
if echo "$CONTENT" | grep -qi "$search_pattern"; then
|
||||||
|
echo -e "${GREEN}✅ Meta tag '$meta_name' encontrado${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Meta tag '$meta_name' NO encontrado${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
check_meta "Cache-Control" 'meta.*http-equiv.*Cache-Control.*no-cache'
|
||||||
|
check_meta "Pragma" 'meta.*http-equiv.*Pragma.*no-cache'
|
||||||
|
check_meta "Expires" 'meta.*http-equiv.*Expires.*0'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar cache busting en scripts
|
||||||
|
echo "🔍 Verificando cache busting en scripts..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
if echo "$CONTENT" | grep -qE 'chat-common\.js\?v=[0-9]+\.[0-9]+'; then
|
||||||
|
VERSION=$(echo "$CONTENT" | grep -oE 'chat-common\.js\?v=[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||||
|
echo -e "${GREEN}✅ Cache busting encontrado:${NC} $VERSION"
|
||||||
|
|
||||||
|
# Extraer solo el número de versión
|
||||||
|
VERSION_NUMBER=$(echo "$VERSION" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
echo -e " ${GREEN}→ Versión actual:${NC} $VERSION_NUMBER"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Cache busting NO encontrado o formato incorrecto${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar versión en consola
|
||||||
|
if echo "$CONTENT" | grep -q "WHATSAPP BOT v1.2.1 - ANTI-CACHÉ ACTIVADO"; then
|
||||||
|
echo -e "${GREEN}✅ Versión 1.2.1 con anti-caché confirmada${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Versión no detectada o diferente${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo "📊 RESUMEN"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
|
||||||
|
# Contar checks exitosos y fallidos
|
||||||
|
TOTAL_CHECKS=7
|
||||||
|
PASSED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
if echo "$HEADERS" | grep -qi "Cache-Control.*no-store"; then ((PASSED++)); else ((FAILED++)); fi
|
||||||
|
if echo "$HEADERS" | grep -qi "Pragma.*no-cache"; then ((PASSED++)); else ((FAILED++)); fi
|
||||||
|
if echo "$HEADERS" | grep -qi "Expires.*1970"; then ((PASSED++)); else ((FAILED++)); fi
|
||||||
|
if echo "$CONTENT" | grep -qi 'meta.*Cache-Control.*no-cache'; then ((PASSED++)); else ((FAILED++)); fi
|
||||||
|
if echo "$CONTENT" | grep -qi 'meta.*Pragma.*no-cache'; then ((PASSED++)); else ((FAILED++)); fi
|
||||||
|
if echo "$CONTENT" | grep -qi 'meta.*Expires.*0'; then ((PASSED++)); else ((FAILED++)); fi
|
||||||
|
if echo "$CONTENT" | grep -qE 'chat-common\.js\?v=[0-9]+'; then ((PASSED++)); else ((FAILED++)); fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ Checks exitosos: $PASSED/$TOTAL_CHECKS${NC}"
|
||||||
|
echo -e "${RED}❌ Checks fallidos: $FAILED/$TOTAL_CHECKS${NC}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $FAILED -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${GREEN}🎉 ¡TODO ESTÁ CONFIGURADO CORRECTAMENTE!${NC}"
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "✅ La página está configurada para NO cachearse"
|
||||||
|
echo "✅ Los scripts tienen cache busting activo"
|
||||||
|
echo "✅ Los usuarios verán siempre la versión más reciente"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Si aún ves problemas, pide al usuario hacer:"
|
||||||
|
echo " - Ctrl+Shift+R (hard reload)"
|
||||||
|
echo " - Limpiar caché del navegador"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${YELLOW}⚠️ HAY PROBLEMAS DE CONFIGURACIÓN${NC}"
|
||||||
|
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "🔧 Posibles soluciones:"
|
||||||
|
echo ""
|
||||||
|
echo "1. Verifica que mod_headers esté habilitado en Apache:"
|
||||||
|
echo " sudo a2enmod headers"
|
||||||
|
echo " sudo systemctl restart apache2"
|
||||||
|
echo ""
|
||||||
|
echo "2. Verifica que .htaccess se esté leyendo:"
|
||||||
|
echo " - Asegúrate que AllowOverride All esté configurado"
|
||||||
|
echo ""
|
||||||
|
echo "3. Verifica que conversations.php tenga los headers PHP:"
|
||||||
|
echo " - Busca 'header(\"Cache-Control' al inicio del archivo"
|
||||||
|
echo ""
|
||||||
|
echo "4. Si usas Nginx en lugar de Apache:"
|
||||||
|
echo " - Los headers deben configurarse en el server block"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+146
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script para enviar mensajes programados
|
||||||
|
* Se ejecuta cada minuto desde cron o worker
|
||||||
|
*
|
||||||
|
* Uso:
|
||||||
|
* php send_scheduled_messages.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/config/config.php';
|
||||||
|
|
||||||
|
// Configurar logs
|
||||||
|
$logFile = __DIR__ . '/logs/scheduled_messages.log';
|
||||||
|
|
||||||
|
function writeScheduledLog($message) {
|
||||||
|
global $logFile;
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeScheduledLog("=== Iniciando envío de mensajes programados ===");
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$whatsappService = new WhatsAppService();
|
||||||
|
|
||||||
|
// Obtener mensajes programados para enviar
|
||||||
|
// Buscar mensajes cuya fecha/hora ya pasó y están pendientes
|
||||||
|
$currentDateTime = date('Y-m-d H:i:s');
|
||||||
|
$currentDate = date('Y-m-d');
|
||||||
|
$currentTime = date('H:i:00'); // Redondear a minuto exacto
|
||||||
|
|
||||||
|
$sql = "SELECT sm.*, u.phone_number, u.name as user_name
|
||||||
|
FROM scheduled_messages sm
|
||||||
|
LEFT JOIN users u ON sm.user_id = u.id
|
||||||
|
WHERE sm.status = 'pending'
|
||||||
|
AND (
|
||||||
|
(sm.scheduled_date < ? AND sm.scheduled_time <= ?)
|
||||||
|
OR (sm.scheduled_date = ? AND sm.scheduled_time <= ?)
|
||||||
|
)
|
||||||
|
LIMIT 50";
|
||||||
|
|
||||||
|
$messages = $db->fetchAll($sql, [$currentDate, '23:59:59', $currentDate, $currentTime]);
|
||||||
|
|
||||||
|
writeScheduledLog("Mensajes encontrados para enviar: " . count($messages));
|
||||||
|
|
||||||
|
if (empty($messages)) {
|
||||||
|
writeScheduledLog("No hay mensajes programados para enviar");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sentCount = 0;
|
||||||
|
$errorCount = 0;
|
||||||
|
|
||||||
|
foreach ($messages as $msg) {
|
||||||
|
try {
|
||||||
|
writeScheduledLog("Procesando mensaje ID {$msg['id']} para usuario {$msg['user_name']} ({$msg['phone_number']})");
|
||||||
|
|
||||||
|
// Verificar que el usuario tenga teléfono
|
||||||
|
if (empty($msg['phone_number'])) {
|
||||||
|
throw new Exception("Usuario sin número de teléfono");
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = null;
|
||||||
|
|
||||||
|
if ($msg['message_type'] === 'template') {
|
||||||
|
// Enviar plantilla
|
||||||
|
$templateName = $msg['template_name'];
|
||||||
|
$language = $msg['template_language'] ?? 'es';
|
||||||
|
$parameters = [];
|
||||||
|
|
||||||
|
if (!empty($msg['template_parameters'])) {
|
||||||
|
$parameters = json_decode($msg['template_parameters'], true);
|
||||||
|
if (!is_array($parameters)) {
|
||||||
|
$parameters = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeScheduledLog("Enviando plantilla: {$templateName} con " . count($parameters) . " parámetros");
|
||||||
|
|
||||||
|
$response = $whatsappService->sendTemplateMessage(
|
||||||
|
$msg['phone_number'],
|
||||||
|
$templateName,
|
||||||
|
$language,
|
||||||
|
$parameters
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if ($msg['message_type'] === 'text') {
|
||||||
|
// Enviar mensaje de texto
|
||||||
|
$content = $msg['message_content'];
|
||||||
|
|
||||||
|
writeScheduledLog("Enviando mensaje de texto");
|
||||||
|
|
||||||
|
$response = $whatsappService->sendTextMessage(
|
||||||
|
$msg['phone_number'],
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Exception("Tipo de mensaje no soportado: " . $msg['message_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar respuesta
|
||||||
|
if ($response && (isset($response['messages']) || isset($response['success']))) {
|
||||||
|
// Marcar como enviado
|
||||||
|
$db->execute(
|
||||||
|
"UPDATE scheduled_messages SET status = 'sent', sent_at = NOW(), updated_at = NOW() WHERE id = ?",
|
||||||
|
[$msg['id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
writeScheduledLog("✅ Mensaje ID {$msg['id']} enviado exitosamente");
|
||||||
|
$sentCount++;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Exception("Respuesta inválida de WhatsApp API: " . json_encode($response));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errorMessage = $e->getMessage();
|
||||||
|
writeScheduledLog("❌ Error enviando mensaje ID {$msg['id']}: {$errorMessage}");
|
||||||
|
|
||||||
|
// Marcar como fallido
|
||||||
|
$db->execute(
|
||||||
|
"UPDATE scheduled_messages SET status = 'failed', error_message = ?, updated_at = NOW() WHERE id = ?",
|
||||||
|
[$errorMessage, $msg['id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
$errorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pequeña pausa entre envíos para no saturar API
|
||||||
|
usleep(250000); // 250ms
|
||||||
|
}
|
||||||
|
|
||||||
|
writeScheduledLog("=== Resumen ===");
|
||||||
|
writeScheduledLog("Enviados: {$sentCount}");
|
||||||
|
writeScheduledLog("Errores: {$errorCount}");
|
||||||
|
writeScheduledLog("=== Fin ===");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
writeScheduledLog("ERROR FATAL: " . $e->getMessage());
|
||||||
|
writeScheduledLog($e->getTraceAsString());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
+56
-7
@@ -851,8 +851,9 @@ class BotService {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'message':
|
case 'message':
|
||||||
// Enviar mensaje de respuesta
|
// Enviar mensaje de respuesta (procesando saltos de línea)
|
||||||
$this->whatsappService->sendTextMessage($phoneNumber, $option['action_value']);
|
$message = $this->processLineBreaks($option['action_value']);
|
||||||
|
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
||||||
$this->exitMenu($phoneNumber);
|
$this->exitMenu($phoneNumber);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -864,6 +865,7 @@ class BotService {
|
|||||||
case 'request_documents':
|
case 'request_documents':
|
||||||
// Solicitar al usuario que envíe documentación y activar estado awaiting_documents
|
// Solicitar al usuario que envíe documentación y activar estado awaiting_documents
|
||||||
$message = $option['action_value'] ?: "Por favor, envíe los documentos o fotos requeridas.";
|
$message = $option['action_value'] ?: "Por favor, envíe los documentos o fotos requeridas.";
|
||||||
|
$message = $this->processLineBreaks($message);
|
||||||
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
||||||
try {
|
try {
|
||||||
$stateSvc = new ConversationStateService();
|
$stateSvc = new ConversationStateService();
|
||||||
@@ -891,6 +893,7 @@ class BotService {
|
|||||||
case 'end':
|
case 'end':
|
||||||
// Finalizar conversación (comportamiento por defecto)
|
// Finalizar conversación (comportamiento por defecto)
|
||||||
$message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
|
$message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
|
||||||
|
$message = $this->processLineBreaks($message);
|
||||||
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
||||||
$this->exitMenu($phoneNumber);
|
$this->exitMenu($phoneNumber);
|
||||||
// Pausar el bot para esta conversación por 3 minutos (antes eran 4 horas)
|
// Pausar el bot para esta conversación por 3 minutos (antes eran 4 horas)
|
||||||
@@ -899,6 +902,20 @@ class BotService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Procesar texto para respetar saltos de línea
|
||||||
|
* Convierte \n escapados a saltos reales
|
||||||
|
*/
|
||||||
|
private function processLineBreaks($text) {
|
||||||
|
if (empty($text)) return $text;
|
||||||
|
|
||||||
|
// Convertir \n escapados (cadena literal) a saltos de línea reales
|
||||||
|
$text = str_replace('\\n', "\n", $text);
|
||||||
|
$text = str_replace('\n', "\n", $text);
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Procesar llamada API externa
|
* Procesar llamada API externa
|
||||||
*/
|
*/
|
||||||
@@ -934,7 +951,8 @@ class BotService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($autoResponse) {
|
if ($autoResponse) {
|
||||||
$this->whatsappService->sendTextMessage($phoneNumber, $autoResponse['response_text']);
|
$message = $this->processLineBreaks($autoResponse['response_text']);
|
||||||
|
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
||||||
|
|
||||||
// Si la keyword es 'menu', mostrar el menú
|
// Si la keyword es 'menu', mostrar el menú
|
||||||
if ($keyword === 'menu' || $keyword === 'menú') {
|
if ($keyword === 'menu' || $keyword === 'menú') {
|
||||||
@@ -1036,6 +1054,23 @@ class BotService {
|
|||||||
$this->db->update('users', ['advisor_requested' => 1, 'bot_paused_until' => $pausedUntil, 'bot_enabled' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
$this->db->update('users', ['advisor_requested' => 1, 'bot_paused_until' => $pausedUntil, 'bot_enabled' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
||||||
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Le hemos transferido a un asesor; le responderemos en breve.");
|
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Le hemos transferido a un asesor; le responderemos en breve.");
|
||||||
|
|
||||||
|
// Enviar automáticamente el menú de horario_asesor para que el cliente sepa cuándo esperar
|
||||||
|
try {
|
||||||
|
$horarioMenu = $this->db->fetch(
|
||||||
|
"SELECT id FROM menus WHERE name = :name AND is_active = 1",
|
||||||
|
['name' => 'horario_asesor']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($horarioMenu && !empty($horarioMenu['id'])) {
|
||||||
|
$this->showMenu($phoneNumber, $horarioMenu['id']);
|
||||||
|
error_log("[BotService] Sent horario_asesor menu to {$phoneNumber} after advisor request");
|
||||||
|
} else {
|
||||||
|
error_log("[BotService] horario_asesor menu not found when requesting advisor for {$phoneNumber}");
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Failed to send horario_asesor menu on advisor request: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
if (function_exists('writeLog')) writeLog('INFO', "Advisor solicited for $phoneNumber until $pausedUntil");
|
if (function_exists('writeLog')) writeLog('INFO', "Advisor solicited for $phoneNumber until $pausedUntil");
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('requestAdvisor failed: ' . $e->getMessage());
|
error_log('requestAdvisor failed: ' . $e->getMessage());
|
||||||
@@ -1152,15 +1187,29 @@ class BotService {
|
|||||||
if (function_exists('writeLog')) writeLog('INFO', "Operator {$operatorId} finished attending for {$phoneNumber}, survey={$template}");
|
if (function_exists('writeLog')) writeLog('INFO', "Operator {$operatorId} finished attending for {$phoneNumber}, survey={$template}");
|
||||||
|
|
||||||
// Ensure consistent behavior: release hold / clear flags when finishing attend
|
// Ensure consistent behavior: release hold / clear flags when finishing attend
|
||||||
// Do NOT send the main menu nor notify the user that the conversation is resumed.
|
|
||||||
try {
|
try {
|
||||||
// Release hold silently to avoid duplicate notification after finishing attend
|
// Release hold silently to avoid duplicate notification after finishing attend
|
||||||
// Send notification that conversation has been resumed, but do NOT auto-send the menu.
|
$this->releaseHold($phoneNumber, false);
|
||||||
$this->releaseHold($phoneNumber, true);
|
|
||||||
// Intentionally not calling showMainMenu or sending a 'conversation resumed' message here.
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('finishAttendConversation: releaseHold failed: ' . $e->getMessage());
|
error_log('finishAttendConversation: releaseHold failed: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enviar automáticamente el menú de horario_asesor después de finalizar
|
||||||
|
try {
|
||||||
|
$horarioMenu = $this->db->fetch(
|
||||||
|
"SELECT id FROM menus WHERE name = :name AND is_active = 1",
|
||||||
|
['name' => 'horario_asesor']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($horarioMenu && !empty($horarioMenu['id'])) {
|
||||||
|
$this->showMenu($phoneNumber, $horarioMenu['id']);
|
||||||
|
error_log("[BotService] Sent horario_asesor menu to {$phoneNumber} after finishing attend");
|
||||||
|
} else {
|
||||||
|
error_log("[BotService] horario_asesor menu not found for {$phoneNumber}");
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Failed to send horario_asesor menu: ' . $e->getMessage());
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('finishAttendConversation failed: ' . $e->getMessage());
|
error_log('finishAttendConversation failed: ' . $e->getMessage());
|
||||||
throw $e;
|
throw $e;
|
||||||
|
|||||||
@@ -1,352 +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 SSE - WhatsApp Bot</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #25d366;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
margin: 5px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
.btn-connect {
|
|
||||||
background: #25d366;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.btn-connect:hover {
|
|
||||||
background: #128c7e;
|
|
||||||
}
|
|
||||||
.btn-disconnect {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.btn-disconnect:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
.btn-clear {
|
|
||||||
background: #6c757d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.status.connected {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
|
||||||
.status.disconnected {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
|
||||||
.status.connecting {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
border: 1px solid #ffeeba;
|
|
||||||
}
|
|
||||||
.events {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.event {
|
|
||||||
padding: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
border-left: 4px solid;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.event-connected {
|
|
||||||
border-color: #28a745;
|
|
||||||
background: #d4edda;
|
|
||||||
}
|
|
||||||
.event-new_message {
|
|
||||||
border-color: #007bff;
|
|
||||||
background: #cfe2ff;
|
|
||||||
}
|
|
||||||
.event-new_conversation {
|
|
||||||
border-color: #17a2b8;
|
|
||||||
background: #d1ecf1;
|
|
||||||
}
|
|
||||||
.event-notification {
|
|
||||||
border-color: #ffc107;
|
|
||||||
background: #fff3cd;
|
|
||||||
}
|
|
||||||
.event-heartbeat {
|
|
||||||
border-color: #6c757d;
|
|
||||||
background: #e2e3e5;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.event-error {
|
|
||||||
border-color: #dc3545;
|
|
||||||
background: #f8d7da;
|
|
||||||
}
|
|
||||||
.timestamp {
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.event-type {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #495057;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.event-data {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 3px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.stat {
|
|
||||||
background: #e9ecef;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.stat-value {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
.stat-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #6c757d;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🔌 Test SSE - WhatsApp Bot</h1>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button class="btn-connect" onclick="connect()">▶️ Conectar SSE</button>
|
|
||||||
<button class="btn-disconnect" onclick="disconnect()">⏹️ Desconectar</button>
|
|
||||||
<button class="btn-clear" onclick="clearEvents()">🗑️ Limpiar eventos</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status disconnected" id="status">
|
|
||||||
⚪ Desconectado
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-value" id="stat-messages">0</div>
|
|
||||||
<div class="stat-label">Mensajes</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-value" id="stat-conversations">0</div>
|
|
||||||
<div class="stat-label">Conversaciones</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-value" id="stat-notifications">0</div>
|
|
||||||
<div class="stat-label">Notificaciones</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-value" id="stat-heartbeats">0</div>
|
|
||||||
<div class="stat-label">Heartbeats</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="events">
|
|
||||||
<h3>📋 Eventos Recibidos:</h3>
|
|
||||||
<div id="events"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let eventSource = null;
|
|
||||||
let stats = {
|
|
||||||
messages: 0,
|
|
||||||
conversations: 0,
|
|
||||||
notifications: 0,
|
|
||||||
heartbeats: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateStatus(text, className) {
|
|
||||||
const statusEl = document.getElementById('status');
|
|
||||||
statusEl.textContent = text;
|
|
||||||
statusEl.className = 'status ' + className;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStats() {
|
|
||||||
document.getElementById('stat-messages').textContent = stats.messages;
|
|
||||||
document.getElementById('stat-conversations').textContent = stats.conversations;
|
|
||||||
document.getElementById('stat-notifications').textContent = stats.notifications;
|
|
||||||
document.getElementById('stat-heartbeats').textContent = stats.heartbeats;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEvent(type, data) {
|
|
||||||
const eventsDiv = document.getElementById('events');
|
|
||||||
const eventDiv = document.createElement('div');
|
|
||||||
eventDiv.className = 'event event-' + type;
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const timestamp = now.toLocaleTimeString() + '.' + now.getMilliseconds();
|
|
||||||
|
|
||||||
eventDiv.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<span class="timestamp">${timestamp}</span>
|
|
||||||
<span class="event-type">${type}</span>
|
|
||||||
</div>
|
|
||||||
<div class="event-data">${JSON.stringify(data, null, 2)}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
eventsDiv.insertBefore(eventDiv, eventsDiv.firstChild);
|
|
||||||
|
|
||||||
// Limitar a 50 eventos
|
|
||||||
while (eventsDiv.children.length > 50) {
|
|
||||||
eventsDiv.removeChild(eventsDiv.lastChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect() {
|
|
||||||
if (eventSource) {
|
|
||||||
console.log('Ya hay una conexión activa');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus('🔄 Conectando...', 'connecting');
|
|
||||||
|
|
||||||
const baseUrl = window.location.origin;
|
|
||||||
const sseUrl = `${baseUrl}/api/sse_events.php?token=demo_token&t=${Date.now()}`;
|
|
||||||
|
|
||||||
console.log('Conectando a:', sseUrl);
|
|
||||||
eventSource = new EventSource(sseUrl);
|
|
||||||
|
|
||||||
eventSource.addEventListener('connected', (e) => {
|
|
||||||
console.log('✅ Conectado:', e.data);
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
updateStatus('🟢 Conectado (' + data.mode + ')', 'connected');
|
|
||||||
addEvent('connected', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('new_message', (e) => {
|
|
||||||
console.log('📨 Nuevo mensaje:', e.data);
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
stats.messages++;
|
|
||||||
updateStats();
|
|
||||||
addEvent('new_message', data);
|
|
||||||
|
|
||||||
// Reproducir sonido
|
|
||||||
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGH0fPTgjMGHm7A7+OZUQ0NVKzo8bllHAU+jdfyvmwhBTGH0fPTgjMGHm7A7+OZUQ0NVKzo8bllHAU+jdfyv2whBTGH0fPTgjMGHm7A7+OZUQ0NVKzo8bllHAU+jdfy');
|
|
||||||
audio.play().catch(e => console.log('No se pudo reproducir sonido'));
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('new_conversation', (e) => {
|
|
||||||
console.log('💬 Nueva conversación:', e.data);
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
stats.conversations++;
|
|
||||||
updateStats();
|
|
||||||
addEvent('new_conversation', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('notification', (e) => {
|
|
||||||
console.log('🔔 Notificación:', e.data);
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
stats.notifications++;
|
|
||||||
updateStats();
|
|
||||||
addEvent('notification', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('heartbeat', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
stats.heartbeats++;
|
|
||||||
updateStats();
|
|
||||||
addEvent('heartbeat', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('error', (e) => {
|
|
||||||
console.error('❌ Error SSE:', e);
|
|
||||||
if (e.data) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
addEvent('error', data);
|
|
||||||
} catch (err) {
|
|
||||||
addEvent('error', { message: 'Error desconocido' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
|
||||||
console.error('❌ Error en conexión SSE:', error);
|
|
||||||
const state = eventSource.readyState;
|
|
||||||
|
|
||||||
if (state === EventSource.CONNECTING) {
|
|
||||||
updateStatus('🔄 Reconectando...', 'connecting');
|
|
||||||
} else if (state === EventSource.CLOSED) {
|
|
||||||
updateStatus('🔴 Desconectado', 'disconnected');
|
|
||||||
addEvent('error', { message: 'Conexión cerrada', state: state });
|
|
||||||
eventSource = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnect() {
|
|
||||||
if (eventSource) {
|
|
||||||
eventSource.close();
|
|
||||||
eventSource = null;
|
|
||||||
updateStatus('🔴 Desconectado', 'disconnected');
|
|
||||||
console.log('Desconectado');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearEvents() {
|
|
||||||
document.getElementById('events').innerHTML = '';
|
|
||||||
stats = {
|
|
||||||
messages: 0,
|
|
||||||
conversations: 0,
|
|
||||||
notifications: 0,
|
|
||||||
heartbeats: 0
|
|
||||||
};
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-conectar al cargar
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
console.log('Página cargada, conectando automáticamente...');
|
|
||||||
setTimeout(connect, 500);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Script de testing para la arquitectura mejorada
|
|
||||||
* Verifica que todos los componentes estén funcionando correctamente
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
|
||||||
require_once __DIR__ . '/config/config.php';
|
|
||||||
|
|
||||||
use WhatsApp\Queue\RedisQueue;
|
|
||||||
use WhatsApp\Queue\ConversationState;
|
|
||||||
|
|
||||||
echo "🧪 Testing Arquitectura Mejorada\n";
|
|
||||||
echo "================================\n\n";
|
|
||||||
|
|
||||||
$results = [
|
|
||||||
'passed' => 0,
|
|
||||||
'failed' => 0,
|
|
||||||
'warnings' => 0
|
|
||||||
];
|
|
||||||
|
|
||||||
function test($name, $callback) {
|
|
||||||
global $results;
|
|
||||||
|
|
||||||
try {
|
|
||||||
echo "Testing: $name... ";
|
|
||||||
$result = $callback();
|
|
||||||
|
|
||||||
if ($result === true) {
|
|
||||||
echo "✅ PASS\n";
|
|
||||||
$results['passed']++;
|
|
||||||
} elseif ($result === null) {
|
|
||||||
echo "⚠️ WARNING\n";
|
|
||||||
$results['warnings']++;
|
|
||||||
} else {
|
|
||||||
echo "❌ FAIL: $result\n";
|
|
||||||
$results['failed']++;
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "❌ ERROR: " . $e->getMessage() . "\n";
|
|
||||||
$results['failed']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Test Redis Connection
|
|
||||||
test("Redis connection", function() {
|
|
||||||
try {
|
|
||||||
$redis = new Predis\Client([
|
|
||||||
'scheme' => getenv('REDIS_SCHEME') ?: 'tcp',
|
|
||||||
'host' => getenv('REDIS_HOST') ?: '127.0.0.1',
|
|
||||||
'port' => getenv('REDIS_PORT') ?: 6379,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$ping = $redis->ping();
|
|
||||||
return ($ping === 'PONG' || $ping === true);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return "No se pudo conectar a Redis: " . $e->getMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Test RedisQueue - Push
|
|
||||||
test("RedisQueue::push", function() {
|
|
||||||
$queue = new RedisQueue();
|
|
||||||
$result = $queue->push('test_queue', ['message' => 'test'], 1);
|
|
||||||
return $result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Test RedisQueue - Pop
|
|
||||||
test("RedisQueue::pop", function() {
|
|
||||||
$queue = new RedisQueue();
|
|
||||||
$message = $queue->pop('test_queue', 1);
|
|
||||||
|
|
||||||
if (!$message) {
|
|
||||||
return "No se pudo obtener mensaje de la cola";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($message['data']['message'] !== 'test') {
|
|
||||||
return "Mensaje recibido no coincide con el enviado";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Test RedisQueue - Stats
|
|
||||||
test("RedisQueue::getStats", function() {
|
|
||||||
$queue = new RedisQueue();
|
|
||||||
$stats = $queue->getStats();
|
|
||||||
|
|
||||||
if (!is_array($stats)) {
|
|
||||||
return "Stats no devuelve un array";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Test ConversationState - Set
|
|
||||||
test("ConversationState::setState", function() {
|
|
||||||
$state = new ConversationState();
|
|
||||||
$result = $state->setState('test_user_123', 'testing', ['key' => 'value'], 300);
|
|
||||||
return $result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 6. Test ConversationState - Get
|
|
||||||
test("ConversationState::getState", function() {
|
|
||||||
$state = new ConversationState();
|
|
||||||
$result = $state->getState('test_user_123');
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
return "No se pudo obtener estado";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result['state'] !== 'testing') {
|
|
||||||
return "Estado no coincide";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result['context']['key'] !== 'value') {
|
|
||||||
return "Contexto no coincide";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7. Test ConversationState - Update Context
|
|
||||||
test("ConversationState::updateContext", function() {
|
|
||||||
$state = new ConversationState();
|
|
||||||
$result = $state->updateContext('test_user_123', ['new_key' => 'new_value']);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
return "No se pudo actualizar contexto";
|
|
||||||
}
|
|
||||||
|
|
||||||
$current = $state->getState('test_user_123');
|
|
||||||
if (!isset($current['context']['new_key'])) {
|
|
||||||
return "Contexto no se actualizó correctamente";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8. Test ConversationState - Clear
|
|
||||||
test("ConversationState::clearState", function() {
|
|
||||||
$state = new ConversationState();
|
|
||||||
$state->clearState('test_user_123');
|
|
||||||
|
|
||||||
$result = $state->getState('test_user_123');
|
|
||||||
return $result === null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 9. Test Logger
|
|
||||||
test("LoggerFactory", function() {
|
|
||||||
if (!class_exists('LoggerFactory')) {
|
|
||||||
return "LoggerFactory no existe";
|
|
||||||
}
|
|
||||||
|
|
||||||
$logger = LoggerFactory::getLogger('test');
|
|
||||||
$logger->info("Test message");
|
|
||||||
|
|
||||||
// Verificar que el archivo de log existe
|
|
||||||
$logPath = __DIR__ . '/logs/test.log';
|
|
||||||
if (!file_exists($logPath)) {
|
|
||||||
return "Archivo de log no fue creado";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limpiar
|
|
||||||
@unlink($logPath);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 10. Test Database Connection
|
|
||||||
test("Database connection", function() {
|
|
||||||
try {
|
|
||||||
$db = Database::getInstance();
|
|
||||||
$result = $db->query("SELECT 1 as test")->fetch();
|
|
||||||
return $result && $result['test'] == 1;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return "Error de conexión: " . $e->getMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 11. Test WhatsApp Config
|
|
||||||
test("WhatsApp configuration", function() {
|
|
||||||
if (!function_exists('getWhatsAppConfigFromDB')) {
|
|
||||||
return "Función getWhatsAppConfigFromDB no existe";
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = getWhatsAppConfigFromDB();
|
|
||||||
|
|
||||||
if (empty($config['token'])) {
|
|
||||||
return null; // Warning, no error crítico
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 12. Test file permissions
|
|
||||||
test("Log directory permissions", function() {
|
|
||||||
$logDir = __DIR__ . '/logs';
|
|
||||||
|
|
||||||
if (!is_dir($logDir)) {
|
|
||||||
return "Directorio logs no existe";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_writable($logDir)) {
|
|
||||||
return "Directorio logs no tiene permisos de escritura";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 13. Test uploads directory
|
|
||||||
test("Uploads directory permissions", function() {
|
|
||||||
$uploadsDir = __DIR__ . '/uploads';
|
|
||||||
|
|
||||||
if (!is_dir($uploadsDir)) {
|
|
||||||
return null; // Warning
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_writable($uploadsDir)) {
|
|
||||||
return "Directorio uploads no tiene permisos de escritura";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 14. Test Composer autoload
|
|
||||||
test("Composer autoload", function() {
|
|
||||||
if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
|
|
||||||
return "vendor/autoload.php no existe. Ejecuta: composer install";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar clases clave
|
|
||||||
if (!class_exists('Predis\Client')) {
|
|
||||||
return "Predis no está instalado";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists('Monolog\Logger')) {
|
|
||||||
return "Monolog no está instalado";
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 15. Test .env file
|
|
||||||
test(".env configuration", function() {
|
|
||||||
if (!file_exists(__DIR__ . '/.env')) {
|
|
||||||
return "Archivo .env no existe. Copia .env.example";
|
|
||||||
}
|
|
||||||
|
|
||||||
$requiredVars = ['DB_HOST', 'DB_NAME', 'REDIS_HOST'];
|
|
||||||
foreach ($requiredVars as $var) {
|
|
||||||
if (!getenv($var)) {
|
|
||||||
return "Variable $var no está configurada en .env";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Limpiar test data
|
|
||||||
echo "\n🧹 Limpiando datos de test...\n";
|
|
||||||
try {
|
|
||||||
$queue = new RedisQueue();
|
|
||||||
$queue->flush('test_queue');
|
|
||||||
$state = new ConversationState();
|
|
||||||
$state->clearState('test_user_123');
|
|
||||||
echo "✅ Limpieza completada\n";
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "⚠️ Error en limpieza: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resultados finales
|
|
||||||
echo "\n";
|
|
||||||
echo "================================\n";
|
|
||||||
echo "📊 RESULTADOS\n";
|
|
||||||
echo "================================\n";
|
|
||||||
echo "✅ Pasados: " . $results['passed'] . "\n";
|
|
||||||
echo "❌ Fallidos: " . $results['failed'] . "\n";
|
|
||||||
echo "⚠️ Warnings: " . $results['warnings'] . "\n";
|
|
||||||
echo "================================\n";
|
|
||||||
|
|
||||||
if ($results['failed'] > 0) {
|
|
||||||
echo "\n❌ Algunos tests fallaron. Revisa la configuración.\n";
|
|
||||||
exit(1);
|
|
||||||
} elseif ($results['warnings'] > 0) {
|
|
||||||
echo "\n⚠️ Tests completados con warnings. Algunas funciones pueden no estar completamente configuradas.\n";
|
|
||||||
exit(0);
|
|
||||||
} else {
|
|
||||||
echo "\n✅ Todos los tests pasaron correctamente!\n";
|
|
||||||
echo "\n🚀 Tu arquitectura está lista para usarse.\n";
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script para probar envío de archivos multimedia
|
|
||||||
# Verifica que se guarden correctamente en la BD
|
|
||||||
|
|
||||||
echo "🧪 Prueba de Envío de Archivos Multimedia"
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Verificar estructura de BD
|
|
||||||
echo "1️⃣ Verificando columnas de la tabla conversations..."
|
|
||||||
docker exec whatsapp-dev-app php -r "
|
|
||||||
require 'config/config.php';
|
|
||||||
\$db = Database::getInstance();
|
|
||||||
\$cols = \$db->fetchAll('DESCRIBE conversations');
|
|
||||||
\$important = ['whatsapp_media_id', 'media_url', 'local_file', 'local_thumb', 'filename', 'mime_type'];
|
|
||||||
echo 'Columnas importantes:' . PHP_EOL;
|
|
||||||
foreach (\$cols as \$col) {
|
|
||||||
if (in_array(\$col['Field'], \$important)) {
|
|
||||||
echo ' ✅ ' . \$col['Field'] . ' (' . \$col['Type'] . ')' . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
" || { echo "❌ Error verificando BD"; exit 1; }
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "2️⃣ Últimos 3 mensajes multimedia enviados:"
|
|
||||||
docker exec whatsapp-dev-app php -r "
|
|
||||||
require 'config/config.php';
|
|
||||||
\$db = Database::getInstance();
|
|
||||||
\$messages = \$db->fetchAll(
|
|
||||||
'SELECT id, user_id, message_type, content, media_url, whatsapp_media_id,
|
|
||||||
local_file, filename, created_at
|
|
||||||
FROM conversations
|
|
||||||
WHERE message_type IN (\"image\", \"video\", \"audio\", \"document\")
|
|
||||||
AND direction = \"outgoing\"
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 3'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty(\$messages)) {
|
|
||||||
echo ' ℹ️ No hay mensajes multimedia en la BD aún' . PHP_EOL;
|
|
||||||
} else {
|
|
||||||
foreach (\$messages as \$msg) {
|
|
||||||
echo PHP_EOL;
|
|
||||||
echo ' 📎 ID: ' . \$msg['id'] . PHP_EOL;
|
|
||||||
echo ' Tipo: ' . \$msg['message_type'] . PHP_EOL;
|
|
||||||
echo ' Contenido: ' . substr(\$msg['content'], 0, 50) . PHP_EOL;
|
|
||||||
echo ' Media ID WA: ' . (\$msg['whatsapp_media_id'] ?: 'null') . PHP_EOL;
|
|
||||||
echo ' Archivo Local: ' . (\$msg['local_file'] ?: 'null') . PHP_EOL;
|
|
||||||
echo ' Fecha: ' . \$msg['created_at'] . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "✅ Pruebas completadas"
|
|
||||||
echo ""
|
|
||||||
echo "📝 Cómo probar:"
|
|
||||||
echo " 1. Abre http://localhost:8080/conversations.php"
|
|
||||||
echo " 2. Selecciona una conversación"
|
|
||||||
echo " 3. Haz click en el botón adjuntar (📎)"
|
|
||||||
echo " 4. Selecciona una imagen o documento"
|
|
||||||
echo " 5. Envía el archivo"
|
|
||||||
echo " 6. Ejecuta este script de nuevo para ver si se guardó"
|
|
||||||
echo ""
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
# 🧪 Prueba de Mensajes Nuevos en Tiempo Real
|
|
||||||
|
|
||||||
## ✅ Cambios Implementados:
|
|
||||||
|
|
||||||
### 1. **Estilo Visual para Mensajes No Leídos**
|
|
||||||
- Fondo amarillo claro (#fffbea) con borde sutil
|
|
||||||
- Animación de highlight al aparecer
|
|
||||||
- Se quita automáticamente después de 3 segundos
|
|
||||||
|
|
||||||
### 2. **Detección Mejorada de Scroll**
|
|
||||||
- Logs detallados en consola para debugging
|
|
||||||
- Cambio de `initial=false` a `initial=true` cuando estás abajo (esto fuerza una recarga completa)
|
|
||||||
- Marca el último mensaje como no leído temporalmente
|
|
||||||
|
|
||||||
### 3. **Flujo de Funcionamiento:**
|
|
||||||
|
|
||||||
#### **Cuando estás AL FINAL del chat:**
|
|
||||||
1. SSE detecta nuevo mensaje entrante
|
|
||||||
2. Verifica que estás dentro de 150px del final
|
|
||||||
3. Marca flag `_nextMessageUnread = true`
|
|
||||||
4. Llama `loadMessages(userId, true, true)` para recargar
|
|
||||||
5. El último mensaje se renderiza con clase `unread`
|
|
||||||
6. Después de 3 segundos, quita el highlight automáticamente
|
|
||||||
|
|
||||||
#### **Cuando estás LEYENDO ARRIBA:**
|
|
||||||
1. SSE detecta nuevo mensaje entrante
|
|
||||||
2. Verifica que NO estás cerca del final
|
|
||||||
3. Muestra botón "Nuevo mensaje" o "Nuevos mensajes (N)"
|
|
||||||
4. Guarda mensaje en staging
|
|
||||||
5. Cuando haces click, baja y muestra todos los mensajes
|
|
||||||
|
|
||||||
## 🔍 Cómo Probar:
|
|
||||||
|
|
||||||
### Paso 1: Abrir Navegador y Consola
|
|
||||||
```bash
|
|
||||||
# Abrir http://localhost:8080/conversations.php
|
|
||||||
# Presionar F12 para abrir Developer Tools
|
|
||||||
# Ver pestaña "Console"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 2: Abrir una Conversación
|
|
||||||
- Hacer click en cualquier conversación de la lista
|
|
||||||
- Verificar en consola que ves: `✅ SSE conectado (🔐 autenticado)`
|
|
||||||
|
|
||||||
### Paso 3: Enviar Mensaje de Prueba desde WhatsApp
|
|
||||||
**IMPORTANTE:** Envía el mensaje desde WhatsApp a tu número de bot
|
|
||||||
|
|
||||||
### Paso 4: Observar Logs en Consola
|
|
||||||
Deberías ver algo como:
|
|
||||||
```
|
|
||||||
📨 SSE new_message recibido: {...}
|
|
||||||
📨 Datos parseados del mensaje: {...}
|
|
||||||
👤 User ID del mensaje: 123 | Conversación actual: 123
|
|
||||||
♻️ Mensaje para conversación ACTIVA, procesando...
|
|
||||||
📍 Posición scroll: {scrollHeight: 1234, scrollTop: 1000, clientHeight: 600, distanceFromBottom: 34, isAtBottom: true}
|
|
||||||
✅ Usuario AL FINAL, recargando mensajes...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 5: Verificar Resultado
|
|
||||||
- El mensaje debe aparecer automáticamente (sin refrescar)
|
|
||||||
- Debe tener fondo amarillo claro durante 3 segundos
|
|
||||||
- Después el fondo vuelve a blanco
|
|
||||||
|
|
||||||
### Paso 6: Probar Scroll Arriba
|
|
||||||
1. Hacer scroll hacia arriba (leer mensajes viejos)
|
|
||||||
2. Enviar otro mensaje desde WhatsApp
|
|
||||||
3. Debe aparecer botón "Nuevo mensaje" en la parte inferior
|
|
||||||
4. Al hacer click, baja y muestra el mensaje
|
|
||||||
|
|
||||||
## 🐛 Debugging:
|
|
||||||
|
|
||||||
### Si no aparece el mensaje cuando estás abajo:
|
|
||||||
|
|
||||||
1. **Verificar SSE está conectado:**
|
|
||||||
```bash
|
|
||||||
# En otra terminal:
|
|
||||||
./test_sse_debug.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Verificar logs en consola:**
|
|
||||||
- Buscar "SSE new_message recibido"
|
|
||||||
- Buscar "Usuario AL FINAL"
|
|
||||||
- Buscar errores en rojo
|
|
||||||
|
|
||||||
3. **Verificar que el mensaje se guardó en BD:**
|
|
||||||
```bash
|
|
||||||
docker exec -it whatsapp-dev-app php -r "
|
|
||||||
require 'config/config.php';
|
|
||||||
\$db = Database::getInstance();
|
|
||||||
\$msgs = \$db->fetchAll('SELECT * FROM conversations ORDER BY created_at DESC LIMIT 5');
|
|
||||||
print_r(\$msgs);
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Verificar el SSE está enviando eventos:**
|
|
||||||
- Abrir http://localhost:8080/api/sse_events.php?token=demo_token directamente
|
|
||||||
- Deberías ver stream de eventos (connected, heartbeat cada 30s)
|
|
||||||
|
|
||||||
### Si el highlight no aparece:
|
|
||||||
|
|
||||||
1. **Verificar flag se está seteando:**
|
|
||||||
- Buscar en consola: "Usuario AL FINAL, recargando mensajes..."
|
|
||||||
- Abrir Inspector (Elementos) y buscar la clase `unread` en el mensaje
|
|
||||||
|
|
||||||
2. **Verificar CSS está cargado:**
|
|
||||||
- Buscar en Inspector `.message.unread .message-bubble`
|
|
||||||
- Debe tener `background: #fffbea`
|
|
||||||
|
|
||||||
## 📊 Métricas Esperadas:
|
|
||||||
|
|
||||||
- **Tiempo de aparición:** 3-5 segundos después de enviar mensaje
|
|
||||||
- **Duración del highlight:** 3 segundos
|
|
||||||
- **Intervalo de polling SSE:** Cada 3 segundos revisa BD
|
|
||||||
- **Heartbeat:** Cada 30 segundos
|
|
||||||
|
|
||||||
## ✨ Próximas Mejoras (Opcional):
|
|
||||||
|
|
||||||
1. Contador de mensajes no leídos en la lista de conversaciones
|
|
||||||
2. Marcar conversación como leída cuando abres
|
|
||||||
3. Notificación de escritura ("Usuario está escribiendo...")
|
|
||||||
4. Confirmación de entrega y lectura más precisa
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<?php session_start(); $_SESSION["user_id"] = 1; ?>
|
|
||||||
-133
@@ -1,133 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script de prueba para el sistema SSE
|
|
||||||
# Fecha: 27 de enero de 2026
|
|
||||||
|
|
||||||
echo "🧪 Testing SSE Real-time System"
|
|
||||||
echo "================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Colores para output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Base URL (cambiar según tu entorno)
|
|
||||||
BASE_URL="http://localhost"
|
|
||||||
|
|
||||||
echo "📝 Configuración:"
|
|
||||||
echo " Base URL: $BASE_URL"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 1: Verificar que sse_events.php es accesible
|
|
||||||
echo "1️⃣ Probando conexión SSE..."
|
|
||||||
timeout 3 curl -N -s "$BASE_URL/api/sse_events.php" 2>&1 | head -5 &
|
|
||||||
PID=$!
|
|
||||||
sleep 2
|
|
||||||
if kill -0 $PID 2>/dev/null; then
|
|
||||||
echo -e " ${GREEN}✓${NC} SSE endpoint responde"
|
|
||||||
kill $PID 2>/dev/null
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} SSE endpoint no responde"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 2: Verificar que push_event.php es accesible
|
|
||||||
echo "2️⃣ Probando push interno..."
|
|
||||||
RESPONSE=$(curl -s -X POST "$BASE_URL/api/push_event.php" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-Push-Token: internal_push_secret_2026" \
|
|
||||||
-d '{
|
|
||||||
"event_type": "test",
|
|
||||||
"data": {"message": "Test from script"},
|
|
||||||
"target_user_id": "global"
|
|
||||||
}')
|
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q "success"; then
|
|
||||||
echo -e " ${GREEN}✓${NC} Push endpoint funciona"
|
|
||||||
echo " Response: $RESPONSE"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Push endpoint falló"
|
|
||||||
echo " Response: $RESPONSE"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 3: Verificar permisos en directorio uploads
|
|
||||||
echo "3️⃣ Verificando permisos de escritura..."
|
|
||||||
UPLOADS_DIR="../uploads"
|
|
||||||
if [ -w "$UPLOADS_DIR" ]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} Directorio uploads tiene permisos de escritura"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Directorio uploads NO tiene permisos de escritura"
|
|
||||||
echo " Ejecuta: chmod 755 $UPLOADS_DIR"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 4: Simular evento de mensaje nuevo
|
|
||||||
echo "4️⃣ Simulando mensaje nuevo..."
|
|
||||||
PUSH_RESPONSE=$(curl -s -X POST "$BASE_URL/api/push_event.php" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-Push-Token: internal_push_secret_2026" \
|
|
||||||
-d '{
|
|
||||||
"event_type": "new_message",
|
|
||||||
"data": {
|
|
||||||
"user_id": 999,
|
|
||||||
"phone_number": "573001234567",
|
|
||||||
"name": "Usuario Prueba",
|
|
||||||
"message": "Mensaje de prueba desde script",
|
|
||||||
"message_type": "text",
|
|
||||||
"timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
|
||||||
},
|
|
||||||
"target_user_id": "global"
|
|
||||||
}')
|
|
||||||
|
|
||||||
if echo "$PUSH_RESPONSE" | grep -q "success"; then
|
|
||||||
EVENT_ID=$(echo "$PUSH_RESPONSE" | grep -o '"event_id":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
echo -e " ${GREEN}✓${NC} Evento enviado exitosamente"
|
|
||||||
echo " Event ID: $EVENT_ID"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Fallo al enviar evento"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 5: Verificar que el evento se guardó
|
|
||||||
echo "5️⃣ Verificando archivo de eventos..."
|
|
||||||
EVENT_FILE="../uploads/events_global.json"
|
|
||||||
if [ -f "$EVENT_FILE" ]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} Archivo de eventos existe"
|
|
||||||
echo " Últimos eventos guardados:"
|
|
||||||
cat "$EVENT_FILE" | python3 -m json.tool 2>/dev/null | tail -20 || cat "$EVENT_FILE"
|
|
||||||
else
|
|
||||||
echo -e " ${YELLOW}⚠${NC} Archivo de eventos no encontrado"
|
|
||||||
echo " Esto es normal si es la primera ejecución"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 6: Probar conexión SSE y recepción de evento
|
|
||||||
echo "6️⃣ Probando recepción de evento en SSE (5 segundos)..."
|
|
||||||
timeout 5 curl -N -s "$BASE_URL/api/sse_events.php" 2>&1 | while IFS= read -r line; do
|
|
||||||
if [[ "$line" == event:* ]]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} Evento recibido: $line"
|
|
||||||
elif [[ "$line" == data:* ]]; then
|
|
||||||
echo " Data: $line"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Resumen
|
|
||||||
echo "================================"
|
|
||||||
echo "✅ Pruebas completadas"
|
|
||||||
echo ""
|
|
||||||
echo "📌 Próximos pasos:"
|
|
||||||
echo " 1. Abre conversations.php en el navegador"
|
|
||||||
echo " 2. Abre DevTools → Console"
|
|
||||||
echo " 3. Deberías ver: '✅ SSE conectado'"
|
|
||||||
echo " 4. Envía un mensaje de prueba desde WhatsApp"
|
|
||||||
echo " 5. El mensaje debería aparecer en < 1 segundo"
|
|
||||||
echo ""
|
|
||||||
echo "🐛 Para debugging:"
|
|
||||||
echo " - Ver logs: tail -f /var/log/apache2/error.log"
|
|
||||||
echo " - Ver eventos: cat ../uploads/events_global.json"
|
|
||||||
echo " - Conectar manualmente: curl -N $BASE_URL/api/sse_events.php"
|
|
||||||
echo ""
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Diagnóstico Completo de SSE
|
|
||||||
* Prueba la conexión SSE y muestra todos los eventos recibidos
|
|
||||||
*/
|
|
||||||
|
|
||||||
echo "═══════════════════════════════════════════════════════\n";
|
|
||||||
echo " 🔍 DIAGNÓSTICO COMPLETO SSE\n";
|
|
||||||
echo "═══════════════════════════════════════════════════════\n\n";
|
|
||||||
|
|
||||||
// Configuración
|
|
||||||
$baseUrl = 'http://localhost:8000';
|
|
||||||
$token = 'demo_token';
|
|
||||||
$sseUrl = "{$baseUrl}/api/sse_events.php?token={$token}";
|
|
||||||
|
|
||||||
echo "📍 URL SSE: {$sseUrl}\n\n";
|
|
||||||
|
|
||||||
// Test 1: Verificar que el endpoint responde
|
|
||||||
echo "═══ Test 1: Verificar Headers ═══\n";
|
|
||||||
$ch = curl_init($sseUrl);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
|
|
||||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
echo "HTTP Code: {$httpCode}\n";
|
|
||||||
|
|
||||||
if (strpos($response, 'Content-Type: text/event-stream') !== false) {
|
|
||||||
echo "✅ Headers SSE correctos\n";
|
|
||||||
} else {
|
|
||||||
echo "❌ Headers SSE incorrectos\n";
|
|
||||||
echo "Headers recibidos:\n{$response}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n═══ Test 2: Conectar y Escuchar Eventos ═══\n";
|
|
||||||
echo "Esperando eventos (Ctrl+C para cancelar)...\n\n";
|
|
||||||
|
|
||||||
// Test 2: Conectar y leer stream
|
|
||||||
$ch = curl_init($sseUrl);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
|
|
||||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
|
||||||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
|
|
||||||
static $buffer = '';
|
|
||||||
static $eventCount = 0;
|
|
||||||
|
|
||||||
$buffer .= $data;
|
|
||||||
|
|
||||||
// Procesar líneas completas
|
|
||||||
while (($pos = strpos($buffer, "\n")) !== false) {
|
|
||||||
$line = substr($buffer, 0, $pos);
|
|
||||||
$buffer = substr($buffer, $pos + 1);
|
|
||||||
|
|
||||||
// Parsear línea SSE
|
|
||||||
if (empty(trim($line))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strpos($line, 'event:') === 0) {
|
|
||||||
$eventType = trim(substr($line, 6));
|
|
||||||
$eventCount++;
|
|
||||||
echo "[{$eventCount}] 📨 Evento: {$eventType}\n";
|
|
||||||
} elseif (strpos($line, 'data:') === 0) {
|
|
||||||
$eventData = trim(substr($line, 5));
|
|
||||||
$decoded = json_decode($eventData, true);
|
|
||||||
if ($decoded) {
|
|
||||||
echo " 📦 Data: " . json_encode($decoded, JSON_PRETTY_PRINT) . "\n";
|
|
||||||
} else {
|
|
||||||
echo " 📦 Data: {$eventData}\n";
|
|
||||||
}
|
|
||||||
} elseif (strpos($line, 'id:') === 0) {
|
|
||||||
$eventId = trim(substr($line, 3));
|
|
||||||
echo " 🆔 ID: {$eventId}\n";
|
|
||||||
} elseif (strpos($line, 'retry:') === 0) {
|
|
||||||
$retry = trim(substr($line, 6));
|
|
||||||
echo " 🔄 Retry: {$retry}ms\n";
|
|
||||||
} else {
|
|
||||||
echo " ℹ️ {$line}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return strlen($data);
|
|
||||||
});
|
|
||||||
|
|
||||||
echo "Conectando...\n";
|
|
||||||
|
|
||||||
$startTime = microtime(true);
|
|
||||||
$result = curl_exec($ch);
|
|
||||||
|
|
||||||
if ($result === false) {
|
|
||||||
$error = curl_error($ch);
|
|
||||||
$errno = curl_errno($ch);
|
|
||||||
echo "\n❌ Error de conexión:\n";
|
|
||||||
echo " Código: {$errno}\n";
|
|
||||||
echo " Mensaje: {$error}\n";
|
|
||||||
} else {
|
|
||||||
$elapsed = microtime(true) - $startTime;
|
|
||||||
echo "\n✅ Conexión cerrada después de {$elapsed}s\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
echo "HTTP Code final: {$httpCode}\n";
|
|
||||||
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
echo "\n═══════════════════════════════════════════════════════\n";
|
|
||||||
echo " ✅ Diagnóstico completado\n";
|
|
||||||
echo "═══════════════════════════════════════════════════════\n";
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script para probar conexión SSE y ver eventos en tiempo real
|
|
||||||
# Uso: ./test_sse_debug.sh
|
|
||||||
|
|
||||||
echo "🔍 Probando conexión SSE..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo ""
|
|
||||||
echo "Conectando a: http://localhost:8080/api/sse_events.php?token=demo_token"
|
|
||||||
echo ""
|
|
||||||
echo "📡 Eventos recibidos (presiona Ctrl+C para salir):"
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Conectar al SSE y mostrar eventos con timestamp
|
|
||||||
curl -N -H "Accept: text/event-stream" \
|
|
||||||
"http://localhost:8080/api/sse_events.php?token=demo_token&t=$(date +%s)" 2>&1 | \
|
|
||||||
while IFS= read -r line; do
|
|
||||||
timestamp=$(date '+%H:%M:%S')
|
|
||||||
if [[ "$line" == event:* ]]; then
|
|
||||||
echo -e "\n\033[1;36m[$timestamp]\033[0m \033[1;33m${line}\033[0m"
|
|
||||||
elif [[ "$line" == data:* ]]; then
|
|
||||||
echo -e "\033[0;32m${line}\033[0m"
|
|
||||||
elif [[ "$line" == id:* ]]; then
|
|
||||||
echo -e "\033[0;90m${line}\033[0m"
|
|
||||||
else
|
|
||||||
[[ -n "$line" ]] && echo "$line"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'config/config.php';
|
|
||||||
require_once 'classes/Database.php';
|
|
||||||
|
|
||||||
$db = Database::getInstance();
|
|
||||||
$result = $db->getConnection()->query('DESCRIBE media_files');
|
|
||||||
|
|
||||||
echo "📋 Tabla media_files creada exitosamente\n\n";
|
|
||||||
echo "Estructura de la tabla:\n";
|
|
||||||
echo str_repeat('-', 90) . "\n";
|
|
||||||
printf("%-20s %-30s %-10s %-10s %-15s\n", 'Campo', 'Tipo', 'Null', 'Key', 'Extra');
|
|
||||||
echo str_repeat('-', 90) . "\n";
|
|
||||||
|
|
||||||
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
|
|
||||||
printf("%-20s %-30s %-10s %-10s %-15s\n",
|
|
||||||
$row['Field'],
|
|
||||||
$row['Type'],
|
|
||||||
$row['Null'],
|
|
||||||
$row['Key'],
|
|
||||||
$row['Extra']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo str_repeat('-', 90) . "\n";
|
|
||||||
echo "\n✅ Sistema multimedia listo para usar\n";
|
|
||||||
echo "\n📝 Puedes ahora:\n";
|
|
||||||
echo " 1. Abrir conversations.php\n";
|
|
||||||
echo " 2. Seleccionar una conversación\n";
|
|
||||||
echo " 3. Hacer clic en el botón 📎 para adjuntar archivos\n";
|
|
||||||
echo " 4. Enviar imágenes, videos, audios o documentos\n\n";
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script para verificar la corrección del error de autenticación SSE
|
|
||||||
# Fecha: 27 de enero de 2026
|
|
||||||
|
|
||||||
echo "🔧 Verificando corrección de SSE..."
|
|
||||||
echo "===================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
# Probar conexión SSE sin token
|
|
||||||
echo "1️⃣ Probando SSE sin token (modo global)..."
|
|
||||||
RESPONSE=$(timeout 2 curl -s -N "http://localhost/api/sse_events.php" 2>&1 | head -3)
|
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q "event: connected"; then
|
|
||||||
echo -e " ${GREEN}✓${NC} SSE conecta en modo global"
|
|
||||||
echo "$RESPONSE" | grep -A 1 "event: connected"
|
|
||||||
else
|
|
||||||
echo -e " ${YELLOW}⚠${NC} SSE puede requerir configuración adicional"
|
|
||||||
echo " Response: $RESPONSE"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Probar conexión SSE con token
|
|
||||||
echo "2️⃣ Probando SSE con token..."
|
|
||||||
RESPONSE=$(timeout 2 curl -s -N "http://localhost/api/sse_events.php?token=demo_token" 2>&1 | head -3)
|
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q "event: connected"; then
|
|
||||||
echo -e " ${GREEN}✓${NC} SSE conecta con token"
|
|
||||||
echo "$RESPONSE" | grep -A 1 "event: connected"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Error al conectar con token"
|
|
||||||
echo " Response: $RESPONSE"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Verificar que el cliente use el token correcto
|
|
||||||
echo "3️⃣ Verificando código cliente..."
|
|
||||||
if grep -q "token=demo_token" ../conversations.php 2>/dev/null; then
|
|
||||||
echo -e " ${GREEN}✓${NC} Cliente configurado para usar token"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Cliente no configurado correctamente"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Simular evento y verificar recepción
|
|
||||||
echo "4️⃣ Simulando evento de prueba..."
|
|
||||||
PUSH_RESPONSE=$(curl -s -X POST "http://localhost/api/push_event.php" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-Push-Token: internal_push_secret_2026" \
|
|
||||||
-d '{
|
|
||||||
"event_type": "test_connection",
|
|
||||||
"data": {"message": "Prueba de conexión corregida"},
|
|
||||||
"target_user_id": "global"
|
|
||||||
}')
|
|
||||||
|
|
||||||
if echo "$PUSH_RESPONSE" | grep -q "success"; then
|
|
||||||
echo -e " ${GREEN}✓${NC} Evento enviado correctamente"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Error al enviar evento"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "===================================="
|
|
||||||
echo "✅ Verificación completa"
|
|
||||||
echo ""
|
|
||||||
echo "📌 Próximos pasos:"
|
|
||||||
echo " 1. Recarga conversations.php en tu navegador"
|
|
||||||
echo " 2. Abre DevTools → Console"
|
|
||||||
echo " 3. Deberías ver: '✅ SSE conectado (🌐 global)' o '✅ SSE conectado (🔐 autenticado)'"
|
|
||||||
echo " 4. Ya NO deberías ver el error 'Usuario no autenticado'"
|
|
||||||
echo ""
|
|
||||||
echo "🎉 El error ha sido corregido!"
|
|
||||||
echo ""
|
|
||||||
Reference in New Issue
Block a user