2026-05-21 09:05:13 +02:00
|
|
|
export function frameworkSkillSummaryKey(id) {
|
|
|
|
|
return `framework_program:${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function moduleSkillSummaryKey(id) {
|
|
|
|
|
return `training_module:${id}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function skillEntryFromSummary(summary, skillId) {
|
2026-05-21 09:42:13 +02:00
|
|
|
if (!summary) return null
|
|
|
|
|
const sid = String(skillId)
|
|
|
|
|
const fromSkills = (summary.skills || []).find((s) => String(s.skill_id) === sid)
|
|
|
|
|
if (fromSkills) return fromSkills
|
|
|
|
|
return (summary.top_by_category || []).find((s) => String(s.skill_id) === sid) || null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function summaryHasSkill(summary, skillId, minClubPct = 0) {
|
|
|
|
|
const sk = skillEntryFromSummary(summary, skillId)
|
|
|
|
|
if (!sk || !(Number(sk.weight) > 0)) return false
|
|
|
|
|
const pct = sk.universal_percent
|
|
|
|
|
if (pct == null) return minClubPct === 0
|
|
|
|
|
return pct >= minClubPct
|
2026-05-21 09:05:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function maxSelectedSkillClubPercent(summary, skillIds = []) {
|
|
|
|
|
if (!summary || !skillIds.length) return null
|
|
|
|
|
let max = null
|
|
|
|
|
for (const id of skillIds) {
|
|
|
|
|
const sk = skillEntryFromSummary(summary, id)
|
|
|
|
|
if (!sk) continue
|
|
|
|
|
const pct = sk.universal_percent
|
|
|
|
|
if (pct == null) continue
|
|
|
|
|
if (max == null || pct > max) max = pct
|
|
|
|
|
}
|
|
|
|
|
return max
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatClubPercent(value) {
|
|
|
|
|
if (value == null || !Number.isFinite(Number(value))) return '—'
|
2026-05-21 09:33:20 +02:00
|
|
|
const n = Math.min(100, Number(value))
|
2026-05-21 09:05:13 +02:00
|
|
|
return n % 1 === 0 ? `${n}%` : `${n.toFixed(1)}%`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 09:42:13 +02:00
|
|
|
export function formatSkillWeight(value) {
|
|
|
|
|
const n = Number(value)
|
|
|
|
|
if (!Number.isFinite(n)) return '—'
|
|
|
|
|
return n % 1 === 0 ? String(n) : n.toFixed(1)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 09:33:20 +02:00
|
|
|
/** KPI-Zeilen: immer Top je Unterkategorie; bei Skill-Filter nur passende Kategorien. */
|
|
|
|
|
export function kpiRowsFromSummary(summary, { skillIds = [], limit = 8 } = {}) {
|
|
|
|
|
if (!summary) return []
|
|
|
|
|
let rows = (summary.top_by_category || []).map((row) => ({
|
|
|
|
|
skill_id: row.skill_id,
|
|
|
|
|
skill_name: row.skill_name,
|
|
|
|
|
category_name: row.category_name,
|
|
|
|
|
main_category_name: row.main_category_name,
|
|
|
|
|
weight: row.weight ?? row.score,
|
|
|
|
|
universal_percent: row.universal_percent,
|
|
|
|
|
is_club_best_for_skill: row.is_club_best_for_skill,
|
|
|
|
|
}))
|
|
|
|
|
if (skillIds.length) {
|
|
|
|
|
const wanted = new Set(skillIds.map(String))
|
|
|
|
|
rows = rows.filter((row) => wanted.has(String(row.skill_id)))
|
|
|
|
|
}
|
|
|
|
|
return rows.slice(0, limit)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated Nutze kpiRowsFromSummary für Listen */
|
|
|
|
|
export function compactSkillDisplayRows(summary, opts = {}) {
|
|
|
|
|
return kpiRowsFromSummary(summary, opts)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 09:05:13 +02:00
|
|
|
export function artifactTypeLabel(type) {
|
|
|
|
|
if (type === 'framework_program') return 'Rahmenprogramm'
|
|
|
|
|
if (type === 'training_module') return 'Modul'
|
|
|
|
|
if (type === 'progression_graph') return 'Regressionspfad'
|
|
|
|
|
return type || 'Artefakt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function artifactPath(ref) {
|
|
|
|
|
if (!ref) return null
|
|
|
|
|
if (ref.artifact_type === 'framework_program') {
|
|
|
|
|
return `/planning/framework-programs/${ref.artifact_id}`
|
|
|
|
|
}
|
|
|
|
|
if (ref.artifact_type === 'training_module') {
|
|
|
|
|
return `/planning/training-modules/${ref.artifact_id}`
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|