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,
|
||||
openFilter,
|
||||
setOpenFilter,
|
||||
suggestions,
|
||||
}: {
|
||||
label: string;
|
||||
filterKey: string;
|
||||
@@ -43,6 +44,7 @@ function FilterHeader({
|
||||
setFilters: React.Dispatch<React.SetStateAction<{ rfcEmisor: string; nombreEmisor: string; rfcReceptor: string; nombreReceptor: string }>>;
|
||||
openFilter: string | null;
|
||||
setOpenFilter: (v: string | null) => void;
|
||||
suggestions: string[];
|
||||
}) {
|
||||
const rawValue = (filters as any)[filterKey] || '';
|
||||
const [localValue, setLocalValue] = useState(rawValue);
|
||||
@@ -61,6 +63,10 @@ function FilterHeader({
|
||||
}, [debouncedValue]);
|
||||
|
||||
const hasFilter = !!rawValue;
|
||||
const filteredSuggestions = localValue.length >= 1
|
||||
? suggestions.filter((s) => s.toLowerCase().includes(localValue.toLowerCase())).slice(0, 8)
|
||||
: suggestions.slice(0, 8);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{label}
|
||||
@@ -70,10 +76,10 @@ function FilterHeader({
|
||||
<Filter className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64" align="start">
|
||||
<PopoverContent className="w-72" align="start">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-sm">Filtrar por {label}</h4>
|
||||
<div>
|
||||
<div className="relative">
|
||||
<Label className="text-xs">Contiene</Label>
|
||||
<Input
|
||||
placeholder={`Buscar ${label.toLowerCase()}...`}
|
||||
@@ -82,6 +88,23 @@ function FilterHeader({
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
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 className="flex gap-2">
|
||||
<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 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
|
||||
const getMonto = (c: any) => Number(c.montoMxn || c.totalMxn || 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' })}>
|
||||
<span className="flex items-center justify-center gap-1">Fecha <ArrowUpDown className="h-3 w-3" /></span>
|
||||
</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="Nombre Emisor" filterKey="nombreEmisor" 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} /></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="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} suggestions={uniqueSuggestions.nombreEmisor} /></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} 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' })}>
|
||||
<span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span>
|
||||
</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' })}>
|
||||
<span className="flex items-center justify-center gap-1">Fecha Emisión <ArrowUpDown className="h-3 w-3" /></span>
|
||||
</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="Nombre Emisor" filterKey="nombreEmisor" 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} 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' })}>
|
||||
<span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span>
|
||||
</th>
|
||||
|
||||
Reference in New Issue
Block a user