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.
2026-01-16 10:20:22 +01:00
const PERMANENT_DROPBOX_URL = "https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&st=7vk22iod&dl=0" ;
2025-12-11 11:25:26 +01:00
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 );
try {
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
2026-01-16 10:50:39 +01:00
// Unified Fetch Logic for Local (Vite) and Production (Vercel Function)
// Both environments now support the /api/dropbox/... path.
// - Local: Vite proxies /api/dropbox -> https://www.dropbox.com
// - Vercel: api/dropbox.js handles the request -> https://www.dropbox.com
2025-12-12 11:46:04 +01:00
2026-01-16 10:50:39 +01:00
const urlObj = new URL ( directUrl );
const searchParams = urlObj . search ;
// Construct path relative to root: /api/dropbox/scl/fi/...
const fetchUrl = `/api/dropbox ${ urlObj . pathname }${ searchParams } ` ;
2025-12-12 11:46:04 +01:00
2026-01-16 10:20:22 +01:00
const response = await fetch ( fetchUrl );
if ( ! response . ok ) throw new Error ( `Failed to fetch CSV from URL: ${ response . status } ${ response . statusText } ` );
2025-12-12 11:46:04 +01:00
const csvText = await response . text ();
const data = await processCSV ( csvText );
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
2026-01-16 10:20:22 +01:00
} catch ( error ) {
console . error ( "Failed to fetch/parse CSV from URL" , error );
// Don't alert on auto-fetch to avoid spamming the user on startup if offline
// alert("Error syncing data. Please check the URL.");
throw error ; // re-throw to be caught by caller
2025-12-12 11:46:04 +01:00
} finally {
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 : [],
2026-01-16 11:47:59 +01:00
week : [],
2025-12-12 11:46:04 +01:00
});
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 );
2026-01-16 10:20:22 +01:00
const currentStoredUrl = localStorage . getItem ( 'craze_csv_url' );
if ( currentStoredUrl !== PERMANENT_DROPBOX_URL ) {
localStorage . setItem ( 'craze_csv_url' , PERMANENT_DROPBOX_URL );
setActiveUrl ( PERMANENT_DROPBOX_URL );
}
2025-12-11 11:25:26 +01:00
2026-01-16 10:20:22 +01:00
const { data , lastUpdated : date } = await loadSalesData ();
2025-12-12 11:46:04 +01:00
2026-01-16 10:20:22 +01:00
if ( data && data . length > 0 ) {
console . log ( "Loaded data from cache:" , data . length , "rows" );
initializeData ( data );
setLastUpdated ( date );
setLoading ( false );
} else {
console . log ( "No cache found. Auto-fetching from Permanent URL..." );
handleUrlFetch ( PERMANENT_DROPBOX_URL ). catch ( e => {
console . error ( "Initial fetch failed." );
});
2025-12-12 11:46:04 +01:00
}
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..." );
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
2026-01-16 10:20:22 +01:00
checkAndRefresh ();
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
2026-01-16 10:20:22 +01:00
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' ),
2026-01-16 11:47:59 +01:00
week : Array.from ( new Set ( rawData . map ( r => r . week ). filter ( w => w !== undefined ))). sort (( a , b ) => ( a as number ) - ( b as number )). map ( w => `W ${ w } ` ),
2025-12-11 11:25:26 +01:00
};
}, [ 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 ([]);
};
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 ? (
2026-01-16 10:20:22 +01:00
// Initial loading spinner
< div className = "flex flex-col items-center justify-center h-[80vh] gap-4" >
< div className = "w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin" ></ div >
< h2 className = "text-xl font-bold text-slate-300" > Loading Dashboard ...</ h2 >
< p className = "text-sm text-slate-500" > Syncing with Dropbox ...</ p >
2025-12-12 11:46:04 +01:00
</ 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 >
);
};
2026-01-16 10:20:22 +01:00
2025-12-11 14:03:33 +01:00
export default App ;