mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 12:25:23 +02:00
Add multi-select filters to Missing Data view using ColumnFilterPopover
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { ExcelRow, COLUMNS } from '../types';
|
||||
import { Search, ChevronDown, ChevronUp, X, Edit2, Save, Filter, XCircle, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Search, ChevronDown, ChevronUp, X, Edit2, Save } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||
|
||||
interface MissingDataViewProps {
|
||||
data: ExcelRow[];
|
||||
@@ -52,9 +53,8 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
||||
const [sortDesc, setSortDesc] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [editing, setEditing] = useState<EditingState | null>(null);
|
||||
const [columnFilters, setColumnFilters] = useState<Record<number, string>>({});
|
||||
const [showFilterMenu, setShowFilterMenu] = useState<number | null>(null);
|
||||
const [filterSearch, setFilterSearch] = useState<Record<number, string>>({});
|
||||
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
|
||||
const [openFilter, setOpenFilter] = useState<number | null>(null);
|
||||
const pageSize = 100;
|
||||
|
||||
const launchDateCol = useMemo(() =>
|
||||
@@ -92,12 +92,10 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
||||
return result;
|
||||
}, [data, columns, launchDateCol, readyToOrderCol]);
|
||||
|
||||
const getDisplayValues = (col: number) => {
|
||||
const getUniqueValues = (col: number): string[] => {
|
||||
const values = columnUniqueValues[col];
|
||||
if (!values) return [];
|
||||
const searchVal = filterSearch[col]?.toLowerCase() || '';
|
||||
const arr = Array.from(values).sort();
|
||||
return searchVal ? arr.filter((v: string) => v.toLowerCase().includes(searchVal)) : arr;
|
||||
return Array.from(values).sort() as string[];
|
||||
};
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
@@ -118,16 +116,15 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
||||
);
|
||||
}
|
||||
|
||||
(Object.entries(columnFilters) as [string, string][]).forEach(([colIdx, filterValue]) => {
|
||||
if (!filterValue) return;
|
||||
(Object.entries(columnFilters) as [string, string[]][]).forEach(([colIdx, filterValues]) => {
|
||||
if (!filterValues || filterValues.length === 0) return;
|
||||
const colIdxNum = parseInt(colIdx);
|
||||
const filterLower = filterValue.toLowerCase();
|
||||
result = result.filter(r => {
|
||||
const val: any = r.row[colIdxNum];
|
||||
const displayVal = colIdxNum === launchDateCol || colIdxNum === readyToOrderCol
|
||||
? formatDateValue(val) || String(val ?? '')
|
||||
: String(val ?? '');
|
||||
return displayVal.toLowerCase().includes(filterLower);
|
||||
return filterValues.includes(displayVal);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,9 +232,9 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
||||
<thead className="bg-slate-900/80 text-slate-400 sticky top-0 z-10">
|
||||
<tr>
|
||||
{columns.map(({ col, label, width }) => {
|
||||
const isOpen = showFilterMenu === col;
|
||||
const hasFilter = columnFilters[col];
|
||||
const displayValues = getDisplayValues(col);
|
||||
const selectedFilters = columnFilters[col] || [];
|
||||
const allValues = getUniqueValues(col);
|
||||
const filterCount = selectedFilters.length;
|
||||
return (
|
||||
<th
|
||||
key={col}
|
||||
@@ -255,51 +252,30 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
||||
</div>
|
||||
<div className="mt-1 relative">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowFilterMenu(isOpen ? null : col); }}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between px-1.5 py-0.5 bg-slate-800 border rounded text-[10px] transition-colors",
|
||||
hasFilter ? "border-indigo-500 text-white" : "border-slate-600 text-slate-400 hover:border-slate-500"
|
||||
filterCount > 0 ? "border-indigo-500 text-white" : "border-slate-600 text-slate-400 hover:border-slate-500"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{hasFilter || 'Filter...'}</span>
|
||||
<ChevronDown className={cn("w-3 h-3 transition-transform", isOpen && "rotate-180")} />
|
||||
<span className="truncate">{filterCount > 0 ? `${filterCount} selected` : 'Filter...'}</span>
|
||||
<ChevronDown className={cn("w-3 h-3 transition-transform", openFilter === col && "rotate-180")} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-slate-800 border border-slate-600 rounded-md shadow-lg z-50 max-h-64 flex flex-col">
|
||||
<div className="p-1.5 border-b border-slate-700">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={filterSearch[col] || ''}
|
||||
onChange={(e) => setFilterSearch(prev => ({ ...prev, [col]: e.target.value }))}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-full px-2 py-1 bg-slate-900 border border-slate-700 rounded text-xs text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setColumnFilters(prev => { const n = { ...prev }; delete n[col]; return n; }); setPage(1); }}
|
||||
className="w-full px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-700 hover:text-white"
|
||||
>
|
||||
(All)
|
||||
</button>
|
||||
{displayValues.map((val) => (
|
||||
<button
|
||||
key={val}
|
||||
onClick={(e) => { e.stopPropagation(); setColumnFilters(prev => ({ ...prev, [col]: val })); setShowFilterMenu(null); setPage(1); }}
|
||||
className={cn(
|
||||
"w-full px-2 py-1.5 text-left text-xs hover:bg-slate-700 hover:text-white",
|
||||
columnFilters[col] === val ? "text-indigo-400 bg-indigo-500/10" : "text-slate-300"
|
||||
)}
|
||||
>
|
||||
{val}
|
||||
</button>
|
||||
))}
|
||||
{displayValues.length === 0 && (
|
||||
<div className="px-2 py-2 text-xs text-slate-500 text-center">No values</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{openFilter === col && (
|
||||
<ColumnFilterPopover
|
||||
uniqueValues={allValues}
|
||||
selectedValues={selectedFilters}
|
||||
onToggle={(val) => setColumnFilters(prev => {
|
||||
const current = prev[col] || [];
|
||||
if (current.includes(val)) {
|
||||
return { ...prev, [col]: current.filter(v => v !== val) };
|
||||
}
|
||||
return { ...prev, [col]: [...current, val] };
|
||||
})}
|
||||
onSelectAll={(vals) => setColumnFilters(prev => ({ ...prev, [col]: vals }))}
|
||||
onClear={() => { setColumnFilters(prev => { const n = { ...prev }; delete n[col]; return n; }); }}
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
|
||||
Reference in New Issue
Block a user