Files
roi-theme/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
FrankZamora 0846a3bf03 Migración completa a Clean Architecture con componentes funcionales
- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/
- 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero,
  FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost,
  RelatedPost, ContactForm, Footer
- Panel de administración con tabs Bootstrap 5 funcionales
- Schemas JSON para configuración de componentes
- Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado)
- FormBuilders para UI admin con Design System consistente
- Fix: Bootstrap JS cargado en header para tabs funcionales
- Fix: buildTextInput maneja valores mixed (bool/string)
- Eliminación de estructura legacy (src/, admin/, assets/css/componente-*)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 21:20:06 -06:00

408 lines
15 KiB
JavaScript

/**
* 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() {
initializeTabs();
initializeFormValidation();
initializeButtons();
initializeColorPickers();
});
/**
* Inicializa el sistema de tabs
*/
function initializeTabs() {
const tabs = document.querySelectorAll('.nav-tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function(e) {
// Prevenir comportamiento por defecto si es necesario
// (En este caso dejamos que funcione la navegación normal)
});
});
}
/**
* 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 = '<p>' + escapeHtml(message) + '</p>';
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 = '<i class="bi bi-hourglass-split me-1"></i> 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 = '<i class="bi bi-hourglass-split me-1"></i> 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.');
setTimeout(() => 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 = `
<div id="${toastId}" class="toast align-items-center text-white bg-${config.bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="bi ${config.icon} me-2"></i>
<strong>${escapeHtml(message)}</strong>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
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 = `
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
<span id="roiConfirmModalTitle">Confirmar</span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
Mensaje de confirmación
</div>
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i>
Cancelar
</button>
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
<i class="bi bi-check-circle me-1"></i>
Confirmar
</button>
</div>
</div>
</div>
</div>
`;
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();
});
}
});
}
})();