fix(cfdi): filtros de Conceptos aplican solo al dar Aplicar/Limpiar

This commit is contained in:
Horux Dev
2026-06-30 21:09:51 +00:00
parent 57c4908e68
commit 1b202bc542
2 changed files with 98 additions and 38 deletions

View File

@@ -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
<Popover open={conceptosOpenFilter === 'uuid'} onOpenChange={(open) => setConceptosOpenFilter(open ? 'uuid' : null)}>
<PopoverTrigger asChild>
<button className={`p-1 rounded hover:bg-muted ${conceptosFilters.uuidLike ? 'text-primary' : ''}`}>
<button className={`p-1 rounded hover:bg-muted ${appliedConceptosFilters.uuidLike ? 'text-primary' : ''}`}>
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-64" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por UUID</h4>
<Input className="h-8 text-sm font-mono" placeholder="Fragmento del UUID..." value={conceptosFilters.uuidLike} onChange={(e) => setConceptosFilters({ ...conceptosFilters, uuidLike: e.target.value })} />
<Input className="h-8 text-sm font-mono" placeholder="Fragmento del UUID..." value={conceptosDraftFilters.uuidLike} onChange={(e) => setConceptosDraftFilters({ ...conceptosDraftFilters, uuidLike: e.target.value })} />
<div className="flex gap-2">
<Button size="sm" className="flex-1" onClick={() => { setFilters({ ...filters, page: 1 }); setConceptosOpenFilter(null); }}>Aplicar</Button>
{conceptosFilters.uuidLike && <Button size="sm" variant="outline" onClick={() => { setConceptosFilters({ ...conceptosFilters, uuidLike: '' }); setFilters({ ...filters, page: 1 }); }}>Limpiar</Button>}
<Button size="sm" className="flex-1" onClick={applyConceptosFilters}>Aplicar</Button>
{appliedConceptosFilters.uuidLike && <Button size="sm" variant="outline" onClick={() => clearConceptosFilter('uuidLike')}>Limpiar</Button>}
</div>
</div>
</PopoverContent>
@@ -1620,17 +1644,17 @@ export default function CfdiPage() {
Clave
<Popover open={conceptosOpenFilter === 'clave'} onOpenChange={(open) => setConceptosOpenFilter(open ? 'clave' : null)}>
<PopoverTrigger asChild>
<button className={`p-1 rounded hover:bg-muted ${conceptosFilters.claveProdServ ? 'text-primary' : ''}`}>
<button className={`p-1 rounded hover:bg-muted ${appliedConceptosFilters.claveProdServ ? 'text-primary' : ''}`}>
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-64" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por Clave SAT</h4>
<Input className="h-8 text-sm font-mono" placeholder="Ej: 81112502" value={conceptosFilters.claveProdServ} onChange={(e) => setConceptosFilters({ ...conceptosFilters, claveProdServ: e.target.value })} />
<Input className="h-8 text-sm font-mono" placeholder="Ej: 81112502" value={conceptosDraftFilters.claveProdServ} onChange={(e) => setConceptosDraftFilters({ ...conceptosDraftFilters, claveProdServ: e.target.value })} />
<div className="flex gap-2">
<Button size="sm" className="flex-1" onClick={() => { setFilters({ ...filters, page: 1 }); setConceptosOpenFilter(null); }}>Aplicar</Button>
{conceptosFilters.claveProdServ && <Button size="sm" variant="outline" onClick={() => { setConceptosFilters({ ...conceptosFilters, claveProdServ: '' }); setFilters({ ...filters, page: 1 }); }}>Limpiar</Button>}
<Button size="sm" className="flex-1" onClick={applyConceptosFilters}>Aplicar</Button>
{appliedConceptosFilters.claveProdServ && <Button size="sm" variant="outline" onClick={() => clearConceptosFilter('claveProdServ')}>Limpiar</Button>}
</div>
</div>
</PopoverContent>
@@ -1642,17 +1666,17 @@ export default function CfdiPage() {
Descripción
<Popover open={conceptosOpenFilter === 'descripcion'} onOpenChange={(open) => setConceptosOpenFilter(open ? 'descripcion' : null)}>
<PopoverTrigger asChild>
<button className={`p-1 rounded hover:bg-muted ${conceptosFilters.descripcionConcepto ? 'text-primary' : ''}`}>
<button className={`p-1 rounded hover:bg-muted ${appliedConceptosFilters.descripcionConcepto ? 'text-primary' : ''}`}>
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-72" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por descripción</h4>
<Input className="h-8 text-sm" placeholder="Texto contenido en la descripción..." value={conceptosFilters.descripcionConcepto} onChange={(e) => setConceptosFilters({ ...conceptosFilters, descripcionConcepto: e.target.value })} />
<Input className="h-8 text-sm" placeholder="Texto contenido en la descripción..." value={conceptosDraftFilters.descripcionConcepto} onChange={(e) => setConceptosDraftFilters({ ...conceptosDraftFilters, descripcionConcepto: e.target.value })} />
<div className="flex gap-2">
<Button size="sm" className="flex-1" onClick={() => { setFilters({ ...filters, page: 1 }); setConceptosOpenFilter(null); }}>Aplicar</Button>
{conceptosFilters.descripcionConcepto && <Button size="sm" variant="outline" onClick={() => { setConceptosFilters({ ...conceptosFilters, descripcionConcepto: '' }); setFilters({ ...filters, page: 1 }); }}>Limpiar</Button>}
<Button size="sm" className="flex-1" onClick={applyConceptosFilters}>Aplicar</Button>
{appliedConceptosFilters.descripcionConcepto && <Button size="sm" variant="outline" onClick={() => clearConceptosFilter('descripcionConcepto')}>Limpiar</Button>}
</div>
</div>
</PopoverContent>
@@ -1664,17 +1688,17 @@ export default function CfdiPage() {
No. Identificación
<Popover open={conceptosOpenFilter === 'noIdentificacion'} onOpenChange={(open) => setConceptosOpenFilter(open ? 'noIdentificacion' : null)}>
<PopoverTrigger asChild>
<button className={`p-1 rounded hover:bg-muted ${conceptosFilters.noIdentificacion ? 'text-primary' : ''}`}>
<button className={`p-1 rounded hover:bg-muted ${appliedConceptosFilters.noIdentificacion ? 'text-primary' : ''}`}>
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-64" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por No. Identificación</h4>
<Input className="h-8 text-sm font-mono" placeholder="Ej: PROD-001" value={conceptosFilters.noIdentificacion} onChange={(e) => setConceptosFilters({ ...conceptosFilters, noIdentificacion: e.target.value })} />
<Input className="h-8 text-sm font-mono" placeholder="Ej: PROD-001" value={conceptosDraftFilters.noIdentificacion} onChange={(e) => setConceptosDraftFilters({ ...conceptosDraftFilters, noIdentificacion: e.target.value })} />
<div className="flex gap-2">
<Button size="sm" className="flex-1" onClick={() => { setFilters({ ...filters, page: 1 }); setConceptosOpenFilter(null); }}>Aplicar</Button>
{conceptosFilters.noIdentificacion && <Button size="sm" variant="outline" onClick={() => { setConceptosFilters({ ...conceptosFilters, noIdentificacion: '' }); setFilters({ ...filters, page: 1 }); }}>Limpiar</Button>}
<Button size="sm" className="flex-1" onClick={applyConceptosFilters}>Aplicar</Button>
{appliedConceptosFilters.noIdentificacion && <Button size="sm" variant="outline" onClick={() => clearConceptosFilter('noIdentificacion')}>Limpiar</Button>}
</div>
</div>
</PopoverContent>
@@ -1694,8 +1718,8 @@ export default function CfdiPage() {
title="Ordenar por importe"
>
Importe
{conceptosFilters.orderBy === 'importe' ? (
<span className="text-primary">{conceptosFilters.orderDir === 'asc' ? '▲' : '▼'}</span>
{conceptosSort.orderBy === 'importe' ? (
<span className="text-primary">{conceptosSort.orderDir === 'asc' ? '▲' : '▼'}</span>
) : (
<span className="text-muted-foreground/40"></span>
)}

View File

@@ -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