mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 12:25:23 +02:00
Add dropdown filters to Missing Data view with unique values
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
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 } from 'lucide-react';
|
import { Search, ChevronDown, ChevronUp, X, Edit2, Save, Filter, XCircle, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface MissingDataViewProps {
|
interface MissingDataViewProps {
|
||||||
@@ -54,6 +54,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
|||||||
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 [showFilterMenu, setShowFilterMenu] = useState<number | null>(null);
|
||||||
|
const [filterSearch, setFilterSearch] = useState<Record<number, string>>({});
|
||||||
const pageSize = 100;
|
const pageSize = 100;
|
||||||
|
|
||||||
const launchDateCol = useMemo(() =>
|
const launchDateCol = useMemo(() =>
|
||||||
@@ -64,6 +65,33 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
|||||||
const launchHeader = launchDateCol >= 0 ? headers[launchDateCol] : 'Launch Date';
|
const launchHeader = launchDateCol >= 0 ? headers[launchDateCol] : 'Launch Date';
|
||||||
const readyHeader = readyToOrderCol >= 0 ? headers[readyToOrderCol] : 'Ready to Order';
|
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(() => {
|
const filteredData = useMemo(() => {
|
||||||
let result = data.map((row, index) => ({ row, index }));
|
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">
|
<table className="w-full text-left text-xs">
|
||||||
<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 hasFilter = columnFilters[col];
|
||||||
|
const displayValues = getDisplayValues(col);
|
||||||
|
return (
|
||||||
<th
|
<th
|
||||||
key={col}
|
key={col}
|
||||||
style={{ width, minWidth: width }}
|
style={{ width, minWidth: width }}
|
||||||
@@ -230,32 +262,56 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 relative">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Filter..."
|
placeholder="Search..."
|
||||||
value={columnFilters[col] || ''}
|
value={filterSearch[col] || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFilterSearch(prev => ({ ...prev, [col]: e.target.value }))}
|
||||||
setColumnFilters(prev => ({ ...prev, [col]: e.target.value }));
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
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
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => { e.stopPropagation(); setColumnFilters(prev => { const n = { ...prev }; delete n[col]; return n; }); setPage(1); }}
|
||||||
e.stopPropagation();
|
className="w-full px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-700 hover:text-white"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<XCircle className="w-3 h-3" />
|
(All)
|
||||||
</button>
|
</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>
|
||||||
))}
|
);})}
|
||||||
<th className="px-2 py-2 font-medium text-right" style={{ width: 60 }}></th>
|
<th className="px-2 py-2 font-medium text-right" style={{ width: 60 }}></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user