Files
HoruxDespachosNuevo/docs/sessions/2026-05-24-cfdi-bulk-xml-download-refactor.md
Horux Dev e35eae2a72 refactor(cfdi): descarga masiva de XMLs por filtros en lugar de checkboxes
- 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
2026-05-24 21:40:08 +00:00

138 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `whereClause` builder de `getCfdis()` 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/zip` con `Content-Disposition: attachment; filename="cfdis-{timestamp}.zip"`.
- Si ningún CFDI tiene XML (campo `xml` null o vacío), retorna 404.
### 3.2 Frontend
#### API client: `apps/web/lib/api/cfdi.ts`
```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>`
- `downloadingXmls` se 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:**
1. Verifica si `data.total > 1000`.
2. Si sí → `window.confirm()` con mensaje de advertencia.
3. Si el usuario cancela → no hace nada.
4. Si el usuario acepta (o total ≤ 1000) → construye objeto `CfdiFilters` con los filtros actuales (`tipo`, `tipoComprobante`, `estado`, `fechaInicio`, `fechaFin`, `rfc`, `emisor`, `receptor`, `search`, `contribuyenteId`).
5. Llama `downloadXmlsZip(filters)`.
6. Crea blob URL y dispara descarga con nombre `cfdis-xml-{timestamp}.zip`.
7. 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
```bash
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 un `COUNT(*` 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 2030 MB.
- El endpoint requiere autenticación y tenant middleware (como todo el módulo CFDI).