feat: add sorting to Pricing Units table

This commit is contained in:
Christian Vidal Wolf
2026-04-21 09:32:15 +02:00
parent f15871f89f
commit 99ad4f7d25
+132 -51
View File
@@ -83,6 +83,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const [editingType, setEditingType] = useState<{ rowIndex: number; value: string } | null>(null); const [editingType, setEditingType] = useState<{ rowIndex: number; value: string } | null>(null);
const [typeSuggestions, setTypeSuggestions] = useState<string[]>([]); const [typeSuggestions, setTypeSuggestions] = useState<string[]>([]);
const typeInputRef = useRef<HTMLInputElement>(null); const typeInputRef = useRef<HTMLInputElement>(null);
const [sortConfig, setSortConfig] = useState<{ key: string | number; direction: 'asc' | 'desc' | null }>({ key: null, direction: null });
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<number>>(new Set()); const [selectedSearchItems, setSelectedSearchItems] = useState<Set<number>>(new Set());
@@ -332,6 +333,70 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
return result; return result;
}, [analyzedRows, filterMode, search, nameColFilter, lineMultiFilter, classificationFilter, unitsOuterFilter, outerWFilter, outerLFilter, outerHFilter, dynamicColFilters]); }, [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]);
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 ─────────────────────────────────────────────────── // ── Inline edit helpers ───────────────────────────────────────────────────
const startEdit = (rowIndex: number, colIndex: number, currentValue: string) => { const startEdit = (rowIndex: number, colIndex: number, currentValue: string) => {
setEditingCell({ rowIndex, colIndex, value: currentValue }); setEditingCell({ rowIndex, colIndex, value: currentValue });
@@ -691,7 +756,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{/* ── Table ── */} {/* ── Table ── */}
<div className="flex-1 overflow-auto bg-slate-800 rounded-xl border border-slate-700 shadow-xl"> <div className="flex-1 overflow-auto bg-slate-800 rounded-xl border border-slate-700 shadow-xl">
{filteredRows.length === 0 ? ( {sortedRows.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-slate-400"> <div className="flex flex-col items-center justify-center h-64 text-slate-400">
<CheckCircle2 className="w-12 h-12 text-emerald-500 mb-3" /> <CheckCircle2 className="w-12 h-12 text-emerald-500 mb-3" />
<p className="text-lg font-medium text-emerald-400">No issues found</p> <p className="text-lg font-medium text-emerald-400">No issues found</p>
@@ -699,19 +764,19 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</div> </div>
) : ( ) : (
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed"> <table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
<thead className="sticky top-0 z-10 bg-slate-900"> <thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
<tr> <tr>
<th style={{ width: columnWidths.articleNo }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap relative"> <th style={{ width: columnWidths.articleNo }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap relative group cursor-pointer hover:bg-slate-800/50 transition-colors" onClick={() => handleSort('articleNo')}>
<div className="flex items-center gap-1"> <div className="flex items-center justify-between gap-1">
<span>Art. No.</span> <span className="flex items-center gap-1">Art. No. <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} /></span>
</div> </div>
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleNo', columnWidths.articleNo)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleNo', columnWidths.articleNo); }} />
</th> </th>
<th style={{ width: columnWidths.articleName }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 group relative"> <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')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">Article Name</span> <span className="truncate flex items-center gap-1">Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
<button <button
onClick={() => setOpenFilter(openFilter === 'name' ? null : 'name')} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
nameColFilter ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' nameColFilter ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
@@ -727,13 +792,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleName', columnWidths.articleName)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleName', columnWidths.articleName); }} />
</th> </th>
<th style={{ width: columnWidths.line }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">Line</span> <span className="truncate flex items-center gap-1">Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
<button <button
onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
lineMultiFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' lineMultiFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
@@ -752,13 +817,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'line', columnWidths.line)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'line', columnWidths.line); }} />
</th> </th>
<th style={{ width: columnWidths.classification }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">Classification</span> <span className="truncate flex items-center gap-1">Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
<button <button
onClick={() => setOpenFilter(openFilter === 'classification' ? null : 'classification')} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
@@ -777,21 +842,21 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'classification', columnWidths.classification)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'classification', columnWidths.classification); }} />
</th> </th>
<th style={{ width: columnWidths.productType }} className="text-left px-3 py-3 text-xs font-semibold text-purple-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap relative"> <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')}>
<span>Type</span> <span className="flex items-center gap-1">Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} /></span>
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'productType', columnWidths.productType)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
</th> </th>
{pricingEditableCols.map(col => { {pricingEditableCols.map(col => {
const colKey = `prc_${col.index}`; const colKey = `prc_${col.index}`;
const width = columnWidths[colKey] ?? 100; const width = columnWidths[colKey] ?? 100;
return ( return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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)}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">{col.name}</span> <span className="truncate flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -820,16 +885,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, colKey, width); }} />
</th> </th>
); );
})} })}
<th style={{ width: columnWidths.unitsOuter }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">Units/Outer</span> <span className="truncate flex items-center gap-1">Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
<button <button
onClick={() => setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter')} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
unitsOuterFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' unitsOuterFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
@@ -848,14 +913,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} />
</th> </th>
<th style={{ width: columnWidths.outerW }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">Outer W</span> <span className="truncate flex items-center gap-1">Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
<button <button
onClick={() => setOpenFilter(openFilter === 'outerW' ? null : 'outerW')} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
outerWFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' outerWFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
@@ -874,14 +939,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerW', columnWidths.outerW)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} />
</th> </th>
<th style={{ width: columnWidths.outerL }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">Outer L</span> <span className="truncate flex items-center gap-1">Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
<button <button
onClick={() => setOpenFilter(openFilter === 'outerL' ? null : 'outerL')} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
outerLFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' outerLFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
@@ -900,14 +965,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerL', columnWidths.outerL)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerL', columnWidths.outerL); }} />
</th> </th>
<th style={{ width: columnWidths.outerH }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">Outer H</span> <span className="truncate flex items-center gap-1">Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
<button <button
onClick={() => setOpenFilter(openFilter === 'outerH' ? null : 'outerH')} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
outerHFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' outerHFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
@@ -926,16 +991,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerH', columnWidths.outerH)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerH', columnWidths.outerH); }} />
</th> </th>
{containerCols.map(col => { {containerCols.map(col => {
const colKey = `con_${col.index}`; const colKey = `con_${col.index}`;
const width = columnWidths[colKey] ?? 110; const width = columnWidths[colKey] ?? 110;
return ( return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative"> <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)}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate">{col.name}</span> <span className="truncate flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -964,34 +1029,34 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, colKey, width); }} />
</th> </th>
); );
})} })}
<th style={{ width: columnWidths.check }} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap relative"> <th style={{ width: columnWidths.check }} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative" onClick={() => handleSort(COLUMNS.VALIDATED_CHECK)}>
Check <span className="flex items-center gap-1">Check <SortIcon current={sortConfig.key === COLUMNS.VALIDATED_CHECK ? sortConfig.direction : null} /></span>
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'check', columnWidths.check)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'check', columnWidths.check); }} />
</th> </th>
{matrixCols.map(col => { {matrixCols.map(col => {
const colKey = `mat_${col.index}`; const colKey = `mat_${col.index}`;
const width = columnWidths[colKey] ?? 120; const width = columnWidths[colKey] ?? 120;
return ( return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative" title={col.name}> <th key={col.index} style={{ width }} 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" title={col.name} onClick={() => handleSort(col.index)}>
<span className="truncate block">{col.name}</span> <span className="truncate flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, colKey, width); }} />
</th> </th>
); );
})} })}
<th style={{ width: columnWidths.actions }} className="px-3 py-3 border-b border-slate-700 sticky right-0 bg-slate-900 shadow-[-4px_0_8px_rgba(0,0,0,0.2)]"> <th style={{ width: columnWidths.actions }} className="px-3 py-3 border-b border-slate-700 sticky right-0 bg-slate-900 shadow-[-4px_0_8px_rgba(0,0,0,0.2)]">
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'actions', columnWidths.actions)} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'actions', columnWidths.actions); }} />
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => { {sortedRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => {
const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])]; const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])];
const isValidated = !!row[COLUMNS.VALIDATED_CHECK]; const isValidated = !!row[COLUMNS.VALIDATED_CHECK];
const note = String(row[COLUMNS.VALIDATED_NOTE] || ''); const note = String(row[COLUMNS.VALIDATED_NOTE] || '');
@@ -1247,6 +1312,22 @@ function TextFilterPopover({ value, onChange, onClose }: {
); );
} }
// ── Sort Icon Helper ───────────────────────────────────────────────────
function SortIcon({ current }: { current: 'asc' | 'desc' | null }) {
return (
<span className="inline-flex flex-col ml-1">
<ChevronDown className={cn(
"w-2.5 h-2.5 -mb-0.5 transition-colors",
current === 'asc' ? "text-blue-400 rotate-180" : "text-slate-600 group-hover:text-slate-400"
)} />
<ChevronDown className={cn(
"w-2.5 h-2.5 transition-colors",
current === 'desc' ? "text-blue-400" : "text-slate-600 group-hover:text-slate-400"
)} />
</span>
);
}
// ── Resize Handle Component ───────────────────────────────────────────── // ── Resize Handle Component ─────────────────────────────────────────────
function ResizeHandle({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) { function ResizeHandle({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) {
return ( return (