feat: automatic ads data loading and attributed sales line in chart

This commit is contained in:
Christian Vidal Wolf
2026-01-21 10:46:50 +01:00
parent 1c826c2487
commit e5c72c48c5
6 changed files with 307 additions and 9 deletions
+50 -5
View File
@@ -9,10 +9,10 @@ import FilterBar from './components/FilterBar';
import AIChat from './components/AIChat';
import CrazeLogo from './components/CrazeLogo';
import { SalesRecord, FilterState, AggregatedData, AdsRecord } from './types'; // Imported AdsRecord
import { processCSV, filterData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor'; // Imported new processors
import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor';
import { queryGemini } from './services/geminiService';
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon } from './components/Icons';
import { loadSalesData, saveSalesData, clearSalesData } from './services/storage';
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage';
// New Refresh Icon
const RefreshIcon = ({ className }: { className?: string }) => (
@@ -24,6 +24,7 @@ const RefreshIcon = ({ className }: { className?: string }) => (
// 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&st=pzn1zkrg&dl=0";
const PERMANENT_ADS_URL = "https://www.dropbox.com/scl/fi/wng8tep7awhvzd65amwad/Ads-Weekly.xlsx?rlkey=kcmoq8dxgibsb2eb8zz47xvyo&st=z02m4w3g&dl=0";
const App: React.FC = () => {
const [rawData, setRawData] = useState<SalesRecord[]>([]);
@@ -84,6 +85,29 @@ const App: React.FC = () => {
}
}, []);
const handleAdsFetch = useCallback(async () => {
setSyncing(true);
try {
console.log('[App] Fetching ads from /api/fetch-ads...');
const response = await fetch('/api/fetch-ads');
if (!response.ok) {
throw new Error(`Failed to fetch Ads: ${response.status} ${response.statusText}`);
}
const buffer = await response.arrayBuffer();
const data = await processAdsExcel(buffer);
await saveAdsData(data);
setAdsData(data);
console.log('[App] Successfully loaded', data.length, 'ads records');
} catch (error) {
console.error("Failed to fetch/parse Ads", error);
} finally {
setSyncing(false);
}
}, []);
const initializeData = (data: SalesRecord[]) => {
setRawData(data);
setFilters({
@@ -134,6 +158,16 @@ const App: React.FC = () => {
console.error("Initial fetch failed.");
});
}
// 1b. Load Ads from cache or auto-fetch
const { data: cachedAds, lastUpdated: adsLastUpdated } = await loadAdsData();
if (cachedAds && cachedAds.length > 0) {
console.log("Loaded ads from cache:", cachedAds.length, "records");
setAdsData(cachedAds);
} else {
console.log("Fetching fresh Ads from URL...");
handleAdsFetch();
}
};
initApp();
}, [handleDataFetch]);
@@ -147,6 +181,8 @@ const App: React.FC = () => {
initializeData(data);
setLastUpdated(new Date().toISOString());
setIsDataModalOpen(false);
// Also trigger ads fetch if not loaded to maintain sync
if (adsData.length === 0) handleAdsFetch();
} catch (error) {
console.error("Failed to parse CSV", error);
alert("Error parsing CSV. Please check the format.");
@@ -162,6 +198,7 @@ const App: React.FC = () => {
const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
const data = isExcel ? await processAdsExcel(file) : await processAdsCSV(file);
setAdsData(data);
await saveAdsData(data);
console.log("Ads loaded:", data.length, "records from", file.name);
setIsDataModalOpen(false);
} catch (error) {
@@ -185,6 +222,8 @@ const App: React.FC = () => {
handleDataFetch().then(() => {
localStorage.setItem('craze_last_refresh_date', today);
console.log("Daily refresh successful.");
// Also refresh ads
handleAdsFetch();
}).catch(err => {
console.error("Daily refresh failed, will retry later.", err);
});
@@ -203,6 +242,7 @@ const App: React.FC = () => {
// Derive Data
const filteredData = useMemo(() => filterData(rawData, filters), [rawData, filters]);
const filteredAdsData = useMemo(() => filterAdsData(adsData, filters), [adsData, filters]);
const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]);
// Combine Sales & Ads Data dynamically based on current filters
@@ -264,8 +304,10 @@ const App: React.FC = () => {
// 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();
await clearAdsData();
setActiveUrl(null);
setRawData([]);
setAdsData([]);
};
return (
@@ -308,7 +350,10 @@ const App: React.FC = () => {
{/* NEW REFRESH BUTTON */}
<button
onClick={handleDataFetch}
onClick={() => {
handleDataFetch();
handleAdsFetch();
}}
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"
@@ -361,10 +406,10 @@ const App: React.FC = () => {
<Dashboard
data={aggregatedData}
contextData={contextAggregatedData}
adsData={adsData}
adsData={filteredAdsData}
/>
)}
{view === 'table' && <DataGrid data={filteredData} hasCustomerFilter={filters.customer.length > 0} adsData={adsData} />}
{view === 'table' && <DataGrid data={filteredData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
{view === 'movers' && <TopMovers data={filteredData} />}
</div>
</>