Files
Craze-Data-check/src/App.tsx
T

753 lines
30 KiB
TypeScript
Raw Normal View History

2026-03-27 12:03:57 +01:00
import React, { useState, useMemo, useEffect } from 'react';
import * as XLSX from 'xlsx';
import { X } from 'lucide-react';
import { cn } from './lib/utils';
import { AppState, ExcelRow, resolveColumnIndices, COLUMNS } from './types';
import { ColumnsProvider } from './contexts/ColumnsContext';
import { Sidebar } from './components/Sidebar';
import { TopBar } from './components/TopBar';
import { ProductDescriptions } from './components/ProductDescriptions';
import { MatrixView } from './components/MatrixView';
import { EditPanel } from './components/EditPanel';
2026-04-10 09:47:35 +02:00
import { getAllSyncedRows, saveRowToSupabase, resetAllPendingRows, saveHistoryEntry, deleteHistoryEntry } from './lib/supabase';
import { getStoredSession, signOut, type AuthSession } from './lib/auth';
import { LoginPage } from './components/LoginPage';
import { DimensionsView } from './components/DimensionsView';
import { PricingView } from './components/PricingView';
import { ArticleDetails } from './components/ArticleDetails';
import { HistoryView } from './components/HistoryView';
import { UndoToast } from './components/UndoToast';
import { PendingValidationView } from './components/PendingValidationView';
import { MissingDataView } from './components/MissingDataView';
export default function App() {
const [session, setSession] = useState<AuthSession | null>(() => getStoredSession());
const [appState, setAppState] = useState<AppState>({
headers: [],
data: [],
fileName: '',
fileDate: null,
hasUnsavedChanges: false,
asinColumnIndex: null
});
const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data'>('descriptions');
const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]);
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
2026-03-27 12:03:57 +01:00
const [isLoadingDefault, setIsLoadingDefault] = useState(true);
const [defaultLoadError, setDefaultLoadError] = useState<string | null>(null);
const [rowStatuses, setRowStatuses] = useState<Record<string, string>>({});
const [pendingRows, setPendingRows] = useState<Record<string, { rowIndex: number; originalData: ExcelRow; newData: ExcelRow; articleName: string }>>({});
const [isSavingAll, setIsSavingAll] = useState(false);
2026-04-20 20:30:21 +02:00
const [refreshTrigger, setRefreshTrigger] = useState(0);
2026-03-27 12:03:57 +01:00
2026-04-10 08:27:36 +02:00
useEffect(() => {
console.log('[App] session changed:', session ? 'logged in' : 'logged out');
}, [session]);
const handleSignOut = () => {
signOut();
setSession(null);
};
// Sync session state when localStorage is updated (e.g. by token refresh)
useEffect(() => {
const handleStorage = (e: StorageEvent) => {
if (e.key === 'craze_auth_session') {
setSession(getStoredSession());
}
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, []);
2026-03-27 12:03:57 +01:00
useEffect(() => {
const loadDefaultData = async () => {
setIsLoadingDefault(true);
setDefaultLoadError(null);
try {
const isDev = import.meta.env.DEV;
let rows: any[][];
let allData: any[][];
let fileMeta = { rev: '', size: 0 };
if (isDev) {
2026-04-20 20:39:23 +02:00
const cacheBuster = `t_${Date.now()}`;
const fileUrl = `/dropbox-file/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&dl=1&${cacheBuster}=${Date.now()}`;
2026-04-20 20:39:23 +02:00
console.log('Fetching Data-Matrix.xlsx from Dropbox (hard refresh)...');
const response = await fetch(fileUrl, { cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/html')) {
throw new Error('Dropbox returned an HTML page instead of the Excel file. This usually means the sharing link has expired or requires manual interaction. Please generate a new "Copy Link" from Dropbox.');
}
const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength < 100) throw new Error('File too small');
const wb = XLSX.read(arrayBuffer, { type: 'array' });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
allData = XLSX.utils.sheet_to_json<any[]>(ws, { header: 1 });
rows = allData.slice(1);
fileMeta = { rev: 'dev', size: arrayBuffer.byteLength };
} else {
console.log('Fetching file info from Dropbox...');
const infoRes = await fetch('/api/dropbox-proxy?info=1');
if (infoRes.ok) {
fileMeta = await infoRes.json();
console.log('File meta:', fileMeta);
}
2026-04-20 20:39:23 +02:00
console.log('Fetching Data-Matrix.xlsx from proxy (hard refresh)...');
const response = await fetch('/api/dropbox-proxy', { cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength < 100) throw new Error('File too small');
const wb = XLSX.read(arrayBuffer, { type: 'array' });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
allData = XLSX.utils.sheet_to_json<any[]>(ws, { header: 1 });
rows = allData.slice(1);
}
if (rows.length > 0) {
const headers = allData.slice(0, 1)[0];
console.log('Syncing Excel data to Supabase...');
const syncRes = await fetch('/api/dropbox-sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rows, fileMeta })
});
if (syncRes.ok) {
const syncResult = await syncRes.json();
console.log('Supabase sync result:', syncResult);
} else {
const errorText = await syncRes.text();
console.warn('Supabase sync failed:', syncRes.status, errorText);
}
console.log('Fetching synced data from Supabase...');
const syncedData = await getAllSyncedRows();
const resolvedCols = resolveColumnIndices(headers);
const editableColumns = new Set([
resolvedCols.CLASSIFICATION,
resolvedCols.LONG_DE, resolvedCols.LONG_EN,
resolvedCols.SHORT_DE, resolvedCols.SHORT_EN,
resolvedCols.DETAILS_DE, resolvedCols.DETAILS_EN,
resolvedCols.INNER_L, resolvedCols.INNER_W, resolvedCols.INNER_H,
resolvedCols.OUTER_L, resolvedCols.OUTER_W, resolvedCols.OUTER_H,
resolvedCols.UNITS_OUTER, resolvedCols.MOQ,
resolvedCols.VERIFIED_DIMS,
resolvedCols.VALIDATED_CHECK,
resolvedCols.VALIDATED_NOTE,
resolvedCols.PRODUCT_TYPE,
resolvedCols.ITEM_TO_LOGISTIC
]);
headers.forEach((h: any, i: number) => {
const hl = String(h || '').toLowerCase();
if (hl.includes('srp') || hl.includes('uvp') || hl.includes('40') || hl.includes('price')) {
editableColumns.add(i);
}
});
const articleNoIdx = resolvedCols.ARTICLE_NO;
const processedRows = rows.map(row => {
const articleNo = String(row[articleNoIdx]);
const synced = syncedData[articleNo];
// Start with Dropbox values (default)
let finalRow = [...row];
// Only preserve Supabase values if there's a PENDING manual edit
// NOT for 'synced' status - we want fresh Dropbox data to overwrite
if (synced && synced.status === 'pending') {
// DETECT COLUMN SHIFT: If the saved data uses the old structure (where index 12 was a numeric date),
// we need to shift all indices from 12 onwards by +1 to match today's Excel structure.
const oldData = synced.data;
const valAt12 = oldData[12];
// Launch Date was index 12 yesterday and it's always a numeric serial.
// PM Classification is index 12 today and it's a string or empty.
const needsShift = typeof valAt12 === 'number' || (typeof valAt12 === 'string' && /^[0-9.]+$/.test(valAt12));
editableColumns.forEach(idx => {
let mergeIdx = idx;
if (needsShift && idx >= 12) {
mergeIdx = idx - 1;
}
if (oldData[mergeIdx] !== undefined && oldData[mergeIdx] !== null) {
finalRow[idx] = oldData[mergeIdx];
}
});
setRowStatuses(prev => ({ ...prev, [articleNo]: synced.status }));
}
// Otherwise use Dropbox values (default - will show 4.5 not 4.95)
return finalRow.map((val: any, idx: number) => {
if (val === undefined || val === null || val === '') return val;
const header = (headers[idx] || '').toLowerCase();
if ((header.includes('id') || header.includes('no') || header.includes('code') ||
header.includes('art.') || header.includes('barcode') || header.includes('article')) &&
!(header.includes('nw') || header.includes('gw') || header.includes('weight') || header.includes('kg'))) {
return val;
}
const formatKeywords = ['price', 'eur', 'cost', 'msrp', 'net', 'gross', 'netto', 'brutto', 'pp', 'pph', 'uvp', 'vpe', 'stk', 'nw', 'gw', 'weight', 'kg'];
const shouldFormat = formatKeywords.some(kw => header.includes(kw));
if (typeof val === 'number') {
return Number(val.toFixed(2));
}
if (typeof val === 'string') {
const normalized = val.trim().replace(',', '.');
const num = parseFloat(normalized);
if (!isNaN(num) && (shouldFormat || val.includes('.') || val.includes(','))) {
return num.toFixed(2);
}
}
return val;
});
});
const asinIdx = (headers as string[]).findIndex((h: string) =>
String(h).toLowerCase().trim() === 'asin'
);
setAppState({
headers: headers,
data: processedRows,
fileName: 'Data-Matrix.xlsx (Cloud Sync)',
fileDate: new Date(),
hasUnsavedChanges: false,
asinColumnIndex: asinIdx !== -1 ? asinIdx : null
});
setActiveModule('descriptions');
}
} catch (err) {
console.error('Failed to load from Dropbox:', err);
setDefaultLoadError(err instanceof Error ? err.message : 'Connection failed');
} finally {
setIsLoadingDefault(false);
}
2026-03-27 12:03:57 +01:00
};
if (session) loadDefaultData();
2026-04-20 20:30:21 +02:00
}, [session, refreshTrigger]);
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (appState.hasUnsavedChanges) {
if (!window.confirm('You have unsaved changes. Are you sure you want to load a new file and discard them?')) {
e.target.value = '';
return;
}
}
const reader = new FileReader();
reader.onload = (evt) => {
const bstr = evt.target?.result;
const wb = XLSX.read(bstr, { type: 'binary' });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const data = XLSX.utils.sheet_to_json<any[]>(ws, { header: 1 });
if (data.length > 0) {
const headers = data[0];
const rawRows = data.slice(1);
const processedRows = rawRows.map(row => {
return row.map((val, idx) => {
if (val === undefined || val === null || val === '') return val;
const header = (headers[idx] || '').toLowerCase();
if (header.includes('id') || header.includes('no') || header.includes('code') ||
header.includes('art.') || header.includes('barcode') || header.includes('article')) {
// But allow if it's a weight/measure column (e.g. Article NW (kg))
if (!(header.includes('nw') || header.includes('gw') || header.includes('weight') || header.includes('kg'))) {
return val;
}
}
// Handle date columns - Excel serial dates are numbers >= 25569 (Jan 1, 1970)
if (header.includes('date') || header.includes('launch') || header.includes('ready')) {
if (typeof val === 'number' && val >= 25569 && val <= 60000) {
const excelEpoch = new Date(1899, 11, 30);
const date = new Date(excelEpoch.getTime() + val * 86400000);
return date.toISOString().split('T')[0]; // Returns YYYY-MM-DD
}
return val;
}
const formatKeywords = ['price', 'eur', 'cost', 'msrp', 'net', 'gross', 'netto', 'brutto', 'pp', 'pph', 'uvp', 'vpe', 'stk', 'nw', 'gw', 'weight', 'kg'];
const shouldFormat = formatKeywords.some(kw => header.includes(kw));
if (typeof val === 'number') {
return Number(val.toFixed(2));
}
if (typeof val === 'string') {
const normalized = val.trim().replace(',', '.');
const num = parseFloat(normalized);
if (!isNaN(num) && (shouldFormat || val.includes('.') || val.includes(','))) {
return num.toFixed(2);
}
}
return val;
});
});
setAppState({
headers: headers,
data: processedRows,
fileName: file.name,
fileDate: new Date(),
hasUnsavedChanges: false,
asinColumnIndex: null
});
setActiveModule('descriptions');
}
};
reader.readAsBinaryString(file);
};
const handleExport = () => {
if (appState.data.length === 0) return;
const wsData = [
appState.headers,
...appState.data.map(row => {
const copy = [...row];
delete copy[COLUMNS.VERIFIED_DIMS];
return copy.slice(0, appState.headers.length);
})
];
const ws = XLSX.utils.aoa_to_sheet(wsData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Products');
const dateStr = new Date().toISOString().split('T')[0];
XLSX.writeFile(wb, `CRAZE_Products_Updated_${dateStr}.xlsx`);
// 3. Post-export: Reset pending statuses in Supabase
console.log('Resetting pending statuses in Supabase...');
resetAllPendingRows().then(success => {
if (success) {
console.log('Successfully reset all pending statuses');
setRowStatuses({}); // Clear local statuses
}
});
setAppState(prev => ({ ...prev, hasUnsavedChanges: false }));
};
const handleSaveRow = (rowIndex: number, updatedRow: ExcelRow) => {
const articleNo = String(updatedRow[COLUMNS.ARTICLE_NO]);
const currentPending = pendingRows[articleNo];
const originalData = currentPending?.originalData ?? appState.data[rowIndex];
const isRowDifferent = (rowA: any[], rowB: any[]) => {
if (!rowA || !rowB) return rowA !== rowB;
const length = Math.max(rowA.length, rowB.length);
for (let i = 0; i < length; i++) {
const a = rowA[i];
const b = rowB[i];
if (a === b) continue;
const normA = (a === null || a === undefined || a === '' || a === false) ? null : a;
const normB = (b === null || b === undefined || b === '' || b === false) ? null : b;
if (normA === normB) continue;
if (String(a) === String(b)) continue;
return true;
}
return false;
};
const isActuallyModified = isRowDifferent(updatedRow, originalData);
if (isActuallyModified) {
setAppState(prev => {
const newData = [...prev.data];
newData[rowIndex] = updatedRow;
return { ...prev, data: newData, hasUnsavedChanges: true };
});
setPendingRows(prev => ({
...prev,
[articleNo]: {
rowIndex,
originalData,
newData: updatedRow,
articleName: String(updatedRow[COLUMNS.ARTICLE_NAME] || articleNo),
}
}));
setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' }));
} else {
// Changed back to original state - remove from pending
setPendingRows(prev => {
const n = { ...prev };
delete n[articleNo];
return n;
});
setRowStatuses(prev => {
const n = { ...prev };
delete n[articleNo];
return n;
});
setAppState(prev => {
const newData = [...prev.data];
newData[rowIndex] = updatedRow;
// Use a more reliable way to check if there are still other pending rows
const otherPendingCount = Object.keys(pendingRows).filter(k => k !== articleNo).length;
const stillHasChanges = otherPendingCount > 0;
return { ...prev, data: newData, hasUnsavedChanges: stillHasChanges };
});
}
setEditingRowIndex(null);
};
const handleRevertRow = (articleNo: string) => {
const pending = pendingRows[articleNo];
if (!pending) return;
setAppState(prev => {
const newData = [...prev.data];
newData[pending.rowIndex] = pending.originalData;
const stillPending = Object.keys(pendingRows).length > 0;
return { ...prev, data: newData, hasUnsavedChanges: stillPending };
});
setPendingRows(prev => { const n = { ...prev }; delete n[articleNo]; return n; });
setRowStatuses(prev => { const n = { ...prev }; delete n[articleNo]; return n; });
};
const handleSaveAll = async () => {
console.log('[handleSaveAll] Starting save, pendingRows:', pendingRows);
const entries = Object.entries(pendingRows) as [string, { rowIndex: number; originalData: ExcelRow; newData: ExcelRow; articleName: string }][];
if (entries.length === 0) {
console.log('[handleSaveAll] No entries to save, returning');
return;
}
setIsSavingAll(true);
let failedArticles: string[] = [];
let sessionIssue = false;
try {
for (const [articleNo, { newData, originalData, articleName }] of entries) {
console.log('[handleSaveAll] Saving article:', articleNo);
const result = await saveRowToSupabase(articleNo, newData);
console.log('[handleSaveAll] Save result for', articleNo, ':', result);
if (result.success) {
// Also save to history
await saveHistoryEntry(articleNo, articleName, originalData, newData, session?.user?.email || 'unknown');
setRowStatuses(prev => ({ ...prev, [articleNo]: 'saved' }));
setPendingRows(prev => {
const n = { ...prev };
delete n[articleNo];
return n;
});
} else {
setRowStatuses(prev => ({ ...prev, [articleNo]: 'error' }));
failedArticles.push(`${articleNo} [${result.error || 'Unknown error'}]`);
if (result.error?.includes('401') || result.error?.includes('expired') || result.error?.includes('Session expired')) {
sessionIssue = true;
}
}
}
console.log('[handleSaveAll] Finished loop. Failed:', failedArticles.length);
if (sessionIssue) {
alert("Tu sesión ha caducado definitivamente. Por favor, cierra sesión e inicia sesión de nuevo para continuar. No refresques la página para no perder tus cambios pendientes en pantalla.");
} else if (failedArticles.length > 0) {
alert(`Failed to save items:\n\n${failedArticles.join('\n')}\n\nPlease try again.`);
} else {
setAppState(prev => ({ ...prev, hasUnsavedChanges: false }));
}
2026-04-10 12:16:06 +02:00
} catch (err: any) {
console.error('[handleSaveAll] Critical error:', err);
2026-04-10 12:16:06 +02:00
alert(`A critical error occurred while saving: ${err.message || err}\nCheck console for details.`);
} finally {
setIsSavingAll(false);
console.log('[handleSaveAll] isSavingAll set to false');
2026-03-27 12:23:35 +01:00
}
};
const captureState = (message: string) => {
setUndoHistory(prev => {
const newState = {
data: JSON.parse(JSON.stringify(appState.data)), // Deep copy
rowStatuses: { ...rowStatuses },
pendingRows: { ...pendingRows },
message
};
// Keep last 50 steps
const newHistory = [newState, ...prev].slice(0, 50);
return newHistory;
});
};
const handleUndo = () => {
if (undoHistory.length === 0) return;
const [lastAction, ...remainingHistory] = undoHistory;
// Restore data
setAppState(prev => ({
...prev,
data: lastAction.data,
hasUnsavedChanges: Object.keys(lastAction.pendingRows || {}).length > 0
}));
// Restore statuses and pending state
if (lastAction.rowStatuses) setRowStatuses(lastAction.rowStatuses);
if (lastAction.pendingRows) setPendingRows(lastAction.pendingRows);
setUndoHistory(remainingHistory);
};
const stats = useMemo(() => {
if (appState.data.length === 0) return null;
let missingDeLong = 0;
let missingEnLong = 0;
let missingDeShort = 0;
let missingEnShort = 0;
let fullyComplete = 0;
appState.data.forEach(row => {
const deLong = row[COLUMNS.LONG_DE];
const enLong = row[COLUMNS.LONG_EN];
const deShort = row[COLUMNS.SHORT_DE];
const enShort = row[COLUMNS.SHORT_EN];
if (!deLong) missingDeLong++;
if (!enLong) missingEnLong++;
if (!deShort) missingDeShort++;
if (!enShort) missingEnShort++;
if (deLong && enLong && deShort && enShort) fullyComplete++;
});
return {
total: appState.data.length,
missingDeLong,
missingEnLong,
missingDeShort,
missingEnShort,
fullyComplete
};
}, [appState.data]);
const [isMaximized, setIsMaximized] = useState(false);
if (!session) {
return <LoginPage onLogin={() => {
const stored = getStoredSession();
console.log('[App] onLogin, stored session:', stored ? 'found' : 'null');
setSession(stored);
}} />;
}
return (
<ColumnsProvider headers={appState.headers}>
<div className="h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden">
{!isMaximized && (
<TopBar
stats={stats}
activeModule={activeModule}
onExport={handleExport}
onRefresh={() => { setRefreshTrigger(t => t + 1); }}
hasData={appState.data.length > 0}
hasUnsavedChanges={appState.hasUnsavedChanges}
userEmail={session.user.email}
onSignOut={handleSignOut}
canUndo={undoHistory.length > 0}
onUndo={handleUndo}
undoMessage={undoHistory[0]?.message}
undoSteps={undoHistory.length}
pendingCount={Object.keys(pendingRows).length}
pendingChanges={Object.fromEntries(Object.entries(pendingRows).map(([k, v]) => [k, { articleName: (v as any).articleName }]))}
onSaveAll={handleSaveAll}
onRevertRow={handleRevertRow}
isSavingAll={isSavingAll}
isMaximized={isMaximized}
onToggleMaximize={() => setIsMaximized(true)}
/>
)}
<div className="flex flex-1 overflow-hidden">
{!isMaximized && (
<Sidebar activeModule={activeModule} setActiveModule={setActiveModule} userEmail={session.user.email} />
)}
<main className={cn(
"flex-1 overflow-auto relative bg-[#041021] transition-all duration-300",
isMaximized ? "p-0" : "p-6"
)}>
{isMaximized && (
<button
onClick={() => setIsMaximized(false)}
className="fixed top-4 right-4 z-[100] p-3 bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white rounded-full shadow-2xl border border-slate-700 transition-all active:scale-95 group"
title="Exit Fullscreen"
>
<X className="w-6 h-6" />
<span className="absolute right-full mr-2 top-1/2 -translate-y-1/2 px-2 py-1 bg-slate-900 text-xs text-white rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
Exit Fullscreen
</span>
</button>
)}
2026-03-27 12:03:57 +01:00
{isLoadingDefault ? (
<div className="flex flex-col items-center justify-center h-full text-slate-400">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
<p className="text-lg">Loading latest data...</p>
</div>
) : defaultLoadError && appState.data.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-red-400">
<p className="mb-4 text-lg">Failed to auto-load data: {defaultLoadError}</p>
<label className="cursor-pointer bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg shadow-lg transition-colors">
Load Excel File Manually
<input type="file" accept=".xlsx, .xls" className="hidden" onChange={handleFileUpload} />
</label>
</div>
) : appState.data.length === 0 && activeModule !== 'upload' ? (
<div className="flex flex-col items-center justify-center h-full text-slate-400">
<p className="mb-4 text-lg">No data loaded.</p>
<label className="cursor-pointer bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg shadow-lg transition-colors">
Load Excel File
<input type="file" accept=".xlsx, .xls" className="hidden" onChange={handleFileUpload} />
</label>
</div>
) : (
<>
{activeModule === 'descriptions' && (
<ProductDescriptions
data={appState.data}
headers={appState.headers}
asinColumnIndex={appState.asinColumnIndex}
onEdit={(index) => setEditingRowIndex(index)}
rowStatuses={rowStatuses}
/>
)}
{activeModule === 'matrix' && (
<MatrixView data={appState.data} headers={appState.headers} rowStatuses={rowStatuses} />
)}
2026-03-29 17:10:27 +02:00
{activeModule === 'dimensions' && (
<DimensionsView
data={appState.data}
headers={appState.headers}
onEdit={(index) => setEditingRowIndex(index)}
onSaveRow={handleSaveRow}
onCaptureState={captureState}
rowStatuses={rowStatuses}
onRevertRow={handleRevertRow}
/>
)}
{activeModule === 'pricing' && (
<PricingView
data={appState.data}
headers={appState.headers}
onSaveRow={handleSaveRow}
onCaptureState={captureState}
onEdit={(index) => setEditingRowIndex(index)}
rowStatuses={rowStatuses}
/>
)}
{activeModule === 'article_details' && (
<ArticleDetails
data={appState.data}
onEdit={(index) => setEditingRowIndex(index)}
rowStatuses={rowStatuses}
/>
)}
{activeModule === 'pending_validation' && (
<PendingValidationView
data={appState.data}
pendingRows={pendingRows}
rowStatuses={rowStatuses}
onRevertRow={handleRevertRow}
onEdit={(index) => setEditingRowIndex(index)}
/>
)}
{activeModule === 'missing_data' && (
<MissingDataView
data={appState.data}
headers={appState.headers}
onSaveRow={handleSaveRow}
onCaptureState={captureState}
/>
)}
{activeModule === 'history' && (
<HistoryView
headers={appState.headers}
data={appState.data}
onEdit={(index) => setEditingRowIndex(index)}
2026-04-10 09:47:35 +02:00
onRevert={async (articleNo, revertedData, historyId) => {
// Find the row in appState.data and update it
const rowIndex = appState.data.findIndex(r => String(r[COLUMNS.ARTICLE_NO]) === articleNo);
if (rowIndex !== -1) {
setAppState(prev => {
const newData = [...prev.data];
newData[rowIndex] = revertedData;
return { ...prev, data: newData, hasUnsavedChanges: true };
});
// Mark as pending for sync
setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' }));
setPendingRows(prev => ({
...prev,
[articleNo]: {
rowIndex,
originalData: appState.data[rowIndex],
newData: revertedData,
articleName: String(revertedData[COLUMNS.ARTICLE_NAME] || articleNo),
}
}));
2026-04-10 09:47:35 +02:00
// Delete the history entry after revert
if (historyId) {
await deleteHistoryEntry(String(historyId));
2026-04-10 09:47:35 +02:00
}
}
}}
/>
)}
</>
)}
</main>
</div>
<UndoToast
undoState={undoHistory[0] || null}
onUndo={handleUndo}
onClose={() => {
// Instead of clearing history, we can just hide the toast
// But since UndoToast is driven by undoHistory[0],
// we might want a way to "acknowledge" the current top of history
// For now, let's just not clear the history.
}}
/>
{editingRowIndex !== null && (
<EditPanel
row={appState.data[editingRowIndex]}
rowIndex={editingRowIndex}
onSave={handleSaveRow}
onClose={() => setEditingRowIndex(null)}
onCaptureState={captureState}
/>
)}
</div>
</ColumnsProvider>
);
}