fix(cfdi): filtros de Conceptos aplican solo al dar Aplicar/Limpiar
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user