import React, { useState, useMemo, useEffect } from 'react'; import * as XLSX from 'xlsx'; import { AppState, ExcelRow, COLUMNS } from './types'; import { Sidebar } from './components/Sidebar'; import { TopBar } from './components/TopBar'; import { ProductDescriptions } from './components/ProductDescriptions'; import { MatrixView } from './components/MatrixView'; import { EditPanel } from './components/EditPanel'; import { getAllSyncedRows, saveRowToSupabase } from './lib/supabase'; import { getStoredSession, signOut, type AuthSession } from './lib/auth'; import { LoginPage } from './components/LoginPage'; import { DimensionsView } from './components/DimensionsView'; import { UndoToast } from './components/UndoToast'; export default function App() { const [session, setSession] = useState(() => getStoredSession()); const handleSignOut = () => { signOut(); setSession(null); }; if (!session) { return setSession(getStoredSession())} />; } const [appState, setAppState] = useState({ headers: [], data: [], fileName: '', fileDate: null, hasUnsavedChanges: false }); const [activeModule, setActiveModule] = useState<'descriptions' | 'matrix' | 'dimensions'>('descriptions'); const [undoState, setUndoState] = useState<{ data: ExcelRow[], message: string } | null>(null); const [editingRowIndex, setEditingRowIndex] = useState(null); const [isLoadingDefault, setIsLoadingDefault] = useState(true); const [defaultLoadError, setDefaultLoadError] = useState(null); useEffect(() => { const loadDefaultData = async () => { // In dev: Vite proxy handles /dropbox-file (see vite.config.ts) // In prod: Vercel serverless function at /api/dropbox-proxy handles it const fileUrl = import.meta.env.DEV ? '/dropbox-file/scl/fi/nkyabkq2jdwfudof2ovrl/Data-Matrix.xlsx?rlkey=1cz5fuabwipqqivihii7sybw0&st=io0g4wvb&dl=1' : '/api/dropbox-proxy'; setIsLoadingDefault(true); setDefaultLoadError(null); try { console.log('Fetching Data-Matrix.xlsx via Vite proxy...'); const response = await fetch(fileUrl); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); if (arrayBuffer.byteLength < 100) { throw new Error('File too small — possibly empty or error response'); } const wb = XLSX.read(arrayBuffer, { type: 'array' }); const wsname = wb.SheetNames[0]; const ws = wb.Sheets[wsname]; const data = XLSX.utils.sheet_to_json(ws, { header: 1 }); if (data.length > 0) { const rawHeaders = data[0]; const rawRows = data.slice(1); console.log('Applying Supabase overrides...'); const syncedData = await getAllSyncedRows(); const articleNoIdx = COLUMNS.ARTICLE_NO; const processedRows = rawRows.map(row => { const articleNo = String(row[articleNoIdx]); const finalRow = syncedData[articleNo] || row; // Format numeric/price fields to 2 decimal places return finalRow.map((val, idx) => { if (val === undefined || val === null || val === '') return val; const header = (rawHeaders[idx] || '').toLowerCase(); // Skip Article No, Barcodes, and other code-like fields // But allow if it's a weight/measure column (e.g. Article NW (kg)) if ((header.includes('id') || header.includes('no') || header.includes('code') || header.includes('art.') || header.includes('barcode') || header.includes('article')) && !(header.includes('nw') || header.includes('gw') || header.includes('weight') || header.includes('kg'))) { return val; } const formatKeywords = ['price', 'eur', 'cost', 'msrp', 'net', 'gross', 'netto', 'brutto', 'pp', 'pph', 'uvp', 'vpe', 'stk', 'nw', 'gw', 'weight', 'kg']; const shouldFormat = formatKeywords.some(kw => header.includes(kw)); if (typeof val === 'number') { return Number(val.toFixed(2)); } if (typeof val === 'string') { const normalized = val.trim().replace(',', '.'); const num = parseFloat(normalized); if (!isNaN(num) && (shouldFormat || val.includes('.') || val.includes(','))) { return num.toFixed(2); } } return val; }); }); setAppState({ headers: rawHeaders, data: processedRows, fileName: 'Data-Matrix.xlsx (Cloud Sync)', fileDate: new Date(), hasUnsavedChanges: false }); setActiveModule('descriptions'); } } catch (err) { console.error('Failed to load from Dropbox:', err); setDefaultLoadError(err instanceof Error ? err.message : 'Connection failed'); } finally { setIsLoadingDefault(false); } }; loadDefaultData(); }, []); const handleFileUpload = (e: React.ChangeEvent) => { 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(ws, { header: 1 }); if (data.length > 0) { const rawHeaders = data[0]; const rawRows = data.slice(1); const processedRows = rawRows.map(row => { return row.map((val, idx) => { if (val === undefined || val === null || val === '') return val; const header = (rawHeaders[idx] || '').toLowerCase(); if (header.includes('id') || header.includes('no') || header.includes('code') || header.includes('art.') || header.includes('barcode') || header.includes('article')) { // But allow if it's a weight/measure column (e.g. Article NW (kg)) if (!(header.includes('nw') || header.includes('gw') || header.includes('weight') || header.includes('kg'))) { return val; } } // Handle date columns - Excel serial dates are numbers >= 25569 (Jan 1, 1970) if (header.includes('date') || header.includes('launch') || header.includes('ready')) { if (typeof val === 'number' && val >= 25569 && val <= 60000) { const excelEpoch = new Date(1899, 11, 30); const date = new Date(excelEpoch.getTime() + val * 86400000); return date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }); } return val; } const formatKeywords = ['price', 'eur', 'cost', 'msrp', 'net', 'gross', 'netto', 'brutto', 'pp', 'pph', 'uvp', 'vpe', 'stk', 'nw', 'gw', 'weight', 'kg']; const shouldFormat = formatKeywords.some(kw => header.includes(kw)); if (typeof val === 'number') { return Number(val.toFixed(2)); } if (typeof val === 'string') { const normalized = val.trim().replace(',', '.'); const num = parseFloat(normalized); if (!isNaN(num) && (shouldFormat || val.includes('.') || val.includes(','))) { return num.toFixed(2); } } return val; }); }); setAppState({ headers: rawHeaders, data: processedRows, fileName: file.name, fileDate: new Date(), hasUnsavedChanges: false }); setActiveModule('descriptions'); } }; reader.readAsBinaryString(file); }; const handleExport = () => { if (appState.data.length === 0) return; 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'); const dateStr = new Date().toISOString().split('T')[0]; XLSX.writeFile(wb, `CRAZE_Products_Updated_${dateStr}.xlsx`); setAppState(prev => ({ ...prev, hasUnsavedChanges: false })); }; const handleSaveRow = async (rowIndex: number, updatedRow: ExcelRow) => { // 1. Update UI state setAppState(prev => { const newData = [...prev.data]; newData[rowIndex] = updatedRow; return { ...prev, data: newData, hasUnsavedChanges: true }; }); setEditingRowIndex(null); // 2. Persist to Supabase const articleNo = String(updatedRow[COLUMNS.ARTICLE_NO]); console.log(`Saving article ${articleNo} to Supabase...`); const success = await saveRowToSupabase(articleNo, updatedRow); if (success) { console.log(`Successfully saved ${articleNo}`); setAppState(prev => ({ ...prev, hasUnsavedChanges: false })); } else { console.error(`Failed to save ${articleNo} to Supabase`); alert("Error saving to database. Local changes will be lost on refresh if not saved."); } }; const captureState = (message: string) => { setUndoState({ data: JSON.parse(JSON.stringify(appState.data)), // Deep copy message }); }; const handleUndo = () => { if (!undoState) return; setAppState(prev => ({ ...prev, data: undoState.data, hasUnsavedChanges: true })); setUndoState(null); }; 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]); return (
0} hasUnsavedChanges={appState.hasUnsavedChanges} userEmail={session.user.email} onSignOut={handleSignOut} canUndo={!!undoState} onUndo={handleUndo} undoMessage={undoState?.message} />
{isLoadingDefault ? (

Loading latest data...

) : defaultLoadError && appState.data.length === 0 ? (

Failed to auto-load data: {defaultLoadError}

) : appState.data.length === 0 && activeModule !== 'upload' ? (

No data loaded.

) : ( <> {activeModule === 'descriptions' && ( setEditingRowIndex(index)} /> )} {activeModule === 'matrix' && ( )} {activeModule === 'dimensions' && ( setEditingRowIndex(index)} onSaveRow={handleSaveRow} onCaptureState={captureState} /> )} )}
setUndoState(null)} /> {editingRowIndex !== null && ( setEditingRowIndex(null)} onCaptureState={captureState} /> )}
); }