import fs from 'fs'; import path from 'path'; import { getStore } from '../db/store.js'; import { createLogger } from '../util/logger.js'; const log = createLogger('import'); /** * Parses RFC 4180 CSV text into an array of objects keyed by header. * * @param {string} text - Raw CSV text * @returns {Array>} */ export function parseCsvRecords(text) { const clean = text.replace(/^\uFEFF/, ''); const rows = []; let field = ''; let row = []; let inQuotes = false; let i = 0; const len = clean.length; while (i < len) { const c = clean[i]; if (inQuotes) { if (c === '"') { if (i + 1 < len && clean[i + 1] === '"') { field += '"'; i += 2; continue; } else { inQuotes = false; i++; continue; } } else { field += c; i++; continue; } } else { if (c === '"') { inQuotes = true; i++; continue; } else if (c === ',') { row.push(field); field = ''; i++; continue; } else if (c === '\r') { if (i + 1 < len && clean[i + 1] === '\n') { i++; } row.push(field); rows.push(row); field = ''; row = []; i++; continue; } else if (c === '\n') { row.push(field); rows.push(row); field = ''; row = []; i++; continue; } else { field += c; i++; continue; } } } if (field !== '' || row.length > 0) { row.push(field); rows.push(row); } if (rows.length === 0) return []; const header = rows[0].map((h) => h.trim()); const records = []; for (let r = 1; r < rows.length; r++) { const curr = rows[r]; if (curr.length === 1 && curr[0] === '') continue; const obj = {}; for (let col = 0; col < header.length; col++) { obj[header[col]] = curr[col] ?? ''; } records.push(obj); } return records; } /** * Normalizes common variant attribute names and typos from JTL exports. * * @param {string} name * @returns {string} */ export function normalizeAttributeName(name) { const clean = (name || '').trim(); if (!clean) return 'Variante'; const lower = clean.toLowerCase(); if (lower === 'menge' || lower === 'mange' || lower === 'meneg') return 'Menge'; if (lower === 'farbe') return 'Farbe'; if (lower === 'größe' || lower === 'grosse') return 'Größe'; if (lower === 'sorte') return 'Sorte'; if (lower === 'geschmack') return 'Geschmack'; if (lower === 'typ') return 'Typ'; if (lower === 'stück' || lower === 'stueck') return 'Stück'; if (lower === 'ausführung' || lower === 'ausfuehrung') return 'Ausführung'; if (lower === 'motiv') return 'Motiv'; return clean.charAt(0).toUpperCase() + clean.slice(1); } /** * 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. * * @param {object} opts * @param {number} opts.googleAuthId * @param {number} opts.companyId * @param {string} opts.importDir * @param {function} [opts.onProgress] - (phase, current, total, item) => void * @returns {Promise} Summary outcome */ export async function executeCompanyImport({ googleAuthId, companyId, importDir, onProgress = () => {}, }) { const store = await getStore(); const catCsvPath = path.join(importDir, 'categories.csv'); const artCsvPath = path.join(importDir, 'articles.csv'); if (!fs.existsSync(catCsvPath) || !fs.existsSync(artCsvPath)) { throw new Error('Import files missing: categories.csv and articles.csv are required'); } const catRaw = fs.readFileSync(catCsvPath, 'utf8'); const catRecords = parseCsvRecords(catRaw); const artRaw = fs.readFileSync(artCsvPath, 'utf8'); const artRecords = parseCsvRecords(artRaw); console.log(`[Import] Read ${catRecords.length} category rows and ${artRecords.length} article rows`); // ─── Phase 1: Category Mapping & Creation ──────────────────────────────── // 1) Find existing category tree & company root category const treeResult = await store.category.getTreeForCompany(googleAuthId); const existingTree = treeResult.tree || []; const rootCategory = existingTree.find((c) => c.parentId == null) || existingTree[0]; if (!rootCategory) { throw new Error('Company root category not found'); } const rootCategoryId = rootCategory.id; // Helper map of existing categories by parentId and lowercase name const existingMap = new Map(); for (const c of existingTree) { const key = `${c.parentId ?? 'root'}:${String(c.name || '').trim().toLowerCase()}`; existingMap.set(key, c.id); } // Identify excluded categories (Papierkorb / Trash and its descendants) const excludedJtlIds = new Set(['72']); for (const c of catRecords) { let name = ''; try { const tr = JSON.parse(c.translations || '[]'); name = tr[0]?.cName || ''; } catch {} if (name.toLowerCase().includes('papierkorb') || name.toLowerCase().includes('trash')) { excludedJtlIds.add(String(c.kKategorie)); } } // Also exclude any children of excluded categories let addedExcluded = true; while (addedExcluded) { addedExcluded = false; for (const c of catRecords) { const id = String(c.kKategorie); const parent = String(c.kOberKategorie); if (!excludedJtlIds.has(id) && excludedJtlIds.has(parent)) { excludedJtlIds.add(id); addedExcluded = true; } } } console.log(`[Import] Excluded categories (trash/papierkorb):`, Array.from(excludedJtlIds)); // Filter included categories const includedCategories = catRecords.filter((c) => !excludedJtlIds.has(String(c.kKategorie))); // Sort categories so parents are created before children // JTL kOberKategorie === '0' are top-level const jtlCatIdToDashCatId = new Map(); const pendingCats = [...includedCategories]; let createdCatCount = 0; let catPicCount = 0; onProgress('categories', 0, includedCategories.length, 'Starting categories import'); let safety = 0; while (pendingCats.length > 0 && safety < 10) { safety++; const remaining = []; for (let i = 0; i < pendingCats.length; i++) { const c = pendingCats[i]; const jtlId = String(c.kKategorie); const jtlParent = String(c.kOberKategorie); let name = ''; try { const tr = JSON.parse(c.translations || '[]'); name = tr[0]?.cName || ''; } catch {} if (!name) name = `Kategorie ${jtlId}`; let dashParentId; if (jtlParent === '0' || !jtlParent) { dashParentId = rootCategoryId; } else if (jtlCatIdToDashCatId.has(jtlParent)) { dashParentId = jtlCatIdToDashCatId.get(jtlParent); } else { // Parent not created yet in this pass remaining.push(c); continue; } // Check if category already exists under dashParentId const key = `${dashParentId}:${name.trim().toLowerCase()}`; let dashCatId = existingMap.get(key); if (!dashCatId) { try { const created = await store.category.createUnderParent({ googleAuthId, parentId: dashParentId, name: name.trim(), }); dashCatId = created.id; existingMap.set(key, dashCatId); createdCatCount++; } catch (err) { console.warn(`[Import] Failed to create category "${name}":`, err.message); } } if (dashCatId) { jtlCatIdToDashCatId.set(jtlId, dashCatId); // Check for category picture const picRel = (c.pictureFiles || '').trim(); if (picRel) { const picPath = path.join(importDir, picRel); if (fs.existsSync(picPath)) { try { const imgBuf = fs.readFileSync(picPath); await store.category.addImage(googleAuthId, dashCatId, imgBuf); catPicCount++; } catch (err) { console.warn(`[Import] Category image error (${picRel}):`, err.message); } } } } onProgress('categories', jtlCatIdToDashCatId.size, includedCategories.length, name); } pendingCats.length = 0; pendingCats.push(...remaining); } console.log(`[Import] Categories resolved: ${jtlCatIdToDashCatId.size}/${includedCategories.length}, created new: ${createdCatCount}, images: ${catPicCount}`); // Fallback: Haupt category ID for unmapped items const hauptDashId = jtlCatIdToDashCatId.get('1') || rootCategoryId; const includedArticles = artRecords.filter((a) => { let cats = []; try { const rawCats = JSON.parse(a.categories || '[]'); cats = rawCats.map((it) => String(it.kKategorie)); } catch {} // If all assigned categories are excluded, exclude article if (cats.length > 0 && cats.every((cid) => excludedJtlIds.has(cid))) { return false; } return true; }); // ─── Phase 2: Brand (Hersteller) Import ────────────────────────────────── const brandMap = new Map(); // brandName -> Set of picture relative paths for (const a of includedArticles) { const hersteller = String(a.Hersteller || '').trim(); if (!hersteller) continue; if (!brandMap.has(hersteller)) { brandMap.set(hersteller, new Set()); } const rawPics = String(a.manufacturerPictureFiles || '').trim(); if (rawPics) { for (const p of rawPics.split(/[;,]/).map((s) => s.trim()).filter(Boolean)) { brandMap.get(hersteller).add(p); } } } const brandNameToId = new Map(); let createdBrandCount = 0; let brandPicCount = 0; const brandEntries = Array.from(brandMap.entries()); log.info(`Processing ${brandEntries.length} brands (Hersteller)...`); // Fetch existing brands to accurately track new vs existing const { brands: existingBrandRows } = await store.brand.list(googleAuthId, { offset: 0, limit: 100 }); const existingBrandNames = new Set( existingBrandRows.map((b) => (b.name || '').trim().toLowerCase()).filter(Boolean) ); for (let i = 0; i < brandEntries.length; i++) { const [brandName, picSet] = brandEntries[i]; const lowerName = brandName.trim().toLowerCase(); const isNew = !existingBrandNames.has(lowerName); try { const brand = await store.brand.upsert(googleAuthId, { name: brandName }); brandNameToId.set(lowerName, brand.id); if (isNew) { createdBrandCount++; existingBrandNames.add(lowerName); } // If brand doesn't have an image yet, import from manufacturerPictureFiles if ((!brand.pics || brand.pics.length === 0) && picSet.size > 0) { for (const picRel of picSet) { const picPath = path.resolve(importDir, picRel); if (fs.existsSync(picPath)) { try { const imgBuf = fs.readFileSync(picPath); await store.brand.addImage(googleAuthId, brand.id, imgBuf); brandPicCount++; break; // 1 primary logo per brand } catch (err) { log.warn(`Brand image error for "${brandName}" (${picRel}):`, err.message); } } } } } catch (err) { log.warn(`Failed to upsert brand "${brandName}":`, err.message); } onProgress('brands', i + 1, brandEntries.length, brandName); } log.info(`Brands resolved: ${brandNameToId.size}/${brandEntries.length}, brand images: ${brandPicCount}`); // ─── Phase 3: Article Import Pass 1 (Create or Update) ─────────────────── const totalArticles = includedArticles.length; log.info(`Articles to process: ${totalArticles}`); onProgress('articles', 0, totalArticles, 'Starting articles import'); let articlesImported = 0; let articlesUpdated = 0; const jtlArtIdToSku = new Map(); const articlesWithPictures = []; for (let i = 0; i < totalArticles; i++) { const a = includedArticles[i]; const jtlArtId = String(a.kArtikel); const sku = String(a.cArtNr || '').trim(); if (!sku) continue; jtlArtIdToSku.set(jtlArtId, sku); let name = ''; let desc = ''; let seo = ''; try { const descriptions = JSON.parse(a.descriptions || '[]'); const descItem = Array.isArray(descriptions) ? descriptions[0] : descriptions; if (descItem) { name = String(descItem.cName || '').trim(); desc = String(descItem.cBeschreibung || '').trim(); seo = String(descItem.cKurzBeschreibung || '').trim(); } } catch {} if (!name) name = `Artikel ${sku}`; // Category mapping const assignedDashCatIds = []; try { const rawCats = JSON.parse(a.categories || '[]'); for (const it of rawCats) { const jtlCId = String(it.kKategorie); if (jtlCatIdToDashCatId.has(jtlCId)) { assignedDashCatIds.push(jtlCatIdToDashCatId.get(jtlCId)); } } } catch {} const primaryCatId = assignedDashCatIds[0] || hauptDashId; // Stock tracking const trackStock = a.cLagerAktiv === 'Y' ? 1 : 0; // Price & Tax const netPrice = parseFloat(String(a.fVKNetto || '0').replace(',', '.')) || 0; // kSteuerklasse '3' is reduced tax (7%), '1' is standard tax (19%) const taxRate = a.kSteuerklasse === '3' ? 7 : 19; // Barcode / EAN const barcode = String(a.cBarcode || '').trim(); // Check if article already exists let existingArticle = null; try { const res = await store.article.getBySku(googleAuthId, sku); existingArticle = res.article; } catch { existingArticle = null; } try { if (existingArticle) { // Update existing article await store.article.updateBySku(googleAuthId, sku, { name: name.trim(), track_stock: trackStock, desc: desc || undefined, seo: seo || undefined, }); // Update prices await store.article.setPricesBySku(googleAuthId, sku, [ { net: netPrice, tax: taxRate }, ]); // Update category assignment if (assignedDashCatIds.length > 0) { await store.article.setCategoryIdsBySku(googleAuthId, sku, assignedDashCatIds); } // Update barcode if provided if (barcode) { try { const currentEans = existingArticle.eans || []; if (!currentEans.includes(barcode)) { await store.article.setEansBySku(googleAuthId, sku, [...currentEans, barcode]); } } catch (eanErr) { console.warn(`[Import] Could not set EAN for ${sku}:`, eanErr.message); } } // Update brand if provided const hersteller = String(a.Hersteller || '').trim(); const brandId = hersteller ? (brandNameToId.get(hersteller.toLowerCase()) || null) : null; if (brandId && existingArticle.brand?.id !== brandId) { try { await store.article.setBrandIdBySku(googleAuthId, sku, brandId); } catch (bErr) { log.warn(`Could not set brand for ${sku}:`, bErr.message); } } articlesUpdated++; } else { // Create new article await store.article.createUnderParent({ googleAuthId, parentId: primaryCatId, name: name.trim(), sku, track_stock: trackStock, desc: desc || undefined, seo: seo || undefined, }); // Set price await store.article.setPricesBySku(googleAuthId, sku, [ { net: netPrice, tax: taxRate }, ]); // Set additional categories if more than 1 if (assignedDashCatIds.length > 1) { await store.article.setCategoryIdsBySku(googleAuthId, sku, assignedDashCatIds); } // Set barcode if present if (barcode) { try { await store.article.setEansBySku(googleAuthId, sku, [barcode]); } catch (eanErr) { console.warn(`[Import] Could not set EAN for ${sku}:`, eanErr.message); } } // Set brand if provided const hersteller = String(a.Hersteller || '').trim(); const brandId = hersteller ? (brandNameToId.get(hersteller.toLowerCase()) || null) : null; if (brandId) { try { await store.article.setBrandIdBySku(googleAuthId, sku, brandId); } catch (bErr) { log.warn(`Could not set brand for ${sku}:`, bErr.message); } } articlesImported++; } } catch (err) { console.warn(`[Import] Failed to save article ${sku} ("${name}"):`, err.message); } // Keep note of picture references for image phase const rawPics = (a.pictureFiles || '').trim(); if (rawPics) { articlesWithPictures.push({ sku, pictureFiles: rawPics }); } onProgress('articles', i + 1, totalArticles, `${sku} - ${name}`); } console.log(`[Import] Article pass 1 complete. New: ${articlesImported}, Updated: ${articlesUpdated}`); // ─── Phase 3: Article Import Pass 2 (Variant Links) ────────────────────── const variantChildren = includedArticles.filter( (a) => a.kVaterArtikel && a.kVaterArtikel !== '0' && a.kVaterArtikel.trim() !== '' ); console.log(`[Import] Linking ${variantChildren.length} child variants to parents...`); onProgress('variants', 0, variantChildren.length, 'Linking variants'); // Fetch existing attribute names for this company const attributeMap = new Map(); // lowercase name -> attributeId try { const attrList = await store.attributename.list(googleAuthId); for (const attr of attrList) { if (attr.name) { attributeMap.set(attr.name.trim().toLowerCase(), attr.id); } } } catch (err) { log.warn('Could not list attributes:', err.message); } // Helper to resolve or create an attribute name const resolveAttributeId = async (rawName) => { const normalized = normalizeAttributeName(rawName); const key = normalized.toLowerCase(); if (attributeMap.has(key)) { return attributeMap.get(key); } try { const created = await store.attributename.create(googleAuthId, { name: normalized, exclusive: true, }); attributeMap.set(key, created.id); return created.id; } catch (err) { log.warn(`Could not create attribute "${normalized}":`, err.message); return null; } }; // Group children by parent SKU and preserve parent row reference 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 (!parentChildrenMap.has(parentSku)) { const parentRow = includedArticles.find((a) => String(a.kArtikel) === parentJtlId) || null; parentChildrenMap.set(parentSku, { parentRow, children: [] }); } parentChildrenMap.get(parentSku).children.push(child); } let variantsLinked = 0; let processedCount = 0; for (const [parentSku, { parentRow, 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; } // Determine variant attribute name from parent row or children variations let rawAttrName = null; try { const pVars = JSON.parse(parentRow?.variations || '[]'); rawAttrName = pVars[0]?.cEigenschaftName?.trim() || null; } catch {} if (!rawAttrName) { for (const child of children) { try { const cVars = JSON.parse(child.variations || '[]'); rawAttrName = cVars[0]?.cEigenschaftName?.trim() || null; if (rawAttrName) break; } catch {} } } const targetAttrId = await resolveAttributeId(rawAttrName || 'Variante'); // Ensure parent has is_parent = 1 and parentattributename_id set let parentAttrId = parentDetail?.parentAttributeNameId; if (!parentDetail?.isParent || !parentAttrId) { if (targetAttrId) { try { await store.article.toggleParent(googleAuthId, parentSku, { isParent: true, attributeNameId: targetAttrId, }); parentAttrId = targetAttrId; } catch (err) { log.warn(`[Import] Could not toggle parent for ${parentSku}:`, err.message); } } } 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 {} // Determine attribute value from child variations, falling back to name derivation let val = null; try { const cVars = JSON.parse(child.variations || '[]'); val = cVars[0]?.selectedValues?.[0]?.cEigenschaftWertName?.trim() || cVars[0]?.variationValues?.[0]?.cEigenschaftWertName?.trim() || null; } catch {} if (!val) { 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); } onProgress('variants', processedCount, variantChildren.length, `${childSku} -> ${parentSku}`); } } console.log(`[Import] Linked ${variantsLinked}/${variantChildren.length} variants`); // ─── Phase 4: Article Pictures Import ──────────────────────────────────── const imageTasks = []; for (const { sku, pictureFiles } of articlesWithPictures) { const lines = pictureFiles.replace(/\r/g, '\n').split('\n').map((l) => l.trim()).filter(Boolean); for (const relPath of lines) { imageTasks.push({ sku, relPath }); } } console.log(`[Import] Importing ${imageTasks.length} pictures for ${articlesWithPictures.length} articles...`); onProgress('images', 0, imageTasks.length, 'Starting article pictures import'); let totalImagesImported = 0; for (let i = 0; i < imageTasks.length; i++) { const { sku, relPath } = imageTasks[i]; const imgPath = path.join(importDir, relPath); if (fs.existsSync(imgPath)) { try { const imgBuf = fs.readFileSync(imgPath); await store.article.addImage(googleAuthId, sku, imgBuf); totalImagesImported++; } catch (err) { // If already attached or format issue, log and continue console.warn(`[Import] Image add failed for SKU ${sku} (${relPath}):`, err.message); } } onProgress('images', i + 1, imageTasks.length, `${sku} (${path.basename(relPath)})`); } console.log(`[Import] Completed article pictures: ${totalImagesImported} images processed`); // ─── Phase 5: Orders (Manifests) Import ────────────────────────────────── const manCsvPath = path.join(importDir, 'manifests.csv'); const itemsCsvPath = path.join(importDir, 'manifestitems.csv'); let ordersImported = 0; let ordersSkipped = 0; let totalOrders = 0; if (fs.existsSync(manCsvPath) && fs.existsSync(itemsCsvPath)) { const manRecords = parseCsvRecords(fs.readFileSync(manCsvPath, 'utf8')); const itemRecords = parseCsvRecords(fs.readFileSync(itemsCsvPath, 'utf8')); totalOrders = manRecords.length; console.log(`[Import] Manifests found: ${totalOrders} orders, ${itemRecords.length} items`); onProgress('orders', 0, totalOrders, 'Starting orders import'); // Group line items by kAuftrag const itemsByAuftrag = new Map(); for (const it of itemRecords) { const kAuftrag = String(it.kAuftrag || '').trim(); if (!kAuftrag) continue; if (!itemsByAuftrag.has(kAuftrag)) { itemsByAuftrag.set(kAuftrag, []); } itemsByAuftrag.get(kAuftrag).push(it); } // Pre-fetch existing orders to make import idempotent const existingOrders = await store.order.list(googleAuthId); const existingOrderNumbers = new Set( existingOrders.map((o) => (o.number || '').trim()).filter(Boolean) ); // Pre-fetch customer groups and existing entities to resolve customer let defaultCustomerGroupId = null; try { const groups = await store.customergroup.list(googleAuthId); defaultCustomerGroupId = groups[0]?.id || null; } catch {} const entityMap = new Map(); // lowercase name -> entityId try { const entities = await store.entity.list(googleAuthId); for (const ent of entities) { if (ent.name) { entityMap.set(ent.name.trim().toLowerCase(), ent.id); } } } catch {} const resolveEntityId = async (name, firstName) => { const cleanName = (name || '').trim(); if (!cleanName || !defaultCustomerGroupId) return null; const key = cleanName.toLowerCase(); if (entityMap.has(key)) return entityMap.get(key); try { const ent = await store.entity.create(googleAuthId, { customergroupId: defaultCustomerGroupId, name: cleanName, firstName: (firstName || '').trim() || undefined, }); entityMap.set(key, ent.id); return ent.id; } catch (err) { log.warn(`[Import] Could not create entity "${cleanName}":`, err.message); return null; } }; for (let i = 0; i < totalOrders; i++) { const m = manRecords[i]; const orderNumber = String(m.cAuftragsNr || '').trim(); if (!orderNumber) continue; if (existingOrderNumbers.has(orderNumber)) { ordersSkipped++; onProgress('orders', i + 1, totalOrders, `${orderNumber} (skipped)`); continue; } const rawItems = itemsByAuftrag.get(String(m.kAuftrag || '').trim()) || []; if (rawItems.length === 0) { ordersSkipped++; onProgress('orders', i + 1, totalOrders, `${orderNumber} (skipped)`); continue; } const lines = rawItems.map((it) => { const sku = (it.cArtNr || '').trim() || null; const name = (it.cName || it.cNameStandard || '').trim() || (sku ? `Artikel ${sku}` : 'Artikel'); const amount = parseFloat(String(it.fAnzahl || '1').replace(',', '.')) || 1; const net = parseFloat(String(it.fVKNetto || '0').replace(',', '.')) || 0; const taxrate = parseFloat(String(it.fMwSt || '19').replace(',', '.')) || 0; const rebate = parseFloat(String(it.fRabatt || '0').replace(',', '.')) || 0; const comment = (it.cHinweis || '').trim() || null; return { sku, name, amount, net, taxrate, rebate, comment, }; }); const custName = (m.cName || '').trim() || 'Laufkunde'; const custFirstName = (m.cVorname || '').trim(); const entityId = await resolveEntityId(custName, custFirstName); const dateStr = (m.dErstellt || '').trim(); const parsedDate = dateStr ? new Date(dateStr) : new Date(); const validDate = !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : new Date().toISOString(); try { await store.order.create(googleAuthId, { number: orderNumber, externalNumber: (m.cExterneAuftragsnummer || '').trim() || null, date: validDate, createdAt: validDate, entityId: entityId || undefined, lines, }); existingOrderNumbers.add(orderNumber); ordersImported++; } catch (err) { console.warn(`[Import] Failed to create order ${orderNumber}:`, err.message); } onProgress('orders', i + 1, totalOrders, orderNumber); } console.log(`[Import] Orders import complete: ${ordersImported} created, ${ordersSkipped} skipped`); } return { ok: true, message: 'Import erfolgreich abgeschlossen.', stats: { categories: jtlCatIdToDashCatId.size, categoriesCreated: createdCatCount, categoryPictures: catPicCount, brands: brandNameToId.size, brandsCreated: createdBrandCount, brandPictures: brandPicCount, articlesTotal: totalArticles, articlesImported, articlesUpdated, variantsLinked, articlePictures: totalImagesImported, ordersTotal: totalOrders, ordersImported, ordersSkipped, }, }; }