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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user