shinkan-jinkendo/frontend/src/utils/skillCatalogTree.js

218 lines
7.1 KiB
JavaScript
Raw Normal View History

/**
* 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
}