Add dropdown filters to Missing Data view with unique values

This commit is contained in:
Christian Vidal Wolf
2026-04-12 18:36:19 +02:00
parent fd5d7458b9
commit dce4588d0c
+74 -18
View File
@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import { ExcelRow, COLUMNS } from '../types';
import { Search, ChevronDown, ChevronUp, X, Edit2, Save, Filter, XCircle } from 'lucide-react';
import { Search, ChevronDown, ChevronUp, X, Edit2, Save, Filter, XCircle, ChevronLeft, ChevronRight } from 'lucide-react';
import { cn } from '../lib/utils';
interface MissingDataViewProps {
@@ -54,6 +54,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
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 pageSize = 100;
const launchDateCol = useMemo(() =>
@@ -64,6 +65,33 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
const launchHeader = launchDateCol >= 0 ? headers[launchDateCol] : 'Launch Date';
const readyHeader = readyToOrderCol >= 0 ? headers[readyToOrderCol] : 'Ready to Order';
const columnUniqueValues = useMemo(() => {
const cols = columns.map(c => c.col);
const result: Record<number, Set<string>> = {};
cols.forEach(col => result[col] = new Set());
data.forEach(row => {
cols.forEach(col => {
let val: any = row[col];
if (col === launchDateCol || col === readyToOrderCol) {
val = formatDateValue(val) || String(val ?? '');
} else {
val = String(val ?? '');
}
if (val) result[col].add(val);
});
});
return result;
}, [data, launchDateCol, readyToOrderCol]);
const getDisplayValues = (col: number) => {
const values = columnUniqueValues[col];
if (!values) return [];
const searchVal = filterSearch[col]?.toLowerCase() || '';
const arr = Array.from(values).sort();
return searchVal ? arr.filter(v => v.toLowerCase().includes(searchVal)) : arr;
};
const filteredData = useMemo(() => {
let result = data.map((row, index) => ({ row, index }));
@@ -214,7 +242,11 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
<table className="w-full text-left text-xs">
<thead className="bg-slate-900/80 text-slate-400 sticky top-0 z-10">
<tr>
{columns.map(({ col, label, width }) => (
{columns.map(({ col, label, width }) => {
const isOpen = showFilterMenu === col;
const hasFilter = columnFilters[col];
const displayValues = getDisplayValues(col);
return (
<th
key={col}
style={{ width, minWidth: width }}
@@ -230,32 +262,56 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
)}
</div>
<div className="mt-1 relative">
<button
onClick={(e) => { e.stopPropagation(); setShowFilterMenu(isOpen ? 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"
)}
>
<span className="truncate">{hasFilter || 'Filter...'}</span>
<ChevronDown className={cn("w-3 h-3 transition-transform", isOpen && "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="Filter..."
value={columnFilters[col] || ''}
onChange={(e) => {
setColumnFilters(prev => ({ ...prev, [col]: e.target.value }));
setPage(1);
}}
placeholder="Search..."
value={filterSearch[col] || ''}
onChange={(e) => setFilterSearch(prev => ({ ...prev, [col]: e.target.value }))}
onClick={(e) => e.stopPropagation()}
className="w-full px-1.5 py-0.5 bg-slate-800 border border-slate-600 rounded text-[10px] text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500"
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"
/>
{columnFilters[col] && (
</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="absolute right-1 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white"
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"
>
<XCircle className="w-3 h-3" />
(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>
</th>
))}
);})}
<th className="px-2 py-2 font-medium text-right" style={{ width: 60 }}></th>
</tr>
</thead>