This commit is contained in:
Lizandro Guarnizo
2026-01-12 11:06:43 -05:00
parent ce12ac2cce
commit dd36ed6fe0
110 changed files with 20873 additions and 696 deletions
+420 -27
View File
@@ -24,15 +24,22 @@ class WhatsAppBotManager {
this.refreshStats();
}
}, 30000);
// Verificar sesión cada 5 minutos
setInterval(() => {
this.checkSession();
}, 300000); // 5 minutos
}
setupEventListeners() {
// Navegación de tabs
document.querySelectorAll('.nav-link').forEach(link => {
// Navegación de tabs - solo enlaces que tengan data-tab
document.querySelectorAll('.nav-link[data-tab]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const tabName = link.getAttribute('data-tab');
this.showTab(tabName);
if (tabName) {
this.showTab(tabName);
}
});
});
@@ -165,8 +172,13 @@ class WhatsAppBotManager {
const stats = await this.apiCall('get_stats.php');
this.updateStats(stats);
const recentMessages = await this.apiCall('get_recent_messages.php');
this.updateRecentMessages(recentMessages);
const recentMessagesResponse = await this.apiCall('get_recent_messages.php');
if (recentMessagesResponse && recentMessagesResponse.success && Array.isArray(recentMessagesResponse.data)) {
this.updateRecentMessages(recentMessagesResponse.data);
} else if (recentMessagesResponse && Array.isArray(recentMessagesResponse)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateRecentMessages(recentMessagesResponse);
}
this.updateChart();
} catch (error) {
@@ -275,9 +287,20 @@ class WhatsAppBotManager {
async loadConversations() {
try {
const conversations = await this.apiCall('get_conversations.php');
this.updateConversationsTable(conversations);
const response = await this.apiCall('get_conversations.php');
console.log('Respuesta get_conversations:', response); // Debug
if (response && response.success && Array.isArray(response.data)) {
this.updateConversationsTable(response.data);
} else if (response && Array.isArray(response)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateConversationsTable(response);
} else {
console.error('Formato de respuesta inesperado:', response);
this.showError('Error: formato de datos inválido para conversaciones');
}
} catch (error) {
console.error('Error en loadConversations:', error);
this.showError('Error cargando conversaciones: ' + error.message);
}
}
@@ -288,6 +311,19 @@ class WhatsAppBotManager {
tbody.innerHTML = '';
console.log('Datos para tabla de conversaciones:', conversations); // Debug
if (!Array.isArray(conversations)) {
console.error('conversations no es un array:', typeof conversations, conversations);
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">Error: datos inválidos</td></tr>';
return;
}
if (conversations.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No hay conversaciones</td></tr>';
return;
}
conversations.forEach(conv => {
const row = this.createConversationRow(conv);
tbody.appendChild(row);
@@ -327,8 +363,15 @@ class WhatsAppBotManager {
async loadUsers() {
try {
const users = await this.apiCall('get_users.php');
this.updateUsersTable(users);
const response = await this.apiCall('get_users.php');
if (response && response.success && Array.isArray(response.data)) {
this.updateUsersTable(response.data);
} else if (response && Array.isArray(response)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateUsersTable(response);
} else {
this.showError('Error: formato de datos inválido para usuarios');
}
} catch (error) {
this.showError('Error cargando usuarios: ' + error.message);
}
@@ -373,7 +416,14 @@ class WhatsAppBotManager {
async loadMenus() {
try {
const menus = await this.apiCall('get_menus.php');
const response = await this.apiCall('get_menus.php');
let menus = response;
if (response && response.success && Array.isArray(response.data)) {
menus = response.data;
} else if (!Array.isArray(response)) {
this.showError('Error: formato de datos inválido para menús');
return;
}
this.updateMenusTree(menus);
this.loadMenuParents(menus);
} catch (error) {
@@ -457,8 +507,18 @@ class WhatsAppBotManager {
async loadMessageUsers() {
try {
const users = await this.apiCall('get_users.php');
const templates = await this.apiCall('get_templates.php');
const usersResponse = await this.apiCall('get_users.php');
const templatesResponse = await this.apiCall('get_templates.php');
let users = usersResponse;
if (usersResponse && usersResponse.success && Array.isArray(usersResponse.data)) {
users = usersResponse.data;
}
let templates = templatesResponse;
if (templatesResponse && templatesResponse.success && Array.isArray(templatesResponse.data)) {
templates = templatesResponse.data;
}
this.updateMessageUserSelect(users);
this.updateTemplateSelect(templates);
@@ -512,16 +572,39 @@ class WhatsAppBotManager {
}
async sendMessage() {
const recipient = document.getElementById('message-recipient').value;
const recipientType = document.getElementById('recipient-type').value;
const messageType = document.getElementById('message-type').value;
let recipient;
if (recipientType === 'existing') {
recipient = document.getElementById('message-recipient').value;
} else {
recipient = document.getElementById('manual-recipient').value.trim();
// Para números manuales, solo permitir plantillas
if (messageType !== 'template') {
this.showError('Para números manuales solo se pueden enviar plantillas aprobadas');
return;
}
}
const messageText = document.getElementById('message-text').value;
const template = document.getElementById('message-template').value;
if (!recipient) {
this.showError('Por favor selecciona un destinatario');
this.showError('Por favor ingresa un destinatario');
return;
}
// Validar formato de número manual
if (recipientType === 'manual') {
const phoneRegex = /^\+\d{10,15}$/;
if (!phoneRegex.test(recipient)) {
this.showError('El número debe tener el formato: +57XXXXXXXXX (10-15 dígitos)');
return;
}
}
const data = {
recipient: recipient,
type: messageType
@@ -548,6 +631,30 @@ class WhatsAppBotManager {
if (result.success) {
this.showSuccess('Mensaje enviado correctamente');
document.getElementById('send-message-form').reset();
// Resetear la interfaz
this.resetMessageForm();
// Actualizar conversaciones si es necesario
if (this.currentTab === 'conversations') {
this.loadConversations();
}
} else {
this.showError(result.error || 'Error enviando mensaje');
}
} catch (error) {
this.showError('Error enviando mensaje: ' + error.message);
} finally {
this.hideLoading();
}
}
resetMessageForm() {
document.getElementById('recipient-type').value = 'existing';
document.getElementById('message-type').value = 'text';
toggleRecipientType();
toggleMessageType();
}
} else {
this.showError(result.error || 'Error enviando mensaje');
}
@@ -661,8 +768,15 @@ class WhatsAppBotManager {
async loadLogs() {
try {
const logs = await this.apiCall('get_logs.php');
this.updateLogsTable(logs);
const response = await this.apiCall('get_logs.php');
if (response && response.success && Array.isArray(response.data)) {
this.updateLogsTable(response.data);
} else if (response && Array.isArray(response)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateLogsTable(response);
} else {
this.showError('Error: formato de datos inválido para logs');
}
} catch (error) {
this.showError('Error cargando logs: ' + error.message);
}
@@ -710,6 +824,7 @@ class WhatsAppBotManager {
method: method,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' // Para identificar peticiones AJAX
}
};
@@ -717,13 +832,51 @@ class WhatsAppBotManager {
options.body = JSON.stringify(data);
}
const response = await fetch(this.apiBaseUrl + endpoint, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
try {
const response = await fetch(this.apiBaseUrl + endpoint, options);
// Verificar si es error de autenticación (401)
if (response.status === 401) {
try {
const errorData = await response.json();
if (errorData.redirect) {
this.showError(errorData.error || 'Sesión expirada. Redirigiendo al login...');
setTimeout(() => {
window.location.href = errorData.redirect;
}, 2000);
return null;
}
} catch (e) {
// Si no puede parsear JSON, redirigir al login
this.showError('Sesión expirada. Redirigiendo al login...');
setTimeout(() => {
window.location.href = 'login.php';
}, 2000);
return null;
}
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
const result = await response.json();
// Verificar si la respuesta indica problemas de autenticación
if (result.success === false && result.redirect) {
this.showError(result.error || 'Sesión expirada. Redirigiendo al login...');
setTimeout(() => {
window.location.href = result.redirect;
}, 2000);
return null;
}
return result;
} catch (error) {
console.error('Error en apiCall:', error);
throw error;
}
}
showError(message) {
@@ -832,6 +985,26 @@ class WhatsAppBotManager {
}
// Funciones específicas
searchConversations(query) {
// Función de búsqueda de conversaciones
const tbody = document.getElementById('conversations-table');
if (!tbody) return;
const rows = tbody.getElementsByTagName('tr');
const searchTerm = query.toLowerCase();
for (let row of rows) {
const phoneNumber = row.cells[0]?.textContent?.toLowerCase() || '';
const lastMessage = row.cells[1]?.textContent?.toLowerCase() || '';
if (phoneNumber.includes(searchTerm) || lastMessage.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
}
refreshData() {
this.loadTabData(this.currentTab);
this.showSuccess('Datos actualizados');
@@ -900,13 +1073,233 @@ class WhatsAppBotManager {
refreshLogs() {
this.loadLogs();
}
async loadTemplates() {
try {
const response = await this.apiCall('get_templates.php');
if (response && response.success && Array.isArray(response.data)) {
this.updateTemplatesTable(response.data);
} else if (response && Array.isArray(response)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateTemplatesTable(response);
} else {
this.showError('Error: formato de datos inválido para plantillas');
}
} catch (error) {
this.showError('Error cargando plantillas: ' + error.message);
}
}
updateTemplatesTable(templates) {
const tbody = document.getElementById('templates-table');
if (!tbody) return;
tbody.innerHTML = '';
if (templates && templates.length > 0) {
templates.forEach(template => {
const row = this.createTemplateRow(template);
tbody.appendChild(row);
});
} else {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No hay plantillas registradas</td></tr>';
}
}
createTemplateRow(template) {
const tr = document.createElement('tr');
const statusBadge = this.getStatusBadge(template.status);
tr.innerHTML = `
<td>${template.name}</td>
<td><code>${template.template_name}</code></td>
<td>${template.language_code.toUpperCase()}</td>
<td>
<span class="badge bg-secondary">${template.category}</span>
</td>
<td>${statusBadge}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="app.editTemplate(${template.id})" title="Editar">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger" onclick="app.deleteTemplate(${template.id})" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
return tr;
}
getStatusBadge(status) {
const badges = {
'approved': '<span class="badge bg-success">Aprobada</span>',
'pending': '<span class="badge bg-warning">Pendiente</span>',
'rejected': '<span class="badge bg-danger">Rechazada</span>'
};
return badges[status] || '<span class="badge bg-secondary">Desconocido</span>';
}
async saveTemplate() {
const formData = {
name: document.getElementById('template-name').value.trim(),
template_name: document.getElementById('template-whatsapp-name').value.trim(),
language_code: document.getElementById('template-language').value,
category: document.getElementById('template-category').value
};
if (!formData.name || !formData.template_name) {
this.showError('Por favor complete todos los campos requeridos');
return;
}
try {
this.showLoading('Guardando plantilla...');
const result = await this.apiCall('save_template.php', 'POST', formData);
if (result.success) {
this.showSuccess(result.message);
document.getElementById('template-form').reset();
// Cerrar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('createTemplateModal'));
modal.hide();
// Recargar plantillas
this.loadTemplates();
} else {
this.showError(result.error || 'Error guardando plantilla');
}
} catch (error) {
this.showError('Error guardando plantilla: ' + error.message);
} finally {
this.hideLoading();
}
}
async editTemplate(templateId) {
// Por ahora, solo permitir cambiar el estado
const status = prompt('Cambiar estado de la plantilla:\n- pending (Pendiente)\n- approved (Aprobada)\n- rejected (Rechazada)', 'approved');
if (!status || !['pending', 'approved', 'rejected'].includes(status)) {
return;
}
try {
this.showLoading('Actualizando plantilla...');
const result = await this.apiCall('update_template_status.php', 'POST', {
id: templateId,
status: status
});
if (result.success) {
this.showSuccess(result.message);
this.loadTemplates();
} else {
this.showError(result.error || 'Error actualizando plantilla');
}
} catch (error) {
this.showError('Error actualizando plantilla: ' + error.message);
} finally {
this.hideLoading();
}
}
async deleteTemplate(templateId) {
if (!confirm('¿Estás seguro de que deseas eliminar esta plantilla?')) {
return;
}
try {
this.showLoading('Eliminando plantilla...');
// Por ahora usaremos update_template_status para marcar como eliminada
// En el futuro se puede crear delete_template.php
this.showError('Función de eliminar aún no implementada');
} catch (error) {
this.showError('Error eliminando plantilla: ' + error.message);
} finally {
this.hideLoading();
}
}
// Método para verificar el estado de la sesión
async checkSession() {
try {
const result = await this.apiCall('get_stats.php');
// Si la llamada es exitosa, la sesión está activa
if (!result) {
// La función apiCall ya manejó la redirección al login
return false;
}
return true;
} catch (error) {
console.warn('Error verificando sesión:', error);
// No mostrar error, solo en caso de problemas de red
return false;
}
}
}
// 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();
// Funciones globales - usando function declarations para evitar problemas con SES
window.refreshData = function() {
if (typeof app !== 'undefined') app.refreshData();
};
window.exportUsers = function() {
if (typeof app !== 'undefined') app.exportUsers();
};
window.refreshLogs = function() {
if (typeof app !== 'undefined') app.refreshLogs();
};
window.clearLogs = function() {
if (typeof app !== 'undefined') app.clearLogs();
};
// Las funciones de modales se definen ahora en el HTML para evitar problemas con SES
// window.showCreateTemplateModal y window.showCreateMenuModal están en index.php
window.toggleRecipientType = () => {
const recipientType = document.getElementById('recipient-type').value;
const existingGroup = document.getElementById('existing-recipient-group');
const manualGroup = document.getElementById('manual-recipient-group');
const messageTypeSelect = document.getElementById('message-type');
if (recipientType === 'existing') {
existingGroup.style.display = 'block';
manualGroup.style.display = 'none';
// Habilitar todas las opciones
messageTypeSelect.innerHTML = `
<option value="text">Texto</option>
<option value="template">Plantilla</option>
`;
} else {
existingGroup.style.display = 'none';
manualGroup.style.display = 'block';
// Solo permitir plantillas para números manuales
messageTypeSelect.innerHTML = `
<option value="template">Plantilla (Solo opción disponible)</option>
`;
messageTypeSelect.value = 'template';
toggleMessageType(); // Actualizar la interfaz
}
};
// Funciones movidas al HTML inline para evitar problemas con SES
// window.showCreateMenuModal y window.saveNewMenu están ahora en index.php
window.toggleMessageType = function() {
const messageType = document.getElementById('message-type').value;
const textGroup = document.getElementById('message-text-group');
const templateGroup = document.getElementById('template-group');
if (messageType === 'text') {
textGroup.style.display = 'block';
templateGroup.style.display = 'none';
} else {
textGroup.style.display = 'none';
templateGroup.style.display = 'block';
}
};