// MARK: - Imports // #region Imports import fs from 'fs'; import { cleanValue } from './csvParser.js'; import { computeSkeleHash } from './imageHasher.js'; import { createLogger } from '../../util/logger.js'; // #endregion const log = createLogger('import'); // MARK: - Category Importer // #region Category Importer /** * Imports category tree and attaches category images. * * @param {object} params * @param {object} params.store - Store singleton * @param {number} params.googleAuthId * @param {Array>} params.includedCategories * @param {Array} params.categoryImageTasks * @param {object} params.progressTracker * @returns {Promise<{ jtlCatIdToDashCatId: Map, hauptDashId: number, createdCatCount: number, catPicCount: number }>} */ export async function importCategories({ store, googleAuthId, includedCategories, categoryImageTasks, progressTracker, }) { 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; const existingMap = new Map(); for (const c of existingTree) { const key = `${c.parentId ?? 'root'}:${String(c.name || '').trim().toLowerCase()}`; existingMap.set(key, c.id); } const jtlCatIdToDashCatId = new Map(); jtlCatIdToDashCatId.set('0', rootCategoryId); // Group category images by JTL category ID const catImagesByJtlId = new Map(); for (const task of categoryImageTasks) { if (!catImagesByJtlId.has(task.catJtlId)) { catImagesByJtlId.set(task.catJtlId, []); } catImagesByJtlId.get(task.catJtlId).push(task); } function parseSortValue(val) { if (val == null || val === '') return 0; const n = Number(val); return Number.isFinite(n) ? n : 0; } const pendingCats = includedCategories.map((c, idx) => ({ ...c, _csvIndex: idx })); let createdCatCount = 0; let catPicCount = 0; let safety = 0; const importedMetaByCatId = new Map(); const touchedParentIds = new Set(); while (pendingCats.length > 0 && safety < 10) { safety++; const readyCats = []; const remaining = []; for (const c of pendingCats) { const jtlParent = String(c.parentid ?? c.kOberKategorie ?? '0'); if (jtlParent === '0' || !jtlParent || jtlCatIdToDashCatId.has(jtlParent)) { readyCats.push(c); } else { remaining.push(c); } } // Group by parent and order siblings numerically by CSV sort column, maintaining CSV order on ties readyCats.sort((a, b) => { const parentA = String(a.parentid ?? a.kOberKategorie ?? '0'); const parentB = String(b.parentid ?? b.kOberKategorie ?? '0'); if (parentA !== parentB) { return parentA.localeCompare(parentB); } const sortA = parseSortValue(a.sort ?? a.nSort); const sortB = parseSortValue(b.sort ?? b.nSort); if (sortA !== sortB) return sortA - sortB; return a._csvIndex - b._csvIndex; }); for (const c of readyCats) { const jtlId = String(c.id ?? c.kKategorie); const jtlParent = String(c.parentid ?? c.kOberKategorie ?? '0'); let name = cleanValue(c.name ?? c.cName); if (!name) { try { const tr = JSON.parse(c.translations || '[]'); name = tr[0]?.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 { remaining.push(c); continue; } const key = `${dashParentId}:${name.toLowerCase()}`; let dashCatId = existingMap.get(key); if (!dashCatId) { try { const created = await store.category.createUnderParent({ googleAuthId, parentId: dashParentId, name, }); dashCatId = created.id; existingMap.set(key, dashCatId); createdCatCount++; } catch (err) { log.warn(`Failed to create category "${name}":`, err.message); } } if (dashCatId) { jtlCatIdToDashCatId.set(jtlId, dashCatId); importedMetaByCatId.set(dashCatId, { sort: parseSortValue(c.sort ?? c.nSort), csvIndex: c._csvIndex, }); touchedParentIds.add(dashParentId); // Add category images const imgs = catImagesByJtlId.get(jtlId) || []; for (const imgTask of imgs) { try { const imgBuf = fs.readFileSync(imgTask.fullPath); await store.category.addImage(googleAuthId, dashCatId, imgBuf); catPicCount++; const skele = await computeSkeleHash(imgBuf); progressTracker.emitSkeleHash(skele?.base62 || null); } catch (err) { log.warn(`Category image error (${imgTask.relPath}):`, err.message); } progressTracker.counts.imagesdone++; } } progressTracker.counts.categoriesdone++; progressTracker.emitProgress(); } pendingCats.length = 0; pendingCats.push(...remaining); } // Explicit sibling reordering pass using store.category.order if (touchedParentIds.size > 0 && typeof store.category?.order === 'function') { try { const treeAfter = await store.category.getTreeForCompany(googleAuthId); const allNodes = treeAfter.tree || []; for (const parentId of touchedParentIds) { const siblings = allNodes.filter((node) => node.parentId === parentId); if (siblings.length <= 1) continue; siblings.sort((a, b) => { const metaA = importedMetaByCatId.get(a.id); const metaB = importedMetaByCatId.get(b.id); const sortA = metaA ? metaA.sort : Infinity; const sortB = metaB ? metaB.sort : Infinity; if (sortA !== sortB) return sortA - sortB; const idxA = metaA ? metaA.csvIndex : (a.sort ?? 0); const idxB = metaB ? metaB.csvIndex : (b.sort ?? 0); return idxA - idxB; }); const categoryIds = siblings.map((s) => s.id); try { await store.category.order({ googleAuthId, parentId, categoryIds, }); } catch (orderErr) { log.warn(`Failed to apply category order for parent ${parentId}:`, orderErr.message); } } } catch (err) { log.warn('Failed to fetch tree for category reordering:', err.message); } } progressTracker.flushSkeleHashes(); progressTracker.emitProgress(true); log.info(`Categories resolved: ${jtlCatIdToDashCatId.size}/${includedCategories.length}, created: ${createdCatCount}, images: ${catPicCount}`); const hauptDashId = jtlCatIdToDashCatId.get('1') || rootCategoryId; return { jtlCatIdToDashCatId, hauptDashId, createdCatCount, catPicCount, }; } // #endregion