mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:35:23 +02:00
feat: standardize ColumnFilterPopover and add SELECT ALL to all views/tabs
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user