Files
whatsapp/assets/js/app_simple.js
T
2026-05-27 11:44:07 -05:00

5284 lines
224 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WhatsApp Bot Manager - JavaScript Simplificado
* Versión sin librerías externas problemáticas
*/
class SimpleWhatsAppManager {
constructor() {
this.apiBaseUrl = './api/';
this.currentTab = 'dashboard';
this.init();
}
init() {
this.log('Iniciando sistema simplificado');
this.setupEventListeners();
this.setupCharts();
this.loadDashboard();
}
log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
console.log(`[${timestamp}] [${type.toUpperCase()}] ${message}`);
}
setupEventListeners() {
this.log('Configurando event listeners');
// Navegación de tabs - más defensiva
const navLinks = document.querySelectorAll('.nav-link[data-tab]');
if (navLinks) {
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const tabName = link.getAttribute('data-tab');
if (tabName) {
this.showTab(tabName);
}
});
});
}
// Formularios
this.setupFormListeners();
}
setupFormListeners() {
// Formulario de configuración
const system_configForm = document.getElementById('system_config-form');
if (system_configForm) {
system_configForm.addEventListener('submit', (e) => {
e.preventDefault();
this.savesystem_config();
});
}
// Formulario de envío de mensaje
const sendForm = document.getElementById('send-message-form');
if (sendForm) {
sendForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendMessage();
});
}
}
showTab(tabName) {
this.log(`Cambiando a tab: ${tabName}`);
// Ocultar todas las pestañas
const tabs = document.querySelectorAll('.tab-content');
if (tabs) {
tabs.forEach(tab => tab.classList.remove('active'));
}
// Remover clase active de navegación
const navLinks = document.querySelectorAll('.nav-link');
if (navLinks) {
navLinks.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;
this.loadTabData(tabName);
}
loadTabData(tabName) {
this.log(`Cargando datos para tab: ${tabName}`);
switch (tabName) {
case 'dashboard':
this.loadDashboard();
break;
case 'conversations':
this.loadConversations();
break;
case 'users':
this.loadUsers();
break;
case 'admin_users':
this.loadAdminUsers();
break;
case 'send_message':
this.loadconversations();
break;
case 'templates':
this.loadTemplates();
break;
case 'whatsapp_profile':
if (typeof window.loadWhatsAppProfile === 'function') {
window.loadWhatsAppProfile();
}
break;
case 'broadcast':
// Recargar plantillas cuando se abre el tab de broadcast
if (typeof loadBroadcastTemplates === 'function') {
loadBroadcastTemplates();
}
break;
case 'menus':
this.loadMenus();
break;
case 'autoresponses':
this.loadAutoResponses();
break;
case 'terms':
if (typeof termsAdmin !== 'undefined' && typeof termsAdmin.cargar === 'function') {
termsAdmin.cargar(1);
}
break;
case 'logs':
this.loadLogs();
break;
case 'system_config':
this.loadsystem_config();
break;
default:
this.log(`Tab no reconocido: ${tabName}`, 'warning');
}
}
async apiCall(endpoint, options = {}) {
const url = `${this.apiBaseUrl}${endpoint}${endpoint.includes('?') ? '&' : '?'}debug=true`;
this.log(`API Call: ${url}`, 'info');
try {
const response = await fetch(url, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
this.log(`API Response: ${JSON.stringify(data).substring(0, 100)}...`, 'success');
return data;
} catch (error) {
this.log(`API Error: ${error.message}`, 'error');
this.showError('Error en petición: ' + error.message);
throw error;
}
}
async apiCallReal(endpoint, options = {}) {
const url = `${this.apiBaseUrl}${endpoint}`;
this.log(`API Call (REAL): ${url}`, 'info');
try {
const response = await fetch(url, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
this.log(`API Response (REAL): ${JSON.stringify(data).substring(0, 100)}...`, 'success');
return data;
} catch (error) {
this.log(`API Error (REAL): ${error.message}`, 'error');
this.showError('Error en petición real: ' + error.message);
throw error;
}
}
async loadDashboard() {
this.log('Cargando dashboard');
try {
// Cargar estadísticas básicas
const stats = await this.apiCall('get_stats.php');
this.updateStats(stats);
// Cargar mensajes recientes
const recentconversationsResponse = await this.apiCall('get_recent_messages.php');
if (recentconversationsResponse && recentconversationsResponse.success && Array.isArray(recentconversationsResponse.data)) {
this.updateRecentconversations(recentconversationsResponse.data);
} else if (recentconversationsResponse && Array.isArray(recentconversationsResponse)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateRecentconversations(recentconversationsResponse);
}
// Actualizar gráfico si existe
this.updateChart();
} catch (error) {
this.showError('Error cargando dashboard: ' + error.message);
}
}
updateStats(stats) {
this.log('Actualizando estadísticas del dashboard');
if (stats) {
const elements = [
{ id: 'total-users', value: stats.total_users || 0 },
{ id: 'conversations-today', value: stats.conversations_today || 0 },
{ id: 'active-users', value: stats.active_users || 0 },
{ id: 'total-conversations', value: stats.total_conversations || 0 }
];
elements.forEach(item => {
const element = document.getElementById(item.id);
if (element) {
element.textContent = item.value;
this.log(`Estadística actualizada: ${item.id} = ${item.value}`);
}
});
// Actualizar badge de conversaciones en el menú lateral (usar mensajes pendientes si existe)
const convBadge = document.getElementById('conversations-count');
if (convBadge) {
const val = (typeof stats.unread_messages !== 'undefined') ? stats.unread_messages : (stats.total_conversations || 0);
convBadge.textContent = val;
convBadge.style.display = val > 0 ? 'inline-block' : 'none';
}
}
}
updateRecentconversations(conversations) {
const container = document.getElementById('recent-conversations');
if (!container) {
this.log('Contenedor de mensajes recientes no encontrado', 'warning');
return;
}
container.innerHTML = '';
if (conversations && conversations.length > 0) {
this.log(`Mostrando ${conversations.length} mensajes recientes`);
conversations.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 mb-3 p-2 border-bottom';
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 text-muted small">${time}</span>
</div>
<div class="content mt-1">${this.truncateText(message.content, 50)}</div>
`;
return div;
}
truncateText(text, length) {
if (!text) return '';
return text.length > length ? text.substring(0, length) + '...' : text;
}
async updateChart() {
if (!this.charts || !this.charts.conversations) {
this.log('Gráfico no inicializado, omitiendo actualización', 'warning');
return;
}
try {
const chartData = await this.apiCall('get_chart_data.php');
if (chartData && this.charts.conversations) {
this.charts.conversations.data.labels = chartData.labels || [];
this.charts.conversations.data.datasets[0].data = chartData.data || [];
this.charts.conversations.update();
this.log('Gráfico actualizado correctamente');
}
} catch (error) {
this.log('Error actualizando gráfico: ' + error.message, 'warning');
}
}
setupCharts() {
const ctx = document.getElementById('conversationsChart');
if (!ctx) {
this.log('Canvas de gráfico no encontrado', 'warning');
return;
}
if (typeof Chart === 'undefined') {
this.log('Chart.js no está cargado', 'warning');
return;
}
try {
this.charts = this.charts || {};
this.charts.conversations = 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
}
}
}
});
this.log('Gráfico de mensajes inicializado');
} catch (error) {
this.log('Error inicializando gráfico: ' + error.message, 'error');
}
}
async loadUsers(page = 1, search = '') {
this.log('Cargando usuarios - página ' + page);
try {
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) {
this.updateUsersList(response.data || [], response.pagination || {});
} else {
this.showError('No se pudieron cargar los usuarios');
}
} catch (error) {
this.showError('Error cargando usuarios: ' + error.message);
}
}
updateUsersList(users, pagination = {}) {
const container = document.getElementById('users-table');
if (!container) return;
// Guardar datos para uso posterior
this.usersData = users;
this.usersPagination = pagination;
if (users.length === 0) {
container.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No hay usuarios registrados</td></tr>';
this.updateUsersPagination({});
return;
}
let html = '';
users.forEach(user => {
const createdDate = new Date(user.created_at);
const formatDate = createdDate.toLocaleDateString() + ' ' + createdDate.toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' });
html += `
<tr>
<td>${user.id}</td>
<td>${user.phone_number}</td>
<td>${user.name || 'Sin nombre'}</td>
<td><span class="badge bg-${user.status === 'active' ? 'success' : 'secondary'}">${user.status}</span></td>
<td>${user.current_menu || 'Sin menú'}</td>
<td>${formatDate}</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="editUser(${user.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteUser(${user.id})">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`;
});
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() {
this.log('Cargando conversaciones');
try {
const response = await this.apiCall('get_conversation_list.php');
console.log('Respuesta get_conversation_list:', response); // Debug
if (response && response.success && Array.isArray(response.conversations)) {
this.updateConversationsList(response.conversations);
} else if (response && Array.isArray(response)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateConversationsList(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);
}
}
updateConversationsList(conversations) {
const container = document.getElementById('conversations-container');
if (!container) return;
if (conversations.length === 0) {
container.innerHTML = `
<div class="conversation-empty">
<div class="empty-icon">
<i class="fas fa-comments"></i>
</div>
<h5>No hay conversaciones</h5>
<p>Los chats aparecerán aquí cuando recibas o envíes mensajes</p>
</div>
`;
return;
}
let html = '';
conversations.forEach(conv => {
const timeAgo = conv.time_ago || this.formatTimeAgo(conv.last_message_time || conv.created_at);
const lastMessage = conv.last_message || conv.content || 'Sin mensajes';
const userName = conv.name || conv.phone_number;
const userInitial = userName.charAt(0).toUpperCase();
const isOnline = Math.random() > 0.6; // Simulación de estado online
// Unread badge
const unreadBadge = (conv.unread_count && conv.unread_count > 0) ?
`<div class="conversation-unread">${conv.unread_count}</div>` : '';
// Add class to highlight unread conversation (darker green)
const unreadClass = (conv.unread_count && conv.unread_count > 0) ? ' unread' : '';
// Attention badge for advisor requests
const attentionBadge = conv.advisor_requested ? `<div class="conversation-attention" title="Requiere atención"><i class="fas fa-exclamation"></i></div>` : '';
const attentionClass = conv.advisor_requested ? ' attention' : '';
// Status icon
const statusIcon = conv.last_direction === 'outgoing'
? '<i class="fas fa-check-double conversation-status-icon outgoing"></i>'
: '<i class="fas fa-arrow-down conversation-status-icon incoming"></i>';
// Message type indicator
const messageTypeIcon = conv.last_message_type === 'template'
? '<i class="fas fa-file-alt text-primary" title="Plantilla"></i> '
: conv.last_message_type === 'image'
? '<i class="fas fa-image text-info" title="Imagen"></i> '
: '';
html += `
<div class="conversation-item${unreadClass}${attentionClass}" onclick="openChatWindow(${conv.user_id})">
<div class="conversation-avatar ${isOnline ? 'online' : ''}">
${userInitial}
</div>
<div class="conversation-details">
<div class="conversation-header">
<div class="conversation-name">
${userName}
${conv.last_message_status === 'read' ? '<i class="fas fa-check-double text-primary" title="Leído"></i>' : ''}
</div>
<div class="conversation-time">${timeAgo}</div>
</div>
<div class="conversation-preview">
${statusIcon}
<div class="conversation-last-message">
${messageTypeIcon}${lastMessage}
</div>
</div>
</div>
<div class="conversation-meta">
${unreadBadge}
${attentionBadge}
<div class="conversation-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); openChatWindow(${conv.user_id})" title="Abrir chat">
<i class="fas fa-comments"></i>
</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); viewConversationDetails(${conv.user_id})" title="Ver detalles">
<i class="fas fa-info-circle"></i>
</button>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
// Agregar funcionalidad de búsqueda
this.setupConversationSearch(conversations);
}
setupConversationSearch(conversations) {
const searchInput = document.getElementById('search-conversations');
if (!searchInput) return;
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
if (!query) {
this.updateConversationsDisplay(conversations);
return;
}
const filtered = conversations.filter(conv => {
const name = (conv.name || '').toLowerCase();
const phone = (conv.phone_number || '').toLowerCase();
const message = (conv.last_message || conv.content || '').toLowerCase();
return name.includes(query) ||
phone.includes(query) ||
message.includes(query);
});
this.updateConversationsDisplay(filtered);
});
}
updateConversationsDisplay(conversations) {
const container = document.getElementById('conversations-container');
if (!container) return;
if (conversations.length === 0) {
container.innerHTML = `
<div class="conversation-empty">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h5>No se encontraron conversaciones</h5>
<p>Intenta con otros términos de búsqueda</p>
</div>
`;
return;
}
let html = '';
conversations.forEach(conv => {
const timeAgo = conv.time_ago || this.formatTimeAgo(conv.last_message_time || conv.created_at);
const lastMessage = conv.last_message || conv.content || 'Sin mensajes';
const userName = conv.name || conv.phone_number;
const userInitial = userName.charAt(0).toUpperCase();
const isOnline = Math.random() > 0.6;
const unreadBadge = (conv.unread_count && conv.unread_count > 0) ?
`<div class="conversation-unread">${conv.unread_count}</div>` : '';
// Attention badge for advisor requests (ensure defined for filtered view)
const attentionBadge = conv.advisor_requested ? `<div class="conversation-attention" title="Requiere atención"><i class="fas fa-exclamation"></i></div>` : '';
const attentionClass = conv.advisor_requested ? ' attention' : '';
const statusIcon = conv.last_direction === 'outgoing'
? '<i class="fas fa-check-double conversation-status-icon outgoing"></i>'
: '<i class="fas fa-arrow-down conversation-status-icon incoming"></i>';
const messageTypeIcon = conv.last_message_type === 'template'
? '<i class="fas fa-file-alt text-primary" title="Plantilla"></i> '
: conv.last_message_type === 'image'
? '<i class="fas fa-image text-info" title="Imagen"></i> '
: '';
const unreadClass = (conv.unread_count && conv.unread_count > 0) ? ' unread' : '';
html += `
<div class="conversation-item${unreadClass}${attentionClass}" onclick="openChatWindow(${conv.user_id})">
<div class="conversation-avatar ${isOnline ? 'online' : ''}">
${userInitial}
</div>
<div class="conversation-details">
<div class="conversation-header">
<div class="conversation-name">
${userName}
${conv.last_message_status === 'read' ? '<i class="fas fa-check-double text-primary" title="Leído"></i>' : ''}
</div>
<div class="conversation-time">${timeAgo}</div>
</div>
<div class="conversation-preview">
${statusIcon}
<div class="conversation-last-message">
${messageTypeIcon}${lastMessage}
</div>
</div>
</div>
<div class="conversation-meta">
${unreadBadge}
${attentionBadge}
<div class="conversation-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); openChatWindow(${conv.user_id})" title="Abrir chat">
<i class="fas fa-comments"></i>
</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); viewConversationDetails(${conv.user_id})" title="Ver detalles">
<i class="fas fa-info-circle"></i>
</button>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
formatTimeAgo(datetime) {
if (!datetime) return '';
const now = new Date();
const date = new Date(datetime);
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Ahora';
if (diff < 3600) return `${Math.floor(diff/60)}m`;
if (diff < 86400) return `${Math.floor(diff/3600)}h`;
if (diff < 2592000) return `${Math.floor(diff/86400)}d`;
return date.toLocaleDateString();
}
async loadsystem_config() {
this.log('Cargando configuraciones');
try {
const response = await this.apiCall('manage_config.php?action=whatsapp');
if (response && response.success) {
this.updatesystem_configForm(response.data);
}
} catch (error) {
this.showError('Error cargando configuraciones: ' + error.message);
}
// Cargar siempre el estado del documento de términos
if (typeof loadTermsDoc === 'function') loadTermsDoc();
}
updatesystem_configForm(data) {
if (!data) return;
const fields = [
{ id: 'whatsapp-token', value: data.whatsapp_token },
{ 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: 'api-url', value: data.api_url },
{ id: 'business-name', value: data.business_name },
{ id: 'welcome-message', value: data.welcome_message },
{ id: 'terms-message', value: data.terms_message },
{ id: 'terms-rejected-message', value: data.terms_rejected_message }
];
fields.forEach(field => {
const element = document.getElementById(field.id);
if (element && field.value) {
element.value = field.value;
this.log(`Campo ${field.id} actualizado con valor: ${field.value}`);
} else {
this.log(`Campo ${field.id} no encontrado o sin valor`, 'warning');
}
});
}
async savesystem_config() {
this.log('Guardando configuraciones');
const system_config = {
token: document.getElementById('whatsapp-token').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,
api_url: document.getElementById('api-url').value,
business_name: document.getElementById('business-name').value,
welcome_message: document.getElementById('welcome-message').value,
terms_message: document.getElementById('terms-message')?.value || '',
terms_rejected_message: document.getElementById('terms-rejected-message')?.value || ''
};
try {
const response = await this.apiCall('manage_config.php?action=save_whatsapp', {
method: 'POST',
body: { whatsapp: system_config }
});
if (response && response.success) {
this.showSuccess('Configuraciones guardadas exitosamente');
} else {
this.showError('Error guardando configuraciones');
}
} catch (error) {
this.showError('Error: ' + error.message);
}
}
showError(message) {
this.log(`Error: ${message}`, 'error');
this.showAlert(message, 'danger');
}
showSuccess(message) {
this.log(`Success: ${message}`, 'success');
this.showAlert(message, 'success');
}
showInfo(message) {
this.log(`Info: ${message}`, 'info');
this.showAlert(message, 'info');
}
showWarning(message) {
this.log(`Warning: ${message}`, 'warning');
this.showAlert(message, 'warning');
}
showAlert(message, type) {
// Crear alerta temporal
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Auto-remover después de 5 segundos
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 5000);
}
async loadconversations() {
this.log('Cargando interfaz de mensajes');
const userSelect = document.getElementById('message-recipient');
const templateSelect = document.getElementById('message-template');
if (userSelect) userSelect.innerHTML = '<option value="">Cargando usuarios...</option>';
if (templateSelect) templateSelect.innerHTML = '<option value="">Cargando plantillas...</option>';
try {
// Cargar TODOS los usuarios sin límite
const usersResponse = await this.apiCall('get_users.php?limit=10000');
let users = [];
if (usersResponse && usersResponse.success && Array.isArray(usersResponse.data)) {
users = usersResponse.data;
} else if (Array.isArray(usersResponse)) {
users = usersResponse;
}
// Cargar plantillas para el select
const templatesResponse = await this.apiCall('get_templates.php');
let templates = [];
if (templatesResponse && templatesResponse.success && Array.isArray(templatesResponse.data)) {
templates = templatesResponse.data;
} else if (Array.isArray(templatesResponse)) {
templates = templatesResponse;
}
this.log(`Usuarios cargados: ${users.length}, Plantillas cargadas: ${templates.length}`);
// Guardar plantillas para uso posterior
this.templatesData = templates;
if (users.length > 0) {
this.populateUsersSelect(users);
} else if (userSelect) {
userSelect.innerHTML = '<option value="">No hay usuarios</option>';
}
if (templates.length > 0) {
this.populateTemplatesSelect(templates);
} else if (templateSelect) {
templateSelect.innerHTML = '<option value="">No hay plantillas</option>';
}
} catch (error) {
console.error('Error loadconversations:', error);
if (userSelect) userSelect.innerHTML = '<option value="">Error cargando usuarios</option>';
if (templateSelect) templateSelect.innerHTML = '<option value="">Error cargando plantillas</option>';
this.showError('Error cargando datos para mensajes: ' + (error.message || error));
}
}
populateUsersSelect(users) {
const select = document.getElementById('message-recipient');
if (!select) return;
// Destruir Select2 si existe
if ($(select).data('select2')) {
$(select).select2('destroy');
}
// Limpiar opciones existentes
select.innerHTML = '<option value="">Seleccionar usuario...</option>';
// Agregar todos los usuarios
users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = `${user.name || user.phone_number} (${user.phone_number})`;
select.appendChild(option);
});
// Inicializar Select2 con búsqueda
$(select).select2({
theme: 'bootstrap-5',
placeholder: 'Buscar y seleccionar usuario...',
allowClear: true,
width: '100%'
});
}
populateTemplatesSelect(templates) {
const select = document.getElementById('message-template');
if (!select) return;
// Limpiar opciones existentes (excepto la primera)
select.innerHTML = '<option value="">Seleccionar plantilla...</option>';
// Agregar plantillas (solo las aprobadas)
templates
.filter(template => template.status && template.status.toUpperCase() === 'APPROVED')
.forEach(template => {
const option = document.createElement('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() {
this.log('Cargando plantillas');
try {
const response = await this.apiCall('get_templates.php');
if (response && response.success) {
const tmpl = response.data || [];
this.templatesData = tmpl;
this.templates = tmpl; // compatibilidad con app.js
this.updateTemplatesList(tmpl);
} else {
this.showError('Error cargando plantillas');
}
} catch (error) {
this.showError('Error cargando plantillas: ' + error.message);
}
}
async loadMenus() {
this.log('Cargando menús');
try {
const response = await this.apiCall('get_menus.php');
if (response && response.success) {
this.updateMenusList(response.data || []);
} else if (response && Array.isArray(response)) {
// Retrocompatibilidad
this.updateMenusList(response);
} else {
this.showError('Error cargando menús');
}
} catch (error) {
this.showError('Error cargando menús: ' + error.message);
}
}
updateTemplatesList(templates) {
const container = document.getElementById('templates-table');
if (!container) return;
// Guardar datos para uso posterior
this.templatesData = templates;
if (templates.length === 0) {
container.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No hay plantillas registradas</td></tr>';
return;
}
let html = '';
templates.forEach(template => {
html += `
<tr>
<td><strong>${template.name}</strong></td>
<td><code>${template.template_name}</code></td>
<td>${template.language_code || 'es'}</td>
<td><span class="badge bg-info">${template.category || 'utility'}</span></td>
<td><span class="badge bg-${template.status === 'approved' ? 'success' : 'warning'}">${template.status || 'pending'}</span></td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="editTemplate(${template.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTemplate(${template.id})">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`;
});
container.innerHTML = html;
}
updateMenusList(menus) {
const container = document.getElementById('menus-table');
if (!container) return;
if (menus.length === 0) {
container.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No hay menús configurados</td></tr>';
return;
}
let html = '';
menus.forEach(menu => {
const createdDate = new Date(menu.created_at || Date.now());
const formatDate = createdDate.toLocaleDateString() + ' ' + createdDate.toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' });
html += `
<tr>
<td><strong>${menu.name || menu.menu_name}</strong></td>
<td><code>${menu.menu_key || menu.trigger}</code></td>
<td>${this.truncateText(menu.description || '', 50)}</td>
<td>
<span class="badge bg-${menu.status === 'active' ? 'success' : 'secondary'}">${menu.status || 'active'}</span>
${(menu.menu_type || 'main') === 'no_advisor' ? '<br><span class="badge bg-danger mt-1">🔴 Asesor ausente</span>' : ''}
</td>
<td>${menu.options_count || 0} opciones</td>
<td>${formatDate}</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="editMenu(${menu.id})" title="Editar">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-info me-1" onclick="viewMenuOptions(${menu.id})" title="Ver opciones">
<i class="fas fa-list"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteMenu(${menu.id})" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`;
});
container.innerHTML = html;
}
async saveTemplate() {
this.log('Guardando plantilla');
try {
const templateData = {
name: document.getElementById('template-name').value,
whatsapp_name: document.getElementById('template-whatsapp-name').value,
language: document.getElementById('template-language').value,
category: document.getElementById('template-category').value
};
// Validar campos requeridos
if (!templateData.name || !templateData.whatsapp_name) {
this.showError('Nombre descriptivo y nombre de WhatsApp son requeridos');
return;
}
const response = await this.apiCall('save_template.php', {
method: 'POST',
body: templateData
});
if (response && response.success) {
const status = response.status || 'pending';
if (status === 'approved') {
this.showSuccess('Plantilla guardada y marcada como APROBADA.');
} else {
this.showSuccess('Plantilla guardada correctamente. Estado: PENDIENTE.');
}
// Cerrar modal
const modal = document.getElementById('createTemplateModal');
if (modal) {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) bsModal.hide();
}
// Limpiar formulario
document.getElementById('template-form').reset();
// Recargar plantillas
this.loadTemplates();
} else {
this.showError(`Error guardando plantilla: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
this.showError('Error guardando plantilla: ' + error.message);
}
}
async saveMenu() {
this.log('Guardando menú');
try {
const menuData = {
name: document.getElementById('menu-name')?.value || '',
menu_key: document.getElementById('menu-key')?.value || '',
description: document.getElementById('menu-description')?.value || '',
welcome_message: document.getElementById('menu-welcome-message')?.value || '',
status: document.getElementById('menu-status')?.value || 'active',
menu_type: document.getElementById('menu-type')?.value || 'main'
};
// Validar campos requeridos
if (!menuData.name || !menuData.menu_key) {
this.showError('Nombre del menú y clave del menú son requeridos');
return;
}
// Obtener opciones del menú
const optionsContainer = document.getElementById('menu-options-container');
const options = [];
if (optionsContainer) {
const optionItems = optionsContainer.querySelectorAll('.menu-option-item');
optionItems.forEach((item, index) => {
const optionKey = item.querySelector('.option-key')?.value || '';
const optionText = item.querySelector('.option-text')?.value || '';
const optionAction = item.querySelector('.option-action')?.value || 'message';
const optionValue = item.querySelector('.option-value')?.value || '';
if (optionKey && optionText) {
options.push({
key: optionKey,
text: optionText,
action: optionAction,
value: optionValue,
order_index: index + 1
});
}
});
}
menuData.options = options;
this.log(`Datos del menú a guardar: ${JSON.stringify(menuData)}`);
const response = await this.apiCall('save_menu.php', {
method: 'POST',
body: menuData
});
if (response && response.success) {
this.showSuccess('Menú guardado correctamente');
// Cerrar modal
const modal = document.getElementById('createMenuModal');
if (modal) {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) bsModal.hide();
}
// Limpiar formulario
const form = document.getElementById('menu-form');
if (form) form.reset();
// Limpiar opciones
const optionsContainer = document.getElementById('menu-options-container');
if (optionsContainer) optionsContainer.innerHTML = '';
// Recargar menús
this.loadMenus();
} else {
this.showError(`Error guardando menú: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
this.showError('Error guardando menú: ' + error.message);
}
}
async loadAutoResponses() {
this.log('Cargando respuestas automáticas');
try {
const response = await this.apiCall('get_autoresponses.php');
if (response && response.success) {
this.updateAutoResponsesList(response.data || []);
} else if (response && Array.isArray(response)) {
this.updateAutoResponsesList(response);
} else {
this.showError('Error cargando respuestas automáticas');
}
} catch (error) {
this.showError('Error cargando respuestas automáticas: ' + error.message);
}
}
updateAutoResponsesList(autoresponses) {
const container = document.getElementById('autoresponses-table');
if (!container) return;
// Almacenar los datos para uso posterior
this.autoResponsesData = autoresponses;
if (autoresponses.length === 0) {
container.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No hay respuestas automáticas configuradas</td></tr>';
return;
}
let html = '';
autoresponses.forEach(response => {
const typeLabels = {
'keyword': '🔑 Palabra Clave',
'contains': '📝 Contiene',
'exact': '🎯 Exacta',
'welcome': '👋 Bienvenida',
'default': '🤖 Por Defecto'
};
const typeLabel = typeLabels[response.trigger_type] || response.trigger_type;
html += `
<tr>
<td><span class="badge bg-info">${typeLabel}</span></td>
<td><code>${response.trigger_value || '-'}</code></td>
<td>${this.truncateText(response.response_text || '', 60)}</td>
<td><span class="badge bg-${response.is_active == 1 ? 'success' : 'secondary'}">${response.is_active == 1 ? 'Activo' : 'Inactivo'}</span></td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="editAutoResponse(${response.id})" title="Editar">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteAutoResponse(${response.id})" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`;
});
container.innerHTML = html;
}
async saveAutoResponse() {
this.log('Guardando respuesta automática');
try {
const autoresponseId = document.getElementById('autoresponse-id')?.value || '';
const responseData = {
trigger_type: document.getElementById('autoresponse-trigger-type')?.value || 'keyword',
trigger_value: document.getElementById('autoresponse-trigger-value')?.value || '',
response_text: document.getElementById('autoresponse-text')?.value || '',
response_type: document.getElementById('autoresponse-type')?.value || 'text',
priority: document.getElementById('autoresponse-priority')?.value || 0,
template_name: document.getElementById('autoresponse-template-name')?.value || '',
menu_id: document.getElementById('autoresponse-menu-id')?.value || '',
is_active: document.getElementById('autoresponse-active')?.value === '1' ? 1 : 0
};
// Si hay ID, es una actualización
if (autoresponseId) {
responseData.id = autoresponseId;
}
// Validar campos requeridos
if (!responseData.response_text) {
this.showError('El texto de respuesta es requerido');
return;
}
if (responseData.trigger_type !== 'welcome' && responseData.trigger_type !== 'default' && !responseData.trigger_value) {
this.showError('El disparador es requerido para este tipo de respuesta');
return;
}
this.log(`Datos de respuesta automática a guardar: ${JSON.stringify(responseData)}`);
const response = await this.apiCall('save_autoresponse.php', {
method: 'POST',
body: responseData
});
if (response && response.success) {
this.showSuccess('Respuesta automática guardada correctamente');
// Cerrar modal
const modal = document.getElementById('createAutoResponseModal');
if (modal) {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) bsModal.hide();
}
// Limpiar formulario
const form = document.getElementById('autoresponse-form');
if (form) form.reset();
// Recargar respuestas automáticas
this.loadAutoResponses();
} else {
this.showError(`Error guardando respuesta automática: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
this.showError('Error guardando respuesta automática: ' + error.message);
}
}
async deleteAutoResponseAction(responseId) {
this.log(`Eliminando respuesta automática ID: ${responseId}`);
try {
const response = await this.apiCall('delete_autoresponse.php', {
method: 'POST',
body: { id: responseId }
});
if (response && response.success) {
this.showSuccess('Respuesta automática eliminada correctamente');
this.loadAutoResponses(); // Recargar la lista
} else {
this.showError(`Error eliminando respuesta automática: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
this.showError('Error eliminando respuesta automática: ' + error.message);
}
}
async deleteMenuAction(menuId) {
this.log(`Eliminando menú ID: ${menuId}`);
try {
const response = await this.apiCall('delete_menu.php', {
method: 'POST',
body: { menu_id: menuId }
});
if (response && response.success) {
this.showSuccess('Menú eliminado correctamente');
this.loadMenus(); // Recargar la lista de menús
} else {
this.showError(`Error eliminando menú: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
this.showError('Error eliminando menú: ' + error.message);
}
}
async sendMessage() {
this.log('Enviando mensaje');
try {
const recipientType = document.getElementById('recipient-type').value;
const messageType = document.getElementById('message-type').value;
let recipient;
let messageData = {};
// Obtener destinatario
if (recipientType === 'existing') {
const userSelect = document.getElementById('message-recipient');
if (!userSelect.value) {
this.showError('Por favor selecciona un usuario');
return;
}
// Obtener datos del usuario seleccionado
const selectedOption = userSelect.options[userSelect.selectedIndex];
const userId = userSelect.value;
// Necesitamos obtener el número de teléfono del usuario
const usersResponse = await this.apiCall('get_users.php');
if (usersResponse && usersResponse.success) {
const user = usersResponse.data.find(u => u.id == userId);
if (user) {
recipient = user.phone_number;
} else {
this.showError('Usuario no encontrado');
return;
}
} else {
this.showError('Error obteniendo datos del usuario');
return;
}
} else {
recipient = document.getElementById('manual-recipient').value;
if (!recipient) {
this.showError('Por favor ingresa un número de teléfono');
return;
}
}
// Preparar mensaje según el tipo
if (messageType === 'text') {
const messageText = document.getElementById('message-text').value;
if (!messageText) {
this.showError('Por favor escribe un mensaje');
return;
}
messageData = {
recipient: recipient,
type: 'text',
message: messageText
};
} else {
const templateSelect = document.getElementById('message-template');
if (!templateSelect.value) {
this.showError('Por favor selecciona una plantilla');
return;
}
// Obtener ID y nombre de la plantilla
const templateId = templateSelect.value;
const selectedOption = templateSelect.options[templateSelect.selectedIndex];
// Buscar la plantilla en templatesData para obtener el template_name
let templateName = null;
if (this.templatesData) {
const template = this.templatesData.find(t => t.id == templateId);
if (template) {
templateName = template.template_name;
}
}
if (!templateName) {
this.showError('No se pudo obtener el nombre de la plantilla');
return;
}
// Recopilar variables de los inputs
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);
}
}
// Subir imagen de header si el usuario seleccionó una
let headerImageUrl = null;
const msgImgInput = document.getElementById('message-template-image');
if (msgImgInput && msgImgInput.files && msgImgInput.files[0]) {
try {
this.showInfo('Subiendo imagen de la plantilla...');
headerImageUrl = await this._uploadTemplateImage(msgImgInput);
} catch (e) {
this.showError('Error subiendo imagen de la plantilla: ' + e.message);
return;
}
}
messageData = {
recipient: recipient,
type: 'template',
template_name: templateName, // Usar template_name en lugar de ID
parameters: tplParams,
...(headerImageUrl ? { header_image_url: headerImageUrl } : {})
};
}
this.log(`Enviando mensaje a ${recipient}: ${JSON.stringify(messageData)}`);
// Enviar mensaje por WhatsApp
const response = await this.apiCallReal('send_message.php', {
method: 'POST',
body: messageData
});
if (response && response.success) {
this.showSuccess(`Mensaje enviado correctamente a ${recipient}`);
// Limpiar formulario
document.getElementById('send-message-form').reset();
document.getElementById('message-recipient').value = '';
} else {
this.showError(`Error enviando mensaje: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
this.showError('Error enviando mensaje: ' + error.message);
}
}
async loadLogs() {
try {
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);
}
}
updateLogsTable(logs) {
const tbody = document.getElementById('logs-table');
if (!tbody) {
this.log('Tabla de logs no encontrada', 'warning');
return;
}
tbody.innerHTML = '';
if (!logs || logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No hay logs registrados</td></tr>';
return;
}
logs.forEach(log => {
const row = this.createLogRow(log);
if (row) {
tbody.appendChild(row);
}
});
}
createLogRow(log) {
if (!log) return null;
const tr = document.createElement('tr');
tr.setAttribute('data-level', (log.level || 'INFO').toUpperCase());
tr.setAttribute('data-message', (log.message || log.mensaje || '').toLowerCase());
tr.setAttribute('data-source', (log.source || log.origen || '').toLowerCase());
// Fecha y hora
const tdDateTime = document.createElement('td');
const dateStr = log.datetime || log.created_at || 'N/A';
tdDateTime.innerHTML = `<small class="text-muted">${dateStr}</small>`;
tr.appendChild(tdDateTime);
// Nivel
const tdLevel = document.createElement('td');
const level = (log.level || log.tipo || 'INFO').toUpperCase();
const levelIcon = {
'ERROR': '❌',
'WARNING': '⚠️',
'INFO': '️',
'DEBUG': '🔧',
'SUCCESS': '✅'
}[level] || '️';
const levelBadge = document.createElement('span');
levelBadge.className = `badge bg-${this.getLevelBadgeClass(level)}`;
levelBadge.textContent = `${levelIcon} ${level}`;
tdLevel.appendChild(levelBadge);
tr.appendChild(tdLevel);
// Mensaje
const tdMessage = document.createElement('td');
const message = log.message || log.mensaje || '';
tdMessage.innerHTML = `<span class="log-message">${this.escapeHtml(this.truncateText(message, 120))}</span>`;
if (log.data) {
const dataBtn = document.createElement('button');
dataBtn.className = 'btn btn-xs btn-link text-muted ms-2';
dataBtn.innerHTML = '<i class="fas fa-database"></i>';
dataBtn.title = 'Ver datos adicionales';
dataBtn.onclick = () => this.showLogDetails(log);
tdMessage.appendChild(dataBtn);
}
tr.appendChild(tdMessage);
// Origen
const tdSource = document.createElement('td');
const source = log.source || log.origen || 'Sistema';
tdSource.innerHTML = `<small><code>${this.escapeHtml(source)}</code></small>`;
tr.appendChild(tdSource);
// Acciones
const tdActions = document.createElement('td');
const viewButton = document.createElement('button');
viewButton.className = 'btn btn-sm btn-outline-info';
viewButton.innerHTML = '<i class="fas fa-eye"></i>';
viewButton.title = 'Ver detalles completos';
viewButton.onclick = () => this.showLogDetails(log);
tdActions.appendChild(viewButton);
tr.appendChild(tdActions);
return tr;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getLevelBadgeClass(level) {
const levelClasses = {
'ERROR': 'danger',
'WARNING': 'warning',
'WARN': 'warning',
'INFO': 'info',
'DEBUG': 'secondary',
'SUCCESS': 'success'
};
return levelClasses[level?.toUpperCase()] || 'secondary';
}
showLogDetails(log) {
const details = {
'Fecha/Hora': log.datetime || log.created_at || 'N/A',
'Nivel': (log.level || log.tipo || 'INFO').toUpperCase(),
'Mensaje': log.message || log.mensaje || '',
'Origen': log.source || log.origen || 'Sistema',
'Datos Adicionales': log.data ? JSON.stringify(log.data, null, 2) : 'N/A'
};
let detailsHtml = '<div class="log-details">';
for (const [key, value] of Object.entries(details)) {
detailsHtml += `
<div class="mb-2">
<strong>${key}:</strong>
<div class="ms-2 ${key === 'Datos Adicionales' ? 'font-monospace' : ''}">${value}</div>
</div>`;
}
detailsHtml += '</div>';
// Mostrar en modal o alert
if (typeof bootstrap !== 'undefined') {
// Si Bootstrap está disponible, crear modal
this.showModalAlert('Detalles del Log', detailsHtml);
} else {
// Fallback a alert simple
alert(`Detalles del Log:\n\n${Object.entries(details).map(([k,v]) => `${k}: ${v}`).join('\n')}`);
}
}
showModalAlert(title, content) {
// Crear modal temporal si no existe
let modal = document.getElementById('tempLogModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'tempLogModal';
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>`;
document.body.appendChild(modal);
}
modal.querySelector('.modal-title').textContent = title;
modal.querySelector('.modal-body').innerHTML = content;
const bootstrapModal = new bootstrap.Modal(modal);
bootstrapModal.show();
}
async deleteUserAction(userId, userName, userPhone) {
if (!confirm(`¿Estás seguro de que quieres eliminar este usuario?\n\nUsuario: ${userName}\nTeléfono: ${userPhone}\n\nEsta acción no se puede deshacer y eliminará:\n• Todos sus mensajes\n• Su historial de conversación\n• Sus datos personales`)) {
return;
}
this.log(`Eliminando usuario ID: ${userId}`);
try {
const response = await this.apiCall('delete_user.php', {
method: 'POST',
body: { id: userId }
});
if (response && response.success) {
this.showSuccess('Usuario eliminado correctamente');
this.loadUsers(); // Recargar la lista
} else {
this.showError(`Error eliminando usuario: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
this.showError('Error eliminando usuario: ' + error.message);
}
}
async deleteTemplateAction(templateId) {
// intentar obtener el nombre de la plantilla para confirmar
const tmpl = (this.templatesData || []).find(t => t.id === templateId) || {};
const name = tmpl.name || tmpl.template_name || `ID ${templateId}`;
if (!confirm(`¿Estás seguro de que quiere eliminar la plantilla "${name}"? Esta acción no se puede deshacer.`)) return;
this.log(`Eliminando plantilla ID: ${templateId}`);
try {
const response = await this.apiCall('delete_template.php', {
method: 'POST',
body: { id: templateId }
});
if (response && response.success) {
this.showSuccess('Plantilla eliminada correctamente');
this.loadTemplates(); // Recargar la lista
} else {
this.showError(`Error eliminando plantilla: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
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;
}
// Subir imagen de header broadcast si existe
const bcImgInput = document.getElementById('broadcast-template-image');
if (bcImgInput && bcImgInput.files && bcImgInput.files[0]) {
try {
this.showInfo('Subiendo imagen de la plantilla...');
payload.header_image_url = await this._uploadTemplateImage(bcImgInput);
} catch (e) {
this.showError('Error subiendo imagen de la plantilla: ' + e.message);
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';
// Refrescar historial automáticamente
if (typeof window.loadBroadcastHistory === 'function') {
setTimeout(() => window.loadBroadcastHistory(1), 800);
}
} else {
this.showError(result.error || 'Error enviando broadcast');
}
} catch (error) {
this.showError('Error enviando broadcast: ' + error.message);
}
}
// Sube una imagen al servidor y retorna la URL pública
async _uploadTemplateImage(fileInput) {
const file = fileInput.files[0];
if (!file) return null;
const fd = new FormData();
fd.append('file', file);
const resp = await fetch('./api/upload_media.php', { method: 'POST', body: fd });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (data.success && data.public_url) return data.public_url;
throw new Error(data.error || 'No se pudo subir la imagen');
}
// Gestión de usuarios centralizada en lab_usuarios.php
async loadAdminUsers() {
// No-op: la UI redirige a lab_usuarios.php
}
}
// Inicializar cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', function () {
console.log('DOM cargado, inicializando WhatsApp Manager...');
try {
window.whatsappManager = new SimpleWhatsAppManager();
console.log('WhatsApp Manager inicializado correctamente');
} catch (error) {
console.error('Error inicializando WhatsApp Manager:', error);
}
});
// Funciones globales para modales (compatibilidad con el HTML existente)
window.openCreateTemplateModal = function () {
console.log('🔵 Abriendo modal de plantilla...');
const modalElement = document.getElementById('createTemplateModal');
if (!modalElement) {
console.error('❌ Modal de plantilla no encontrado');
return;
}
try {
const form = document.getElementById('template-form');
if (form) form.reset();
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
const modal = new bootstrap.Modal(modalElement);
modal.show();
} else {
modalElement.style.display = 'block';
modalElement.classList.add('show');
}
console.log('✅ Modal de plantilla abierto');
} catch (error) {
console.error('❌ Error abriendo modal de plantilla:', error);
}
};
window.openCreateMenuModal = function () {
console.log('🔵 Abriendo modal de menú...');
const modalElement = document.getElementById('createMenuModal');
if (!modalElement) {
console.error('❌ Modal de menú no encontrado');
return;
}
try {
const form = document.getElementById('menu-form');
if (form) form.reset();
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
const modal = new bootstrap.Modal(modalElement);
modal.show();
} else {
modalElement.style.display = 'block';
modalElement.classList.add('show');
}
console.log('✅ Modal de menú abierto');
} catch (error) {
console.error('❌ Error abriendo modal de menú:', error);
}
};
// Función global para debugging
window.debugAPI = async function (endpoint) {
try {
const manager = window.whatsappManager;
if (manager) {
const result = await manager.apiCall(endpoint);
console.log('Debug API Result:', result);
return result;
} else {
console.error('Manager no inicializado');
}
} catch (error) {
console.error('Debug API Error:', error);
}
};
// Función para abrir ventana de chat
window.openChatWindow = async function (userId) {
console.log('Abriendo chat del usuario:', userId);
// Marcar conversación como leída en el backend y refrescar lista localmente
try {
await fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})});
if (window.whatsappManager && typeof window.whatsappManager.loadConversations === 'function') {
window.whatsappManager.loadConversations();
}
} catch (e) {
console.warn('mark_conversation_read failed', e);
}
// Abrir nueva ventana de chat
const chatUrl = `chat_window.php?user_id=${userId}&debug=true`;
const windowFeatures = 'width=1000,height=700,resizable=yes,scrollbars=yes,status=yes,toolbar=no,menubar=no,location=no';
const chatWindow = window.open(chatUrl, `chat_${userId}`, windowFeatures);
if (chatWindow) {
chatWindow.focus();
} else {
alert('No se pudo abrir la ventana de chat. Verifica que tu navegador permita ventanas emergentes.');
}
};
// Función para mostrar modal de chat
function showChatModal(user, conversations) {
// Escapar datos del usuario para evitar XSS
const userName = escapeHtml(user.name || 'Usuario');
const userPhone = escapeHtml(user.phone_number || '');
const modalHtml = `
<div class="modal fade" id="chatModal" tabindex="-1" aria-labelledby="chatModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="chatModalLabel">
<i class="fab fa-whatsapp"></i> ${userName}
<small class="opacity-75">(${userPhone})</small>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div id="chat-conversations" class="chat-container" style="height: 400px; overflow-y: auto; padding: 15px; background-color: #e5ddd5;">
${generateChatconversations(conversations)}
</div>
<div class="border-top p-3">
<div class="row">
<div class="col">
<div class="input-group">
<select id="messageType" class="form-select" style="max-width: 120px;">
<option value="text">Texto</option>
<option value="template">Plantilla</option>
</select>
<input type="text" id="messageInput" class="form-control" placeholder="Escribe tu mensaje...">
<button class="btn btn-success" onclick="sendChatMessage(${user.id})">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
<div id="templateSelector" class="mt-2" style="display: none;">
<select id="templateSelect" class="form-select">
<option value="">Selecciona una plantilla...</option>
<option value="hello_world">hello_world</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('chatModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Event listener para cambio de tipo de mensaje
document.getElementById('messageType').addEventListener('change', function() {
const templateSelector = document.getElementById('templateSelector');
const messageInput = document.getElementById('messageInput');
if (this.value === 'template') {
templateSelector.style.display = 'block';
messageInput.placeholder = 'Parámetros de la plantilla (opcional)';
} else {
templateSelector.style.display = 'none';
messageInput.placeholder = 'Escribe tu mensaje...';
}
});
// Mostrar modal
const modal = new bootstrap.Modal(document.getElementById('chatModal'));
modal.show();
// Scroll al final
setTimeout(() => {
const chatContainer = document.getElementById('chat-conversations');
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 200);
}
// Generar HTML de mensajes del chat
function generateChatconversations(conversations) {
if (!conversations || conversations.length === 0) {
return '<div class="text-center text-muted py-4"><i class="fas fa-comments fa-3x"></i><br><br>No hay mensajes en esta conversación</div>';
}
let html = '';
conversations.forEach(msg => {
const isOutgoing = msg.direction === 'outgoing';
const messageClass = isOutgoing ? 'outgoing' : 'incoming';
const alignClass = isOutgoing ? 'justify-content-end' : 'justify-content-start';
const bgClass = isOutgoing ? 'bg-success text-white' : 'bg-white';
// Escapar el contenido del mensaje
const messageContent = escapeHtml(msg.content) || '<em>Sin contenido</em>';
const messageTime = escapeHtml(msg.time || '');
html += `
<div class="d-flex ${alignClass} mb-2">
<div class="message-bubble ${bgClass} rounded-3 p-2 shadow-sm" style="max-width: 70%;">
<div class="message-content">
${messageContent}
</div>
<div class="message-time text-${isOutgoing ? 'light' : 'muted'}" style="font-size: 0.75rem;">
${messageTime} ${isOutgoing ? '✓✓' : ''}
</div>
</div>
</div>
`;
});
return html;
}
// Función para escapar HTML y prevenir XSS
function escapeHtml(text) {
if (!text) return '';
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.toString().replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Función para enviar mensaje desde el chat
window.sendChatMessage = async function (userId) {
const messageType = document.getElementById('messageType').value;
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (!message && messageType === 'text') {
alert('Por favor escribe un mensaje');
return;
}
if (messageType === 'template') {
const templateSelect = document.getElementById('templateSelect');
const template = templateSelect.value;
if (!template) {
alert('Por favor selecciona una plantilla');
return;
}
await sendTemplateMessage(userId, template, message);
} else {
await sendTextMessage(userId, message);
}
messageInput.value = '';
};
// Función para enviar mensaje de texto
async function sendTextMessage(userId, message) {
try {
const manager = window.whatsappManager;
const usersResponse = await manager.apiCall('get_users.php?debug=true');
// Manejar diferentes formatos de respuesta
let users = [];
if (Array.isArray(usersResponse)) {
users = usersResponse;
} else if (usersResponse && usersResponse.data && Array.isArray(usersResponse.data)) {
users = usersResponse.data;
} else if (usersResponse && usersResponse.users && Array.isArray(usersResponse.users)) {
users = usersResponse.users;
} else {
console.error('Formato de respuesta de usuarios inesperado:', usersResponse);
throw new Error('Error obteniendo lista de usuarios');
}
const user = users.find(u => u.id == userId);
if (!user) {
throw new Error('Usuario no encontrado');
}
const response = await manager.apiCall('send_message.php', {
method: 'POST',
body: {
recipient: user.phone_number,
type: 'text',
message: message
}
});
if (response && response.success) {
// Refrescar chat
openChatWindow(userId);
showAlert('Mensaje enviado correctamente', 'success');
} else {
throw new Error(response.error || 'Error enviando mensaje');
}
} catch (error) {
console.error('Error enviando mensaje:', error);
showAlert('Error enviando mensaje: ' + error.message, 'danger');
}
}
// Función para enviar mensaje de plantilla
async function sendTemplateMessage(userId, template, parameters) {
try {
const manager = window.whatsappManager;
const usersResponse = await manager.apiCall('get_users.php?debug=true');
// Manejar diferentes formatos de respuesta
let users = [];
if (Array.isArray(usersResponse)) {
users = usersResponse;
} else if (usersResponse && usersResponse.data && Array.isArray(usersResponse.data)) {
users = usersResponse.data;
} else if (usersResponse && usersResponse.users && Array.isArray(usersResponse.users)) {
users = usersResponse.users;
} else {
console.error('Formato de respuesta de usuarios inesperado:', usersResponse);
throw new Error('Error obteniendo lista de usuarios');
}
const user = users.find(u => u.id == userId);
if (!user) {
throw new Error('Usuario no encontrado');
}
const response = await manager.apiCall('send_message.php', {
method: 'POST',
body: {
recipient: user.phone_number,
type: 'template',
template: template,
language: 'en_US',
parameters: parameters ? parameters.split(',') : []
}
});
if (response && response.success) {
// Refrescar chat
openChatWindow(userId);
showAlert('Mensaje de plantilla enviado correctamente', 'success');
} else {
throw new Error(response.error || 'Error enviando plantilla');
}
} catch (error) {
console.error('Error enviando plantilla:', error);
showAlert('Error enviando plantilla: ' + error.message, 'danger');
}
}
// Función para ver detalles de conversación
window.viewConversationDetails = async function (userId) {
console.log('Mostrando detalles de conversación para usuario:', userId);
try {
// Obtener datos del usuario
const usersResponse = await window.whatsappManager.apiCall('get_users.php');
let user = null;
if (usersResponse && usersResponse.success && Array.isArray(usersResponse.data)) {
user = usersResponse.data.find(u => u.id == userId);
}
if (!user) {
window.whatsappManager.showError('Usuario no encontrado');
return;
}
// Obtener estadísticas de la conversación (simuladas por ahora)
const stats = {
total_conversations: Math.floor(Math.random() * 50) + 1,
conversations_today: Math.floor(Math.random() * 10),
last_activity: user.updated_at || user.created_at,
first_contact: user.created_at,
status: user.status || 'active'
};
// Mostrar modal con detalles
showConversationDetailsModal(user, stats);
} catch (error) {
console.error('Error obteniendo detalles de conversación:', error);
window.whatsappManager.showError('Error obteniendo detalles: ' + error.message);
}
};
// Función para mostrar modal de detalles de conversación
function showConversationDetailsModal(user, stats) {
console.log('Mostrando modal de detalles para:', user);
const userName = escapeHtml(user.name || 'Sin nombre');
const userPhone = escapeHtml(user.phone_number || '');
const userStatus = user.status || 'unknown';
const currentMenu = user.current_menu || 'Sin menú';
// Formatear fechas
const firstContact = user.created_at ? new Date(user.created_at).toLocaleString('es') : 'N/A';
const lastActivity = stats.last_activity ? new Date(stats.last_activity).toLocaleString('es') : 'N/A';
const statusBadgeClass = userStatus === 'active' ? 'success' : userStatus === 'inactive' ? 'secondary' : 'warning';
const modalHtml = `
<div class="modal fade" id="conversationDetailsModal" tabindex="-1" aria-labelledby="conversationDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title" id="conversationDetailsModalLabel">
<i class="fas fa-info-circle"></i> Detalles de Conversación
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-3"><i class="fas fa-user"></i> Información del Usuario</h6>
<div class="mb-3">
<strong>ID:</strong> ${user.id}
</div>
<div class="mb-3">
<strong>Nombre:</strong> ${userName}
</div>
<div class="mb-3">
<strong>Teléfono:</strong> ${userPhone}
</div>
<div class="mb-3">
<strong>Estado:</strong>
<span class="badge bg-${statusBadgeClass}">${userStatus.toUpperCase()}</span>
</div>
<div class="mb-3">
<strong>Menú Actual:</strong> ${currentMenu}
</div>
<div class="mb-3">
<strong>Primer Contacto:</strong><br>
<small class="text-muted">${firstContact}</small>
</div>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-3"><i class="fas fa-chart-line"></i> Estadísticas de Conversación</h6>
<div class="mb-3">
<strong>Total de Mensajes:</strong>
<span class="badge bg-primary">${stats.total_conversations || 0}</span>
</div>
<div class="mb-3">
<strong>Mensajes Hoy:</strong>
<span class="badge bg-success">${stats.conversations_today || 0}</span>
</div>
<div class="mb-3">
<strong>Última Actividad:</strong><br>
<small class="text-muted">${lastActivity}</small>
</div>
<div class="mb-3">
<strong>Tiempo desde último mensaje:</strong><br>
<small class="text-muted">${calculateTimeAgo(stats.last_activity)}</small>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-12">
<h6 class="text-muted mb-3"><i class="fas fa-cogs"></i> Acciones Rápidas</h6>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-success btn-sm" onclick="openChatWindow(${user.id}); bootstrap.Modal.getInstance(document.getElementById('conversationDetailsModal')).hide();">
<i class="fas fa-comments"></i> Abrir Chat
</button>
<button class="btn btn-warning btn-sm" onclick="editUser(${user.id})">
<i class="fas fa-edit"></i> Editar Usuario
</button>
<button class="btn btn-info btn-sm" onclick="viewUserHistory(${user.id})">
<i class="fas fa-history"></i> Ver Historial
</button>
<button class="btn btn-danger btn-sm" onclick="confirmDeleteUser(${user.id})">
<i class="fas fa-trash"></i> Eliminar
</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('conversationDetailsModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Mostrar modal
const modal = new bootstrap.Modal(document.getElementById('conversationDetailsModal'));
modal.show();
}
// Función auxiliar para calcular tiempo transcurrido
function calculateTimeAgo(datetime) {
if (!datetime) return 'N/A';
const now = new Date();
const date = new Date(datetime);
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) return 'Hace menos de 1 minuto';
if (diffInSeconds < 3600) return `Hace ${Math.floor(diffInSeconds / 60)} minutos`;
if (diffInSeconds < 86400) return `Hace ${Math.floor(diffInSeconds / 3600)} horas`;
if (diffInSeconds < 2592000) return `Hace ${Math.floor(diffInSeconds / 86400)} días`;
return date.toLocaleDateString('es');
}
// Función para mostrar alertas
function showAlert(message, type = 'info') {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show position-fixed" style="top: 20px; right: 20px; z-index: 9999;" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', alertHtml);
// Auto-dismiss después de 5 segundos
setTimeout(() => {
const alert = document.querySelector('.alert');
if (alert) {
alert.remove();
}
}, 5000);
}
// Función para ver conversación específica (mantener compatibilidad)
window.viewConversation = function (userId) {
openChatWindow(userId);
};
// Función para editar usuario
window.editUser = function (userId) {
console.log('Editando usuario:', userId);
// Buscar el usuario en los datos cargados
if (window.whatsappManager && window.whatsappManager.usersData) {
const user = window.whatsappManager.usersData.find(u => u.id == userId);
if (user) {
showEditUserModal(user);
} else {
alert('Error: Usuario no encontrado');
}
} else {
alert('Error: Datos no disponibles, recarga la página');
}
};
// Función para ver historial de usuario
window.viewUserHistory = function (userId) {
console.log('Viendo historial del usuario:', userId);
// Por ahora mostrar un modal simple
const modalHtml = `
<div class="modal fade" id="userHistoryModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="fas fa-history"></i> Historial del Usuario
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="text-center py-4">
<i class="fas fa-history fa-3x text-muted mb-3"></i>
<h5>Historial de Usuario</h5>
<p class="text-muted">Para ver el historial completo de conversaciones, usa la pestaña <strong>"Conversaciones"</strong> en el menú principal.<br>
Allí podrás ver todos los mensajes y el historial detallado de cada usuario.</p>
<a href="#" onclick="window.whatsappManager.showTab('conversations'); bootstrap.Modal.getInstance(document.getElementById('userHistoryModal')).hide(); return false;" class="btn btn-primary mt-3">
<i class="fas fa-comments"></i> Ir a Conversaciones
</a>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior
const existing = document.getElementById('userHistoryModal');
if (existing) existing.remove();
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('userHistoryModal'));
modal.show();
};
// Función para confirmar eliminación de usuario
window.confirmDeleteUser = function (userId) {
if (confirm('¿Estás seguro de que deseas eliminar este usuario?\n\nEsta acción no se puede deshacer.')) {
deleteUser(userId);
}
};
// Función para eliminar usuario
window.deleteUser = async function (userId) {
console.log('Eliminando usuario:', userId);
try {
const response = await window.whatsappManager.apiCall('delete_user.php', {
method: 'POST',
body: { user_id: userId }
});
if (response && response.success) {
window.whatsappManager.showSuccess('Usuario eliminado correctamente');
// Cerrar modal si está abierto
const modal = document.getElementById('conversationDetailsModal');
if (modal) {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) bsModal.hide();
}
// Recargar datos
if (window.whatsappManager.currentTab === 'users') {
window.whatsappManager.loadUsers();
} else if (window.whatsappManager.currentTab === 'conversations') {
window.whatsappManager.loadConversations();
}
} else {
window.whatsappManager.showError('Error eliminando usuario: ' + (response?.error || 'Error desconocido'));
}
} catch (error) {
window.whatsappManager.showError('Error eliminando usuario: ' + error.message);
}
};
// Función para eliminar usuario
window.deleteUser = function (userId) {
console.log('Eliminando usuario:', userId);
// Buscar el usuario para mostrar información
let userName = 'Usuario ' + userId;
let userPhone = '';
if (window.whatsappManager && window.whatsappManager.usersData) {
const user = window.whatsappManager.usersData.find(u => u.id == userId);
if (user) {
userName = user.name || 'Usuario ' + userId;
userPhone = user.phone_number || '';
}
}
if (window.whatsappManager) {
window.whatsappManager.deleteUserAction(userId, userName, userPhone);
}
};
// Funciones para plantillas
window.showCreateTemplateModal = function () {
const modal = new bootstrap.Modal(document.getElementById('createTemplateModal'));
modal.show();
};
window.editTemplate = function (templateId) {
console.log('Editando plantilla:', templateId);
// Las plantillas de WhatsApp no se pueden editar una vez creadas
// Solo se pueden crear nuevas o eliminar existentes
if (window.whatsappManager) {
window.whatsappManager.showInfo(
'Las plantillas de WhatsApp aprobadas no se pueden editar una vez creadas. ' +
'Si necesitas cambiar el contenido, debes crear una nueva plantilla y enviarla a revisión.'
);
} else {
alert('Las plantillas no se pueden editar. Crea una nueva plantilla si necesitas modificar el contenido.');
}
};
window.deleteTemplate = function (templateId) {
console.log('Eliminando plantilla:', templateId);
// Buscar la plantilla para mostrar información
let templateName = 'Plantilla ' + templateId;
if (window.whatsappManager && window.whatsappManager.templatesData) {
const template = window.whatsappManager.templatesData.find(t => t.id == templateId);
if (template) {
templateName = template.name || 'Plantilla ' + templateId;
}
}
if (confirm(`¿Está seguro que desea eliminar la plantilla "${templateName}"?\n\nEsta acción no se puede deshacer.`)) {
if (window.whatsappManager) {
window.whatsappManager.deleteTemplateAction(templateId);
}
}
};
// Función para editar usuario - Modal simple
window.showEditUserModal = function(user) {
const modalHtml = `
<div class="modal fade" id="editUserModal" 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-user-edit"></i> Editar Usuario
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="edit-user-form">
<input type="hidden" id="edit-user-id" value="${user.id}">
<div class="mb-3">
<label class="form-label">Teléfono</label>
<input type="text" class="form-control" value="${user.phone_number}" readonly>
</div>
<div class="mb-3">
<label class="form-label">Nombre</label>
<input type="text" class="form-control" id="edit-user-name" value="${user.name || ''}" placeholder="Nombre del usuario">
</div>
<div class="mb-3">
<label class="form-label">Estado</label>
<select class="form-control" id="edit-user-status">
<option value="active" ${user.status === 'active' ? 'selected' : ''}>Activo</option>
<option value="inactive" ${user.status === 'inactive' ? 'selected' : ''}>Inactivo</option>
<option value="blocked" ${user.status === 'blocked' ? 'selected' : ''}>Bloqueado</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" onclick="saveEditedUser()">
<i class="fas fa-save"></i> Guardar Cambios
</button>
</div>
</div>
</div>
</div>
`;
// Eliminar modal anterior si existe
const existingModal = document.getElementById('editUserModal');
if (existingModal) {
existingModal.remove();
}
// Agregar nuevo modal
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Mostrar modal
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
};
// Función para guardar usuario editado
window.saveEditedUser = async function() {
const userId = document.getElementById('edit-user-id').value;
const name = document.getElementById('edit-user-name').value;
const status = document.getElementById('edit-user-status').value;
if (window.whatsappManager) {
try {
const response = await window.whatsappManager.apiCall('update_user.php', {
method: 'POST',
body: {
id: userId,
name: name,
status: status
}
});
if (response && response.success) {
window.whatsappManager.showSuccess('Usuario actualizado correctamente');
// Cerrar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
if (modal) modal.hide();
// Recargar lista
window.whatsappManager.loadUsers();
} else {
window.whatsappManager.showError(`Error: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
window.whatsappManager.showError('Error actualizando usuario: ' + error.message);
}
}
};
// Hacer que app.saveTemplate() y app.saveMenu() funcionen
window.app = {
saveTemplate: function () {
if (window.whatsappManager) {
window.whatsappManager.saveTemplate();
}
},
saveMenu: function () {
if (window.whatsappManager) {
window.whatsappManager.saveMenu();
}
},
saveAutoResponse: function () {
if (window.whatsappManager) {
window.whatsappManager.saveAutoResponse();
}
}
};
// Funciones globales para gestión de menús
window.editMenu = async function (menuId) {
console.log('Editando menú:', menuId);
try {
// Obtener datos del menú
const response = await window.whatsappManager.apiCall('get_menus.php');
if (response && response.success) {
const menu = response.data.find(m => m.id == menuId);
if (menu) {
showEditMenuModal(menu);
} else {
window.whatsappManager.showError('Menú no encontrado');
}
} else {
window.whatsappManager.showError('Error cargando datos del menú');
}
} catch (error) {
console.error('Error editando menú:', error);
window.whatsappManager.showError('Error editando menú: ' + error.message);
}
};
window.deleteMenu = function (menuId) {
console.log('Eliminando menú:', menuId);
if (confirm(`¿Está seguro que desea eliminar el menú ${menuId}?`)) {
if (window.whatsappManager) {
window.whatsappManager.deleteMenuAction(menuId);
}
}
};
window.viewMenuOptions = async function (menuId) {
console.log('Viendo opciones del menú:', menuId);
try {
// Obtener datos del menú y sus opciones
const response = await window.whatsappManager.apiCall('get_menus.php');
if (response && response.success) {
const menu = response.data.find(m => m.id == menuId);
if (menu) {
showMenuOptionsModal(menu);
} else {
window.whatsappManager.showError('Menú no encontrado');
}
} else {
window.whatsappManager.showError('Error cargando opciones del menú');
}
} catch (error) {
console.error('Error viendo opciones:', error);
window.whatsappManager.showError('Error viendo opciones: ' + error.message);
}
};
// Función para agregar nueva opción al menú
window.addMenuOption = function () {
const container = document.getElementById('menu-options-container');
if (!container) return;
const optionIndex = container.children.length + 1;
const optionHtml = `
<div class="menu-option-item border rounded p-3 mb-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0 flex-grow-1 text-truncate" style="max-width:85%">Opción ${optionIndex}</h6>
<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="removeMenuOption(this)" aria-label="Eliminar opción">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row">
<div class="col-md-3">
<label class="form-label">Clave</label>
<input type="text" class="form-control option-key" placeholder="ej: 1" required>
</div>
<div class="col-md-4">
<label class="form-label">Texto</label>
<input type="text" class="form-control option-text" placeholder="ej: Información" required>
</div>
<div class="col-md-3">
<label class="form-label">Acción</label>
<select class="form-select option-action">
<option value="message">Enviar mensaje</option>
<option value="submenu">Submenú</option>
<option value="template">Plantilla</option>
<option value="function">Función personalizada</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Valor</label>
<input type="text" class="form-control option-value" placeholder="Contenido">
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', optionHtml);
};
// Función para remover opción del menú
window.removeMenuOption = function (button) {
const optionItem = button.closest('.menu-option-item');
if (optionItem) {
optionItem.remove();
updateOptionNumbers();
}
};
// Funciones globales para gestión de respuestas automáticas
// La función real está más abajo en el archivo (línea ~2390)
window.deleteAutoResponse = function (responseId) {
console.log('Eliminando respuesta automática:', responseId);
if (confirm(`¿Está seguro que desea eliminar la respuesta automática ${responseId}?`)) {
if (window.whatsappManager) {
window.whatsappManager.deleteAutoResponseAction(responseId);
}
}
};
// Funciones para modal de autorespuestas
window.showCreateAutoResponseModal = function() {
document.getElementById('autoresponse-form').reset();
document.getElementById('autoresponse-id').value = '';
document.getElementById('createAutoResponseModalLabel').innerHTML = '<i class="fas fa-robot"></i> Crear Nueva Respuesta Automática';
// Reset campos específicos
updateTriggerFields('keyword');
updateResponseTypeFields('text');
const modal = new bootstrap.Modal(document.getElementById('createAutoResponseModal'));
modal.show();
};
window.showEditAutoResponseModal = function(autoresponse) {
document.getElementById('autoresponse-id').value = autoresponse.id;
document.getElementById('createAutoResponseModalLabel').innerHTML = '<i class="fas fa-edit"></i> Editar Respuesta Automática';
// Llenar campos del formulario
document.getElementById('autoresponse-trigger-type').value = autoresponse.trigger_type;
document.getElementById('autoresponse-trigger-value').value = autoresponse.trigger_value || '';
document.getElementById('autoresponse-text').value = autoresponse.response_text;
document.getElementById('autoresponse-type').value = autoresponse.response_type;
document.getElementById('autoresponse-priority').value = autoresponse.priority;
document.getElementById('autoresponse-active').value = autoresponse.is_active ? '1' : '0';
document.getElementById('autoresponse-template-name').value = autoresponse.template_name || '';
document.getElementById('autoresponse-menu-id').value = autoresponse.menu_id || '';
// Actualizar campos según el tipo
updateTriggerFields(autoresponse.trigger_type);
updateResponseTypeFields(autoresponse.response_type);
const modal = new bootstrap.Modal(document.getElementById('createAutoResponseModal'));
modal.show();
};
window.updateTriggerFields = function(triggerType) {
const valueGroup = document.getElementById('trigger-value-group');
const valueLabel = document.getElementById('trigger-value-label');
const valueInput = document.getElementById('autoresponse-trigger-value');
const valueHelp = document.getElementById('trigger-value-help');
if (!valueGroup || !valueLabel || !valueInput || !valueHelp) {
console.warn('Elementos del modal de autorespuesta no encontrados');
return;
}
switch (triggerType) {
case 'keyword':
valueGroup.style.display = 'block';
valueLabel.textContent = 'Palabras Clave *';
valueInput.placeholder = 'hola,buenos días,hi,hello';
valueInput.required = true;
valueHelp.textContent = 'Separa múltiples palabras con comas';
break;
case 'contains':
valueGroup.style.display = 'block';
valueLabel.textContent = 'Texto a Buscar *';
valueInput.placeholder = 'precio';
valueInput.required = true;
valueHelp.textContent = 'Texto que debe contener el mensaje del usuario';
break;
case 'exact':
valueGroup.style.display = 'block';
valueLabel.textContent = 'Mensaje Exacto *';
valueInput.placeholder = 'quiero información';
valueInput.required = true;
valueHelp.textContent = 'El mensaje debe coincidir exactamente';
break;
case 'welcome':
case 'default':
valueGroup.style.display = 'none';
valueInput.required = false;
valueInput.value = '';
break;
}
};
window.updateResponseTypeFields = function(responseType) {
const templateFields = document.getElementById('template-fields');
const menuFields = document.getElementById('menu-fields');
if (!templateFields || !menuFields) {
console.warn('Campos adicionales del modal no encontrados');
return;
}
// Ocultar todos los campos adicionales
templateFields.style.display = 'none';
menuFields.style.display = 'none';
// Mostrar campos según el tipo
switch (responseType) {
case 'template':
templateFields.style.display = 'block';
break;
case 'menu':
menuFields.style.display = 'block';
// Cargar menús disponibles si no están cargados
loadMenusForSelect();
break;
}
};
window.loadMenusForSelect = function() {
fetch('/api/get_menus.php')
.then(response => response.json())
.then(data => {
if (data.success) {
const select = document.getElementById('autoresponse-menu-id');
if (select) {
select.innerHTML = '<option value="">Seleccionar menú...</option>';
data.data.forEach(menu => {
const option = document.createElement('option');
option.value = menu.id;
option.textContent = `${menu.name} (${menu.menu_key})`;
select.appendChild(option);
});
}
}
})
.catch(error => console.error('Error loading menus for select:', error));
};
// Actualizar función editAutoResponse para usar el modal
window.editAutoResponse = function (responseId) {
console.log('Editando respuesta automática:', responseId);
// Buscar la respuesta en los datos cargados
if (window.whatsappManager && window.whatsappManager.autoResponsesData) {
const autoresponse = window.whatsappManager.autoResponsesData.find(ar => ar.id == responseId);
if (autoresponse) {
showEditAutoResponseModal(autoresponse);
} else {
console.error('Respuesta automática no encontrada:', responseId);
alert('Error: No se pudo cargar la respuesta automática');
}
} else {
console.error('Datos de respuestas automáticas no disponibles');
alert('Error: Datos no disponibles, recarga la página');
}
};
window.showCreateAutoResponseModal = function () {
console.log('🔵 Abriendo modal de respuesta automática...');
const modalElement = document.getElementById('createAutoResponseModal');
if (!modalElement) {
console.error('❌ Modal de respuesta automática no encontrado');
return;
}
try {
const form = document.getElementById('autoresponse-form');
if (form) form.reset();
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
const modal = new bootstrap.Modal(modalElement);
modal.show();
} else {
modalElement.style.display = 'block';
modalElement.classList.add('show');
}
console.log('✅ Modal de respuesta automática abierto');
} catch (error) {
console.error('❌ Error abriendo modal de respuesta automática:', error);
}
};
// Función para cambiar tipo de disparador
window.toggleTriggerType = function () {
const triggerType = document.getElementById('autoresponse-type')?.value;
const triggerContainer = document.getElementById('trigger-container');
if (triggerContainer) {
if (triggerType === 'welcome' || triggerType === 'default') {
triggerContainer.style.display = 'none';
} else {
triggerContainer.style.display = 'block';
const triggerInput = document.getElementById('autoresponse-trigger');
const triggerLabel = document.querySelector('label[for="autoresponse-trigger"]');
if (triggerInput && triggerLabel) {
switch (triggerType) {
case 'keyword':
triggerLabel.textContent = 'Palabra Clave *';
triggerInput.placeholder = 'ej: hola, info, precios';
break;
case 'contains':
triggerLabel.textContent = 'Texto que debe contener *';
triggerInput.placeholder = 'ej: precio, información';
break;
case 'exact':
triggerLabel.textContent = 'Mensaje exacto *';
triggerInput.placeholder = 'ej: ¿Cuáles son sus precios?';
break;
}
}
}
}
};
// Funciones para el formulario de mensajes
window.toggleRecipientType = function () {
const recipientType = document.getElementById('recipient-type').value;
const existingGroup = document.getElementById('existing-recipient-group');
const manualGroup = document.getElementById('manual-recipient-group');
if (recipientType === 'manual') {
existingGroup.style.display = 'none';
manualGroup.style.display = 'block';
} else {
existingGroup.style.display = 'block';
manualGroup.style.display = 'none';
}
};
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 === 'template') {
textGroup.style.display = 'none';
templateGroup.style.display = 'block';
} else {
textGroup.style.display = 'block';
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');
// Detectar header de imagen
const hasImageHeader = (template.header_type || '').toUpperCase() === 'IMAGE';
let html = '';
if (hasImageHeader) {
html += `
<div class="mb-3 border rounded p-2 bg-light" id="msg-image-header-section">
<label class="form-label fw-bold text-primary"><i class="fas fa-image me-1"></i> Imagen del encabezado</label>
<p class="text-muted small mb-2">Esta plantilla requiere una imagen de encabezado dinámica. Súbela aquí.</p>
<input type="file" class="form-control form-control-sm" id="message-template-image" accept="image/jpeg,image/png,image/webp" required>
<div id="msg-image-preview" class="mt-2" style="display:none">
<img id="msg-image-thumb" src="" style="max-height:100px;border-radius:6px;" class="border">
</div>
</div>
`;
setTimeout(() => {
const imgInput = document.getElementById('message-template-image');
if (imgInput) imgInput.addEventListener('change', function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = e => {
document.getElementById('msg-image-thumb').src = e.target.result;
document.getElementById('msg-image-preview').style.display = 'block';
};
reader.readAsDataURL(file);
}
});
}, 50);
}
if (template.variables && template.variables.length > 0) {
console.log(`✅ Se encontraron ${template.variables.length} variables`);
html += `<label class="form-label fw-bold${hasImageHeader ? ' mt-2' : ''}"><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 {
if (!hasImageHeader) html += '<p class="text-muted small">Esta plantilla no tiene variables de texto</p>';
console.log('⚠️ Template sin variables de texto');
variablesContainer.innerHTML = html;
variablesContainer.style.display = (hasImageHeader || html) ? 'block' : 'none';
}
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
window.runDiagnostic = function () {
const resultsDiv = document.getElementById('diagnostic-results');
// Mostrar spinner de carga
resultsDiv.innerHTML = `
<div class="text-center">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p class="mt-2">Ejecutando diagnóstico...</p>
</div>
`;
fetch('./api/diagnose_whatsapp.php')
.then(response => response.json())
.then(data => {
if (data.success) {
displayDiagnosticResults(data);
} else {
resultsDiv.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle"></i>
Error: ${data.error || 'Error desconocido'}
</div>
`;
}
})
.catch(error => {
console.error('Error en diagnóstico:', error);
resultsDiv.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle"></i>
Error de conexión: ${error.message}
</div>
`;
});
};
function displayDiagnosticResults(data) {
const resultsDiv = document.getElementById('diagnostic-results');
let html = `
<div class="diagnostic-results">
<small class="text-muted">Última verificación: ${data.timestamp}</small>
<!-- Estado de Configuración -->
<div class="mt-3">
<h6><i class="fas fa-cog"></i> Configuración</h6>
<div class="list-group list-group-flush">
<div class="list-group-item d-flex justify-content-between align-items-center py-2">
<small>Phone Number ID</small>
<span class="badge bg-${data.config_status.phone_number_id ? 'success' : 'danger'}">
${data.config_status.phone_number_id ? '✓' : '✗'}
</span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center py-2">
<small>Access Token</small>
<span class="badge bg-${data.config_status.access_token ? 'success' : 'danger'}">
${data.config_status.access_token ? '✓' : '✗'}
</span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center py-2">
<small>Webhook Token</small>
<span class="badge bg-${data.config_status.webhook_verify_token ? 'success' : 'danger'}">
${data.config_status.webhook_verify_token ? '✓' : '✗'}
</span>
</div>
</div>
</div>
<!-- Estado de Plantillas -->
<div class="mt-3">
<h6><i class="fas fa-file-text"></i> Plantillas</h6>
<div class="row text-center">
<div class="col-4">
<div class="text-success">
<strong>${data.template_status.approved || 0}</strong><br>
<small>Aprobadas</small>
</div>
</div>
<div class="col-4">
<div class="text-warning">
<strong>${data.template_status.pending || 0}</strong><br>
<small>Pendientes</small>
</div>
</div>
<div class="col-4">
<div class="text-danger">
<strong>${data.template_status.rejected || 0}</strong><br>
<small>Rechazadas</small>
</div>
</div>
</div>
</div>
<!-- Estado de API -->
<div class="mt-3">
<h6><i class="fas fa-cloud"></i> API WhatsApp</h6>
<div class="alert alert-${data.api_status.reachable ? 'success' : 'danger'} p-2">
<small>
${data.api_status.reachable ?
`✓ Conectado - ${data.api_status.phone_number || 'N/A'}` :
`✗ ${data.api_status.error || 'No conectado'}`
}
</small>
</div>
</div>
`;
// Recomendaciones
if (data.recommendations && data.recommendations.length > 0) {
html += `
<div class="mt-3">
<h6><i class="fas fa-lightbulb"></i> Recomendaciones</h6>
<ul class="list-unstyled">
`;
data.recommendations.forEach(rec => {
html += `<li><small><i class="fas fa-arrow-right text-muted"></i> ${rec}</small></li>`;
});
html += `
</ul>
</div>
`;
}
html += '</div>';
resultsDiv.innerHTML = html;
}
// ==============================================
// EVENT LISTENERS PARA MODALES DE AUTORESPUESTAS
// ==============================================
// Agregar event listeners cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', function() {
// Event listener para cambio de tipo de trigger
const triggerTypeSelect = document.getElementById('autoresponse-trigger-type');
if (triggerTypeSelect) {
triggerTypeSelect.addEventListener('change', function() {
updateTriggerFields(this.value);
});
}
// Event listener para cambio de tipo de respuesta
const responseTypeSelect = document.getElementById('autoresponse-type');
if (responseTypeSelect) {
responseTypeSelect.addEventListener('change', function() {
updateResponseTypeFields(this.value);
});
}
// Event listener para el modal cuando se muestre
const autoResponseModal = document.getElementById('createAutoResponseModal');
if (autoResponseModal) {
autoResponseModal.addEventListener('shown.bs.modal', function () {
// Enfocar el primer campo cuando se abra el modal
const firstInput = autoResponseModal.querySelector('input, select, textarea');
if (firstInput) {
firstInput.focus();
}
});
}
});
// ─── Gestión del documento PDF de Términos y Condiciones ─────────────────────
(function () {
function showTermsStatus(msg, type) {
const el = document.getElementById('terms-upload-status');
if (!el) return;
el.style.display = 'block';
el.className = 'mt-2 small text-' + (type || 'secondary');
el.textContent = msg;
}
function _fallbackCopy(text) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;top:-999px;left:-999px;opacity:0;';
document.body.appendChild(ta);
ta.focus();
ta.select();
try {
const ok = document.execCommand('copy');
showTermsStatus(ok ? '✔ URL copiada al portapapeles.' : '✖ No se pudo copiar.', ok ? 'success' : 'danger');
} catch (e) {
showTermsStatus('✖ No se pudo copiar: ' + text, 'danger');
}
document.body.removeChild(ta);
}
async function loadTermsDoc() {
try {
const res = await fetch('api/get_terms_config.php');
const json = await res.json();
if (!json.success || !json.data) return;
const d = json.data;
const nameEl = document.getElementById('terms-doc-name');
const verEl = document.getElementById('terms-doc-version');
const linkEl = document.getElementById('terms-doc-link');
const copyBtn = document.getElementById('terms-doc-copy-btn');
const currentEl = document.getElementById('terms-doc-current');
const msgAcEl = document.getElementById('terms-message');
const msgRjEl = document.getElementById('terms-rejected-message');
if (nameEl) nameEl.textContent = d.documento_nombre || '(sin nombre)';
if (verEl) verEl.textContent = d.version || '—';
if (linkEl) linkEl.href = d.documento_url || '#';
if (currentEl) currentEl.style.display = d.documento_url ? '' : 'none';
// Rellenar textos si están en terms_versions (no sobreescribir si ya se cargaron)
if (msgAcEl && !msgAcEl.value && d.mensaje_aceptacion) {
msgAcEl.value = d.mensaje_aceptacion;
}
if (msgRjEl && !msgRjEl.value && d.mensaje_rechazo) {
msgRjEl.value = d.mensaje_rechazo;
}
if (copyBtn && d.documento_url) {
copyBtn.onclick = () => {
const url = d.documento_url;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(url)
.then(() => showTermsStatus('✔ URL copiada al portapapeles.', 'success'))
.catch(() => _fallbackCopy(url));
} else {
_fallbackCopy(url);
}
};
}
} catch (e) {
console.warn('loadTermsDoc error:', e);
}
}
async function uploadTermsDocument() {
const fileInput = document.getElementById('terms-pdf-file');
const versionEl = document.getElementById('terms-new-version');
const forceEl = document.getElementById('terms-force-reaccept');
const msgAcEl = document.getElementById('terms-message');
const msgRjEl = document.getElementById('terms-rejected-message');
const formData = new FormData();
if (fileInput && fileInput.files.length > 0) {
formData.append('pdf', fileInput.files[0]);
}
if (versionEl) formData.append('version', versionEl.value.trim());
if (forceEl) formData.append('forzar_reenvio', forceEl.checked ? '1' : '0');
if (msgAcEl) formData.append('mensaje_aceptacion', msgAcEl.value);
if (msgRjEl) formData.append('mensaje_rechazo', msgRjEl.value);
showTermsStatus('Guardando…', 'secondary');
try {
const res = await fetch('api/upload_terms_document.php', { method: 'POST', body: formData });
const json = await res.json();
if (json.success) {
showTermsStatus('✔ ' + (json.message || 'Guardado correctamente.'), 'success');
if (json.documento_url) {
const linkEl = document.getElementById('terms-doc-link');
if (linkEl) linkEl.href = json.documento_url;
document.getElementById('terms-doc-current').style.display = '';
const nameEl = document.getElementById('terms-doc-name');
if (nameEl && fileInput && fileInput.files[0]) nameEl.textContent = fileInput.files[0].name;
const verEl = document.getElementById('terms-doc-version');
if (verEl && versionEl) verEl.textContent = versionEl.value || '—';
}
// Limpiar inputs de archivo
if (fileInput) fileInput.value = '';
if (forceEl) forceEl.checked = false;
} else {
showTermsStatus('✖ ' + (json.error || 'Error al guardar.'), 'danger');
}
} catch (e) {
showTermsStatus('✖ Error de red: ' + e.message, 'danger');
}
}
document.addEventListener('DOMContentLoaded', function () {
const uploadBtn = document.getElementById('terms-upload-btn');
if (uploadBtn) uploadBtn.addEventListener('click', uploadTermsDocument);
// Cargar estado del documento cuando se activa el tab de configuración
const cfgTab = document.querySelector('[data-bs-target="#system-config"], [href="#system-config"]');
if (cfgTab) {
cfgTab.addEventListener('shown.bs.tab', loadTermsDoc);
}
// Si el tab ya está activo al cargar la página
const cfgPanel = document.getElementById('system-config');
if (cfgPanel && cfgPanel.classList.contains('active')) {
loadTermsDoc();
}
});
// Exponer para que loadsystem_config() pueda llamarla
window.loadTermsDoc = loadTermsDoc;
})();
// ─── / Términos ───────────────────────────────────────────────────────────────
// Funciones globales para gestión de usuarios
// Función para editar usuario
window.editUser = function(userId) {
console.log('Editando usuario:', userId);
// Buscar los datos del usuario actual en la tabla
const userRow = document.querySelector(`button[onclick="editUser(${userId})"]`).closest('tr');
const cells = userRow.querySelectorAll('td');
const currentData = {
id: userId,
phone: cells[1].textContent,
name: cells[2].textContent === 'Sin nombre' ? '' : cells[2].textContent,
status: cells[3].querySelector('.badge').textContent.toLowerCase()
};
showEditUserModal(currentData);
};
// Función para eliminar usuario
window.deleteUser = function(userId) {
console.log('Eliminando usuario:', userId);
const userRow = document.querySelector(`button[onclick="deleteUser(${userId})"]`).closest('tr');
const userName = userRow.querySelectorAll('td')[2].textContent;
const userPhone = userRow.querySelectorAll('td')[1].textContent;
if (confirm(`¿Estás seguro de que quieres eliminar este usuario?\n\nUsuario: ${userName}\nTeléfono: ${userPhone}\n\nEsta acción no se puede deshacer y eliminará:\n• Todos sus mensajes\n• Su historial de conversación\n• Sus datos personales`)) {
deleteUserAction(userId);
}
};
// Mostrar modal de edición de usuario
function showEditUserModal(userData) {
const modalHtml = `
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title">
<i class="fas fa-edit"></i> Editar Usuario
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editUserForm">
<input type="hidden" id="editUserId" value="${userData.id}">
<div class="mb-3">
<label for="editUserPhone" class="form-label">
<i class="fas fa-phone"></i> Número de Teléfono
</label>
<input type="text" class="form-control" id="editUserPhone" value="${userData.phone}" readonly>
<small class="text-muted">El número de teléfono no se puede modificar</small>
</div>
<div class="mb-3">
<label for="editUserName" class="form-label">
<i class="fas fa-user"></i> Nombre
</label>
<input type="text" class="form-control" id="editUserName" value="${userData.name}" placeholder="Nombre del usuario">
<small class="text-muted">Deja vacío para mostrar 'Sin nombre'</small>
</div>
<div class="mb-3">
<label for="editUserStatus" class="form-label">
<i class="fas fa-toggle-on"></i> Estado
</label>
<select class="form-select" id="editUserStatus">
<option value="active" ${userData.status === 'active' ? 'selected' : ''}>🟢 Activo</option>
<option value="inactive" ${userData.status === 'inactive' ? 'selected' : ''}>🔴 Inactivo</option>
<option value="blocked" ${userData.status === 'blocked' ? 'selected' : ''}>🚫 Bloqueado</option>
</select>
<small class="text-muted">El estado afecta cómo interactúa el bot con este usuario</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" onclick="saveUserEdit()">
<i class="fas fa-save"></i> Guardar Cambios
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('editUserModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
}
// Guardar edición de usuario
window.saveUserEdit = async function() {
const userId = document.getElementById('editUserId').value;
const userName = document.getElementById('editUserName').value.trim();
const userStatus = document.getElementById('editUserStatus').value;
console.log('Guardando usuario:', { userId, userName, userStatus });
const userData = {
user_id: userId,
name: userName || null, // null si está vacío
status: userStatus
};
try {
const response = await window.whatsappManager.apiCall('update_user.php', {
method: 'POST',
body: userData
});
if (response && response.success) {
window.whatsappManager.showSuccess('Usuario actualizado correctamente');
// Cerrar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
if (modal) modal.hide();
// Recargar lista de usuarios
setTimeout(() => {
window.whatsappManager.loadUsers();
}, 500);
} else {
window.whatsappManager.showError(`Error actualizando usuario: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error actualizando usuario: ' + error.message);
}
};
// Acción de eliminar usuario
async function deleteUserAction(userId) {
try {
console.log('Eliminando usuario con ID:', userId);
const response = await window.whatsappManager.apiCall('delete_user.php', {
method: 'POST',
body: { user_id: userId }
});
if (response && response.success) {
window.whatsappManager.showSuccess('Usuario eliminado correctamente');
// Recargar lista de usuarios
setTimeout(() => {
window.whatsappManager.loadUsers();
}, 500);
} else {
window.whatsappManager.showError(`Error eliminando usuario: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error eliminando usuario: ' + error.message);
}
}
// ========================= FUNCIONES MODALES PARA MENÚS =========================
// Modal para editar menú
function showEditMenuModal(menuData) {
const modalHtml = `
<div class="modal fade" id="editMenuModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit"></i> Editar Menú: ${menuData.name || 'Sin nombre'}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editMenuForm">
<input type="hidden" id="editMenuId" value="${menuData.id}">
<div class="row">
<div class="col-md-6">
<label for="editMenuName" class="form-label">
<i class="fas fa-tag"></i> Nombre del Menú
</label>
<input type="text" class="form-control" id="editMenuName"
value="${menuData.name || ''}" required>
</div>
<div class="col-md-6">
<label for="editMenuKey" class="form-label">
<i class="fas fa-key"></i> Clave del Menú
</label>
<input type="text" class="form-control" id="editMenuKey"
value="${menuData.menu_key || menuData.trigger || ''}" required>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<label for="editMenuDescription" class="form-label">
<i class="fas fa-info-circle"></i> Descripción
</label>
<textarea class="form-control" id="editMenuDescription" rows="2"
placeholder="Descripción del menú">${menuData.description || ''}</textarea>
</div>
</div>
<div class="row mt-3">
<div class="col-md-8">
<label for="editMenuWelcome" class="form-label">
<i class="fas fa-comment"></i> Mensaje de Bienvenida
</label>
<textarea class="form-control" id="editMenuWelcome" rows="3"
placeholder="¡Hola! Selecciona una opción:">${menuData.welcome_message || ''}</textarea>
</div>
<div class="col-md-4">
<label for="editMenuStatus" class="form-label">
<i class="fas fa-toggle-on"></i> Estado
</label>
<select class="form-control" id="editMenuStatus">
<option value="active" ${menuData.status === 'active' ? 'selected' : ''}>Activo</option>
<option value="inactive" ${menuData.status === 'inactive' ? 'selected' : ''}>Inactivo</option>
</select>
<label for="editMenuType" class="form-label mt-2">
<i class="fas fa-layer-group"></i> Tipo de Menú
</label>
<select class="form-control" id="editMenuType">
<option value="main" ${(menuData.menu_type || 'main') === 'main' ? 'selected' : ''}>Principal (normal)</option>
<option value="no_advisor" ${menuData.menu_type === 'no_advisor' ? 'selected' : ''}>🔴 Asesor ausente (secundario)</option>
</select>
</div>
</div>
${menuData.options && menuData.options.length > 0 ? `
<div class="mt-4">
<h6><i class="fas fa-list"></i> Opciones Actuales (${menuData.options.length})</h6>
<div class="row">
${menuData.options.map(option => `
<div class="col-md-6 mb-2">
<div class="card card-body py-2">
<small><strong>${option.option_key || option.key}.</strong> ${option.option_text || option.text}</small>
<small class="text-muted">${option.option_action || option.action}: ${(option.option_value || option.value || '').substring(0, 30)}${(option.option_value || option.value || '').length > 30 ? '...' : ''}</small>
</div>
</div>
`).join('')}
</div>
<small class="text-muted">Para editar opciones individuales, usar "Ver opciones" desde la tabla principal.</small>
</div>
` : ''}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" onclick="saveMenuEdit()">
<i class="fas fa-save"></i> Guardar Cambios
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('editMenuModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('editMenuModal'));
modal.show();
}
// Modal para ver opciones del menú
function showMenuOptionsModal(menuData) {
const modalHtml = `
<div class="modal fade" id="viewMenuOptionsModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-list"></i> Opciones del Menú: ${menuData.name || 'Sin nombre'}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-4">
<strong><i class="fas fa-info-circle"></i> Información del Menú</strong><br>
<small class="text-muted">
<strong>Clave:</strong> ${menuData.menu_key || menuData.trigger || 'N/A'}<br>
<strong>Estado:</strong> <span class="badge bg-${menuData.status === 'active' ? 'success' : 'secondary'}">${menuData.status || 'active'}</span><br>
<strong>Descripción:</strong> ${menuData.description || 'Sin descripción'}
</small>
</div>
<div class="col-md-8">
<strong><i class="fas fa-comment"></i> Mensaje de Bienvenida</strong><br>
<div class="border rounded p-2 bg-light">
<small>${menuData.welcome_message || 'Sin mensaje configurado'}</small>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center mb-3">
<h6><i class="fas fa-list-ol"></i> Opciones del Menú (${menuData.options ? menuData.options.length : 0})</h6>
<button class="btn btn-outline-primary btn-sm" onclick="addMenuOptionInModal(${menuData.id})">
<i class="fas fa-plus"></i> Agregar Opción
</button>
</div>
<div id="menuOptionsContainer">
${menuData.options && menuData.options.length > 0 ? `
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th width="60">Clave</th>
<th>Texto</th>
<th width="100">Acción</th>
<th>Valor</th>
<th width="80">Orden</th>
<th width="120">Acciones</th>
</tr>
</thead>
<tbody>
${menuData.options.map((option, index) => `
<tr data-option-id="${option.id || index}">
<td><code>${option.option_key || option.key || index + 1}</code></td>
<td>${option.option_text || option.text || ''}</td>
<td><span class="badge bg-info">${option.option_action || option.action || 'message'}</span></td>
<td><small>${(option.option_value || option.value || '').substring(0, 40)}${(option.option_value || option.value || '').length > 40 ? '...' : ''}</small></td>
<td>${option.order_index || index + 1}</td>
<td class="text-nowrap">
<div class="d-flex gap-1">
<button class="btn btn-outline-warning btn-sm" onclick="editMenuOption(${menuData.id}, ${option.id || index})" title="Editar">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteMenuOption(${menuData.id}, ${option.id || index})" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : `
<div class="text-center py-4">
<i class="fas fa-list fa-3x text-muted mb-3"></i>
<p class="text-muted">Este menú no tiene opciones configuradas.</p>
<button class="btn btn-primary" onclick="addMenuOptionInModal(${menuData.id})">
<i class="fas fa-plus"></i> Agregar Primera Opción
</button>
</div>
`}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-success" onclick="testMenuPreview(${menuData.id})">
<i class="fas fa-eye"></i> Vista Previa
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cerrar
</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('viewMenuOptionsModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('viewMenuOptionsModal'));
modal.show();
}
// Guardar edición de menú
window.saveMenuEdit = async function() {
const menuId = document.getElementById('editMenuId').value;
const menuName = document.getElementById('editMenuName').value.trim();
const menuKey = document.getElementById('editMenuKey').value.trim();
const menuDescription = document.getElementById('editMenuDescription').value.trim();
const menuWelcome = document.getElementById('editMenuWelcome').value.trim();
const menuStatus = document.getElementById('editMenuStatus').value;
const menuType = document.getElementById('editMenuType')?.value || 'main';
if (!menuName || !menuKey) {
window.whatsappManager.showError('Nombre y clave del menú son obligatorios');
return;
}
console.log('Guardando menú editado:', { menuId, menuName, menuKey, menuStatus, menuType });
const menuData = {
id: menuId,
name: menuName,
menu_key: menuKey,
description: menuDescription,
welcome_message: menuWelcome,
status: menuStatus,
menu_type: menuType
};
try {
const response = await window.whatsappManager.apiCall('save_menu.php', {
method: 'POST',
body: menuData
});
if (response && response.success) {
window.whatsappManager.showSuccess('Menú actualizado correctamente');
// Cerrar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('editMenuModal'));
if (modal) modal.hide();
// Recargar lista de menús
setTimeout(() => {
window.whatsappManager.loadMenus();
}, 500);
} else {
window.whatsappManager.showError(`Error actualizando menú: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error actualizando menú: ' + error.message);
}
};
// Funciones auxiliares para opciones
window.addMenuOptionInModal = function(menuId) {
console.log('Agregando opción al menú:', menuId);
const modalHtml = `
<div class="modal fade" id="addMenuOptionModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus"></i> Agregar Opción al Menú
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addOptionForm">
<input type="hidden" id="addOptionMenuId" value="${menuId}">
<div class="mb-3">
<label for="addOptionKey" class="form-label">
<i class="fas fa-key"></i> Clave (Número de Opción)
</label>
<input type="text" class="form-control" id="addOptionKey"
placeholder="1" required>
<small class="text-muted">Número que el usuario escribirá para seleccionar esta opción</small>
</div>
<div class="mb-3">
<label for="addOptionText" class="form-label">
<i class="fas fa-text"></i> Texto de la Opción
</label>
<input type="text" class="form-control" id="addOptionText"
placeholder="Información del servicio" required>
<small class="text-muted">Texto que se mostrará al usuario</small>
</div>
<div class="mb-3">
<label for="addOptionAction" class="form-label">
<i class="fas fa-cog"></i> Tipo de Acción
</label>
<select class="form-control" id="addOptionAction">
<option value="message">Mensaje de respuesta</option>
<option value="menu">Ir a otro menú</option>
<option value="api_call">Llamar API</option>
<option value="end">Finalizar conversación</option>
</select>
</div>
<div class="mb-3">
<label for="addOptionValue" class="form-label">
<i class="fas fa-edit"></i> Valor de la Acción
</label>
<textarea class="form-control" id="addOptionValue" rows="3"
placeholder="Contenido de la respuesta o valor de la acción"></textarea>
<small class="text-muted">Contenido que se enviará o ejecutará según el tipo de acción</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" onclick="saveNewMenuOption()">
<i class="fas fa-save"></i> Guardar Opción
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('addMenuOptionModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('addMenuOptionModal'));
modal.show();
};
window.editMenuOption = async function(menuId, optionId) {
console.log('Editando opción:', { menuId, optionId });
try {
// Obtener datos del menú y sus opciones
const response = await window.whatsappManager.apiCall('get_menus.php');
if (response && response.success) {
const menu = response.data.find(m => m.id == menuId);
if (menu && menu.options) {
const option = menu.options.find(o => o.id == optionId);
if (option) {
showEditMenuOptionModal(menuId, option);
} else {
window.whatsappManager.showError('Opción no encontrada');
}
} else {
window.whatsappManager.showError('Menú no encontrado');
}
} else {
window.whatsappManager.showError('Error cargando datos de la opción');
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error cargando opción: ' + error.message);
}
};
// Modal para editar opción de menú
function showEditMenuOptionModal(menuId, optionData) {
const modalHtml = `
<div class="modal fade" id="editMenuOptionModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit"></i> Editar Opción del Menú
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editOptionForm">
<input type="hidden" id="editOptionId" value="${optionData.id}">
<input type="hidden" id="editOptionMenuId" value="${menuId}">
<div class="mb-3">
<label for="editOptionKey" class="form-label">
<i class="fas fa-key"></i> Clave (Número de Opción)
</label>
<input type="text" class="form-control" id="editOptionKey"
value="${optionData.option_key || optionData.option_number || ''}" required>
<small class="text-muted">Número que el usuario escribirá para seleccionar esta opción</small>
</div>
<div class="mb-3">
<label for="editOptionText" class="form-label">
<i class="fas fa-text"></i> Texto de la Opción
</label>
<input type="text" class="form-control" id="editOptionText"
value="${optionData.option_text || optionData.text || ''}" required>
<small class="text-muted">Texto que se mostrará al usuario</small>
</div>
<div class="mb-3">
<label for="editOptionAction" class="form-label">
<i class="fas fa-cog"></i> Tipo de Acción
</label>
<select class="form-control" id="editOptionAction">
<option value="message" ${(optionData.option_action || optionData.action_type) === 'message' ? 'selected' : ''}>Mensaje de respuesta</option>
<option value="menu" ${(optionData.option_action || optionData.action_type) === 'menu' ? 'selected' : ''}>Ir a otro menú</option>
<option value="api_call" ${(optionData.option_action || optionData.action_type) === 'api_call' ? 'selected' : ''}>Llamar API</option>
<option value="end" ${(optionData.option_action || optionData.action_type) === 'end' ? 'selected' : ''}>Finalizar conversación</option>
</select>
</div>
<div class="mb-3">
<label for="editOptionValue" class="form-label">
<i class="fas fa-edit"></i> Valor de la Acción
</label>
<textarea class="form-control" id="editOptionValue" rows="3"
placeholder="Contenido de la respuesta o valor de la acción">${optionData.option_value || optionData.action_value || ''}</textarea>
<small class="text-muted">Contenido que se enviará o ejecutará según el tipo de acción</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" onclick="saveEditedMenuOption()">
<i class="fas fa-save"></i> Guardar Cambios
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('editMenuOptionModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('editMenuOptionModal'));
modal.show();
}
// Función para guardar cambios en opción editada
window.saveEditedMenuOption = async function() {
const optionId = document.getElementById('editOptionId').value;
const menuId = document.getElementById('editOptionMenuId').value;
const optionKey = document.getElementById('editOptionKey').value.trim();
const optionText = document.getElementById('editOptionText').value.trim();
const optionAction = document.getElementById('editOptionAction').value;
const optionValue = document.getElementById('editOptionValue').value.trim();
if (!optionKey || !optionText) {
window.whatsappManager.showError('Clave y texto de la opción son obligatorios');
return;
}
console.log('Actualizando opción:', { optionId, menuId, optionKey, optionText });
const optionData = {
option_id: optionId,
menu_id: menuId,
option_key: optionKey,
option_text: optionText,
option_action: optionAction,
option_value: optionValue
};
try {
const response = await window.whatsappManager.apiCall('update_menu_option.php', {
method: 'POST',
body: optionData
});
if (response && response.success) {
window.whatsappManager.showSuccess('Opción actualizada correctamente');
// Cerrar modales
const editModal = bootstrap.Modal.getInstance(document.getElementById('editMenuOptionModal'));
if (editModal) editModal.hide();
const viewModal = bootstrap.Modal.getInstance(document.getElementById('viewMenuOptionsModal'));
if (viewModal) viewModal.hide();
// Reabrir modal actualizado
setTimeout(async () => {
const menusResponse = await window.whatsappManager.apiCall('get_menus.php');
if (menusResponse && menusResponse.success) {
const menu = menusResponse.data.find(m => m.id == menuId);
if (menu) {
showMenuOptionsModal(menu);
}
}
}, 500);
} else {
window.whatsappManager.showError(`Error actualizando opción: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error actualizando opción: ' + error.message);
}
};
window.saveNewMenuOption = async function() {
const menuId = document.getElementById('addOptionMenuId').value;
const optionKey = document.getElementById('addOptionKey').value.trim();
const optionText = document.getElementById('addOptionText').value.trim();
const optionAction = document.getElementById('addOptionAction').value;
const optionValue = document.getElementById('addOptionValue').value.trim();
if (!optionKey || !optionText) {
window.whatsappManager.showError('Clave y texto de la opción son obligatorios');
return;
}
console.log('Guardando nueva opción:', { menuId, optionKey, optionText, optionAction, optionValue });
const optionData = {
menu_id: menuId,
option_key: optionKey,
option_text: optionText,
option_action: optionAction,
option_value: optionValue,
is_active: true
};
try {
const response = await window.whatsappManager.apiCall('save_menu_option.php', {
method: 'POST',
body: optionData
});
if (response && response.success) {
window.whatsappManager.showSuccess('Opción agregada correctamente');
// Cerrar modal de agregar opción
const addModal = bootstrap.Modal.getInstance(document.getElementById('addMenuOptionModal'));
if (addModal) addModal.hide();
// Cerrar modal de ver opciones si está abierto
const viewModal = bootstrap.Modal.getInstance(document.getElementById('viewMenuOptionsModal'));
if (viewModal) viewModal.hide();
// Reabrir modal de ver opciones actualizado
setTimeout(async () => {
const menusResponse = await window.whatsappManager.apiCall('get_menus.php');
if (menusResponse && menusResponse.success) {
const menu = menusResponse.data.find(m => m.id == menuId);
if (menu) {
showMenuOptionsModal(menu);
}
}
}, 500);
} else {
window.whatsappManager.showError(`Error agregando opción: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error agregando opción: ' + error.message);
}
};
window.deleteMenuOption = async function(menuId, optionId) {
if (!confirm('¿Estás seguro de que deseas eliminar esta opción del menú?')) {
return;
}
console.log('Eliminando opción:', { menuId, optionId });
try {
const response = await window.whatsappManager.apiCall('delete_menu_option.php', {
method: 'POST',
body: {
menu_id: menuId,
option_id: optionId
}
});
if (response && response.success) {
window.whatsappManager.showSuccess('Opción eliminada correctamente');
// Cerrar modal actual
const viewModal = bootstrap.Modal.getInstance(document.getElementById('viewMenuOptionsModal'));
if (viewModal) viewModal.hide();
// Reabrir modal actualizado
setTimeout(async () => {
const menusResponse = await window.whatsappManager.apiCall('get_menus.php');
if (menusResponse && menusResponse.success) {
const menu = menusResponse.data.find(m => m.id == menuId);
if (menu) {
showMenuOptionsModal(menu);
}
}
}, 500);
} else {
window.whatsappManager.showError(`Error eliminando opción: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error eliminando opción: ' + error.message);
}
};
window.testMenuPreview = async function(menuId) {
console.log('Mostrando vista previa del menú:', menuId);
try {
// Obtener datos del menú
const response = await window.whatsappManager.apiCall('get_menus.php');
if (response && response.success) {
const menu = response.data.find(m => m.id == menuId);
if (menu) {
showMenuPreviewModal(menu);
} else {
window.whatsappManager.showError('Menú no encontrado');
}
} else {
window.whatsappManager.showError('Error cargando datos del menú');
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error mostrando vista previa: ' + error.message);
}
};
// Modal de vista previa del menú estilo WhatsApp
function showMenuPreviewModal(menuData) {
const options = menuData.options || [];
const welcomeMsg = menuData.welcome_message || `¡Hola! Bienvenido al ${menuData.title || menuData.name}`;
// Construir el mensaje del menú como se vería en WhatsApp
let menuMessage = `📋 *${menuData.title || menuData.name}*\n\n`;
if (menuData.description) {
menuMessage += `${menuData.description}\n\n`;
}
if (options.length > 0) {
menuMessage += '*Opciones disponibles:*\n\n';
options.forEach(option => {
const key = option.option_key || option.key || '?';
const text = option.option_text || option.text || 'Sin texto';
menuMessage += `${key}. ${text}\n`;
});
menuMessage += '\n💬 *Responde con el número de la opción que deseas*';
} else {
menuMessage += '⚠️ _Este menú no tiene opciones configuradas_';
}
const modalHtml = `
<div class="modal fade" id="menuPreviewModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">
<i class="fab fa-whatsapp"></i> Vista Previa del Menú - Simulación WhatsApp
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div class="row g-0">
<!-- Panel de información -->
<div class="col-md-4 bg-light p-3 border-end">
<h6 class="fw-bold mb-3"><i class="fas fa-info-circle"></i> Información del Menú</h6>
<div class="mb-3">
<small class="text-muted d-block">ID del Menú</small>
<strong>${menuData.id}</strong>
</div>
<div class="mb-3">
<small class="text-muted d-block">Nombre/Clave</small>
<strong>${menuData.name || 'N/A'}</strong>
</div>
<div class="mb-3">
<small class="text-muted d-block">Estado</small>
<span class="badge bg-${menuData.status === 'active' || menuData.is_active ? 'success' : 'secondary'}">
${menuData.status === 'active' || menuData.is_active ? 'Activo' : 'Inactivo'}
</span>
</div>
<div class="mb-3">
<small class="text-muted d-block">Total de Opciones</small>
<strong>${options.length}</strong>
</div>
${options.length > 0 ? `
<hr>
<h6 class="fw-bold mb-2">Acciones Configuradas:</h6>
<ul class="list-unstyled small">
${options.map(opt => {
const action = opt.option_action || opt.action_type || 'message';
const icons = {
'message': '💬',
'menu': '📋',
'api_call': '🔌',
'end': '🚪'
};
return `<li class="mb-1">${icons[action] || '•'} ${opt.option_key || '?'}: <span class="badge bg-info">${action}</span></li>`;
}).join('')}
</ul>
` : ''}
</div>
<!-- Simulación de WhatsApp -->
<div class="col-md-8 p-0">
<div class="whatsapp-preview" style="background: #e5ddd5; min-height: 500px; max-height: 600px; overflow-y: auto; position: relative;">
<!-- Header de WhatsApp -->
<div class="whatsapp-header" style="background: #075e54; color: white; padding: 10px 15px; display: flex; align-items: center; gap: 10px;">
<div style="width: 40px; height: 40px; border-radius: 50%; background: #25d366; display: flex; align-items: center; justify-content: center;">
<i class="fas fa-robot"></i>
</div>
<div>
<div style="font-weight: bold;">Bot WhatsApp</div>
<div style="font-size: 12px; opacity: 0.8;">En línea</div>
</div>
</div>
<!-- Chat -->
<div class="whatsapp-chat p-4" style="padding-bottom: 80px !important;">
<!-- Mensaje del bot -->
<div class="d-flex mb-3">
<div style="max-width: 80%; background: white; padding: 8px 12px; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);">
<div style="white-space: pre-wrap; font-size: 14px; line-height: 1.5;">${menuMessage.replace(/\*/g, '<strong>').replace(/\*\*/g, '</strong>')}</div>
<div style="text-align: right; font-size: 11px; color: #667781; margin-top: 4px;">
<i class="fas fa-check-double text-primary"></i> ${new Date().toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
${options.length > 0 ? `
<!-- Ejemplo de respuesta del usuario -->
<div class="d-flex justify-content-end mb-3">
<div style="max-width: 80%; background: #dcf8c6; padding: 8px 12px; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);">
<div style="font-size: 14px;">${options[0].option_key || '1'}</div>
<div style="text-align: right; font-size: 11px; color: #667781; margin-top: 4px;">
${new Date().toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
<!-- Respuesta del bot a la opción -->
<div class="d-flex mb-3">
<div style="max-width: 80%; background: white; padding: 8px 12px; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);">
<div style="font-size: 14px; line-height: 1.5;">
${options[0].option_value || options[0].action_value || 'Respuesta de la opción ' + (options[0].option_key || '1')}
</div>
<div style="text-align: right; font-size: 11px; color: #667781; margin-top: 4px;">
<i class="fas fa-check-double text-primary"></i> ${new Date().toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
` : ''}
</div>
<!-- Input de WhatsApp -->
<div class="whatsapp-input" style="position: absolute; bottom: 0; left: 0; right: 0; background: #f0f0f0; padding: 10px; border-top: 1px solid #ccc;">
<div style="display: flex; align-items: center; gap: 8px; background: white; padding: 8px 12px; border-radius: 20px;">
<i class="far fa-smile text-muted"></i>
<input type="text" placeholder="Escribe un mensaje" style="border: none; outline: none; flex: 1; font-size: 14px;" disabled>
<i class="fas fa-paperclip text-muted"></i>
<i class="fas fa-microphone text-muted"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" onclick="copyMenuPreview('${menuMessage.replace(/'/g, "\\'")}')">
<i class="fas fa-copy"></i> Copiar Mensaje
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cerrar
</button>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('menuPreviewModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('menuPreviewModal'));
modal.show();
}
// Función para copiar el mensaje de vista previa
window.copyMenuPreview = function(message) {
// Limpiar el mensaje de HTML
const cleanMessage = message.replace(/<[^>]*>/g, '');
navigator.clipboard.writeText(cleanMessage).then(() => {
window.whatsappManager.showSuccess('Mensaje copiado al portapapeles');
}).catch(err => {
window.whatsappManager.showError('Error al copiar: ' + err.message);
});
};
// Función para guardar nuevo menú desde el modal de index.php
window.saveNewMenu = async function() {
const menuName = document.getElementById('menu-name')?.value?.trim();
const menuDescription = document.getElementById('menu-description')?.value?.trim();
if (!menuName) {
window.whatsappManager.showError('El nombre del menú es obligatorio');
return;
}
console.log('Guardando nuevo menú:', { menuName, menuDescription });
// Generar clave del menú basada en el nombre
const menuKey = menuName.toLowerCase()
.replace(/[^\w\s]/g, '') // Remover caracteres especiales
.replace(/\s+/g, '_') // Reemplazar espacios con guiones bajos
.substring(0, 20); // Limitar longitud
const menuData = {
name: menuName,
menu_key: menuKey,
description: menuDescription,
welcome_message: `¡Hola! Bienvenido al ${menuName}. Por favor selecciona una opción:`,
status: 'active',
options: [] // Sin opciones inicialmente
};
try {
const response = await window.whatsappManager.apiCall('save_menu.php', {
method: 'POST',
body: menuData
});
if (response && response.success) {
window.whatsappManager.showSuccess('Menú creado correctamente');
// Limpiar formulario
document.getElementById('menu-name').value = '';
document.getElementById('menu-description').value = '';
// Cerrar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('createMenuModal'));
if (modal) modal.hide();
// Recargar lista de menús si existe la función
setTimeout(() => {
if (window.whatsappManager.loadMenus) {
window.whatsappManager.loadMenus();
}
if (typeof loadMenusTest === 'function') {
loadMenusTest();
}
}, 500);
} else {
window.whatsappManager.showError(`Error creando menú: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
console.error('Error:', error);
window.whatsappManager.showError('Error creando menú: ' + error.message);
}
};
// ========================= FUNCIONES GLOBALES ADICIONALES =========================
// Función para refrescar datos del dashboard
window.refreshData = function() {
console.log('Refrescando datos del dashboard');
if (window.whatsappManager) {
window.whatsappManager.showInfo('Actualizando datos...');
// Recargar todas las secciones
if (window.whatsappManager.loadUsers) {
window.whatsappManager.loadUsers();
}
if (window.whatsappManager.loadMenus) {
window.whatsappManager.loadMenus();
}
if (window.whatsappManager.loadTemplates) {
window.whatsappManager.loadTemplates();
}
if (window.whatsappManager.loadAutoResponses) {
window.whatsappManager.loadAutoResponses();
}
setTimeout(() => {
window.whatsappManager.showSuccess('Datos actualizados correctamente');
}, 1000);
}
};
// Función para exportar usuarios
window.exportConversations = function(filters) {
const params = new URLSearchParams(filters || {});
const url = (window.whatsappManager ? window.whatsappManager.apiBaseUrl : 'api/') + 'export_conversations.php';
const full = params.toString() ? url + '?' + params.toString() : url;
const a = document.createElement('a');
a.href = full;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
if (window.whatsappManager) window.whatsappManager.showSuccess('Descarga de conversaciones iniciada');
};
window.exportUsers = function() {
console.log('Exportando usuarios...');
if (window.whatsappManager) {
window.whatsappManager.showInfo('Preparando exportación de usuarios...');
// La API devuelve un archivo CSV directamente, así que abrimos en nueva ventana
const url = window.whatsappManager.apiBaseUrl + 'export_users.php';
window.open(url, '_blank');
window.whatsappManager.showSuccess('Descarga de usuarios iniciada');
}
};
// 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
window.refreshLogs = function() {
console.log('Refrescando logs...');
if (window.whatsappManager) {
window.whatsappManager.showInfo('Actualizando logs...');
window.whatsappManager.loadLogs();
} else {
alert('Error: Sistema no inicializado');
}
};
// Función para filtrar logs
window.filterLogs = function() {
const searchTerm = (document.getElementById('log-search')?.value || '').toLowerCase();
const levelFilter = document.getElementById('log-level-filter')?.value || 'all';
const tbody = document.getElementById('logs-table');
if (!tbody) return;
const rows = tbody.querySelectorAll('tr');
let visibleCount = 0;
rows.forEach(row => {
const level = row.getAttribute('data-level');
const message = row.getAttribute('data-message');
const source = row.getAttribute('data-source');
const matchesSearch = !searchTerm ||
(message && message.includes(searchTerm)) ||
(source && source.includes(searchTerm));
const matchesLevel = levelFilter === 'all' || level === levelFilter;
if (matchesSearch && matchesLevel) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
console.log(`Mostrando ${visibleCount} de ${rows.length} logs`);
};
// Función para limpiar logs
window.clearLogs = async function() {
console.log('Limpiando logs...');
if (!confirm('¿Estás seguro de que quieres limpiar todos los logs? Esta acción no se puede deshacer.')) {
return;
}
if (window.whatsappManager) {
try {
const response = await window.whatsappManager.apiCall('clear_logs.php', {
method: 'POST',
body: { type: 'all' }
});
if (response && response.success) {
window.whatsappManager.showSuccess(
`Logs limpiados correctamente. ${response.deleted_count || 0} registros eliminados.`
);
// Recargar logs
window.whatsappManager.loadLogs();
} else {
window.whatsappManager.showError(`Error al limpiar logs: ${response?.error || 'Error desconocido'}`);
}
} catch (error) {
window.whatsappManager.showError('Error al limpiar logs: ' + error.message);
}
} else {
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);
}
};
// ─────────────────────────────────────────────────────────────────────────────
// PERFIL DE WHATSAPP BUSINESS
// ─────────────────────────────────────────────────────────────────────────────
window.loadWhatsAppProfile = async function() {
if (!window.whatsappManager) return;
window.whatsappManager.showInfo('Cargando perfil desde WhatsApp...');
try {
const res = await window.whatsappManager.apiCall('get_whatsapp_profile.php');
if (!res || !res.success) {
window.whatsappManager.showError(res?.error || 'Error al obtener el perfil');
return;
}
const p = res.data || {};
document.getElementById('wp-about').value = p.about || '';
document.getElementById('wp-description').value = p.description || '';
document.getElementById('wp-address').value = p.address || '';
document.getElementById('wp-email').value = p.email || '';
document.getElementById('wp-website').value = (p.websites && p.websites[0]) ? p.websites[0] : '';
const vertSel = document.getElementById('wp-vertical');
if (vertSel && p.vertical) {
vertSel.value = p.vertical;
}
// Foto de perfil
const img = document.getElementById('wp-profile-photo-preview');
const placeholder = document.getElementById('wp-profile-photo-placeholder');
if (p.profile_picture_url && img) {
img.src = p.profile_picture_url;
img.style.display = 'inline-block';
if (placeholder) placeholder.style.display = 'none';
}
window.whatsappManager.showSuccess('Perfil cargado correctamente');
} catch (e) {
window.whatsappManager.showError('Error: ' + e.message);
}
};
window.saveWhatsAppProfile = async function() {
if (!window.whatsappManager) return;
const payload = {
about: document.getElementById('wp-about')?.value?.trim() || '',
description: document.getElementById('wp-description')?.value?.trim() || '',
address: document.getElementById('wp-address')?.value?.trim() || '',
email: document.getElementById('wp-email')?.value?.trim() || '',
vertical: document.getElementById('wp-vertical')?.value || '',
websites: [document.getElementById('wp-website')?.value?.trim()].filter(Boolean),
};
try {
window.whatsappManager.showInfo('Guardando perfil...');
const res = await window.whatsappManager.apiCall('update_whatsapp_profile.php', {
method: 'POST',
body: payload,
});
if (res && res.success) {
window.whatsappManager.showSuccess(res.message || 'Perfil actualizado correctamente');
} else {
window.whatsappManager.showError(res?.error || 'Error al guardar el perfil');
}
} catch (e) {
window.whatsappManager.showError('Error: ' + e.message);
}
};
window.uploadWhatsAppProfilePhoto = async function() {
const fileInput = document.getElementById('wp-photo-file');
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
alert('Selecciona una foto primero');
return;
}
const formData = new FormData();
formData.append('photo', fileInput.files[0]);
try {
window.whatsappManager.showInfo('Subiendo foto de perfil...');
const baseUrl = window.whatsappManager.baseUrl || 'api/';
const response = await fetch(baseUrl + 'upload_whatsapp_profile_photo.php', {
method: 'POST',
body: formData,
credentials: 'same-origin',
});
const res = await response.json();
if (res && res.success) {
window.whatsappManager.showSuccess(res.message || 'Foto actualizada correctamente');
// Actualizar preview local
const reader = new FileReader();
reader.onload = (e) => {
const img = document.getElementById('wp-profile-photo-preview');
const ph = document.getElementById('wp-profile-photo-placeholder');
if (img) { img.src = e.target.result; img.style.display = 'inline-block'; }
if (ph) { ph.style.display = 'none'; }
};
reader.readAsDataURL(fileInput.files[0]);
fileInput.value = '';
} else {
window.whatsappManager.showError(res?.error || 'Error al subir la foto');
}
} catch (e) {
window.whatsappManager.showError('Error: ' + e.message);
}
};
// Cargar perfil al abrir la pestaña
document.addEventListener('DOMContentLoaded', () => {
const profileLinks = document.querySelectorAll('[data-tab="whatsapp_profile"]');
profileLinks.forEach(link => {
link.addEventListener('click', () => {
// Pequeño delay para que el tab esté visible
setTimeout(() => window.loadWhatsAppProfile && window.loadWhatsAppProfile(), 100);
});
});
});
// ─────────────────────────────────────────────────────────────────────────────
// RESET + RE-SINCRONIZACIÓN DE PLANTILLAS
// ─────────────────────────────────────────────────────────────────────────────
window.resetAndSyncTemplates = async function() {
if (!window.whatsappManager) { alert('Error: Sistema no inicializado'); return; }
const confirmed = confirm(
'⚠️ ATENCIÓN: Esta acción eliminará TODAS las plantillas locales y las volverá a importar desde WhatsApp/Facebook.\n\n' +
'¿Estás seguro de que deseas continuar?'
);
if (!confirmed) return;
const btn = document.getElementById('btn-reset-sync-templates');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Procesando...'; }
try {
window.whatsappManager.showInfo('Eliminando plantillas locales y re-sincronizando desde WhatsApp...');
const response = await window.whatsappManager.apiCall('reset_and_sync_templates.php', { method: 'POST' });
if (response && response.success) {
const d = response.data || {};
const msg = `✅ ${response.message}\n• Recibidas de Facebook: ${d.total_facebook || 0}\n• Importadas: ${d.imported || 0}${d.errors?.length ? '\n⚠️ ' + d.errors.length + ' errores' : ''}`;
window.whatsappManager.showSuccess(msg);
if (window.whatsappManager.loadTemplates) window.whatsappManager.loadTemplates();
} else {
window.whatsappManager.showError(response?.error || 'Error desconocido al re-sincronizar');
}
} catch (e) {
window.whatsappManager.showError('Error de conexión: ' + e.message);
} finally {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-trash-restore"></i> Limpiar y Re-sincronizar'; }
}
};