import React, { useState, useMemo, useRef, useCallback } from 'react'; import { ExcelRow, COLUMNS } from '../types'; import { AlertTriangle, AlertCircle, CheckCircle2, DollarSign, Package, Save, X, ChevronDown, Edit2, } from 'lucide-react'; import { cn } from '../lib/utils'; interface PricingViewProps { data: ExcelRow[]; headers: string[]; onSaveRow: (index: number, updatedRow: ExcelRow) => void; onCaptureState: (message: string) => void; onEdit: (index: number) => void; } 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 }: PricingViewProps) { const [filterMode, setFilterMode] = useState('all_errors'); const [editingCell, setEditingCell] = useState(null); const [savingCell, setSavingCell] = useState<{ rowIndex: number; colIndex: number } | null>(null); const inputRef = useRef(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]); // ── Filtered rows ───────────────────────────────────────────────────────── const filteredRows = useMemo(() => { switch (filterMode) { case 'all_errors': return analyzedRows.filter(r => r.hasErrors); case 'pricing_errors': return analyzedRows.filter(r => r.pricingErrors.length > 0); case 'units_errors': return analyzedRows.filter(r => r.unitErrors.length > 0); default: return analyzedRows; } }, [analyzedRows, filterMode]); // ── 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 (
{/* ── 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" />
{/* ── Filter tabs ── */}
{FILTERS.map(f => ( ))}
{/* ── Table ── */}
{filteredRows.length === 0 ? (

No issues found

All products are correctly configured.

) : ( {/* 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; return ( 0 ? 'bg-amber-950/10' : '' )} > {/* Article No */} {/* Article Name */} {/* Line */} {/* 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 Line {col.name} Units/Outer {col.name} Status
{row[COLUMNS.ARTICLE_NO]} {row[COLUMNS.ARTICLE_NAME] || '—'} {row[COLUMNS.LINE] || '—'} {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 )}

); } // ── 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}

); }