mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:05:23 +02:00
feat: Add UK inventory sync from Dropbox
- Create new API endpoint api/fetch-uk-inventory.ts - Add processUKInventoryExcel function in dataProcessor.ts - Update handleVendorStockFetch to load PANEU + UK in parallel - Configure Vite proxy for UK inventory endpoint - UK stock correctly populates 'uk' field in vendorStockMap
This commit is contained in:
@@ -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<string, { eu: number; uk: number }>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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<string, { eu: number; uk: number }>
|
||||
): Promise<Map<string, { eu: number; uk: number }>> => {
|
||||
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<string, { eu: number; uk: number }>();
|
||||
|
||||
// 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<string, number> => {
|
||||
// 4-Week Average Sales Calculation
|
||||
const validYears = data.map(r => r.year).filter(y => y > 0);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user