import React, { useState, useMemo, useRef, useCallback } from 'react'; import { ExcelRow, COLUMNS } from '../types'; import { AlertTriangle, AlertCircle, CheckCircle2, DollarSign, Package, Save, X, ChevronDown, Edit2, Filter, Check, Search, } 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 [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(() => { 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]); // ── 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) ); } // 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, nameColFilter, lineMultiFilter, classificationFilter]); // ── 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(); }; // ── Column helpers ──────────────────────────────────────────────────────── const pricingEditableCols: DetectedCol[] = [ ...(uvpIdx >= 0 ? [{ index: uvpIdx, name: headers[uvpIdx] || 'UVP' }] : []), ...srpCols, ]; 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} ); }; // ── 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 (
{/* ── Search bar ── */}
setSearch(e.target.value)} className="w-full pl-4 pr-10 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all" /> {search ? ( ) : (
)}
{/* ── Filter tabs ── */}
{FILTERS.map(f => ( ))}
{/* ── Column detection warning ── */} {missingCols.length > 0 && (
Could not auto-detect columns for: {missingCols.join(', ')}. Check that the Excel headers contain "UVP", "SRP", or "40HC"/"40HQ".
)} {/* ── Stat cards ── */}
} color="slate" /> } color="amber" /> } color="red" /> } color="emerald" />
{/* ── Table ── */}
{filteredRows.length === 0 ? (

No issues found

All products are correctly configured.

) : ( {/* Article Name with text filter */} {/* Line with multi-select filter */} {/* Classification with multi-select filter */} {/* Editable pricing columns */} {pricingEditableCols.map(col => ( ))} {/* Units/Outer */} {/* Container columns */} {containerCols.map(col => ( ))} {/* Status */} {/* Actions */} {filteredRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => { const hasAnyError = pricingErrors.length > 0 || unitErrors.length > 0; const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])]; return ( 0 ? 'bg-amber-950/10' : '' )} > {/* Article No */} {/* Article Name */} {/* Line */} {/* Classification */} {/* Editable pricing cells */} {pricingEditableCols.map(col => { const isEditing = editingCell?.rowIndex === dataIndex && editingCell?.colIndex === col.index; const isSaving = savingCell?.rowIndex === dataIndex && savingCell?.colIndex === col.index; const val = formatPrice(row[col.index]); const isEmpty = !row[col.index] || row[col.index] === '' || Number(row[col.index]) === 0; return ( ); })} {/* Units/Outer */} {/* Container unit columns */} {containerCols.map(col => ( ))} {/* Status badge */} {/* Edit button */} ); })}
Art. No.
Article Name
{openFilter === 'name' && ( setOpenFilter(null)} /> )}
Line
{openFilter === 'line' && ( setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} onSelectAll={vals => setLineMultiFilter(vals)} onClear={() => { setLineMultiFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} /> )}
Classification
{openFilter === 'classification' && ( setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} onSelectAll={vals => setClassificationFilter(vals)} onClear={() => { setClassificationFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} /> )}
{col.name} Units/Outer {col.name} Status
{row[COLUMNS.ARTICLE_NO]} {row[COLUMNS.ARTICLE_NAME] || '—'} {row[COLUMNS.LINE] || '—'} {row[COLUMNS.CLASSIFICATION] || '—'} {isEditing ? (
setEditingCell(prev => prev ? { ...prev, value: e.target.value } : null)} onKeyDown={handleKeyDown} onBlur={commitEdit} className="w-24 bg-slate-900 border border-blue-500 rounded px-2 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500" />
) : ( )}
{unitOuterBadge(row[COLUMNS.UNITS_OUTER])} {unitBadge(row[col.index], col.name)} {!hasAnyError ? ( OK ) : isCritical ? (
Critical {unitErrors.map((e, i) => ( {e} ))}
) : (
Incomplete {pricingErrors.map((e, i) => ( {e} ))}
)}
)}
{/* ── Footer count ── */}

Showing {filteredRows.length} of {stats.total} products {pricingEditableCols.length > 0 && ( <> · Click any price cell to edit inline )}

); } // ── 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" />
); } // ── Stat card ───────────────────────────────────────────────────────────────── function StatCard({ label, value, icon, color, }: { label: string; value: number; icon: React.ReactNode; color: 'slate' | 'amber' | 'red' | 'emerald'; }) { const colors = { slate: { bg: 'bg-slate-800', border: 'border-slate-700', text: 'text-slate-200', icon: 'text-slate-400' }, amber: { bg: 'bg-amber-950/30', border: 'border-amber-700/40', text: 'text-amber-300', icon: 'text-amber-500' }, red: { bg: 'bg-red-950/30', border: 'border-red-700/40', text: 'text-red-300', icon: 'text-red-500' }, emerald: { bg: 'bg-emerald-950/20', border: 'border-emerald-700/30', text: 'text-emerald-300', icon: 'text-emerald-500' }, }; const c = colors[color]; return (
{icon}

{value}

{label}

); }