feat: standardize ColumnFilterPopover and add SELECT ALL to all views/tabs

This commit is contained in:
Christian Vidal Wolf
2026-04-08 16:56:43 +02:00
parent 786c66fd87
commit 89aa222470
6 changed files with 383 additions and 303 deletions
+112 -16
View File
@@ -1,8 +1,9 @@
import React, { useState, useMemo } from 'react';
import { ExcelRow, COLUMNS } from '../types';
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers, Link2 } from 'lucide-react';
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers, Link2, Search, Filter, X as XIcon } from 'lucide-react';
import { cn } from '../lib/utils';
import { ConfirmModal } from './ConfirmModal';
import { ColumnFilterPopover } from './ColumnFilterPopover';
interface DimensionsViewProps {
data: ExcelRow[];
@@ -48,6 +49,10 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
targetGroupKey: string;
selectedIndices: number[];
} | null>(null);
const [search, setSearch] = useState('');
const [lineFilter, setLineFilter] = useState<string[]>([]);
const [classFilter, setClassFilter] = useState<string[]>([]);
const [openFilter, setOpenFilter] = useState<'line' | 'class' | null>(null);
const groups = useMemo(() => {
const groupMap = new Map<string, { row: ExcelRow; index: number }[]>();
@@ -113,8 +118,33 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
}, [data]);
const filteredGroups = useMemo(() => {
return showOnlyInconsistent ? groups.filter(g => g.isInconsistent) : groups;
}, [groups, showOnlyInconsistent]);
let result = groups;
if (showOnlyInconsistent) result = result.filter(g => g.isInconsistent);
if (search || lineFilter.length > 0 || classFilter.length > 0) {
const s = search.toLowerCase();
result = result.filter(g => {
const matchesSearch = !search || g.rows.some(({ row }) =>
String(row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) ||
String(row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s)
);
const matchesLine = lineFilter.length === 0 || g.rows.some(({ row }) => lineFilter.includes(String(row[COLUMNS.LINE] || '')));
const matchesClass = classFilter.length === 0 || g.rows.some(({ row }) => classFilter.includes(String(row[COLUMNS.CLASSIFICATION] || '')));
return matchesSearch && matchesLine && matchesClass;
});
}
return result;
}, [groups, showOnlyInconsistent, search, lineFilter, classFilter]);
const uniqueLines = useMemo(() =>
Array.from(new Set(data.map(r => String(r[COLUMNS.LINE] || '')))).sort()
, [data]);
const uniqueClasses = useMemo(() =>
Array.from(new Set(data.map(r => String(r[COLUMNS.CLASSIFICATION] || '')))).sort()
, [data]);
const nearDuplicateClusters = useMemo((): NearDuplicateCluster[] => {
// Two groups are "similar" if every sorted dimension pair differs by < 1 cm absolute
@@ -261,17 +291,83 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
return (
<div className="space-y-6">
<div className="flex items-center justify-between bg-slate-800/50 p-4 rounded-lg border border-slate-700">
<div>
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
<Boxes className="text-blue-400" />
Dimension Consistency Check
</h2>
<p className="text-sm text-slate-400 mt-1">
Grouping products by Inner Box dimensions to find Packaging or MOQ discrepancies.
</p>
<div className="flex flex-wrap items-center gap-4 bg-slate-800/50 p-4 rounded-lg border border-slate-700">
<div className="relative flex-1 min-w-[250px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
type="text"
placeholder="Search SKU or Name in groups..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-md text-sm text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="relative">
<button
onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
lineFilter.length > 0 ? "bg-blue-600/10 border-blue-500/50 text-blue-400" : "bg-slate-900 border-slate-700 text-slate-400 hover:border-slate-600"
)}
>
<Filter className="w-4 h-4" />
Line {lineFilter.length > 0 && `(${lineFilter.length})`}
</button>
{openFilter === 'line' && (
<ColumnFilterPopover
uniqueValues={uniqueLines}
selectedValues={lineFilter}
onToggle={val => setLineFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
onSelectAll={setLineFilter}
onClear={() => setLineFilter([])}
onClose={() => setOpenFilter(null)}
title="Filter by Line"
className="left-auto right-0"
/>
)}
</div>
<div className="relative">
<button
onClick={() => setOpenFilter(openFilter === 'class' ? null : 'class')}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
classFilter.length > 0 ? "bg-blue-600/10 border-blue-500/50 text-blue-400" : "bg-slate-900 border-slate-700 text-slate-400 hover:border-slate-600"
)}
>
<Filter className="w-4 h-4" />
Class {classFilter.length > 0 && `(${classFilter.length})`}
</button>
{openFilter === 'class' && (
<ColumnFilterPopover
uniqueValues={uniqueClasses}
selectedValues={classFilter}
onToggle={val => setClassFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
onSelectAll={setClassFilter}
onClear={() => setClassFilter([])}
onClose={() => setOpenFilter(null)}
title="Filter by Classification"
className="left-auto right-0"
/>
)}
</div>
{(lineFilter.length > 0 || classFilter.length > 0 || search) && (
<button
onClick={() => { setSearch(''); setLineFilter([]); setClassFilter([]); }}
className="p-2 text-red-400 hover:text-red-300 transition-colors"
title="Clear all filters"
>
<XIcon className="w-5 h-5" />
</button>
)}
</div>
<div className="h-8 w-px bg-slate-700 mx-2 hidden sm:block" />
<div className="flex items-center gap-4 ml-auto">
<label className="flex items-center gap-2 text-sm text-slate-300 cursor-pointer">
<input
type="checkbox"
@@ -279,10 +375,10 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
onChange={e => setShowOnlyInconsistent(e.target.checked)}
className="rounded border-slate-600 bg-slate-700 text-blue-600 focus:ring-blue-500"
/>
Show only inconsistent groups
Show only inconsistent
</label>
<div className="text-xs text-slate-500 bg-slate-900 px-3 py-1.5 rounded-full border border-slate-700">
{groups.filter(g => g.isInconsistent).length} Inconsistencies found
<div className="text-[10px] font-bold text-amber-500 bg-amber-500/10 px-2 py-1 rounded border border-amber-500/20 whitespace-nowrap">
{groups.filter(g => g.isInconsistent).length} ISSUES
</div>
</div>
</div>