From b35c87bd281ea57faf82e0f02bfff8dd902ef487 Mon Sep 17 00:00:00 2001 From: JCedillo Date: Thu, 9 Oct 2025 13:01:49 -0500 Subject: [PATCH] feat: Add checkbox selection and bulk operations - Add checkbox functionality to Expedientes.jsx with bulk document deletion - Add checkbox functionality to PedimentoDetail.jsx documents table - Implement custom modal for deletion confirmation with modern UI - Fix pagination reset on filter changes in Procesos.jsx - Add bulk selection with 'select all' functionality - Integrate with /record/documents/bulk-delete/ endpoint - Improve UX with loading states and success/error messages --- src/pages/Expedientes.jsx | 289 ++++++++++++++++++++++++++++++++-- src/pages/PedimentoDetail.jsx | 230 +++++++++++++++++++++++++++ src/pages/Procesos.jsx | 35 +++- 3 files changed, 541 insertions(+), 13 deletions(-) diff --git a/src/pages/Expedientes.jsx b/src/pages/Expedientes.jsx index 0000fac..dd6b03c 100644 --- a/src/pages/Expedientes.jsx +++ b/src/pages/Expedientes.jsx @@ -58,6 +58,14 @@ export default function Documents() { const [tipoOperacionFilter, setTipoOperacionFilter] = useState(''); const [clavePedimentoFilter, setClavePedimentoFilter] = useState(''); const { showMessage } = useNotification(); + + // Estados para selección múltiple + const [selectedDocuments, setSelectedDocuments] = useState([]); + const [isSelectAll, setIsSelectAll] = useState(false); + + // Estado para modal de confirmación de eliminación + const [showDeleteModal, setShowDeleteModal] = useState(false); + // Estado para controlar la animación de entrada const [showAnimation, setShowAnimation] = useState(false); const [hasAnimated, setHasAnimated] = useState(false); @@ -148,6 +156,119 @@ export default function Documents() { setCurrentPage(1); // Reset a la primera página }; + // Funciones para manejo de selección múltiple + const handleSelectDocument = (documentId, isSelected) => { + if (isSelected) { + setSelectedDocuments(prev => [...prev, documentId]); + } else { + setSelectedDocuments(prev => prev.filter(id => id !== documentId)); + } + }; + + const handleSelectAll = () => { + if (isSelectAll) { + setSelectedDocuments([]); + setIsSelectAll(false); + } else { + const allDocumentIds = currentDocuments.map(doc => doc.id); + setSelectedDocuments(allDocumentIds); + setIsSelectAll(true); + } + }; + + // Función para descargar documentos seleccionados + const handleDownloadSelected = async () => { + if (selectedDocuments.length === 0) { + showMessage('No hay documentos seleccionados para descargar', 'warning'); + return; + } + + try { + showMessage(`Iniciando descarga de ${selectedDocuments.length} documento(s)...`, 'info'); + + for (const docId of selectedDocuments) { + const document = currentDocuments.find(doc => doc.id === docId); + if (document) { + await downloadFile(docId, `expediente_${document.pedimento_app}`, null, null, showMessage); + // Pequeña pausa entre descargas para no sobrecargar + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + showMessage(`Descarga completada de ${selectedDocuments.length} documento(s)`, 'success'); + setSelectedDocuments([]); + setIsSelectAll(false); + } catch (error) { + showMessage('Error durante la descarga masiva', 'error'); + } + }; + + // Función para eliminar documentos seleccionados + const handleDeleteSelected = async () => { + if (selectedDocuments.length === 0) { + showMessage('No hay documentos seleccionados para eliminar', 'warning'); + return; + } + + // Mostrar modal de confirmación + setShowDeleteModal(true); + }; + + // Función para confirmar la eliminación + const confirmDelete = async () => { + setShowDeleteModal(false); + + try { + showMessage(`Eliminando ${selectedDocuments.length} documento(s)...`, 'info'); + + // Enviar todos los IDs seleccionados a un endpoint específico + const response = await fetchWithAuth(`${API_URL}/customs/pedimentos/bulk-delete/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ids: selectedDocuments + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.detail || errorData?.message || `Error ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + showMessage( + `${result.deleted_count || selectedDocuments.length} documento(s) eliminado(s) exitosamente`, + 'success' + ); + + setSelectedDocuments([]); + setIsSelectAll(false); + + // Refrescar la lista + refetch(); + } catch (error) { + console.error('Error durante la eliminación masiva:', error); + showMessage(`Error durante la eliminación: ${error.message}`, 'error'); + } + }; + + // Actualizar isSelectAll cuando cambia la selección + useEffect(() => { + if (currentDocuments.length > 0) { + const allSelected = currentDocuments.every(doc => selectedDocuments.includes(doc.id)); + setIsSelectAll(allSelected && selectedDocuments.length > 0); + } + }, [selectedDocuments, currentDocuments]); + + // Limpiar selección cuando cambia de página + useEffect(() => { + setSelectedDocuments([]); + setIsSelectAll(false); + }, [currentPage]); + // El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla @@ -362,6 +483,54 @@ export default function Documents() { + {/* Área de acciones para documentos seleccionados */} + {selectedDocuments.length > 0 && ( +
+
+
+
+
+ + + +
+
+

+ {selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''} seleccionado{selectedDocuments.length !== 1 ? 's' : ''} +

+

Selecciona una acción para continuar

+
+
+ +
+
+
+
+ +
+
+
+ )} +
@@ -425,6 +594,14 @@ export default function Documents() { + @@ -442,7 +619,7 @@ export default function Documents() { {loading ? ( - ) : error ? ( -
+ + Pedimento Fecha Pago Contribuyente
+
Cargando expedientes... @@ -451,7 +628,7 @@ export default function Documents() {
+
@@ -465,6 +642,14 @@ export default function Documents() { ) : currentDocuments.length > 0 ? ( currentDocuments.map(ped => (
+ + - +
@@ -571,14 +756,24 @@ export default function Documents() { ) : currentDocuments.length > 0 ? ( currentDocuments.map(ped => (
-
+ {/* Checkbox en la esquina superior derecha */} +
+ handleSelectDocument(ped.id, e.target.checked)} + className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> +
+ +
{/* Agregamos pr-8 para dar espacio al checkbox */}
-
+

{ped.fechapago}

+ + {ped.existe_expediente ? 'Con expediente' : 'Sin expediente'} +
- - {ped.existe_expediente ? 'Con expediente' : 'Sin expediente'} -
@@ -750,6 +945,76 @@ export default function Documents() { )}
+ + {/* Modal de confirmación para eliminación */} + {showDeleteModal && ( +
+
+ {/* Header del modal */} +
+
+
+ + + +
+
+

+ Confirmar eliminación +

+

Esta acción no se puede deshacer

+
+
+
+ + {/* Contenido del modal */} +
+
+

+ ¿Estás seguro de que deseas eliminar{' '} + + {selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''} + + ? +

+ +
+
+ + + +
+

Advertencia importante

+

+ Los documentos eliminados no podrán ser recuperados. Asegúrate de que realmente deseas proceder con esta acción. +

+
+
+
+
+
+ + {/* Botones del modal */} +
+ + +
+
+
+ )}
); diff --git a/src/pages/PedimentoDetail.jsx b/src/pages/PedimentoDetail.jsx index c32fe11..3f58494 100644 --- a/src/pages/PedimentoDetail.jsx +++ b/src/pages/PedimentoDetail.jsx @@ -124,6 +124,12 @@ export default function PedimentoDetail() { const [selected, setSelected] = useState([]); const [downloading, setDownloading] = useState(false); const [downloadingAll, setDownloadingAll] = useState(false); + + // Estados para selección múltiple con checkboxes + const [selectedDocuments, setSelectedDocuments] = useState([]); + const [isSelectAllDocs, setIsSelectAllDocs] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [dashboardSummary, setDashboardSummary] = useState(null); const [showFilters, setShowFilters] = useState(false); @@ -580,6 +586,95 @@ export default function PedimentoDetail() { return fuentes[fuente] || 'Desconocida'; }; + // Funciones para manejo de selección múltiple de documentos + const handleSelectDocument = (documentId) => { + const isSelected = selectedDocuments.includes(documentId); + if (isSelected) { + setSelectedDocuments(prev => prev.filter(id => id !== documentId)); + } else { + setSelectedDocuments(prev => [...prev, documentId]); + } + }; + + const handleSelectAllDocuments = () => { + if (isSelectAllDocs) { + setSelectedDocuments([]); + setIsSelectAllDocs(false); + } else { + const allDocumentIds = documents.map(doc => doc.id); + setSelectedDocuments(allDocumentIds); + setIsSelectAllDocs(true); + } + }; + + // Función para eliminar documentos seleccionados + const handleDeleteSelectedDocuments = async () => { + if (selectedDocuments.length === 0) { + showMessage('No hay documentos seleccionados para eliminar', 'warning'); + return; + } + + // Mostrar modal de confirmación + setShowDeleteModal(true); + }; + + // Función para confirmar la eliminación + const confirmDeleteDocuments = async () => { + setShowDeleteModal(false); + + try { + showMessage(`Eliminando ${selectedDocuments.length} documento(s)...`, 'info'); + + // Enviar todos los IDs seleccionados a un endpoint específico + const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-delete/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ids: selectedDocuments + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.detail || errorData?.message || `Error ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + showMessage( + `${result.deleted_count || selectedDocuments.length} documento(s) eliminado(s) exitosamente`, + 'success' + ); + + setSelectedDocuments([]); + setIsSelectAllDocs(false); + + // Forzar recarga de documentos cambiando la página temporalmente + const currentPage = page; + setPage(0); // Cambio temporal + setTimeout(() => setPage(currentPage), 100); // Restaurar después de un breve delay + } catch (error) { + console.error('Error durante la eliminación masiva:', error); + showMessage(`Error durante la eliminación: ${error.message}`, 'error'); + } + }; + + // Efecto para actualizar isSelectAllDocs cuando cambia la selección + useEffect(() => { + if (documents.length > 0) { + const allSelected = documents.every(doc => selectedDocuments.includes(doc.id)); + setIsSelectAllDocs(allSelected && selectedDocuments.length > 0); + } + }, [selectedDocuments, documents]); + + // Efecto para limpiar selección cuando cambia de página + useEffect(() => { + setSelectedDocuments([]); + setIsSelectAllDocs(false); + }, [page]); + // Efecto para cargar datos del pedimento useEffect(() => { const fetchPedimento = async () => { @@ -2093,6 +2188,55 @@ export default function PedimentoDetail() {
)}
+ + {/* Área de acciones para documentos seleccionados */} + {selectedDocuments.length > 0 && ( +
+
+
+
+
+ + + +
+
+

+ {selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''} seleccionado{selectedDocuments.length !== 1 ? 's' : ''} +

+

Selecciona una acción para continuar

+
+
+ +
+
+
+
+ +
+
+
+ )} + {loading ? (
@@ -2126,6 +2270,14 @@ export default function PedimentoDetail() { + {documents.map((doc, index) => ( +
+ +
+ handleSelectDocument(doc.id)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> +
@@ -4461,6 +4621,76 @@ export default function PedimentoDetail() {
)} + + {/* Modal de confirmación para eliminación */} + {showDeleteModal && ( +
+
+ {/* Header del modal */} +
+
+
+ + + +
+
+

+ Confirmar eliminación +

+

Esta acción no se puede deshacer

+
+
+
+ + {/* Contenido del modal */} +
+
+

+ ¿Estás seguro de que deseas eliminar{' '} + + {selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''} + + ? +

+ +
+
+ + + +
+

Advertencia importante

+

+ Los documentos eliminados no podrán ser recuperados. Asegúrate de que realmente deseas proceder con esta acción. +

+
+
+
+
+
+ + {/* Botones del modal */} +
+ + +
+
+
+ )} ); } \ No newline at end of file diff --git a/src/pages/Procesos.jsx b/src/pages/Procesos.jsx index d1ecf03..a1b71bf 100644 --- a/src/pages/Procesos.jsx +++ b/src/pages/Procesos.jsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Link } from 'react-router-dom'; import { fetchProcesamientoPedimentos } from '../api/procesos.ts'; import { postWithAuth, putWithAuth } from '../fetchWithAuth'; @@ -40,6 +40,15 @@ export default function Procesos() { const [selectedProcesos, setSelectedProcesos] = useState([]); const [isSelectAll, setIsSelectAll] = useState(false); + // Ref para rastrear valores previos de filtros y detectar cambios + const prevFiltersRef = useRef({ + pedimentoPedimentoFilter: '', + estadoFilter: '', + servicioFilter: '', + sortField: '', + sortOrder: 'asc' + }); + // Función para mostrar toast const showToast = (type, title, message, details = '', persistent = false, progress = null) => { const id = Date.now(); @@ -691,6 +700,30 @@ export default function Procesos() { useEffect(() => { async function fetchProcesos() { + // Detectar si algún filtro cambió + const currentFilters = { + pedimentoPedimentoFilter, + estadoFilter, + servicioFilter, + sortField, + sortOrder + }; + + const filtersChanged = Object.keys(currentFilters).some( + key => currentFilters[key] !== prevFiltersRef.current[key] + ); + + // Si los filtros cambiaron y no estamos en la página 1, resetear página + if (filtersChanged && page !== 1) { + setPage(1); + // Actualizar ref con valores actuales + prevFiltersRef.current = { ...currentFilters }; + return; // Salir temprano, el efecto se ejecutará de nuevo con page = 1 + } + + // Actualizar ref con valores actuales + prevFiltersRef.current = { ...currentFilters }; + setLoading(true); setError(''); try {