feat: resizable columns in ProductDescriptions and ArticleDetails

This commit is contained in:
Christian Vidal Wolf
2026-04-08 20:21:36 +02:00
parent 968547c8c7
commit da4673f7c4
2 changed files with 102 additions and 31 deletions
+53 -16
View File
@@ -26,6 +26,17 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
const [page, setPage] = useState(1);
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
const [openFilterCol, setOpenFilterCol] = useState<number | null>(null);
const [columnWidths, setColumnWidths] = useState<Record<number, number>>({
[COLUMNS.ARTICLE_NO]: 100,
[COLUMNS.ARTICLE_NAME]: 250,
[COLUMNS.LINE]: 80,
[COLUMNS.LICENSE]: 120,
[COLUMNS.CLASSIFICATION]: 100,
[COLUMNS.LONG_DE]: 80,
[COLUMNS.LONG_EN]: 80,
[COLUMNS.SHORT_DE]: 80,
[COLUMNS.SHORT_EN]: 80,
});
const lines = useMemo(() => Array.from(new Set(data.map(r => r[COLUMNS.LINE]).filter(Boolean))), [data]);
const licenses = useMemo(() => Array.from(new Set(data.map(r => r[COLUMNS.LICENSE]).filter(Boolean))), [data]);
@@ -116,6 +127,25 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
}
};
const handleResize = (colIndex: number, e: React.MouseEvent) => {
e.preventDefault();
const startX = e.pageX;
const startWidth = columnWidths[colIndex] || 100;
const onMouseMove = (moveEvent: MouseEvent) => {
const newWidth = Math.max(60, startWidth + (moveEvent.pageX - startX));
setColumnWidths(prev => ({ ...prev, [colIndex]: newWidth }));
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
};
const getUniqueValues = (col: number) => {
// For description columns, return only 'Present' and 'Missing'
if (DESCRIPTION_COLUMNS.includes(col)) {
@@ -252,7 +282,7 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
<div className="flex-1 bg-slate-800 rounded-xl border border-slate-700 shadow-xl overflow-hidden flex flex-col">
<div className="overflow-x-auto flex-1">
<table className="w-full text-left text-sm">
<table className="w-full text-left text-sm" style={{ tableLayout: 'fixed' }}>
<thead className="bg-slate-900/50 text-slate-400 sticky top-0 z-10">
<tr>
{[
@@ -268,13 +298,14 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
].map(({ col, label }) => (
<th
key={col}
className="px-4 py-3 font-medium transition-colors select-none group relative"
className="px-4 py-3 font-medium transition-colors select-none group relative border-r border-slate-700/30"
style={{ width: columnWidths[col] || 'auto', minWidth: columnWidths[col] || 'auto' }}
>
<div className="flex items-center justify-between gap-1">
<div className="flex items-center gap-1 cursor-pointer hover:text-white" onClick={() => handleSort(col)}>
<div className="flex items-center justify-between gap-1 overflow-hidden">
<div className="flex items-center gap-1 cursor-pointer hover:text-white truncate" onClick={() => handleSort(col)}>
{label}
{sortCol === col && (
sortDesc ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />
sortDesc ? <ChevronDown className="w-4 h-4 shrink-0" /> : <ChevronUp className="w-4 h-4 shrink-0" />
)}
</div>
<button
@@ -283,7 +314,7 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
setOpenFilterCol(openFilterCol === col ? null : col);
}}
className={cn(
"p-1 rounded hover:bg-slate-700 transition-colors",
"p-1 rounded hover:bg-slate-700 transition-colors shrink-0",
(columnFilters[col]?.length || 0) > 0 ? "text-blue-400 bg-blue-400/10" : "text-slate-500 opacity-0 group-hover:opacity-100"
)}
>
@@ -291,6 +322,12 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
</button>
</div>
{/* Resizer handle */}
<div
onMouseDown={(e) => handleResize(col, e)}
className="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-blue-500/50 group-hover:bg-slate-700/50 transition-colors z-20"
/>
{openFilterCol === col && (
<ColumnFilterPopover
uniqueValues={getUniqueValues(col)}
@@ -310,7 +347,7 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
)}
</th>
))}
<th className="px-4 py-3 font-medium text-right">Actions</th>
<th className="px-4 py-3 font-medium text-right" style={{ width: 100 }}>Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/50">
@@ -325,11 +362,11 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
isPending ? "bg-yellow-400/20 border-l-4 border-l-yellow-400" : ""
)}
>
<td className="px-4 py-3 font-mono text-slate-300 text-xs">{row[COLUMNS.ARTICLE_NO]}</td>
<td className="px-4 py-3 font-medium text-white max-w-[200px] truncate" title={row[COLUMNS.ARTICLE_NAME]}>{row[COLUMNS.ARTICLE_NAME]}</td>
<td className="px-4 py-3 text-slate-300 text-xs">{row[COLUMNS.LINE]}</td>
<td className="px-4 py-3 text-slate-300 text-xs truncate max-w-[120px]" title={row[COLUMNS.LICENSE]}>{row[COLUMNS.LICENSE] || '—'}</td>
<td className="px-4 py-3">
<td className="px-4 py-3 font-mono text-slate-300 text-xs truncate" style={{ width: columnWidths[COLUMNS.ARTICLE_NO] }}>{row[COLUMNS.ARTICLE_NO]}</td>
<td className="px-4 py-3 font-medium text-white truncate" style={{ width: columnWidths[COLUMNS.ARTICLE_NAME] }} title={row[COLUMNS.ARTICLE_NAME]}>{row[COLUMNS.ARTICLE_NAME]}</td>
<td className="px-4 py-3 text-slate-300 text-xs truncate" style={{ width: columnWidths[COLUMNS.LINE] }}>{row[COLUMNS.LINE]}</td>
<td className="px-4 py-3 text-slate-300 text-xs truncate" style={{ width: columnWidths[COLUMNS.LICENSE] }} title={row[COLUMNS.LICENSE]}>{row[COLUMNS.LICENSE] || '—'}</td>
<td className="px-4 py-3 truncate" style={{ width: columnWidths[COLUMNS.CLASSIFICATION] }}>
<span className={cn(
"px-2 py-0.5 rounded text-[10px] font-bold border",
String(row[COLUMNS.CLASSIFICATION]).includes('OOC')
@@ -339,10 +376,10 @@ export function ProductDescriptions({ data, onEdit, rowStatuses }: ProductDescri
{row[COLUMNS.CLASSIFICATION] || '—'}
</span>
</td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.LONG_DE]} row={row} /></td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.LONG_EN]} row={row} /></td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.SHORT_DE]} row={row} /></td>
<td className="px-4 py-3"><Badge content={row[COLUMNS.SHORT_EN]} row={row} /></td>
<td className="px-4 py-3" style={{ width: columnWidths[COLUMNS.LONG_DE] }}><Badge content={row[COLUMNS.LONG_DE]} row={row} /></td>
<td className="px-4 py-3" style={{ width: columnWidths[COLUMNS.LONG_EN] }}><Badge content={row[COLUMNS.LONG_EN]} row={row} /></td>
<td className="px-4 py-3" style={{ width: columnWidths[COLUMNS.SHORT_DE] }}><Badge content={row[COLUMNS.SHORT_DE]} row={row} /></td>
<td className="px-4 py-3" style={{ width: columnWidths[COLUMNS.SHORT_EN] }}><Badge content={row[COLUMNS.SHORT_EN]} row={row} /></td>
<td className="px-4 py-3 text-right">
<button
onClick={() => onEdit(index)}