224 lines
8.1 KiB
JavaScript
224 lines
8.1 KiB
JavaScript
// 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<Record<string, string>>} params.includedCategories
|
|
* @param {Array<object>} params.categoryImageTasks
|
|
* @param {object} params.progressTracker
|
|
* @returns {Promise<{ jtlCatIdToDashCatId: Map<string, number>, 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
|