feat: replace global EditPanel with focused 3-field panel in Missing Data

When editing from Missing Data view, only Classification, Launch Date
and Ready to Order fields are shown in a lightweight slide-in panel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-04-12 15:23:23 +02:00
co-authored by Claude Sonnet 4.6
parent ce9e91bad1
commit 39869edcef
2 changed files with 136 additions and 14 deletions
+134 -13
View File
@@ -1,12 +1,13 @@
import React, { useState, useMemo } from 'react';
import { ExcelRow, COLUMNS } from '../types';
import { Search, ChevronDown, ChevronUp, X, Edit2 } from 'lucide-react';
import { Search, ChevronDown, ChevronUp, X, Edit2, Save } from 'lucide-react';
import { cn } from '../lib/utils';
interface MissingDataViewProps {
data: ExcelRow[];
headers: string[];
onEdit: (index: number) => void;
onSaveRow: (rowIndex: number, updatedRow: ExcelRow) => void;
onCaptureState: (message: string) => void;
}
function formatDateValue(val: any): string {
@@ -26,11 +27,7 @@ function isEmptyOrEpoch(val: any): boolean {
if (val === null || val === undefined || val === '') return true;
if (typeof val === 'number') {
if (val === 0 || val === 1) return true;
if (val >= 25569 && val <= 60000) {
// valid date range — not empty
return false;
}
// number outside date range — treat as empty
if (val >= 25569 && val <= 60000) return false;
return true;
}
const s = String(val).trim();
@@ -41,12 +38,20 @@ function isEmptyOrEpoch(val: any): boolean {
type TabType = 'missingClass' | 'missingLaunch';
export function MissingDataView({ data, headers, onEdit }: MissingDataViewProps) {
interface EditingState {
rowIndex: number;
classification: string;
launchDate: string;
readyToOrder: string;
}
export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: MissingDataViewProps) {
const [activeTab, setActiveTab] = useState<TabType>('missingClass');
const [search, setSearch] = useState('');
const [sortCol, setSortCol] = useState<number | null>(null);
const [sortDesc, setSortDesc] = useState(false);
const [page, setPage] = useState(1);
const [editing, setEditing] = useState<EditingState | null>(null);
const pageSize = 100;
const launchDateCol = useMemo(() =>
@@ -54,6 +59,9 @@ export function MissingDataView({ data, headers, onEdit }: MissingDataViewProps)
const readyToOrderCol = useMemo(() =>
headers.findIndex(h => h.toLowerCase().includes('ready')), [headers]);
const launchHeader = launchDateCol >= 0 ? headers[launchDateCol] : 'Launch Date';
const readyHeader = readyToOrderCol >= 0 ? headers[readyToOrderCol] : 'Ready to Order';
const filteredData = useMemo(() => {
let result = data.map((row, index) => ({ row, index }));
@@ -107,14 +115,32 @@ export function MissingDataView({ data, headers, onEdit }: MissingDataViewProps)
else { setSortCol(col); setSortDesc(false); }
};
const openEdit = (rowIndex: number, row: ExcelRow) => {
setEditing({
rowIndex,
classification: String(row[COLUMNS.CLASSIFICATION] || ''),
launchDate: launchDateCol >= 0 ? formatDateValue(row[launchDateCol]) || String(row[launchDateCol] ?? '') : '',
readyToOrder: readyToOrderCol >= 0 ? formatDateValue(row[readyToOrderCol]) || String(row[readyToOrderCol] ?? '') : '',
});
};
const handleSave = () => {
if (!editing) return;
const row = data[editing.rowIndex];
onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`);
const newRow = [...row];
newRow[COLUMNS.CLASSIFICATION] = editing.classification;
if (launchDateCol >= 0) newRow[launchDateCol] = editing.launchDate;
if (readyToOrderCol >= 0) newRow[readyToOrderCol] = editing.readyToOrder;
onSaveRow(editing.rowIndex, newRow);
setEditing(null);
};
const tabs: { id: TabType; label: string }[] = [
{ id: 'missingClass', label: 'Missing Classification' },
{ id: 'missingLaunch', label: 'Missing Launch Date' },
];
const launchHeader = launchDateCol >= 0 ? headers[launchDateCol] : 'Launch Date';
const readyHeader = readyToOrderCol >= 0 ? headers[readyToOrderCol] : 'Ready to Order';
const columns = [
{ col: COLUMNS.ARTICLE_NO, label: 'SKU', width: 100 },
{ col: COLUMNS.ARTICLE_NAME, label: 'Name', width: 220 },
@@ -123,6 +149,8 @@ export function MissingDataView({ data, headers, onEdit }: MissingDataViewProps)
...(readyToOrderCol >= 0 ? [{ col: readyToOrderCol, label: readyHeader, width: 130 }] : []),
];
const editingRow = editing ? data[editing.rowIndex] : null;
return (
<div className="flex flex-col h-full">
<div className="flex flex-wrap gap-2 mb-6">
@@ -232,7 +260,7 @@ export function MissingDataView({ data, headers, onEdit }: MissingDataViewProps)
)}
<td className="px-3 py-2 text-right" style={{ width: 60 }}>
<button
onClick={() => onEdit(index)}
onClick={() => openEdit(index, row)}
className="p-1.5 text-slate-500 hover:text-indigo-400 hover:bg-indigo-400/10 rounded transition-colors"
>
<Edit2 className="w-4 h-4" />
@@ -242,7 +270,7 @@ export function MissingDataView({ data, headers, onEdit }: MissingDataViewProps)
))}
{paginatedData.length === 0 && (
<tr>
<td colSpan={columns.length} className="px-4 py-8 text-center text-slate-500">
<td colSpan={columns.length + 1} className="px-4 py-8 text-center text-slate-500">
No items found.
</td>
</tr>
@@ -272,6 +300,99 @@ export function MissingDataView({ data, headers, onEdit }: MissingDataViewProps)
</div>
</div>
</div>
{/* Focused edit panel */}
{editing && editingRow && (
<>
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-40" onClick={() => setEditing(null)} />
<div className="fixed right-0 top-0 bottom-0 w-[400px] bg-slate-800 border-l border-slate-700 shadow-2xl z-50 flex flex-col animate-in slide-in-from-right duration-200">
<div className="flex items-center justify-between p-6 border-b border-slate-700 bg-slate-800/50">
<div>
<h2 className="text-lg font-bold text-white">Edit Fields</h2>
<p className="text-xs text-slate-400 mt-0.5">
{editingRow[COLUMNS.ARTICLE_NO]} {editingRow[COLUMNS.ARTICLE_NAME]}
</p>
</div>
<button
onClick={() => setEditing(null)}
className="p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded-full transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-auto p-6 space-y-5">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Classification</label>
<input
type="text"
value={editing.classification}
onChange={e => setEditing(prev => prev ? { ...prev, classification: e.target.value } : prev)}
placeholder="e.g. CORE, OOC..."
className={cn(
"w-full bg-slate-900 border rounded-md px-3 py-2.5 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
editing.classification !== String(editingRow[COLUMNS.CLASSIFICATION] || '')
? "border-blue-500 focus:ring-blue-500"
: "border-slate-700 focus:border-slate-500 focus:ring-slate-500"
)}
/>
</div>
{launchDateCol >= 0 && (
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">{launchHeader}</label>
<input
type="text"
value={editing.launchDate}
onChange={e => setEditing(prev => prev ? { ...prev, launchDate: e.target.value } : prev)}
placeholder="DD/MM/YYYY"
className={cn(
"w-full bg-slate-900 border rounded-md px-3 py-2.5 text-sm text-white font-mono focus:outline-none focus:ring-1 transition-colors",
editing.launchDate !== (formatDateValue(editingRow[launchDateCol]) || String(editingRow[launchDateCol] ?? ''))
? "border-blue-500 focus:ring-blue-500"
: "border-slate-700 focus:border-slate-500 focus:ring-slate-500"
)}
/>
</div>
)}
{readyToOrderCol >= 0 && (
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">{readyHeader}</label>
<input
type="text"
value={editing.readyToOrder}
onChange={e => setEditing(prev => prev ? { ...prev, readyToOrder: e.target.value } : prev)}
placeholder="DD/MM/YYYY"
className={cn(
"w-full bg-slate-900 border rounded-md px-3 py-2.5 text-sm text-white font-mono focus:outline-none focus:ring-1 transition-colors",
editing.readyToOrder !== (formatDateValue(editingRow[readyToOrderCol]) || String(editingRow[readyToOrderCol] ?? ''))
? "border-blue-500 focus:ring-blue-500"
: "border-slate-700 focus:border-slate-500 focus:ring-slate-500"
)}
/>
</div>
)}
</div>
<div className="p-6 border-t border-slate-700 bg-slate-800/50 flex justify-end gap-3">
<button
onClick={() => setEditing(null)}
className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-700 rounded-md transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md shadow-lg shadow-blue-900/20 transition-colors"
>
<Save className="w-4 h-4" />
Queue Changes
</button>
</div>
</div>
</>
)}
</div>
);
}