/** * app.js — JavaScript global del sistema * Bootstrap para todos los módulos UI */ 'use strict'; // ── Tema oscuro / claro ─────────────────────────────────────────────────────── const html = document.documentElement; const themeBtn = document.getElementById('themeToggle'); const themeIcon = document.getElementById('themeIcon'); function applyTheme(theme) { html.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); if (themeIcon) { themeIcon.className = theme === 'dark' ? 'fa-solid fa-sun' : 'fa-solid fa-moon'; } // Sincronizar con Chart.js si hay gráficos if (window.Chart) { Chart.defaults.color = theme === 'dark' ? '#94a3b8' : '#64748b'; Chart.defaults.borderColor = theme === 'dark' ? '#334155' : '#e2e8f0'; } } // Cargar tema al iniciar const savedTheme = localStorage.getItem('theme') || html.getAttribute('data-theme') || 'light'; applyTheme(savedTheme); if (themeBtn) { themeBtn.addEventListener('click', () => { const current = html.getAttribute('data-theme'); applyTheme(current === 'dark' ? 'light' : 'dark'); // Guardar en servidor vía AJAX fetch('?action=set_theme&theme=' + html.getAttribute('data-theme'), { method: 'POST' }).catch(() => {}); }); } // ── Sidebar toggle (móvil) ──────────────────────────────────────────────────── const sidebar = document.getElementById('sidebar'); const sidebarBtn = document.getElementById('sidebarToggle'); const overlay = document.getElementById('sidebarOverlay'); if (sidebarBtn && sidebar) { sidebarBtn.addEventListener('click', () => { sidebar.classList.toggle('open'); }); } // Click fuera para cerrar sidebar en móvil document.addEventListener('click', (e) => { if (window.innerWidth < 768 && sidebar && !sidebar.contains(e.target) && e.target !== sidebarBtn) { sidebar.classList.remove('open'); } }); // ── Notificaciones dropdown ─────────────────────────────────────────────────── const notifBtn = document.getElementById('notifToggle'); const notifDropdown = document.getElementById('notifDropdown'); if (notifBtn && notifDropdown) { notifBtn.addEventListener('click', (e) => { e.stopPropagation(); notifDropdown.classList.toggle('show'); }); document.addEventListener('click', () => { notifDropdown.classList.remove('show'); }); } // ── Búsqueda global ─────────────────────────────────────────────────────────── const searchToggle = document.getElementById('toggleSearch'); const searchWrap = document.getElementById('globalSearchWrap'); const searchInput = document.getElementById('globalSearch'); if (searchToggle && searchWrap) { searchToggle.addEventListener('click', () => { const isVisible = searchWrap.style.display !== 'none'; searchWrap.style.display = isVisible ? 'none' : 'flex'; if (!isVisible) searchInput.focus(); }); searchInput && searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const q = searchInput.value.trim(); if (q) { window.location.href = (window.appUrl || '') + '/views/oficios/lista.php?busqueda=' + encodeURIComponent(q); } } if (e.key === 'Escape') { searchWrap.style.display = 'none'; } }); } // ── Tabs ────────────────────────────────────────────────────────────────────── document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', function() { const container = this.closest('[data-tabs]') || document.body; container.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); container.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); this.classList.add('active'); const target = document.getElementById(this.dataset.tab); if (target) target.classList.add('active'); }); }); // ── Modales ─────────────────────────────────────────────────────────────────── document.querySelectorAll('[data-modal]').forEach(btn => { btn.addEventListener('click', function() { const modal = document.getElementById(this.dataset.modal); if (modal) modal.classList.add('show'); }); }); document.querySelectorAll('[data-modal-close]').forEach(btn => { btn.addEventListener('click', function() { this.closest('.modal-overlay').classList.remove('show'); }); }); document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('show'); }); }); // ── Toast notifications ─────────────────────────────────────────────────────── function showToast(message, type = 'success', duration = 4000) { const container = document.getElementById('toastContainer'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: 'fa-check-circle', error: 'fa-circle-xmark', warn: 'fa-triangle-exclamation' }; const colors = { success: '#10b981', error: '#ef4444', warn: '#f59e0b' }; toast.innerHTML = ` ${message} `; container.appendChild(toast); setTimeout(() => toast.remove(), duration); } // Leer flash messages desde la URL const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('success')) showToast(decodeURIComponent(urlParams.get('success'))); if (urlParams.get('error')) showToast(decodeURIComponent(urlParams.get('error')), 'error'); // ── Confirmación de eliminación ─────────────────────────────────────────────── document.querySelectorAll('[data-confirm]').forEach(el => { el.addEventListener('click', function(e) { if (!confirm(this.dataset.confirm || '¿Confirma esta acción?')) { e.preventDefault(); } }); }); // ── DataTables globales ─────────────────────────────────────────────────────── function initDataTable(selector, opts = {}) { if (typeof $ === 'undefined' || !$.fn.DataTable) return; const defaults = { language: { url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/es-ES.json' }, responsive: true, pageLength: 25, order: [], }; return $(selector).DataTable({ ...defaults, ...opts }); } // ── Dropzone ────────────────────────────────────────────────────────────────── document.querySelectorAll('.dropzone').forEach(zone => { const input = zone.querySelector('input[type="file"]'); zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragover'); }); zone.addEventListener('dragleave', () => zone.classList.remove('dragover')); zone.addEventListener('drop', e => { e.preventDefault(); zone.classList.remove('dragover'); if (input && e.dataTransfer.files.length) { input.files = e.dataTransfer.files; zone.querySelector('.dropzone-label') && (zone.querySelector('.dropzone-label').textContent = `${e.dataTransfer.files.length} archivo(s) seleccionado(s)`); } }); zone.addEventListener('click', () => input && input.click()); }); // ── Autocompletar número de oficio ──────────────────────────────────────────── const tipoSelect = document.getElementById('tipo'); const numOficio = document.getElementById('numero_oficio'); const autoGenBtn = document.getElementById('autoGenNumero'); if (tipoSelect && autoGenBtn) { autoGenBtn.addEventListener('click', () => { const tipo = tipoSelect.value; fetch(`${window.appUrl}/api/oficios.php?action=gen_numero&tipo=${tipo}`) .then(r => r.json()) .then(d => { if (numOficio && d.numero) numOficio.value = d.numero; }); }); } // ── Contador de caracteres en textarea ─────────────────────────────────────── document.querySelectorAll('textarea[data-maxlen]').forEach(ta => { const max = parseInt(ta.dataset.maxlen); const counter = document.createElement('div'); counter.className = 'form-text text-end'; counter.style.marginTop = '.25rem'; ta.parentNode.insertBefore(counter, ta.nextSibling); const update = () => counter.textContent = `${ta.value.length} / ${max}`; ta.addEventListener('input', update); update(); });