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-04-12 15:11:57 +02:00
import { MissingDataView } from './components/MissingDataView' ;
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-12 15:11:57 +02:00
const [ activeModule , setActiveModule ] = useState < 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data' > ( '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-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 );
};
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 ) {
2026-04-20 20:39:23 +02:00
const cacheBuster = `t_ ${ Date . now () } ` ;
2026-04-21 08:40:54 +02:00
const fileUrl = `/dropbox-file/scl/fi/ulcgt5iyyvrkeynprhali/Data-Matrix.xlsx?rlkey=i9p9xgwzm0fr7jip91vxdeal5&st=k8jmibh4&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' });
2026-04-10 10:54:50 +02:00
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
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' });
2026-04-10 10:54:50 +02:00
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-04-10 12:31:57 +02:00
const editableColumns = new Set ([
2026-04-12 18:32:08 +02:00
COLUMNS . CLASSIFICATION ,
2026-04-10 12:31:57 +02:00
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 ,
2026-04-11 11:48:56 +02:00
COLUMNS . UNITS_OUTER , COLUMNS . MOQ ,
2026-04-20 18:46:50 +02:00
COLUMNS . VERIFIED_DIMS ,
COLUMNS . VALIDATED_CHECK ,
2026-04-21 09:24:02 +02:00
COLUMNS . VALIDATED_NOTE ,
COLUMNS . PRODUCT_TYPE
2026-04-10 12:31:57 +02:00
]);
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 );
}
});
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 ];
2026-04-10 10:54:50 +02:00
2026-04-10 12:31:57 +02:00
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' }));
}
2026-04-08 19:30:41 +02:00
}
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 ();
2026-04-20 20:30:21 +02:00
}, [ session , refreshTrigger ]);
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 );
2026-04-14 18:55:23 +02:00
return date . toISOString (). split ( 'T' )[ 0 ]; // Returns YYYY-MM-DD
2026-03-29 17:17:50 +02:00
}
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-04-11 11:48:56 +02:00
const wsData = [
appState . headers ,
... appState . data . map ( row => {
const copy = [... row ];
delete copy [ COLUMNS . VERIFIED_DIMS ];
return copy . slice ( 0 , appState . headers . length );
})
];
2026-03-27 11:34:09 +01:00
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 ]);
2026-04-21 08:55:50 +02:00
const currentPending = pendingRows [ articleNo ];
const originalData = currentPending ? . originalData ?? appState . data [ rowIndex ];
2026-04-21 09:05:14 +02:00
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 );
2026-04-21 08:55:50 +02:00
if ( isActuallyModified ) {
2026-04-21 09:01:17 +02:00
setAppState ( prev => {
const newData = [... prev . data ];
newData [ rowIndex ] = updatedRow ;
return { ... prev , data : newData , hasUnsavedChanges : true };
2026-04-21 08:55:50 +02:00
});
2026-04-21 09:01:17 +02:00
setPendingRows ( prev => ({
... prev ,
[ articleNo ] : {
rowIndex ,
originalData ,
newData : updatedRow ,
articleName : String ( updatedRow [ COLUMNS . ARTICLE_NAME ] || articleNo ),
}
}));
2026-04-21 08:55:50 +02:00
setRowStatuses ( prev => ({ ... prev , [ articleNo ] : 'pending' }));
} else {
2026-04-21 09:01:17 +02:00
// Changed back to original state - remove from pending
2026-04-21 08:55:50 +02:00
setPendingRows ( prev => {
const n = { ... prev };
delete n [ articleNo ];
return n ;
});
setRowStatuses ( prev => {
const n = { ... prev };
delete n [ articleNo ];
return n ;
});
2026-04-21 09:01:17 +02:00
setAppState ( prev => {
const newData = [... prev . data ];
newData [ rowIndex ] = updatedRow ;
2026-04-21 09:05:14 +02:00
// 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 ;
2026-04-21 09:01:17 +02:00
return { ... prev , data : newData , hasUnsavedChanges : stillHasChanges };
});
2026-04-21 08:55:50 +02:00
}
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 [] = [];
2026-04-10 12:09:49 +02:00
let jwtExpired = false ;
2026-04-10 12:25:58 +02:00
const token = session ? . access_token ;
2026-04-10 12:03:12 +02:00
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:09:49 +02:00
if ( result . error ? . includes ( '401' ) || result . error ? . includes ( 'JWT expired' )) {
jwtExpired = true ;
}
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 );
2026-04-10 12:09:49 +02:00
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 ) {
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 }));
}
2026-04-10 12:16:06 +02:00
} catch ( err : any ) {
2026-04-10 12:03:12 +02:00
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.` );
2026-04-10 12:03:12 +02:00
} 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
2026-04-21 09:05:14 +02:00
rowStatuses : { ... rowStatuses },
pendingRows : { ... pendingRows },
2026-03-29 17:50:31 +02:00
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-04-21 09:05:14 +02:00
// Restore data
2026-03-29 17:35:07 +02:00
setAppState ( prev => ({
... prev ,
2026-03-29 17:50:31 +02:00
data : lastAction.data ,
2026-04-21 09:05:14 +02:00
hasUnsavedChanges : Object.keys ( lastAction . pendingRows || {}). length > 0
2026-03-29 17:35:07 +02:00
}));
2026-04-21 09:05:14 +02:00
// Restore statuses and pending state
if ( lastAction . rowStatuses ) setRowStatuses ( lastAction . rowStatuses );
if ( lastAction . pendingRows ) setPendingRows ( lastAction . pendingRows );
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 }
2026-04-20 16:35:40 +02:00
activeModule = { activeModule }
2026-03-27 17:49:10 +01:00
onExport = { handleExport }
2026-04-20 20:30:21 +02:00
onRefresh = {() => { setRefreshTrigger ( t => t + 1 ); }}
2026-03-27 17:49:10 +01:00
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" >
2026-04-11 15:00:58 +02:00
< Sidebar activeModule = { activeModule } setActiveModule = { setActiveModule } userEmail = { session . user . email } />
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 )}
/>
)}
2026-04-12 15:11:57 +02:00
{ activeModule === 'missing_data' && (
< MissingDataView
data = { appState . data }
headers = { appState . headers }
2026-04-12 15:23:23 +02:00
onSaveRow = { handleSaveRow }
onCaptureState = { captureState }
2026-04-12 15:11:57 +02:00
/>
)}
2026-04-09 09:24:17 +02:00
{ 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 12:46:26 +02:00
onEdit = {( index ) => setEditingRowIndex ( index )}
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 >
);
}