mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 16:45:25 +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 [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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user