/** * Hierarchie Fähigkeiten-Katalog: Hauptgruppe → Kategorie → Fähigkeit. * Datenbasis: GET /api/skills/catalog (catalog_* Felder). */ export function bySortThenName(a, b) { const sa = a.sort_order != null ? Number(a.sort_order) : 99999 const sb = b.sort_order != null ? Number(b.sort_order) : 99999 if (sa !== sb) return sa - sb return String(a.name || a.label || '').localeCompare(String(b.name || b.label || ''), 'de') } /** @param {object} skill */ export function skillCatalogPathLabel(skill) { if (!skill || typeof skill !== 'object') return '' const parts = [] const main = (skill.catalog_main_category_name || '').trim() const cat = (skill.catalog_category_name || '').trim() if (main) parts.push(main) if (cat) parts.push(cat) parts.push((skill.name || '').trim() || `#${skill.id}`) return parts.join(' › ') } /** * @param {object[]} skills * @returns {Array<{ key: string, type: string, label: string, skillId?: number, skill?: object, children?: object[] }>} */ export function buildSkillCatalogTree(skills) { if (!Array.isArray(skills) || skills.length === 0) return [] /** @type {Map }>} */ const mainMap = new Map() for (const skill of skills) { const mainId = skill.main_category_id ?? null const mainKey = mainId != null ? `m-${mainId}` : 'm-none' const mainLabel = (skill.catalog_main_category_name || '').trim() || 'Ohne Hauptgruppe' const mainSort = Number(skill.catalog_main_sort ?? 99999) if (!mainMap.has(mainKey)) { mainMap.set(mainKey, { key: mainKey, type: 'main', label: mainLabel, sortOrder: mainSort, children: new Map(), }) } const mainNode = mainMap.get(mainKey) const catId = skill.category_id ?? null const catKey = catId != null ? `c-${catId}` : 'c-none' const catLabel = (skill.catalog_category_name || '').trim() || 'Ohne Kategorie' const catSort = Number(skill.catalog_category_sort ?? 99999) if (!mainNode.children.has(catKey)) { mainNode.children.set(catKey, { key: `${mainKey}-${catKey}`, type: 'category', label: catLabel, sortOrder: catSort, skills: [], }) } mainNode.children.get(catKey).skills.push(skill) } return [...mainMap.values()] .sort((a, b) => { if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder return a.label.localeCompare(b.label, 'de') }) .map((main) => ({ key: main.key, type: main.type, label: main.label, children: [...main.children.values()] .sort((a, b) => { if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder return a.label.localeCompare(b.label, 'de') }) .map((cat) => { const skillLeaves = [...cat.skills].sort(bySortThenName).map((skill) => ({ key: `s-${skill.id}`, type: 'skill', label: (skill.name || '').trim() || `#${skill.id}`, skillId: Number(skill.id), skill, })) const n = skillLeaves.length return { key: cat.key, type: cat.type, label: cat.label, children: n > 0 ? [ { key: `${cat.key}-skills`, type: 'skillGroup', label: n === 1 ? '1 Fähigkeit' : `${n} Fähigkeiten`, skillCount: n, children: skillLeaves, }, ] : [], } }), })) } /** * Alle wählbaren Blätter mit Pfad-Label (für Suche / Chips). * @param {ReturnType} tree * @param {Set|number[]} [excludeIds] */ export function collectSkillLeavesFromTree(tree, excludeIds) { const exclude = new Set( (excludeIds instanceof Set ? [...excludeIds] : excludeIds || []) .map((x) => Number(x)) .filter((n) => Number.isFinite(n)) ) /** @type {Array<{ id: number, label: string, pathLabel: string, skill: object }>} */ const out = [] const walk = (nodes, pathParts) => { for (const n of nodes) { if (n.type === 'skill' && n.skillId != null) { if (!exclude.has(n.skillId)) { const pathLabel = [...pathParts, n.label].join(' › ') out.push({ id: n.skillId, label: n.label, pathLabel, skill: n.skill }) } } else if (n.children?.length) { const nextPath = n.type === 'skillGroup' ? pathParts : [...pathParts, n.label] walk(n.children, nextPath) } } } walk(tree, []) return out } /** @param {ReturnType} tree */ export function filterSkillTreeByQuery(tree, query) { const q = String(query || '').trim().toLowerCase() if (!q) return tree const filterNodes = (nodes) => { /** @type {typeof nodes} */ const result = [] for (const n of nodes) { if (n.type === 'skill') { const hay = `${n.label} ${skillCatalogPathLabel(n.skill)}`.toLowerCase() if (hay.includes(q)) result.push(n) } else if (n.type === 'skillGroup') { const kids = filterNodes(n.children || []) if (kids.length) result.push({ ...n, children: kids }) } else { const kids = filterNodes(n.children || []) if (kids.length) result.push({ ...n, children: kids }) } } return result } return filterNodes(tree) } /** Alle aufklappbaren Knoten (inkl. Fähigkeiten-Gruppen unter Kategorien). */ export function allExpandableKeys(tree) { const keys = [] const walk = (nodes) => { for (const n of nodes) { if (n.type !== 'skill' && n.children?.length) { keys.push(n.key) walk(n.children) } } } walk(tree) return keys } /** Standard: Hauptgruppe + Kategorie offen, Fähigkeiten-Liste zugeklappt. */ export function defaultExpandedKeysForSkillTree(tree) { const keys = [] const walk = (nodes) => { for (const n of nodes) { if (n.type === 'main' || n.type === 'category') { keys.push(n.key) walk(n.children || []) } } } walk(tree) return keys }