// Inicializar Swiper con efectos parallax (un solo slide) const heroSwiper = new Swiper('.heroSwiper', { loop: false, speed: 1400, parallax: true, autoplay: false, effect: 'slide', grabCursor: false, allowTouchMove: false, on: { init: function () { animateSlideContent(this); } } }); // Función para animar el contenido del slide function animateSlideContent(swiper) { const activeSlide = swiper.slides[swiper.activeIndex]; const elements = activeSlide.querySelectorAll( '.slide-main-title, .slide-subtitle, .slide-cta-group, .visual-main, .visual-float' ); elements.forEach((el, index) => { el.style.opacity = '0'; el.style.transform = 'translateY(40px)'; setTimeout(() => { el.style.transition = 'all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }, 150 + (index * 120)); }); } // Smooth scroll para los botones del slider document.querySelectorAll('.cta-btn').forEach(btn => { btn.addEventListener('click', function (e) { if (this.getAttribute('href') === '#catalogo-section') { e.preventDefault(); const target = document.querySelector('#catalogo-section'); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }); }); // Sistema de filtrado de materiales con selectores y búsqueda AJAX (function () { const availabilityFilter = document.getElementById('availabilityFilter'); const categoryFilter = document.getElementById('categoryFilter'); const quickSearch = document.getElementById('quickSearch'); const resetBtn = document.getElementById('resetFilters'); const materialCards = document.querySelectorAll('.material-card'); const materialsGrid = document.getElementById('materialsGrid'); let allMaterials = Array.from(materialCards); // Guardar referencia a todas las tarjetas originales let searchTimeout; let isSearching = false; // Función para aplicar filtros locales (disponibilidad y categoría) function applyLocalFilters() { const selectedAvailability = availabilityFilter.value; const selectedCategory = categoryFilter.value; let visibleCount = 0; // Si hay una búsqueda activa, no aplicar filtros locales if (isSearching) return; allMaterials.forEach(card => { const cardCategory = card.getAttribute('data-category'); const cardAvailability = card.getAttribute('data-availability'); const categoryMatch = selectedCategory === 'all' || cardCategory === selectedCategory; const availabilityMatch = selectedAvailability === 'all' || cardAvailability === selectedAvailability; if (categoryMatch && availabilityMatch) { card.style.display = 'block'; card.style.animation = 'fadeInUp 0.4s ease forwards'; visibleCount++; } else { card.style.display = 'none'; } }); // Mostrar mensaje si no hay resultados checkEmptyResults(visibleCount); } // Búsqueda AJAX usando la misma API que el buscador principal async function performQuickSearch(query) { if (query.length < 2) { // Si la búsqueda es muy corta, restaurar vista original y aplicar filtros isSearching = false; restoreOriginalCards(); applyLocalFilters(); return; } isSearching = true; try { const response = await fetch(`${BASE_URL}api/catalogo/buscar?q=${encodeURIComponent(query)}`); const data = await response.json(); if (data.success && data.materiales.length > 0) { displaySearchResults(data.materiales, data.total); } else { displayNoSearchResults(query); } } catch (error) { console.error('Error en búsqueda rápida:', error); isSearching = false; restoreOriginalCards(); applyLocalFilters(); } } // Mostrar resultados de búsqueda AJAX function displaySearchResults(materiales, total) { // Limpiar grid materialsGrid.innerHTML = ''; materiales.forEach(material => { const card = createMaterialCard(material); materialsGrid.appendChild(card); }); checkEmptyResults(materiales.length); } // Crear tarjeta de material desde datos AJAX function createMaterialCard(material) { const card = document.createElement('div'); card.className = 'material-card'; card.setAttribute('data-material-id', material.id); card.setAttribute('data-category', material.categoria.toLowerCase()); card.setAttribute('data-availability', material.disponible ? 'available' : 'unavailable'); const availabilityClass = material.disponible ? 'available' : 'unavailable'; const availabilityText = material.disponible ? 'Disponible' : 'No disponible'; const availabilityIcon = material.disponible ? 'fa-check-circle' : 'fa-times-circle'; card.innerHTML = `
${material.titulo}
Ver detalles
${availabilityText}
${material.categoria}

${material.titulo}

${material.autor ? `

${material.autor}

` : ''}
`; return card; } // Mostrar mensaje de sin resultados de búsqueda function displayNoSearchResults(query) { materialsGrid.innerHTML = `

No se encontraron resultados

No hay materiales que coincidan con "${query}".

`; if (resultsCount) { resultsCount.textContent = 0; } // Agregar evento al botón de limpiar const clearBtn = document.getElementById('clearSearchBtn'); if (clearBtn) { clearBtn.addEventListener('click', () => { quickSearch.value = ''; isSearching = false; restoreOriginalCards(); applyLocalFilters(); }); } } // Restaurar tarjetas originales function restoreOriginalCards() { materialsGrid.innerHTML = ''; allMaterials.forEach(card => { materialsGrid.appendChild(card); }); } // Verificar si no hay resultados function checkEmptyResults(count) { let emptyMessage = materialsGrid.querySelector('.no-results-message'); if (count === 0 && !isSearching) { if (!emptyMessage) { emptyMessage = document.createElement('div'); emptyMessage.className = 'no-results-message'; emptyMessage.innerHTML = `

No se encontraron materiales

No hay materiales que coincidan con los filtros seleccionados.

`; materialsGrid.appendChild(emptyMessage); } } else { if (emptyMessage) { emptyMessage.remove(); } } } // Debounce para la búsqueda function debounceSearch() { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const query = quickSearch.value.trim(); performQuickSearch(query); }, 400); } // Event listeners para los filtros if (availabilityFilter) { availabilityFilter.addEventListener('change', () => { if (!isSearching) { applyLocalFilters(); } }); } if (categoryFilter) { categoryFilter.addEventListener('change', function () { const val = this.value; const url = new URL(window.location.href); if (val === 'all') { url.searchParams.delete('categoria'); } else { url.searchParams.set('categoria', val); } url.searchParams.delete('page'); window.location.href = url.toString(); }); } if (quickSearch) { quickSearch.addEventListener('input', debounceSearch); } // Resetear filtros if (resetBtn) { resetBtn.addEventListener('click', function () { window.location.href = (typeof BASE_URL !== 'undefined' ? BASE_URL : '/') + 'catalogo'; }); } // Agregar animación CSS const style = document.createElement('style'); style.textContent = ` @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .no-results-message { grid-column: 1 / -1; } `; document.head.appendChild(style); })();// Función para registrar salida de un lector function registrarSalida(id) { Swal.fire({ title: '¿Registrar Salida?', text: "¿Confirma que desea registrar la salida de este lector?", icon: 'question', showCancelButton: true, confirmButtonColor: '#ffc107', cancelButtonColor: '#6c757d', confirmButtonText: 'Sí, registrar salida', cancelButtonText: 'Cancelar' }).then((result) => { if (result.isConfirmed) { const formData = new FormData(); // Añadir el token CSRF al FormData if (typeof CSRF_TOKEN !== 'undefined') { formData.append('csrf_token', CSRF_TOKEN); } fetch(`${BASE_URL}asistencia/salida/${id}`, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: formData }) .then(response => { // Verificar si la respuesta es JSON válida const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new Error('La respuesta del servidor no es JSON válida'); } return response.json(); }) .then(data => { if (data.success) { Swal.fire({ icon: 'success', title: 'Salida Registrada', text: data.message, timer: 2000, showConfirmButton: false }).then(() => { location.reload(); }); } else { Swal.fire({ icon: 'error', title: 'Error', text: data.message || 'Error al registrar la salida', }); } }) .catch(error => { console.error('Error:', error); Swal.fire({ icon: 'error', title: 'Error de conexión', text: 'No se pudo conectar con el servidor. Por favor, verifica tu conexión e intenta nuevamente.', }); }); } }); } // Función para ver detalles de una asistencia function verDetalles(id) { const modal = new bootstrap.Modal(document.getElementById('modalDetalles')); const content = document.getElementById('detallesContent'); content.innerHTML = `

Cargando...

`; modal.show(); fetch(`${BASE_URL}asistencia/detalles/${id}`) .then(response => response.json()) .then(data => { const tiempoHTML = data.fecha_salida ? calcularTiempo(data.fecha_ingreso, data.fecha_salida) : 'En biblioteca'; content.innerHTML = `
Información del Lector
Nombre:${data.nombre} ${data.apellido}
CI:${data.ci || 'N/A'}
Email:${data.email || 'N/A'}
Tipo:${data.tipo_lector || 'N/A'}
Carrera:${data.carrera || 'N/A'}
Facultad:${data.facultad || 'N/A'}
Información de la Visita
Fecha Ingreso:${formatearFecha(data.fecha_ingreso)}
Fecha Salida:${data.fecha_salida ? formatearFecha(data.fecha_salida) : 'En biblioteca'}
Tipo Visita:${data.tipo_visita.replace('_', ' ')}
Tiempo:${tiempoHTML}
Registrado por:${data.registrado_por_username || 'Sistema'}
${data.observaciones ? `
Observaciones

${data.observaciones}

` : ''} `; }) .catch(error => { console.error('Error:', error); content.innerHTML = `
Error al cargar los detalles
`; }); } // Función para eliminar una asistencia function eliminarAsistencia(id) { Swal.fire({ title: '¿Eliminar Asistencia?', text: "Esta acción no se puede deshacer. ¿Está seguro?", icon: 'warning', showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: '#3085d6', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar' }).then((result) => { if (result.isConfirmed) { fetch(`${BASE_URL}asistencia/eliminar/${id}`, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(() => { Swal.fire({ icon: 'success', title: 'Eliminado', text: 'La asistencia ha sido eliminada.', timer: 2000, showConfirmButton: false }).then(() => { location.reload(); }); }) .catch(error => { console.error('Error:', error); Swal.fire({ icon: 'error', title: 'Error', text: 'Error al eliminar la asistencia', }); }); } }); } // Función para exportar a Excel function exportarExcel() { const formFiltros = document.getElementById('formFiltros'); if (formFiltros) { const params = new URLSearchParams(new FormData(formFiltros)); window.location.href = `${BASE_URL}asistencia/exportar?${params.toString()}`; } else { window.location.href = `${BASE_URL}asistencia/exportar`; } } // Función para formatear fecha function formatearFecha(fecha) { const d = new Date(fecha); return d.toLocaleString('es-PY'); } // Función para calcular tiempo transcurrido function calcularTiempo(ingreso, salida) { const inicio = new Date(ingreso); const fin = new Date(salida); const diff = fin - inicio; const horas = Math.floor(diff / 3600000); const minutos = Math.floor((diff % 3600000) / 60000); if (horas > 0) { return `${horas}h ${minutos}m`; } return `${minutos} min`; } // Inicializar DataTable cuando el DOM esté listo document.addEventListener('DOMContentLoaded', function () { const tablaAsistencias = document.getElementById('tablaAsistencias'); if (tablaAsistencias) { new simpleDatatables.DataTable(tablaAsistencias, { searchable: true, fixedHeight: true, perPage: 25, labels: { placeholder: "Buscar...", perPage: "Filas por página", noRows: "No hay registros encontrados", info: "Mostrando {start} a {end} de {rows} registros" }, perPageSelect: [10, 25, 50, 100] }); } }); /* ======================================== MODERN UX/UI ENHANCEMENTS - LOGIC ======================================== */ document.addEventListener('DOMContentLoaded', function () { // 1. Counter Animation for Stats const animateCounters = () => { const counters = document.querySelectorAll('.counter-anim'); const speed = 200; counters.forEach(counter => { const target = +counter.getAttribute('data-target'); const updateCount = () => { const count = +counter.innerText; const inc = target / speed; if (count < target) { counter.innerText = Math.ceil(count + inc); setTimeout(updateCount, 1); } else { counter.innerText = target; } }; updateCount(); }); }; // Intersection Observer to trigger counters const statsSection = document.querySelector('.stats-bar-section'); if (statsSection) { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { animateCounters(); observer.unobserve(statsSection); } }, { threshold: 0.5 }); observer.observe(statsSection); } // 2. Arrivals Swiper Initialization if (document.querySelector('.arrivalsSwiper')) { const arrivalsSwiper = new Swiper('.arrivalsSwiper', { slidesPerView: 1, spaceBetween: 20, loop: true, autoplay: { delay: 4000, disableOnInteraction: false, }, pagination: { el: '.swiper-pagination', clickable: true, }, navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, breakpoints: { 640: { slidesPerView: 2, spaceBetween: 20 }, 1024: { slidesPerView: 3, spaceBetween: 30 }, 1440: { slidesPerView: 4, spaceBetween: 30 } } }); } // 3. Category Visual Filtering (AJAX Integration) const catButtons = document.querySelectorAll('.filter-cat-btn'); const categorySelect = document.getElementById('categoryFilter'); catButtons.forEach(btn => { btn.addEventListener('click', function () { const category = this.getAttribute('data-category'); // UI Feedback catButtons.forEach(b => b.classList.remove('active')); this.classList.add('active'); // Sync with select dropdown if (categorySelect) { categorySelect.value = category; // Dispatch change event to trigger existing filtering logic categorySelect.dispatchEvent(new Event('change')); } // Scroll to results const target = document.getElementById('catalogo-section'); if (target) target.scrollIntoView({ behavior: 'smooth' }); }); }); // 5. Skeleton Loading UI Enhancement const grid = document.getElementById('materialsGrid'); if (grid) { // We inject the skeleton markup when filtering starts const showSkeletons = () => { grid.innerHTML = Array(8).fill(`
`).join(''); }; } }); // Inicializar Swiper con efectos parallax (un solo slide) const heroSwiper = new Swiper('.heroSwiper', { loop: false, speed: 1400, parallax: true, autoplay: false, effect: 'slide', grabCursor: false, allowTouchMove: false, on: { init: function () { animateSlideContent(this); } } }); // Función para animar el contenido del slide function animateSlideContent(swiper) { const activeSlide = swiper.slides[swiper.activeIndex]; const elements = activeSlide.querySelectorAll( '.slide-main-title, .slide-subtitle, .slide-cta-group, .visual-main, .visual-float' ); elements.forEach((el, index) => { el.style.opacity = '0'; el.style.transform = 'translateY(40px)'; setTimeout(() => { el.style.transition = 'all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }, 150 + (index * 120)); }); } // Smooth scroll para los botones del slider document.querySelectorAll('.cta-btn').forEach(btn => { btn.addEventListener('click', function (e) { if (this.getAttribute('href') === '#catalogo-section') { e.preventDefault(); const target = document.querySelector('#catalogo-section'); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }); }); // Sistema de filtrado de materiales con selectores y búsqueda AJAX (function () { const availabilityFilter = document.getElementById('availabilityFilter'); const categoryFilter = document.getElementById('categoryFilter'); const quickSearch = document.getElementById('quickSearch'); const resetBtn = document.getElementById('resetFilters'); const materialCards = document.querySelectorAll('.material-card'); const materialsGrid = document.getElementById('materialsGrid'); let allMaterials = Array.from(materialCards); // Guardar referencia a todas las tarjetas originales let searchTimeout; let isSearching = false; // Función para aplicar filtros locales (disponibilidad y categoría) function applyLocalFilters() { const selectedAvailability = availabilityFilter.value; const selectedCategory = categoryFilter.value; let visibleCount = 0; // Si hay una búsqueda activa, no aplicar filtros locales if (isSearching) return; allMaterials.forEach(card => { const cardCategory = card.getAttribute('data-category'); const cardAvailability = card.getAttribute('data-availability'); const categoryMatch = selectedCategory === 'all' || cardCategory === selectedCategory; const availabilityMatch = selectedAvailability === 'all' || cardAvailability === selectedAvailability; if (categoryMatch && availabilityMatch) { card.style.display = 'block'; card.style.animation = 'fadeInUp 0.4s ease forwards'; visibleCount++; } else { card.style.display = 'none'; } }); // Mostrar mensaje si no hay resultados checkEmptyResults(visibleCount); } // Búsqueda AJAX usando la misma API que el buscador principal async function performQuickSearch(query) { if (query.length < 2) { // Si la búsqueda es muy corta, restaurar vista original y aplicar filtros isSearching = false; restoreOriginalCards(); applyLocalFilters(); return; } isSearching = true; try { const response = await fetch(`${BASE_URL}api/catalogo/buscar?q=${encodeURIComponent(query)}`); const data = await response.json(); if (data.success && data.materiales.length > 0) { displaySearchResults(data.materiales, data.total); } else { displayNoSearchResults(query); } } catch (error) { console.error('Error en búsqueda rápida:', error); isSearching = false; restoreOriginalCards(); applyLocalFilters(); } } // Mostrar resultados de búsqueda AJAX function displaySearchResults(materiales, total) { // Limpiar grid materialsGrid.innerHTML = ''; materiales.forEach(material => { const card = createMaterialCard(material); materialsGrid.appendChild(card); }); checkEmptyResults(materiales.length); } // Crear tarjeta de material desde datos AJAX function createMaterialCard(material) { const card = document.createElement('div'); card.className = 'material-card'; card.setAttribute('data-material-id', material.id); card.setAttribute('data-category', material.categoria.toLowerCase()); card.setAttribute('data-availability', material.disponible ? 'available' : 'unavailable'); const availabilityClass = material.disponible ? 'available' : 'unavailable'; const availabilityText = material.disponible ? 'Disponible' : 'No disponible'; const availabilityIcon = material.disponible ? 'fa-check-circle' : 'fa-times-circle'; card.innerHTML = `
${material.titulo}
Ver detalles
${availabilityText}
${material.categoria}

${material.titulo}

${material.autor ? `

${material.autor}

` : ''}
`; return card; } // Mostrar mensaje de sin resultados de búsqueda function displayNoSearchResults(query) { materialsGrid.innerHTML = `

No se encontraron resultados

No hay materiales que coincidan con "${query}".

`; if (resultsCount) { resultsCount.textContent = 0; } // Agregar evento al botón de limpiar const clearBtn = document.getElementById('clearSearchBtn'); if (clearBtn) { clearBtn.addEventListener('click', () => { quickSearch.value = ''; isSearching = false; restoreOriginalCards(); applyLocalFilters(); }); } } // Restaurar tarjetas originales function restoreOriginalCards() { materialsGrid.innerHTML = ''; allMaterials.forEach(card => { materialsGrid.appendChild(card); }); } // Verificar si no hay resultados function checkEmptyResults(count) { let emptyMessage = materialsGrid.querySelector('.no-results-message'); if (count === 0 && !isSearching) { if (!emptyMessage) { emptyMessage = document.createElement('div'); emptyMessage.className = 'no-results-message'; emptyMessage.innerHTML = `

No se encontraron materiales

No hay materiales que coincidan con los filtros seleccionados.

`; materialsGrid.appendChild(emptyMessage); } } else { if (emptyMessage) { emptyMessage.remove(); } } } // Debounce para la búsqueda function debounceSearch() { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const query = quickSearch.value.trim(); performQuickSearch(query); }, 400); } // Event listeners para los filtros if (availabilityFilter) { availabilityFilter.addEventListener('change', () => { if (!isSearching) { applyLocalFilters(); } }); } if (categoryFilter) { categoryFilter.addEventListener('change', function () { const val = this.value; const url = new URL(window.location.href); if (val === 'all') { url.searchParams.delete('categoria'); } else { url.searchParams.set('categoria', val); } url.searchParams.delete('page'); window.location.href = url.toString(); }); } if (quickSearch) { quickSearch.addEventListener('input', debounceSearch); } // Resetear filtros if (resetBtn) { resetBtn.addEventListener('click', function () { window.location.href = (typeof BASE_URL !== 'undefined' ? BASE_URL : '/') + 'catalogo'; }); } // Agregar animación CSS const style = document.createElement('style'); style.textContent = ` @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .no-results-message { grid-column: 1 / -1; } `; document.head.appendChild(style); })();// Función para registrar salida de un lector function registrarSalida(id) { Swal.fire({ title: '¿Registrar Salida?', text: "¿Confirma que desea registrar la salida de este lector?", icon: 'question', showCancelButton: true, confirmButtonColor: '#ffc107', cancelButtonColor: '#6c757d', confirmButtonText: 'Sí, registrar salida', cancelButtonText: 'Cancelar' }).then((result) => { if (result.isConfirmed) { const formData = new FormData(); // Añadir el token CSRF al FormData if (typeof CSRF_TOKEN !== 'undefined') { formData.append('csrf_token', CSRF_TOKEN); } fetch(`${BASE_URL}asistencia/salida/${id}`, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: formData }) .then(response => { // Verificar si la respuesta es JSON válida const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new Error('La respuesta del servidor no es JSON válida'); } return response.json(); }) .then(data => { if (data.success) { Swal.fire({ icon: 'success', title: 'Salida Registrada', text: data.message, timer: 2000, showConfirmButton: false }).then(() => { location.reload(); }); } else { Swal.fire({ icon: 'error', title: 'Error', text: data.message || 'Error al registrar la salida', }); } }) .catch(error => { console.error('Error:', error); Swal.fire({ icon: 'error', title: 'Error de conexión', text: 'No se pudo conectar con el servidor. Por favor, verifica tu conexión e intenta nuevamente.', }); }); } }); } // Función para ver detalles de una asistencia function verDetalles(id) { const modal = new bootstrap.Modal(document.getElementById('modalDetalles')); const content = document.getElementById('detallesContent'); content.innerHTML = `

Cargando...

`; modal.show(); fetch(`${BASE_URL}asistencia/detalles/${id}`) .then(response => response.json()) .then(data => { const tiempoHTML = data.fecha_salida ? calcularTiempo(data.fecha_ingreso, data.fecha_salida) : 'En biblioteca'; content.innerHTML = `
Información del Lector
Nombre:${data.nombre} ${data.apellido}
CI:${data.ci || 'N/A'}
Email:${data.email || 'N/A'}
Tipo:${data.tipo_lector || 'N/A'}
Carrera:${data.carrera || 'N/A'}
Facultad:${data.facultad || 'N/A'}
Información de la Visita
Fecha Ingreso:${formatearFecha(data.fecha_ingreso)}
Fecha Salida:${data.fecha_salida ? formatearFecha(data.fecha_salida) : 'En biblioteca'}
Tipo Visita:${data.tipo_visita.replace('_', ' ')}
Tiempo:${tiempoHTML}
Registrado por:${data.registrado_por_username || 'Sistema'}
${data.observaciones ? `
Observaciones

${data.observaciones}

` : ''} `; }) .catch(error => { console.error('Error:', error); content.innerHTML = `
Error al cargar los detalles
`; }); } // Función para eliminar una asistencia function eliminarAsistencia(id) { Swal.fire({ title: '¿Eliminar Asistencia?', text: "Esta acción no se puede deshacer. ¿Está seguro?", icon: 'warning', showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: '#3085d6', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar' }).then((result) => { if (result.isConfirmed) { fetch(`${BASE_URL}asistencia/eliminar/${id}`, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(() => { Swal.fire({ icon: 'success', title: 'Eliminado', text: 'La asistencia ha sido eliminada.', timer: 2000, showConfirmButton: false }).then(() => { location.reload(); }); }) .catch(error => { console.error('Error:', error); Swal.fire({ icon: 'error', title: 'Error', text: 'Error al eliminar la asistencia', }); }); } }); } // Función para exportar a Excel function exportarExcel() { const formFiltros = document.getElementById('formFiltros'); if (formFiltros) { const params = new URLSearchParams(new FormData(formFiltros)); window.location.href = `${BASE_URL}asistencia/exportar?${params.toString()}`; } else { window.location.href = `${BASE_URL}asistencia/exportar`; } } // Función para formatear fecha function formatearFecha(fecha) { const d = new Date(fecha); return d.toLocaleString('es-PY'); } // Función para calcular tiempo transcurrido function calcularTiempo(ingreso, salida) { const inicio = new Date(ingreso); const fin = new Date(salida); const diff = fin - inicio; const horas = Math.floor(diff / 3600000); const minutos = Math.floor((diff % 3600000) / 60000); if (horas > 0) { return `${horas}h ${minutos}m`; } return `${minutos} min`; } // Inicializar DataTable cuando el DOM esté listo document.addEventListener('DOMContentLoaded', function () { const tablaAsistencias = document.getElementById('tablaAsistencias'); if (tablaAsistencias) { new simpleDatatables.DataTable(tablaAsistencias, { searchable: true, fixedHeight: true, perPage: 25, labels: { placeholder: "Buscar...", perPage: "Filas por página", noRows: "No hay registros encontrados", info: "Mostrando {start} a {end} de {rows} registros" }, perPageSelect: [10, 25, 50, 100] }); } }); /* ======================================== MODERN UX/UI ENHANCEMENTS - LOGIC ======================================== */ document.addEventListener('DOMContentLoaded', function () { // 1. Counter Animation for Stats const animateCounters = () => { const counters = document.querySelectorAll('.counter-anim'); const speed = 200; counters.forEach(counter => { const target = +counter.getAttribute('data-target'); const updateCount = () => { const count = +counter.innerText; const inc = target / speed; if (count < target) { counter.innerText = Math.ceil(count + inc); setTimeout(updateCount, 1); } else { counter.innerText = target; } }; updateCount(); }); }; // Intersection Observer to trigger counters const statsSection = document.querySelector('.stats-bar-section'); if (statsSection) { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { animateCounters(); observer.unobserve(statsSection); } }, { threshold: 0.5 }); observer.observe(statsSection); } // 2. Arrivals Swiper Initialization if (document.querySelector('.arrivalsSwiper')) { const arrivalsSwiper = new Swiper('.arrivalsSwiper', { slidesPerView: 1, spaceBetween: 20, loop: true, autoplay: { delay: 4000, disableOnInteraction: false, }, pagination: { el: '.swiper-pagination', clickable: true, }, navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, breakpoints: { 640: { slidesPerView: 2, spaceBetween: 20 }, 1024: { slidesPerView: 3, spaceBetween: 30 }, 1440: { slidesPerView: 4, spaceBetween: 30 } } }); } // 3. Category Visual Filtering (AJAX Integration) const catButtons = document.querySelectorAll('.filter-cat-btn'); const categorySelect = document.getElementById('categoryFilter'); catButtons.forEach(btn => { btn.addEventListener('click', function () { const category = this.getAttribute('data-category'); // UI Feedback catButtons.forEach(b => b.classList.remove('active')); this.classList.add('active'); // Sync with select dropdown if (categorySelect) { categorySelect.value = category; // Dispatch change event to trigger existing filtering logic categorySelect.dispatchEvent(new Event('change')); } // Scroll to results const target = document.getElementById('catalogo-section'); if (target) target.scrollIntoView({ behavior: 'smooth' }); }); }); // 5. Skeleton Loading UI Enhancement const grid = document.getElementById('materialsGrid'); if (grid) { // We inject the skeleton markup when filtering starts const showSkeletons = () => { grid.innerHTML = Array(8).fill(`
`).join(''); }; } });

Error Fatal

Class 'App\Controllers\CatalogoController' not found