// 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 = `
${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 = `
`;
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 = `
${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 = `
`;
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