2026-03-27 12:03:57 +01:00
|
|
|
import React, { useState, useMemo, useEffect } from 'react';
|
2026-03-27 11:34:09 +01:00
|
|
|
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';
|
2026-04-10 09:47:35 +02:00
|
|
|
import { getAllSyncedRows, saveRowToSupabase, resetAllPendingRows, saveHistoryEntry, deleteHistoryEntry } from './lib/supabase';
|
2026-03-27 17:49:10 +01:00
|
|
|
import { getStoredSession, signOut, type AuthSession } from './lib/auth';
|
|
|
|
|
import { LoginPage } from './components/LoginPage';
|
2026-03-29 17:30:04 +02:00
|
|
|
import { DimensionsView } from './components/DimensionsView';
|
2026-04-07 11:23:34 +02:00
|
|
|
import { PricingView } from './components/PricingView';
|
2026-04-08 16:10:14 +02:00
|
|
|
import { ArticleDetails } from './components/ArticleDetails';
|
2026-04-09 09:24:17 +02:00
|
|
|
import { HistoryView } from './components/HistoryView';
|
2026-03-29 17:35:07 +02:00
|
|
|
import { UndoToast } from './components/UndoToast';
|
2026-04-09 09:24:17 +02:00
|
|
|
import { PendingValidationView } from './components/PendingValidationView';
|
2026-03-27 11:34:09 +01:00
|
|
|
|
|
|
|
|
export default function App() {
|
2026-03-27 17:49:10 +01:00
|
|
|
const [session, setSession] = useState<AuthSession | null>(() => getStoredSession());
|
|
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
const [appState, setAppState] = useState<AppState>({
|
|
|
|
|
headers: [],
|
|
|
|
|
data: [],
|
|
|
|
|
fileName: '',
|
|
|
|
|
fileDate: null,
|
2026-04-09 16:33:05 +02:00
|
|
|
hasUnsavedChanges: false,
|
|
|
|
|
asinColumnIndex: null
|
2026-03-27 11:34:09 +01:00
|
|
|
});
|
2026-04-09 09:24:17 +02:00
|
|
|
const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history'>('descriptions');
|
2026-03-29 17:50:31 +02:00
|
|
|
const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]);
|
2026-03-27 11:34:09 +01:00
|
|
|
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);
|
2026-04-08 19:30:41 +02:00
|
|
|
const [rowStatuses, setRowStatuses] = useState<Record<string, string>>({});
|
2026-04-09 08:58:31 +02:00
|
|
|
const [pendingRows, setPendingRows] = useState<Record<string, { rowIndex: number; originalData: ExcelRow; newData: ExcelRow; articleName: string }>>({});
|
2026-04-09 08:45:22 +02:00
|
|
|
const [isSavingAll, setIsSavingAll] = useState(false);
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 12:03:57 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
const loadDefaultData = async () => {
|
2026-03-27 12:07:22 +01:00
|
|
|
setIsLoadingDefault(true);
|
|
|
|
|
setDefaultLoadError(null);
|
|
|
|
|
|
2026-03-27 12:28:30 +01:00
|
|
|
try {
|
2026-04-10 10:54:50 +02:00
|
|
|
const isDev = import.meta.env.DEV;
|
|
|
|
|
|
|
|
|
|
let rows: any[][];
|
2026-04-10 11:01:55 +02:00
|
|
|
let allData: any[][];
|
2026-04-10 10:54:50 +02:00
|
|
|
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];
|
2026-04-10 11:01:55 +02:00
|
|
|
allData = XLSX.utils.sheet_to_json<any[]>(ws, { header: 1 });
|
|
|
|
|
rows = allData.slice(1);
|
2026-04-10 10:54:50 +02:00
|
|
|
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-09 16:29:54 +02:00
|
|
|
}
|
2026-04-10 10:54:50 +02:00
|
|
|
|
|
|
|
|
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];
|
2026-04-10 11:01:55 +02:00
|
|
|
allData = XLSX.utils.sheet_to_json<any[]>(ws, { header: 1 });
|
|
|
|
|
rows = allData.slice(1);
|
2026-04-10 10:54:50 +02:00
|
|
|
}
|
2026-04-09 16:29:54 +02:00
|
|
|
|
2026-04-10 10:54:50 +02:00
|
|
|
if (rows.length > 0) {
|
2026-04-10 11:01:55 +02:00
|
|
|
const headers = allData.slice(0, 1)[0];
|
2026-04-10 10:54:50 +02:00
|
|
|
|
|
|
|
|
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 {
|
2026-04-10 11:01:55 +02:00
|
|
|
const errorText = await syncRes.text();
|
|
|
|
|
console.warn('Supabase sync failed:', syncRes.status, errorText);
|
2026-04-10 10:54:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Fetching synced data from Supabase...');
|
2026-04-10 12:03:12 +02:00
|
|
|
const syncedData = await getAllSyncedRows(session?.access_token);
|
2026-04-10 10:54:50 +02:00
|
|
|
|
2026-03-27 12:28:30 +01:00
|
|
|
const articleNoIdx = COLUMNS.ARTICLE_NO;
|
2026-04-10 10:54:50 +02:00
|
|
|
const processedRows = rows.map(row => {
|
2026-03-27 12:28:30 +01:00
|
|
|
const articleNo = String(row[articleNoIdx]);
|
2026-04-08 19:30:41 +02:00
|
|
|
const synced = syncedData[articleNo];
|
|
|
|
|
const finalRow = synced ? synced.data : row;
|
2026-04-10 10:54:50 +02:00
|
|
|
|
2026-04-08 19:30:41 +02:00
|
|
|
if (synced && synced.status === 'pending') {
|
|
|
|
|
setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' }));
|
|
|
|
|
}
|
2026-04-10 10:54:50 +02:00
|
|
|
|
|
|
|
|
return finalRow.map((val: any, idx: number) => {
|
2026-03-29 14:46:05 +02:00
|
|
|
if (val === undefined || val === null || val === '') return val;
|
2026-04-10 11:01:55 +02:00
|
|
|
const header = (headers[idx] || '').toLowerCase();
|
2026-04-10 10:54:50 +02:00
|
|
|
|
2026-04-08 19:32:28 +02:00
|
|
|
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'))) {
|
2026-03-29 14:46:05 +02:00
|
|
|
return val;
|
|
|
|
|
}
|
2026-04-10 10:54:50 +02:00
|
|
|
|
2026-03-29 14:55:30 +02:00
|
|
|
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));
|
2026-04-10 10:54:50 +02:00
|
|
|
|
2026-03-29 14:46:05 +02:00
|
|
|
if (typeof val === 'number') {
|
|
|
|
|
return Number(val.toFixed(2));
|
|
|
|
|
}
|
2026-04-10 10:54:50 +02:00
|
|
|
|
2026-03-29 14:46:05 +02:00
|
|
|
if (typeof val === 'string') {
|
|
|
|
|
const normalized = val.trim().replace(',', '.');
|
|
|
|
|
const num = parseFloat(normalized);
|
2026-03-29 14:55:30 +02:00
|
|
|
if (!isNaN(num) && (shouldFormat || val.includes('.') || val.includes(','))) {
|
2026-03-29 14:46:05 +02:00
|
|
|
return num.toFixed(2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
});
|
2026-03-27 12:28:30 +01:00
|
|
|
});
|
2026-04-10 10:54:50 +02:00
|
|
|
|
2026-04-10 11:01:55 +02:00
|
|
|
const asinIdx = (headers as string[]).findIndex((h: string) =>
|
2026-04-10 10:54:50 +02:00
|
|
|
String(h).toLowerCase().trim() === 'asin'
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-27 12:28:30 +01:00
|
|
|
setAppState({
|
2026-04-10 11:01:55 +02:00
|
|
|
headers: headers,
|
2026-03-27 12:28:30 +01:00
|
|
|
data: processedRows,
|
|
|
|
|
fileName: 'Data-Matrix.xlsx (Cloud Sync)',
|
|
|
|
|
fileDate: new Date(),
|
2026-04-09 16:33:05 +02:00
|
|
|
hasUnsavedChanges: false,
|
|
|
|
|
asinColumnIndex: asinIdx !== -1 ? asinIdx : null
|
2026-03-27 12:28:30 +01:00
|
|
|
});
|
|
|
|
|
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
|
|
|
};
|
|
|
|
|
|
2026-04-10 08:32:03 +02:00
|
|
|
if (session) loadDefaultData();
|
|
|
|
|
}, [session]);
|
2026-03-27 11:34:09 +01:00
|
|
|
|
|
|
|
|
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 });
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
if (data.length > 0) {
|
2026-04-10 11:01:55 +02:00
|
|
|
const headers = data[0];
|
2026-03-29 14:46:05 +02:00
|
|
|
const rawRows = data.slice(1);
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-03-29 14:46:05 +02:00
|
|
|
const processedRows = rawRows.map(row => {
|
|
|
|
|
return row.map((val, idx) => {
|
|
|
|
|
if (val === undefined || val === null || val === '') return val;
|
2026-04-10 11:01:55 +02:00
|
|
|
const header = (headers[idx] || '').toLowerCase();
|
2026-04-08 19:32:28 +02:00
|
|
|
|
|
|
|
|
if (header.includes('id') || header.includes('no') || header.includes('code') ||
|
|
|
|
|
header.includes('art.') || header.includes('barcode') || header.includes('article')) {
|
2026-03-29 14:55:30 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
2026-03-29 14:46:05 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-29 17:17:50 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 14:55:30 +02:00
|
|
|
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));
|
2026-03-29 14:53:17 +02:00
|
|
|
|
2026-03-29 14:46:05 +02:00
|
|
|
if (typeof val === 'number') {
|
|
|
|
|
return Number(val.toFixed(2));
|
|
|
|
|
}
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-03-29 14:46:05 +02:00
|
|
|
if (typeof val === 'string') {
|
|
|
|
|
const normalized = val.trim().replace(',', '.');
|
|
|
|
|
const num = parseFloat(normalized);
|
2026-03-29 14:55:30 +02:00
|
|
|
if (!isNaN(num) && (shouldFormat || val.includes('.') || val.includes(','))) {
|
2026-03-29 14:46:05 +02:00
|
|
|
return num.toFixed(2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
setAppState({
|
2026-04-10 11:01:55 +02:00
|
|
|
headers: headers,
|
2026-03-29 14:46:05 +02:00
|
|
|
data: processedRows,
|
2026-03-27 11:34:09 +01:00
|
|
|
fileName: file.name,
|
|
|
|
|
fileDate: new Date(),
|
2026-04-09 16:33:05 +02:00
|
|
|
hasUnsavedChanges: false,
|
|
|
|
|
asinColumnIndex: null
|
2026-03-27 11:34:09 +01:00
|
|
|
});
|
|
|
|
|
setActiveModule('descriptions');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
reader.readAsBinaryString(file);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleExport = () => {
|
|
|
|
|
if (appState.data.length === 0) return;
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
const wsData = [appState.headers, ...appState.data];
|
|
|
|
|
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Products');
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
const dateStr = new Date().toISOString().split('T')[0];
|
|
|
|
|
XLSX.writeFile(wb, `CRAZE_Products_Updated_${dateStr}.xlsx`);
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-04-08 19:30:41 +02:00
|
|
|
// 3. Post-export: Reset pending statuses in Supabase
|
|
|
|
|
console.log('Resetting pending statuses in Supabase...');
|
2026-04-10 12:03:12 +02:00
|
|
|
resetAllPendingRows(session?.access_token).then(success => {
|
2026-04-08 19:30:41 +02:00
|
|
|
if (success) {
|
|
|
|
|
console.log('Successfully reset all pending statuses');
|
|
|
|
|
setRowStatuses({}); // Clear local statuses
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
setAppState(prev => ({ ...prev, hasUnsavedChanges: false }));
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-09 08:45:22 +02:00
|
|
|
const handleSaveRow = (rowIndex: number, updatedRow: ExcelRow) => {
|
2026-04-09 08:58:31 +02:00
|
|
|
const articleNo = String(updatedRow[COLUMNS.ARTICLE_NO]);
|
|
|
|
|
const originalData = appState.data[rowIndex]; // Capture before update
|
2026-03-27 11:34:09 +01:00
|
|
|
setAppState(prev => {
|
|
|
|
|
const newData = [...prev.data];
|
|
|
|
|
newData[rowIndex] = updatedRow;
|
2026-04-09 08:22:58 +02:00
|
|
|
return { ...prev, data: newData, hasUnsavedChanges: true };
|
2026-03-27 11:34:09 +01:00
|
|
|
});
|
2026-04-09 08:58:31 +02:00
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
}));
|
2026-04-08 19:30:41 +02:00
|
|
|
setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' }));
|
2026-04-09 08:45:22 +02:00
|
|
|
setEditingRowIndex(null);
|
|
|
|
|
};
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-04-09 08:58:31 +02:00
|
|
|
const handleRevertRow = (articleNo: string) => {
|
|
|
|
|
const pending = pendingRows[articleNo];
|
|
|
|
|
if (!pending) return;
|
|
|
|
|
setAppState(prev => {
|
|
|
|
|
const newData = [...prev.data];
|
|
|
|
|
newData[pending.rowIndex] = pending.originalData;
|
2026-04-10 10:37:40 +02:00
|
|
|
const stillPending = Object.keys(pendingRows).length > 0;
|
2026-04-09 08:58:31 +02:00
|
|
|
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; });
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-09 08:45:22 +02:00
|
|
|
const handleSaveAll = async () => {
|
2026-04-10 10:45:11 +02:00
|
|
|
console.log('[handleSaveAll] Starting save, pendingRows:', pendingRows);
|
2026-04-09 08:58:31 +02:00
|
|
|
const entries = Object.entries(pendingRows) as [string, { rowIndex: number; originalData: ExcelRow; newData: ExcelRow; articleName: string }][];
|
2026-04-10 10:45:11 +02:00
|
|
|
if (entries.length === 0) {
|
|
|
|
|
console.log('[handleSaveAll] No entries to save, returning');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-10 12:03:12 +02:00
|
|
|
|
2026-04-09 08:45:22 +02:00
|
|
|
setIsSavingAll(true);
|
2026-04-10 12:03:12 +02:00
|
|
|
let failedArticles: string[] = [];
|
|
|
|
|
const token = session?.access_token;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
for (const [articleNo, { newData, originalData, articleName }] of entries) {
|
|
|
|
|
console.log('[handleSaveAll] Saving article:', articleNo);
|
2026-04-10 12:06:16 +02:00
|
|
|
const result = await saveRowToSupabase(articleNo, newData, token);
|
|
|
|
|
console.log('[handleSaveAll] Save result for', articleNo, ':', result);
|
2026-04-09 09:24:17 +02:00
|
|
|
|
2026-04-10 12:06:16 +02:00
|
|
|
if (result.success) {
|
2026-04-10 12:03:12 +02:00
|
|
|
// 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' }));
|
2026-04-10 12:06:16 +02:00
|
|
|
failedArticles.push(`${articleNo} [${result.error || 'Unknown error'}]`);
|
2026-04-10 12:03:12 +02:00
|
|
|
}
|
2026-04-09 08:45:22 +02:00
|
|
|
}
|
2026-04-10 12:03:12 +02:00
|
|
|
|
|
|
|
|
console.log('[handleSaveAll] Finished loop. Failed:', failedArticles.length);
|
|
|
|
|
|
|
|
|
|
if (failedArticles.length > 0) {
|
2026-04-10 12:06:16 +02:00
|
|
|
alert(`Failed to save items:\n\n${failedArticles.join('\n')}\n\nPlease try again.`);
|
2026-04-10 12:03:12 +02:00
|
|
|
} else {
|
|
|
|
|
setAppState(prev => ({ ...prev, hasUnsavedChanges: false }));
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[handleSaveAll] Critical error:', err);
|
|
|
|
|
alert('A critical error occurred while saving. Please check your connection and try again.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSavingAll(false);
|
|
|
|
|
console.log('[handleSaveAll] isSavingAll set to false');
|
2026-03-27 12:23:35 +01:00
|
|
|
}
|
2026-03-27 11:34:09 +01:00
|
|
|
};
|
|
|
|
|
|
2026-03-29 17:35:07 +02:00
|
|
|
const captureState = (message: string) => {
|
2026-03-29 17:50:31 +02:00
|
|
|
setUndoHistory(prev => {
|
|
|
|
|
const newState = {
|
|
|
|
|
data: JSON.parse(JSON.stringify(appState.data)), // Deep copy
|
|
|
|
|
message
|
|
|
|
|
};
|
2026-04-08 20:11:00 +02:00
|
|
|
// Keep last 50 steps
|
|
|
|
|
const newHistory = [newState, ...prev].slice(0, 50);
|
2026-03-29 17:50:31 +02:00
|
|
|
return newHistory;
|
2026-03-29 17:35:07 +02:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUndo = () => {
|
2026-03-29 17:50:31 +02:00
|
|
|
if (undoHistory.length === 0) return;
|
2026-04-08 19:32:28 +02:00
|
|
|
|
2026-03-29 17:50:31 +02:00
|
|
|
const [lastAction, ...remainingHistory] = undoHistory;
|
2026-03-29 17:35:07 +02:00
|
|
|
setAppState(prev => ({
|
|
|
|
|
...prev,
|
2026-03-29 17:50:31 +02:00
|
|
|
data: lastAction.data,
|
2026-03-29 17:35:07 +02:00
|
|
|
hasUnsavedChanges: true
|
|
|
|
|
}));
|
2026-03-29 17:50:31 +02:00
|
|
|
setUndoHistory(remainingHistory);
|
2026-03-29 17:35:07 +02:00
|
|
|
};
|
|
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
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]);
|
|
|
|
|
|
2026-04-10 08:32:03 +02:00
|
|
|
if (!session) {
|
|
|
|
|
return <LoginPage onLogin={() => {
|
|
|
|
|
const stored = getStoredSession();
|
|
|
|
|
console.log('[App] onLogin, stored session:', stored ? 'found' : 'null');
|
|
|
|
|
setSession(stored);
|
|
|
|
|
}} />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
return (
|
2026-03-27 18:18:06 +01:00
|
|
|
<div className="h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden">
|
2026-03-27 17:49:10 +01:00
|
|
|
<TopBar
|
|
|
|
|
stats={stats}
|
|
|
|
|
onExport={handleExport}
|
|
|
|
|
hasData={appState.data.length > 0}
|
2026-03-27 11:34:09 +01:00
|
|
|
hasUnsavedChanges={appState.hasUnsavedChanges}
|
2026-03-27 17:49:10 +01:00
|
|
|
userEmail={session.user.email}
|
|
|
|
|
onSignOut={handleSignOut}
|
2026-03-29 17:50:31 +02:00
|
|
|
canUndo={undoHistory.length > 0}
|
2026-03-29 17:37:58 +02:00
|
|
|
onUndo={handleUndo}
|
2026-03-29 17:50:31 +02:00
|
|
|
undoMessage={undoHistory[0]?.message}
|
|
|
|
|
undoSteps={undoHistory.length}
|
2026-04-09 08:45:22 +02:00
|
|
|
pendingCount={Object.keys(pendingRows).length}
|
2026-04-09 08:58:31 +02:00
|
|
|
pendingChanges={Object.fromEntries(Object.entries(pendingRows).map(([k, v]) => [k, { articleName: (v as any).articleName }]))}
|
2026-04-09 08:45:22 +02:00
|
|
|
onSaveAll={handleSaveAll}
|
2026-04-09 08:58:31 +02:00
|
|
|
onRevertRow={handleRevertRow}
|
2026-04-09 08:45:22 +02:00
|
|
|
isSavingAll={isSavingAll}
|
2026-03-27 11:34:09 +01:00
|
|
|
/>
|
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
|
|
|
<Sidebar activeModule={activeModule} setActiveModule={setActiveModule} />
|
2026-03-27 18:18:06 +01:00
|
|
|
<main className="flex-1 overflow-auto relative p-6 bg-[#041021]">
|
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' ? (
|
2026-03-27 11:34:09 +01:00
|
|
|
<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' && (
|
2026-04-08 19:32:28 +02:00
|
|
|
<ProductDescriptions
|
|
|
|
|
data={appState.data}
|
2026-04-09 16:33:05 +02:00
|
|
|
headers={appState.headers}
|
|
|
|
|
asinColumnIndex={appState.asinColumnIndex}
|
2026-04-08 19:32:28 +02:00
|
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
2026-04-08 19:49:48 +02:00
|
|
|
rowStatuses={rowStatuses}
|
2026-03-27 11:34:09 +01:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{activeModule === 'matrix' && (
|
2026-04-08 19:30:41 +02:00
|
|
|
<MatrixView data={appState.data} headers={appState.headers} rowStatuses={rowStatuses} />
|
2026-03-27 11:34:09 +01:00
|
|
|
)}
|
2026-03-29 17:10:27 +02:00
|
|
|
|
2026-03-29 17:30:04 +02:00
|
|
|
{activeModule === 'dimensions' && (
|
2026-04-07 11:23:34 +02:00
|
|
|
<DimensionsView
|
|
|
|
|
data={appState.data}
|
2026-03-29 17:30:04 +02:00
|
|
|
headers={appState.headers}
|
|
|
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
|
|
|
|
onSaveRow={handleSaveRow}
|
2026-03-29 17:35:07 +02:00
|
|
|
onCaptureState={captureState}
|
2026-04-08 19:30:41 +02:00
|
|
|
rowStatuses={rowStatuses}
|
2026-04-09 09:13:18 +02:00
|
|
|
onRevertRow={handleRevertRow}
|
2026-03-29 17:30:04 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
2026-04-07 11:23:34 +02:00
|
|
|
|
|
|
|
|
{activeModule === 'pricing' && (
|
|
|
|
|
<PricingView
|
|
|
|
|
data={appState.data}
|
|
|
|
|
headers={appState.headers}
|
|
|
|
|
onSaveRow={handleSaveRow}
|
|
|
|
|
onCaptureState={captureState}
|
|
|
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
2026-04-08 19:30:41 +02:00
|
|
|
rowStatuses={rowStatuses}
|
2026-04-07 11:23:34 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
2026-04-08 16:10:14 +02:00
|
|
|
{activeModule === 'article_details' && (
|
|
|
|
|
<ArticleDetails
|
|
|
|
|
data={appState.data}
|
|
|
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
2026-04-08 19:30:41 +02:00
|
|
|
rowStatuses={rowStatuses}
|
2026-04-08 16:10:14 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
2026-04-09 09:24:17 +02:00
|
|
|
{activeModule === 'pending_validation' && (
|
|
|
|
|
<PendingValidationView
|
|
|
|
|
data={appState.data}
|
|
|
|
|
pendingRows={pendingRows}
|
|
|
|
|
rowStatuses={rowStatuses}
|
|
|
|
|
onRevertRow={handleRevertRow}
|
|
|
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{activeModule === 'history' && (
|
|
|
|
|
<HistoryView
|
|
|
|
|
headers={appState.headers}
|
2026-04-10 09:42:04 +02:00
|
|
|
data={appState.data}
|
2026-04-10 12:03:12 +02:00
|
|
|
sessionToken={session?.access_token}
|
2026-04-10 09:47:35 +02:00
|
|
|
onRevert={async (articleNo, revertedData, historyId) => {
|
2026-04-09 09:24:17 +02:00
|
|
|
// 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) {
|
2026-04-10 12:03:12 +02:00
|
|
|
await deleteHistoryEntry(String(historyId), session?.access_token);
|
2026-04-10 09:47:35 +02:00
|
|
|
}
|
2026-04-09 09:24:17 +02:00
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-03-27 11:34:09 +01:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-08 19:32:28 +02:00
|
|
|
<UndoToast
|
|
|
|
|
undoState={undoHistory[0] || null}
|
|
|
|
|
onUndo={handleUndo}
|
2026-04-08 20:11:00 +02:00
|
|
|
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.
|
|
|
|
|
}}
|
2026-03-29 17:35:07 +02:00
|
|
|
/>
|
|
|
|
|
|
2026-03-27 11:34:09 +01:00
|
|
|
{editingRowIndex !== null && (
|
2026-04-08 19:32:28 +02:00
|
|
|
<EditPanel
|
|
|
|
|
row={appState.data[editingRowIndex]}
|
2026-03-27 11:34:09 +01:00
|
|
|
rowIndex={editingRowIndex}
|
|
|
|
|
onSave={handleSaveRow}
|
|
|
|
|
onClose={() => setEditingRowIndex(null)}
|
2026-03-29 17:35:07 +02:00
|
|
|
onCaptureState={captureState}
|
2026-03-27 11:34:09 +01:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|