- Backend: POST /cfdi/download-xmls acepta CfdiFilters, usa getXmlsByFilters con LIMIT 1000 - Frontend: eliminados checkboxes y estado selectedIds; botón Descargar XMLs usa filtros activos - Si >1000 resultados, muestra confirm() de advertencia pero permite proceder - Agregada documentación técnica y changelog
5.4 KiB
Refactor: Descarga masiva de XMLs por filtros
Fecha: 2026-05-24
Feature: CFDI — descarga masiva de XMLs refactorizada de selección por checkbox a descarga por filtros
1. Requerimiento
Cambiar el mecanismo de descarga masiva de XMLs en la página /cfdi:
- Antes: El usuario debía seleccionar CFDIs individuales mediante checkboxes por fila y en el header de la tabla. Solo se descargaban los seleccionados.
- Después: Un único botón "Descargar XMLs" descarga todos los CFDIs que coincidan con los filtros activos, sin necesidad de selección manual.
- Límite: Si los filtros aplicados devuelven más de 1,000 CFDIs, se muestra una advertencia (
confirm) informando que solo se descargarán los primeros 1,000, pero el usuario puede proceder.
2. Decisiones de diseño
2.1 Sin checkboxes
Eliminar toda la UI de selección (header con checkbox maestro, checkboxes por fila, barra de "X seleccionados" y botón "Limpiar"). Esto simplifica la UX y reduce el estado del componente.
2.2 Filtros como fuente de verdad
El backend ya soportaba descarga por IDs (downloadXmlsZip(ids: number[])). Se reemplazó por descarga por filtros (downloadXmlsZip(filters: CfdiFilters)). Esto aprovecha el mismo whereClause que usa getCfdis() para listar, garantizando consistencia entre lo que se ve y lo que se descarga.
2.3 Warning, no error, al superar 1,000
En lugar de bloquear la descarga cuando hay >1,000 resultados, se muestra un window.confirm() con el mensaje:
"Solo se descargarán los primeros 1,000 XMLs. ¿Continuar?"
Si el usuario acepta, el backend ejecuta la query con LIMIT 1000 y genera el ZIP.
3. Cambios implementados
3.1 Backend
Service: apps/api/src/services/cfdi.service.ts
Función existente reutilizada: getXmlsByFilters(pool, filters, limit)
- Reusa el mismo
whereClausebuilder degetCfdis()para garantizar consistencia. - SELECT:
id, uuid, xml_original as xml - LIMIT parametrizado (default 1000).
Controller: apps/api/src/controllers/cfdi.controller.ts
Endpoint: POST /cfdi/download-xmls
- Body:
{ filters: CfdiFilters } - Llama
getXmlsByFilters(req.tenantPool, filters, 1000). - Genera ZIP con
adm-zip. - Retorna
application/zipconContent-Disposition: attachment; filename="cfdis-{timestamp}.zip". - Si ningún CFDI tiene XML (campo
xmlnull o vacío), retorna 404.
3.2 Frontend
API client: apps/web/lib/api/cfdi.ts
// Antes
export async function downloadXmlsZip(ids: number[]): Promise<Blob>
// Después
export async function downloadXmlsZip(filters: CfdiFilters): Promise<Blob>
La función ahora envía filters en el body en lugar de un array de ids.
Página: apps/web/app/(dashboard)/cfdi/page.tsx
Estados eliminados:
selectedIds: Set<number>downloadingXmlsse mantiene solo para indicador de loading en el botón.
UI eliminada:
- Checkbox en header de tabla (
<th className="w-8">). - Checkbox por fila en cada
<tr>. - Barra de "X seleccionados" con botones "Descargar XMLs" y "Limpiar" condicionales.
UI nueva/modificada:
- Botón "Descargar XMLs" ubicado permanentemente en la barra de acciones del
CardHeader. - Deshabilitado cuando:
downloadingXmls === true(ya hay una descarga en curso)!data?.total(no hay resultados con los filtros actuales)
Flujo del botón:
- Verifica si
data.total > 1000. - Si sí →
window.confirm()con mensaje de advertencia. - Si el usuario cancela → no hace nada.
- Si el usuario acepta (o total ≤ 1000) → construye objeto
CfdiFilterscon los filtros actuales (tipo,tipoComprobante,estado,fechaInicio,fechaFin,rfc,emisor,receptor,search,contribuyenteId). - Llama
downloadXmlsZip(filters). - Crea blob URL y dispara descarga con nombre
cfdis-xml-{timestamp}.zip. - Limpia URL object.
4. Estructura del ZIP
Cada archivo dentro del ZIP se nombra {uuid}.xml o {id}.xml si el UUID es null.
El contenido es el XML original tal como se almacenó en cfdis.xml_original.
5. Archivos modificados
| Archivo | Cambio |
|---|---|
apps/api/src/services/cfdi.service.ts |
getXmlsByFilters() — reusa whereClause de getCfdis, limit 1000 |
apps/api/src/controllers/cfdi.controller.ts |
downloadXmlsZip() — acepta filtros, genera ZIP con adm-zip |
apps/api/src/routes/cfdi.routes.ts |
Ruta POST /download-xmls registrada |
apps/web/lib/api/cfdi.ts |
downloadXmlsZip() ahora recibe CfdiFilters |
apps/web/app/(dashboard)/cfdi/page.tsx |
Eliminados checkboxes y selectedIds; botón de descarga usa filtros actuales |
6. Deploy
cd /root/HoruxDespachosNuevo
# Builds
npm run build --filter=@horux/api
npm run build --filter=@horux/web
# PM2 reload
pm2 reload horux-api
pm2 reload horux-web
Estado: ✅ Exitoso. Builds sin errores. Procesos reiniciados.
7. Notas técnicas
- El backend no envía el mensaje de advertencia como respuesta; el frontend lo calcula comparando
data.total(del listado paginado) contra 1,000. Esto es una aproximación eficiente porque evita unCOUNT(*adicional. - Si el usuario aplica filtros muy amplios (ej. todo un mes sin restricciones), el ZIP puede contener hasta 1,000 archivos. Cada XML típicamente pesa entre 3 KB y 50 KB, por lo que el ZIP rara vez superará los 20–30 MB.
- El endpoint requiere autenticación y tenant middleware (como todo el módulo CFDI).