2025-12-11 14:03:33 +01:00
2025-12-11 11:25:26 +01:00
import React , { useState , useMemo , useEffect , useCallback } from 'react' ;
import FileUpload from './components/FileUpload' ;
import Dashboard from './components/Dashboard' ;
import DataGrid from './components/DataGrid' ;
2025-12-11 14:03:33 +01:00
import TopMovers from './components/TopMovers' ;
2025-12-11 15:08:01 +01:00
import AdvertisingDashboard from './components/AdvertisingDashboard' ; // Imported
2025-12-11 11:25:26 +01:00
import FilterBar from './components/FilterBar' ;
import AIChat from './components/AIChat' ;
import CrazeLogo from './components/CrazeLogo' ;
2025-12-11 15:08:01 +01:00
import { SalesRecord , FilterState , AggregatedData , AdsRecord } from './types' ; // Imported AdsRecord
import { processCSV , filterData , aggregateData , getUniqueValues , processAdsCSV , mergeSalesAndAdsData } from './services/dataProcessor' ; // Imported new processors
2025-12-11 11:25:26 +01:00
import { queryGemini } from './services/geminiService' ;
2025-12-11 15:08:01 +01:00
import { ChartIcon , TableIcon , UploadIcon , DownloadIcon , CloseIcon , TrendingIcon , MegaphoneIcon } from './components/Icons' ; // Imported MegaphoneIcon
2025-12-11 11:25:26 +01:00
import { loadSalesData , saveSalesData , clearSalesData } from './services/storage' ;
// New Refresh Icon
const RefreshIcon = ({ className } : { className? : string }) => (
< svg xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" strokeWidth = { 1.5 } stroke = "currentColor" className = { className || "w-5 h-5" }>
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</ svg >
);
// Hardcoded Permanent URL for Auto-Loading
// Using the original share link to leverage Dropbox's redirect for robust fetching.
const PERMANENT_DROPBOX_URL = "https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&dl=0" ;
const App : React.FC = () => {
const [ rawData , setRawData ] = useState < SalesRecord [] >([]);
2025-12-11 15:08:01 +01:00
const [ adsData , setAdsData ] = useState < AdsRecord [] >([]); // New Ads State
2025-12-12 11:46:04 +01:00
const [ loading , setLoading ] = useState ( true );
const [ syncing , setSyncing ] = useState ( false );
2025-12-11 15:08:01 +01:00
const [ view , setView ] = useState < 'dashboard' | 'table' | 'movers' | 'ads' > ( 'dashboard' ); // Added 'ads' view
2025-12-11 11:25:26 +01:00
const [ isChatOpen , setIsChatOpen ] = useState ( false );
const [ activeUrl , setActiveUrl ] = useState < string | null >(() => localStorage . getItem ( 'craze_csv_url' ) || PERMANENT_DROPBOX_URL );
const [ lastUpdated , setLastUpdated ] = useState < string | null >( null );
// Modal State
const [ isDataModalOpen , setIsDataModalOpen ] = useState ( false );
// Filters State
const [ filters , setFilters ] = useState < FilterState >({
customer : [],
year : [],
month : [],
line : [],
asin : [],
sku : [],
title : [],
});
// Handle URL Fetch (Auto/Manual)
const handleUrlFetch = useCallback ( async ( url : string ) => {
2025-12-12 11:46:04 +01:00
setSyncing ( true );
setLoading ( true );
// Reset previous errors on new attempt
// setLastUpdated(null);
2025-12-11 11:25:26 +01:00
2025-12-12 11:46:04 +01:00
const controller = new AbortController ();
const timeoutId = setTimeout (() => controller . abort (), 15000 ); // 15s Timeout
2025-12-11 11:25:26 +01:00
2025-12-12 11:46:04 +01:00
try {
console . log ( `[Sync] Starting fetch from: ${ url } ` );
let directUrl = url ;
// Create a direct download link for Dropbox if it's a share link.
if ( url . includes ( 'dropbox.com/' ) && ! url . includes ( 'dl.dropboxusercontent.com' )) {
const urlObject = new URL ( url );
urlObject . searchParams . set ( 'dl' , '1' );
directUrl = urlObject . toString ();
2025-12-11 11:25:26 +01:00
}
2025-12-12 11:46:04 +01:00
// Use a CORS proxy to bypass browser's same-origin policy restrictions.
const proxyUrl = `https://corsproxy.io/? ${ encodeURIComponent ( directUrl ) } ` ;
console . log ( `[Sync] Fetching via proxy: ${ proxyUrl } ` );
const response = await fetch ( proxyUrl , {
signal : controller.signal
});
if ( ! response . ok ) {
throw new Error ( `Failed to fetch CSV: ${ response . status } ${ response . statusText } ` );
}
const csvText = await response . text ();
console . log ( `[Sync] Download complete. Bytes: ${ csvText . length } ` );
if ( ! csvText || csvText . trim (). length === 0 ) {
throw new Error ( "Downloaded file is empty" );
}
const data = await processCSV ( csvText );
console . log ( `[Sync] Processing complete. Rows: ${ data . length } ` );
await saveSalesData ( data );
initializeData ( data );
setActiveUrl ( url ); // Store the original user-facing URL
const now = new Date (). toISOString ();
setLastUpdated ( now );
localStorage . setItem ( 'craze_last_updated' , now );
localStorage . setItem ( 'craze_csv_url' , url );
setIsDataModalOpen ( false ); // Close modal on success
} catch ( error : any ) {
console . error ( "[Sync] Error during data fetch:" , error );
let msg = "Failed to sync data." ;
if ( error . name === 'AbortError' ) {
msg = "Connection timed out (15s). Proxy might be slow." ;
} else if ( error . message ) {
msg = error . message ;
}
alert ( `Error: ${ msg } \ n \ nSwitching to manual mode.` );
// IMPORTANT: If fetch fails, we MUST stop loading so the user can interact
setRawData ([]);
setActiveUrl ( null ); // Clear active URL so valid manual upload is required or retry
} finally {
clearTimeout ( timeoutId );
setSyncing ( false );
setLoading ( false );
}
2025-12-11 11:25:26 +01:00
}, []);
const initializeData = ( data : SalesRecord []) => {
2025-12-12 11:46:04 +01:00
setRawData ( data );
setFilters ({
customer : [],
year : [],
month : [],
line : [],
asin : [],
sku : [],
title : [],
});
2025-12-11 11:25:26 +01:00
};
// 1. Initial Load from Cache (IndexedDB) or Auto-Fetch Permanent URL
useEffect (() => {
const initApp = async () => {
2025-12-12 11:46:04 +01:00
setLoading ( true );
console . log ( "[Init] App starting..." );
try {
2025-12-11 11:25:26 +01:00
const currentStoredUrl = localStorage . getItem ( 'craze_csv_url' );
if ( currentStoredUrl !== PERMANENT_DROPBOX_URL ) {
2025-12-12 11:46:04 +01:00
console . log ( "[Init] Updating stored URL to default permanent URL" );
localStorage . setItem ( 'craze_csv_url' , PERMANENT_DROPBOX_URL );
setActiveUrl ( PERMANENT_DROPBOX_URL );
2025-12-11 11:25:26 +01:00
}
const { data , lastUpdated : date } = await loadSalesData ();
2025-12-12 11:46:04 +01:00
2025-12-11 11:25:26 +01:00
if ( data && data . length > 0 ) {
2025-12-12 11:46:04 +01:00
console . log ( "[Init] Loaded data from cache:" , data . length , "rows. Last updated:" , date );
initializeData ( data );
setLastUpdated ( date );
setLoading ( false );
2025-12-11 11:25:26 +01:00
} else {
2025-12-12 11:46:04 +01:00
console . log ( "[Init] No cache found. Auto-fetching from Permanent URL..." );
// Catch error here so initApp doesn't crash, handleUrlFetch handles UI
await handleUrlFetch ( PERMANENT_DROPBOX_URL );
2025-12-11 11:25:26 +01:00
}
2025-12-12 11:46:04 +01:00
} catch ( e ) {
console . error ( "[Init] Critical failure during initialization:" , e );
setLoading ( false ); // Ensure we never get stuck in infinite load
}
2025-12-11 11:25:26 +01:00
};
initApp ();
}, [ handleUrlFetch ]);
2025-12-11 15:08:01 +01:00
// Handle uploaded Sales file (Manual)
const handleSalesUpload = async ( file : File ) => {
2025-12-11 11:25:26 +01:00
setSyncing ( true );
try {
2025-12-11 14:03:33 +01:00
const data = await processCSV ( file );
2025-12-11 11:25:26 +01:00
await saveSalesData ( data );
initializeData ( data );
setLastUpdated ( new Date (). toISOString ());
setIsDataModalOpen ( false );
} catch ( error ) {
2025-12-11 14:03:33 +01:00
console . error ( "Failed to parse CSV" , error );
alert ( "Error parsing CSV. Please check the format." );
2025-12-11 11:25:26 +01:00
} finally {
setSyncing ( false );
}
};
2025-12-11 15:08:01 +01:00
// Handle uploaded Ads file (Manual)
const handleAdsUpload = async ( file : File ) => {
setSyncing ( true );
try {
2025-12-12 11:46:04 +01:00
const data = await processAdsCSV ( file );
setAdsData ( data );
console . log ( "Ads loaded:" , data . length );
setIsDataModalOpen ( false );
setView ( 'ads' ); // Switch to ads view automatically
2025-12-11 15:08:01 +01:00
} catch ( error ) {
2025-12-12 11:46:04 +01:00
console . error ( "Failed to parse Ads CSV" , error );
alert ( "Error parsing Ads CSV. Please check the format." );
2025-12-11 15:08:01 +01:00
} finally {
2025-12-12 11:46:04 +01:00
setSyncing ( false );
2025-12-11 15:08:01 +01:00
}
};
2025-12-11 11:25:26 +01:00
// 2. Schedule Auto-Refresh (Background)
useEffect (() => {
2025-12-12 11:46:04 +01:00
const checkAndRefresh = () => {
const now = new Date ();
const today = now . toISOString (). split ( 'T' )[ 0 ]; // YYYY-MM-DD
const lastRefreshDate = localStorage . getItem ( 'craze_last_refresh_date' );
2025-12-11 11:25:26 +01:00
2025-12-12 11:46:04 +01:00
// Refresh if it's after 7 AM and we haven't refreshed today
if ( now . getHours () >= 7 && lastRefreshDate !== today ) {
console . log ( "Triggering daily data refresh..." );
// Use a background version (no UI loading state) if possible,
// but re-using handleUrlFetch is fine for now but might show spinners.
// Ideally split the "silent sync" logic.
handleUrlFetch ( PERMANENT_DROPBOX_URL ). then (() => {
localStorage . setItem ( 'craze_last_refresh_date' , today );
console . log ( "Daily refresh successful." );
}). catch ( err => {
console . error ( "Daily refresh failed, will retry later." , err );
});
}
};
2025-12-11 11:25:26 +01:00
2025-12-12 11:46:04 +01:00
// Check immediately on load in case the user opens the app after 7 AM
// checkAndRefresh(); // Disabled for now to prevent double-fetch on startup logic conflict
2025-12-11 11:25:26 +01:00
2025-12-12 11:46:04 +01:00
// And then check periodically (e.g., every 15 minutes) in case app is left open across midnight
// const interval = setInterval(checkAndRefresh, 15 * 60 * 1000);
// return () => clearInterval(interval);
2025-12-11 11:25:26 +01:00
}, [ handleUrlFetch ]);
// Derive Data
const filteredData = useMemo (() => filterData ( rawData , filters ), [ rawData , filters ]);
const aggregatedData = useMemo (() => aggregateData ( filteredData ), [ filteredData ]);
2025-12-12 11:46:04 +01:00
2025-12-11 15:08:01 +01:00
// Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo (() => {
2025-12-12 11:46:04 +01:00
return mergeSalesAndAdsData ( filteredData , adsData );
2025-12-11 15:08:01 +01:00
}, [ filteredData , adsData ]);
2025-12-11 11:25:26 +01:00
// Derive Context Data (Product Line Context when drilling down)
const contextAggregatedData = useMemo (() => {
// Check if we are filtering by specific items (SKU, ASIN, Title)
const hasItemFilters = filters . sku . length > 0 || filters . asin . length > 0 || filters . title . length > 0 ;
if ( ! hasItemFilters || filteredData . length === 0 ) {
return null ;
}
// 1. Identify the Product Lines associated with the currently filtered items
const activeLines = Array . from ( new Set ( filteredData . map ( r => r . line )));
// 2. Create a "broad" filter: Keep Year/Customer/Month, but CLEAR Item filters, and restrict to these Lines
const contextFilters : FilterState = {
... filters ,
line : activeLines , // Force these lines
sku : [], // Clear specific item filters
asin : [],
title : []
};
// 3. Process this broader dataset
const broadData = filterData ( rawData , contextFilters );
return aggregateData ( broadData );
}, [ rawData , filters , filteredData ]);
// Derive Options for Filter Dropdowns
const filterOptions = useMemo (() => {
return {
customer : getUniqueValues ( rawData , 'customer' ),
year : getUniqueValues ( rawData , 'year' ),
month : [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ],
line : getUniqueValues ( rawData , 'line' ),
asin : getUniqueValues ( rawData , 'asin' ),
sku : getUniqueValues ( rawData , 'sku' ),
title : getUniqueValues ( rawData , 'title' ),
};
}, [ rawData ]);
const handleFilterChange = ( key : keyof FilterState , value : string []) => {
setFilters ( prev => ({ ... prev , [ key ] : value }));
};
const handleAskGemini = async ( text : string ) => {
2025-12-11 14:03:33 +01:00
return await queryGemini ( text , aggregatedData , filteredData . length );
2025-12-11 11:25:26 +01:00
};
const disconnectUrl = async () => {
2025-12-12 11:46:04 +01:00
// Allow disconnecting to clear data, but the app will likely re-connect on next reload due to "Permanent" requirement
localStorage . removeItem ( 'craze_csv_url' );
await clearSalesData ();
setActiveUrl ( null );
setRawData ([]);
};
const handleSkipLoading = () => {
console . log ( "User skipped loading." );
setLoading ( false );
setSyncing ( false );
2025-12-11 11:25:26 +01:00
};
return (
< div className = "min-h-screen flex flex-col bg-background text-slate-200" >
2025-12-12 11:46:04 +01:00
2025-12-11 11:25:26 +01:00
{ /* Header */ }
< header className = "bg-slate-950/90 backdrop-blur border-b border-border py-4 px-6 relative z-40 shadow-2xl" >
< div className = "max-w-7xl mx-auto flex justify-between items-center" >
< div className = "flex items-center gap-8" >
2025-12-12 11:46:04 +01:00
{ /* Logo Container (Horizontal Box) - Persistent User Image */ }
< div className = "h-16 w-64 relative flex-shrink-0" >
< CrazeLogo />
</ div >
2025-12-11 11:25:26 +01:00
2025-12-12 11:46:04 +01:00
{ /* Title & Status */ }
< div className = "hidden lg:block border-l border-slate-700 pl-6" >
< h1 className = "text-xl font-bold tracking-tight text-white text-shadow-sm" > Analytics Dashboard </ h1 >
{ activeUrl && lastUpdated && (
< p className = "text-[10px] text-emerald-400 mt-1 flex items-center gap-1 uppercase font-bold tracking-wider" >
< span className = "w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" ></ span >
Live Sync Active
</ p >
)}
</ div >
2025-12-11 11:25:26 +01:00
</ div >
2025-12-12 11:46:04 +01:00
2025-12-11 11:25:26 +01:00
< div className = "flex items-center gap-4" >
2025-12-12 11:46:04 +01:00
2025-12-11 11:25:26 +01:00
{ /* Main Action: Data Source Button */ }
2025-12-12 11:46:04 +01:00
< button
onClick = {() => setIsDataModalOpen ( true )}
className = { `flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-bold shadow-lg transition-all border
${ activeUrl
? 'bg-slate-800 text-emerald-400 border-emerald-500/50 hover:bg-slate-700'
: 'bg-indigo-600 text-white border-transparent hover:bg-indigo-500' } ` }
2025-12-11 11:25:26 +01:00
>
2025-12-12 11:46:04 +01:00
{ syncing ? < div className = "w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" ></ div > : < UploadIcon />}
< span className = "hidden md:inline" >{ activeUrl ? 'Data Settings' : 'Connect Data' }</ span >
2025-12-11 11:25:26 +01:00
</ button >
{ /* NEW REFRESH BUTTON */ }
< button
2025-12-12 11:46:04 +01:00
onClick = {() => activeUrl && handleUrlFetch ( activeUrl )}
disabled = { syncing }
title = "Refresh Data"
className = "p-3 rounded-lg bg-slate-800 border border-border text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
2025-12-11 11:25:26 +01:00
>
2025-12-12 11:46:04 +01:00
< RefreshIcon className = { `w-5 h-5 ${ syncing ? 'animate-spin' : '' } ` } />
2025-12-11 11:25:26 +01:00
</ button >
{ /* View Switcher */ }
< div className = "flex bg-slate-900 rounded-lg p-1 border border-border" >
2025-12-12 11:46:04 +01:00
< button
onClick = {() => setView ( 'dashboard' )}
className = { `flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
2025-12-11 11:25:26 +01:00
${ view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50' } ` }
2025-12-12 11:46:04 +01:00
>
< ChartIcon /> < span className = "hidden sm:inline" > Dashboard </ span >
</ button >
< button
onClick = {() => setView ( 'table' )}
className = { `flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
2025-12-11 11:25:26 +01:00
${ view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50' } ` }
2025-12-12 11:46:04 +01:00
>
< TableIcon /> < span className = "hidden sm:inline" > Grid </ span >
</ button >
< button
onClick = {() => setView ( 'movers' )}
className = { `flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
2025-12-11 14:03:33 +01:00
${ view === 'movers' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50' } ` }
2025-12-12 11:46:04 +01:00
>
< TrendingIcon /> < span className = "hidden sm:inline" > Movers </ span >
</ button >
< button
onClick = {() => setView ( 'ads' )}
className = { `flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
2025-12-11 15:08:01 +01:00
${ view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50' } ` }
2025-12-12 11:46:04 +01:00
>
< MegaphoneIcon /> < span className = "hidden sm:inline" > Ads </ span >
</ button >
2025-12-11 11:25:26 +01:00
</ div >
</ div >
</ div >
</ header >
{ /* Main Content */ }
< main className = "flex-1 relative" >
{ loading ? (
2025-12-12 11:46:04 +01:00
// Initial loading spinner with SKIP Option
< div className = "flex flex-col items-center justify-center h-[80vh] gap-6 text-center animate-fade-in" >
< div className = "relative" >
< div className = "w-20 h-20 border-4 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin" ></ div >
< div className = "absolute inset-0 flex items-center justify-center" >
< UploadIcon className = "w-8 h-8 text-indigo-400 animate-pulse" />
</ div >
2025-12-11 11:25:26 +01:00
</ div >
2025-12-12 11:46:04 +01:00
< div >
< h2 className = "text-2xl font-bold text-white mb-2" > Syncing Data </ h2 >
< p className = "text-slate-400 max-w-md mx-auto" >
Connecting to Dropbox to fetch the latest analytics ...
</ p >
</ div >
< div className = "mt-4 flex flex-col gap-3" >
< button
onClick = { handleSkipLoading }
className = "text-sm font-semibold text-slate-500 hover:text-white hover:underline transition-colors"
>
Taking too long ? Skip Sync
</ button >
</ div >
</ div >
2025-12-11 11:25:26 +01:00
) : (
2025-12-12 11:46:04 +01:00
<>
< FilterBar filters = { filters } onFilterChange = { handleFilterChange } options = { filterOptions } />
< div className = "mt-6" >
{ view === 'dashboard' && (
< Dashboard
data = { aggregatedData }
contextData = { contextAggregatedData }
/>
)}
{ view === 'table' && < DataGrid data = { filteredData } />}
{ view === 'movers' && < TopMovers data = { filteredData } />}
{ view === 'ads' && < AdvertisingDashboard data = { combinedAdsData } />}
</ div >
</>
2025-12-11 11:25:26 +01:00
)}
</ main >
2025-12-11 14:03:33 +01:00
{ /* Chat Assistant */ }
< AIChat onSendMessage = { handleAskGemini } isOpen = { isChatOpen } setIsOpen = { setIsChatOpen } />
2025-12-11 11:25:26 +01:00
{ /* DATA MODAL */ }
{ isDataModalOpen && (
< div className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm animate-fade-in p-4" >
2025-12-12 11:46:04 +01:00
< div className = "bg-slate-950 border border-border rounded-2xl shadow-2xl w-full max-w-lg relative overflow-hidden" >
< div className = "bg-slate-900 px-6 py-4 border-b border-border flex justify-between items-center" >
< h2 className = "text-lg font-bold text-white" > Data Source Settings </ h2 >
< button onClick = {() => setIsDataModalOpen ( false )} className = "text-slate-400 hover:text-white transition-colors" >
< CloseIcon />
</ button >
2025-12-11 11:25:26 +01:00
</ div >
2025-12-12 11:46:04 +01:00
< div className = "p-6" >
< FileUpload
onSalesUpload = { handleSalesUpload } // CORRECTED: Was handleFileUpload
onAdsUpload = { handleAdsUpload } // ADDED: Missing prop causing error
onUrlSubmit = { handleUrlFetch }
isLoading = { syncing }
activeUrl = { activeUrl }
onDisconnect = { disconnectUrl }
lastUpdated = { lastUpdated }
/>
</ div >
</ div >
2025-12-11 11:25:26 +01:00
</ div >
)}
</ div >
);
};
2025-12-11 14:03:33 +01:00
export default App ;