mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:55:23 +02:00
feat: Automate Buy Box tracker sync from Dropbox and improve parsing robustness
This commit is contained in:
@@ -222,8 +222,8 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
const handleBuyBoxFetch = useCallback(async () => {
|
const handleBuyBoxFetch = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
console.log('[App] Fetching Buy Box data from /Buy_Box_tracker.xlsx...');
|
console.log('[App] Fetching Buy Box data from /api/fetch-buybox...');
|
||||||
const response = await fetch('/Buy_Box_tracker.xlsx');
|
const response = await fetch('/api/fetch-buybox');
|
||||||
if (!response.ok) throw new Error(`Failed to fetch Buy Box data: ${response.status}`);
|
if (!response.ok) throw new Error(`Failed to fetch Buy Box data: ${response.status}`);
|
||||||
|
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
@@ -678,7 +678,7 @@ const App: React.FC = () => {
|
|||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
{/* Force Sync Action */}
|
{/* Force Sync Action */}
|
||||||
<button
|
<button
|
||||||
onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); }}
|
onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); handleBuyBoxFetch(); }}
|
||||||
disabled={syncing}
|
disabled={syncing}
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all
|
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all
|
||||||
${syncing
|
${syncing
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||||
|
|
||||||
|
const BUYBOX_DROPBOX_URL = "https://www.dropbox.com/scl/fi/dooz56abib51ifaf42cpx/Buy_Box_tracker.xlsx?rlkey=2sx85bne42fju3d8vekfygzau&st=t4n5fbcg&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-buybox] Fetching Buy Box tracker from Dropbox...');
|
||||||
|
const response = await fetch(BUYBOX_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-buybox] Successfully fetched Buy Box 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-buybox] Error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
-15
@@ -2142,13 +2142,12 @@ export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sheet configuration for Buy Box tracking
|
const BB_SHEET_CONFIG: { sheet: string; country: string }[] = [
|
||||||
const BB_SHEET_CONFIG: { sheet: string; country: string; reasonCol: number }[] = [
|
{ sheet: 'BB_FR', country: 'FR' },
|
||||||
{ sheet: 'BB_FR', country: 'FR', reasonCol: 18 }, // Col S = index 18
|
{ sheet: 'BB_UK', country: 'UK' },
|
||||||
{ sheet: 'BB_UK', country: 'UK', reasonCol: 9 }, // Col J = index 9
|
{ sheet: 'BB_DE', country: 'DE' },
|
||||||
{ sheet: 'BB_DE', country: 'DE', reasonCol: 13 }, // Col N = index 13
|
{ sheet: 'BB_IT', country: 'IT' },
|
||||||
{ sheet: 'BB_IT', country: 'IT', reasonCol: 12 }, // Col M = index 12
|
{ sheet: 'BB_ES', country: 'ES' },
|
||||||
{ sheet: 'BB_ES', country: 'ES', reasonCol: 13 }, // Col N = index 13
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const processBuyBoxExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, { countries: string[]; reasons: Record<string, string> }>> => {
|
export const processBuyBoxExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, { countries: string[]; reasons: Record<string, string> }>> => {
|
||||||
@@ -2161,32 +2160,70 @@ export const processBuyBoxExcel = async (fileOrBuffer: File | ArrayBuffer): Prom
|
|||||||
// Map: ASIN -> { countries: [], reasons: {} }
|
// Map: ASIN -> { countries: [], reasons: {} }
|
||||||
const buyBoxMap = new Map<string, { countries: string[]; reasons: Record<string, string> }>();
|
const buyBoxMap = new Map<string, { countries: string[]; reasons: Record<string, string> }>();
|
||||||
|
|
||||||
|
// Log available sheets for debugging
|
||||||
|
console.log('[BuyBox] Available sheets:', workbook.SheetNames.join(', '));
|
||||||
|
|
||||||
for (const config of BB_SHEET_CONFIG) {
|
for (const config of BB_SHEET_CONFIG) {
|
||||||
const worksheet = workbook.Sheets[config.sheet];
|
// Find sheet case-insensitively and with flexible separators (BB_ES, BB-ES, BB ES)
|
||||||
if (!worksheet) {
|
const sheetName = workbook.SheetNames.find(name => {
|
||||||
console.warn(`[BuyBox] Sheet ${config.sheet} not found, skipping...`);
|
const n = name.toUpperCase().replace(/[-_ ]/g, '');
|
||||||
|
const target = config.sheet.toUpperCase().replace(/[-_ ]/g, '');
|
||||||
|
return n === target;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sheetName) {
|
||||||
|
console.warn(`[BuyBox] Sheet matching ${config.sheet} not found, skipping...`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||||
|
if (jsonData.length === 0) continue;
|
||||||
|
|
||||||
const headers: any[] = jsonData[0] || [];
|
const headers: any[] = jsonData[0] || [];
|
||||||
const asinIdx = headers.findIndex(h => String(h || '').toUpperCase() === 'ASIN');
|
const asinIdx = headers.findIndex(h => {
|
||||||
|
const s = String(h || '').toUpperCase().trim();
|
||||||
|
return s === 'ASIN' || s.includes('AMAZON ASIN') || s.includes('IDENTIFIER') || s.includes('PRODUCT ID') || s.includes('SKU');
|
||||||
|
});
|
||||||
const finalAsinIdx = asinIdx !== -1 ? asinIdx : 1; // Default to Col B if not found
|
const finalAsinIdx = asinIdx !== -1 ? asinIdx : 1; // Default to Col B if not found
|
||||||
|
|
||||||
|
// Dynamically find the "Issue Type" or "Reason" column
|
||||||
|
let reasonIdx = headers.findIndex(h => {
|
||||||
|
const s = String(h || '').toUpperCase();
|
||||||
|
return s.includes('ISSUE TYPE') || s.includes('REASON') || s.includes('BUY BOX STATUS') || s.includes('LBB REASON') || s.includes('ESTADO BB') || s.includes('COMENTARIO');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback to previous hardcoded indices if header search fails
|
||||||
|
if (reasonIdx === -1) {
|
||||||
|
if (config.country === 'FR') reasonIdx = 18;
|
||||||
|
else if (config.country === 'UK') reasonIdx = 9;
|
||||||
|
else if (config.country === 'DE') reasonIdx = 13;
|
||||||
|
else if (config.country === 'IT') reasonIdx = 12;
|
||||||
|
else if (config.country === 'ES') reasonIdx = 13;
|
||||||
|
else reasonIdx = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BuyBox] Sheet "${sheetName}" (Country: ${config.country}): ASIN Col=${finalAsinIdx} ("${headers[finalAsinIdx]}"), Reason Col=${reasonIdx} ("${headers[reasonIdx] || 'N/A'}")`);
|
||||||
|
|
||||||
// Skip header row (index 0)
|
// Skip header row (index 0)
|
||||||
for (let i = 1; i < jsonData.length; i++) {
|
for (let i = 1; i < jsonData.length; i++) {
|
||||||
const row = jsonData[i];
|
const row = jsonData[i];
|
||||||
if (!row || row.length <= Math.max(finalAsinIdx, config.reasonCol)) continue;
|
if (!row || row.length <= Math.max(finalAsinIdx, reasonIdx)) continue;
|
||||||
|
|
||||||
const rawAsin = String(row[finalAsinIdx] || '').trim().toUpperCase();
|
const rawAsin = String(row[finalAsinIdx] || '').trim().toUpperCase();
|
||||||
if (!rawAsin || rawAsin.length < 5) continue;
|
if (!rawAsin || rawAsin.length < 5) continue;
|
||||||
|
|
||||||
const rawReason = String(row[config.reasonCol] || '').trim();
|
const rawReason = String(row[reasonIdx] || '').trim();
|
||||||
|
|
||||||
|
// Debug specific ASIN reported by user
|
||||||
|
if (rawAsin === 'B0D3874WSD' && config.country === 'ES') {
|
||||||
|
console.log(`[BuyBox Debug] ASIN B0D3874WSD found in ES. Raw Reason: "${rawReason}" (Length: ${rawReason.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
// Only consider as BB lost if there is an Issue Type / Reason specified
|
// Only consider as BB lost if there is an Issue Type / Reason specified
|
||||||
// Empty reason or "Fixed" status means no issue = they have the Buy Box
|
// Empty reason or "Fixed" status means no issue = they have the Buy Box
|
||||||
if (!rawReason || rawReason.toLowerCase() === 'fixed') continue;
|
const lowReason = rawReason.toLowerCase();
|
||||||
|
if (!rawReason || lowReason === 'fixed' || lowReason === 'ok' || lowReason === 'hecho' || lowReason === 'solucionado') continue;
|
||||||
|
|
||||||
let entry = buyBoxMap.get(rawAsin);
|
let entry = buyBoxMap.get(rawAsin);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
@@ -2203,7 +2240,7 @@ export const processBuyBoxExcel = async (fileOrBuffer: File | ArrayBuffer): Prom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[BuyBox] Processed ${buyBoxMap.size} ASINs with BB lost`);
|
console.log(`[BuyBox] Processed ${buyBoxMap.size} total ASINs with BB lost across all countries`);
|
||||||
return buyBoxMap;
|
return buyBoxMap;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing Buy Box Excel:", error);
|
console.error("Error processing Buy Box Excel:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user