feat(pricing): add EMCO Compliant column next to CategorizationCode

Read-only Yes/No badge sourced from the matrix EmpCO_Compliant column
(detected dynamically by header; shows '-' until the new matrix layout
is live in Dropbox). Sortable, pinnable and resizable like the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-07-08 16:36:13 +02:00
co-authored by Claude Fable 5
parent 0d50fce790
commit 0c152a4742
+38 -7
View File
@@ -70,6 +70,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
line: 100, line: 100,
classification: 130, classification: 130,
productType: 140, productType: 140,
empco: 110,
itemToLogistic: 160, itemToLogistic: 160,
unitsOuter: 100, unitsOuter: 100,
outerW: 80, outerW: 80,
@@ -244,7 +245,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
}, [resizingColumn, resizeStartX, resizeStartWidth]); }, [resizingColumn, resizeStartX, resizeStartWidth]);
// ── Dynamic column detection ────────────────────────────────────────────── // ── Dynamic column detection ──────────────────────────────────────────────
const { uvpIdx, srpCols, containerCols, nwIdx, gwIdx, unitsOuterIdx, units40fIdx } = useMemo(() => { const { uvpIdx, srpCols, containerCols, nwIdx, gwIdx, unitsOuterIdx, units40fIdx, empcoIdx } = useMemo(() => {
const uvpIdx = findCol(headers, 'uvp'); const uvpIdx = findCol(headers, 'uvp');
// All SRP columns, sorted: INT first, UK second, then alphabetically // All SRP columns, sorted: INT first, UK second, then alphabetically
@@ -273,7 +274,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const units40fIdx = findCol(headers, '40f'); const units40fIdx = findCol(headers, '40f');
return { uvpIdx, srpCols: srp, containerCols: container, nwIdx, gwIdx, unitsOuterIdx, units40fIdx }; // EmpCO_Compliant column from the matrix file (empty until the new layout is live)
const empcoIdx = findCol(headers, 'empco');
return { uvpIdx, srpCols: srp, containerCols: container, nwIdx, gwIdx, unitsOuterIdx, units40fIdx, empcoIdx };
}, [headers, COLUMNS]); }, [headers, COLUMNS]);
useEffect(() => { useEffect(() => {
@@ -616,6 +620,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
case 'line': colIndex = COLUMNS.LINE; break; case 'line': colIndex = COLUMNS.LINE; break;
case 'classification': colIndex = COLUMNS.CLASSIFICATION; break; case 'classification': colIndex = COLUMNS.CLASSIFICATION; break;
case 'productType': colIndex = COLUMNS.CATEGORIZATION_CODE; break; case 'productType': colIndex = COLUMNS.CATEGORIZATION_CODE; break;
case 'empco': colIndex = empcoIdx; break;
case 'unitsOuter': colIndex = unitsOuterIdx; break; case 'unitsOuter': colIndex = unitsOuterIdx; break;
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;
@@ -761,6 +766,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{ key: 'line', label: 'Line', index: COLUMNS.LINE }, { key: 'line', label: 'Line', index: COLUMNS.LINE },
{ key: 'classification', label: 'Classification', index: COLUMNS.CLASSIFICATION }, { key: 'classification', label: 'Classification', index: COLUMNS.CLASSIFICATION },
{ key: 'productType', label: 'CategorizationCode', index: COLUMNS.CATEGORIZATION_CODE }, { key: 'productType', label: 'CategorizationCode', index: COLUMNS.CATEGORIZATION_CODE },
{ key: 'empco', label: 'EMCO Compliant', index: empcoIdx },
{ key: 'itemToLogistic', label: 'Item to Logistic', index: COLUMNS.ITEM_TO_LOGISTIC }, { key: 'itemToLogistic', label: 'Item to Logistic', index: COLUMNS.ITEM_TO_LOGISTIC },
{ key: 'unitsOuter', label: 'Units/Outer', index: unitsOuterIdx }, { key: 'unitsOuter', label: 'Units/Outer', index: unitsOuterIdx },
{ key: 'outerW', label: 'Outer W', index: COLUMNS.OUTER_W }, { key: 'outerW', label: 'Outer W', index: COLUMNS.OUTER_W },
@@ -775,7 +781,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
cols.push({ key: `con_${col.index}`, label: col.name, index: col.index }); cols.push({ key: `con_${col.index}`, label: col.name, index: col.index });
}); });
return cols; return cols;
}, [COLUMNS, unitsOuterIdx, pricingEditableCols, containerCols]); }, [COLUMNS, unitsOuterIdx, empcoIdx, pricingEditableCols, containerCols]);
const togglePinnedColumn = (key: string) => { const togglePinnedColumn = (key: string) => {
setPinnedColumns(prev => { setPinnedColumns(prev => {
@@ -791,7 +797,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
// Calculate sticky left position for each pinned column // Calculate sticky left position for each pinned column
const getStickyLeft = useMemo(() => { const getStickyLeft = useMemo(() => {
const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq']; const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'empco', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq'];
const dynamicOrder: string[] = []; const dynamicOrder: string[] = [];
pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`)); pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`));
containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`)); containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`));
@@ -816,7 +822,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
// Calculate z-index for sticky columns (so they stack correctly) // Calculate z-index for sticky columns (so they stack correctly)
const getStickyRank = useMemo(() => { const getStickyRank = useMemo(() => {
const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq']; const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'empco', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq'];
const dynamicOrder: string[] = []; const dynamicOrder: string[] = [];
pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`)); pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`));
containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`)); containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`));
@@ -1545,8 +1551,19 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
</th> </th>
<th style={{ <th style={{ width: columnWidths.empco, ...(isPinned('empco') ? { left: getStickyLeft('empco') ?? 0, zIndex: getHeaderStickyRank('empco') ?? 0 } : {}) }} className={cn(
width: columnWidths.itemToLogistic, "text-left px-3 py-3 text-xs font-semibold text-teal-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('empco') && "sticky bg-slate-900"
)} onClick={() => handleSort('empco')}>
<span className="flex items-center gap-1">
{isPinned('empco') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
EMCO Compliant <SortIcon current={sortConfig.key === 'empco' ? sortConfig.direction : null} />
</span>
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'empco', columnWidths.empco); }} />
</th>
<th style={{
width: columnWidths.itemToLogistic,
...(isPinned('itemToLogistic') ...(isPinned('itemToLogistic')
? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: openFilter === 'logistic' ? 500 : (getHeaderStickyRank('itemToLogistic') ?? 0) } ? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: openFilter === 'logistic' ? 500 : (getHeaderStickyRank('itemToLogistic') ?? 0) }
: (openFilter === 'logistic' ? { zIndex: 500, position: 'relative' } : {})) : (openFilter === 'logistic' ? { zIndex: 500, position: 'relative' } : {}))
@@ -2111,6 +2128,20 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
</td> </td>
{/* EMCO Compliant cell (read-only, from matrix file) */}
<td className={cn("px-3 py-2 relative", isPinned('empco') && "sticky bg-slate-800")} style={isPinned('empco') ? { left: getStickyLeft('empco') ?? 0, zIndex: getStickyRank('empco') ?? 0 } : {}}>
{(() => {
const raw = empcoIdx >= 0 ? String(row[empcoIdx] ?? '').trim().toLowerCase() : '';
if (raw === 'true' || raw === 'yes') {
return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-teal-500/10 text-teal-300 border border-teal-500/30">Yes</span>;
}
if (raw === 'false' || raw === 'no') {
return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-500/10 text-red-300 border border-red-500/30">No</span>;
}
return <span className="text-xs text-slate-500">-</span>;
})()}
</td>
{/* ITEM TO LOGISTIC cell */} {/* ITEM TO LOGISTIC cell */}
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('itemToLogistic') && "sticky bg-slate-800")} style={isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: getStickyRank('itemToLogistic') ?? 0 } : {}}> <td className={cn("px-3 py-2 overflow-visible relative", isPinned('itemToLogistic') && "sticky bg-slate-800")} style={isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: getStickyRank('itemToLogistic') ?? 0 } : {}}>
{editingLogistic?.rowIndex === dataIndex ? ( {editingLogistic?.rowIndex === dataIndex ? (