diff --git a/App.tsx b/App.tsx index cedf8d7..12ac8c7 100644 --- a/App.tsx +++ b/App.tsx @@ -5,7 +5,7 @@ import Dashboard from './components/Dashboard'; import FilterBar from './components/FilterBar'; import AIChat from './components/AIChat'; import CrazeLogo from './components/CrazeLogo'; -import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel } from './services/dataProcessor'; +import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel } from './services/dataProcessor'; import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types'; import { queryGemini } from './services/geminiService'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons'; @@ -185,16 +185,38 @@ const App: React.FC = () => { const handleVendorStockFetch = useCallback(async () => { try { - console.log('[App] Fetching PANEU vendor stock from /api/fetch-paneu-stock...'); - const response = await fetch('/api/fetch-paneu-stock'); - if (!response.ok) throw new Error(`Failed to fetch PANEU vendor stock: ${response.status}`); + console.log('[App] Fetching vendor stock (PANEU + UK) from Dropbox...'); - const buffer = await response.arrayBuffer(); - const data = await processVendorStockExcel(buffer); - setVendorStockMap(data); - console.log('[App] Successfully loaded PANEU vendor stock for', data.size, 'ASINs'); + // Fetch both PANEU and UK inventory in parallel + const [paneuResponse, ukResponse] = await Promise.all([ + fetch('/api/fetch-paneu-stock'), + fetch('/api/fetch-uk-inventory') + ]); + + let combinedMap = new Map(); + + // Process PANEU stock (EU countries) + if (paneuResponse.ok) { + const paneuBuffer = await paneuResponse.arrayBuffer(); + combinedMap = await processVendorStockExcel(paneuBuffer); + console.log('[App] PANEU stock loaded:', combinedMap.size, 'ASINs'); + } else { + console.warn('[App] PANEU fetch failed:', paneuResponse.status); + } + + // Process UK inventory and merge with existing map + if (ukResponse.ok) { + const ukBuffer = await ukResponse.arrayBuffer(); + combinedMap = await processUKInventoryExcel(ukBuffer, combinedMap); + console.log('[App] UK inventory merged. Total ASINs:', combinedMap.size); + } else { + console.warn('[App] UK inventory fetch failed:', ukResponse.status); + } + + setVendorStockMap(combinedMap); + console.log('[App] Successfully loaded combined vendor stock for', combinedMap.size, 'ASINs'); } catch (error) { - console.error("Failed to fetch/parse PANEU Vendor Stock", error); + console.error("Failed to fetch/parse Vendor Stock", error); } }, []); diff --git a/api/fetch-uk-inventory.ts b/api/fetch-uk-inventory.ts new file mode 100644 index 0000000..a8a13f5 --- /dev/null +++ b/api/fetch-uk-inventory.ts @@ -0,0 +1,39 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +// UK Inventory from Dropbox (using dl=1 for direct download) +const UK_INVENTORY_DROPBOX_URL = "https://www.dropbox.com/scl/fi/an1ldjmtej7tbt6fuh2ut/UK-Inventory.xlsx?rlkey=5h8e4st885bggibujk0b1lmm8&st=7vbagnwz&dl=1"; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + return res.status(200).end(); + } + + try { + console.log('[fetch-uk-inventory] Fetching UK Inventory from Dropbox...'); + const response = await fetch(UK_INVENTORY_DROPBOX_URL, { + cache: 'no-store', + headers: { + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache' + } + }); + + if (!response.ok) { + throw new Error(`Dropbox responded with ${response.status}`); + } + + const buffer = await response.arrayBuffer(); + console.log('[fetch-uk-inventory] Successfully fetched UK Inventory Excel, size:', buffer.byteLength); + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.status(200).send(Buffer.from(buffer)); + } catch (error: any) { + console.error('[fetch-uk-inventory] Error:', error); + res.status(500).json({ error: error.message }); + } +} diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 544bfd4..5bb7ce6 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1985,6 +1985,89 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer): } }; +/** + * Process UK Inventory Excel file from Amazon Vendor Central. + * This file contains ONLY UK inventory, so all stock goes to the 'uk' property. + * It merges with an existing vendorStockMap to combine PANEU + UK data. + */ +export const processUKInventoryExcel = async ( + fileOrBuffer: File | ArrayBuffer, + existingMap?: Map +): Promise> => { + try { + const arrayBuffer = fileOrBuffer instanceof File + ? await fileOrBuffer.arrayBuffer() + : fileOrBuffer; + const workbook = XLSX.read(arrayBuffer, { type: 'array' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + + // Start with existing map or create new one + const vendorStockMap = existingMap || new Map(); + + // Find header row (it contains "ASIN") + let headerRowIndex = -1; + for (let i = 0; i < Math.min(jsonData.length, 20); i++) { + if (jsonData[i] && (jsonData[i].includes('ASIN') || jsonData[i].includes('asin'))) { + headerRowIndex = i; + break; + } + } + + if (headerRowIndex === -1) { + console.warn("[UK Inventory] Could not find header row"); + return vendorStockMap; + } + + const headers: any[] = jsonData[headerRowIndex]; + const asinIdx = headers.findIndex(h => String(h || '').toUpperCase() === 'ASIN'); + + // Find "Sellable On Hand Units" column (same logic as PANEU) + let stockIdx = headers.findIndex(h => String(h || '').toUpperCase() === 'SELLABLE ON HAND UNITS'); + + if (stockIdx === -1) { + stockIdx = headers.findIndex(h => { + const sh = String(h || '').toUpperCase(); + return sh.includes('SELLABLE') && sh.includes('UNITS') && + !sh.includes('UNSELLABLE') && !sh.includes('AGED'); + }); + } + + // Fallback to default index if not found + const finalAsinIdx = asinIdx !== -1 ? asinIdx : 0; + const finalStockIdx = stockIdx !== -1 ? stockIdx : 14; // UK file has it at index 14 + + console.log(`[UK Inventory] ASIN Column: "${headers[finalAsinIdx]}" (Index: ${finalAsinIdx})`); + console.log(`[UK Inventory] Stock Column: "${headers[finalStockIdx]}" (Index: ${finalStockIdx})`); + + let ukCount = 0; + for (let i = headerRowIndex + 1; i < jsonData.length; i++) { + const row = jsonData[i]; + if (!row || row.length <= Math.max(finalAsinIdx, finalStockIdx)) continue; + + const asin = String(row[finalAsinIdx] || '').trim().toUpperCase(); + if (!asin) continue; + + const stockValue = parseUnits(String(row[finalStockIdx] || '0')); + + if (!vendorStockMap.has(asin)) { + vendorStockMap.set(asin, { eu: 0, uk: 0 }); + } + + const current = vendorStockMap.get(asin)!; + current.uk += stockValue; + ukCount++; + } + + console.log(`[UK Inventory] Processed ${ukCount} UK stock entries`); + return vendorStockMap; + } catch (error) { + console.error("Error processing UK Inventory Excel:", error); + throw error; + } +}; + export const calculateVelocityMap = (data: SalesRecord[]): Map => { // 4-Week Average Sales Calculation const validYears = data.map(r => r.year).filter(y => y > 0); diff --git a/vite.config.ts b/vite.config.ts index 1d46ded..b14abec 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,6 +20,12 @@ export default defineConfig(({ mode }) => { changeOrigin: true, rewrite: () => '', followRedirects: true + }, + '/api/fetch-uk-inventory': { + target: 'https://www.dropbox.com/scl/fi/an1ldjmtej7tbt6fuh2ut/UK-Inventory.xlsx?rlkey=5h8e4st885bggibujk0b1lmm8&st=7vbagnwz&dl=1', + changeOrigin: true, + rewrite: () => '', + followRedirects: true } } },