mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 17:45:23 +02:00
Adds a new 'Missing Class / Launch' tab in Article Details that surfaces items where Classification is empty or Launch Date is blank/01-01-1900. The tab shows Classification, Launch Date, and Ready to Order columns for quick reference. Column indices for Launch Date and Ready to Order are resolved dynamically from the Excel headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
627 lines
25 KiB
TypeScript
627 lines
25 KiB
TypeScript
import React, { useState, useMemo, useEffect } from 'react';
|
|
import * as XLSX from 'xlsx';
|
|
import { AppState, ExcelRow, COLUMNS } from './types';
|
|
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';
|
|
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';
|
|
|
|
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'>('descriptions');
|
|
const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]);
|
|
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
|
|
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);
|
|
|
|
useEffect(() => {
|
|
console.log('[App] session changed:', session ? 'logged in' : 'logged out');
|
|
}, [session]);
|
|
|
|
const handleSignOut = () => {
|
|
signOut();
|
|
setSession(null);
|
|
};
|
|
|
|
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) {
|
|
const fileUrl = '/dropbox-file/scl/fi/nkyabkq2jdwfudof2ovrl/Data-Matrix.xlsx?rlkey=1cz5fuabwipqqivihii7sybw0&st=io0g4wvb&dl=1';
|
|
console.log('Fetching Data-Matrix.xlsx from Dropbox...');
|
|
const response = await fetch(fileUrl);
|
|
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);
|
|
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);
|
|
}
|
|
|
|
console.log('Fetching Data-Matrix.xlsx from proxy...');
|
|
const response = await fetch('/api/dropbox-proxy');
|
|
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(session?.access_token);
|
|
|
|
const editableColumns = new Set([
|
|
COLUMNS.LONG_DE, COLUMNS.LONG_EN,
|
|
COLUMNS.SHORT_DE, COLUMNS.SHORT_EN,
|
|
COLUMNS.DETAILS_DE, COLUMNS.DETAILS_EN,
|
|
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
|
|
]);
|
|
|
|
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 = COLUMNS.ARTICLE_NO;
|
|
const processedRows = rows.map(row => {
|
|
const articleNo = String(row[articleNoIdx]);
|
|
const synced = syncedData[articleNo];
|
|
|
|
const finalRow = [...row];
|
|
if (synced) {
|
|
// Smart Merge: Only overlay fields we logically "own" via this app's editors
|
|
editableColumns.forEach(idx => {
|
|
if (synced.data[idx] !== undefined && synced.data[idx] !== null) {
|
|
finalRow[idx] = synced.data[idx];
|
|
}
|
|
});
|
|
|
|
if (synced.status === 'pending') {
|
|
setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' }));
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
if (session) loadDefaultData();
|
|
}, [session]);
|
|
|
|
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.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
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(session?.access_token).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 originalData = appState.data[rowIndex]; // Capture before update
|
|
setAppState(prev => {
|
|
const newData = [...prev.data];
|
|
newData[rowIndex] = updatedRow;
|
|
return { ...prev, data: newData, hasUnsavedChanges: true };
|
|
});
|
|
setPendingRows(prev => ({
|
|
...prev,
|
|
[articleNo]: {
|
|
rowIndex,
|
|
// Keep the very first originalData if already pending (re-edit case)
|
|
originalData: prev[articleNo]?.originalData ?? originalData,
|
|
newData: updatedRow,
|
|
articleName: String(updatedRow[COLUMNS.ARTICLE_NAME] || articleNo),
|
|
}
|
|
}));
|
|
setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' }));
|
|
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 jwtExpired = false;
|
|
const token = session?.access_token;
|
|
|
|
try {
|
|
for (const [articleNo, { newData, originalData, articleName }] of entries) {
|
|
console.log('[handleSaveAll] Saving article:', articleNo);
|
|
const result = await saveRowToSupabase(articleNo, newData, token);
|
|
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', token);
|
|
|
|
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('JWT expired')) {
|
|
jwtExpired = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('[handleSaveAll] Finished loop. Failed:', failedArticles.length);
|
|
|
|
if (jwtExpired) {
|
|
alert("Tu sesión ha caducado (JWT expired). Por favor, haz clic en el botón de Cerrar Sesión (Sign out) arriba a la derecha y vuelve a iniciar sesión para guardar tus cambios.");
|
|
} 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 }));
|
|
}
|
|
} catch (err: any) {
|
|
console.error('[handleSaveAll] Critical error:', err);
|
|
alert(`A critical error occurred while saving: ${err.message || err}\nCheck console for details.`);
|
|
} finally {
|
|
setIsSavingAll(false);
|
|
console.log('[handleSaveAll] isSavingAll set to false');
|
|
}
|
|
};
|
|
|
|
const captureState = (message: string) => {
|
|
setUndoHistory(prev => {
|
|
const newState = {
|
|
data: JSON.parse(JSON.stringify(appState.data)), // Deep copy
|
|
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;
|
|
setAppState(prev => ({
|
|
...prev,
|
|
data: lastAction.data,
|
|
hasUnsavedChanges: true
|
|
}));
|
|
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]);
|
|
|
|
if (!session) {
|
|
return <LoginPage onLogin={() => {
|
|
const stored = getStoredSession();
|
|
console.log('[App] onLogin, stored session:', stored ? 'found' : 'null');
|
|
setSession(stored);
|
|
}} />;
|
|
}
|
|
|
|
return (
|
|
<div className="h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden">
|
|
<TopBar
|
|
stats={stats}
|
|
onExport={handleExport}
|
|
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}
|
|
/>
|
|
<div className="flex flex-1 overflow-hidden">
|
|
<Sidebar activeModule={activeModule} setActiveModule={setActiveModule} userEmail={session.user.email} />
|
|
<main className="flex-1 overflow-auto relative p-6 bg-[#041021]">
|
|
{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} />
|
|
)}
|
|
|
|
{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}
|
|
headers={appState.headers}
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
|
rowStatuses={rowStatuses}
|
|
/>
|
|
)}
|
|
{activeModule === 'pending_validation' && (
|
|
<PendingValidationView
|
|
data={appState.data}
|
|
pendingRows={pendingRows}
|
|
rowStatuses={rowStatuses}
|
|
onRevertRow={handleRevertRow}
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
|
/>
|
|
)}
|
|
{activeModule === 'history' && (
|
|
<HistoryView
|
|
headers={appState.headers}
|
|
data={appState.data}
|
|
sessionToken={session?.access_token}
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
|
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),
|
|
}
|
|
}));
|
|
// Delete the history entry after revert
|
|
if (historyId) {
|
|
await deleteHistoryEntry(String(historyId), session?.access_token);
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</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>
|
|
);
|
|
}
|