diff --git a/src/components/ArticleDetails.tsx b/src/components/ArticleDetails.tsx
index 1456378..9af3a83 100644
--- a/src/components/ArticleDetails.tsx
+++ b/src/components/ArticleDetails.tsx
@@ -1,7 +1,8 @@
import React, { useState, useMemo } from 'react';
import { ExcelRow, COLUMNS } from '../types';
-import { Search, Filter, Edit2, ChevronDown, ChevronUp, Info, Package, X as XIcon, Check } from 'lucide-react';
+import { Search, Filter, Edit2, ChevronDown, ChevronUp, Info, Package, X as XIcon } from 'lucide-react';
import { cn } from '../lib/utils';
+import { ColumnFilterPopover } from './ColumnFilterPopover';
interface ArticleDetailsProps {
data: ExcelRow[];
@@ -319,99 +320,3 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) {
);
}
-
-interface ColumnFilterPopoverProps {
- uniqueValues: string[];
- selectedValues: string[];
- onToggle: (val: string) => void;
- onSelectAll: (vals: string[]) => void;
- onClear: () => void;
- onClose: () => void;
-}
-
-function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onSelectAll, onClear, onClose }: ColumnFilterPopoverProps) {
- const [search, setSearch] = useState('');
-
- const filteredValues = useMemo(() => {
- return uniqueValues.filter(v => v.toLowerCase().includes(search.toLowerCase()));
- }, [uniqueValues, search]);
-
- return (
-
-
-
- setSearch(e.target.value)}
- className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 text-xs text-white focus:outline-none focus:border-indigo-500"
- autoFocus
- />
-
-
-
-
-
{selectedValues.length} selected
-
-
-
- {filteredValues.map(val => (
-
- ))}
- {filteredValues.length === 0 && (
-
No values found
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/ColumnFilterPopover.tsx b/src/components/ColumnFilterPopover.tsx
new file mode 100644
index 0000000..faeb128
--- /dev/null
+++ b/src/components/ColumnFilterPopover.tsx
@@ -0,0 +1,119 @@
+import React, { useState, useMemo } from 'react';
+import { Search, Check, X } from 'lucide-react';
+import { cn } from '../lib/utils';
+
+interface ColumnFilterPopoverProps {
+ uniqueValues: string[];
+ selectedValues: string[];
+ onToggle: (val: string) => void;
+ onSelectAll: (vals: string[]) => void;
+ onClear: () => void;
+ onClose: () => void;
+ title?: string;
+ className?: string;
+}
+
+export function ColumnFilterPopover({
+ uniqueValues,
+ selectedValues,
+ onToggle,
+ onSelectAll,
+ onClear,
+ onClose,
+ title,
+ className
+}: ColumnFilterPopoverProps) {
+ const [search, setSearch] = useState('');
+
+ const filteredValues = useMemo(() => {
+ return uniqueValues.filter(v =>
+ String(v || '').toLowerCase().includes(search.toLowerCase())
+ );
+ }, [uniqueValues, search]);
+
+ const isAllSelected = selectedValues.length === uniqueValues.length && uniqueValues.length > 0;
+
+ return (
+
+ {title &&
{title}
}
+
+
+
+ setSearch(e.target.value)}
+ className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 text-xs text-white focus:outline-none focus:border-blue-500"
+ autoFocus
+ />
+
+
+
+
+
{selectedValues.length} selected
+
+
+
+ {filteredValues.map(val => (
+
+ ))}
+ {filteredValues.length === 0 && (
+
No values found
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/DimensionsView.tsx b/src/components/DimensionsView.tsx
index 31b49ac..837b828 100644
--- a/src/components/DimensionsView.tsx
+++ b/src/components/DimensionsView.tsx
@@ -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([]);
+ const [classFilter, setClassFilter] = useState([]);
+ const [openFilter, setOpenFilter] = useState<'line' | 'class' | null>(null);
const groups = useMemo(() => {
const groupMap = new Map();
@@ -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 (
-
-
-
-
- Dimension Consistency Check
-
-
- Grouping products by Inner Box dimensions to find Packaging or MOQ discrepancies.
-
+
+
+
+ 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"
+ />
-
+
+
+
+
+ {openFilter === 'line' && (
+ 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"
+ />
+ )}
+
+
+
+
+ {openFilter === 'class' && (
+ 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"
+ />
+ )}
+
+
+ {(lineFilter.length > 0 || classFilter.length > 0 || search) && (
+
+ )}
+
+
+
+
+
-
- {groups.filter(g => g.isInconsistent).length} Inconsistencies found
+
+ {groups.filter(g => g.isInconsistent).length} ISSUES
diff --git a/src/components/MatrixView.tsx b/src/components/MatrixView.tsx
index 358afdb..6e00438 100644
--- a/src/components/MatrixView.tsx
+++ b/src/components/MatrixView.tsx
@@ -1,6 +1,8 @@
import React, { useState, useMemo } from 'react';
-import { Search } from 'lucide-react';
import { ExcelRow } from '../types';
+import { Search, Filter, ChevronDown, ChevronUp, X as XIcon } from 'lucide-react';
+import { cn } from '../lib/utils';
+import { ColumnFilterPopover } from './ColumnFilterPopover';
interface MatrixViewProps {
data: ExcelRow[];
@@ -11,20 +13,88 @@ export function MatrixView({ data, headers }: MatrixViewProps) {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
const [search, setSearch] = useState('');
+ const [columnFilters, setColumnFilters] = useState
>({});
+ const [openFilterCol, setOpenFilterCol] = useState(null);
+ const [sortCol, setSortCol] = useState(null);
+ const [sortDesc, setSortDesc] = useState(false);
const filteredData = useMemo(() => {
- if (!search) return data;
- const s = search.toLowerCase();
- return data.filter(row =>
- row.some(cell => String(cell || '').toLowerCase().includes(s))
- );
- }, [data, search]);
+ let result = data.map((row, index) => ({ row, index }));
+
+ // Global search
+ if (search) {
+ const s = search.toLowerCase();
+ result = result.filter(r =>
+ r.row.some(cell => String(cell || '').toLowerCase().includes(s))
+ );
+ }
+
+ // Column-specific filters
+ Object.entries(columnFilters).forEach(([colIdx, selectedValues]) => {
+ const vals = selectedValues as string[];
+ if (vals.length > 0) {
+ result = result.filter(r => vals.includes(String(r.row[Number(colIdx)] || '')));
+ }
+ });
+
+ // Sorting
+ if (sortCol !== null) {
+ result.sort((a, b) => {
+ const valA = String(a.row[sortCol] || '').toLowerCase();
+ const valB = String(b.row[sortCol] || '').toLowerCase();
+
+ // Handle numbers sorting correctly
+ const numA = parseFloat(valA.replace(',', '.'));
+ const numB = parseFloat(valB.replace(',', '.'));
+
+ if (!isNaN(numA) && !isNaN(numB)) {
+ return sortDesc ? numB - numA : numA - numB;
+ }
+
+ return sortDesc
+ ? valB.localeCompare(valA)
+ : valA.localeCompare(valB);
+ });
+ }
+
+ return result;
+ }, [data, search, columnFilters, sortCol, sortDesc]);
const paginatedData = useMemo(() => {
const start = (page - 1) * pageSize;
return filteredData.slice(start, start + pageSize);
}, [filteredData, page, pageSize]);
+ const handleSort = (col: number) => {
+ if (sortCol === col) {
+ setSortDesc(!sortDesc);
+ } else {
+ setSortCol(col);
+ setSortDesc(false);
+ }
+ };
+
+ const getUniqueValues = (col: number) => {
+ const values = data.map(r => String(r[col] || ''));
+ return Array.from(new Set(values)).sort();
+ };
+
+ const toggleColumnFilter = (col: number, value: string) => {
+ setColumnFilters(prev => {
+ const current = prev[col] || [];
+ const next = current.includes(value)
+ ? current.filter(v => v !== value)
+ : [...current, value];
+ return { ...prev, [col]: next };
+ });
+ setPage(1);
+ };
+
+ const setBatchColumnFilter = (col: number, values: string[]) => {
+ setColumnFilters(prev => ({ ...prev, [col]: values }));
+ setPage(1);
+ };
+
const formatCellValue = (val: any, header: string = '') => {
if (val === undefined || val === null || val === '') return '';
@@ -91,15 +161,29 @@ export function MatrixView({ data, headers }: MatrixViewProps) {
Matrix View
All data fields formatted to 2 decimal places for numbers/prices.
-
-
-
{ setSearch(e.target.value); setPage(1); }}
- 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 focus:ring-1 focus:ring-blue-500 transition-all font-medium"
- />
+
+
+
+ { setSearch(e.target.value); setPage(1); }}
+ 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 focus:ring-1 focus:ring-blue-500 transition-all font-medium"
+ />
+
+ {(Object.values(columnFilters) as string[][]).some(v => v.length > 0) && (
+
+ )}
@@ -108,14 +192,58 @@ export function MatrixView({ data, headers }: MatrixViewProps) {
{headers.map((header, index) => (
- |
- {header}
+ |
+
+ handleSort(index)}
+ >
+ {header}
+ {sortCol === index && (
+ sortDesc ? :
+ )}
+
+
+
+
+ {openFilterCol === index && (
+ toggleColumnFilter(index, val)}
+ onSelectAll={(vals) => setBatchColumnFilter(index, vals)}
+ onClear={() => {
+ setColumnFilters(prev => {
+ const next = { ...prev };
+ delete next[index];
+ return next;
+ });
+ setOpenFilterCol(null);
+ }}
+ onClose={() => setOpenFilterCol(null)}
+ title={header}
+ />
+ )}
|
))}
- {paginatedData.map((row, rowIndex) => (
+ {paginatedData.map(({ row, index: rowIndex }) => (
{headers.map((header, colIndex) => {
const formattedValue = formatCellValue(row[colIndex], header);
diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx
index a4baedb..d9585ef 100644
--- a/src/components/PricingView.tsx
+++ b/src/components/PricingView.tsx
@@ -15,6 +15,7 @@ import {
Search,
} from 'lucide-react';
import { cn } from '../lib/utils';
+import { ColumnFilterPopover } from './ColumnFilterPopover';
interface PricingViewProps {
data: ExcelRow[];
@@ -648,80 +649,6 @@ function TextFilterPopover({ value, onChange, onClose }: {
);
}
-// ── Multi-select column filter popover ────────────────────────────────────────
-function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onSelectAll, onClear, onClose }: {
- uniqueValues: string[];
- selectedValues: string[];
- onToggle: (val: string) => void;
- onSelectAll: (vals: string[]) => void;
- onClear: () => void;
- onClose: () => void;
-}) {
- const [search, setSearch] = useState('');
- const filtered = uniqueValues.filter(v => v.toLowerCase().includes(search.toLowerCase()));
-
- return (
-
-
-
- setSearch(e.target.value)}
- autoFocus
- className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 text-xs text-white focus:outline-none focus:border-blue-500"
- />
-
-
-
-
-
{selectedValues.length} selected
-
-
-
- {filtered.map(val => (
-
- ))}
- {filtered.length === 0 &&
No values
}
-
-
-
-
-
-
- );
-}
-
// ── Stat card ─────────────────────────────────────────────────────────────────
function StatCard({
label,
diff --git a/src/components/ProductDescriptions.tsx b/src/components/ProductDescriptions.tsx
index 4e0972f..6c9773c 100644
--- a/src/components/ProductDescriptions.tsx
+++ b/src/components/ProductDescriptions.tsx
@@ -1,7 +1,8 @@
import React, { useState, useMemo } from 'react';
import { ExcelRow, COLUMNS } from '../types';
-import { Search, Filter, Edit2, ChevronDown, ChevronUp, X, Check } from 'lucide-react';
+import { Search, Filter, Edit2, ChevronDown, ChevronUp, X } from 'lucide-react';
import { cn } from '../lib/utils';
+import { ColumnFilterPopover } from './ColumnFilterPopover';
interface ProductDescriptionsProps {
data: ExcelRow[];
@@ -361,99 +362,3 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps)
);
}
-
-interface ColumnFilterPopoverProps {
- uniqueValues: string[];
- selectedValues: string[];
- onToggle: (val: string) => void;
- onSelectAll: (vals: string[]) => void;
- onClear: () => void;
- onClose: () => void;
-}
-
-function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onSelectAll, onClear, onClose }: ColumnFilterPopoverProps) {
- const [search, setSearch] = useState('');
-
- const filteredValues = useMemo(() => {
- return uniqueValues.filter(v => v.toLowerCase().includes(search.toLowerCase()));
- }, [uniqueValues, search]);
-
- return (
-
-
-
- setSearch(e.target.value)}
- className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 text-xs text-white focus:outline-none focus:border-blue-500"
- autoFocus
- />
-
-
-
-
-
{selectedValues.length} selected
-
-
-
- {filteredValues.map(val => (
-
- ))}
- {filteredValues.length === 0 && (
-
No values found
- )}
-
-
-
-
-
-
-
- );
-}