feat(conciliacion): filtros de columna con sugerencias autocomplete

- Agregar prop suggestions a FilterHeader con dropdown de opciones
- Calcular valores unicos de rfc/nombre emisor/receptor desde los
  CFDIs cargados en memoria
- Filtrar sugerencias segun texto escrito (max 8 resultados)
- Al seleccionar una sugerencia se aplica el filtro y cierra el popover
This commit is contained in:
Horux Dev
2026-05-24 00:55:08 +00:00
parent a30060050b
commit 918d84f2d2

View File

@@ -36,6 +36,7 @@ function FilterHeader({
setFilters, setFilters,
openFilter, openFilter,
setOpenFilter, setOpenFilter,
suggestions,
}: { }: {
label: string; label: string;
filterKey: string; filterKey: string;
@@ -43,6 +44,7 @@ function FilterHeader({
setFilters: React.Dispatch<React.SetStateAction<{ rfcEmisor: string; nombreEmisor: string; rfcReceptor: string; nombreReceptor: string }>>; setFilters: React.Dispatch<React.SetStateAction<{ rfcEmisor: string; nombreEmisor: string; rfcReceptor: string; nombreReceptor: string }>>;
openFilter: string | null; openFilter: string | null;
setOpenFilter: (v: string | null) => void; setOpenFilter: (v: string | null) => void;
suggestions: string[];
}) { }) {
const rawValue = (filters as any)[filterKey] || ''; const rawValue = (filters as any)[filterKey] || '';
const [localValue, setLocalValue] = useState(rawValue); const [localValue, setLocalValue] = useState(rawValue);
@@ -61,6 +63,10 @@ function FilterHeader({
}, [debouncedValue]); }, [debouncedValue]);
const hasFilter = !!rawValue; const hasFilter = !!rawValue;
const filteredSuggestions = localValue.length >= 1
? suggestions.filter((s) => s.toLowerCase().includes(localValue.toLowerCase())).slice(0, 8)
: suggestions.slice(0, 8);
return ( return (
<div className="flex items-center justify-center gap-1"> <div className="flex items-center justify-center gap-1">
{label} {label}
@@ -70,10 +76,10 @@ function FilterHeader({
<Filter className="h-3.5 w-3.5" /> <Filter className="h-3.5 w-3.5" />
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-64" align="start"> <PopoverContent className="w-72" align="start">
<div className="space-y-3"> <div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por {label}</h4> <h4 className="font-medium text-sm">Filtrar por {label}</h4>
<div> <div className="relative">
<Label className="text-xs">Contiene</Label> <Label className="text-xs">Contiene</Label>
<Input <Input
placeholder={`Buscar ${label.toLowerCase()}...`} placeholder={`Buscar ${label.toLowerCase()}...`}
@@ -82,6 +88,23 @@ function FilterHeader({
onChange={(e) => setLocalValue(e.target.value)} onChange={(e) => setLocalValue(e.target.value)}
autoFocus autoFocus
/> />
{filteredSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-background border rounded-md shadow-lg max-h-40 overflow-y-auto z-50">
{filteredSuggestions.map((s, idx) => (
<button
key={idx}
className="w-full text-left px-3 py-1.5 text-sm hover:bg-muted truncate"
onClick={() => {
setLocalValue(s);
setFilters((prev: any) => ({ ...prev, [filterKey]: s }));
setOpenFilter(null);
}}
>
{s}
</button>
))}
</div>
)}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" onClick={() => setOpenFilter(null)}>Aplicar</Button> <Button size="sm" onClick={() => setOpenFilter(null)}>Aplicar</Button>
@@ -144,6 +167,15 @@ export default function ConciliacionPage() {
const pendientes = cfdis?.filter((c) => c.conciliado !== 'true') || []; const pendientes = cfdis?.filter((c) => c.conciliado !== 'true') || [];
const conciliadas = cfdis?.filter((c) => c.conciliado === 'true') || []; const conciliadas = cfdis?.filter((c) => c.conciliado === 'true') || [];
// Sugerencias únicas para filtros de columna (de todos los CFDIs cargados)
const allCfdis = cfdis || [];
const uniqueSuggestions = {
rfcEmisor: [...new Set(allCfdis.map((c: any) => c.rfcEmisor).filter(Boolean))].sort(),
nombreEmisor: [...new Set(allCfdis.map((c: any) => c.nombreEmisor).filter(Boolean))].sort(),
rfcReceptor: [...new Set(allCfdis.map((c: any) => c.rfcReceptor).filter(Boolean))].sort(),
nombreReceptor: [...new Set(allCfdis.map((c: any) => c.nombreReceptor).filter(Boolean))].sort(),
};
// Score cards — tipo P usa monto_pago_mxn, otros usan total_mxn // Score cards — tipo P usa monto_pago_mxn, otros usan total_mxn
const getMonto = (c: any) => Number(c.montoMxn || c.totalMxn || 0); const getMonto = (c: any) => Number(c.montoMxn || c.totalMxn || 0);
const montoConciliado = conciliadas.reduce((s, c) => s + getMonto(c), 0); const montoConciliado = conciliadas.reduce((s, c) => s + getMonto(c), 0);
@@ -362,10 +394,10 @@ export default function ConciliacionPage() {
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortPendientes(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}> <th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortPendientes(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Fecha <ArrowUpDown className="h-3 w-3" /></span> <span className="flex items-center justify-center gap-1">Fecha <ArrowUpDown className="h-3 w-3" /></span>
</th> </th>
<th className="pb-3 font-medium"><FilterHeader label="RFC Emisor" filterKey="rfcEmisor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} /></th> <th className="pb-3 font-medium"><FilterHeader label="RFC Emisor" filterKey="rfcEmisor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.rfcEmisor} /></th>
<th className="pb-3 font-medium"><FilterHeader label="Nombre Emisor" filterKey="nombreEmisor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} /></th> <th className="pb-3 font-medium"><FilterHeader label="Nombre Emisor" filterKey="nombreEmisor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.nombreEmisor} /></th>
<th className="pb-3 font-medium"><FilterHeader label="RFC Receptor" filterKey="rfcReceptor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} /></th> <th className="pb-3 font-medium"><FilterHeader label="RFC Receptor" filterKey="rfcReceptor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.rfcReceptor} /></th>
<th className="pb-3 font-medium"><FilterHeader label="Nombre Receptor" filterKey="nombreReceptor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} /></th> <th className="pb-3 font-medium"><FilterHeader label="Nombre Receptor" filterKey="nombreReceptor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.nombreReceptor} /></th>
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortPendientes(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}> <th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortPendientes(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span> <span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span>
</th> </th>
@@ -481,8 +513,8 @@ export default function ConciliacionPage() {
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortConciliadas(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}> <th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortConciliadas(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Fecha Emisión <ArrowUpDown className="h-3 w-3" /></span> <span className="flex items-center justify-center gap-1">Fecha Emisión <ArrowUpDown className="h-3 w-3" /></span>
</th> </th>
<th className="pb-3 font-medium"><FilterHeader label="RFC Emisor" filterKey="rfcEmisor" filters={filtersConciliadas} setFilters={setFiltersConciliadas} openFilter={openFilterConciliadas} setOpenFilter={setOpenFilterConciliadas} /></th> <th className="pb-3 font-medium"><FilterHeader label="RFC Emisor" filterKey="rfcEmisor" filters={filtersConciliadas} setFilters={setFiltersConciliadas} openFilter={openFilterConciliadas} setOpenFilter={setOpenFilterConciliadas} suggestions={uniqueSuggestions.rfcEmisor} /></th>
<th className="pb-3 font-medium"><FilterHeader label="Nombre Emisor" filterKey="nombreEmisor" filters={filtersConciliadas} setFilters={setFiltersConciliadas} openFilter={openFilterConciliadas} setOpenFilter={setOpenFilterConciliadas} /></th> <th className="pb-3 font-medium"><FilterHeader label="Nombre Emisor" filterKey="nombreEmisor" filters={filtersConciliadas} setFilters={setFiltersConciliadas} openFilter={openFilterConciliadas} setOpenFilter={setOpenFilterConciliadas} suggestions={uniqueSuggestions.nombreEmisor} /></th>
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortConciliadas(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}> <th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortConciliadas(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span> <span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span>
</th> </th>