feat: redesign PricingView with manual validation check and notes, and add Matrix columns

This commit is contained in:
Christian Vidal Wolf
2026-04-20 18:46:50 +02:00
parent 111a44ae82
commit db696d2816
4 changed files with 142 additions and 78 deletions
+3 -1
View File
@@ -122,7 +122,9 @@ export default function App() {
COLUMNS.INNER_L, COLUMNS.INNER_W, COLUMNS.INNER_H, COLUMNS.INNER_L, COLUMNS.INNER_W, COLUMNS.INNER_H,
COLUMNS.OUTER_L, COLUMNS.OUTER_W, COLUMNS.OUTER_H, COLUMNS.OUTER_L, COLUMNS.OUTER_W, COLUMNS.OUTER_H,
COLUMNS.UNITS_OUTER, COLUMNS.MOQ, COLUMNS.UNITS_OUTER, COLUMNS.MOQ,
COLUMNS.VERIFIED_DIMS COLUMNS.VERIFIED_DIMS,
COLUMNS.VALIDATED_CHECK,
COLUMNS.VALIDATED_NOTE
]); ]);
headers.forEach((h: any, i: number) => { headers.forEach((h: any, i: number) => {
+1 -1
View File
@@ -55,7 +55,7 @@ function isEmptyOrEpoch(val: any): boolean {
return false; return false;
} }
type TabType = 'missingLaunch'; type TabType = 'missingLaunch' | 'launchInconsistent' | 'upcomingLaunch';
interface EditingState { interface EditingState {
rowIndex: number; rowIndex: number;
+134 -74
View File
@@ -13,6 +13,7 @@ import {
Filter, Filter,
Check, Check,
Search, Search,
MessageSquare,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover'; import { ColumnFilterPopover } from './ColumnFilterPopover';
@@ -311,6 +312,52 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
); );
}; };
// ── Matrix columns (the rest) ─────────────────────────────────────────────
const displayedIndices = useMemo(() => {
const set = new Set<number>([
COLUMNS.ARTICLE_NO,
COLUMNS.ARTICLE_NAME,
COLUMNS.LINE,
COLUMNS.CLASSIFICATION,
COLUMNS.UNITS_OUTER,
COLUMNS.OUTER_W,
COLUMNS.OUTER_L,
COLUMNS.OUTER_H,
]);
if (uvpIdx >= 0) set.add(uvpIdx);
srpCols.forEach(c => set.add(c.index));
containerCols.forEach(c => set.add(c.index));
return set;
}, [uvpIdx, srpCols, containerCols]);
const matrixCols = useMemo(() => {
return headers
.map((h, i) => ({ index: i, name: h || '' }))
.filter(({ index }) => !displayedIndices.has(index))
.filter(({ name }) => name.trim() !== ''); // Skip empty headers
}, [headers, displayedIndices]);
const [noteEditor, setNoteEditor] = useState<{ rowIndex: number; text: string } | null>(null);
const handleToggleCheck = async (rowIndex: number, checked: boolean) => {
const original = data[rowIndex];
const newRow = [...original];
newRow[COLUMNS.VALIDATED_CHECK] = checked;
onCaptureState(`${checked ? 'Checked' : 'Unchecked'} ${original[COLUMNS.ARTICLE_NO]}`);
await onSaveRow(rowIndex, newRow);
};
const saveNote = async () => {
if (!noteEditor) return;
const { rowIndex, text } = noteEditor;
const original = data[rowIndex];
const newRow = [...original];
newRow[COLUMNS.VALIDATED_NOTE] = text;
setNoteEditor(null);
onCaptureState(`Updated note for ${original[COLUMNS.ARTICLE_NO]}`);
await onSaveRow(rowIndex, newRow);
};
// ── Column detection notice ─────────────────────────────────────────────── // ── Column detection notice ───────────────────────────────────────────────
const missingCols: string[] = []; const missingCols: string[] = [];
if (uvpIdx < 0) missingCols.push('UVP'); if (uvpIdx < 0) missingCols.push('UVP');
@@ -325,7 +372,40 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
]; ];
return ( return (
<div className="flex flex-col h-full gap-4"> <div className="flex flex-col h-full gap-4 relative">
{/* ── Note Editor Modal ── */}
{noteEditor && (
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
<div className="bg-slate-800 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md animate-in zoom-in-95 duration-200">
<div className="p-4 border-b border-slate-700 flex items-center justify-between">
<h3 className="font-semibold text-white flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-blue-400" />
Validation Note
</h3>
<p className="text-[10px] text-slate-500 font-mono">{data[noteEditor.rowIndex][COLUMNS.ARTICLE_NO]}</p>
</div>
<div className="p-4">
<textarea
value={noteEditor.text}
onChange={e => setNoteEditor(prev => prev ? { ...prev, text: e.target.value } : null)}
placeholder="Write a note about this product..."
className="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-blue-500 resize-none"
autoFocus
/>
</div>
<div className="p-4 bg-slate-900/50 rounded-b-xl flex justify-end gap-3">
<button onClick={() => setNoteEditor(null)} className="px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">Cancel</button>
<button
onClick={saveNote}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-bold rounded-lg shadow-lg active:scale-95 transition-all"
>
Save Note
</button>
</div>
</div>
</div>
)}
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
{/* ── Search bar ── */} {/* ── Search bar ── */}
<div className="flex-1 max-w-md relative"> <div className="flex-1 max-w-md relative">
@@ -411,7 +491,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<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"> <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">
Art. No. Art. No.
</th> </th>
{/* Article Name with text filter */}
<th 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 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"> <div className="flex items-center gap-1">
<span>Article Name</span> <span>Article Name</span>
@@ -433,7 +512,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
/> />
)} )}
</th> </th>
{/* Line with multi-select filter */}
<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 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"> <div className="flex items-center gap-1">
<span>Line</span> <span>Line</span>
@@ -458,7 +536,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
/> />
)} )}
</th> </th>
{/* Classification with multi-select filter */}
<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 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"> <div className="flex items-center gap-1">
<span>Classification</span> <span>Classification</span>
@@ -484,7 +561,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
</th> </th>
{/* Editable pricing columns */}
{pricingEditableCols.map(col => ( {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"> <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"> <div className="flex items-center gap-1">
@@ -520,7 +596,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</th> </th>
))} ))}
{/* Units/Outer */}
<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 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"> <div className="flex items-center gap-1">
<span>Units/Outer</span> <span>Units/Outer</span>
@@ -546,7 +621,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
</th> </th>
{/* Outer W */}
<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 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"> <div className="flex items-center gap-1">
<span>Outer W</span> <span>Outer W</span>
@@ -572,7 +646,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
</th> </th>
{/* Outer L */}
<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 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"> <div className="flex items-center gap-1">
<span>Outer L</span> <span>Outer L</span>
@@ -598,7 +671,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
</th> </th>
{/* Outer H */}
<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 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"> <div className="flex items-center gap-1">
<span>Outer H</span> <span>Outer H</span>
@@ -624,7 +696,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
</th> </th>
{/* Container columns */}
{containerCols.map(col => ( {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"> <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"> <div className="flex items-center gap-1">
@@ -660,19 +731,28 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</th> </th>
))} ))}
{/* Status */} {/* Validation Check Column */}
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700"> <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">
Status Check
</th> </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>
))}
{/* Actions */} {/* Actions */}
<th className="px-3 py-3 border-b border-slate-700 w-10" /> <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)]" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => { {filteredRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => {
const hasAnyError = pricingErrors.length > 0 || unitErrors.length > 0;
const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])]; const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])];
const isValidated = !!row[COLUMNS.VALIDATED_CHECK];
const note = String(row[COLUMNS.VALIDATED_NOTE] || '');
return ( return (
<tr <tr
key={dataIndex} key={dataIndex}
@@ -689,7 +769,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
: '' : ''
)} )}
> >
{/* Article No */} {/* 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">
{row[COLUMNS.ARTICLE_NO]} {row[COLUMNS.ARTICLE_NO]}
</td> </td>
@@ -720,7 +800,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</span> </span>
</td> </td>
{/* Editable pricing cells */} {/* Pricing editable cells */}
{pricingEditableCols.map(col => { {pricingEditableCols.map(col => {
const isEditing = editingCell?.rowIndex === dataIndex && editingCell?.colIndex === col.index; const isEditing = editingCell?.rowIndex === dataIndex && editingCell?.colIndex === col.index;
const isSaving = savingCell?.rowIndex === dataIndex && savingCell?.colIndex === col.index; const isSaving = savingCell?.rowIndex === dataIndex && savingCell?.colIndex === col.index;
@@ -743,9 +823,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<button onClick={commitEdit} className="text-emerald-400 hover:text-emerald-300 p-0.5"> <button onClick={commitEdit} className="text-emerald-400 hover:text-emerald-300 p-0.5">
<Save className="w-3.5 h-3.5" /> <Save className="w-3.5 h-3.5" />
</button> </button>
<button onClick={cancelEdit} className="text-slate-500 hover:text-slate-300 p-0.5">
<X className="w-3.5 h-3.5" />
</button>
</div> </div>
) : ( ) : (
<button <button
@@ -761,15 +838,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{isSaving ? ( {isSaving ? (
<span className="text-slate-500 italic">saving</span> <span className="text-slate-500 italic">saving</span>
) : isEmpty ? ( ) : isEmpty ? (
<> <><AlertTriangle className="w-3 h-3" /><span>Missing</span></>
<AlertTriangle className="w-3 h-3" />
<span>Missing</span>
</>
) : ( ) : (
<> <>{val}<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50" /></>
<span>{val}</span>
<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50" />
</>
)} )}
</button> </button>
)} )}
@@ -777,66 +848,55 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
); );
})} })}
{/* Units/Outer */} {/* Units columns */}
<td className="px-3 py-2.5"> <td className="px-3 py-2.5">{unitOuterBadge(row[COLUMNS.UNITS_OUTER])}</td>
{unitOuterBadge(row[COLUMNS.UNITS_OUTER])} <td className="px-3 py-2.5 text-slate-400 font-mono text-xs">{row[COLUMNS.OUTER_W] ?? '-'}</td>
</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>
{/* Outer W */} {/* Container units columns */}
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">
{row[COLUMNS.OUTER_W] ?? '-'}
</td>
{/* Outer L */}
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">
{row[COLUMNS.OUTER_L] ?? '-'}
</td>
{/* Outer H */}
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">
{row[COLUMNS.OUTER_H] ?? '-'}
</td>
{/* Container unit columns */}
{containerCols.map(col => ( {containerCols.map(col => (
<td key={col.index} className="px-3 py-2.5"> <td key={col.index} className="px-3 py-2.5">
{unitBadge(row[col.index], col.name)} {unitBadge(row[col.index], col.name)}
</td> </td>
))} ))}
{/* Status badge */} {/* Check Column (Validated + Note) */}
<td className="px-3 py-2.5"> <td className="px-3 py-2.5">
{!hasAnyError ? ( <div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"> <input
<CheckCircle2 className="w-3 h-3" /> OK type="checkbox"
</span> checked={isValidated}
) : isCritical ? ( onChange={(e) => handleToggleCheck(dataIndex, e.target.checked)}
<div className="flex flex-col gap-1"> className="w-4 h-4 rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-slate-800 cursor-pointer"
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-bold bg-red-500/15 text-red-400 border border-red-500/30"> />
<AlertCircle className="w-3 h-3" /> Critical <button
</span> onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })}
{unitErrors.map((e, i) => ( className={cn(
<span key={i} className="text-[10px] text-red-400/70 pl-1">{e}</span> "p-1.5 rounded-md transition-all shrink-0",
))} note
</div> ? "bg-amber-500/10 text-amber-400 border border-amber-500/20 hover:bg-amber-500/20"
) : ( : "text-slate-500 hover:text-slate-300 hover:bg-slate-700/50"
<div className="flex flex-col gap-1">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20">
<AlertTriangle className="w-3 h-3" /> Incomplete
</span>
{pricingErrors.map((e, i) => (
<span key={i} className="text-[10px] text-amber-400/70 pl-1">{e}</span>
))}
</div>
)} )}
title={note || "Add note"}
>
<MessageSquare className={cn("w-3.5 h-3.5", note && "fill-amber-400/20")} />
</button>
</div>
</td> </td>
{/* Edit button */} {/* Matrix columns cells */}
<td className="px-2 py-2.5"> {matrixCols.map(col => (
<td key={col.index} className="px-3 py-2.5 text-slate-500 text-xs max-w-[150px] truncate" title={String(row[col.index] || '')}>
{row[col.index] ?? '—'}
</td>
))}
{/* Actions sticky */}
<td className="px-2 py-2.5 sticky right-0 bg-slate-800 shadow-[-4px_0_8px_rgba(0,0,0,0.1)] group-hover:bg-slate-700/40">
<button <button
onClick={() => onEdit(dataIndex)} onClick={() => onEdit(dataIndex)}
className="p-1.5 text-slate-500 hover:text-slate-200 hover:bg-slate-700 rounded transition-colors" className="p-1.5 text-slate-500 hover:text-slate-200 hover:bg-slate-700 rounded transition-colors"
title="Open full editor"
> >
<Edit2 className="w-3.5 h-3.5" /> <Edit2 className="w-3.5 h-3.5" />
</button> </button>
+3 -1
View File
@@ -36,5 +36,7 @@ export const COLUMNS = {
OUTER_W: 47, OUTER_W: 47,
OUTER_L: 48, OUTER_L: 48,
OUTER_H: 49, OUTER_H: 49,
VERIFIED_DIMS: 100 VERIFIED_DIMS: 100,
VALIDATED_CHECK: 101,
VALIDATED_NOTE: 102
}; };