- 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
138 lines
5.4 KiB
Markdown
138 lines
5.4 KiB
Markdown
# 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 20–30 MB.
|
||
- El endpoint requiere autenticación y tenant middleware (como todo el módulo CFDI).
|