feat: add column pinning (freeze) functionality in Pricing Units tab

- Add Pin Columns button to select columns to freeze
- Implement sticky columns with left positioning for pinned columns
- Support pinning dynamic pricing and container columns
- Add Pin icon indicator on pinned columns
- Include reset and unpin all options in pin panel
This commit is contained in:
Christian Vidal Wolf
2026-04-24 13:23:18 +02:00
parent b8a960e3e5
commit e6207dff64
3 changed files with 278 additions and 47 deletions
+262 -39
View File
@@ -16,6 +16,8 @@ import {
Search,
MessageSquare,
Maximize2,
Pin,
PinOff,
} from 'lucide-react';
import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover';
@@ -101,6 +103,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<string>>(new Set());
const searchDropdownRef = useRef<HTMLDivElement>(null);
// Pinned columns state
const [pinnedColumns, setPinnedColumns] = useState<Set<string>>(new Set(['articleNo', 'articleName']));
const [showPinPanel, setShowPinPanel] = useState(false);
// Close search dropdown on click outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -113,8 +119,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
}, []);
const searchSuggestions = useMemo(() => {
if (!search && selectedSearchItems.size === 0) return data.slice(0, 50);
if (!search) return [];
if (!search) return data.slice(0, 50);
const terms = search.toLowerCase().split(/\s+/).filter(Boolean);
if (terms.length === 0) return data.slice(0, 50);
@@ -559,6 +564,62 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
...srpCols,
];
// ── Pinned columns for freeze ──────────────────────────────────────────────
const allPinnableColumns = useMemo(() => {
const cols: { key: string; label: string; index: number }[] = [
{ key: 'articleNo', label: 'Article No', index: COLUMNS.ARTICLE_NO },
{ key: 'articleName', label: 'Article Name', index: COLUMNS.ARTICLE_NAME },
{ key: 'line', label: 'Line', index: COLUMNS.LINE },
{ key: 'classification', label: 'Classification', index: COLUMNS.CLASSIFICATION },
{ key: 'productType', label: 'Type', index: COLUMNS.PRODUCT_TYPE },
{ key: 'itemToLogistic', label: 'Item to Logistic', index: COLUMNS.ITEM_TO_LOGISTIC },
{ key: 'unitsOuter', label: 'Units/Outer', index: unitsOuterIdx },
{ 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 },
];
pricingEditableCols.forEach(col => {
cols.push({ key: `prc_${col.index}`, label: col.name, index: col.index });
});
containerCols.forEach(col => {
cols.push({ key: `con_${col.index}`, label: col.name, index: col.index });
});
return cols;
}, [COLUMNS, unitsOuterIdx, pricingEditableCols, containerCols]);
const togglePinnedColumn = (key: string) => {
setPinnedColumns(prev => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
};
// Column order for sticky positioning
const columnOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH'];
// Calculate sticky left position for each pinned column
const getStickyLeft = useMemo(() => {
const pinnedOrder = columnOrder.filter(k => pinnedColumns.has(k));
return (key: string): number | null => {
if (!pinnedColumns.has(key)) return null;
const idx = pinnedOrder.indexOf(key);
if (idx === -1) return null;
let left = 0;
for (let i = 0; i < idx; i++) {
const k = pinnedOrder[i];
left += columnWidths[k] || 100;
}
return left;
};
}, [pinnedColumns, columnOrder, columnWidths]);
const isPinned = (key: string) => pinnedColumns.has(key);
// ═ Unique values for dynamic pricing columns ═════════════════════════════
const dynamicColUniqueValues = useMemo(() => {
const result: Record<number, string[]> = {};
@@ -844,6 +905,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
return (
<button
key={`${row[COLUMNS.ARTICLE_NO]}-${idx}`}
onMouseDown={(e) => e.preventDefault()}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -957,6 +1019,95 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</button>
</div>
{/* ── Pin columns control ── */}
<div className="flex items-center gap-3 bg-slate-800/60 border border-slate-700/50 rounded-xl p-3">
<div className="relative">
<button
onClick={() => setShowPinPanel(!showPinPanel)}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
pinnedColumns.size > 0
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
)}
>
<Pin className="w-4 h-4" />
<span>Pin Columns</span>
{pinnedColumns.size > 0 && (
<span className="bg-white/20 px-1.5 py-0.5 rounded text-xs font-bold">{pinnedColumns.size}</span>
)}
</button>
{showPinPanel && (
<div className="absolute top-full left-0 mt-2 w-72 bg-slate-800 border border-slate-700 rounded-xl shadow-2xl z-50 overflow-hidden animate-in fade-in zoom-in-95 duration-100">
<div className="p-3 border-b border-slate-700 bg-slate-900/50">
<p className="text-xs font-bold text-slate-300">Select columns to pin (freeze)</p>
</div>
<div className="max-h-64 overflow-y-auto p-2 grid grid-cols-2 gap-1">
{allPinnableColumns.map(col => (
<button
key={col.key}
onClick={() => togglePinnedColumn(col.key)}
className={cn(
'flex items-center gap-2 px-3 py-2 rounded-lg text-xs transition-all text-left',
pinnedColumns.has(col.key)
? 'bg-blue-600/20 text-blue-400 border border-blue-500/40'
: 'bg-slate-700/50 text-slate-300 hover:bg-slate-700 border border-transparent'
)}
>
<div className={cn(
'w-4 h-4 rounded border flex items-center justify-center shrink-0',
pinnedColumns.has(col.key)
? 'bg-blue-600 border-blue-600'
: 'border-slate-600'
)}>
{pinnedColumns.has(col.key) && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="truncate">{col.label}</span>
</button>
))}
</div>
<div className="p-2 border-t border-slate-700 bg-slate-900/50 flex gap-2">
<button
onClick={() => setPinnedColumns(new Set(['articleNo', 'articleName']))}
className="flex-1 px-3 py-1.5 text-xs text-slate-400 hover:text-white hover:bg-slate-700 rounded transition-colors"
>
Reset
</button>
<button
onClick={() => setPinnedColumns(new Set())}
className="flex-1 px-3 py-1.5 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded transition-colors"
>
Unpin All
</button>
</div>
</div>
)}
</div>
<div className="flex items-center gap-2 text-xs text-slate-500">
<PinOff className="w-3.5 h-3.5" />
<span>Pinned:</span>
{pinnedColumns.size === 0 ? (
<span className="text-slate-600">None</span>
) : (
<div className="flex flex-wrap gap-1">
{Array.from(pinnedColumns).map(key => {
const col = allPinnableColumns.find(c => c.key === key);
return col ? (
<span key={key} className="bg-blue-500/10 text-blue-400 px-2 py-0.5 rounded border border-blue-500/20">
{col.label}
</span>
) : null;
})}
</div>
)}
</div>
</div>
{/* ── Table ── */}
<div className="flex-1 overflow-auto bg-slate-800 rounded-xl border border-slate-700 shadow-xl">
{sortedRows.length === 0 ? (
@@ -969,9 +1120,15 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
<thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
<tr>
<th style={{ width: columnWidths.articleNo }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('articleNo')}>
<th style={{ width: columnWidths.articleNo, ...(isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('articleNo') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('articleNo')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Article No <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('articleNo') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Article No <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} />
</span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }}
className={cn(
@@ -991,9 +1148,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleNo', columnWidths.articleNo); }} />
</th>
<th style={{ width: columnWidths.articleName }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('articleName')}>
<th style={{ width: columnWidths.articleName, ...(isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('articleName') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('articleName')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('articleName') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
className={cn(
@@ -1013,9 +1175,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleName', columnWidths.articleName); }} />
</th>
<th style={{ width: columnWidths.line }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('line')}>
<th style={{ width: columnWidths.line, ...(isPinned('line') ? { left: getStickyLeft('line') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('line') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('line')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('line') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
className={cn(
@@ -1038,9 +1205,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'line', columnWidths.line); }} />
</th>
<th style={{ width: columnWidths.classification }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('classification')}>
<th style={{ width: columnWidths.classification, ...(isPinned('classification') ? { left: getStickyLeft('classification') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('classification') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('classification')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('classification') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
className={cn(
@@ -1064,8 +1236,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'classification', columnWidths.classification); }} />
</th>
<th style={{ width: columnWidths.productType }} className="text-left px-3 py-3 text-xs font-semibold text-purple-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('productType')}>
<th style={{ width: columnWidths.productType, ...(isPinned('productType') ? { left: getStickyLeft('productType') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-purple-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('productType') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('productType')}>
<span className="flex items-center gap-1">
{isPinned('productType') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} />
<button
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
@@ -1087,8 +1263,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
</th>
<th style={{ width: columnWidths.itemToLogistic }} className="text-left px-3 py-3 text-xs font-semibold text-pink-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('itemToLogistic')}>
<span className="flex items-center gap-1">Item to Logistic <SortIcon current={sortConfig.key === 'itemToLogistic' ? sortConfig.direction : null} /></span>
<th style={{ width: columnWidths.itemToLogistic, ...(isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-pink-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('itemToLogistic') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('itemToLogistic')}>
<span className="flex items-center gap-1">
{isPinned('itemToLogistic') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Item to Logistic <SortIcon current={sortConfig.key === 'itemToLogistic' ? sortConfig.direction : null} /></span>
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'itemToLogistic', columnWidths.itemToLogistic); }} />
</th>
@@ -1096,9 +1277,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const colKey = `prc_${col.index}`;
const width = columnWidths[colKey] ?? 100;
return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort(col.index)}>
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned(colKey) && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort(col.index)}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
<button
onClick={(e) => {
e.stopPropagation();
@@ -1132,9 +1318,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
);
})}
<th style={{ width: columnWidths.unitsOuter }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('unitsOuter')}>
<th style={{ width: columnWidths.unitsOuter, ...(isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('unitsOuter') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('unitsOuter')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('unitsOuter') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
className={cn(
@@ -1158,9 +1349,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} />
</th>
<th style={{ width: columnWidths.outerW }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('outerW')}>
<th style={{ width: columnWidths.outerW, ...(isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerW') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('outerW')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('outerW') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
className={cn(
@@ -1184,9 +1380,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} />
</th>
<th style={{ width: columnWidths.outerL }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('outerL')}>
<th style={{ width: columnWidths.outerL, ...(isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerL') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('outerL')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('outerL') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
className={cn(
@@ -1210,9 +1411,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerL', columnWidths.outerL); }} />
</th>
<th style={{ width: columnWidths.outerH }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort('outerH')}>
<th style={{ width: columnWidths.outerH, ...(isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerH') && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort('outerH')}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned('outerH') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
className={cn(
@@ -1240,9 +1446,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const colKey = `con_${col.index}`;
const width = columnWidths[colKey] ?? 110;
return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort(col.index)}>
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned(colKey) && "sticky bg-slate-900 z-20"
)} onClick={() => handleSort(col.index)}>
<div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
<span className="truncate flex items-center gap-1">
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
<button
onClick={(e) => {
e.stopPropagation();
@@ -1358,24 +1569,24 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
>
{/* Art. No. */}
<td className="px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate">
<td className={cn("px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate", isPinned('articleNo') && "sticky bg-slate-800 z-10")} style={isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0 } : {}}>
{row[COLUMNS.ARTICLE_NO]}
</td>
{/* Article Name */}
<td className="px-3 py-2.5 text-slate-200 max-w-[200px]">
<td className={cn("px-3 py-2.5 text-slate-200 max-w-[200px]", isPinned('articleName') && "sticky bg-slate-800 z-10")} style={isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0 } : {}}>
<span className="line-clamp-1" title={row[COLUMNS.ARTICLE_NAME]}>
{row[COLUMNS.ARTICLE_NAME] || '—'}
</span>
</td>
{/* Line */}
<td className="px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate">
<td className={cn("px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate", isPinned('line') && "sticky bg-slate-800 z-10")} style={isPinned('line') ? { left: getStickyLeft('line') ?? 0 } : {}}>
{row[COLUMNS.LINE] || '—'}
</td>
{/* Classification */}
<td className="px-3 py-2.5 overflow-hidden">
<td className={cn("px-3 py-2.5 overflow-hidden", isPinned('classification') && "sticky bg-slate-800 z-10")} style={isPinned('classification') ? { left: getStickyLeft('classification') ?? 0 } : {}}>
<span className={cn(
'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap inline-block max-w-full truncate',
String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('CORE')
@@ -1389,7 +1600,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</td>
{/* TYPE cell */}
<td className="px-3 py-2 overflow-visible relative">
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('productType') && "sticky bg-slate-800 z-10")} style={isPinned('productType') ? { left: getStickyLeft('productType') ?? 0 } : {}}>
{editingType?.rowIndex === dataIndex ? (
<div className="relative">
<input
@@ -1435,7 +1646,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</td>
{/* ITEM TO LOGISTIC cell */}
<td className="px-3 py-2 overflow-visible relative">
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('itemToLogistic') && "sticky bg-slate-800 z-10")} style={isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0 } : {}}>
{editingLogistic?.rowIndex === dataIndex ? (
<input
ref={logisticInputRef}
@@ -1471,9 +1682,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
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;
const colKey = `prc_${col.index}`;
return (
<td key={col.index} className="px-3 py-2 overflow-hidden">
<td key={col.index} className={cn("px-3 py-2 overflow-hidden", isPinned(colKey) && "sticky bg-slate-800 z-10")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}}>
{isEditing ? (
<div className="flex items-center gap-1">
<input
@@ -1514,17 +1726,28 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
})}
{/* Units columns */}
<td className="px-3 py-2.5">{unitOuterBadge(row[unitsOuterIdx])}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_W] ?? '-'}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_L] ?? '-'}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_H] ?? '-'}</td>
<td className={cn("px-3 py-2.5", isPinned('unitsOuter') && "sticky bg-slate-800 z-10")} style={isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0 } : {}}>
{unitOuterBadge(row[unitsOuterIdx])}
</td>
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerW') && "sticky bg-slate-800 z-10")} style={isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0 } : {}}>
{row[COLUMNS.OUTER_W] ?? '-'}
</td>
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerL') && "sticky bg-slate-800 z-10")} style={isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0 } : {}}>
{row[COLUMNS.OUTER_L] ?? '-'}
</td>
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerH') && "sticky bg-slate-800 z-10")} style={isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0 } : {}}>
{row[COLUMNS.OUTER_H] ?? '-'}
</td>
{/* Container units columns */}
{containerCols.map(col => (
<td key={col.index} className="px-3 py-2.5 overflow-hidden truncate">
{containerCols.map(col => {
const colKey = `con_${col.index}`;
return (
<td key={col.index} className={cn("px-3 py-2.5 overflow-hidden truncate", isPinned(colKey) && "sticky bg-slate-800 z-10")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0 } : {}}>
{unitBadge(row[col.index], col.name)}
</td>
))}
);
})}
{/* Issues column (weight only) */}
<td className="px-3 py-2 overflow-hidden">