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 { UploadReload } from './components/UploadReload';
|
|
|
|
|
import { EditPanel } from './components/EditPanel';
|
2026-03-27 12:23:35 +01:00
|
|
|
import { getAllSyncedRows, saveRowToSupabase } from './lib/supabase';
|
2026-03-27 11:34:09 +01:00
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
|
const [appState, setAppState] = useState<AppState>({
|
|
|
|
|
headers: [],
|
|
|
|
|
data: [],
|
|
|
|
|
fileName: '',
|
|
|
|
|
fileDate: null,
|
|
|
|
|
hasUnsavedChanges: false
|
|
|
|
|
});
|
2026-03-27 12:32:28 +01:00
|
|
|
const [activeModule, setActiveModule] = useState<'descriptions' | 'matrix' | 'upload'>('descriptions');
|
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);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadDefaultData = async () => {
|
2026-03-27 13:26:16 +01:00
|
|
|
// 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';
|
2026-03-27 12:07:22 +01:00
|
|
|
|
|
|
|
|
setIsLoadingDefault(true);
|
|
|
|
|
setDefaultLoadError(null);
|
|
|
|
|
|
2026-03-27 12:28:30 +01:00
|
|
|
try {
|
|
|
|
|
console.log('Fetching Data-Matrix.xlsx via Vite proxy...');
|
|
|
|
|
const response = await fetch(fileUrl);
|
2026-03-27 12:07:22 +01:00
|
|
|
|
2026-03-27 12:28:30 +01:00
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
2026-03-27 12:03:57 +01:00
|
|
|
}
|
2026-03-27 12:07:22 +01:00
|
|
|
|
2026-03-27 12:28:30 +01:00
|
|
|
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<any[]>(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]);
|
|
|
|
|
return syncedData[articleNo] || row;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-03-27 12:03:57 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadDefaultData();
|
|
|
|
|
}, []);
|
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 });
|
|
|
|
|
|
|
|
|
|
if (data.length > 0) {
|
|
|
|
|
setAppState({
|
|
|
|
|
headers: data[0],
|
|
|
|
|
data: data.slice(1),
|
|
|
|
|
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 }));
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 12:23:35 +01:00
|
|
|
const handleSaveRow = async (rowIndex: number, updatedRow: ExcelRow) => {
|
|
|
|
|
// 1. Update UI state
|
2026-03-27 11:34:09 +01:00
|
|
|
setAppState(prev => {
|
|
|
|
|
const newData = [...prev.data];
|
|
|
|
|
newData[rowIndex] = updatedRow;
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
data: newData,
|
|
|
|
|
hasUnsavedChanges: true
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
setEditingRowIndex(null);
|
2026-03-27 12:23:35 +01:00
|
|
|
|
|
|
|
|
// 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.");
|
|
|
|
|
}
|
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]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-screen bg-slate-900 text-slate-200 flex flex-col font-sans overflow-hidden">
|
|
|
|
|
<TopBar
|
|
|
|
|
stats={stats}
|
|
|
|
|
onExport={handleExport}
|
|
|
|
|
hasData={appState.data.length > 0}
|
|
|
|
|
hasUnsavedChanges={appState.hasUnsavedChanges}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
|
|
|
<Sidebar activeModule={activeModule} setActiveModule={setActiveModule} />
|
|
|
|
|
<main className="flex-1 overflow-auto relative p-6 bg-slate-950">
|
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' && (
|
|
|
|
|
<ProductDescriptions
|
|
|
|
|
data={appState.data}
|
|
|
|
|
onEdit={(index) => setEditingRowIndex(index)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{activeModule === 'matrix' && (
|
|
|
|
|
<MatrixView data={appState.data} headers={appState.headers} />
|
|
|
|
|
)}
|
|
|
|
|
{activeModule === 'upload' && (
|
|
|
|
|
<UploadReload
|
|
|
|
|
appState={appState}
|
|
|
|
|
onUpload={handleFileUpload}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{editingRowIndex !== null && (
|
|
|
|
|
<EditPanel
|
|
|
|
|
row={appState.data[editingRowIndex]}
|
|
|
|
|
rowIndex={editingRowIndex}
|
|
|
|
|
onSave={handleSaveRow}
|
|
|
|
|
onClose={() => setEditingRowIndex(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|