From 78ad8f12e49ef72276b4f132a74a63d1a10671f6 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 16 Oct 2026 00:59:31 +0200 Subject: [PATCH] u --- src/api/routes/company.js | 6 +- src/services/companyImport.js | 53 +-- src/services/companyImport/articleImporter.js | 207 +++++++++++- src/services/companyImport/pfandImporter.js | 304 +++++++++++------- src/services/companyImport/progressTracker.js | 26 +- src/services/companyImport/variantImporter.js | 48 ++- 6 files changed, 471 insertions(+), 173 deletions(-) diff --git a/src/api/routes/company.js b/src/api/routes/company.js index e4e58bd..483e04a 100644 --- a/src/api/routes/company.js +++ b/src/api/routes/company.js @@ -548,8 +548,12 @@ export function register(app, basePath = '') { !req.headers.accept?.includes('application/json'); if (wantsSse) { + req.socket?.setTimeout(0); + res.setTimeout?.(0); + req.socket?.setNoDelay(true); + res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); if (typeof res.flushHeaders === 'function') { diff --git a/src/services/companyImport.js b/src/services/companyImport.js index 6511a30..19fd1d5 100644 --- a/src/services/companyImport.js +++ b/src/services/companyImport.js @@ -47,8 +47,9 @@ const log = createLogger('import'); * @param {number} opts.googleAuthId * @param {number} opts.companyId * @param {string} opts.importDir - * @param {function} [opts.onProgress] - (counts) => void; throttled to 200ms + * @param {function} [opts.onProgress] - (counts) => void; throttled to 50ms * @param {function} [opts.onImages] - (skelehashes: string[]) => void; batched every 50ms + * @param {number} [opts.progressThrottleMs=50] - Progress emission throttle/debounce interval in ms * @returns {Promise} Summary outcome with statistics */ export async function executeCompanyImport({ @@ -57,6 +58,7 @@ export async function executeCompanyImport({ importDir, onProgress = () => {}, onImages = () => {}, + progressThrottleMs = 50, }) { const store = await getStore(); @@ -105,6 +107,7 @@ export async function executeCompanyImport({ }, onProgress, onImages, + progressThrottleMs, }); // Emit baseline counts immediately @@ -133,12 +136,13 @@ export async function executeCompanyImport({ }); // #endregion - // MARK: - Phase 3: Articles + // MARK: - Phase 3: Articles (base data, variants, pfand, pictures) // #region Phase 3: Articles const articlesResult = await importArticles({ store, googleAuthId, includedArticles, + articleImageTasks, jtlCatIdToDashCatId: categoriesResult.jtlCatIdToDashCatId, hauptDashId: categoriesResult.hauptDashId, brandNameToId: brandsResult.brandNameToId, @@ -146,39 +150,8 @@ export async function executeCompanyImport({ }); // #endregion - // MARK: - Phase 4: Variants - // #region Phase 4: Variants - const variantsResult = await importVariants({ - store, - googleAuthId, - includedArticles, - jtlArtIdToSku: articlesResult.jtlArtIdToSku, - }); - // #endregion - - // MARK: - Phase 5: Pfand / Deposit Options - // #region Phase 5: Pfand - const pfandResult = await importPfand({ - store, - googleAuthId, - includedArticles, - hauptDashId: categoriesResult.hauptDashId, - parentSkus: variantsResult.parentSkus, - }); - // #endregion - - // MARK: - Phase 6: Article Pictures - // #region Phase 6: Article Pictures - const imagesResult = await importArticleImages({ - store, - googleAuthId, - articleImageTasks, - progressTracker, - }); - // #endregion - - // MARK: - Phase 7: Orders / Receipts - // #region Phase 7: Orders / Receipts + // MARK: - Phase 4: Orders / Receipts + // #region Phase 4: Orders / Receipts const allArtIdToSku = new Map(); for (const a of artRecords) { const kArt = cleanValue(a.id ?? a.kArtikel); @@ -213,11 +186,11 @@ export async function executeCompanyImport({ articlesTotal: includedArticles.length, articlesImported: articlesResult.articlesImported, articlesUpdated: articlesResult.articlesUpdated, - variantsLinked: variantsResult.variantsLinked, - pfandArticlesCreated: pfandResult.pfandArticlesCreated, - pfandArticlesUpdated: pfandResult.pfandArticlesUpdated, - pfandOptionsLinked: pfandResult.pfandOptionsLinked, - articlePictures: imagesResult.totalImagesImported, + variantsLinked: articlesResult.variantsLinked, + pfandArticlesCreated: articlesResult.pfandArticlesCreated, + pfandArticlesUpdated: articlesResult.pfandArticlesUpdated, + pfandOptionsLinked: articlesResult.pfandOptionsLinked, + articlePictures: articlesResult.totalImagesImported, ordersTotal: totalReceiptsCount, ordersImported: ordersResult.ordersImported, ordersSkipped: ordersResult.ordersSkipped, diff --git a/src/services/companyImport/articleImporter.js b/src/services/companyImport/articleImporter.js index 1bde335..3674977 100644 --- a/src/services/companyImport/articleImporter.js +++ b/src/services/companyImport/articleImporter.js @@ -1,6 +1,10 @@ // MARK: - Imports // #region Imports +import fs from 'fs'; import { cleanValue, parseGermanFloat } from './csvParser.js'; +import { setupVariantAttributes } from './variantImporter.js'; +import { setupPfandArticles, linkArticlePfand, extractPfandValue } from './pfandImporter.js'; +import { computeSkeleHash } from './imageHasher.js'; import { createLogger } from '../../util/logger.js'; // #endregion @@ -10,32 +14,120 @@ const log = createLogger('import'); // #region Article Importer /** - * Imports articles (insert or update), setting prices, categories, barcodes, and brands. + * Imports articles (insert or update), setting prices, categories, barcodes, brands, + * variants, pfand deposit options, and pictures in a single unified loop. * * @param {object} params * @param {object} params.store - Store singleton * @param {number} params.googleAuthId * @param {Array>} params.includedArticles + * @param {Array} [params.articleImageTasks=[]] * @param {Map} params.jtlCatIdToDashCatId * @param {number} params.hauptDashId * @param {Map} params.brandNameToId * @param {object} params.progressTracker - * @returns {Promise<{ jtlArtIdToSku: Map, articlesImported: number, articlesUpdated: number }>} + * @returns {Promise<{ + * jtlArtIdToSku: Map, + * articlesImported: number, + * articlesUpdated: number, + * variantsLinked: number, + * parentsCount: number, + * parentSkus: Set, + * pfandArticlesCreated: number, + * pfandArticlesUpdated: number, + * pfandOptionsLinked: number, + * totalImagesImported: number + * }>} */ export async function importArticles({ store, googleAuthId, includedArticles, + articleImageTasks = [], jtlCatIdToDashCatId, hauptDashId, brandNameToId, progressTracker, }) { + // 1. Group article image tasks by SKU + const articleImagesBySku = new Map(); + for (const task of articleImageTasks) { + if (!task.sku) continue; + if (!articleImagesBySku.has(task.sku)) { + articleImagesBySku.set(task.sku, []); + } + articleImagesBySku.get(task.sku).push(task); + } + + // 2. Setup Pfand category and Pfand articles upfront (e.g. PFAND-0.08, PFAND-0.15, etc.) + const pfandSetup = await setupPfandArticles({ + store, + googleAuthId, + hauptDashId, + includedArticles, + }); + + // 3. Setup variant attributes resolver + const variantSetup = await setupVariantAttributes({ store, googleAuthId }); + + // 4. Pre-scan articles to identify parents and map parent Pfand / parent rows + const parentJtlIds = new Set(); + const parentPfandMap = new Map(); + const parentRowMap = new Map(); const jtlArtIdToSku = new Map(); - let articlesImported = 0; - let articlesUpdated = 0; for (const a of includedArticles) { + const id = cleanValue(a.id ?? a.kArtikel); + const pid = cleanValue(a.parentid ?? a.kVaterArtikel); + const sku = cleanValue(a.sku ?? a.cArtNr); + const isParent = a.is_parent === '1' || a.nIstVater === '1'; + + if (id && sku) { + jtlArtIdToSku.set(id, sku); + } + if (pid && pid !== '0') { + parentJtlIds.add(pid); + } + if (isParent && id) { + parentJtlIds.add(id); + } + } + + // Identify parent rows and their Pfand values + for (const a of includedArticles) { + const id = cleanValue(a.id ?? a.kArtikel); + if (id && parentJtlIds.has(id)) { + parentRowMap.set(id, a); + const val = extractPfandValue(a.attributes); + if (val != null) { + parentPfandMap.set(id, val); + } + } + } + + // 5. Sort articles so parents come first, followed by children and standalone articles + const sortedArticles = [...includedArticles].sort((a, b) => { + const aId = cleanValue(a.id ?? a.kArtikel); + const bId = cleanValue(b.id ?? b.kArtikel); + const aIsParent = parentJtlIds.has(aId); + const bIsParent = parentJtlIds.has(bId); + if (aIsParent && !bIsParent) return -1; + if (!aIsParent && bIsParent) return 1; + return 0; + }); + + const parentSkus = new Set(); + const parentTargetAttrIds = new Map(); // parentSku -> targetAttrId + const parentUsedValues = new Map(); // parentSku -> Set + + let articlesImported = 0; + let articlesUpdated = 0; + let variantsLinked = 0; + let pfandOptionsLinked = 0; + let articlePicturesImported = 0; + + // 6. Unified single loop over all articles + for (const a of sortedArticles) { const jtlArtId = String(a.id ?? a.kArtikel); const sku = cleanValue(a.sku ?? a.cArtNr); if (!sku) { @@ -45,6 +137,12 @@ export async function importArticles({ jtlArtIdToSku.set(jtlArtId, sku); + const isParent = parentJtlIds.has(jtlArtId); + if (isParent) { + parentSkus.add(sku); + } + + // --- Basic article fields --- let name = cleanValue(a.name ?? a.cName); let desc = cleanValue(a.desc ?? a.descriptions); @@ -174,17 +272,116 @@ export async function importArticles({ log.warn(`Failed to save article ${sku} ("${name}"):`, err.message); } + // --- Variants handling --- + if (isParent) { + const rawAttrName = cleanValue(a.variation ?? a.kEigenschaftKombi); + const targetAttrId = await variantSetup.resolveAttributeId(rawAttrName || 'Variante'); + if (targetAttrId) { + parentTargetAttrIds.set(sku, targetAttrId); + try { + await store.article.toggleParent(googleAuthId, sku, { + isParent: true, + attributeNameId: targetAttrId, + }); + } catch (err) { + log.warn(`Could not toggle parent for ${sku}:`, err.message); + } + } + } else { + const parentJtlId = cleanValue(a.parentid ?? a.kVaterArtikel); + if (parentJtlId && parentJtlId !== '0') { + const parentSku = jtlArtIdToSku.get(parentJtlId); + if (parentSku && parentSku !== sku) { + const targetAttrId = parentTargetAttrIds.get(parentSku); + if (!parentUsedValues.has(parentSku)) { + parentUsedValues.set(parentSku, new Set()); + } + const usedSet = parentUsedValues.get(parentSku); + + let val = cleanValue(a.variation ?? a.kEigenschaftKombi) || 'Standard'; + if (usedSet.has(val.toLowerCase())) { + val = `${val} (${sku})`; + } + usedSet.add(val.toLowerCase()); + + try { + if (targetAttrId) { + await store.article.attachChild(googleAuthId, parentSku, { + sku, + value: val, + reparent: true, + }); + variantsLinked++; + } else { + await store.article.setParentBySku(googleAuthId, sku, parentSku); + variantsLinked++; + } + } catch (err) { + log.warn(`Failed to attach child ${sku} to parent ${parentSku}:`, err.message); + } + } + } + } + + // --- Pfand handling (only for non-parents) --- + if (!isParent) { + const parentJtlId = cleanValue(a.parentid ?? a.kVaterArtikel); + let pfandVal = extractPfandValue(a.attributes); + if (pfandVal == null && parentJtlId && parentJtlId !== '0') { + if (parentPfandMap.has(parentJtlId)) { + pfandVal = parentPfandMap.get(parentJtlId); + } + } + + if (pfandVal != null) { + const linked = await linkArticlePfand({ + store, + googleAuthId, + sku, + pfandVal, + }); + if (linked) pfandOptionsLinked++; + } + } + + // --- Pictures handling for this article --- + const imagesForThisArt = articleImagesBySku.get(sku); + if (imagesForThisArt && imagesForThisArt.length > 0) { + for (const imgTask of imagesForThisArt) { + try { + const imgBuf = fs.readFileSync(imgTask.fullPath); + await store.article.addImage(googleAuthId, sku, imgBuf); + articlePicturesImported++; + + const skele = await computeSkeleHash(imgBuf); + progressTracker.emitSkeleHash(skele?.base62 || null); + } catch (imgErr) { + log.warn(`Article image add failed for ${sku} (${imgTask.relPath}):`, imgErr.message); + } + progressTracker.counts.imagesdone++; + progressTracker.emitProgress(); + } + } + progressTracker.counts.articlesdone++; progressTracker.emitProgress(); } + progressTracker.flushSkeleHashes(); progressTracker.emitProgress(true); - log.info(`Articles complete: New: ${articlesImported}, Updated: ${articlesUpdated}`); + log.info(`Articles complete: New: ${articlesImported}, Updated: ${articlesUpdated}, Variants: ${variantsLinked}, Pfand: ${pfandOptionsLinked}, Pictures: ${articlePicturesImported}`); return { jtlArtIdToSku, articlesImported, articlesUpdated, + variantsLinked, + parentsCount: parentSkus.size, + parentSkus, + pfandArticlesCreated: pfandSetup.pfandArticlesCreated, + pfandArticlesUpdated: pfandSetup.pfandArticlesUpdated, + pfandOptionsLinked, + totalImagesImported: articlePicturesImported, }; } diff --git a/src/services/companyImport/pfandImporter.js b/src/services/companyImport/pfandImporter.js index 44ed566..4621414 100644 --- a/src/services/companyImport/pfandImporter.js +++ b/src/services/companyImport/pfandImporter.js @@ -48,6 +48,180 @@ export function formatPfandName(val) { // #endregion +// MARK: - Pfand Setup & Linking Helpers +// #region Pfand Setup & Linking Helpers + +/** + * Pre-creates the Pfand category and Pfand articles upfront. + * + * @param {object} params + * @param {object} params.store - Store singleton + * @param {number} params.googleAuthId + * @param {number} params.hauptDashId + * @param {Array>} params.includedArticles + * @returns {Promise<{ pfandArticlesCreated: number, pfandArticlesUpdated: number, pfandCatId: number|null, uniqueValues: number[] }>} + */ +export async function setupPfandArticles({ + store, + googleAuthId, + hauptDashId, + includedArticles, +}) { + // 1. Discover unique Pfand values from all included articles (checking attributes) + const uniqueValuesSet = new Set(); + for (const a of includedArticles) { + const val = extractPfandValue(a.attributes); + if (val != null) { + uniqueValuesSet.add(val); + } + } + + if (uniqueValuesSet.size === 0) { + log.info('No pfand attributes detected among articles.'); + return { + pfandArticlesCreated: 0, + pfandArticlesUpdated: 0, + pfandCatId: null, + uniqueValues: [], + }; + } + + const uniqueValues = Array.from(uniqueValuesSet).sort((a, b) => a - b); + log.info(`Discovered ${uniqueValues.length} unique Pfand values: ${uniqueValues.join(', ')}`); + + // 2. Resolve or create "Pfand" subcategory under "Haupt" + let pfandCatId = null; + try { + const treeResult = await store.category.getTreeForCompany(googleAuthId); + const existingTree = treeResult.tree || []; + const existingPfandCat = existingTree.find( + (c) => c.parentId === hauptDashId && String(c.name || '').trim().toLowerCase() === 'pfand' + ); + + if (existingPfandCat) { + pfandCatId = existingPfandCat.id; + } else { + const created = await store.category.createUnderParent({ + googleAuthId, + parentId: hauptDashId, + name: 'Pfand', + }); + pfandCatId = created.id; + log.info(`Created "Pfand" category under Haupt (id: ${pfandCatId})`); + } + } catch (err) { + log.warn('Could not resolve or create Pfand category under Haupt, falling back to Haupt:', err.message); + pfandCatId = hauptDashId; + } + + // 3. Create or update Pfand articles + let pfandArticlesCreated = 0; + let pfandArticlesUpdated = 0; + + for (const val of uniqueValues) { + const pfandSku = formatPfandSku(val); + const pfandName = formatPfandName(val); + + let existingArticle = null; + try { + const res = await store.article.getBySku(googleAuthId, pfandSku); + existingArticle = res.article; + } catch { + existingArticle = null; + } + + try { + if (existingArticle) { + await store.article.updateBySku(googleAuthId, pfandSku, { + name: pfandName, + track_stock: 0, + }); + await store.article.setPricesBySku(googleAuthId, pfandSku, [ + { gross: val, tax: 19 }, + ]); + pfandArticlesUpdated++; + log.info(`Updated existing Pfand article: ${pfandSku} ("${pfandName}")`); + } else { + await store.article.createUnderParent({ + googleAuthId, + parentId: pfandCatId, + name: pfandName, + sku: pfandSku, + track_stock: 0, + }); + await store.article.setPricesBySku(googleAuthId, pfandSku, [ + { gross: val, tax: 19 }, + ]); + pfandArticlesCreated++; + log.info(`Created new Pfand article: ${pfandSku} ("${pfandName}")`); + } + } catch (err) { + log.warn(`Failed to create or update Pfand article ${pfandSku}:`, err.message); + } + } + + return { + pfandArticlesCreated, + pfandArticlesUpdated, + pfandCatId, + uniqueValues, + }; +} + +/** + * Links an article to its mandatory Pfand deposit option. + * + * @param {object} params + * @param {object} params.store + * @param {number} params.googleAuthId + * @param {string} params.sku + * @param {number} params.pfandVal + * @returns {Promise} + */ +export async function linkArticlePfand({ + store, + googleAuthId, + sku, + pfandVal, +}) { + if (!sku || pfandVal == null) return false; + const pfandSku = formatPfandSku(pfandVal); + + let existingOptions = []; + try { + existingOptions = (await store.article.getOptionslist(googleAuthId, sku)) || []; + } catch { + existingOptions = []; + } + + const otherOptions = existingOptions.filter((o) => !o.sku?.startsWith('PFAND-')); + const newOptions = [ + ...otherOptions.map((o, idx) => ({ + sku: o.sku, + articleId: o.articleId, + count: o.count || 1, + sort: idx, + mandated: Boolean(o.mandated), + })), + { + sku: pfandSku, + count: 1, + sort: otherOptions.length, + mandated: true, + }, + ]; + + try { + await store.article.setOptionslist(googleAuthId, sku, newOptions); + return true; + } catch (err) { + log.warn(`Failed to link Pfand option ${pfandSku} to article ${sku}:`, err.message); + return false; + } +} + +// #endregion + // MARK: - Pfand Importer // #region Pfand Importer @@ -116,125 +290,31 @@ export async function importPfand({ }; } - // 3. Find unique Pfand values - const uniqueValues = Array.from(new Set(articleTargets.map((t) => t.pfandVal))).sort((a, b) => a - b); - log.info(`Discovered ${articleTargets.length} articles with Pfand across ${uniqueValues.length} unique values: ${uniqueValues.join(', ')}`); + // 3. Setup Pfand category and Pfand articles + const setupResult = await setupPfandArticles({ + store, + googleAuthId, + hauptDashId, + includedArticles, + }); - // 4. Resolve or create "Pfand" subcategory under "Haupt" - let pfandCatId = null; - try { - const treeResult = await store.category.getTreeForCompany(googleAuthId); - const existingTree = treeResult.tree || []; - const existingPfandCat = existingTree.find( - (c) => c.parentId === hauptDashId && String(c.name || '').trim().toLowerCase() === 'pfand' - ); - - if (existingPfandCat) { - pfandCatId = existingPfandCat.id; - } else { - const created = await store.category.createUnderParent({ - googleAuthId, - parentId: hauptDashId, - name: 'Pfand', - }); - pfandCatId = created.id; - log.info(`Created "Pfand" category under Haupt (id: ${pfandCatId})`); - } - } catch (err) { - log.warn('Could not resolve or create Pfand category under Haupt, falling back to Haupt:', err.message); - pfandCatId = hauptDashId; - } - - // 5. Create or update Pfand articles - let pfandArticlesCreated = 0; - let pfandArticlesUpdated = 0; - - for (const val of uniqueValues) { - const pfandSku = formatPfandSku(val); - const pfandName = formatPfandName(val); - - let existingArticle = null; - try { - const res = await store.article.getBySku(googleAuthId, pfandSku); - existingArticle = res.article; - } catch { - existingArticle = null; - } - - try { - if (existingArticle) { - await store.article.updateBySku(googleAuthId, pfandSku, { - name: pfandName, - track_stock: 0, - }); - await store.article.setPricesBySku(googleAuthId, pfandSku, [ - { gross: val, tax: 19 }, - ]); - pfandArticlesUpdated++; - log.info(`Updated existing Pfand article: ${pfandSku} ("${pfandName}")`); - } else { - await store.article.createUnderParent({ - googleAuthId, - parentId: pfandCatId, - name: pfandName, - sku: pfandSku, - track_stock: 0, - }); - await store.article.setPricesBySku(googleAuthId, pfandSku, [ - { gross: val, tax: 19 }, - ]); - pfandArticlesCreated++; - log.info(`Created new Pfand article: ${pfandSku} ("${pfandName}")`); - } - } catch (err) { - log.warn(`Failed to create or update Pfand article ${pfandSku}:`, err.message); - } - } - - // 6. Link each target article to its Pfand article as a mandatory option + // 4. Link each target article to its Pfand article as a mandatory option let pfandOptionsLinked = 0; - for (const { sku, pfandVal } of articleTargets) { - const pfandSku = formatPfandSku(pfandVal); - - let existingOptions = []; - try { - existingOptions = (await store.article.getOptionslist(googleAuthId, sku)) || []; - } catch { - existingOptions = []; - } - - // Filter out any existing PFAND options to prevent duplicates, keep any other options - const otherOptions = existingOptions.filter((o) => !o.sku?.startsWith('PFAND-')); - const newOptions = [ - ...otherOptions.map((o, idx) => ({ - sku: o.sku, - articleId: o.articleId, - count: o.count || 1, - sort: idx, - mandated: Boolean(o.mandated), - })), - { - sku: pfandSku, - count: 1, - sort: otherOptions.length, - mandated: true, - }, - ]; - - try { - await store.article.setOptionslist(googleAuthId, sku, newOptions); - pfandOptionsLinked++; - } catch (err) { - log.warn(`Failed to link Pfand option ${pfandSku} to article ${sku}:`, err.message); - } + const linked = await linkArticlePfand({ + store, + googleAuthId, + sku, + pfandVal, + }); + if (linked) pfandOptionsLinked++; } - log.info(`Pfand import complete: ${pfandArticlesCreated} created, ${pfandArticlesUpdated} updated, ${pfandOptionsLinked} linked as mandatory options.`); + log.info(`Pfand import complete: ${setupResult.pfandArticlesCreated} created, ${setupResult.pfandArticlesUpdated} updated, ${pfandOptionsLinked} linked as mandatory options.`); return { - pfandArticlesCreated, - pfandArticlesUpdated, + pfandArticlesCreated: setupResult.pfandArticlesCreated, + pfandArticlesUpdated: setupResult.pfandArticlesUpdated, pfandOptionsLinked, }; } diff --git a/src/services/companyImport/progressTracker.js b/src/services/companyImport/progressTracker.js index 1d665f5..ef6afe8 100644 --- a/src/services/companyImport/progressTracker.js +++ b/src/services/companyImport/progressTracker.js @@ -16,7 +16,7 @@ export function createProgressTracker({ initialCounts, onProgress = () => {}, onImages = () => {}, - progressThrottleMs = 200, + progressThrottleMs = 50, imageBatchMs = 50, }) { const counts = { @@ -32,11 +32,33 @@ export function createProgressTracker({ }; let lastProgressEmit = 0; + let progressTimer = null; + const emitProgress = (force = false) => { const now = Date.now(); - if (force || now - lastProgressEmit >= progressThrottleMs) { + if (force) { + if (progressTimer) { + clearTimeout(progressTimer); + progressTimer = null; + } lastProgressEmit = now; onProgress({ ...counts }); + return; + } + + if (now - lastProgressEmit >= progressThrottleMs) { + if (progressTimer) { + clearTimeout(progressTimer); + progressTimer = null; + } + lastProgressEmit = now; + onProgress({ ...counts }); + } else if (!progressTimer) { + progressTimer = setTimeout(() => { + progressTimer = null; + lastProgressEmit = Date.now(); + onProgress({ ...counts }); + }, progressThrottleMs); } }; diff --git a/src/services/companyImport/variantImporter.js b/src/services/companyImport/variantImporter.js index 8851aed..a916463 100644 --- a/src/services/companyImport/variantImporter.js +++ b/src/services/companyImport/variantImporter.js @@ -7,25 +7,18 @@ import { createLogger } from '../../util/logger.js'; const log = createLogger('import'); -// MARK: - Variant Importer -// #region Variant Importer +// MARK: - Variant Setup Helper +// #region Variant Setup Helper /** - * Resolves attribute definitions and links variant child articles to parents. + * Initializes attribute definitions and provides a cached resolver for attribute IDs. * * @param {object} params - * @param {object} params.store - Store singleton + * @param {object} params.store * @param {number} params.googleAuthId - * @param {Array>} params.includedArticles - * @param {Map} params.jtlArtIdToSku - * @returns {Promise<{ variantsLinked: number, parentsCount: number }>} + * @returns {Promise<{ attributeMap: Map, resolveAttributeId: (rawName: string) => Promise }>} */ -export async function importVariants({ - store, - googleAuthId, - includedArticles, - jtlArtIdToSku, -}) { +export async function setupVariantAttributes({ store, googleAuthId }) { const attributeMap = new Map(); try { const attrList = await store.attributename.list(googleAuthId); @@ -57,6 +50,35 @@ export async function importVariants({ } }; + return { + attributeMap, + resolveAttributeId, + }; +} + +// #endregion + +// MARK: - Variant Importer +// #region Variant Importer + +/** + * Resolves attribute definitions and links variant child articles to parents. + * + * @param {object} params + * @param {object} params.store - Store singleton + * @param {number} params.googleAuthId + * @param {Array>} params.includedArticles + * @param {Map} params.jtlArtIdToSku + * @returns {Promise<{ variantsLinked: number, parentsCount: number }>} + */ +export async function importVariants({ + store, + googleAuthId, + includedArticles, + jtlArtIdToSku, +}) { + const { resolveAttributeId } = await setupVariantAttributes({ store, googleAuthId }); + // Group children by parent SKU const parentChildrenMap = new Map(); for (const child of includedArticles) {