diff --git a/src/services/companyImport.js b/src/services/companyImport.js index cd9e4f9..acd8e97 100644 --- a/src/services/companyImport.js +++ b/src/services/companyImport.js @@ -91,6 +91,51 @@ export function parseCsvRecords(text) { return records; } +/** + * Extracts or derives a variant attribute value for a child article + * based on its name relative to its parent name, or SKU suffixes. + * + * @param {string} parentName + * @param {string} childName + * @param {string} childSku + * @param {string} parentSku + * @returns {string} + */ +export function extractVariantValue(parentName, childName, childSku, parentSku) { + let pName = (parentName || '').replace(/[©®™]/g, '').trim(); + let cName = (childName || '').replace(/[©®™]/g, '').trim(); + + if (pName && cName) { + // Child name starts with parent name + if (cName.toLowerCase().startsWith(pName.toLowerCase())) { + let val = cName.slice(pName.length).trim().replace(/^[-–—:/,\s]+/, '').trim(); + if (val) return val; + } + // Match common leading tokens + const pWords = pName.toLowerCase().split(/\s+/); + const cWords = cName.split(/\s+/); + if (cWords.length > 1 && pWords[0] === cWords[0].toLowerCase()) { + let matchCount = 0; + while (matchCount < pWords.length && matchCount < cWords.length && pWords[matchCount] === cWords[matchCount].toLowerCase()) { + matchCount++; + } + if (matchCount > 0) { + const remaining = cWords.slice(matchCount).join(' ').trim().replace(/^[-–—:/,\s]+/, '').trim(); + if (remaining) return remaining; + } + } + } + + // SKU suffix: "42001505-1" with parent "42001505" -> "1" + if (parentSku && childSku && childSku.startsWith(parentSku)) { + let suffix = childSku.slice(parentSku.length).trim().replace(/^[-_]+/, '').trim(); + if (suffix) return suffix; + } + + if (cName && cName !== pName) return cName; + return childSku || 'Standard'; +} + /** * Executes full catalog import from preserved files in importDir. * @@ -502,23 +547,108 @@ export async function executeCompanyImport({ console.log(`[Import] Linking ${variantChildren.length} child variants to parents...`); onProgress('variants', 0, variantChildren.length, 'Linking variants'); - let variantsLinked = 0; - for (let i = 0; i < variantChildren.length; i++) { - const child = variantChildren[i]; - const childSku = String(child.cArtNr || '').trim(); + // Ensure a default attribute (e.g. "Variante") exists for variant parents + let defaultVariantAttrId = null; + try { + const attrList = await store.attributename.list(googleAuthId); + const existingAttr = attrList.find( + (a) => (a.name || '').trim().toLowerCase() === 'variante' + ); + if (existingAttr) { + defaultVariantAttrId = existingAttr.id; + } else { + const createdAttr = await store.attributename.create(googleAuthId, { + name: 'Variante', + exclusive: true, + }); + defaultVariantAttrId = createdAttr.id; + } + } catch (err) { + log.warn('Could not ensure default "Variante" attribute:', err.message); + } + + // Group children by parent SKU + const parentChildrenMap = new Map(); + for (const child of variantChildren) { const parentJtlId = String(child.kVaterArtikel || '').trim(); const parentSku = jtlArtIdToSku.get(parentJtlId); + const childSku = String(child.cArtNr || '').trim(); + if (!parentSku || !childSku || parentSku === childSku) continue; - if (childSku && parentSku && childSku !== parentSku) { - try { - await store.article.setParentBySku(googleAuthId, childSku, parentSku); - variantsLinked++; - } catch (err) { - console.warn(`[Import] Failed to link child ${childSku} to parent ${parentSku}:`, err.message); + if (!parentChildrenMap.has(parentSku)) { + parentChildrenMap.set(parentSku, []); + } + parentChildrenMap.get(parentSku).push(child); + } + + let variantsLinked = 0; + let processedCount = 0; + + for (const [parentSku, children] of parentChildrenMap.entries()) { + let parentDetail = null; + try { + const res = await store.article.getBySku(googleAuthId, parentSku); + parentDetail = res.article; + } catch (err) { + log.warn(`[Import] Could not load parent ${parentSku}:`, err.message); + processedCount += children.length; + continue; + } + + // Ensure parent has is_parent = 1 and parentattributename_id set + let parentAttrId = parentDetail?.parentAttributeNameId; + if (!parentDetail?.isParent || !parentAttrId) { + if (defaultVariantAttrId) { + try { + await store.article.toggleParent(googleAuthId, parentSku, { + isParent: true, + attributeNameId: defaultVariantAttrId, + }); + parentAttrId = defaultVariantAttrId; + } catch (err) { + log.warn(`[Import] Could not toggle parent for ${parentSku}:`, err.message); + } } } - if (i % 20 === 0 || i === variantChildren.length - 1) { - onProgress('variants', i + 1, variantChildren.length, `${childSku} -> ${parentSku}`); + + const parentName = parentDetail?.name || ''; + const usedValues = new Set(); + + for (const child of children) { + processedCount++; + const childSku = String(child.cArtNr || '').trim(); + let childName = ''; + try { + const desc = JSON.parse(child.descriptions || '[]'); + childName = (Array.isArray(desc) ? desc[0]?.cName : desc?.cName) || ''; + } catch {} + + let val = extractVariantValue(parentName, childName, childSku, parentSku); + if (usedValues.has(val.toLowerCase())) { + val = `${val} (${childSku})`; + } + usedValues.add(val.toLowerCase()); + + try { + if (parentAttrId) { + await store.article.attachChild(googleAuthId, parentSku, { + sku: childSku, + value: val, + reparent: true, + }); + variantsLinked++; + } else { + // Fallback to setParentBySku if toggleParent failed + await store.article.setParentBySku(googleAuthId, childSku, parentSku); + variantsLinked++; + } + } catch (err) { + console.warn(`[Import] Failed to attach child ${childSku} to parent ${parentSku}:`, err.message); + } + + if (processedCount % 20 === 0 || processedCount === variantChildren.length) { + onProgress('variants', processedCount, variantChildren.length, `${childSku} -> ${parentSku}`); + } } } console.log(`[Import] Linked ${variantsLinked}/${variantChildren.length} variants`);