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