Files
whatsapp/assets/js/app.js
T
2025-11-18 20:26:36 -05:00

912 lines
29 KiB
JavaScript

/**
* WhatsApp Bot Manager - JavaScript
* Fecha: 13 de noviembre de 2025
*/
class WhatsAppBotManager {
constructor() {
this.apiBaseUrl = './api/';
this.currentTab = 'dashboard';
this.currentMenuId = null;
this.charts = {};
this.init();
}
init() {
this.setupEventListeners();
this.loadDashboard();
this.setupCharts();
// Auto-refresh cada 30 segundos
setInterval(() => {
if (this.currentTab === 'dashboard') {
this.refreshStats();
}
}, 30000);
}
setupEventListeners() {
// Navegación de tabs
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const tabName = link.getAttribute('data-tab');
this.showTab(tabName);
});
});
// Formularios
this.setupFormListeners();
// Búsquedas
this.setupSearchListeners();
}
setupFormListeners() {
// Formulario de configuración
const settingsForm = document.getElementById('settings-form');
if (settingsForm) {
settingsForm.addEventListener('submit', (e) => {
e.preventDefault();
this.saveSettings();
});
}
// Formulario de menú
const menuForm = document.getElementById('menu-form');
if (menuForm) {
menuForm.addEventListener('submit', (e) => {
e.preventDefault();
this.saveMenu();
});
}
// Formulario de envío de mensaje
const sendMessageForm = document.getElementById('send-message-form');
if (sendMessageForm) {
sendMessageForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendMessage();
});
}
// Formulario de mensaje masivo
const broadcastForm = document.getElementById('broadcast-form');
if (broadcastForm) {
broadcastForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendBroadcast();
});
}
// Cambio de tipo de mensaje
const messageType = document.getElementById('message-type');
if (messageType) {
messageType.addEventListener('change', () => {
this.toggleMessageFields();
});
}
}
setupSearchListeners() {
// Búsqueda de conversaciones
const searchConversations = document.getElementById('search-conversations');
if (searchConversations) {
searchConversations.addEventListener('input', (e) => {
this.searchConversations(e.target.value);
});
}
}
showTab(tabName) {
// Ocultar todas las pestañas
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Remover clase active de navegación
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
// Mostrar pestaña seleccionada
const targetTab = document.getElementById(tabName);
if (targetTab) {
targetTab.classList.add('active');
}
// Activar enlace de navegación
const activeLink = document.querySelector(`[data-tab="${tabName}"]`);
if (activeLink) {
activeLink.classList.add('active');
}
this.currentTab = tabName;
// Cargar datos específicos del tab
this.loadTabData(tabName);
}
loadTabData(tabName) {
switch (tabName) {
case 'dashboard':
this.loadDashboard();
break;
case 'conversations':
this.loadConversations();
break;
case 'users':
this.loadUsers();
break;
case 'menus':
this.loadMenus();
break;
case 'messages':
this.loadMessageUsers();
break;
case 'templates':
this.loadTemplates();
break;
case 'autoresponses':
this.loadAutoResponses();
break;
case 'settings':
this.loadSettings();
break;
case 'logs':
this.loadLogs();
break;
}
}
async loadDashboard() {
try {
const stats = await this.apiCall('get_stats.php');
this.updateStats(stats);
const recentMessages = await this.apiCall('get_recent_messages.php');
this.updateRecentMessages(recentMessages);
this.updateChart();
} catch (error) {
this.showError('Error cargando dashboard: ' + error.message);
}
}
updateStats(stats) {
if (stats) {
document.getElementById('total-users').textContent = stats.total_users || 0;
document.getElementById('messages-today').textContent = stats.messages_today || 0;
document.getElementById('active-users').textContent = stats.active_users || 0;
document.getElementById('total-messages').textContent = stats.total_messages || 0;
}
}
updateRecentMessages(messages) {
const container = document.getElementById('recent-messages');
if (!container) return;
container.innerHTML = '';
if (messages && messages.length > 0) {
messages.forEach(message => {
const messageElement = this.createRecentMessageElement(message);
container.appendChild(messageElement);
});
} else {
container.innerHTML = '<div class="text-center text-muted">No hay mensajes recientes</div>';
}
}
createRecentMessageElement(message) {
const div = document.createElement('div');
div.className = 'recent-message';
const direction = message.direction === 'incoming' ? '📥' : '📤';
const time = new Date(message.created_at).toLocaleString();
div.innerHTML = `
<div class="d-flex justify-content-between">
<span class="fw-bold">${direction} ${message.phone_number}</span>
<span class="time">${time}</span>
</div>
<div class="content">${this.truncateText(message.content, 50)}</div>
`;
return div;
}
setupCharts() {
const ctx = document.getElementById('messagesChart');
if (ctx) {
this.charts.messages = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Mensajes',
data: [],
borderColor: '#25d366',
backgroundColor: 'rgba(37, 211, 102, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0,0,0,0.1)'
}
},
x: {
grid: {
color: 'rgba(0,0,0,0.1)'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
async updateChart() {
try {
const chartData = await this.apiCall('get_chart_data.php');
if (chartData && this.charts.messages) {
this.charts.messages.data.labels = chartData.labels;
this.charts.messages.data.datasets[0].data = chartData.data;
this.charts.messages.update();
}
} catch (error) {
console.error('Error updating chart:', error);
}
}
async loadConversations() {
try {
const conversations = await this.apiCall('get_conversations.php');
this.updateConversationsTable(conversations);
} catch (error) {
this.showError('Error cargando conversaciones: ' + error.message);
}
}
updateConversationsTable(conversations) {
const tbody = document.getElementById('conversations-table');
if (!tbody) return;
tbody.innerHTML = '';
conversations.forEach(conv => {
const row = this.createConversationRow(conv);
tbody.appendChild(row);
});
}
createConversationRow(conv) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<div class="fw-bold">${conv.phone_number}</div>
<small class="text-muted">${conv.name || 'Sin nombre'}</small>
</td>
<td>${this.truncateText(conv.last_message, 50)}</td>
<td>
<span class="badge badge-${this.getMessageTypeColor(conv.message_type)}">
${conv.message_type}
</span>
</td>
<td>
<span class="badge badge-${this.getStatusColor(conv.status)}">
${conv.status}
</span>
</td>
<td>${new Date(conv.last_activity).toLocaleString()}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="app.viewConversation('${conv.user_id}')">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-success" onclick="app.replyToUser('${conv.phone_number}')">
<i class="fas fa-reply"></i>
</button>
</td>
`;
return tr;
}
async loadUsers() {
try {
const users = await this.apiCall('get_users.php');
this.updateUsersTable(users);
} catch (error) {
this.showError('Error cargando usuarios: ' + error.message);
}
}
updateUsersTable(users) {
const tbody = document.getElementById('users-table');
if (!tbody) return;
tbody.innerHTML = '';
users.forEach(user => {
const row = this.createUserRow(user);
tbody.appendChild(row);
});
}
createUserRow(user) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${user.id}</td>
<td>${user.phone_number}</td>
<td>${user.name || 'Sin nombre'}</td>
<td>
<span class="badge badge-${this.getStatusColor(user.status)}">
${user.status}
</span>
</td>
<td>${user.current_menu || 'Ninguno'}</td>
<td>${new Date(user.created_at).toLocaleString()}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="app.editUser(${user.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-warning" onclick="app.blockUser(${user.id})">
<i class="fas fa-ban"></i>
</button>
</td>
`;
return tr;
}
async loadMenus() {
try {
const menus = await this.apiCall('get_menus.php');
this.updateMenusTree(menus);
this.loadMenuParents(menus);
} catch (error) {
this.showError('Error cargando menús: ' + error.message);
}
}
updateMenusTree(menus) {
const container = document.getElementById('menus-tree');
if (!container) return;
container.innerHTML = '';
// Organizar menús por jerarquía
const rootMenus = menus.filter(menu => menu.is_root);
rootMenus.forEach(menu => {
const menuElement = this.createMenuElement(menu, menus);
container.appendChild(menuElement);
});
}
createMenuElement(menu, allMenus) {
const div = document.createElement('div');
div.className = 'menu-item ' + (menu.is_root ? 'root' : 'child');
div.innerHTML = `
<div>
<h6 class="mb-1">${menu.title}</h6>
<small class="text-muted">${menu.description || 'Sin descripción'}</small>
<div class="mt-2">
<span class="badge badge-${menu.is_active ? 'success' : 'secondary'}">
${menu.is_active ? 'Activo' : 'Inactivo'}
</span>
</div>
</div>
<div>
<button class="btn btn-sm btn-primary me-2" onclick="app.editMenu(${menu.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-info me-2" onclick="app.manageMenuOptions(${menu.id})">
<i class="fas fa-list"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="app.deleteMenu(${menu.id})">
<i class="fas fa-trash"></i>
</button>
</div>
`;
// Agregar submenús
const childMenus = allMenus.filter(child => child.parent_id === menu.id);
if (childMenus.length > 0) {
const childContainer = document.createElement('div');
childContainer.style.marginLeft = '20px';
childContainer.style.marginTop = '10px';
childMenus.forEach(child => {
const childElement = this.createMenuElement(child, allMenus);
childContainer.appendChild(childElement);
});
div.appendChild(childContainer);
}
return div;
}
loadMenuParents(menus) {
const select = document.getElementById('menu-parent');
if (!select) return;
select.innerHTML = '<option value="">Sin padre (Menú raíz)</option>';
menus.forEach(menu => {
const option = document.createElement('option');
option.value = menu.id;
option.textContent = menu.title;
select.appendChild(option);
});
}
async loadMessageUsers() {
try {
const users = await this.apiCall('get_users.php');
const templates = await this.apiCall('get_templates.php');
this.updateMessageUserSelect(users);
this.updateTemplateSelect(templates);
} catch (error) {
this.showError('Error cargando datos de mensaje: ' + error.message);
}
}
updateMessageUserSelect(users) {
const select = document.getElementById('message-recipient');
if (!select) return;
select.innerHTML = '<option value="">Seleccionar usuario...</option>';
users.forEach(user => {
const option = document.createElement('option');
option.value = user.phone_number;
option.textContent = `${user.phone_number} ${user.name ? '(' + user.name + ')' : ''}`;
select.appendChild(option);
});
}
updateTemplateSelect(templates) {
const select = document.getElementById('message-template');
if (!select) return;
select.innerHTML = '<option value="">Seleccionar plantilla...</option>';
templates.forEach(template => {
if (template.status === 'approved') {
const option = document.createElement('option');
option.value = template.template_name;
option.textContent = template.name;
select.appendChild(option);
}
});
}
toggleMessageFields() {
const messageType = document.getElementById('message-type').value;
const textGroup = document.getElementById('message-text-group');
const templateGroup = document.getElementById('template-group');
if (messageType === 'template') {
textGroup.style.display = 'none';
templateGroup.style.display = 'block';
} else {
textGroup.style.display = 'block';
templateGroup.style.display = 'none';
}
}
async sendMessage() {
const recipient = document.getElementById('message-recipient').value;
const messageType = document.getElementById('message-type').value;
const messageText = document.getElementById('message-text').value;
const template = document.getElementById('message-template').value;
if (!recipient) {
this.showError('Por favor selecciona un destinatario');
return;
}
const data = {
recipient: recipient,
type: messageType
};
if (messageType === 'text') {
if (!messageText) {
this.showError('Por favor escribe un mensaje');
return;
}
data.message = messageText;
} else if (messageType === 'template') {
if (!template) {
this.showError('Por favor selecciona una plantilla');
return;
}
data.template = template;
}
try {
this.showLoading('Enviando mensaje...');
const result = await this.apiCall('send_message.php', 'POST', data);
if (result.success) {
this.showSuccess('Mensaje enviado correctamente');
document.getElementById('send-message-form').reset();
} else {
this.showError(result.error || 'Error enviando mensaje');
}
} catch (error) {
this.showError('Error enviando mensaje: ' + error.message);
} finally {
this.hideLoading();
}
}
async sendBroadcast() {
const filter = document.getElementById('broadcast-filter').value;
const message = document.getElementById('broadcast-message').value;
if (!message) {
this.showError('Por favor escribe un mensaje');
return;
}
if (!confirm('¿Estás seguro de enviar este mensaje a múltiples usuarios?')) {
return;
}
try {
this.showLoading('Enviando mensajes...');
const result = await this.apiCall('send_broadcast.php', 'POST', {
filter: filter,
message: message
});
if (result.success) {
this.showSuccess(`Mensajes enviados a ${result.sent_count} usuarios`);
document.getElementById('broadcast-form').reset();
} else {
this.showError(result.error || 'Error en envío masivo');
}
} catch (error) {
this.showError('Error en envío masivo: ' + error.message);
} finally {
this.hideLoading();
}
}
async saveMenu() {
const formData = {
name: document.getElementById('menu-name').value,
title: document.getElementById('menu-title').value,
description: document.getElementById('menu-description').value,
parent_id: document.getElementById('menu-parent').value || null,
is_active: document.getElementById('menu-active').checked ? 1 : 0
};
try {
this.showLoading('Guardando menú...');
const result = await this.apiCall('save_menu.php', 'POST', formData);
if (result.success) {
this.showSuccess('Menú guardado correctamente');
document.getElementById('menu-form').reset();
this.loadMenus();
} else {
this.showError(result.error || 'Error guardando menú');
}
} catch (error) {
this.showError('Error guardando menú: ' + error.message);
} finally {
this.hideLoading();
}
}
async saveSettings() {
const formData = {
whatsapp_token: document.getElementById('whatsapp-token').value,
phone_number_id: document.getElementById('phone-number-id').value,
webhook_token: document.getElementById('webhook-token').value,
business_name: document.getElementById('business-name').value,
welcome_message: document.getElementById('welcome-message').value
};
try {
this.showLoading('Guardando configuración...');
const result = await this.apiCall('save_settings.php', 'POST', formData);
if (result.success) {
this.showSuccess('Configuración guardada correctamente');
} else {
this.showError(result.error || 'Error guardando configuración');
}
} catch (error) {
this.showError('Error guardando configuración: ' + error.message);
} finally {
this.hideLoading();
}
}
async loadSettings() {
try {
const settings = await this.apiCall('get_settings.php');
if (settings) {
document.getElementById('whatsapp-token').value = settings.whatsapp_token || '';
document.getElementById('phone-number-id').value = settings.phone_number_id || '';
document.getElementById('webhook-token').value = settings.webhook_token || '';
document.getElementById('business-name').value = settings.business_name || '';
document.getElementById('welcome-message').value = settings.welcome_message || '';
}
} catch (error) {
this.showError('Error cargando configuración: ' + error.message);
}
}
async loadLogs() {
try {
const logs = await this.apiCall('get_logs.php');
this.updateLogsTable(logs);
} catch (error) {
this.showError('Error cargando logs: ' + error.message);
}
}
updateLogsTable(logs) {
const tbody = document.getElementById('logs-table');
if (!tbody) return;
tbody.innerHTML = '';
logs.forEach(log => {
const row = this.createLogRow(log);
tbody.appendChild(row);
});
}
createLogRow(log) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${new Date(log.created_at).toLocaleString()}</td>
<td>${log.ip_address}</td>
<td>
<span class="badge badge-${log.status_code === 200 ? 'success' : 'danger'}">
${log.status_code}
</span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="app.viewLogDetails('${log.id}', 'request')">
Ver Request
</button>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="app.viewLogDetails('${log.id}', 'response')">
Ver Response
</button>
</td>
`;
return tr;
}
// Utility methods
async apiCall(endpoint, method = 'GET', data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
if (data && method !== 'GET') {
options.body = JSON.stringify(data);
}
const response = await fetch(this.apiBaseUrl + endpoint, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
showError(message) {
this.showNotification(message, 'error');
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showWarning(message) {
this.showNotification(message, 'warning');
}
showInfo(message) {
this.showNotification(message, 'info');
}
showNotification(message, type = 'info') {
// Crear notificación toast
const toast = document.createElement('div');
toast.className = `alert alert-${type} position-fixed`;
toast.style.top = '20px';
toast.style.right = '20px';
toast.style.zIndex = '9999';
toast.style.minWidth = '300px';
toast.innerHTML = `
<i class="fas fa-${this.getNotificationIcon(type)}"></i>
${message}
<button type="button" class="btn-close" onclick="this.parentElement.remove()"></button>
`;
document.body.appendChild(toast);
// Auto-remove después de 5 segundos
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 5000);
}
getNotificationIcon(type) {
const icons = {
success: 'check-circle',
error: 'exclamation-circle',
warning: 'exclamation-triangle',
info: 'info-circle'
};
return icons[type] || 'info-circle';
}
showLoading(message = 'Cargando...') {
const loading = document.createElement('div');
loading.id = 'loading-overlay';
loading.className = 'position-fixed w-100 h-100 d-flex align-items-center justify-content-center';
loading.style.top = '0';
loading.style.left = '0';
loading.style.backgroundColor = 'rgba(0,0,0,0.5)';
loading.style.zIndex = '9999';
loading.innerHTML = `
<div class="bg-white p-4 rounded text-center">
<div class="spinner mb-3"></div>
<div>${message}</div>
</div>
`;
document.body.appendChild(loading);
}
hideLoading() {
const loading = document.getElementById('loading-overlay');
if (loading) {
loading.remove();
}
}
truncateText(text, length) {
if (!text) return '';
return text.length > length ? text.substring(0, length) + '...' : text;
}
getStatusColor(status) {
const colors = {
'active': 'success',
'inactive': 'secondary',
'blocked': 'danger',
'sent': 'info',
'delivered': 'success',
'read': 'success',
'failed': 'danger'
};
return colors[status] || 'secondary';
}
getMessageTypeColor(type) {
const colors = {
'text': 'primary',
'image': 'info',
'audio': 'warning',
'video': 'danger',
'document': 'secondary',
'template': 'success'
};
return colors[type] || 'primary';
}
// Funciones específicas
refreshData() {
this.loadTabData(this.currentTab);
this.showSuccess('Datos actualizados');
}
refreshStats() {
this.loadDashboard();
}
exportUsers() {
window.open(this.apiBaseUrl + 'export_users.php', '_blank');
}
viewConversation(userId) {
// Implementar modal o página de conversación
console.log('Ver conversación del usuario:', userId);
}
replyToUser(phoneNumber) {
document.getElementById('message-recipient').value = phoneNumber;
this.showTab('messages');
}
editUser(userId) {
// Implementar modal de edición de usuario
console.log('Editar usuario:', userId);
}
blockUser(userId) {
if (confirm('¿Estás seguro de bloquear este usuario?')) {
// Implementar bloqueo de usuario
console.log('Bloquear usuario:', userId);
}
}
editMenu(menuId) {
this.currentMenuId = menuId;
// Cargar datos del menú en el formulario
console.log('Editar menú:', menuId);
}
deleteMenu(menuId) {
if (confirm('¿Estás seguro de eliminar este menú?')) {
// Implementar eliminación
console.log('Eliminar menú:', menuId);
}
}
manageMenuOptions(menuId) {
// Mostrar modal de opciones de menú
console.log('Gestionar opciones del menú:', menuId);
}
viewLogDetails(logId, type) {
// Mostrar detalles del log en modal
console.log('Ver detalles del log:', logId, type);
}
clearLogs() {
if (confirm('¿Estás seguro de eliminar todos los logs?')) {
// Implementar limpieza de logs
console.log('Limpiar logs');
}
}
refreshLogs() {
this.loadLogs();
}
}
// Inicializar la aplicación
const app = new WhatsAppBotManager();
// Funciones globales
window.refreshData = () => app.refreshData();
window.exportUsers = () => app.exportUsers();
window.refreshLogs = () => app.refreshLogs();
window.clearLogs = () => app.clearLogs();