mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 12:45:24 +02:00
feat: implement resizable columns in Pricing Units tab
This commit is contained in:
+164
-126
@@ -56,6 +56,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
articleName: 220,
|
||||
line: 100,
|
||||
classification: 130,
|
||||
unitsOuter: 100,
|
||||
outerW: 80,
|
||||
outerL: 80,
|
||||
outerH: 80,
|
||||
check: 100,
|
||||
actions: 60,
|
||||
});
|
||||
const [resizingColumn, setResizingColumn] = useState<string | null>(null);
|
||||
const [resizeStartX, setResizeStartX] = useState<number | null>(null);
|
||||
@@ -135,7 +141,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = e.clientX - resizeStartX;
|
||||
const newWidth = Math.max(50, Math.min(400, resizeStartWidth + delta));
|
||||
const newWidth = Math.max(40, Math.min(800, resizeStartWidth + delta));
|
||||
setColumnWidths(prev => ({ ...prev, [resizingColumn]: newWidth }));
|
||||
};
|
||||
|
||||
@@ -659,26 +665,22 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
<p className="text-sm mt-1">All products are correctly configured.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table ref={tableRef} className="w-full text-sm border-collapse">
|
||||
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
|
||||
<thead className="sticky top-0 z-10 bg-slate-900">
|
||||
<tr>
|
||||
<th data-col-key="articleNo" style={{ width: columnWidths.articleNo || 100, minWidth: 60 }} className="text-left px-2 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 justify-between">
|
||||
<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>
|
||||
<div
|
||||
onMouseDown={(e) => handleResizeStart(e, 'articleNo', columnWidths.articleNo || 100)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-1.5 h-4 bg-slate-600 hover:bg-blue-500 cursor-col-resize rounded-sm ml-1 shrink-0 pointer-events-auto"
|
||||
title="Drag to resize"
|
||||
/>
|
||||
</div>
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleNo', columnWidths.articleNo)} />
|
||||
</th>
|
||||
<th data-col-key="articleName" style={{ width: columnWidths.articleName || 220 }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 group relative">
|
||||
<div className="flex items-center gap-1"><span>Article Name</span>
|
||||
<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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="truncate">Article Name</span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'name' ? null : 'name')}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
@@ -692,14 +694,15 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleName', columnWidths.articleName)} />
|
||||
</th>
|
||||
<th 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 border-b border-slate-700 whitespace-nowrap group relative">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Line</span>
|
||||
<span className="truncate">Line</span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
@@ -716,14 +719,15 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'line', columnWidths.line)} />
|
||||
</th>
|
||||
<th 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 border-b border-slate-700 whitespace-nowrap group relative">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Classification</span>
|
||||
<span className="truncate">Classification</span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'classification' ? null : 'classification')}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
@@ -740,50 +744,56 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'classification', columnWidths.classification)} />
|
||||
</th>
|
||||
|
||||
{pricingEditableCols.map(col => (
|
||||
<th key={col.index} 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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{col.name}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`);
|
||||
}}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{openFilter === `prc_${col.index}` && (
|
||||
<ColumnFilterPopover
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
selectedValues={dynamicColFilters[col.index] || []}
|
||||
onToggle={val => setDynamicColFilters(prev => {
|
||||
const current = prev[col.index] || [];
|
||||
return current.includes(val)
|
||||
? { ...prev, [col.index]: current.filter(v => v !== val) }
|
||||
: { ...prev, [col.index]: [...current, val] };
|
||||
})}
|
||||
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
|
||||
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="truncate">{col.name}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`);
|
||||
}}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{openFilter === `prc_${col.index}` && (
|
||||
<ColumnFilterPopover
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
selectedValues={dynamicColFilters[col.index] || []}
|
||||
onToggle={val => setDynamicColFilters(prev => {
|
||||
const current = prev[col.index] || [];
|
||||
return current.includes(val)
|
||||
? { ...prev, [col.index]: current.filter(v => v !== val) }
|
||||
: { ...prev, [col.index]: [...current, val] };
|
||||
})}
|
||||
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
|
||||
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
|
||||
<th 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 border-b border-slate-700 whitespace-nowrap group relative">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Units/Outer</span>
|
||||
<span className="truncate">Units/Outer</span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter')}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
@@ -800,15 +810,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter)} />
|
||||
</th>
|
||||
|
||||
<th 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 border-b border-slate-700 whitespace-nowrap group relative">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Outer W</span>
|
||||
<span className="truncate">Outer W</span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'outerW' ? null : 'outerW')}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
@@ -825,15 +836,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerW', columnWidths.outerW)} />
|
||||
</th>
|
||||
|
||||
<th 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 border-b border-slate-700 whitespace-nowrap group relative">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Outer L</span>
|
||||
<span className="truncate">Outer L</span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'outerL' ? null : 'outerL')}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
@@ -850,15 +862,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerL', columnWidths.outerL)} />
|
||||
</th>
|
||||
|
||||
<th 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 border-b border-slate-700 whitespace-nowrap group relative">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Outer H</span>
|
||||
<span className="truncate">Outer H</span>
|
||||
<button
|
||||
onClick={() => setOpenFilter(openFilter === 'outerH' ? null : 'outerH')}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
@@ -875,57 +888,68 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerH', columnWidths.outerH)} />
|
||||
</th>
|
||||
|
||||
{containerCols.map(col => (
|
||||
<th key={col.index} 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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{col.name}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`);
|
||||
}}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors',
|
||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{openFilter === `con_${col.index}` && (
|
||||
<ColumnFilterPopover
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
selectedValues={dynamicColFilters[col.index] || []}
|
||||
onToggle={val => setDynamicColFilters(prev => {
|
||||
const current = prev[col.index] || [];
|
||||
return current.includes(val)
|
||||
? { ...prev, [col.index]: current.filter(v => v !== val) }
|
||||
: { ...prev, [col.index]: [...current, val] };
|
||||
})}
|
||||
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
|
||||
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="truncate">{col.name}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`);
|
||||
}}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{openFilter === `con_${col.index}` && (
|
||||
<ColumnFilterPopover
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
selectedValues={dynamicColFilters[col.index] || []}
|
||||
onToggle={val => setDynamicColFilters(prev => {
|
||||
const current = prev[col.index] || [];
|
||||
return current.includes(val)
|
||||
? { ...prev, [col.index]: current.filter(v => v !== val) }
|
||||
: { ...prev, [col.index]: [...current, val] };
|
||||
})}
|
||||
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
|
||||
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
|
||||
onClose={() => setOpenFilter(null)}
|
||||
/>
|
||||
)}
|
||||
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Validation Check Column */}
|
||||
<th className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap">
|
||||
<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>
|
||||
|
||||
{/* All remaining Matrix columns */}
|
||||
{matrixCols.map(col => (
|
||||
<th key={col.index} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap overflow-hidden max-w-[150px] truncate" title={col.name}>
|
||||
{col.name}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Actions */}
|
||||
<th className="px-3 py-3 border-b border-slate-700 w-10 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)} />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -953,7 +977,7 @@ 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">
|
||||
<td className="px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate">
|
||||
{row[COLUMNS.ARTICLE_NO]}
|
||||
</td>
|
||||
|
||||
@@ -965,14 +989,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</td>
|
||||
|
||||
{/* Line */}
|
||||
<td className="px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs">
|
||||
<td className="px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate">
|
||||
{row[COLUMNS.LINE] || '—'}
|
||||
</td>
|
||||
|
||||
{/* Classification */}
|
||||
<td className="px-3 py-2.5">
|
||||
<td className="px-3 py-2.5 overflow-hidden">
|
||||
<span className={cn(
|
||||
'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap',
|
||||
'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')
|
||||
? 'bg-blue-500/10 text-blue-400 border-blue-500/20'
|
||||
: String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('OOC')
|
||||
@@ -991,7 +1015,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
const isEmpty = !row[col.index] || row[col.index] === '' || Number(row[col.index]) === 0;
|
||||
|
||||
return (
|
||||
<td key={col.index} className="px-3 py-2">
|
||||
<td key={col.index} className="px-3 py-2 overflow-hidden">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
@@ -1001,17 +1025,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
onChange={e => setEditingCell(prev => prev ? { ...prev, value: e.target.value } : null)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={commitEdit}
|
||||
className="w-24 bg-slate-900 border border-blue-500 rounded px-2 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
className="w-full bg-slate-900 border border-blue-500 rounded px-2 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
<button onClick={commitEdit} className="text-emerald-400 hover:text-emerald-300 p-0.5">
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => startEdit(dataIndex, col.index, val)}
|
||||
className={cn(
|
||||
'group flex items-center gap-1.5 px-2 py-1 rounded text-xs font-mono transition-colors hover:bg-slate-700/60 min-w-[72px]',
|
||||
'group flex items-center gap-1.5 px-2 py-1 rounded text-xs font-mono transition-colors hover:bg-slate-700/60 w-full overflow-hidden',
|
||||
isEmpty
|
||||
? 'text-amber-400 border border-amber-500/40 bg-amber-500/5'
|
||||
: 'text-slate-200 border border-transparent hover:border-slate-600'
|
||||
@@ -1019,11 +1040,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
title={isSaving ? 'Saving…' : `Click to edit ${col.name}`}
|
||||
>
|
||||
{isSaving ? (
|
||||
<span className="text-slate-500 italic">saving…</span>
|
||||
<span className="text-slate-500 italic truncate">saving…</span>
|
||||
) : isEmpty ? (
|
||||
<><AlertTriangle className="w-3 h-3" /><span>Missing</span></>
|
||||
<div className="flex items-center gap-1 truncate"><AlertTriangle className="w-3 h-3 shrink-0" /><span>Missing</span></div>
|
||||
) : (
|
||||
<>{val}<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50" /></>
|
||||
<div className="flex items-center justify-between w-full truncate">
|
||||
<span>{val}</span>
|
||||
<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50 shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -1033,13 +1057,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
|
||||
{/* Units columns */}
|
||||
<td className="px-3 py-2.5">{unitOuterBadge(row[COLUMNS.UNITS_OUTER])}</td>
|
||||
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">{row[COLUMNS.OUTER_W] ?? '-'}</td>
|
||||
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">{row[COLUMNS.OUTER_L] ?? '-'}</td>
|
||||
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">{row[COLUMNS.OUTER_H] ?? '-'}</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>
|
||||
|
||||
{/* Container units columns */}
|
||||
{containerCols.map(col => (
|
||||
<td key={col.index} className="px-3 py-2.5">
|
||||
<td key={col.index} className="px-3 py-2.5 overflow-hidden truncate">
|
||||
{unitBadge(row[col.index], col.name)}
|
||||
</td>
|
||||
))}
|
||||
@@ -1139,6 +1163,20 @@ function TextFilterPopover({ value, onChange, onClose }: {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Resize Handle Component ─────────────────────────────────────────────
|
||||
function ResizeHandle({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) {
|
||||
return (
|
||||
<div
|
||||
onMouseDown={onMouseDown}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="absolute right-0 top-0 bottom-0 w-1.5 hover:bg-blue-500/50 cursor-col-resize group/resizer flex items-center justify-center z-20"
|
||||
title="Drag to resize"
|
||||
>
|
||||
<div className="w-[1px] h-4 bg-slate-700 group-hover/resizer:bg-blue-400 transition-colors" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stat card ─────────────────────────────────────────────────────────────────
|
||||
function StatCard({
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user