export function frameworkSkillSummaryKey(id) { return `framework_program:${id}` } export function moduleSkillSummaryKey(id) { return `training_module:${id}` } export function skillEntryFromSummary(summary, skillId) { 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 } 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 '—' const n = Math.min(100, Number(value)) return n % 1 === 0 ? `${n}%` : `${n.toFixed(1)}%` } export function formatSkillWeight(value) { const n = Number(value) if (!Number.isFinite(n)) return '—' return n % 1 === 0 ? String(n) : n.toFixed(1) } const PEER_LABELS = { framework_program: 'Rahmenpr.', training_module: 'Module', progression_graph: 'Pfade', } const PEER_COUNT_LABELS = { framework_program: 'Rahmenprogramme', training_module: 'Module', progression_graph: 'Regressionspfade', } export function peerPercentSuffix(artifactType = 'training_module') { return PEER_LABELS[artifactType] || 'Peers' } export function peerCorpusCountLabel(artifactType = 'training_module') { return PEER_COUNT_LABELS[artifactType] || 'Planungs-Artefakte' } /** 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) } 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 }