diff --git a/apps/web/app/(dashboard)/cfdi/page.tsx b/apps/web/app/(dashboard)/cfdi/page.tsx index 71579f8..003773d 100644 --- a/apps/web/app/(dashboard)/cfdi/page.tsx +++ b/apps/web/app/(dashboard)/cfdi/page.tsx @@ -322,41 +322,65 @@ export default function CfdiPage() { const [activeTab, setActiveTab] = useState<'cfdis' | 'conceptos'>('cfdis'); // Filtros locales de la pestaña Conceptos (no compartidos con CFDIs). // Popovers en headers UUID, Clave, Descripción + ordenamiento por importe. - const [conceptosFilters, setConceptosFilters] = useState<{ + // Valores que el usuario escribe en los inputs de los popovers + const [conceptosDraftFilters, setConceptosDraftFilters] = useState<{ uuidLike: string; claveProdServ: string; descripcionConcepto: string; noIdentificacion: string; + }>({ uuidLike: '', claveProdServ: '', descripcionConcepto: '', noIdentificacion: '' }); + // Filtros realmente aplicados a la query (solo cambian al dar Aplicar o Limpiar) + const [appliedConceptosFilters, setAppliedConceptosFilters] = useState<{ + uuidLike: string; + claveProdServ: string; + descripcionConcepto: string; + noIdentificacion: string; + }>({ uuidLike: '', claveProdServ: '', descripcionConcepto: '', noIdentificacion: '' }); + const [conceptosSort, setConceptosSort] = useState<{ orderBy?: 'fecha' | 'importe'; orderDir?: 'asc' | 'desc'; - }>({ uuidLike: '', claveProdServ: '', descripcionConcepto: '', noIdentificacion: '' }); + }>({}); const [conceptosOpenFilter, setConceptosOpenFilter] = useState<'uuid' | 'clave' | 'descripcion' | 'noIdentificacion' | null>(null); const conceptosQuery = useQuery({ - queryKey: ['cfdi-conceptos', filters, selectedContribuyenteId, conceptosFilters], + queryKey: ['cfdi-conceptos', filters, selectedContribuyenteId, appliedConceptosFilters, conceptosSort], queryFn: () => getConceptosList({ ...filters, contribuyenteId: selectedContribuyenteId || undefined, - uuidLike: conceptosFilters.uuidLike || undefined, - claveProdServ: conceptosFilters.claveProdServ || undefined, - descripcionConcepto: conceptosFilters.descripcionConcepto || undefined, - noIdentificacion: conceptosFilters.noIdentificacion || undefined, - orderBy: conceptosFilters.orderBy, - orderDir: conceptosFilters.orderDir, + uuidLike: appliedConceptosFilters.uuidLike || undefined, + claveProdServ: appliedConceptosFilters.claveProdServ || undefined, + descripcionConcepto: appliedConceptosFilters.descripcionConcepto || undefined, + noIdentificacion: appliedConceptosFilters.noIdentificacion || undefined, + orderBy: conceptosSort.orderBy, + orderDir: conceptosSort.orderDir, }), enabled: activeTab === 'conceptos', }); const toggleImporteSort = () => { - setConceptosFilters(prev => { - // null → asc → desc → null (o ciclo simple asc ↔ desc si prefieres) + setConceptosSort(prev => { const isImporte = prev.orderBy === 'importe'; - if (!isImporte) return { ...prev, orderBy: 'importe', orderDir: 'desc' }; - if (prev.orderDir === 'desc') return { ...prev, orderBy: 'importe', orderDir: 'asc' }; - return { ...prev, orderBy: undefined, orderDir: undefined }; + if (!isImporte) return { orderBy: 'importe', orderDir: 'desc' }; + if (prev.orderDir === 'desc') return { orderBy: 'importe', orderDir: 'asc' }; + return {}; }); setFilters(f => ({ ...f, page: 1 })); }; + + const applyConceptosFilters = () => { + setAppliedConceptosFilters({ ...conceptosDraftFilters }); + setFilters(f => ({ ...f, page: 1 })); + setConceptosOpenFilter(null); + }; + + const clearConceptosFilter = (field: keyof typeof conceptosDraftFilters) => { + const newDraft = { ...conceptosDraftFilters, [field]: '' }; + setConceptosDraftFilters(newDraft); + setAppliedConceptosFilters(newDraft); + setFilters(f => ({ ...f, page: 1 })); + setConceptosOpenFilter(null); + }; + const createCfdi = useCreateCfdi(); const deleteCfdi = useDeleteCfdi(); @@ -480,12 +504,12 @@ export default function CfdiPage() { const fullResponse = await getConceptosList({ ...filters, contribuyenteId: selectedContribuyenteId || undefined, - uuidLike: conceptosFilters.uuidLike || undefined, - claveProdServ: conceptosFilters.claveProdServ || undefined, - descripcionConcepto: conceptosFilters.descripcionConcepto || undefined, - noIdentificacion: conceptosFilters.noIdentificacion || undefined, - orderBy: conceptosFilters.orderBy, - orderDir: conceptosFilters.orderDir, + uuidLike: appliedConceptosFilters.uuidLike || undefined, + claveProdServ: appliedConceptosFilters.claveProdServ || undefined, + descripcionConcepto: appliedConceptosFilters.descripcionConcepto || undefined, + noIdentificacion: appliedConceptosFilters.noIdentificacion || undefined, + orderBy: conceptosSort.orderBy, + orderDir: conceptosSort.orderDir, page: 1, limit: EXPORT_MAX, }); @@ -1598,17 +1622,17 @@ export default function CfdiPage() { UUID setConceptosOpenFilter(open ? 'uuid' : null)}> -

Filtrar por UUID

- setConceptosFilters({ ...conceptosFilters, uuidLike: e.target.value })} /> + setConceptosDraftFilters({ ...conceptosDraftFilters, uuidLike: e.target.value })} />
- - {conceptosFilters.uuidLike && } + + {appliedConceptosFilters.uuidLike && }
@@ -1620,17 +1644,17 @@ export default function CfdiPage() { Clave setConceptosOpenFilter(open ? 'clave' : null)}> -

Filtrar por Clave SAT

- setConceptosFilters({ ...conceptosFilters, claveProdServ: e.target.value })} /> + setConceptosDraftFilters({ ...conceptosDraftFilters, claveProdServ: e.target.value })} />
- - {conceptosFilters.claveProdServ && } + + {appliedConceptosFilters.claveProdServ && }
@@ -1642,17 +1666,17 @@ export default function CfdiPage() { Descripción setConceptosOpenFilter(open ? 'descripcion' : null)}> -

Filtrar por descripción

- setConceptosFilters({ ...conceptosFilters, descripcionConcepto: e.target.value })} /> + setConceptosDraftFilters({ ...conceptosDraftFilters, descripcionConcepto: e.target.value })} />
- - {conceptosFilters.descripcionConcepto && } + + {appliedConceptosFilters.descripcionConcepto && }
@@ -1664,17 +1688,17 @@ export default function CfdiPage() { No. Identificación setConceptosOpenFilter(open ? 'noIdentificacion' : null)}> -

Filtrar por No. Identificación

- setConceptosFilters({ ...conceptosFilters, noIdentificacion: e.target.value })} /> + setConceptosDraftFilters({ ...conceptosDraftFilters, noIdentificacion: e.target.value })} />
- - {conceptosFilters.noIdentificacion && } + + {appliedConceptosFilters.noIdentificacion && }
@@ -1694,8 +1718,8 @@ export default function CfdiPage() { title="Ordenar por importe" > Importe - {conceptosFilters.orderBy === 'importe' ? ( - {conceptosFilters.orderDir === 'asc' ? '▲' : '▼'} + {conceptosSort.orderBy === 'importe' ? ( + {conceptosSort.orderDir === 'asc' ? '▲' : '▼'} ) : ( )} diff --git a/docs/CAMBIOS-2026-05-04.md b/docs/CAMBIOS-2026-05-04.md index 5798495..ae57e2e 100644 --- a/docs/CAMBIOS-2026-05-04.md +++ b/docs/CAMBIOS-2026-05-04.md @@ -795,6 +795,42 @@ pm2 reload horux-web --- +## 33. Filtros de Conceptos con Aplicar/Limpiar + +**Fecha:** 2026-06-30 + +### Cambio +En **CFDIs › Conceptos**, los filtros de encabezado de tabla (UUID, Clave SAT, Descripción, No. Identificación) ahora se aplican únicamente al hacer clic en **Aplicar**, en lugar de disparar la búsqueda en cada cambio de teclado. + +### Antes +Los inputs usaban `onChange` para actualizar `conceptosFilters`, lo que provocaba que `useQuery` re-lanzara la petición por cada letra escrita. + +### Ahora +- Se separó el estado en dos: + - `conceptosDraftFilters`: valores que el usuario escribe en los popovers. + - `appliedConceptosFilters`: valores realmente aplicados a la query. +- **Aplicar**: copia el draft al estado aplicado, resetea paginación y cierra el popover. +- **Limpiar**: borra ese campo, aplica el cambio y resetea paginación. +- El ordenamiento por importe sigue funcionando con un clic en el encabezado. + +### Archivos modificados + +| Archivo | Cambio | +|---|---| +| `apps/web/app/(dashboard)/cfdi/page.tsx` | Separación de draft/applied filters y nuevos handlers `applyConceptosFilters` / `clearConceptosFilter` | + +### Deploy + +```bash +cd /root/HoruxDespachosNuevo +pnpm --filter web build +pm2 reload horux-web +``` + +**Estado:** ✅ Exitoso + +--- + ## Deploy histórico ### Preparación