feat: add MOQ filtering and sorting in PricingView

This commit is contained in:
Christian Vidal Wolf
2026-04-28 09:21:24 +02:00
parent 712a9dac16
commit 69f9773410
2 changed files with 90 additions and 19 deletions
+14
View File
@@ -0,0 +1,14 @@
> react-example@0.0.0 lint
> tsc --noEmit
src/components/PricingView.tsx(200,25): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
src/components/PricingView.tsx(201,26): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
src/components/PricingView.tsx(203,23): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
src/components/PricingView.tsx(1042,16): error TS2741: Property 'triggerId' is missing in type '{ value: { terms: string[]; op: "and" | "or"; }; onChange: (value: { terms: string[]; op: "and" | "or"; } | ((prev: { terms: string[]; op: "and" | "or"; }) => { terms: string[]; op: "and" | "or"; })) => void; onClose: () => any; }' but required in type '{ value: { terms: string[]; op: "and" | "or"; }; onChange: (v: { terms: string[]; op: "and" | "or"; }) => void; onClose: () => void; triggerId: string; }'.
src/components/PricingView.tsx(1304,23): error TS2322: Type '{ triggerId: string; value: { terms: string[]; op: "and" | "or"; }; onChange: (value: { terms: string[]; op: "and" | "or"; } | ((prev: { terms: string[]; op: "and" | "or"; }) => { terms: string[]; op: "and" | "or"; })) => void; onClose: () => any; zIndex: number; }' is not assignable to type '{ value: { terms: string[]; op: "and" | "or"; }; onChange: (v: { terms: string[]; op: "and" | "or"; }) => void; onClose: () => void; triggerId: string; }'.
Property 'zIndex' does not exist on type '{ value: { terms: string[]; op: "and" | "or"; }; onChange: (v: { terms: string[]; op: "and" | "or"; }) => void; onClose: () => void; triggerId: string; }'.
src/components/PricingView.tsx(1339,23): error TS2322: Type '{ triggerId: string; value: { terms: string[]; op: "and" | "or"; }; onChange: (value: { terms: string[]; op: "and" | "or"; } | ((prev: { terms: string[]; op: "and" | "or"; }) => { terms: string[]; op: "and" | "or"; })) => void; onClose: () => any; zIndex: number; }' is not assignable to type '{ value: { terms: string[]; op: "and" | "or"; }; onChange: (v: { terms: string[]; op: "and" | "or"; }) => void; onClose: () => void; triggerId: string; }'.
Property 'zIndex' does not exist on type '{ value: { terms: string[]; op: "and" | "or"; }; onChange: (v: { terms: string[]; op: "and" | "or"; }) => void; onClose: () => void; triggerId: string; }'.
src/components/ProductDescriptions.tsx(205,21): error TS2304: Cannot find name 'TabType'.
src/types.ts(50,14): error TS2740: Type '{ ARTICLE_NO: string[]; ARTICLE_NAME: string[]; LINE: string[]; LICENSE: string[]; DETAILS_EN: string[]; DETAILS_DE: string[]; BARCODE: string[]; TARIFF_CODE: string[]; COUNTRY_ORIGIN: string[]; LONG_DE: string[]; ... 15 more ...; OUTER_H: string[]; }' is missing the following properties from type 'Record<"ASIN" | "ARTICLE_NO" | "ARTICLE_NAME" | "LINE" | "LICENSE" | "DETAILS_EN" | "DETAILS_DE" | "BARCODE" | "TARIFF_CODE" | "COUNTRY_ORIGIN" | "LONG_DE" | "LONG_EN" | "SHORT_DE" | ... 19 more ... | "ANNA_NOTE", string[]>': VERIFIED_DIMS, VALIDATED_CHECK, VALIDATED_NOTE, PRODUCT_TYPE, and 3 more.
+76 -19
View File
@@ -101,6 +101,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const [outerWFilter, setOuterWFilter] = usePersistentState<string[]>('pricing-outerWFilter', []); const [outerWFilter, setOuterWFilter] = usePersistentState<string[]>('pricing-outerWFilter', []);
const [outerLFilter, setOuterLFilter] = usePersistentState<string[]>('pricing-outerLFilter', []); const [outerLFilter, setOuterLFilter] = usePersistentState<string[]>('pricing-outerLFilter', []);
const [outerHFilter, setOuterHFilter] = usePersistentState<string[]>('pricing-outerHFilter', []); const [outerHFilter, setOuterHFilter] = usePersistentState<string[]>('pricing-outerHFilter', []);
const [moqFilter, setMoqFilter] = usePersistentState<string[]>('pricing-moqFilter', []);
const [checkYingFilter, setCheckYingFilter] = usePersistentState<string[]>('pricing-checkYingFilter', []); const [checkYingFilter, setCheckYingFilter] = usePersistentState<string[]>('pricing-checkYingFilter', []);
const [checkAnnaFilter, setCheckAnnaFilter] = usePersistentState<string[]>('pricing-checkAnnaFilter', []); const [checkAnnaFilter, setCheckAnnaFilter] = usePersistentState<string[]>('pricing-checkAnnaFilter', []);
const [dynamicColFilters, setDynamicColFilters] = usePersistentState<Record<number, string[]>>('pricing-dynamicColFilters', {}); const [dynamicColFilters, setDynamicColFilters] = usePersistentState<Record<number, string[]>>('pricing-dynamicColFilters', {});
@@ -368,6 +369,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
Array.from(new Set(data.map(r => String(r[COLUMNS.OUTER_H] || '')))).filter(v => v).sort((a, b) => Number(a) - Number(b)), Array.from(new Set(data.map(r => String(r[COLUMNS.OUTER_H] || '')))).filter(v => v).sort((a, b) => Number(a) - Number(b)),
[data]); [data]);
const uniqueMoq = useMemo(() =>
Array.from(new Set(data.map(r => String(r[COLUMNS.MOQ] || '')))).filter(v => v).sort((a, b) => Number(a) - Number(b)),
[data]);
const uniqueItemToLogistic = useMemo(() => const uniqueItemToLogistic = useMemo(() =>
Array.from(new Set(data.map(r => String(r[COLUMNS.ITEM_TO_LOGISTIC] || '')))).filter(v => v).sort(), Array.from(new Set(data.map(r => String(r[COLUMNS.ITEM_TO_LOGISTIC] || '')))).filter(v => v).sort(),
[data]); [data]);
@@ -472,6 +477,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
if ((outerHFilter || []).length > 0) { if ((outerHFilter || []).length > 0) {
result = result.filter(r => (outerHFilter || []).includes(String(r.row[COLUMNS.OUTER_H] || ''))); result = result.filter(r => (outerHFilter || []).includes(String(r.row[COLUMNS.OUTER_H] || '')));
} }
if ((moqFilter || []).length > 0) {
result = result.filter(r => (moqFilter || []).includes(String(r.row[COLUMNS.MOQ] || '')));
}
if ((itemToLogisticFilter || []).length > 0) { if ((itemToLogisticFilter || []).length > 0) {
result = result.filter(r => (itemToLogisticFilter || []).includes(String(r.row[COLUMNS.ITEM_TO_LOGISTIC] || ''))); result = result.filter(r => (itemToLogisticFilter || []).includes(String(r.row[COLUMNS.ITEM_TO_LOGISTIC] || '')));
} }
@@ -499,7 +507,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
} }
return result; return result;
}, [analyzedRows, filterMode, search, nameColFilter, articleNoColFilter, globalAdvancedFilter, lineMultiFilter, classificationFilter, productTypeFilter, unitsOuterFilter, outerWFilter, outerLFilter, outerHFilter, dynamicColFilters, weightIssueFilter, selectedSearchItems]); }, [analyzedRows, filterMode, search, nameColFilter, articleNoColFilter, globalAdvancedFilter, lineMultiFilter, classificationFilter, productTypeFilter, unitsOuterFilter, outerWFilter, outerLFilter, outerHFilter, moqFilter, dynamicColFilters, weightIssueFilter, selectedSearchItems]);
// ── Sorted rows ────────────────────────────────────────────────────────── // ── Sorted rows ──────────────────────────────────────────────────────────
const sortedRows = useMemo(() => { const sortedRows = useMemo(() => {
@@ -522,6 +530,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
case 'outerW': colIndex = COLUMNS.OUTER_W; break; case 'outerW': colIndex = COLUMNS.OUTER_W; break;
case 'outerL': colIndex = COLUMNS.OUTER_L; break; case 'outerL': colIndex = COLUMNS.OUTER_L; break;
case 'outerH': colIndex = COLUMNS.OUTER_H; break; case 'outerH': colIndex = COLUMNS.OUTER_H; break;
case 'moq': colIndex = COLUMNS.MOQ; break;
case 'itemToLogistic': colIndex = COLUMNS.ITEM_TO_LOGISTIC; break; case 'itemToLogistic': colIndex = COLUMNS.ITEM_TO_LOGISTIC; break;
default: return filteredRows; default: return filteredRows;
} }
@@ -565,7 +574,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
}, [sortedRows, currentPage]); }, [sortedRows, currentPage]);
// Keep ref in sync so drag handlers can read current visible indexes without stale closure // Keep ref in sync so drag handlers can read current visible indexes without stale closure
visibleDataIndexesRef.current = paginatedRows.map(r => r.dataIndex); visibleDataIndexesRef.current = paginatedRows.map((r: { dataIndex: number }) => r.dataIndex);
const totalPages = Math.ceil(sortedRows.length / pageSize); const totalPages = Math.ceil(sortedRows.length / pageSize);
@@ -808,6 +817,11 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
COLUMNS.OUTER_W, COLUMNS.OUTER_W,
COLUMNS.OUTER_L, COLUMNS.OUTER_L,
COLUMNS.OUTER_H, COLUMNS.OUTER_H,
COLUMNS.VALIDATED_CHECK,
COLUMNS.VALIDATED_NOTE,
COLUMNS.ANNA_CHECK,
COLUMNS.ANNA_NOTE,
COLUMNS.MOQ,
]); ]);
if (uvpIdx >= 0) set.add(uvpIdx); if (uvpIdx >= 0) set.add(uvpIdx);
srpCols.forEach(c => set.add(c.index)); srpCols.forEach(c => set.add(c.index));
@@ -1748,7 +1762,29 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<span className="truncate flex items-center gap-1"> <span className="truncate flex items-center gap-1">
{isPinned('moq') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />} {isPinned('moq') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
MOQ <SortIcon current={sortConfig.key === 'moq' ? sortConfig.direction : null} /></span> MOQ <SortIcon current={sortConfig.key === 'moq' ? sortConfig.direction : null} /></span>
<button
id="filter-trigger-moq"
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'moq' ? null : 'moq'); }}
className={cn(
'ml-1 p-0.5 rounded transition-colors filter-trigger-btn',
(moqFilter || []).length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
<Filter className="w-3 h-3" />
</button>
</div> </div>
{openFilter === 'moq' && (
<ColumnFilterPopover
triggerId="filter-trigger-moq"
uniqueValues={uniqueMoq || []}
selectedValues={moqFilter || []}
onToggle={val => setMoqFilter(prev => (prev || []).includes(val) ? (prev || []).filter(v => v !== val) : [...(prev || []), val])}
onSelectAll={vals => setMoqFilter(vals || [])}
onClear={() => { setMoqFilter([]); setOpenFilter(null); }}
onClose={() => setOpenFilter(null)}
zIndex={isPinned('moq') ? 200 : 50}
/>
)}
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'moq', columnWidths.moq); }} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'moq', columnWidths.moq); }} />
</th> </th>
@@ -1863,22 +1899,29 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const isValidated = !!row[COLUMNS.VALIDATED_CHECK]; const isValidated = !!row[COLUMNS.VALIDATED_CHECK];
const note = String(row[COLUMNS.VALIDATED_NOTE] || ''); const note = String(row[COLUMNS.VALIDATED_NOTE] || '');
const isRowSelected = selectedRows.has(dataIndex);
return ( return (
<tr <tr
key={dataIndex} key={dataIndex}
onMouseDown={(e: React.MouseEvent<HTMLTableRowElement>) => handleRowMouseDown(e, dataIndex)}
onMouseEnter={() => handleRowMouseEnter(dataIndex)}
style={{ userSelect: 'none' }}
className={cn( className={cn(
'border-b border-slate-700/50 transition-colors group relative hover:z-50', 'border-b border-slate-700/50 transition-colors group relative hover:z-50 cursor-default',
isValidated isRowSelected
? 'bg-green-400/30 border-l-4 border-l-green-400' // Vibrant high-visibility green ? 'bg-blue-500/20 border-l-4 border-l-blue-400'
: saveStatus === 'error' : isValidated
? 'bg-red-400/20 border-l-4 border-l-red-500' ? 'bg-green-400/30 border-l-4 border-l-green-400'
: saveStatus === 'pending' : saveStatus === 'error'
? 'bg-amber-400/20 border-l-4 border-l-amber-400' ? 'bg-red-400/20 border-l-4 border-l-red-500'
: isCritical : saveStatus === 'pending'
? 'bg-red-950/20' ? 'bg-amber-400/20 border-l-4 border-l-amber-400'
: pricingErrors.length > 0 : isCritical
? 'bg-amber-950/10' ? 'bg-red-950/20'
: '' : pricingErrors.length > 0
? 'bg-amber-950/10'
: ''
)} )}
> >
{/* Art. No. */} {/* Art. No. */}
@@ -2182,12 +2225,26 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{/* ── Footer count & Pagination ── */} {/* ── Footer count & Pagination ── */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs text-slate-500 pb-1"> <div className="flex items-center gap-3">
Showing {paginatedRows.length} of {sortedRows.length} products {totalPages > 1 && `(${currentPage}/${totalPages})`} <p className="text-xs text-slate-500 pb-1">
{pricingEditableCols.length > 0 && ( Showing {paginatedRows.length} of {sortedRows.length} products {totalPages > 1 && `(${currentPage}/${totalPages})`}
<> · Click any <span className="text-blue-400">price cell</span> to edit inline</> {pricingEditableCols.length > 0 && (
<> · Click any <span className="text-blue-400">price cell</span> to edit inline</>
)}
</p>
{selectedRows.size > 0 && (
<span className="flex items-center gap-1.5 text-xs text-blue-400 bg-blue-500/10 border border-blue-500/20 px-2 py-0.5 rounded mb-1">
{selectedRows.size} selected
<button
onClick={() => setSelectedRows(new Set())}
className="ml-1 text-blue-300 hover:text-white transition-colors"
title="Clear selection"
>
<X className="w-3 h-3" />
</button>
</span>
)} )}
</p> </div>
{totalPages > 1 && ( {totalPages > 1 && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button