mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 18:15:23 +02:00
feat: add sorting to Pricing Units table
This commit is contained in:
+132
-51
@@ -83,6 +83,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
const [editingType, setEditingType] = useState<{ rowIndex: number; value: string } | null>(null);
|
||||
const [typeSuggestions, setTypeSuggestions] = useState<string[]>([]);
|
||||
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 [selectedSearchItems, setSelectedSearchItems] = useState<Set<number>>(new Set());
|
||||
@@ -332,6 +333,70 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
return result;
|
||||
}, [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 ───────────────────────────────────────────────────
|
||||
const startEdit = (rowIndex: number, colIndex: number, currentValue: string) => {
|
||||
setEditingCell({ rowIndex, colIndex, value: currentValue });
|
||||
@@ -691,7 +756,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
|
||||
{/* ── Table ── */}
|
||||
<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">
|
||||
<CheckCircle2 className="w-12 h-12 text-emerald-500 mb-3" />
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
<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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Art. No.</span>
|
||||
<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 justify-between gap-1">
|
||||
<span className="flex items-center gap-1">Art. No. <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} /></span>
|
||||
</div>
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleNo', columnWidths.articleNo)} />
|
||||
<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 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">
|
||||
<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
|
||||
onClick={() => setOpenFilter(openFilter === 'name' ? null : 'name')}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
||||
className={cn(
|
||||
'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'
|
||||
@@ -727,13 +792,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleName', columnWidths.articleName)} />
|
||||
<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 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">
|
||||
<span className="truncate">Line</span>
|
||||
<span className="truncate flex items-center gap-1">Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
|
||||
className={cn(
|
||||
'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'
|
||||
@@ -752,13 +817,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'line', columnWidths.line)} />
|
||||
<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 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">
|
||||
<span className="truncate">Classification</span>
|
||||
<span className="truncate flex items-center gap-1">Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'classification' ? null : 'classification')}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
|
||||
className={cn(
|
||||
'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'
|
||||
@@ -777,21 +842,21 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'classification', columnWidths.classification)} />
|
||||
<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 border-b border-slate-700 whitespace-nowrap relative">
|
||||
<span>Type</span>
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'productType', columnWidths.productType)} />
|
||||
<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 className="flex items-center gap-1">Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} /></span>
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
|
||||
</th>
|
||||
|
||||
{pricingEditableCols.map(col => {
|
||||
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 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">
|
||||
<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
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -820,16 +885,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, colKey, width); }} />
|
||||
</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">
|
||||
<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
|
||||
onClick={() => setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter')}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
|
||||
className={cn(
|
||||
'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'
|
||||
@@ -848,14 +913,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter)} />
|
||||
<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 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">
|
||||
<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
|
||||
onClick={() => setOpenFilter(openFilter === 'outerW' ? null : 'outerW')}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
|
||||
className={cn(
|
||||
'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'
|
||||
@@ -874,14 +939,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerW', columnWidths.outerW)} />
|
||||
<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 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">
|
||||
<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
|
||||
onClick={() => setOpenFilter(openFilter === 'outerL' ? null : 'outerL')}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
|
||||
className={cn(
|
||||
'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'
|
||||
@@ -900,14 +965,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerL', columnWidths.outerL)} />
|
||||
<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 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">
|
||||
<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
|
||||
onClick={() => setOpenFilter(openFilter === 'outerH' ? null : 'outerH')}
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
|
||||
className={cn(
|
||||
'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'
|
||||
@@ -926,16 +991,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerH', columnWidths.outerH)} />
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerH', columnWidths.outerH); }} />
|
||||
</th>
|
||||
|
||||
{containerCols.map(col => {
|
||||
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 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">
|
||||
<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
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -964,34 +1029,34 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, colKey, width); }} />
|
||||
</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">
|
||||
Check
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'check', columnWidths.check)} />
|
||||
<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)}>
|
||||
<span className="flex items-center gap-1">Check <SortIcon current={sortConfig.key === COLUMNS.VALIDATED_CHECK ? sortConfig.direction : null} /></span>
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'check', columnWidths.check); }} />
|
||||
</th>
|
||||
|
||||
{matrixCols.map(col => {
|
||||
const colKey = `mat_${col.index}`;
|
||||
const width = columnWidths[colKey] ?? 120;
|
||||
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}>
|
||||
<span className="truncate block">{col.name}</span>
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
|
||||
<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 flex items-center gap-1">{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, colKey, width); }} />
|
||||
</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)]">
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'actions', columnWidths.actions)} />
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'actions', columnWidths.actions); }} />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => {
|
||||
{sortedRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => {
|
||||
const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])];
|
||||
const isValidated = !!row[COLUMNS.VALIDATED_CHECK];
|
||||
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 ─────────────────────────────────────────────
|
||||
function ResizeHandle({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user