import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react'; import { ExcelRow, COLUMNS } from '../types'; import { AlertTriangle, AlertCircle, CheckCircle2, DollarSign, Package, Save, X, ChevronDown, Edit2, Filter, Check, Search, MessageSquare, } from 'lucide-react'; import { cn } from '../lib/utils'; import { ColumnFilterPopover } from './ColumnFilterPopover'; interface PricingViewProps { data: ExcelRow[]; headers: string[]; onSaveRow: (index: number, updatedRow: ExcelRow) => void; onCaptureState: (message: string) => void; onEdit: (index: number) => void; rowStatuses: Record; } interface DetectedCol { index: number; name: string; } type FilterMode = 'all' | 'all_errors' | 'pricing_errors' | 'units_errors'; interface EditingCell { rowIndex: number; colIndex: number; value: string; } // Flexible multi-keyword column finder function findCol(headers: string[], ...keywords: string[]): number { return headers.findIndex(h => keywords.every(kw => (h || '').toLowerCase().includes(kw.toLowerCase())) ); } export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses }: PricingViewProps) { const [filterMode, setFilterMode] = useState('all_errors'); const [search, setSearch] = useState(''); const [searchMode, setSearchMode] = useState<'all' | 'selected'>('all'); const [currentPage, setCurrentPage] = useState(1); const pageSize = 100; const [columnWidths, setColumnWidths] = useState>({ articleNo: 100, articleName: 220, line: 100, classification: 130, productType: 140, unitsOuter: 100, outerW: 80, outerL: 80, outerH: 80, check: 100, actions: 60, }); const [resizingColumn, setResizingColumn] = useState(null); const [resizeStartX, setResizeStartX] = useState(null); const [resizeStartWidth, setResizeStartWidth] = useState(null); const tableRef = useRef(null); 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(null); const [unitsOuterFilter, setUnitsOuterFilter] = useState([]); const [outerWFilter, setOuterWFilter] = useState([]); const [outerLFilter, setOuterLFilter] = useState([]); const [outerHFilter, setOuterHFilter] = useState([]); const [dynamicColFilters, setDynamicColFilters] = useState>({}); const [editingType, setEditingType] = useState<{ rowIndex: number; value: string } | null>(null); const [typeSuggestions, setTypeSuggestions] = useState([]); const typeInputRef = useRef(null); const [sortConfig, setSortConfig] = useState<{ key: string | number; direction: 'asc' | 'desc' | null }>({ key: null, direction: null }); const [isSearchOpen, setIsSearchOpen] = useState(false); const [selectedSearchItems, setSelectedSearchItems] = useState>(new Set()); const searchDropdownRef = useRef(null); // Close search dropdown on click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (searchDropdownRef.current && !searchDropdownRef.current.contains(event.target as Node)) { setIsSearchOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const searchSuggestions = useMemo(() => { if (!search && selectedSearchItems.size === 0) return data.slice(0, 50); if (!search) return []; const s = search.toLowerCase(); return data.filter(r => String(r[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || String(r[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) ).slice(0, 50); }, [data, search, selectedSearchItems]); const handleSearchItemClick = (index: number) => { const newSelected = new Set(selectedSearchItems); if (newSelected.has(index)) { newSelected.delete(index); } else { newSelected.add(index); } setSelectedSearchItems(newSelected); }; const handleApplySearch = () => { if (selectedSearchItems.size > 0) { setSearchMode('selected'); } setIsSearchOpen(false); }; const handleClearSearch = () => { setSelectedSearchItems(new Set()); setSearchMode('all'); }; const handleResizeStart = (e: React.MouseEvent, colKey: string, currentWidth: number) => { console.log('Resize started for:', colKey, 'at x:', e.clientX); e.preventDefault(); e.stopPropagation(); setResizingColumn(colKey); setResizeStartX(e.clientX); setResizeStartWidth(currentWidth); }; React.useEffect(() => { if (!resizingColumn || resizeStartX === null || resizeStartWidth === null) return; const handleMouseMove = (e: MouseEvent) => { const delta = e.clientX - resizeStartX; const newWidth = Math.max(40, Math.min(800, resizeStartWidth + delta)); setColumnWidths(prev => ({ ...prev, [resizingColumn]: newWidth })); }; const handleMouseUp = () => { setResizingColumn(null); setResizeStartX(null); setResizeStartWidth(null); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [resizingColumn, resizeStartX, resizeStartWidth]); // ── Dynamic column detection ────────────────────────────────────────────── const { uvpIdx, srpCols, containerCols } = useMemo(() => { const uvpIdx = findCol(headers, 'uvp'); // All SRP columns, sorted: INT first, UK second, then alphabetically const srp: DetectedCol[] = headers .map((h, i) => ({ index: i, name: h || '' })) .filter(({ name }) => /srp/i.test(name)) .sort((a, b) => { const order = (n: string) => { if (/int/i.test(n)) return 0; if (/uk/i.test(n)) return 1; return 2; }; return order(a.name) - order(b.name); }); // All 40' container columns (HC, HQ, etc.) const container: DetectedCol[] = headers .map((h, i) => ({ index: i, name: h || '' })) .filter(({ name }) => /40/i.test(name) && /h[cq]/i.test(name)); return { uvpIdx, srpCols: srp, containerCols: container }; }, [headers]); // ── Error analysis per row ──────────────────────────────────────────────── const analyzedRows = useMemo(() => { return data.map((row, dataIndex) => { const pricingErrors: string[] = []; const unitErrors: string[] = []; // UVP check if (uvpIdx >= 0) { const v = row[uvpIdx]; if (v === undefined || v === null || v === '' || Number(v) === 0) { pricingErrors.push('UVP missing'); } } // SRP checks srpCols.forEach(({ index, name }) => { const v = row[index]; if (v === undefined || v === null || v === '' || Number(v) === 0) { pricingErrors.push(`${name} missing`); } }); // Units per Outer check const unitsOuter = Number(row[COLUMNS.UNITS_OUTER]); if (!unitsOuter || unitsOuter === 0) { unitErrors.push('Units/Outer: missing'); } // 40' container checks containerCols.forEach(({ index, name }) => { const v = Number(row[index]); if (!v || v === 0) unitErrors.push(`${name}: empty`); else if (v === 1) unitErrors.push(`${name}: value is 1`); }); return { row, dataIndex, pricingErrors, unitErrors, hasErrors: pricingErrors.length > 0 || unitErrors.length > 0, isCritical: unitErrors.length > 0, }; }); }, [data, uvpIdx, srpCols, containerCols]); // ── Stats ───────────────────────────────────────────────────────────────── const stats = useMemo(() => { const withPricing = analyzedRows.filter(r => r.pricingErrors.length > 0).length; const withUnits = analyzedRows.filter(r => r.unitErrors.length > 0).length; const withAny = analyzedRows.filter(r => r.hasErrors).length; const allOk = analyzedRows.length - withAny; 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]); const uniqueUnitsOuter = useMemo(() => Array.from(new Set(data.map(r => String(r[COLUMNS.UNITS_OUTER] || '')))).filter(v => v).sort(), [data]); const uniqueOuterW = useMemo(() => Array.from(new Set(data.map(r => String(r[COLUMNS.OUTER_W] || '')))).filter(v => v).sort((a, b) => Number(a) - Number(b)), [data]); const uniqueOuterL = useMemo(() => Array.from(new Set(data.map(r => String(r[COLUMNS.OUTER_L] || '')))).filter(v => v).sort((a, b) => Number(a) - Number(b)), [data]); const uniqueOuterH = useMemo(() => Array.from(new Set(data.map(r => String(r[COLUMNS.OUTER_H] || '')))).filter(v => v).sort((a, b) => Number(a) - Number(b)), [data]); // ── Filtered rows ───────────────────────────────────────────────────────── const filteredRows = useMemo(() => { let result = analyzedRows; // Mode filter switch (filterMode) { case 'all_errors': result = analyzedRows.filter(r => r.hasErrors); break; case 'pricing_errors': result = analyzedRows.filter(r => r.pricingErrors.length > 0); break; case 'units_errors': result = analyzedRows.filter(r => r.unitErrors.length > 0); break; } // Global search (SKU + Name) 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) ); } // Selected items from search dropdown if (searchMode === 'selected' && selectedSearchItems.size > 0) { result = result.filter(r => { const articleNo = String(r.row[COLUMNS.ARTICLE_NO] || ''); return selectedSearchItems.has(articleNo); }); } // 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] || ''))); } if (unitsOuterFilter.length > 0) { result = result.filter(r => unitsOuterFilter.includes(String(r.row[COLUMNS.UNITS_OUTER] || ''))); } if (outerWFilter.length > 0) { result = result.filter(r => outerWFilter.includes(String(r.row[COLUMNS.OUTER_W] || ''))); } if (outerLFilter.length > 0) { result = result.filter(r => outerLFilter.includes(String(r.row[COLUMNS.OUTER_L] || ''))); } if (outerHFilter.length > 0) { result = result.filter(r => outerHFilter.includes(String(r.row[COLUMNS.OUTER_H] || ''))); } // Dynamic column filters (SRP, container) (Object.keys(dynamicColFilters) as string[]).forEach(colIdx => { const filterVals = dynamicColFilters[Number(colIdx)]; if (filterVals && filterVals.length > 0) { const col = Number(colIdx); result = result.filter(r => filterVals.includes(String(r.row[col] || ''))); } }); return result; }, [analyzedRows, filterMode, search, nameColFilter, lineMultiFilter, classificationFilter, unitsOuterFilter, outerWFilter, outerLFilter, outerHFilter, dynamicColFilters]); // ── Sorted rows ────────────────────────────────────────────────────────── const sortedRows = useMemo(() => { if (sortConfig.key === null || sortConfig.direction === null) return filteredRows; const { key, direction } = sortConfig; let colIndex: number; if (typeof key === 'number') { colIndex = key; } else { // Map string keys to COLUMNS indices switch (key) { case 'articleNo': colIndex = COLUMNS.ARTICLE_NO; break; case 'articleName': colIndex = COLUMNS.ARTICLE_NAME; break; case 'line': colIndex = COLUMNS.LINE; break; case 'classification': colIndex = COLUMNS.CLASSIFICATION; break; case 'productType': colIndex = COLUMNS.PRODUCT_TYPE; break; case 'unitsOuter': colIndex = COLUMNS.UNITS_OUTER; break; case 'outerW': colIndex = COLUMNS.OUTER_W; break; case 'outerL': colIndex = COLUMNS.OUTER_L; break; case 'outerH': colIndex = COLUMNS.OUTER_H; break; default: return filteredRows; } } const sorted = [...filteredRows].sort((a, b) => { const valA = a.row[colIndex]; const valB = b.row[colIndex]; // Handle null/undefined if (valA === valB) return 0; if (valA === null || valA === undefined || valA === '') return 1; if (valB === null || valB === undefined || valB === '') return -1; // Handle numeric comparison const numA = typeof valA === 'number' ? valA : parseFloat(String(valA).replace(',', '.')); const numB = typeof valB === 'number' ? valB : parseFloat(String(valB).replace(',', '.')); if (!isNaN(numA) && !isNaN(numB)) { return direction === 'asc' ? numA - numB : numB - numA; } // Handle string comparison const strA = String(valA).toLowerCase(); const strB = String(valB).toLowerCase(); if (strA < strB) return direction === 'asc' ? -1 : 1; if (strA > strB) return direction === 'asc' ? 1 : -1; return 0; }); return sorted; }, [filteredRows, sortConfig]); // ── Pagination ───────────────────────────────────────────────────────── const paginatedRows = useMemo(() => { const start = (currentPage - 1) * pageSize; return sortedRows.slice(start, start + pageSize); }, [sortedRows, currentPage]); const totalPages = Math.ceil(sortedRows.length / pageSize); const handleSort = (key: string | number) => { setSortConfig(prev => { if (prev.key === key) { if (prev.direction === 'asc') return { key, direction: 'desc' }; return { key: null, direction: null }; } return { key, direction: 'asc' }; }); }; // ── Inline edit helpers ─────────────────────────────────────────────────── const startEdit = (rowIndex: number, colIndex: number, currentValue: string) => { setEditingCell({ rowIndex, colIndex, value: currentValue }); setTimeout(() => inputRef.current?.focus(), 0); }; const commitEdit = useCallback(async () => { if (!editingCell) return; const { rowIndex, colIndex, value } = editingCell; const original = data[rowIndex]; const newRow = [...original]; newRow[colIndex] = value; setSavingCell({ rowIndex, colIndex }); setEditingCell(null); onCaptureState(`Updated pricing for ${original[COLUMNS.ARTICLE_NO]}`); await onSaveRow(rowIndex, newRow); setSavingCell(null); }, [editingCell, data, onSaveRow, onCaptureState]); const cancelEdit = () => setEditingCell(null); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') commitEdit(); if (e.key === 'Escape') cancelEdit(); }; // ── All unique product types (for autocomplete) ─────────────────────────── const allProductTypes = useMemo(() => Array.from(new Set(data.map(r => String(r[COLUMNS.PRODUCT_TYPE] || '')).filter(Boolean))).sort() , [data]); const startEditType = (rowIndex: number, currentValue: string) => { setEditingType({ rowIndex, value: currentValue }); setTypeSuggestions(allProductTypes); setTimeout(() => typeInputRef.current?.focus(), 0); }; const commitTypeEdit = useCallback(async (rowIndex: number, value: string) => { setEditingType(null); setTypeSuggestions([]); const original = data[rowIndex]; const newRow = [...original]; newRow[COLUMNS.PRODUCT_TYPE] = value; onCaptureState(`Updated type for ${original[COLUMNS.ARTICLE_NO]}`); await onSaveRow(rowIndex, newRow); }, [data, onSaveRow, onCaptureState]); const handleTypeInputChange = (value: string) => { setEditingType(prev => prev ? { ...prev, value } : null); const lower = value.toLowerCase(); setTypeSuggestions( lower ? allProductTypes.filter(t => t.toLowerCase().includes(lower)) : allProductTypes ); }; // ── Column helpers ──────────────────────────────────────────────────────── const pricingEditableCols: DetectedCol[] = [ ...(uvpIdx >= 0 ? [{ index: uvpIdx, name: headers[uvpIdx] || 'UVP' }] : []), ...srpCols, ]; // ═ Unique values for dynamic pricing columns ═════════════════════════════ const dynamicColUniqueValues = useMemo(() => { const result: Record = {}; pricingEditableCols.forEach(col => { result[col.index] = Array.from(new Set(data.map(r => String(r[col.index] || '')))).filter(v => v).sort(); }); containerCols.forEach(col => { result[col.index] = Array.from(new Set(data.map(r => String(r[col.index] || '')))).filter(v => v).sort(); }); return result; }, [data, uvpIdx, srpCols, containerCols, headers]); const formatPrice = (val: any) => { if (val === undefined || val === null || val === '') return ''; const n = parseFloat(String(val).replace(',', '.')); return isNaN(n) ? String(val) : n.toFixed(2); }; const formatUnits = (val: any) => { if (val === undefined || val === null || val === '') return '—'; return String(val); }; const unitBadge = (val: any, label: string) => { const n = Number(val); if (!val || n === 0) return ( 0 ); if (n === 1) return ( 1 ); return ( {n.toLocaleString()} ); }; const unitOuterBadge = (val: any) => { const n = Number(val); if (!val || n === 0) return ( Missing ); return ( {n} ); }; // ── Matrix columns (the rest) ───────────────────────────────────────────── const displayedIndices = useMemo(() => { const set = new Set([ COLUMNS.ARTICLE_NO, COLUMNS.ARTICLE_NAME, COLUMNS.LINE, COLUMNS.CLASSIFICATION, COLUMNS.UNITS_OUTER, COLUMNS.OUTER_W, COLUMNS.OUTER_L, COLUMNS.OUTER_H, ]); if (uvpIdx >= 0) set.add(uvpIdx); srpCols.forEach(c => set.add(c.index)); containerCols.forEach(c => set.add(c.index)); return set; }, [uvpIdx, srpCols, containerCols]); const matrixCols = useMemo(() => { return headers .map((h, i) => ({ index: i, name: h || '' })) .filter(({ index }) => !displayedIndices.has(index)) .filter(({ name }) => name.trim() !== ''); // Skip empty headers }, [headers, displayedIndices]); const [noteEditor, setNoteEditor] = useState<{ rowIndex: number; text: string } | null>(null); const handleToggleCheck = async (rowIndex: number, checked: boolean) => { const original = data[rowIndex]; const newRow = [...original]; newRow[COLUMNS.VALIDATED_CHECK] = checked; onCaptureState(`${checked ? 'Checked' : 'Unchecked'} ${original[COLUMNS.ARTICLE_NO]}`); await onSaveRow(rowIndex, newRow); }; const saveNote = async () => { if (!noteEditor) return; const { rowIndex, text } = noteEditor; const original = data[rowIndex]; const newRow = [...original]; newRow[COLUMNS.VALIDATED_NOTE] = text; setNoteEditor(null); onCaptureState(`Updated note for ${original[COLUMNS.ARTICLE_NO]}`); await onSaveRow(rowIndex, newRow); }; // ── Column detection notice ─────────────────────────────────────────────── const missingCols: string[] = []; if (uvpIdx < 0) missingCols.push('UVP'); if (srpCols.length === 0) missingCols.push('SRP'); if (containerCols.length === 0) missingCols.push('Units/40\''); const FILTERS: { id: FilterMode; label: string; count: number; color: string }[] = [ { id: 'all', label: 'All Products', count: stats.total, color: 'text-slate-300' }, { id: 'all_errors', label: 'All Issues', count: stats.withAny, color: 'text-red-400' }, { id: 'pricing_errors', label: 'Pricing Issues', count: stats.withPricing, color: 'text-amber-400' }, { id: 'units_errors', label: 'Units Issues', count: stats.withUnits, color: 'text-orange-400' }, ]; return (
{/* ── Note Editor Modal ── */} {noteEditor && (

Validation Note

{data[noteEditor.rowIndex][COLUMNS.ARTICLE_NO]}