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
+49 -15
View File
@@ -22,6 +22,14 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
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]: 200,
[COLUMNS.CLASSIFICATION]: 80,
[COLUMNS.ITEM_AVAILABLE]: 80,
[COLUMNS.DETAILS_DE]: 150,
[COLUMNS.DETAILS_EN]: 150,
});
const lines = useMemo(() => Array.from(new Set(data.map(r => r[COLUMNS.LINE]).filter(Boolean))), [data]);
@@ -89,6 +97,25 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
}
};
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) => {
const values = data.map(r => String(r[col] || ''));
return Array.from(new Set(values)).sort();
@@ -185,7 +212,7 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
<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-xs">
<table className="w-full text-left text-xs" style={{ tableLayout: 'fixed' }}>
<thead className="bg-slate-900/80 text-slate-400 sticky top-0 z-10">
<tr>
{[
@@ -198,13 +225,14 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
].map(({ col, label }) => (
<th
key={col}
className="px-3 py-3 font-medium transition-colors select-none group relative"
className="px-3 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-3 h-3" /> : <ChevronUp className="w-3 h-3" />
sortDesc ? <ChevronDown className="w-3 h-3 shrink-0" /> : <ChevronUp className="w-3 h-3 shrink-0" />
)}
</div>
<button
@@ -213,7 +241,7 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
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-indigo-400 bg-indigo-400/10" : "text-slate-500 opacity-0 group-hover:opacity-100"
)}
>
@@ -221,6 +249,12 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
</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-indigo-500/50 group-hover:bg-slate-700/50 transition-colors z-20"
/>
{openFilterCol === col && (
<ColumnFilterPopover
uniqueValues={getUniqueValues(col)}
@@ -240,7 +274,7 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
)}
</th>
))}
<th className="px-3 py-3 font-medium text-right">Edit</th>
<th className="px-3 py-3 font-medium text-right" style={{ width: 80 }}>Edit</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/30">
@@ -254,11 +288,11 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
isPending ? "bg-yellow-400/20 border-l-4 border-l-yellow-400" : ""
)}
>
<td className="px-3 py-2 font-mono text-indigo-400">{row[COLUMNS.ARTICLE_NO]}</td>
<td className="px-3 py-2 font-medium text-slate-200 max-w-[150px] truncate" title={row[COLUMNS.ARTICLE_NAME]}>
<td className="px-3 py-2 font-mono text-indigo-400 truncate" style={{ width: columnWidths[COLUMNS.ARTICLE_NO] }}>{row[COLUMNS.ARTICLE_NO]}</td>
<td className="px-3 py-2 font-medium text-slate-200 truncate" style={{ width: columnWidths[COLUMNS.ARTICLE_NAME] }} title={row[COLUMNS.ARTICLE_NAME]}>
{row[COLUMNS.ARTICLE_NAME]}
</td>
<td className="px-3 py-2">
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.CLASSIFICATION] }}>
<span className={cn(
"px-1.5 py-0.5 rounded-[4px] text-[10px] font-bold border",
String(row[COLUMNS.CLASSIFICATION]).includes('OOC') ? "bg-amber-500/10 text-amber-500 border-amber-500/20" : "bg-slate-700/50 text-slate-400 border-slate-600/50"
@@ -266,7 +300,7 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
{row[COLUMNS.CLASSIFICATION] || '—'}
</span>
</td>
<td className="px-3 py-2">
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.ITEM_AVAILABLE] }}>
<span className={cn(
"font-mono font-bold",
Number(row[COLUMNS.ITEM_AVAILABLE] || 0) <= 0 ? "text-red-400" : "text-emerald-400"
@@ -274,14 +308,14 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
{row[COLUMNS.ITEM_AVAILABLE] || 0}
</span>
</td>
<td className="px-3 py-2">
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.DETAILS_DE] }}>
{row[COLUMNS.DETAILS_DE] ? (
<div className="max-w-[120px] truncate text-slate-400" title={row[COLUMNS.DETAILS_DE]}>{row[COLUMNS.DETAILS_DE]}</div>
<div className="truncate text-slate-400" title={row[COLUMNS.DETAILS_DE]}>{row[COLUMNS.DETAILS_DE]}</div>
) : getBadge(null)}
</td>
<td className="px-3 py-2">
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.DETAILS_EN] }}>
{row[COLUMNS.DETAILS_EN] ? (
<div className="max-w-[120px] truncate text-slate-400" title={row[COLUMNS.DETAILS_EN]}>{row[COLUMNS.DETAILS_EN]}</div>
<div className="truncate text-slate-400" title={row[COLUMNS.DETAILS_EN]}>{row[COLUMNS.DETAILS_EN]}</div>
) : getBadge(null)}
</td>
<td className="px-3 py-2 text-right">