217 lines
9.5 KiB
JavaScript
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();
|
|
});
|