diff --git a/src/components/ArticleDetails.tsx b/src/components/ArticleDetails.tsx index d42894d..893ef21 100644 --- a/src/components/ArticleDetails.tsx +++ b/src/components/ArticleDetails.tsx @@ -8,7 +8,7 @@ interface ArticleDetailsProps { onEdit: (index: number) => void; } -type TabType = 'all' | 'missingDetailsDE' | 'missingDetailsEN' | 'missingAnyDetails' | 'lowStock' | 'missingTariff'; +type TabType = 'all' | 'missingDetailsDE' | 'missingDetailsEN' | 'missingAnyDetails' | 'lowStock'; export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { const [activeTab, setActiveTab] = useState('all'); @@ -29,15 +29,13 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { if (activeTab === 'missingDetailsEN') result = result.filter(r => !r.row[COLUMNS.DETAILS_EN]); if (activeTab === 'missingAnyDetails') result = result.filter(r => !r.row[COLUMNS.DETAILS_DE] || !r.row[COLUMNS.DETAILS_EN]); if (activeTab === 'lowStock') result = result.filter(r => Number(r.row[COLUMNS.ITEM_AVAILABLE] || 0) <= 0); - if (activeTab === 'missingTariff') result = result.filter(r => !r.row[COLUMNS.TARIFF_CODE]); // Search filter if (search) { const s = search.toLowerCase(); result = result.filter(r => String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || - String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) || - String(r.row[COLUMNS.BARCODE] || '').toLowerCase().includes(s) + String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) ); } @@ -98,7 +96,6 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { { id: 'missingDetailsEN', label: 'No Details EN' }, { id: 'missingAnyDetails', label: 'Missing Details' }, { id: 'lowStock', label: 'Out of Stock' }, - { id: 'missingTariff', label: 'No Tariff Code' }, ]; return ( @@ -125,7 +122,7 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { { setSearch(e.target.value); setPage(1); }} className="w-full pl-9 pr-4 py-2 bg-slate-900/50 border border-slate-700 rounded-md text-sm text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" @@ -149,9 +146,6 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { {[ { col: COLUMNS.ARTICLE_NO, label: 'SKU' }, { col: COLUMNS.ARTICLE_NAME, label: 'Name' }, - { col: COLUMNS.BARCODE, label: 'Barcode' }, - { col: COLUMNS.TARIFF_CODE, label: 'Tariff' }, - { col: COLUMNS.COUNTRY_ORIGIN, label: 'Origin' }, { col: COLUMNS.CLASSIFICATION, label: 'Class' }, { col: COLUMNS.ITEM_AVAILABLE, label: 'Stock' }, { col: COLUMNS.DETAILS_DE, label: 'Details DE' }, @@ -180,9 +174,6 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { {row[COLUMNS.ARTICLE_NAME]} - {row[COLUMNS.BARCODE] || '—'} - {row[COLUMNS.TARIFF_CODE] || '—'} - {row[COLUMNS.COUNTRY_ORIGIN] || '—'} - + No articles found. diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx index 22254bf..96a5868 100644 --- a/src/components/PricingView.tsx +++ b/src/components/PricingView.tsx @@ -10,6 +10,9 @@ import { X, ChevronDown, Edit2, + Filter, + Check, + Search, } from 'lucide-react'; import { cn } from '../lib/utils'; @@ -47,6 +50,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } const [editingCell, setEditingCell] = useState(null); const [savingCell, setSavingCell] = useState<{ rowIndex: number; colIndex: number } | null>(null); const inputRef = useRef(null); + const [lineMultiFilter, setLineMultiFilter] = useState([]); + const [classificationFilter, setClassificationFilter] = useState([]); + const [nameColFilter, setNameColFilter] = useState(''); + const [openFilter, setOpenFilter] = useState<'name' | 'line' | 'classification' | null>(null); // ── Dynamic column detection ────────────────────────────────────────────── const { uvpIdx, srpCols, containerCols } = useMemo(() => { @@ -128,10 +135,19 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } return { total: analyzedRows.length, withPricing, withUnits, withAny, allOk }; }, [analyzedRows]); + // ── Unique values for column filters ───────────────────────────────────── + const uniqueLines = useMemo(() => + Array.from(new Set(data.map(r => String(r[COLUMNS.LINE] || '')))).sort(), + [data]); + + const uniqueClassifications = useMemo(() => + Array.from(new Set(data.map(r => String(r[COLUMNS.CLASSIFICATION] || '')))).sort(), + [data]); + // ── Filtered rows ───────────────────────────────────────────────────────── const filteredRows = useMemo(() => { let result = analyzedRows; - + // Mode filter switch (filterMode) { case 'all_errors': result = analyzedRows.filter(r => r.hasErrors); break; @@ -139,17 +155,29 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } case 'units_errors': result = analyzedRows.filter(r => r.unitErrors.length > 0); break; } - // Search filter + // Global search (SKU + Name) if (search) { const s = search.toLowerCase(); - result = result.filter(r => + result = result.filter(r => String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) ); } + // Column filters + if (nameColFilter) { + const s = nameColFilter.toLowerCase(); + result = result.filter(r => String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s)); + } + if (lineMultiFilter.length > 0) { + result = result.filter(r => lineMultiFilter.includes(String(r.row[COLUMNS.LINE] || ''))); + } + if (classificationFilter.length > 0) { + result = result.filter(r => classificationFilter.includes(String(r.row[COLUMNS.CLASSIFICATION] || ''))); + } + return result; - }, [analyzedRows, filterMode, search]); + }, [analyzedRows, filterMode, search, nameColFilter, lineMultiFilter, classificationFilter]); // ── Inline edit helpers ─────────────────────────────────────────────────── const startEdit = (rowIndex: number, colIndex: number, currentValue: string) => { @@ -318,11 +346,75 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } Art. No. - - Article Name + {/* Article Name with text filter */} + +
+ Article Name + +
+ {openFilter === 'name' && ( + setOpenFilter(null)} + /> + )} - - Line + {/* Line with multi-select filter */} + +
+ Line + +
+ {openFilter === 'line' && ( + setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} + onClear={() => { setLineMultiFilter([]); setOpenFilter(null); }} + onClose={() => setOpenFilter(null)} + /> + )} + + {/* Classification with multi-select filter */} + +
+ Classification + +
+ {openFilter === 'classification' && ( + setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} + onClear={() => { setClassificationFilter([]); setOpenFilter(null); }} + onClose={() => setOpenFilter(null)} + /> + )} {/* Editable pricing columns */} @@ -388,6 +480,20 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } {row[COLUMNS.LINE] || '—'} + {/* Classification */} + + + {row[COLUMNS.CLASSIFICATION] || '—'} + + + {/* Editable pricing cells */} {pricingEditableCols.map(col => { const isEditing = editingCell?.rowIndex === dataIndex && editingCell?.colIndex === col.index; @@ -513,6 +619,80 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } ); } +// ── Text filter popover ─────────────────────────────────────────────────────── +function TextFilterPopover({ value, onChange, onClose }: { + value: string; + onChange: (v: string) => void; + onClose: () => void; +}) { + return ( +
+
+ + onChange(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" + /> +
+
+ + +
+
+ ); +} + +// ── Multi-select column filter popover ──────────────────────────────────────── +function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onClear, onClose }: { + uniqueValues: string[]; + selectedValues: string[]; + onToggle: (val: 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" + /> +
+
+ {filtered.map(val => ( + + ))} + {filtered.length === 0 &&

No values

} +
+
+ + +
+
+ ); +} + // ── Stat card ───────────────────────────────────────────────────────────────── function StatCard({ label,