diff --git a/App.tsx b/App.tsx index e4a6891..db6aa62 100644 --- a/App.tsx +++ b/App.tsx @@ -5,8 +5,8 @@ import Dashboard from './components/Dashboard'; 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, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor'; +import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord } from './types'; +import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData } from './services/dataProcessor'; import { queryGemini } from './services/geminiService'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons'; import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage'; @@ -38,7 +38,8 @@ const PERMANENT_ADS_URL = "https://www.dropbox.com/scl/fi/wng8tep7awhvzd65amwad/ const App: React.FC = () => { const [rawData, setRawData] = useState([]); - const [adsData, setAdsData] = useState([]); // New Ads State + const [adsData, setAdsData] = useState([]); + const [trafficData, setTrafficData] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads'>('dashboard'); // Added 'weekly' view @@ -228,6 +229,22 @@ const App: React.FC = () => { } }; + // Handle uploaded Traffic file (Manual) + const handleTrafficUpload = async (file: File) => { + setSyncing(true); + try { + const data = await processTrafficExcel(file); + setTrafficData(data); + console.log("Traffic loaded:", data.length, "records from", file.name); + setIsDataModalOpen(false); + } catch (error: any) { + console.error("Failed to parse Traffic file", error); + alert(`Error parsing Traffic file: ${error.message || 'Unknown error'}. Please check the format.`); + } finally { + setSyncing(false); + } + }; + // 2. Schedule Auto-Refresh (Background) useEffect(() => { const checkAndRefresh = () => { @@ -322,8 +339,8 @@ const App: React.FC = () => { // Combine Sales & Ads Data dynamically based on current filters const combinedAdsData = useMemo(() => { - return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata); - }, [filteredData, filteredAdsData, globalAsinMetadata]); + return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData); + }, [filteredData, filteredAdsData, globalAsinMetadata, trafficData]); // Derive Context Data (Product Line Context when drilling down) const contextAggregatedData = useMemo(() => { @@ -527,6 +544,7 @@ const App: React.FC = () => { = ({ data, filters }) => { target.impressions += item.impressions; target.conversions += item.conversions; target.salesOrganic += item.salesOrganic; + target.glanceViews = (target.glanceViews || 0) + (item.glanceViews || 0); } } }); @@ -112,7 +113,8 @@ const AdsPerformance: React.FC = ({ data, filters }) => { impressions: acc.impressions + curr.impressions, clicks: acc.clicks + curr.clicks, conversions: acc.conversions + curr.conversions, - }), { salesTotal: 0, salesAds: 0, salesOrganic: 0, cost: 0, impressions: 0, clicks: 0, conversions: 0 }); + glanceViews: acc.glanceViews + (curr.glanceViews || 0), + }), { salesTotal: 0, salesAds: 0, salesOrganic: 0, cost: 0, impressions: 0, clicks: 0, conversions: 0, glanceViews: 0 }); const currTotals = sum(aggregatedByAsin); const prevTotals = comparisonConfig ? sum(aggregatedByAsin.map(a => a.prevData).filter(Boolean)) : null; @@ -127,6 +129,7 @@ const AdsPerformance: React.FC = ({ data, filters }) => { ctr: t.impressions > 0 ? (t.clicks / t.impressions) * 100 : 0, cpc: t.clicks > 0 ? t.cost / t.clicks : 0, cvr: t.clicks > 0 ? (t.conversions / t.clicks) * 100 : 0, + cvrGv: t.glanceViews > 0 ? (t.conversions / t.glanceViews) * 100 : 0, }); const currMetrics = calculateDerived(totals.curr); @@ -196,6 +199,8 @@ const AdsPerformance: React.FC = ({ data, filters }) => { + + {/* Table Section */} @@ -236,6 +241,7 @@ const AdsPerformance: React.FC = ({ data, filters }) => { + @@ -261,6 +267,7 @@ const AdsPerformance: React.FC = ({ data, filters }) => { + ))} diff --git a/components/FileUpload.tsx b/components/FileUpload.tsx index bf5d30f..4374d8b 100644 --- a/components/FileUpload.tsx +++ b/components/FileUpload.tsx @@ -5,7 +5,8 @@ import { UploadIcon, MegaphoneIcon } from './Icons'; interface FileUploadProps { onSalesUpload: (file: File) => void; onAdsUpload: (file: File) => void; - onUrlSubmit: () => void; // Changed: no longer takes URL parameter + onTrafficUpload?: (file: File) => void; + onUrlSubmit: () => void; isLoading: boolean; activeUrl?: string | null; onDisconnect?: () => void; @@ -15,6 +16,7 @@ interface FileUploadProps { const FileUpload: React.FC = ({ onSalesUpload, onAdsUpload, + onTrafficUpload, onUrlSubmit, isLoading, activeUrl, @@ -35,6 +37,12 @@ const FileUpload: React.FC = ({ } }; + const handleTrafficChange = (e: ChangeEvent) => { + if (e.target.files && e.target.files.length > 0 && onTrafficUpload) { + onTrafficUpload(e.target.files[0]); + } + }; + const handleUrlSubmit = (e: React.FormEvent) => { e.preventDefault(); if (url.trim()) { @@ -128,6 +136,31 @@ const FileUpload: React.FC = ({ /> + + {onTrafficUpload && ( +
+ +
+ )} ); diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 674c6df..cb0a408 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1,4 +1,4 @@ -import { SalesRecord, AdsRecord, CombinedKPIs, FilterState, AggregatedData, LineGrowthMetric, ItemGrowthMetric, SeasonalityPoint, YearlySplitData, PivotRow, YearlyData, TimeSeriesData, ComparisonTimeSeriesPoint } from '../types'; +import { SalesRecord, AdsRecord, TrafficRecord, CombinedKPIs, FilterState, AggregatedData, LineGrowthMetric, ItemGrowthMetric, SeasonalityPoint, YearlySplitData, PivotRow, YearlyData, TimeSeriesData, ComparisonTimeSeriesPoint } from '../types'; import * as XLSX from 'xlsx'; import Papa from 'papaparse'; @@ -439,17 +439,87 @@ export const processAdsExcel = async (fileOrBuffer: File | ArrayBuffer): Promise } }; +// --- TRAFFIC DATA PARSING --- + +export const processTrafficExcel = async (fileOrBuffer: File | ArrayBuffer): Promise => { + try { + const arrayBuffer = fileOrBuffer instanceof File + ? await fileOrBuffer.arrayBuffer() + : fileOrBuffer; + const workbook = XLSX.read(arrayBuffer, { type: 'array' }); + const allData: TrafficRecord[] = []; + + // Process first sheet only (Traffic Weekly.xlsx typically has one sheet) + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" }); + + console.log(`Processing Traffic sheet "${sheetName}": ${jsonData.length} rows`); + + // Column mapping for Traffic Weekly.xlsx: + // A (0): Year + // B (1): Week + // C (2): ASIN + // F (5): Country + // G (6): Glance Views (GV) + + // Skip header row (index 0), process data rows + for (let i = 1; i < jsonData.length; i++) { + const row = jsonData[i]; + if (!row || row.length < 7) continue; + + const yearRaw = row[0]; + const weekRaw = row[1]; + const asin = row[2]; + const countryRaw = row[5]; + const gvRaw = row[6]; + + // Skip if missing essential data + if (!asin || yearRaw === undefined || weekRaw === undefined || !countryRaw) continue; + + const year = parseInt(String(yearRaw)); + const weekNum = parseInt(String(weekRaw)); + if (isNaN(year) || year < 2020 || year > 2100) continue; + if (isNaN(weekNum) || weekNum < 1 || weekNum > 53) continue; + + allData.push({ + country: mapCountryToMarketplace(String(countryRaw)), + year, + week: weekNum, + asin: String(asin).trim().toUpperCase(), + glanceViews: parseUnits(String(gvRaw)), + }); + } + + console.log(`Total Traffic records loaded: ${allData.length}`); + return allData; + } catch (error) { + console.error("Error processing Traffic Excel:", error); + throw error; + } +}; + // --- DATA MERGING --- export const mergeSalesAndAdsData = ( salesData: SalesRecord[], adsData: AdsRecord[], - asinMetadataMap?: Map + asinMetadataMap?: Map, + trafficData?: TrafficRecord[] ): CombinedKPIs[] => { // Key for both sales and ads: ASIN|Customer|Year|Week const createKey = (asin: string, customer: string, year: number, week: number) => `${asin.trim().toUpperCase()}|${customer.trim().toUpperCase()}|${year}|${week}`; + // Build traffic lookup map + const trafficMap = new Map(); + if (trafficData) { + trafficData.forEach(t => { + const key = createKey(t.asin, t.country, t.year, t.week); + trafficMap.set(key, (trafficMap.get(key) || 0) + t.glanceViews); + }); + } + // 1. Initialize metadata lookup map with provided global map if available, otherwise build from current sales const asinMetadata = asinMetadataMap || new Map(); @@ -579,7 +649,8 @@ export const mergeSalesAndAdsData = ( roas, ctr, cpc, - cvrUnits + cvrUnits, + glanceViews: trafficMap.get(key) || 0 }); }); @@ -617,7 +688,8 @@ export const mergeSalesAndAdsData = ( roas: ad.cost > 0 ? ad.attributedSales30d / ad.cost : 0, ctr: ad.impressions > 0 ? (ad.clicks / ad.impressions) * 100 : 0, cpc: ad.clicks > 0 ? ad.cost / ad.clicks : 0, - cvrUnits: ad.clicks > 0 ? (ad.attributedUnits30d / ad.clicks) * 100 : 0 + cvrUnits: ad.clicks > 0 ? (ad.attributedUnits30d / ad.clicks) * 100 : 0, + glanceViews: trafficMap.get(key) || 0 }); } }); diff --git a/types.ts b/types.ts index 6537a72..84e9a8b 100644 --- a/types.ts +++ b/types.ts @@ -150,6 +150,14 @@ export interface AdsRecord { attributedSales30d: number; } +export interface TrafficRecord { + country: string; // Marketplace (DE, UK, etc.) + year: number; + week: number; + asin: string; + glanceViews: number; // Page views +} + export interface CombinedKPIs { id: string; marketplace: string; @@ -184,4 +192,5 @@ export interface CombinedKPIs { ctr: number; cpc: number; cvrUnits: number; + glanceViews: number; // Traffic / page views }