mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 17:05:24 +02:00
feat: redesign PricingView with manual validation check and notes, and add Matrix columns
This commit is contained in:
+3
-1
@@ -122,7 +122,9 @@ export default function App() {
|
||||
COLUMNS.INNER_L, COLUMNS.INNER_W, COLUMNS.INNER_H,
|
||||
COLUMNS.OUTER_L, COLUMNS.OUTER_W, COLUMNS.OUTER_H,
|
||||
COLUMNS.UNITS_OUTER, COLUMNS.MOQ,
|
||||
COLUMNS.VERIFIED_DIMS
|
||||
COLUMNS.VERIFIED_DIMS,
|
||||
COLUMNS.VALIDATED_CHECK,
|
||||
COLUMNS.VALIDATED_NOTE
|
||||
]);
|
||||
|
||||
headers.forEach((h: any, i: number) => {
|
||||
|
||||
@@ -55,7 +55,7 @@ function isEmptyOrEpoch(val: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
type TabType = 'missingLaunch';
|
||||
type TabType = 'missingLaunch' | 'launchInconsistent' | 'upcomingLaunch';
|
||||
|
||||
interface EditingState {
|
||||
rowIndex: number;
|
||||
|
||||
+135
-75
@@ -13,6 +13,7 @@ import {
|
||||
Filter,
|
||||
Check,
|
||||
Search,
|
||||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
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 ───────────────────────────────────────────────
|
||||
const missingCols: string[] = [];
|
||||
if (uvpIdx < 0) missingCols.push('UVP');
|
||||
@@ -325,7 +372,40 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
];
|
||||
|
||||
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">
|
||||
{/* ── Search bar ── */}
|
||||
<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">
|
||||
Art. No.
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Article Name</span>
|
||||
@@ -433,7 +512,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Line</span>
|
||||
@@ -458,7 +536,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Classification</span>
|
||||
@@ -484,7 +561,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
)}
|
||||
</th>
|
||||
|
||||
{/* Editable pricing columns */}
|
||||
{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">
|
||||
@@ -520,7 +596,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Units/Outer</span>
|
||||
@@ -546,7 +621,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Outer W</span>
|
||||
@@ -572,7 +646,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Outer L</span>
|
||||
@@ -598,7 +671,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Outer H</span>
|
||||
@@ -624,7 +696,6 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
)}
|
||||
</th>
|
||||
|
||||
{/* Container columns */}
|
||||
{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">
|
||||
@@ -660,19 +731,28 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</th>
|
||||
))}
|
||||
|
||||
{/* Status */}
|
||||
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700">
|
||||
Status
|
||||
{/* 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">
|
||||
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>
|
||||
))}
|
||||
|
||||
{/* 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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredRows.map(({ row, dataIndex, pricingErrors, unitErrors, isCritical }) => {
|
||||
const hasAnyError = pricingErrors.length > 0 || unitErrors.length > 0;
|
||||
const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])];
|
||||
const isValidated = !!row[COLUMNS.VALIDATED_CHECK];
|
||||
const note = String(row[COLUMNS.VALIDATED_NOTE] || '');
|
||||
|
||||
return (
|
||||
<tr
|
||||
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">
|
||||
{row[COLUMNS.ARTICLE_NO]}
|
||||
</td>
|
||||
@@ -720,7 +800,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Editable pricing cells */}
|
||||
{/* Pricing editable cells */}
|
||||
{pricingEditableCols.map(col => {
|
||||
const isEditing = editingCell?.rowIndex === dataIndex && editingCell?.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">
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
</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>
|
||||
) : (
|
||||
<button
|
||||
@@ -761,15 +838,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isSaving ? (
|
||||
<span className="text-slate-500 italic">saving…</span>
|
||||
) : isEmpty ? (
|
||||
<>
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
<span>Missing</span>
|
||||
</>
|
||||
<><AlertTriangle className="w-3 h-3" /><span>Missing</span></>
|
||||
) : (
|
||||
<>
|
||||
<span>{val}</span>
|
||||
<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50" />
|
||||
</>
|
||||
<>{val}<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50" /></>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -777,66 +848,55 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Units/Outer */}
|
||||
<td className="px-3 py-2.5">
|
||||
{unitOuterBadge(row[COLUMNS.UNITS_OUTER])}
|
||||
</td>
|
||||
{/* 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>
|
||||
|
||||
{/* Outer W */}
|
||||
<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 */}
|
||||
{/* Container units columns */}
|
||||
{containerCols.map(col => (
|
||||
<td key={col.index} className="px-3 py-2.5">
|
||||
{unitBadge(row[col.index], col.name)}
|
||||
</td>
|
||||
))}
|
||||
|
||||
{/* Status badge */}
|
||||
{/* Check Column (Validated + Note) */}
|
||||
<td className="px-3 py-2.5">
|
||||
{!hasAnyError ? (
|
||||
<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">
|
||||
<CheckCircle2 className="w-3 h-3" /> OK
|
||||
</span>
|
||||
) : isCritical ? (
|
||||
<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-bold bg-red-500/15 text-red-400 border border-red-500/30">
|
||||
<AlertCircle className="w-3 h-3" /> Critical
|
||||
</span>
|
||||
{unitErrors.map((e, i) => (
|
||||
<span key={i} className="text-[10px] text-red-400/70 pl-1">{e}</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isValidated}
|
||||
onChange={(e) => handleToggleCheck(dataIndex, e.target.checked)}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md transition-all shrink-0",
|
||||
note
|
||||
? "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"
|
||||
)}
|
||||
title={note || "Add note"}
|
||||
>
|
||||
<MessageSquare className={cn("w-3.5 h-3.5", note && "fill-amber-400/20")} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Edit button */}
|
||||
<td className="px-2 py-2.5">
|
||||
{/* Matrix columns cells */}
|
||||
{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
|
||||
onClick={() => onEdit(dataIndex)}
|
||||
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" />
|
||||
</button>
|
||||
|
||||
+3
-1
@@ -36,5 +36,7 @@ export const COLUMNS = {
|
||||
OUTER_W: 47,
|
||||
OUTER_L: 48,
|
||||
OUTER_H: 49,
|
||||
VERIFIED_DIMS: 100
|
||||
VERIFIED_DIMS: 100,
|
||||
VALIDATED_CHECK: 101,
|
||||
VALIDATED_NOTE: 102
|
||||
};
|
||||
Reference in New Issue
Block a user