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:
Christian Vidal Wolf
2026-02-02 09:24:49 +01:00
parent ab9ac4f47e
commit e1e6f127be
4 changed files with 159 additions and 9 deletions
+83
View File
@@ -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);