mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:45:24 +02:00
feat: resizable columns in ProductDescriptions and ArticleDetails
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user