/** * JavaScript para el Dashboard del Panel de Administración ROI Theme * Vanilla JavaScript - No frameworks */ (function() { 'use strict'; /** * Inicializa el dashboard cuando el DOM está listo */ document.addEventListener('DOMContentLoaded', function() { // Nueva navegación por Cards/Grupos initializeCardNavigation(); // Funcionalidad existente (solo si hay tabs visibles) if (document.querySelector('.nav-tabs-admin')) { initializeTabs(); } initializeFormValidation(); initializeButtons(); initializeColorPickers(); }); /** * Inicializa la navegación por Cards/Grupos (App-Style) */ function initializeCardNavigation() { // Verificar que estamos en el panel correcto const adminPanel = document.querySelector('.roi-admin-panel'); if (!adminPanel) { return; } // Delegación de eventos para mini-cards document.addEventListener('click', function(e) { const minicard = e.target.closest('.roi-component-minicard'); if (minicard) { e.preventDefault(); const componentId = minicard.getAttribute('data-component-id'); if (componentId) { navigateToComponent(componentId); } } }); // Botón volver al home document.addEventListener('click', function(e) { if (e.target.closest('.roi-back-to-home')) { e.preventDefault(); navigateToHome(); } }); } /** * Navega a un componente específico * * @param {string} componentId ID del componente en kebab-case */ function navigateToComponent(componentId) { const url = new URL(window.location.href); url.searchParams.set('component', componentId); // Eliminar el parámetro admin-tab si existe (legacy) url.searchParams.delete('admin-tab'); window.location.href = url.toString(); } /** * Navega de vuelta al home (vista de grupos) */ function navigateToHome() { const url = new URL(window.location.href); url.searchParams.delete('component'); url.searchParams.delete('admin-tab'); window.location.href = url.toString(); } /** * Inicializa el sistema de tabs con persistencia en URL */ function initializeTabs() { const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]'); // Leer parametro admin-tab de la URL al cargar const urlParams = new URLSearchParams(window.location.search); const activeTabParam = urlParams.get('admin-tab'); if (activeTabParam) { // Buscar el boton del tab correspondiente const targetButton = document.querySelector('[data-bs-target="#' + activeTabParam + 'Tab"]'); if (targetButton) { // Activar el tab usando Bootstrap API const tab = new bootstrap.Tab(targetButton); tab.show(); } } // Escuchar cambios de tab para actualizar URL tabButtons.forEach(function(tabButton) { tabButton.addEventListener('shown.bs.tab', function(e) { // Obtener el ID del componente desde data-bs-target const target = e.target.getAttribute('data-bs-target'); const componentId = target.replace('#', '').replace('Tab', ''); // Actualizar URL sin recargar pagina updateUrlWithTab(componentId); }); }); } /** * Actualiza la URL con el parametro admin-tab sin recargar la pagina * * @param {string} tabId ID del tab activo */ function updateUrlWithTab(tabId) { const url = new URL(window.location.href); url.searchParams.set('admin-tab', tabId); window.history.replaceState({}, '', url.toString()); } /** * Obtiene el ID del tab activo actualmente * * @returns {string|null} ID del componente activo o null */ function getActiveTabId() { const activeTab = document.querySelector('.tab-pane.active'); if (activeTab) { return activeTab.id.replace('Tab', ''); } return null; } /** * Inicializa validación de formularios */ function initializeFormValidation() { const forms = document.querySelectorAll('.roi-component-config form'); forms.forEach(function(form) { form.addEventListener('submit', function(e) { if (!validateForm(form)) { e.preventDefault(); showError('Por favor, corrige los errores en el formulario.'); } }); }); } /** * Valida un formulario * * @param {HTMLFormElement} form Formulario a validar * @returns {boolean} True si es válido */ function validateForm(form) { let isValid = true; const requiredFields = form.querySelectorAll('[required]'); requiredFields.forEach(function(field) { if (!field.value.trim()) { field.classList.add('error'); isValid = false; } else { field.classList.remove('error'); } }); return isValid; } /** * Muestra un mensaje de error * * @param {string} message Mensaje a mostrar */ function showError(message) { const notice = document.createElement('div'); notice.className = 'notice notice-error is-dismissible'; notice.innerHTML = '

' + escapeHtml(message) + '

'; const h1 = document.querySelector('.roi-admin-dashboard h1'); if (h1 && h1.nextElementSibling) { h1.nextElementSibling.after(notice); } } /** * Escapa HTML para prevenir XSS * * @param {string} text Texto a escapar * @returns {string} Texto escapado */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Inicializa los botones del panel */ function initializeButtons() { // Botón Guardar Cambios const saveButton = document.getElementById('saveSettings'); if (saveButton) { saveButton.addEventListener('click', handleSaveSettings); } // Botón Cancelar const cancelButton = document.getElementById('cancelChanges'); if (cancelButton) { cancelButton.addEventListener('click', handleCancelChanges); } // Botones Restaurar valores por defecto (dinámico para todos los componentes) const resetButtons = document.querySelectorAll('.btn-reset-defaults[data-component]'); resetButtons.forEach(function(button) { button.addEventListener('click', function(e) { const componentId = this.getAttribute('data-component'); handleResetDefaults(e, componentId, this); }); }); } /** * Guarda los cambios del formulario */ function handleSaveSettings(e) { e.preventDefault(); // Obtener el tab activo const activeTab = document.querySelector('.tab-pane.active'); if (!activeTab) { showNotice('error', 'No hay ningún componente seleccionado.'); return; } // Obtener el ID del componente desde el tab const componentId = activeTab.id.replace('Tab', ''); // Recopilar todos los campos del formulario activo const formData = collectFormData(activeTab); // Mostrar loading en el botón const saveButton = document.getElementById('saveSettings'); const originalText = saveButton.innerHTML; saveButton.disabled = true; saveButton.innerHTML = ' Guardando...'; // Enviar por AJAX fetch(ajaxurl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'roi_save_component_settings', nonce: roiAdminDashboard.nonce, component: componentId, settings: JSON.stringify(formData) }) }) .then(response => response.json()) .then(data => { if (data.success) { showNotice('success', data.data.message || 'Cambios guardados correctamente.'); } else { showNotice('error', data.data.message || 'Error al guardar los cambios.'); } }) .catch(error => { console.error('Error:', error); showNotice('error', 'Error de conexión al guardar los cambios.'); }) .finally(() => { saveButton.disabled = false; saveButton.innerHTML = originalText; }); } /** * Cancela los cambios y recarga la página */ function handleCancelChanges(e) { e.preventDefault(); showConfirmModal( 'Cancelar cambios', '¿Descartar todos los cambios no guardados?', function() { location.reload(); } ); } /** * Restaura los valores por defecto de un componente * * @param {Event} e Evento del click * @param {string} componentId ID del componente a resetear * @param {HTMLElement} resetButton Elemento del botón que disparó el evento */ function handleResetDefaults(e, componentId, resetButton) { e.preventDefault(); showConfirmModal( 'Restaurar valores por defecto', '¿Restaurar todos los valores a los valores por defecto? Esta acción no se puede deshacer.', function() { // Mostrar loading en el botón const originalText = resetButton.innerHTML; resetButton.disabled = true; resetButton.innerHTML = ' Restaurando...'; // Enviar por AJAX fetch(ajaxurl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'roi_reset_component_defaults', nonce: roiAdminDashboard.nonce, component: componentId }) }) .then(response => response.json()) .then(data => { if (data.success) { showNotice('success', data.data.message || 'Valores restaurados correctamente.'); // Recargar preservando el tab activo setTimeout(() => { const activeTabId = getActiveTabId(); if (activeTabId) { const url = new URL(window.location.href); url.searchParams.set('admin-tab', activeTabId); window.location.href = url.toString(); } else { location.reload(); } }, 1500); } else { showNotice('error', data.data.message || 'Error al restaurar los valores.'); } }) .catch(error => { console.error('Error:', error); showNotice('error', 'Error de conexión al restaurar los valores.'); }) .finally(() => { resetButton.disabled = false; resetButton.innerHTML = originalText; }); } ); } /** * Recopila los datos del formulario del tab activo */ function collectFormData(container) { const formData = {}; // Inputs de texto, textarea, select, color, number, email, password const textInputs = container.querySelectorAll('input[type="text"], input[type="url"], input[type="color"], input[type="number"], input[type="email"], input[type="password"], textarea, select'); textInputs.forEach(input => { if (input.id) { formData[input.id] = input.value; } }); // Checkboxes (switches) const checkboxes = container.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(checkbox => { if (checkbox.id) { formData[checkbox.id] = checkbox.checked; } }); return formData; } /** * Muestra un toast de Bootstrap */ function showNotice(type, message) { // Mapear tipos const typeMap = { 'success': { bg: 'success', icon: 'bi-check-circle-fill', text: 'Éxito' }, 'error': { bg: 'danger', icon: 'bi-x-circle-fill', text: 'Error' }, 'warning': { bg: 'warning', icon: 'bi-exclamation-triangle-fill', text: 'Advertencia' }, 'info': { bg: 'info', icon: 'bi-info-circle-fill', text: 'Información' } }; const config = typeMap[type] || typeMap['info']; // Crear container de toasts si no existe let toastContainer = document.getElementById('roiToastContainer'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'roiToastContainer'; toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x'; toastContainer.style.top = '60px'; toastContainer.style.zIndex = '999999'; document.body.appendChild(toastContainer); } // Crear toast const toastId = 'toast-' + Date.now(); const toastHTML = ` `; toastContainer.insertAdjacentHTML('beforeend', toastHTML); // Mostrar toast const toastElement = document.getElementById(toastId); const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 }); toast.show(); // Eliminar del DOM después de ocultarse toastElement.addEventListener('hidden.bs.toast', function() { toastElement.remove(); }); } /** * Muestra un modal de confirmación de Bootstrap */ function showConfirmModal(title, message, onConfirm) { // Crear modal si no existe let modal = document.getElementById('roiConfirmModal'); if (!modal) { const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); modal = document.getElementById('roiConfirmModal'); } // Actualizar contenido document.getElementById('roiConfirmModalTitle').textContent = title; document.getElementById('roiConfirmModalBody').textContent = message; // Configurar callback const confirmButton = document.getElementById('roiConfirmModalConfirm'); const newConfirmButton = confirmButton.cloneNode(true); confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton); newConfirmButton.addEventListener('click', function() { const bsModal = bootstrap.Modal.getInstance(modal); bsModal.hide(); if (typeof onConfirm === 'function') { onConfirm(); } }); // Mostrar modal const bsModal = new bootstrap.Modal(modal); bsModal.show(); } /** * Inicializa los color pickers para mostrar el valor HEX */ function initializeColorPickers() { const colorPickers = document.querySelectorAll('input[type="color"]'); colorPickers.forEach(picker => { // Elemento donde se muestra el valor HEX const valueDisplay = document.getElementById(picker.id + 'Value'); if (valueDisplay) { picker.addEventListener('input', function() { valueDisplay.textContent = this.value.toUpperCase(); }); } }); } })();