shinkan-jinkendo/frontend/src/utils/skillCatalogTree.js
Lars ab612a5335
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m14s
Update Skill Tree Picker and Catalog Functions for Clarity
- Modified comments in `SkillTreePickerPanel` and `skillCatalogTree.js` to accurately reflect the default expansion behavior, specifying that only main groups are open by default.
- Refactored `defaultExpandedKeysForSkillTree` to simplify the logic, ensuring only main groups are returned as expanded.
- Adjusted tests in `skillCatalogTree.test.js` to validate the updated behavior, confirming that only main groups are opened in the skill tree.
2026-05-20 11:21:18 +02:00

218 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, { key: string, type: string, label: string, sortOrder: number, children: Map<string, object> }>} */
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) => ({
key: cat.key,
type: cat.type,
label: cat.label,
children: [...cat.skills].sort(bySortThenName).map((skill) => ({
key: `s-${skill.id}`,
type: 'skill',
label: (skill.name || '').trim() || `#${skill.id}`,
skillId: Number(skill.id),
skill,
})),
})),
}))
}
/**
* Alle wählbaren Blätter mit Pfad-Label (für Suche / Chips).
* @param {ReturnType<typeof buildSkillCatalogTree>} tree
* @param {Set<number>|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) {
walk(n.children, [...pathParts, n.label])
}
}
}
walk(tree, [])
return out
}
/** @param {ReturnType<typeof buildSkillCatalogTree>} 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 {
const kids = filterNodes(n.children || [])
if (kids.length) result.push({ ...n, children: kids })
}
}
return result
}
return filterNodes(tree)
}
/** Alle aufklappbaren Knoten (Hauptgruppe und Kategorie). */
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: nur Hauptgruppe aufgeklappt; Kategorien und Fähigkeiten zugeklappt. */
export function defaultExpandedKeysForSkillTree(tree) {
if (!Array.isArray(tree)) return []
return tree.filter((n) => n.type === 'main').map((n) => n.key)
}
/** Sentinel für Katalog-Admin: keine Hauptgruppe gewählt. */
export const SKILL_MAIN_UNASSIGNED_KEY = '__unassigned_main__'
/** Sentinel für Katalog-Admin: keine Kategorie gewählt. */
export const SKILL_CATEGORY_UNASSIGNED_KEY = '__unassigned_category__'
export const SKILL_UNASSIGNED_MAIN_LABEL = 'Ohne Hauptgruppe'
export const SKILL_UNASSIGNED_CATEGORY_LABEL = 'Ohne Kategorie'
/**
* Fähigkeiten für die Admin-Katalog-Auswahl (Hauptgruppe + Kategorie-Slot).
* @param {object[]} skills
* @param {number|string|null} mainId
* @param {number|string|null} categoryId
*/
export function catalogSkillsForSelection(skills, mainId, categoryId) {
if (!Array.isArray(skills) || categoryId == null) return []
if (categoryId === SKILL_CATEGORY_UNASSIGNED_KEY) {
if (mainId === SKILL_MAIN_UNASSIGNED_KEY) {
return skills.filter((s) => s.main_category_id == null && s.category_id == null)
}
if (typeof mainId === 'number') {
return skills.filter((s) => s.main_category_id === mainId && s.category_id == null)
}
return []
}
if (typeof categoryId === 'number') {
return skills.filter((s) => s.category_id === categoryId)
}
return []
}
/** @param {object[]} skills */
export function countCatalogSkillsWithoutMain(skills) {
if (!Array.isArray(skills)) return 0
return skills.filter((s) => s.main_category_id == null).length
}
/** @param {object[]} skills @param {number|string|null} mainId */
export function countCatalogSkillsWithoutCategory(skills, mainId) {
if (!Array.isArray(skills)) return 0
if (mainId === SKILL_MAIN_UNASSIGNED_KEY) {
return skills.filter((s) => s.main_category_id == null && s.category_id == null).length
}
if (typeof mainId === 'number') {
return skills.filter((s) => s.main_category_id === mainId && s.category_id == null).length
}
return 0
}