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' ;
2026-04-22 20:40:32 +02:00
import { X } from 'lucide-react' ;
import { cn } from './lib/utils' ;
2026-04-22 16:52:59 +02:00
import { AppState , ExcelRow , resolveColumnIndices , COLUMNS } from './types' ;
import { ColumnsProvider } from './contexts/ColumnsContext' ;
2026-04-24 14:19:09 +02:00
import { FilterProvider } from './contexts/FilterContext' ;
2026-03-27 11:34:09 +01:00
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-23 19:07:38 +02:00
import { getAllSyncedRows , saveRowToSupabase , resetAllPendingRows , saveHistoryEntry , deleteHistoryEntry , getHistoryDataForMerge } 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-05-08 13:28:39 +02:00
import { CosmeticItemsView } from './components/CosmeticItemsView' ;
2026-05-20 13:33:37 +02:00
import { ControlDashboardView } from './components/ControlDashboardView' ;
import { UserManagementView } from './components/UserManagementView' ;
2026-05-15 11:00:21 +02:00
import { downloadBusinessCentralItemsExcel , previewBusinessCentralSync , applyBusinessCentralSync , isPreviewTokenMismatchError } from './services/businessCentral' ;
2026-05-20 13:33:37 +02:00
import { ControlTabId , type DashboardDrilldownRequest } from './lib/controlDashboard' ;
2026-04-28 09:18:43 +02:00
const FORCED_ZERO_STOCK_SKUS = new Set ([
'11631VC' , '1237VC' , '1238VC' , '1652VC' , '1653VC' , '1684VC' , '1688VC' , '1717VC' ,
'1718VC' , '180VC' , '1832VC' , '2025VC' , '2027VC' , '2180VC' , '2181VC' , '2210VC' ,
'2260VC' , '2355VC' , '2356VC' , '2360VC' , '2400VC' , '2401VC' , '2402VC' , '2405VC' ,
'2406VC' , '2414VC' , '2415VC' , '2425VC' , '2447VC' , '2450VC' , '2510VC' , '2517VC' ,
'2522VC' , '2611VC' , '2648VC' , '2660VC' , '2665VC' , '2857VC' , '2858VC' , '2990VC' ,
'3011VC' , '3012VC' , '3013VC' , '3015VC' , '3018VC' , '3019VC' , '3020VC' , '3021VC' ,
'3027VC' , '3030VC' , '3035VC' , '3038VC' , '3041VC' , '3043VC' , '3044VC' , '3045VC' ,
'3046VC' , '3048VC' , '3052VC' , '3053VC' , '3054VC' , '3055VC' , '3062VC' , '3063VC' ,
'3064VC' , '3069VC' , '3070VC' , '3071VC' , '3072VC' , '3074VC' , '3089VC' , '3116VC' ,
'3117VC' , '3118VC' , '3119VC' , '3120VC' , '3121VC' , '3123VC' , '3125VC' , '3126VC' ,
'3127VC' , '3130VC' , '3133VC' , '3134VC' , '3135VC' , '3200VC' , '3201VC' , '3230VC' ,
'3311VC' , '3314VC' , '3315VC' , '3354VC' , '3372VC' , '3380VC' , '3391VC' , '3392VC' ,
'3423VC' , '3432VC' , '3437VC' , '3484VC' , '3487VC' , '3555VC' , '3557VC' , '3558VC' ,
'3559VC' , '3712VC' , '3824VC' , '4107VC' , '4108VC' , '4110VC' , '4115VC' , '4125VC' ,
'4130VC' , '4134VC' , '4140VC' , '4155VC' , '4158VC' , '4165VC' , '4231VC' , '4232VC' ,
'4255VC' , '4259VC' , '4261VC' , '4263VC' , '4264VC' , '4265VC' , '4285VC' , '4288VC' ,
'4330VC' , '4361VC' , '4442VC' , '4445VC' , '4462VC' , '4528VC' , '4531VC' , '4534VC' ,
'4558VC' , '4657VC' , '4721VC' , '4722VC' , '4727VC' , '4735VC' , '4737VC' , '4745VC' ,
'4752VC' , '4758VC' , '4765VC' , '4785VC' , '4790VC' , '4801VC' , '4815VC' , '4874VC' ,
'4882VC' , '5032VC' , '5113VC' , '5382VC' , '5383VC' , '5440VC' , '5442VC' , '5456VC' ,
'5540VC' , '5571VC' , '5572VC' , '5574VC' , '5575VC' , '5588VC' , '5592VC' , '5685VC' ,
'5712VC' , '5721VC' , '5807VC' , '5823VC' , '5825VC' , '5835VC' , '5852VC' , '5871VC' ,
'5872VC' , '5873VC' , '5874VC' , '5882VC' , '5883VC' , '5885VC' , '5987VC' , '5991VC' ,
'5995VC' , '6001VC' , '6007VC' , '6040VC' , '6151VC' , '6252VC' , '6255VC' , '6270VC' ,
2026-05-07 08:55:18 +02:00
'6471VC' , '5658VCI' , '2915244CLM' , '30512912CLM' , '2861262CLM' , '41683MDRG' , '41891MDRG'
2026-04-28 09:18:43 +02:00
]);
2026-03-27 11:34:09 +01:00
2026-05-15 11:00:21 +02:00
const BC_SYNC_QUEUE_STORAGE_KEY = 'craze_bc_sync_queue' ;
2026-05-15 13:52:47 +02:00
type BcSyncStatus = 'queued' | 'previewed' | 'preview_only' | 'syncing' | 'synced' | 'failed' ;
2026-05-15 11:00:21 +02:00
interface BcSyncQueueEntry {
rowIndex : number ;
originalData : ExcelRow ;
newData : ExcelRow ;
articleName : string ;
selected : boolean ;
status : BcSyncStatus ;
previewToken? : string ;
error? : string ;
2026-05-15 13:52:47 +02:00
warning? : string ;
2026-05-15 11:00:21 +02:00
source : 'save_all' | 'cosmetic_items' | 'manual' ;
}
type BcSyncQueue = Record < string , BcSyncQueueEntry >;
function readStoredBcSyncQueue () : BcSyncQueue {
try {
const raw = localStorage . getItem ( BC_SYNC_QUEUE_STORAGE_KEY );
if ( ! raw ) return {};
const parsed = JSON . parse ( raw ) as unknown ;
if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed )) return {};
return parsed as BcSyncQueue ;
} catch {
return {};
}
}
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-05-20 13:33:37 +02:00
const [ activeModule , setActiveModule ] = useState < ControlTabId >( 'control_dashboard' );
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-05-15 11:00:21 +02:00
const [ bcSyncQueue , setBcSyncQueue ] = useState < BcSyncQueue >(() => readStoredBcSyncQueue ());
2026-04-09 08:45:22 +02:00
const [ isSavingAll , setIsSavingAll ] = useState ( false );
2026-05-15 11:00:21 +02:00
const [ isSyncingBC , setIsSyncingBC ] = useState ( false );
2026-05-14 17:17:10 +02:00
const [ isDownloadingBCExcel , setIsDownloadingBCExcel ] = useState ( false );
2026-04-20 20:30:21 +02:00
const [ refreshTrigger , setRefreshTrigger ] = useState ( 0 );
2026-05-20 13:33:37 +02:00
const [ dashboardDrilldown , setDashboardDrilldown ] = useState < DashboardDrilldownRequest | null >( null );
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 ]);
2026-05-20 13:33:37 +02:00
// Background user validation status check
useEffect (() => {
if ( ! session ) return ;
let isMounted = true ;
const checkUserStatus = async () => {
try {
const res = await fetch ( '/api/users-admin' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Authorization' : `Bearer ${ session . access_token } `
},
body : JSON.stringify ({ action : 'check-status' })
});
if ( ! isMounted ) return ;
if ( ! res . ok ) {
console . warn ( '[App] Validation check failed, logging out.' );
handleSignOut ();
return ;
}
const data = await res . json ();
if ( ! data . validated ) {
alert ( 'Tu usuario ya no está autorizado o está pendiente de validación.' );
handleSignOut ();
}
} catch ( err ) {
console . error ( '[App] Background validation check error:' , err );
}
};
// Check immediately on mount/session change
checkUserStatus ();
// Check periodically every 5 minutes
const interval = setInterval ( checkUserStatus , 5 * 60 * 1000 );
return () => {
isMounted = false ;
clearInterval ( interval );
};
}, [ session ]);
2026-05-15 11:00:21 +02:00
useEffect (() => {
try {
localStorage . setItem ( BC_SYNC_QUEUE_STORAGE_KEY , JSON . stringify ( bcSyncQueue ));
} catch {
// Ignore storage quota or serialization errors.
}
}, [ bcSyncQueue ]);
2026-04-10 08:27:36 +02:00
const handleSignOut = () => {
signOut ();
setSession ( null );
};
2026-04-23 12:37:04 +02:00
// 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 () => {
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-23 12:50:40 +02:00
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' });
2026-04-10 10:54:50 +02:00
if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` );
2026-04-22 16:17:07 +02:00
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.' );
}
2026-04-10 10:54:50 +02:00
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-23 12:37:04 +02:00
const syncedData = await getAllSyncedRows ();
2026-04-23 19:07:38 +02:00
console . log ( 'Fetching history data from Supabase for merge...' );
const historyData = await getHistoryDataForMerge ();
console . log ( 'Supabase synced rows:' , Object . keys ( syncedData ). length , '| History rows:' , Object . keys ( historyData ). length );
2026-04-10 10:54:50 +02:00
2026-04-23 16:44:42 +02:00
// Extend headers with virtual columns if the Excel is shorter than the saved data.
// COLUMNS hardcoded indices (PRODUCT_TYPE=103, ITEM_TO_LOGISTIC=104, etc.) are used
// by older saved rows — ensure headers covers them so the merge and render work.
const extendedHeaders = [... headers ];
const VIRTUAL_COLS : Record < number , string > = {
[ COLUMNS . VERIFIED_DIMS ] : 'Verified Dims' ,
[ COLUMNS . VALIDATED_CHECK ] : 'Validated' ,
[ COLUMNS . VALIDATED_NOTE ] : 'Note' ,
[ COLUMNS . PRODUCT_TYPE ] : 'TYPE' ,
[ COLUMNS . ITEM_TO_LOGISTIC ] : 'Item to Logistic' ,
[ COLUMNS . ANNA_CHECK ] : 'Anna Check' ,
[ COLUMNS . ANNA_NOTE ] : 'Anna Note' ,
};
Object . entries ( VIRTUAL_COLS ). forEach (([ idxStr , name ]) => {
const idx = Number ( idxStr );
if ( extendedHeaders [ idx ] === undefined ) {
extendedHeaders [ idx ] = name ;
}
});
2026-04-23 16:47:39 +02:00
// Fill any sparse holes with '' to avoid undefined.includes() errors
for ( let i = 0 ; i < extendedHeaders . length ; i ++ ) {
if ( extendedHeaders [ i ] === undefined ) extendedHeaders [ i ] = '' ;
}
2026-04-23 16:44:42 +02:00
const resolvedCols = resolveColumnIndices ( extendedHeaders );
2026-05-20 13:33:37 +02:00
const articleNoIdx = resolvedCols . ARTICLE_NO ;
2026-04-10 10:54:50 +02:00
const processedRows = rows . map ( row => {
2026-04-23 16:14:18 +02:00
const articleNo = String ( row [ articleNoIdx ]);
const synced = syncedData [ articleNo ];
2026-04-23 19:07:38 +02:00
const hist = historyData [ articleNo ];
2026-04-23 16:14:18 +02:00
2026-04-23 16:16:20 +02:00
// Start with fresh Dropbox values (prices from Excel)
2026-04-23 14:53:43 +02:00
let finalRow = [... row ];
2026-04-23 19:17:03 +02:00
// Pad row to match extendedHeaders length so internal cols (index >= 100) are readable
while ( finalRow . length < extendedHeaders . length ) {
finalRow . push ( null );
}
2026-05-15 11:00:21 +02:00
// Merge logic:
// - Use the Dropbox row as the base for master data and prices.
// - Restore internal control columns from history when available.
// This is critical because some synced rows still have empty internal
// slots even when the change history already contains the corrected value.
const mergedInternalSource = hist || synced ? . data ;
if ( mergedInternalSource ) {
// 1. Restore internal control columns (indices >= 100)
2026-04-23 18:36:35 +02:00
// These are the "TYPE", "Item to Logistic", "Checking", etc.
2026-05-15 11:00:21 +02:00
// We use hardcoded indices from COLUMNS to keep them stable.
2026-04-23 18:27:40 +02:00
Object . values ( COLUMNS ). forEach ( idx => {
2026-05-15 11:00:21 +02:00
if ( idx < 100 ) return ;
const historyValue = hist ? .[ idx ];
const syncedValue = synced ? . data ? .[ idx ];
const value =
historyValue !== undefined && historyValue !== null && historyValue !== ''
? historyValue
: syncedValue !== undefined && syncedValue !== null && syncedValue !== ''
? syncedValue
: undefined ;
if ( value !== undefined ) {
finalRow [ idx ] = value ;
2026-04-23 16:14:18 +02:00
}
2026-04-23 18:27:40 +02:00
});
2026-04-23 18:36:35 +02:00
// Update row status in UI if it's not the default 'excel'
2026-04-23 19:07:38 +02:00
if ( synced ? . status && synced . status !== 'excel' ) {
2026-04-23 18:36:35 +02:00
setRowStatuses ( prev => ({ ... prev , [ articleNo ] : synced . status ! }));
}
2026-04-23 16:14:18 +02:00
}
2026-04-28 09:18:43 +02:00
// Force stock 0 for specific SKUs (User request)
if ( FORCED_ZERO_STOCK_SKUS . has ( articleNo )) {
finalRow [ COLUMNS . ITEM_AVAILABLE ] = 0 ;
}
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-23 16:44:42 +02:00
const header = ( extendedHeaders [ idx ] || '' ). toLowerCase ();
2026-04-23 16:12:09 +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' ))) {
return val ;
}
2026-04-23 21:44:26 +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 . toISOString (). split ( 'T' )[ 0 ]; // Returns YYYY-MM-DD
}
return val ;
}
2026-04-23 16:12:09 +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 ));
if ( typeof val === 'number' ) {
return Number ( val . toFixed ( 2 ));
}
if ( typeof val === 'string' ) {
const normalized = val . trim (). replace ( ',' , '.' );
2026-03-29 14:46:05 +02:00
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-23 16:44:42 +02:00
const asinIdx = ( extendedHeaders as string []). findIndex (( h : string ) =>
2026-04-10 10:54:50 +02:00
String ( h ). toLowerCase (). trim () === 'asin'
);
2026-04-23 16:44:42 +02:00
2026-03-27 12:28:30 +01:00
setAppState ({
2026-04-23 16:44:42 +02:00
headers : extendedHeaders ,
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
});
2026-05-20 13:33:37 +02:00
setActiveModule ( 'control_dashboard' );
2026-03-27 12:28:30 +01:00
}
} 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-04-28 09:18:43 +02:00
const resolvedCols = resolveColumnIndices ( headers );
2026-03-29 14:46:05 +02:00
const processedRows = rawRows . map ( row => {
2026-04-28 09:18:43 +02:00
const finalRow = row . map (( val , idx ) => {
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-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-04-28 09:18:43 +02:00
// Force stock 0 for specific SKUs (User request)
const articleNo = String ( finalRow [ resolvedCols . ARTICLE_NO ] || '' );
if ( FORCED_ZERO_STOCK_SKUS . has ( articleNo )) {
finalRow [ resolvedCols . ITEM_AVAILABLE ] = 0 ;
}
return finalRow ;
2026-03-29 14:46:05 +02:00
});
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
});
2026-05-20 13:33:37 +02:00
setActiveModule ( 'control_dashboard' );
2026-03-27 11:34:09 +01:00
}
};
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-23 16:22:35 +02:00
// 3. Post-export: Reset pending statuses in Supabase (pending → edited to preserve on reload)
2026-04-08 19:30:41 +02:00
console . log ( 'Resetting pending statuses in Supabase...' );
2026-04-23 12:37:04 +02:00
resetAllPendingRows (). then ( success => {
2026-04-08 19:30:41 +02:00
if ( success ) {
console . log ( 'Successfully reset all pending statuses' );
2026-04-23 16:22:35 +02:00
// Only clear 'pending' statuses locally; keep 'edited'/'saved' so they remain visible
setRowStatuses (( prev : Record < string , string >) => {
const next : Record < string , string > = {};
for ( const [ id , status ] of Object . entries ( prev )) {
if ( status !== 'pending' ) next [ id ] = status as string ;
}
return next ;
});
2026-04-08 19:30:41 +02:00
}
});
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-05-15 11:00:21 +02:00
setBcSyncQueue ( prev => {
if ( ! prev [ articleNo ]) return prev ;
const n = { ... prev };
delete n [ articleNo ];
return n ;
});
2026-04-09 08:58:31 +02:00
};
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-23 12:37:04 +02:00
let sessionIssue = false ;
2026-04-10 12:03:12 +02:00
try {
2026-05-15 11:00:21 +02:00
await Promise . all ( entries . map ( async ([ articleNo , { rowIndex , newData , originalData , articleName }]) => {
2026-04-10 12:03:12 +02:00
console . log ( '[handleSaveAll] Saving article:' , articleNo );
2026-04-23 12:37:04 +02:00
const result = await saveRowToSupabase ( articleNo , newData );
2026-04-10 12:06:16 +02:00
console . log ( '[handleSaveAll] Save result for' , articleNo , ':' , result );
2026-05-14 17:17:10 +02:00
2026-04-10 12:06:16 +02:00
if ( result . success ) {
2026-05-15 11:00:21 +02:00
const histRes = await saveHistoryEntry ( articleNo , articleName , originalData , newData , session ? . user ? . email || 'unknown' );
2026-05-14 17:17:10 +02:00
2026-05-11 11:48:11 +02:00
if ( ! histRes . success ) {
console . warn ( `[handleSaveAll] History save failed for ${ articleNo } :` , histRes . error );
}
2026-05-14 17:17:10 +02:00
2026-05-15 11:00:21 +02:00
setBcSyncQueue ( prev => ({
... prev ,
[ articleNo ] : {
rowIndex ,
originalData ,
newData ,
articleName ,
selected : true ,
status : 'queued' ,
source : 'save_all' ,
},
}));
setRowStatuses ( prev => ({ ... prev , [ articleNo ] : 'bc_pending' }));
2026-04-10 12:03:12 +02:00
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-23 12:37:04 +02:00
if ( result . error ? . includes ( '401' ) || result . error ? . includes ( 'expired' ) || result . error ? . includes ( 'Session expired' )) {
sessionIssue = true ;
2026-04-10 12:09:49 +02:00
}
2026-04-10 12:03:12 +02:00
}
2026-05-14 17:17:10 +02:00
}));
2026-04-10 12:03:12 +02:00
console . log ( '[handleSaveAll] Finished loop. Failed:' , failedArticles . length );
2026-04-23 12:37:04 +02:00
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." );
2026-04-10 12:09:49 +02:00
} 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-05-15 11:00:21 +02:00
const handleQueueBcSync = ( articleNo : string , rowIndex : number , originalData : ExcelRow , newData : ExcelRow , articleName : string ) => {
setBcSyncQueue ( prev => ({
... prev ,
[ articleNo ] : {
rowIndex ,
originalData ,
newData ,
articleName ,
selected : true ,
status : 'queued' ,
source : 'cosmetic_items' ,
},
}));
setRowStatuses ( prev => ({ ... prev , [ articleNo ] : 'bc_pending' }));
};
const handleToggleBcQueueSelection = ( articleNo : string ) => {
setBcSyncQueue ( prev => {
const item = prev [ articleNo ];
if ( ! item ) return prev ;
return {
... prev ,
[ articleNo ] : {
... item ,
selected : ! item . selected ,
},
};
});
};
2026-05-15 13:52:47 +02:00
const handleDiscardSelectedBcQueue = () => {
2026-05-20 13:33:37 +02:00
const selectedCount = ( Object . values ( bcSyncQueue ) as BcSyncQueueEntry []). filter ( e => e . selected ). length ;
2026-05-15 13:52:47 +02:00
if ( selectedCount === 0 ) return ;
if ( ! window . confirm ( `Discard ${ selectedCount } pending BC sync(s)? They will be removed from the queue.` )) return ;
setBcSyncQueue ( prev => {
const next : BcSyncQueue = { ... prev };
( Object . keys ( next ) as string []). forEach ( articleNo => {
if ( next [ articleNo ]. selected ) delete next [ articleNo ];
});
return next ;
});
};
2026-05-15 11:00:21 +02:00
const handleSelectAllBcQueue = ( selected : boolean ) => {
setBcSyncQueue ( prev => {
const next : BcSyncQueue = { ... prev };
( Object . keys ( next ) as string []). forEach ( articleNo => {
const item = next [ articleNo ];
next [ articleNo ] = { ... item , selected };
});
return next ;
});
};
const bcQueueEntries = useMemo < Array <[ string , BcSyncQueueEntry ] > > (
() => Object . entries ( bcSyncQueue ) as Array < [ string , BcSyncQueueEntry ] > ,
[ bcSyncQueue ]
);
const selectedBcQueueEntries = useMemo < Array <[ string , BcSyncQueueEntry ] > > (
() => bcQueueEntries . filter (([, item ]) => item . selected ),
[ bcQueueEntries ]
);
useEffect (() => {
if ( appState . data . length === 0 ) return ;
const validArticles = new Set ( appState . data . map ( row => String ( row [ COLUMNS . ARTICLE_NO ] ?? '' ). trim ()). filter ( Boolean ));
setBcSyncQueue ( prev => {
let changed = false ;
const next : BcSyncQueue = {};
for ( const [ articleNo , item ] of Object . entries ( prev ) as Array < [ string , BcSyncQueueEntry ] > ) {
if ( ! validArticles . has ( articleNo )) {
changed = true ;
continue ;
}
next [ articleNo ] = item ;
}
return changed ? next : prev ;
});
}, [ appState . data ]);
const handlePreviewSelectedBcSync = async () => {
if ( selectedBcQueueEntries . length === 0 ) return ;
setIsSyncingBC ( true );
try {
const results = await Promise . all ( selectedBcQueueEntries . map ( async ([ articleNo , item ]) => {
const preview = await previewBusinessCentralSync ( appState . headers , item . newData );
return [ articleNo , preview ] as const ;
}));
setBcSyncQueue ( prev => {
const next : BcSyncQueue = { ... prev };
results . forEach (([ articleNo , preview ]) => {
const item = next [ articleNo ];
if ( ! item || ! preview . success ) {
if ( item ) {
next [ articleNo ] = {
... item ,
status : 'failed' ,
error : preview.error || 'Preview failed' ,
2026-05-15 13:52:47 +02:00
warning : undefined ,
2026-05-15 11:00:21 +02:00
};
}
return ;
}
next [ articleNo ] = {
... item ,
status : 'previewed' ,
previewToken : preview.previewToken ,
error : undefined ,
2026-05-15 13:52:47 +02:00
selected : item.selected ,
warning : undefined ,
2026-05-15 11:00:21 +02:00
};
});
return next ;
});
} finally {
setIsSyncingBC ( false );
}
};
const handleSyncSelectedBcSync = async () => {
if ( selectedBcQueueEntries . length === 0 ) return ;
setIsSyncingBC ( true );
try {
const selectedArticles = new Set ( selectedBcQueueEntries . map (([ articleNo ]) => articleNo ));
setBcSyncQueue ( prev => {
const next : BcSyncQueue = { ... prev };
( Object . entries ( next ) as Array < [ string , BcSyncQueueEntry ] > ). forEach (([ articleNo , item ]) => {
if ( selectedArticles . has ( articleNo )) {
next [ articleNo ] = { ... item , status : 'syncing' , error : undefined };
}
});
return next ;
});
for ( const [ articleNo , item ] of selectedBcQueueEntries ) {
2026-05-15 13:52:47 +02:00
const preview = await previewBusinessCentralSync ( appState . headers , item . newData );
if ( ! preview . success ) {
setBcSyncQueue ( prev => ({
... prev ,
[ articleNo ] : {
... prev [ articleNo ],
status : 'failed' ,
error : preview.error || 'Preview failed' ,
warning : undefined ,
},
}));
continue ;
}
const result = await applyBusinessCentralSync ( appState . headers , item . newData , preview . previewToken );
2026-05-15 11:00:21 +02:00
if ( ! result . success ) {
if ( isPreviewTokenMismatchError ( result . error )) {
const refreshedPreview = await previewBusinessCentralSync ( appState . headers , item . newData );
if ( refreshedPreview . success ) {
setBcSyncQueue ( prev => ({
... prev ,
[ articleNo ] : {
... prev [ articleNo ],
status : 'previewed' ,
previewToken : refreshedPreview.previewToken ,
error : undefined ,
2026-05-15 13:52:47 +02:00
warning : undefined ,
2026-05-15 11:00:21 +02:00
},
}));
const retryResult = await applyBusinessCentralSync ( appState . headers , item . newData , refreshedPreview . previewToken );
if ( retryResult . success ) {
setBcSyncQueue ( prev => {
const next = { ... prev };
delete next [ articleNo ];
return next ;
});
setRowStatuses ( prev => ({ ... prev , [ articleNo ] : 'synced' }));
continue ;
}
setBcSyncQueue ( prev => ({
... prev ,
[ articleNo ] : {
... prev [ articleNo ],
status : 'failed' ,
error : retryResult.error || 'BC sync failed' ,
2026-05-15 13:52:47 +02:00
warning : retryResult.warning ,
2026-05-15 11:00:21 +02:00
},
}));
continue ;
}
}
setBcSyncQueue ( prev => ({
... prev ,
[ articleNo ] : {
... prev [ articleNo ],
status : 'failed' ,
error : result.error || 'BC sync failed' ,
2026-05-15 13:52:47 +02:00
warning : result.warning ,
2026-05-15 11:00:21 +02:00
},
}));
continue ;
}
setBcSyncQueue ( prev => {
const next = { ... prev };
delete next [ articleNo ];
return next ;
});
setRowStatuses ( prev => ({ ... prev , [ articleNo ] : 'synced' }));
}
} finally {
setIsSyncingBC ( false );
}
};
2026-05-14 17:17:10 +02:00
const handleDownloadBCExcel = async () => {
setIsDownloadingBCExcel ( true );
try {
const result = await downloadBusinessCentralItemsExcel ();
if ( ! result . success ) {
alert ( `Failed to download Business Central Excel: \ n \ n ${ result . error || 'Unknown error' } ` );
}
} finally {
setIsDownloadingBCExcel ( false );
}
};
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-05-15 11:00:21 +02:00
bcSyncQueue : JSON.parse ( JSON . stringify ( bcSyncQueue )) as BcSyncQueue ,
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-05-15 11:00:21 +02:00
if ( lastAction . bcSyncQueue ) setBcSyncQueue ( lastAction . bcSyncQueue );
2026-04-21 09:05:14 +02:00
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-22 20:38:06 +02:00
const [ isMaximized , setIsMaximized ] = useState ( false );
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-04-22 16:52:59 +02:00
< ColumnsProvider headers = { appState . headers }>
2026-04-24 14:19:09 +02:00
< FilterProvider >
< div className = "h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden" >
2026-04-22 20:38:06 +02:00
{ ! isMaximized && (
< TopBar
stats = { stats }
activeModule = { activeModule }
onExport = { handleExport }
2026-05-14 17:17:10 +02:00
onDownloadBCExcel = { handleDownloadBCExcel }
2026-04-22 20:38:06 +02:00
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 }
2026-05-15 11:00:21 +02:00
bcQueueCount = { Object . keys ( bcSyncQueue ). length }
2026-05-15 13:52:47 +02:00
bcQueueEntries = { Object . fromEntries (( Object . entries ( bcSyncQueue ) as Array < [ string , BcSyncQueueEntry ] > ). map (([ k , v ]) => [ k , { articleName : v.articleName , selected : v.selected , status : v.status , error : v.error , warning : v.warning }]))}
2026-05-15 11:00:21 +02:00
onToggleBcQueueSelection = { handleToggleBcQueueSelection }
onSelectAllBcQueue = { handleSelectAllBcQueue }
onPreviewSelectedBcSync = { handlePreviewSelectedBcSync }
onSyncSelectedBcSync = { handleSyncSelectedBcSync }
2026-05-15 13:52:47 +02:00
onDiscardSelectedBcQueue = { handleDiscardSelectedBcQueue }
2026-05-15 11:00:21 +02:00
isSyncingBC = { isSyncingBC }
2026-05-14 17:17:10 +02:00
isDownloadingBCExcel = { isDownloadingBCExcel }
2026-04-22 20:38:06 +02:00
isMaximized = { isMaximized }
onToggleMaximize = {() => setIsMaximized ( true )}
/>
)}
2026-03-27 11:34:09 +01:00
< div className = "flex flex-1 overflow-hidden" >
2026-04-22 20:38:06 +02:00
{ ! 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' ? (
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-05-20 13:33:37 +02:00
dashboardDrilldown = { dashboardDrilldown }
onDashboardDrilldownApplied = {() => setDashboardDrilldown ( null )}
/>
)}
{ activeModule === 'control_dashboard' && (
< ControlDashboardView
headers = { appState . headers }
data = { appState . data }
pendingRows = { pendingRows }
rowStatuses = { rowStatuses }
activeModule = { activeModule }
onDrillDown = {( request ) => {
setDashboardDrilldown ( request );
setActiveModule ( request . tabId );
}}
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-05-20 13:33:37 +02:00
dashboardDrilldown = { dashboardDrilldown }
onDashboardDrilldownApplied = {() => setDashboardDrilldown ( null )}
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-05-20 13:33:37 +02:00
dashboardDrilldown = { dashboardDrilldown }
onDashboardDrilldownApplied = {() => setDashboardDrilldown ( null )}
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-05-08 13:28:39 +02:00
{ activeModule === 'cosmetic_items' && (
< CosmeticItemsView
data = { appState . data }
headers = { appState . headers }
onSaveRow = { handleSaveRow }
onCaptureState = { captureState }
rowStatuses = { rowStatuses }
2026-05-15 11:00:21 +02:00
onQueueBcSync = { handleQueueBcSync }
2026-05-20 13:33:37 +02:00
dashboardDrilldown = { dashboardDrilldown }
onDashboardDrilldownApplied = {() => setDashboardDrilldown ( null )}
2026-05-08 13:28:39 +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: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-23 12:37:04 +02:00
await deleteHistoryEntry ( String ( historyId ));
2026-04-10 09:47:35 +02:00
}
2026-04-09 09:24:17 +02:00
}
}}
/>
)}
2026-05-20 13:33:37 +02:00
{ activeModule === 'user_management' && session && (
< UserManagementView session = { session } />
)}
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 >
2026-04-24 14:19:09 +02:00
</ FilterProvider >
2026-04-22 16:52:59 +02:00
</ ColumnsProvider >
2026-03-27 11:34:09 +01:00
);
}