import { applyCors, isAllowedOrigin } from './_cors.js'; import { createClient } from '@supabase/supabase-js'; const SUPABASE_URL = process.env.SUPABASE_URL || 'https://hwithddwaapyhnfwcesj.supabase.co'; const SUPABASE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY || 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv'; const supabase = createClient(SUPABASE_URL, SUPABASE_KEY); function setCors(req, res) { applyCors(req, res, 'POST, OPTIONS'); } const COLUMN_PATTERNS = { CLASSIFICATION: ['classification'], LONG_DE: ['long', 'description', 'german'], LONG_EN: ['long', 'description', 'english'], SHORT_DE: ['short', 'description', 'german'], SHORT_EN: ['short', 'description', 'english'], DETAILS_DE: ['details', 'german'], DETAILS_EN: ['details', 'english'], INNER_L: ['inner', 'l'], INNER_W: ['inner', 'w'], INNER_H: ['inner', 'h'], OUTER_L: ['outer', 'l'], OUTER_W: ['outer', 'w'], OUTER_H: ['outer', 'h'], UNITS_OUTER: ['units', 'outer'], MOQ: ['moq'], VERIFIED_DIMS: ['verified', 'dims'], VALIDATED_CHECK: ['validated', 'check'], VALIDATED_NOTE: ['validated', 'note'], PRODUCT_TYPE: ['product', 'type'], ITEM_TO_LOGISTIC: ['item', 'logistic'], CPNP_NO: ['cpnp'], }; function resolveEditableColumns(headers) { if (!headers || !Array.isArray(headers)) { // Fallback default editable column indices return new Set([ 9, 10, 11, 28, 33, 43, 44, 45, 48, 49, 50, 64, 65, 66, 67, 100, 101, 102, 103, 104, 107, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 ]); } const h = headers.map(val => String(val || '').toLowerCase()); const resolved = {}; Object.entries(COLUMN_PATTERNS).forEach(([key, patterns]) => { const idx = h.findIndex(headerText => patterns.every(p => headerText.includes(p.toLowerCase())) ); if (idx >= 0) { resolved[key] = idx; } }); const editableColumns = new Set([ resolved.CLASSIFICATION, resolved.LONG_DE, resolved.LONG_EN, resolved.SHORT_DE, resolved.SHORT_EN, resolved.DETAILS_DE, resolved.DETAILS_EN, resolved.INNER_L, resolved.INNER_W, resolved.INNER_H, resolved.OUTER_L, resolved.OUTER_W, resolved.OUTER_H, resolved.UNITS_OUTER, resolved.MOQ, resolved.VERIFIED_DIMS, resolved.VALIDATED_CHECK, resolved.VALIDATED_NOTE, resolved.PRODUCT_TYPE, resolved.ITEM_TO_LOGISTIC, resolved.CPNP_NO ].filter(val => val !== undefined)); headers.forEach((header, i) => { const headerText = String(header || '').toLowerCase(); if (headerText.includes('srp') || headerText.includes('uvp') || headerText.includes('price')) { editableColumns.add(i); } }); return editableColumns; } export default async function handler(req, res) { setCors(req, res); if (req.method === 'OPTIONS') { return res.status(204).end(); } const origin = req.headers.origin; if (origin && !isAllowedOrigin(origin)) { return res.status(403).json({ error: 'Forbidden' }); } try { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const { rows, headers } = req.body; if (!rows || !Array.isArray(rows)) { return res.status(400).json({ error: 'Missing rows data' }); } const articleNoIdx = 0; // Fetch data and status of ALL products so we can merge non-editable columns for manually edited ones const { data: existingProducts } = await supabase .from('products') .select('product_id, data, status'); // Protect any product that has history entries const { data: historyProducts } = await supabase .from('products_history') .select('product_id'); const historyIds = new Set((historyProducts || []).map(h => h.product_id)); const manuallyEditedProducts = new Map(); (existingProducts || []).forEach(p => { const isProtected = (p.status && p.status !== 'excel') || historyIds.has(p.product_id); if (isProtected) { manuallyEditedProducts.set(p.product_id, p); } }); const editableColumns = resolveEditableColumns(headers); const productsToUpsert = []; for (const row of rows) { const productId = String(row[articleNoIdx]); if (!productId || productId.trim() === '') continue; const manuallyEdited = manuallyEditedProducts.get(productId); if (manuallyEdited) { const dbRowData = manuallyEdited.data; if (dbRowData && Array.isArray(dbRowData)) { const mergedData = [...dbRowData]; while (mergedData.length < row.length) { mergedData.push(null); } let hasChange = false; for (let i = 0; i < row.length; i++) { if (i >= 100) continue; // Protect virtual columns if (editableColumns.has(i)) continue; // Protect editable columns if (mergedData[i] !== row[i]) { mergedData[i] = row[i]; hasChange = true; } } if (hasChange) { productsToUpsert.push({ product_id: productId, data: mergedData, status: manuallyEdited.status || 'edited', updated_at: new Date().toISOString() }); } } } else { // Normal excel sync productsToUpsert.push({ product_id: productId, data: row, status: 'excel', updated_at: new Date().toISOString() }); } } console.log('Upserting/updating', productsToUpsert.length, 'products in Supabase...'); // Chunk upsert updates to avoid payload sizes or API limits const CHUNK_SIZE = 100; for (let i = 0; i < productsToUpsert.length; i += CHUNK_SIZE) { const chunk = productsToUpsert.slice(i, i + CHUNK_SIZE); const { error } = await supabase .from('products') .upsert(chunk, { onConflict: 'product_id' }); if (error) { console.error('Supabase upsert error in chunk:', error); return res.status(500).json({ error: error.message, detail: 'Failed to upsert products chunk' }); } } return res.json({ success: true, syncedCount: productsToUpsert.length }); } catch (err) { console.error('Handler error:', err); res.status(500).json({ error: err.message }); } }