diff --git a/api/dropbox-sync.js b/api/dropbox-sync.js index 5963244..bff3157 100644 --- a/api/dropbox-sync.js +++ b/api/dropbox-sync.js @@ -10,6 +10,86 @@ 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); @@ -27,37 +107,68 @@ export default async function handler(req, res) { return res.status(405).json({ error: 'Method not allowed' }); } - const { rows } = req.body; + const { rows, headers } = req.body; if (!rows || !Array.isArray(rows)) { return res.status(400).json({ error: 'Missing rows data' }); } const articleNoIdx = 0; - // Fetch IDs of ALL products that have manual edits so we don't overwrite them + // 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, status'); + .select('product_id, data, status'); - // Protect ALL products that are not 'excel' status (i.e. they have manual edits or special status) - const manuallyEditedIds = new Set( - (existingProducts || []) - .filter(p => p.status && p.status !== 'excel') - .map(p => p.product_id) - ); - - // Also protect any product that has history entries — they were edited at some point. - // If their status was accidentally reset to 'excel', this restores protection. + // Protect any product that has history entries const { data: historyProducts } = await supabase .from('products_history') .select('product_id'); - (historyProducts || []).forEach(h => manuallyEditedIds.add(h.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]); - // Only add if: new product OR existing but NOT manually edited - if (productId && productId.trim() !== '' && !manuallyEditedIds.has(productId)) { + 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, @@ -67,16 +178,20 @@ export default async function handler(req, res) { } } - console.log('Upserting', productsToUpsert.length, 'new/updated products to Supabase...'); + console.log('Upserting/updating', productsToUpsert.length, 'products in Supabase...'); - // We can now use a normal upsert because we filtered out the manual edits - const { error } = await supabase - .from('products') - .upsert(productsToUpsert, { onConflict: 'product_id' }); + // 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:', error); - return res.status(500).json({ error: error.message, detail: 'Failed to upsert products' }); + 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({ diff --git a/data (3).xlsx b/data (3).xlsx index 06f5dec..9f31dae 100644 Binary files a/data (3).xlsx and b/data (3).xlsx differ diff --git a/src/App.tsx b/src/App.tsx index d504cd4..5ecd87e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -295,7 +295,7 @@ export default function App() { const syncRes = await fetch('/api/dropbox-sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ rows, fileMeta }) + body: JSON.stringify({ rows, headers, fileMeta }) }); if (syncRes.ok) {