diff --git a/src/App.tsx b/src/App.tsx index 61ce8fb..2b7b8c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,34 @@ import { HistoryView } from './components/HistoryView'; import { UndoToast } from './components/UndoToast'; import { PendingValidationView } from './components/PendingValidationView'; import { MissingDataView } from './components/MissingDataView'; + +const FORCED_ZERO_STOCK_SKUS = new Set([ + '11631VC', '1237VC', '1238VC', '1652VC', '1653VC', '1684VC', '1688VC', '1717VC', + '1718VC', '180VC', '1832VC', '2025VC', '2027VC', '2180VC', '2181VC', '2210VC', + '2260VC', '2355VC', '2356VC', '2360VC', '2400VC', '2401VC', '2402VC', '2405VC', + '2406VC', '2414VC', '2415VC', '2425VC', '2447VC', '2450VC', '2510VC', '2517VC', + '2522VC', '2611VC', '2648VC', '2660VC', '2665VC', '2857VC', '2858VC', '2990VC', + '3011VC', '3012VC', '3013VC', '3015VC', '3018VC', '3019VC', '3020VC', '3021VC', + '3027VC', '3030VC', '3035VC', '3038VC', '3041VC', '3043VC', '3044VC', '3045VC', + '3046VC', '3048VC', '3052VC', '3053VC', '3054VC', '3055VC', '3062VC', '3063VC', + '3064VC', '3069VC', '3070VC', '3071VC', '3072VC', '3074VC', '3089VC', '3116VC', + '3117VC', '3118VC', '3119VC', '3120VC', '3121VC', '3123VC', '3125VC', '3126VC', + '3127VC', '3130VC', '3133VC', '3134VC', '3135VC', '3200VC', '3201VC', '3230VC', + '3311VC', '3314VC', '3315VC', '3354VC', '3372VC', '3380VC', '3391VC', '3392VC', + '3423VC', '3432VC', '3437VC', '3484VC', '3487VC', '3555VC', '3557VC', '3558VC', + '3559VC', '3712VC', '3824VC', '4107VC', '4108VC', '4110VC', '4115VC', '4125VC', + '4130VC', '4134VC', '4140VC', '4155VC', '4158VC', '4165VC', '4231VC', '4232VC', + '4255VC', '4259VC', '4261VC', '4263VC', '4264VC', '4265VC', '4285VC', '4288VC', + '4330VC', '4361VC', '4442VC', '4445VC', '4462VC', '4528VC', '4531VC', '4534VC', + '4558VC', '4657VC', '4721VC', '4722VC', '4727VC', '4735VC', '4737VC', '4745VC', + '4752VC', '4758VC', '4765VC', '4785VC', '4790VC', '4801VC', '4815VC', '4874VC', + '4882VC', '5032VC', '5113VC', '5382VC', '5383VC', '5440VC', '5442VC', '5456VC', + '5540VC', '5571VC', '5572VC', '5574VC', '5575VC', '5588VC', '5592VC', '5685VC', + '5712VC', '5721VC', '5807VC', '5823VC', '5825VC', '5835VC', '5852VC', '5871VC', + '5872VC', '5873VC', '5874VC', '5882VC', '5883VC', '5885VC', '5987VC', '5991VC', + '5995VC', '6001VC', '6007VC', '6040VC', '6151VC', '6252VC', '6255VC', '6270VC', + '6471VC', '5658VCI' +]); export default function App() { const [session, setSession] = useState(() => getStoredSession()); @@ -231,6 +259,11 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; } } + // Force stock 0 for specific SKUs (User request) + if (FORCED_ZERO_STOCK_SKUS.has(articleNo)) { + finalRow[COLUMNS.ITEM_AVAILABLE] = 0; + } + return finalRow.map((val: any, idx: number) => { if (val === undefined || val === null || val === '') return val; const header = (extendedHeaders[idx] || '').toLowerCase(); @@ -317,8 +350,9 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; const headers = data[0]; const rawRows = data.slice(1); + const resolvedCols = resolveColumnIndices(headers); const processedRows = rawRows.map(row => { - return row.map((val, idx) => { + const finalRow = row.map((val, idx) => { if (val === undefined || val === null || val === '') return val; const header = (headers[idx] || '').toLowerCase(); @@ -356,6 +390,13 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; } return val; }); + + // Force stock 0 for specific SKUs (User request) + const articleNo = String(finalRow[resolvedCols.ARTICLE_NO] || ''); + if (FORCED_ZERO_STOCK_SKUS.has(articleNo)) { + finalRow[resolvedCols.ITEM_AVAILABLE] = 0; + } + return finalRow; }); setAppState({ diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx index 8663490..38f2822 100644 --- a/src/components/PricingView.tsx +++ b/src/components/PricingView.tsx @@ -71,6 +71,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, outerW: 80, outerL: 80, outerH: 80, + moq: 80, check: 100, actions: 60, }); @@ -100,6 +101,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, const [outerWFilter, setOuterWFilter] = usePersistentState('pricing-outerWFilter', []); const [outerLFilter, setOuterLFilter] = usePersistentState('pricing-outerLFilter', []); const [outerHFilter, setOuterHFilter] = usePersistentState('pricing-outerHFilter', []); + const [checkYingFilter, setCheckYingFilter] = usePersistentState('pricing-checkYingFilter', []); + const [checkAnnaFilter, setCheckAnnaFilter] = usePersistentState('pricing-checkAnnaFilter', []); const [dynamicColFilters, setDynamicColFilters] = usePersistentState>('pricing-dynamicColFilters', {}); const [weightIssueFilter, setWeightIssueFilter] = usePersistentState<'all' | 'with' | 'without'>('pricing-weightIssueFilter', 'all'); const [selectedSearchItems, setSelectedSearchItems] = usePersistentState>('pricing-selectedSearchItems', new Set()); @@ -111,6 +114,62 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, const [pinnedColumns, setPinnedColumns] = usePersistentState>('pricing-pinnedColumns', new Set(['articleNo', 'articleName'])); const [showPinPanel, setShowPinPanel] = useState(false); + // Drag-to-select state + const [selectedRows, setSelectedRows] = useState>(new Set()); + const isDraggingRef = useRef(false); + const dragStartIndexRef = useRef(null); + const dragModeRef = useRef<'add' | 'remove'>('add'); + const lastDragIndexRef = useRef(null); + const visibleDataIndexesRef = useRef([]); + + const handleRowMouseDown = useCallback((e: React.MouseEvent, dataIndex: number) => { + if ((e.target as HTMLElement).closest('input, button, a')) return; + e.preventDefault(); + isDraggingRef.current = true; + dragStartIndexRef.current = dataIndex; + lastDragIndexRef.current = dataIndex; + + if (e.ctrlKey || e.metaKey) { + dragModeRef.current = selectedRows.has(dataIndex) ? 'remove' : 'add'; + setSelectedRows(prev => { + const next = new Set(prev); + if (next.has(dataIndex)) next.delete(dataIndex); else next.add(dataIndex); + return next; + }); + return; + } + + dragModeRef.current = selectedRows.has(dataIndex) ? 'remove' : 'add'; + setSelectedRows(new Set([dataIndex])); + }, [selectedRows]); + + const handleRowMouseEnter = useCallback((dataIndex: number) => { + if (!isDraggingRef.current || dragStartIndexRef.current === null) return; + if (dataIndex === lastDragIndexRef.current) return; + lastDragIndexRef.current = dataIndex; + + const start = dragStartIndexRef.current; + const end = dataIndex; + const [lo, hi] = start <= end ? [start, end] : [end, start]; + + setSelectedRows(prev => { + const next = new Set(prev); + visibleDataIndexesRef.current.forEach((idx: number) => { + if (idx >= lo && idx <= hi) { + if (dragModeRef.current === 'add') next.add(idx); + else next.delete(idx); + } + }); + return next; + }); + }, []); + + useEffect(() => { + const handleMouseUp = () => { isDraggingRef.current = false; }; + document.addEventListener('mouseup', handleMouseUp); + return () => document.removeEventListener('mouseup', handleMouseUp); + }, []); + // Close search dropdown on click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -313,6 +372,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, Array.from(new Set(data.map(r => String(r[COLUMNS.ITEM_TO_LOGISTIC] || '')))).filter(v => v).sort(), [data]); + const uniqueCheckYing = useMemo(() => + Array.from(new Set(data.map(r => r[COLUMNS.VALIDATED_CHECK] ? 'Checked' : 'Unchecked'))).sort(), + [data, COLUMNS.VALIDATED_CHECK]); + + const uniqueCheckAnna = useMemo(() => + Array.from(new Set(data.map(r => r[COLUMNS.ANNA_CHECK] ? 'Checked' : 'Unchecked'))).sort(), + [data, COLUMNS.ANNA_CHECK]); + // ── Filtered rows ───────────────────────────────────────────────────────── const filteredRows = useMemo(() => { let result = analyzedRows; @@ -408,6 +475,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, if ((itemToLogisticFilter || []).length > 0) { result = result.filter(r => (itemToLogisticFilter || []).includes(String(r.row[COLUMNS.ITEM_TO_LOGISTIC] || ''))); } + if ((checkYingFilter || []).length > 0) { + result = result.filter(r => (checkYingFilter || []).includes(r.row[COLUMNS.VALIDATED_CHECK] ? 'Checked' : 'Unchecked')); + } + if ((checkAnnaFilter || []).length > 0) { + result = result.filter(r => (checkAnnaFilter || []).includes(r.row[COLUMNS.ANNA_CHECK] ? 'Checked' : 'Unchecked')); + } // Dynamic column filters (SRP, container) (Object.keys(dynamicColFilters) as string[]).forEach(colIdx => { @@ -491,6 +564,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, return sortedRows.slice(start, start + pageSize); }, [sortedRows, currentPage]); + // Keep ref in sync so drag handlers can read current visible indexes without stale closure + visibleDataIndexesRef.current = paginatedRows.map(r => r.dataIndex); + const totalPages = Math.ceil(sortedRows.length / pageSize); const handleSort = (key: string | number) => { @@ -591,6 +667,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, { key: 'outerW', label: 'Outer W', index: COLUMNS.OUTER_W }, { key: 'outerL', label: 'Outer L', index: COLUMNS.OUTER_L }, { key: 'outerH', label: 'Outer H', index: COLUMNS.OUTER_H }, + { key: 'moq', label: 'MOQ', index: COLUMNS.MOQ }, ]; pricingEditableCols.forEach(col => { cols.push({ key: `prc_${col.index}`, label: col.name, index: col.index }); @@ -615,7 +692,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, // Calculate sticky left position for each pinned column const getStickyLeft = useMemo(() => { - const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH']; + const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq']; const dynamicOrder: string[] = []; pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`)); containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`)); @@ -640,7 +717,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, // Calculate z-index for sticky columns (so they stack correctly) const getStickyRank = useMemo(() => { - const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH']; + const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq']; const dynamicOrder: string[] = []; pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`)); containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`)); @@ -1605,7 +1682,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, { e.stopPropagation(); handleResizeStart(e, 'outerH', columnWidths.outerH); }} /> - {containerCols.map(col => { +{containerCols.map(col => { const colKey = `con_${col.index}`; const width = columnWidths[colKey] ?? 110; return ( @@ -1640,24 +1717,41 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, setDynamicColFilters(prev => { - const current = prev[col.index] || []; - return current.includes(val) - ? { ...prev, [col.index]: current.filter(v => v !== val) } - : { ...prev, [col.index]: [...current, val] }; - })} - onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))} - onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }} - onClose={() => setOpenFilter(null)} - zIndex={isPinned(colKey) ? 200 : 50} - /> - )} + selectedValues={dynamicColFilters[col.index] || []} + onToggle={val => setDynamicColFilters(prev => { + const current = prev[col.index] || []; + return current.includes(val) + ? { ...prev, [col.index]: current.filter(v => v !== val) } + : { ...prev, [col.index]: [...current, val] }; + })} + onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))} + onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }} + onClose={() => setOpenFilter(null)} + zIndex={isPinned(colKey) ? 200 : 50} + /> + )} { e.stopPropagation(); handleResizeStart(e, colKey, width); }} /> ); })} + handleSort('moq')}> +
+ + {isPinned('moq') && } + MOQ +
+ { e.stopPropagation(); handleResizeStart(e, 'moq', columnWidths.moq); }} /> + +
Weight Issues @@ -1690,12 +1784,60 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, handleSort(COLUMNS.VALIDATED_CHECK)}> - Check Ying +
+ Check Ying + +
+ {openFilter === 'check' && ( + setCheckYingFilter(prev => (prev || []).includes(val) ? (prev || []).filter(v => v !== val) : [...(prev || []), val])} + onSelectAll={vals => setCheckYingFilter(vals || [])} + onClear={() => { setCheckYingFilter([]); setOpenFilter(null); }} + onClose={() => setOpenFilter(null)} + zIndex={50} + /> + )} { e.stopPropagation(); handleResizeStart(e, 'check', columnWidths.check); }} /> handleSort(COLUMNS.ANNA_CHECK)}> - Check Anna +
+ Check Anna + +
+ {openFilter === 'checkAnna' && ( + setCheckAnnaFilter(prev => (prev || []).includes(val) ? (prev || []).filter(v => v !== val) : [...(prev || []), val])} + onSelectAll={vals => setCheckAnnaFilter(vals || [])} + onClear={() => { setCheckAnnaFilter([]); setOpenFilter(null); }} + onClose={() => setOpenFilter(null)} + zIndex={50} + /> + )} { e.stopPropagation(); handleResizeStart(e, 'checkAnna', columnWidths.checkAnna ?? 100); }} /> @@ -1920,6 +2062,11 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, ); })} + {/* MOQ cell */} + + {row[COLUMNS.MOQ] !== undefined && row[COLUMNS.MOQ] !== null ? row[COLUMNS.MOQ] : '-'} + + {/* Issues column (weight only) */} {(() => {