import React, { useEffect, useState, useLayoutEffect, useRef } from 'react'; import { fetchWithAuth, postWithAuth } from '../fetchWithAuth'; // Animación fade-in/slide-up para bloques const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`; if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-documents')) { const style = document.createElement('style'); style.id = 'fadein-slideup-documents'; style.innerHTML = fadeInSlideUp; document.head.appendChild(style); } import { fetchDocuments } from '../api/expedientes.ts'; import { useNotification } from '../context/NotificationContext'; import { usePolling } from '../hooks/usePolling'; import { Link } from 'react-router-dom'; const API_URL = import.meta.env.VITE_EFC_API_URL; const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => { try { const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`); if (!res.ok) { alert('No autorizado o error en la descarga'); return; } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); if (setSuccess) setSuccess('Descarga exitosa'); } catch (error) { console.error('Error downloading file:', error); showMessage('Error al descargar el archivo', 'error'); } }; export default function Documents() { const focusKeeperRef = useRef(null); const [success, setSuccess] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const [alertaFilter, setAlertaFilter] = useState('all'); // all, true, false const [expedienteFilter, setExpedienteFilter] = useState('all'); // all, true, false const [contribuyenteFilter, setContribuyenteFilter] = useState(''); const [contribuyenteInput, setContribuyenteInput] = useState(''); const [fechaPagoFilter, setFechaPagoFilter] = useState(''); const [pedimentoFilter, setPedimentoFilter] = useState(''); const [searchFilter, setSearchFilter] = useState(''); const [curpApoderadoFilter, setCurpApoderadoFilter] = useState(''); const [patenteFilter, setPatenteFilter] = useState(''); const [aduanaFilter, setAduanaFilter] = useState(''); const [tipoOperacionFilter, setTipoOperacionFilter] = useState(''); const [clavePedimentoFilter, setClavePedimentoFilter] = useState(''); const { showMessage } = useNotification(); // Estado para controlar la animación de entrada const [showAnimation, setShowAnimation] = useState(false); const [hasAnimated, setHasAnimated] = useState(false); useLayoutEffect(() => { // Forzar un render antes de activar la animación setShowAnimation(true); }, []); useEffect(() => { if (showAnimation && !hasAnimated) { const timeout = setTimeout(() => { setHasAnimated(true); setShowAnimation(false); }, 700); // Duración igual a la animación return () => clearTimeout(timeout); } }, [showAnimation, hasAnimated]); // Fetching usando la función tipada de TypeScript const fetchPedimentosData = async (page = currentPage, pageSize = itemsPerPage) => { // Construir objeto de filtros const filters = { search: searchFilter || undefined, pedimento: pedimentoFilter || undefined, existe_expediente: expedienteFilter === 'all' ? undefined : expedienteFilter, alerta: alertaFilter === 'all' ? undefined : alertaFilter, contribuyente: contribuyenteFilter || undefined, curp_apoderado: curpApoderadoFilter || undefined, fecha_pago: fechaPagoFilter || undefined, patente: patenteFilter || undefined, aduana: aduanaFilter || undefined, tipo_operacion: tipoOperacionFilter || undefined, clave_pedimento: clavePedimentoFilter || undefined, }; return await fetchDocuments(page, pageSize, filters); }; // Hook de polling que se ejecuta cada 30 segundos const { data: pedimentos, loading, error, refetch } = usePolling( () => fetchPedimentosData(currentPage, itemsPerPage), 30000, // 30 segundos [currentPage, itemsPerPage, searchFilter, pedimentoFilter, expedienteFilter, alertaFilter, contribuyenteFilter, curpApoderadoFilter, fechaPagoFilter, patenteFilter, aduanaFilter, tipoOperacionFilter, clavePedimentoFilter] ); // Manejo de errores de sesión useEffect(() => { if (error && error.message === 'SESSION_EXPIRED') { localStorage.removeItem('access'); localStorage.removeItem('refresh'); showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); setTimeout(() => { window.location.href = '/login'; }, 2000); } else if (error) { showMessage(error.message, 'error'); } }, [error, showMessage]); // Cálculos de paginación usando la estructura tipada const documentsArray = pedimentos && pedimentos.results ? pedimentos.results : []; const totalDocuments = pedimentos && typeof pedimentos.count === 'number' ? pedimentos.count : 0; const totalPages = totalDocuments > 0 ? Math.ceil(totalDocuments / itemsPerPage) : 1; const currentDocuments = documentsArray; // Obtener lista única de contribuyentes para el combobox (de la página actual) const contribuyentes = Array.from(new Set(currentDocuments.map(d => d.contribuyente).filter(Boolean))); // Refuerza la paginación SPA: nunca recarga la página, solo cambia el estado local const handlePageChange = (newPage, e) => { if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (e && typeof e.stopPropagation === 'function') e.stopPropagation(); if (newPage < 1 || newPage > totalPages || newPage === currentPage) return; setCurrentPage(newPage); // Quitar el foco del botón activo para evitar salto de scroll if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } }; // Forzar foco al div invisible para evitar saltos por enfoque automático useLayoutEffect(() => { if (focusKeeperRef.current) { focusKeeperRef.current.focus(); } }, [currentPage]); const handleItemsPerPageChange = (newItemsPerPage) => { setItemsPerPage(newItemsPerPage); setCurrentPage(1); // Reset a la primera página }; // El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla return (
{/* Header mejorado y decorativo */}

Expedientes {totalDocuments > 0 && ( {totalDocuments} registros )}

Gestiona y descarga los documentos de tus pedimentos

{/* Efectos decorativos de fondo modernos */}
{/* Partículas flotantes */}
{/* Animación personalizada para el icono y contador */}
{/* Filtros avanzados */}

Filtros de búsqueda

{/* Search global */}
setSearchFilter(e.target.value)} placeholder="Buscar pedimento, contribuyente..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
{/* Pedimento */}
setPedimentoFilter(e.target.value)} placeholder="Número de pedimento..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
{/* Expediente */}
{/* Contribuyente combobox */}
{ setContribuyenteInput(e.target.value); setContribuyenteFilter(''); }} placeholder="Buscar o escribir..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" autoComplete="off" /> {/* Dropdown de sugerencias */} {contribuyenteInput && (
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
Sin coincidencias
) : ( contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => ( )) )}
)}
{/* CURP Apoderado */}
setCurpApoderadoFilter(e.target.value)} placeholder="CURP del apoderado..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
{/* Fecha de pago */}
setFechaPagoFilter(e.target.value)} className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
{/* Patente */}
setPatenteFilter(e.target.value)} placeholder="Patente..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
{/* Aduana */}
setAduanaFilter(e.target.value)} placeholder="Aduana..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
{/* Tipo de operación */}
setTipoOperacionFilter(e.target.value)} placeholder="ID tipo operación..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
{/* Clave pedimento */}
setClavePedimentoFilter(e.target.value)} placeholder="Clave pedimento..." className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
Actualización automática cada 30s {loading && ( Actualizando... )}
{success && (

{success}

)}
{/* Vista de tabla para pantallas grandes */}
{loading ? ( ) : error ? ( ) : currentDocuments.length > 0 ? ( currentDocuments.map(ped => ( )) ) : ( )}
Pedimento Fecha de pago Contribuyente CURP Apoderado Importe total Saldo disponible Importe pedimento Expediente
Cargando expedientes...
Error: {error.message || 'Error al cargar expedientes'}
{ped.pedimento} {ped.fechapago} {ped.contribuyente} {ped.curp_apoderado} ${ped.importe_total} ${ped.saldo_disponible} ${ped.importe_pedimento} {ped.existe_expediente ? ( <> Sí ) : ( <> No )}

No hay expedientes

No se encontraron expedientes con los filtros aplicados.

{/* Vista de tarjetas para pantallas pequeñas y medianas */}
{loading ? (
Cargando expedientes...
) : error ? (
Error: {error.message || 'Error al cargar expedientes'}
) : currentDocuments.length > 0 ? ( currentDocuments.map(ped => (
{ped.pedimento}

{ped.fechapago}

{ped.existe_expediente ? 'Con expediente' : 'Sin expediente'}
Contribuyente: {ped.contribuyente}
{ped.curp_apoderado && (
CURP Apoderado: {ped.curp_apoderado}
)}
Importe total: ${ped.importe_total}
Saldo disponible: ${ped.saldo_disponible}
Importe pedimento: ${ped.importe_pedimento}
)) ) : (

No hay expedientes disponibles

Intenta ajustar los filtros de búsqueda

)}
{/* Paginación moderna y responsiva */} {totalDocuments > 0 && (
{(() => { const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage)); const maxPagesToShow = 5; let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2)); let endPage = startPage + maxPagesToShow - 1; if (endPage > totalPages) { endPage = totalPages; startPage = Math.max(1, endPage - maxPagesToShow + 1); } const pageNumbers = []; for (let i = startPage; i <= endPage; i++) { pageNumbers.push(i); } return (
{pageNumbers.map(num => ( ))}
{currentPage} / {totalPages}
Mostrando {((currentPage - 1) * itemsPerPage) + 1} a {Math.min(currentPage * itemsPerPage, totalDocuments)} de {totalDocuments} registros
); })()}
)}
); }