217 lines
9.5 KiB
JavaScript

/**
* 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 = `
<i class="fa-solid ${icons[type] || icons.success}" style="color:${colors[type]};font-size:1.1rem;flex-shrink:0"></i>
<span style="flex:1">${message}</span>
<button onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:.9rem">
<i class="fa-solid fa-xmark"></i>
</button>
`;
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();
});