Refactor Skill Scoring Functions and Enhance Corpus Handling
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced new helper functions for managing artifact type corpus, improving code organization and readability. - Updated the `compute_club_corpus_reference` function to utilize the new corpus handling methods, enhancing clarity and maintainability. - Refactored skill profile functions to leverage the new corpus structure, ensuring consistent data retrieval across different artifact types. - Improved the handling of visibility clauses for library content, streamlining database queries for skill profiles. - Enhanced the batch skill profile summary function to aggregate reference data by artifact type, improving performance and accuracy.
This commit is contained in:
parent
34966b9e84
commit
2de4c0b7c9
|
|
@ -21,9 +21,11 @@ from skill_scoring import (
|
|||
collect_progression_graph_exercise_occurrences,
|
||||
collect_unit_exercise_occurrences,
|
||||
compact_profile_summary,
|
||||
compute_planning_corpus_by_type,
|
||||
compute_club_corpus_reference,
|
||||
compute_corpus_skill_max_weights,
|
||||
compute_skill_profile,
|
||||
corpus_for_artifact_type,
|
||||
fetch_exercise_skills_bulk,
|
||||
match_score_for_skill_ids,
|
||||
profile_for_occurrences,
|
||||
|
|
@ -78,9 +80,10 @@ def framework_program_skill_profile(
|
|||
)
|
||||
slots_raw = [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
corpus = _load_club_corpus(cur, tenant)
|
||||
ref_max = corpus["max_by_skill"]
|
||||
ref_by_skill = corpus["ref_by_skill"]
|
||||
bundle = _load_planning_corpus(cur, tenant)
|
||||
tc = corpus_for_artifact_type(bundle, "framework_program")
|
||||
ref_max = tc["max_by_skill"]
|
||||
ref_by_skill = tc["ref_by_skill"]
|
||||
|
||||
all_occurrences: List[ExerciseOccurrence] = []
|
||||
slot_profiles: List[Dict[str, Any]] = []
|
||||
|
|
@ -136,7 +139,9 @@ def framework_program_skill_profile(
|
|||
"artifact_type": "framework_program",
|
||||
"artifact_id": framework_id,
|
||||
"artifact_title": row.get("title"),
|
||||
"reference_scale": reference_scale_meta(corpus),
|
||||
"reference_scale": reference_scale_meta(
|
||||
tc, "framework_program", effective_club_id=tenant.effective_club_id
|
||||
),
|
||||
"club_best_by_skill": {
|
||||
str(k): v for k, v in ref_by_skill.items()
|
||||
},
|
||||
|
|
@ -155,9 +160,10 @@ def training_module_skill_profile(
|
|||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row = _module_access(cur, module_id, profile_id, role)
|
||||
corpus = _load_club_corpus(cur, tenant)
|
||||
ref_max = corpus["max_by_skill"]
|
||||
ref_by_skill = corpus["ref_by_skill"]
|
||||
bundle = _load_planning_corpus(cur, tenant)
|
||||
tc = corpus_for_artifact_type(bundle, "training_module")
|
||||
ref_max = tc["max_by_skill"]
|
||||
ref_by_skill = tc["ref_by_skill"]
|
||||
occurrences = collect_module_exercise_occurrences(cur, module_id)
|
||||
overall = (
|
||||
profile_for_occurrences(cur, occurrences, reference_max_by_skill=ref_max)
|
||||
|
|
@ -169,7 +175,9 @@ def training_module_skill_profile(
|
|||
"artifact_type": "training_module",
|
||||
"artifact_id": module_id,
|
||||
"artifact_title": row.get("title"),
|
||||
"reference_scale": reference_scale_meta(corpus),
|
||||
"reference_scale": reference_scale_meta(
|
||||
tc, "training_module", effective_club_id=tenant.effective_club_id
|
||||
),
|
||||
"club_best_by_skill": {
|
||||
str(k): v for k, v in ref_by_skill.items()
|
||||
},
|
||||
|
|
@ -187,9 +195,10 @@ def progression_graph_skill_profile(
|
|||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row = _require_graph_read(cur, graph_id, profile_id, role)
|
||||
corpus = _load_club_corpus(cur, tenant)
|
||||
ref_max = corpus["max_by_skill"]
|
||||
ref_by_skill = corpus["ref_by_skill"]
|
||||
bundle = _load_planning_corpus(cur, tenant)
|
||||
tc = corpus_for_artifact_type(bundle, "progression_graph")
|
||||
ref_max = tc["max_by_skill"]
|
||||
ref_by_skill = tc["ref_by_skill"]
|
||||
occurrences = collect_progression_graph_exercise_occurrences(cur, graph_id)
|
||||
overall = (
|
||||
profile_for_occurrences(
|
||||
|
|
@ -206,7 +215,9 @@ def progression_graph_skill_profile(
|
|||
"artifact_type": "progression_graph",
|
||||
"artifact_id": graph_id,
|
||||
"artifact_title": row.get("name"),
|
||||
"reference_scale": reference_scale_meta(corpus),
|
||||
"reference_scale": reference_scale_meta(
|
||||
tc, "progression_graph", effective_club_id=tenant.effective_club_id
|
||||
),
|
||||
"club_best_by_skill": {
|
||||
str(k): v for k, v in ref_by_skill.items()
|
||||
},
|
||||
|
|
@ -237,13 +248,13 @@ def batch_skill_profile_summaries(
|
|||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
corpus = compute_club_corpus_reference(
|
||||
bundle = compute_planning_corpus_by_type(
|
||||
cur,
|
||||
profile_id=tenant.profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
include_artifact_summaries=True,
|
||||
)
|
||||
ref_by_skill = corpus["ref_by_skill"]
|
||||
|
||||
allowed_fp: List[int] = []
|
||||
if fp_ids:
|
||||
|
|
@ -265,10 +276,13 @@ def batch_skill_profile_summaries(
|
|||
|
||||
summaries = _merge_batch_summaries(
|
||||
cur,
|
||||
corpus=corpus,
|
||||
bundle=bundle,
|
||||
allowed_fp=allowed_fp,
|
||||
allowed_mod=allowed_mod,
|
||||
)
|
||||
ref_by_skill = {}
|
||||
for t in ("framework_program", "training_module", "progression_graph"):
|
||||
ref_by_skill.update(corpus_for_artifact_type(bundle, t).get("ref_by_skill") or {})
|
||||
|
||||
skill_ids_seen: set[int] = set()
|
||||
for summary in summaries.values():
|
||||
|
|
@ -283,7 +297,14 @@ def batch_skill_profile_summaries(
|
|||
}
|
||||
|
||||
return {
|
||||
"reference_scale": reference_scale_meta(corpus),
|
||||
"reference_scale_by_type": {
|
||||
t: reference_scale_meta(
|
||||
corpus_for_artifact_type(bundle, t),
|
||||
t,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
for t in ("framework_program", "training_module", "progression_graph")
|
||||
},
|
||||
"club_best_by_skill": club_best_subset,
|
||||
"summaries": summaries,
|
||||
}
|
||||
|
|
@ -313,6 +334,10 @@ def skill_discovery_suggestions(
|
|||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
planning_bundle = _load_planning_corpus(cur, tenant)
|
||||
fw_ref = corpus_for_artifact_type(planning_bundle, "framework_program")["max_by_skill"]
|
||||
mod_ref = corpus_for_artifact_type(planning_bundle, "training_module")["max_by_skill"]
|
||||
graph_ref = corpus_for_artifact_type(planning_bundle, "progression_graph")["max_by_skill"]
|
||||
|
||||
if "framework_program" in type_set:
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
|
|
@ -351,7 +376,7 @@ def skill_discovery_suggestions(
|
|||
occ.extend(collect_unit_exercise_occurrences(cur, int(u["id"])))
|
||||
if not occ:
|
||||
continue
|
||||
prof = profile_for_occurrences(cur, occ)
|
||||
prof = profile_for_occurrences(cur, occ, reference_max_by_skill=fw_ref)
|
||||
match = match_score_for_skill_ids(prof, wanted)
|
||||
if match["match_weight"] <= 0:
|
||||
continue
|
||||
|
|
@ -395,7 +420,7 @@ def skill_discovery_suggestions(
|
|||
occ = collect_module_exercise_occurrences(cur, mid)
|
||||
if not occ:
|
||||
continue
|
||||
prof = profile_for_occurrences(cur, occ)
|
||||
prof = profile_for_occurrences(cur, occ, reference_max_by_skill=mod_ref)
|
||||
match = match_score_for_skill_ids(prof, wanted)
|
||||
if match["match_weight"] <= 0:
|
||||
continue
|
||||
|
|
@ -440,7 +465,8 @@ def skill_discovery_suggestions(
|
|||
if not occ:
|
||||
continue
|
||||
prof = profile_for_occurrences(
|
||||
cur, occ, default_item_minutes=GRAPH_DEFAULT_ITEM_MINUTES
|
||||
cur, occ, default_item_minutes=GRAPH_DEFAULT_ITEM_MINUTES,
|
||||
reference_max_by_skill=graph_ref,
|
||||
)
|
||||
match = match_score_for_skill_ids(prof, wanted)
|
||||
if match["match_weight"] <= 0:
|
||||
|
|
@ -509,10 +535,11 @@ def _parse_id_list(raw: Any, *, max_count: int = 120) -> List[int]:
|
|||
return out
|
||||
|
||||
|
||||
def _load_club_corpus(cur, tenant: TenantContext) -> Dict[str, Any]:
|
||||
return compute_club_corpus_reference(
|
||||
def _load_planning_corpus(cur, tenant: TenantContext) -> Dict[str, Any]:
|
||||
return compute_planning_corpus_by_type(
|
||||
cur,
|
||||
profile_id=tenant.profile_id,
|
||||
role=tenant.global_role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
|
||||
|
|
@ -602,52 +629,61 @@ def _summarize_training_module(
|
|||
def _merge_batch_summaries(
|
||||
cur,
|
||||
*,
|
||||
corpus: Dict[str, Any],
|
||||
bundle: Dict[str, Any],
|
||||
allowed_fp: List[int],
|
||||
allowed_mod: List[int],
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""Summaries für angeforderte IDs — auch official/private (nicht nur Vereins-Corpus-Cache)."""
|
||||
ref_max = corpus["max_by_skill"]
|
||||
ref_by_skill = corpus["ref_by_skill"]
|
||||
cached = corpus.get("artifact_summaries") or {}
|
||||
"""Summaries für angeforderte IDs — Referenz je Planungs-Kontext (Typ getrennt)."""
|
||||
fw_tc = corpus_for_artifact_type(bundle, "framework_program")
|
||||
mod_tc = corpus_for_artifact_type(bundle, "training_module")
|
||||
fw_cached = fw_tc.get("artifact_summaries") or {}
|
||||
mod_cached = mod_tc.get("artifact_summaries") or {}
|
||||
out: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
missing_fp = [fid for fid in allowed_fp if f"framework_program:{fid}" not in cached]
|
||||
fw_ref_max = fw_tc["max_by_skill"]
|
||||
fw_ref_by = fw_tc["ref_by_skill"]
|
||||
missing_fp = [fid for fid in allowed_fp if f"framework_program:{fid}" not in fw_cached]
|
||||
if missing_fp:
|
||||
occ_map = batch_framework_occurrences_by_id(cur, missing_fp)
|
||||
all_eids = {o.exercise_id for occs in occ_map.values() for o in occs}
|
||||
skills_map = fetch_exercise_skills_bulk(cur, all_eids) if all_eids else {}
|
||||
profiles = batch_compute_profiles(occ_map, skills_map, reference_max_by_skill=ref_max)
|
||||
profiles = batch_compute_profiles(
|
||||
occ_map, skills_map, reference_max_by_skill=fw_ref_max
|
||||
)
|
||||
for fid in missing_fp:
|
||||
key = f"framework_program:{fid}"
|
||||
prof = profiles.get(fid) or _empty_profile()
|
||||
_enrich_profile_club_best(prof, ref_by_skill, "framework_program", fid)
|
||||
out[key] = compact_profile_summary(prof, ref_by_skill)
|
||||
_enrich_profile_club_best(prof, fw_ref_by, "framework_program", fid)
|
||||
out[key] = compact_profile_summary(prof, fw_ref_by)
|
||||
|
||||
missing_mod = [mid for mid in allowed_mod if f"training_module:{mid}" not in cached]
|
||||
mod_ref_max = mod_tc["max_by_skill"]
|
||||
mod_ref_by = mod_tc["ref_by_skill"]
|
||||
missing_mod = [mid for mid in allowed_mod if f"training_module:{mid}" not in mod_cached]
|
||||
if missing_mod:
|
||||
occ_map = batch_module_occurrences_by_id(cur, missing_mod)
|
||||
all_eids = {o.exercise_id for occs in occ_map.values() for o in occs}
|
||||
skills_map = fetch_exercise_skills_bulk(cur, all_eids) if all_eids else {}
|
||||
profiles = batch_compute_profiles(occ_map, skills_map, reference_max_by_skill=ref_max)
|
||||
profiles = batch_compute_profiles(
|
||||
occ_map, skills_map, reference_max_by_skill=mod_ref_max
|
||||
)
|
||||
for mid in missing_mod:
|
||||
key = f"training_module:{mid}"
|
||||
prof = profiles.get(mid) or _empty_profile()
|
||||
_enrich_profile_club_best(prof, ref_by_skill, "training_module", mid)
|
||||
out[key] = compact_profile_summary(prof, ref_by_skill)
|
||||
_enrich_profile_club_best(prof, mod_ref_by, "training_module", mid)
|
||||
out[key] = compact_profile_summary(prof, mod_ref_by)
|
||||
|
||||
for fid in allowed_fp:
|
||||
key = f"framework_program:{fid}"
|
||||
if key in cached:
|
||||
out[key] = cached[key]
|
||||
if key in fw_cached:
|
||||
out[key] = fw_cached[key]
|
||||
elif key not in out:
|
||||
out[key] = _summarize_framework_program(cur, fid, ref_max, ref_by_skill)
|
||||
out[key] = _summarize_framework_program(cur, fid, fw_ref_max, fw_ref_by)
|
||||
|
||||
for mid in allowed_mod:
|
||||
key = f"training_module:{mid}"
|
||||
if key in cached:
|
||||
out[key] = cached[key]
|
||||
if key in mod_cached:
|
||||
out[key] = mod_cached[key]
|
||||
elif key not in out:
|
||||
out[key] = _summarize_training_module(cur, mid, ref_max, ref_by_skill)
|
||||
out[key] = _summarize_training_module(cur, mid, mod_ref_max, mod_ref_by)
|
||||
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -686,34 +686,41 @@ def batch_compute_profiles(
|
|||
}
|
||||
|
||||
|
||||
def compute_club_corpus_reference(
|
||||
def _empty_type_corpus() -> Dict[str, Any]:
|
||||
return {
|
||||
"max_by_skill": {},
|
||||
"ref_by_skill": {},
|
||||
"artifact_count": 0,
|
||||
"artifact_summaries": {},
|
||||
}
|
||||
|
||||
|
||||
def corpus_for_artifact_type(
|
||||
bundle: Dict[str, Any],
|
||||
artifact_type: str,
|
||||
) -> Dict[str, Any]:
|
||||
by_type = bundle.get("by_type") or {}
|
||||
return by_type.get(artifact_type) or _empty_type_corpus()
|
||||
|
||||
|
||||
def _scan_artifact_type_corpus(
|
||||
cur,
|
||||
*,
|
||||
artifact_type: str,
|
||||
profile_id: int,
|
||||
role: Optional[str],
|
||||
effective_club_id: Optional[int],
|
||||
include_artifact_summaries: bool = False,
|
||||
include_artifact_summaries: bool,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Stärkstes Trainingsgewicht je Fähigkeit über alle Vereins-Artefakte (sichtbar im aktiven Verein).
|
||||
Optional: kompakte Profile aller gescannten Artefakte (ein Durchlauf für Listen).
|
||||
"""
|
||||
from tenant_context import club_library_visibility_sql
|
||||
"""Referenz je Fähigkeit nur innerhalb eines Planungs-Kontexts (ein Artefakttyp, sichtbare Bibliothek)."""
|
||||
from tenant_context import library_content_visibility_sql
|
||||
|
||||
max_by_skill: Dict[int, float] = {}
|
||||
ref_by_skill: Dict[int, Dict[str, Any]] = {}
|
||||
artifact_count = 0
|
||||
raw_profiles: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
if effective_club_id is None:
|
||||
return {
|
||||
"club_id": None,
|
||||
"max_by_skill": max_by_skill,
|
||||
"ref_by_skill": ref_by_skill,
|
||||
"artifact_count": 0,
|
||||
"artifact_summaries": {},
|
||||
}
|
||||
|
||||
def ingest(artifact_type: str, artifact_id: int, title: Optional[str], prof: Dict[str, Any]) -> None:
|
||||
def ingest(aid: int, title: Optional[str], prof: Dict[str, Any]) -> None:
|
||||
nonlocal artifact_count
|
||||
if not prof.get("skills"):
|
||||
return
|
||||
|
|
@ -723,15 +730,17 @@ def compute_club_corpus_reference(
|
|||
ref_by_skill,
|
||||
prof,
|
||||
artifact_type=artifact_type,
|
||||
artifact_id=artifact_id,
|
||||
artifact_id=aid,
|
||||
artifact_title=title,
|
||||
)
|
||||
if include_artifact_summaries:
|
||||
raw_profiles[f"{artifact_type}:{artifact_id}"] = prof
|
||||
raw_profiles[f"{artifact_type}:{aid}"] = prof
|
||||
|
||||
vis_clause, vis_params = club_library_visibility_sql(
|
||||
if artifact_type == "framework_program":
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="fp",
|
||||
profile_id=profile_id,
|
||||
role=role or "",
|
||||
effective_club_id=effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
|
|
@ -743,20 +752,22 @@ def compute_club_corpus_reference(
|
|||
""",
|
||||
vis_params,
|
||||
)
|
||||
fw_rows = cur.fetchall()
|
||||
if fw_rows:
|
||||
fw_ids = [int(r["id"]) for r in fw_rows]
|
||||
titles = {int(r["id"]): r.get("title") for r in fw_rows}
|
||||
occ_map = batch_framework_occurrences_by_id(cur, fw_ids)
|
||||
rows = cur.fetchall()
|
||||
if rows:
|
||||
ids = [int(r["id"]) for r in rows]
|
||||
titles = {int(r["id"]): r.get("title") for r in rows}
|
||||
occ_map = batch_framework_occurrences_by_id(cur, ids)
|
||||
all_eids = {o.exercise_id for occs in occ_map.values() for o in occs}
|
||||
skills_map = fetch_exercise_skills_bulk(cur, all_eids)
|
||||
skills_map = fetch_exercise_skills_bulk(cur, all_eids) if all_eids else {}
|
||||
profiles = batch_compute_profiles(occ_map, skills_map)
|
||||
for fid, prof in profiles.items():
|
||||
ingest("framework_program", fid, titles.get(fid), prof)
|
||||
for aid, prof in profiles.items():
|
||||
ingest(aid, titles.get(aid), prof)
|
||||
|
||||
vis_clause, vis_params = club_library_visibility_sql(
|
||||
elif artifact_type == "training_module":
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="m",
|
||||
profile_id=profile_id,
|
||||
role=role or "",
|
||||
effective_club_id=effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
|
|
@ -768,20 +779,22 @@ def compute_club_corpus_reference(
|
|||
""",
|
||||
vis_params,
|
||||
)
|
||||
mod_rows = cur.fetchall()
|
||||
if mod_rows:
|
||||
mod_ids = [int(r["id"]) for r in mod_rows]
|
||||
titles = {int(r["id"]): r.get("title") for r in mod_rows}
|
||||
occ_map = batch_module_occurrences_by_id(cur, mod_ids)
|
||||
rows = cur.fetchall()
|
||||
if rows:
|
||||
ids = [int(r["id"]) for r in rows]
|
||||
titles = {int(r["id"]): r.get("title") for r in rows}
|
||||
occ_map = batch_module_occurrences_by_id(cur, ids)
|
||||
all_eids = {o.exercise_id for occs in occ_map.values() for o in occs}
|
||||
skills_map = fetch_exercise_skills_bulk(cur, all_eids)
|
||||
skills_map = fetch_exercise_skills_bulk(cur, all_eids) if all_eids else {}
|
||||
profiles = batch_compute_profiles(occ_map, skills_map)
|
||||
for mid, prof in profiles.items():
|
||||
ingest("training_module", mid, titles.get(mid), prof)
|
||||
for aid, prof in profiles.items():
|
||||
ingest(aid, titles.get(aid), prof)
|
||||
|
||||
vis_clause, vis_params = club_library_visibility_sql(
|
||||
elif artifact_type == "progression_graph":
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="g",
|
||||
profile_id=profile_id,
|
||||
role=role or "",
|
||||
effective_club_id=effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
|
|
@ -801,7 +814,7 @@ def compute_club_corpus_reference(
|
|||
prof = profile_for_occurrences(
|
||||
cur, occ, default_item_minutes=GRAPH_DEFAULT_ITEM_MINUTES
|
||||
)
|
||||
ingest("progression_graph", gid, row.get("name"), prof)
|
||||
ingest(gid, row.get("name"), prof)
|
||||
|
||||
artifact_summaries: Dict[str, Dict[str, Any]] = {}
|
||||
if include_artifact_summaries and raw_profiles:
|
||||
|
|
@ -810,7 +823,6 @@ def compute_club_corpus_reference(
|
|||
artifact_summaries[key] = compact_profile_summary(prof, ref_by_skill)
|
||||
|
||||
return {
|
||||
"club_id": effective_club_id,
|
||||
"max_by_skill": max_by_skill,
|
||||
"ref_by_skill": ref_by_skill,
|
||||
"artifact_count": artifact_count,
|
||||
|
|
@ -818,15 +830,106 @@ def compute_club_corpus_reference(
|
|||
}
|
||||
|
||||
|
||||
def reference_scale_meta(corpus: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def compute_planning_corpus_by_type(
|
||||
cur,
|
||||
*,
|
||||
profile_id: int,
|
||||
role: Optional[str],
|
||||
effective_club_id: Optional[int],
|
||||
include_artifact_summaries: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Referenz je Fähigkeit getrennt nach Planungs-Kontext:
|
||||
Rahmenprogramme, Trainingsmodule und Regressionspfade jeweils für sich,
|
||||
jeweils über die sichtbare Bibliothek (library_content_visibility_sql).
|
||||
"""
|
||||
by_type = {
|
||||
"framework_program": _scan_artifact_type_corpus(
|
||||
cur,
|
||||
artifact_type="framework_program",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=effective_club_id,
|
||||
include_artifact_summaries=include_artifact_summaries,
|
||||
),
|
||||
"training_module": _scan_artifact_type_corpus(
|
||||
cur,
|
||||
artifact_type="training_module",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=effective_club_id,
|
||||
include_artifact_summaries=include_artifact_summaries,
|
||||
),
|
||||
"progression_graph": _scan_artifact_type_corpus(
|
||||
cur,
|
||||
artifact_type="progression_graph",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=effective_club_id,
|
||||
include_artifact_summaries=include_artifact_summaries,
|
||||
),
|
||||
}
|
||||
return {
|
||||
"scope": "club",
|
||||
"club_id": corpus.get("club_id"),
|
||||
"skills_in_corpus": len(corpus.get("max_by_skill") or {}),
|
||||
"artifacts_scanned": corpus.get("artifact_count") or 0,
|
||||
"effective_club_id": effective_club_id,
|
||||
"by_type": by_type,
|
||||
}
|
||||
|
||||
|
||||
def compute_club_corpus_reference(
|
||||
cur,
|
||||
*,
|
||||
profile_id: int,
|
||||
effective_club_id: Optional[int],
|
||||
include_artifact_summaries: bool = False,
|
||||
role: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Legacy-Hülle — merged summaries über alle Typen (vermeiden für neue Aufrufer)."""
|
||||
bundle = compute_planning_corpus_by_type(
|
||||
cur,
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=effective_club_id,
|
||||
include_artifact_summaries=include_artifact_summaries,
|
||||
)
|
||||
tc = corpus_for_artifact_type(bundle, "framework_program")
|
||||
merged_summaries: Dict[str, Dict[str, Any]] = {}
|
||||
for t in ("framework_program", "training_module", "progression_graph"):
|
||||
merged_summaries.update((bundle["by_type"][t].get("artifact_summaries") or {}))
|
||||
return {
|
||||
"club_id": effective_club_id,
|
||||
"max_by_skill": tc["max_by_skill"],
|
||||
"ref_by_skill": tc["ref_by_skill"],
|
||||
"artifact_count": sum(
|
||||
(bundle["by_type"][t].get("artifact_count") or 0)
|
||||
for t in bundle["by_type"]
|
||||
),
|
||||
"artifact_summaries": merged_summaries,
|
||||
}
|
||||
|
||||
|
||||
_ARTIFACT_TYPE_LABELS = {
|
||||
"framework_program": "Rahmenprogrammen",
|
||||
"training_module": "Trainingsmodulen",
|
||||
"progression_graph": "Regressionspfaden",
|
||||
}
|
||||
|
||||
|
||||
def reference_scale_meta(
|
||||
type_corpus: Dict[str, Any],
|
||||
artifact_type: str,
|
||||
*,
|
||||
effective_club_id: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
label = _ARTIFACT_TYPE_LABELS.get(artifact_type, artifact_type)
|
||||
return {
|
||||
"scope": "planning_peer",
|
||||
"artifact_type": artifact_type,
|
||||
"effective_club_id": effective_club_id,
|
||||
"skills_in_corpus": len(type_corpus.get("max_by_skill") or {}),
|
||||
"artifacts_scanned": type_corpus.get("artifact_count") or 0,
|
||||
"description": (
|
||||
"universal_percent = Anteil am stärksten genutzten Vereins-Artefakt je Fähigkeit "
|
||||
"(nur visibility=club im aktiven Verein)"
|
||||
f"Prozent = Anteil am stärksten sichtbaren Eintrag unter {label} je Fähigkeit "
|
||||
f"(nicht gemischt mit anderen Planungs-Artefakttypen)"
|
||||
),
|
||||
}
|
||||
|
||||
|
|
@ -838,18 +941,17 @@ def compute_corpus_skill_max_weights(
|
|||
role: Optional[str],
|
||||
effective_club_id: Optional[int],
|
||||
limit_per_type: int = 50,
|
||||
artifact_type: str = "framework_program",
|
||||
) -> Dict[int, float]:
|
||||
"""
|
||||
Vereins-Referenz je Fähigkeit (Legacy-Hülle — nutzt compute_club_corpus_reference).
|
||||
role/limit_per_type werden ignoriert (Vereinskontext, alle Vereins-Artefakte).
|
||||
"""
|
||||
del role, limit_per_type
|
||||
corpus = compute_club_corpus_reference(
|
||||
"""Referenz je Fähigkeit innerhalb eines Planungs-Kontexts (Legacy-Hülle)."""
|
||||
del limit_per_type
|
||||
bundle = compute_planning_corpus_by_type(
|
||||
cur,
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=effective_club_id,
|
||||
)
|
||||
return corpus["max_by_skill"]
|
||||
return corpus_for_artifact_type(bundle, artifact_type)["max_by_skill"]
|
||||
|
||||
|
||||
def match_score_for_skill_ids(profile: Dict[str, Any], skill_ids: Sequence[int]) -> Dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ export default function ExerciseProgressionGraphPanel({
|
|||
loading={skillProfileLoading}
|
||||
error={skillProfileError}
|
||||
defaultExpanded
|
||||
artifactType="progression_graph"
|
||||
/>
|
||||
<div className="card" style={{ marginBottom: '12px' }}>
|
||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>Sequenz / Reihe anlegen</h3>
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ export default function FrameworkProgramListCard({
|
|||
</div>
|
||||
<SkillProfileCompact
|
||||
summary={skillSummary}
|
||||
artifactType="framework_program"
|
||||
loading={skillSummaryLoading}
|
||||
displayLimit={skillDisplayLimit}
|
||||
highlightSkillIds={skillFilterIds}
|
||||
|
|
|
|||
|
|
@ -291,10 +291,10 @@ export default function FrameworkProgramsFilterBlock({
|
|||
|
||||
{catalogSkills.length > 0 ? (
|
||||
<div className="fw-import-catalog-block fw-import-catalog-block--skills">
|
||||
<span className="form-label">Fähigkeiten (Vereinsvergleich)</span>
|
||||
<span className="form-label">Fähigkeiten (Rahmenprogramme)</span>
|
||||
<p className="form-sub" style={{ margin: '0 0 8px' }}>
|
||||
Filtert nach Trainingsgewicht relativ zum stärksten Vereins-Programm je Fähigkeit — ohne
|
||||
Punktewerte eingeben.
|
||||
Filtert nach Trainingsgewicht relativ zum stärksten sichtbaren Rahmenprogramm je Fähigkeit — nur
|
||||
unter Rahmenprogrammen, nicht gegen Module.
|
||||
</p>
|
||||
<div className="framework-catalog-checkgrid fw-import-skill-grid">
|
||||
{catalogSkills.map((sk) => (
|
||||
|
|
@ -312,7 +312,7 @@ export default function FrameworkProgramsFilterBlock({
|
|||
{(filters.skillIds || []).length > 0 ? (
|
||||
<div className="fw-import-skill-options">
|
||||
<div className="form-row" style={{ marginBottom: 8 }}>
|
||||
<label className="form-label">Mindest-Anteil am Vereins-Maximum</label>
|
||||
<label className="form-label">Mindest-Anteil am Rahmenprogramm-Maximum</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={String(filters.skillMinClubPercent ?? 0)}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import React from 'react'
|
||||
import { formatClubPercent, formatSkillWeight, kpiRowsFromSummary } from '../../utils/skillProfileListHelpers'
|
||||
import {
|
||||
formatClubPercent,
|
||||
formatSkillWeight,
|
||||
kpiRowsFromSummary,
|
||||
peerPercentSuffix,
|
||||
} from '../../utils/skillProfileListHelpers'
|
||||
|
||||
/**
|
||||
* Kleine KPI-Kacheln: je Unterkategorie die Top-Fähigkeit (Listen/Karten).
|
||||
*/
|
||||
export default function SkillProfileCompact({
|
||||
summary,
|
||||
artifactType = 'training_module',
|
||||
loading = false,
|
||||
emptyText = 'Keine Fähigkeiten',
|
||||
displayLimit = 24,
|
||||
|
|
@ -21,6 +27,7 @@ export default function SkillProfileCompact({
|
|||
|
||||
const rows = kpiRowsFromSummary(summary, { limit: displayLimit })
|
||||
const highlight = new Set((highlightSkillIds || []).map(String))
|
||||
const peerLabel = peerPercentSuffix(artifactType)
|
||||
|
||||
if (!rows.length) {
|
||||
return summary ? <p className="form-sub skill-kpi-grid--empty">{emptyText}</p> : null
|
||||
|
|
@ -39,14 +46,14 @@ export default function SkillProfileCompact({
|
|||
(highlighted ? ' skill-kpi-tile--highlight' : '') +
|
||||
(isBest ? ' skill-kpi-tile--best' : '')
|
||||
}
|
||||
title={`${row.category_name}: ${row.skill_name} — Gewicht ${formatSkillWeight(row.weight ?? row.score)}, ${formatClubPercent(row.universal_percent)} vom Vereins-Maximum`}
|
||||
title={`${row.category_name}: ${row.skill_name} — Gewicht ${formatSkillWeight(row.weight ?? row.score)}, ${formatClubPercent(row.universal_percent)} unter ${peerLabel}`}
|
||||
>
|
||||
<span className="skill-kpi-tile__cat">{row.category_name}</span>
|
||||
<span className="skill-kpi-tile__name">{row.skill_name}</span>
|
||||
<span className="skill-kpi-tile__score">{formatSkillWeight(row.weight ?? row.score)}</span>
|
||||
<span className="skill-kpi-tile__pct">
|
||||
{isBest ? '★ ' : ''}
|
||||
{formatClubPercent(row.universal_percent)}
|
||||
{formatClubPercent(row.universal_percent)} {peerLabel}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import api from '../../utils/api'
|
||||
import { peerCorpusCountLabel } from '../../utils/skillProfileListHelpers'
|
||||
import SkillProfilePanel from './SkillProfilePanel'
|
||||
|
||||
/**
|
||||
|
|
@ -45,6 +46,9 @@ export default function SkillProfileFullModal({
|
|||
|
||||
if (!open) return null
|
||||
|
||||
const peerCountLabel = peerCorpusCountLabel(artifactType)
|
||||
const scale = data?.reference_scale
|
||||
|
||||
return (
|
||||
<div className="skill-profile-modal" role="dialog" aria-modal="true" aria-labelledby="skill-profile-modal-title">
|
||||
<button type="button" className="skill-profile-modal__backdrop" aria-label="Schließen" onClick={onClose} />
|
||||
|
|
@ -66,11 +70,13 @@ export default function SkillProfileFullModal({
|
|||
displayMode="full"
|
||||
embedded
|
||||
defaultExpanded
|
||||
artifactType={artifactType}
|
||||
/>
|
||||
{data?.reference_scale?.scope === 'club' && !loading ? (
|
||||
{scale && !loading ? (
|
||||
<p className="form-sub skill-profile-modal__scale">
|
||||
Vergleichsbasis: {data.reference_scale.artifacts_scanned ?? 0} Vereins-Artefakte (
|
||||
{data.reference_scale.skills_in_corpus ?? 0} Fähigkeiten mit Referenz).
|
||||
Vergleichsbasis: {scale.artifacts_scanned ?? 0} sichtbare {peerCountLabel} (
|
||||
{scale.skills_in_corpus ?? 0} Fähigkeiten mit Referenz).
|
||||
{scale.description ? ` ${scale.description}` : null}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import React, { useMemo, useState } from 'react'
|
||||
import {
|
||||
formatClubPercent,
|
||||
peerPercentSuffix,
|
||||
} from '../../utils/skillProfileListHelpers'
|
||||
|
||||
function skillWeight(skill) {
|
||||
return Number(skill?.weight ?? skill?.score ?? 0)
|
||||
|
|
@ -10,12 +14,6 @@ function formatWeight(value) {
|
|||
return n % 1 === 0 ? String(n) : n.toFixed(1)
|
||||
}
|
||||
|
||||
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)}%`
|
||||
}
|
||||
|
||||
function barFillPercent(skill, maxWeight, hasReferenceScale) {
|
||||
if (hasReferenceScale && skill?.universal_percent != null) {
|
||||
return Math.min(100, Number(skill.universal_percent))
|
||||
|
|
@ -24,15 +22,15 @@ function barFillPercent(skill, maxWeight, hasReferenceScale) {
|
|||
return maxWeight > 0 ? Math.min(100, (w / maxWeight) * 100) : 0
|
||||
}
|
||||
|
||||
function metricLabel(skill, hasReferenceScale) {
|
||||
function metricLabel(skill, hasReferenceScale, peerLabel) {
|
||||
if (hasReferenceScale && skill?.universal_percent != null) {
|
||||
const best = skill.is_club_best_for_skill ? ' ★' : ''
|
||||
return `${formatClubPercent(skill.universal_percent)} Verein${best}`
|
||||
return `${formatClubPercent(skill.universal_percent)} ${peerLabel}${best}`
|
||||
}
|
||||
return formatWeight(skillWeight(skill))
|
||||
}
|
||||
|
||||
function SkillRow({ skill, maxWeight, hasReferenceScale }) {
|
||||
function SkillRow({ skill, maxWeight, hasReferenceScale, peerLabel }) {
|
||||
if (!skill) return null
|
||||
const pct = barFillPercent(skill, maxWeight, hasReferenceScale)
|
||||
return (
|
||||
|
|
@ -45,7 +43,7 @@ function SkillRow({ skill, maxWeight, hasReferenceScale }) {
|
|||
{skill.skill_name}
|
||||
</span>
|
||||
<span className="skill-profile__pct" title="Trainingsgewicht (gewichtete Minuten)">
|
||||
{metricLabel(skill, hasReferenceScale)}
|
||||
{metricLabel(skill, hasReferenceScale, peerLabel)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="skill-profile__bar-track" aria-hidden="true">
|
||||
|
|
@ -57,11 +55,11 @@ function SkillRow({ skill, maxWeight, hasReferenceScale }) {
|
|||
{skill.club_best ? (
|
||||
<>
|
||||
{' '}
|
||||
· Vereins-Top: {skill.club_best.artifact_title || skill.club_best.artifact_id} (
|
||||
· Top unter {peerLabel}: {skill.club_best.artifact_title || skill.club_best.artifact_id} (
|
||||
{formatWeight(skill.club_best.weight)})
|
||||
</>
|
||||
) : skill.is_club_best_for_skill ? (
|
||||
' · Stärkste Vereins-Nutzung'
|
||||
' · Stärkster unter ' + peerLabel
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
|
|
@ -69,12 +67,19 @@ function SkillRow({ skill, maxWeight, hasReferenceScale }) {
|
|||
)
|
||||
}
|
||||
|
||||
function CategoryTopSkill({ skill, maxWeight, hasReferenceScale }) {
|
||||
function CategoryTopSkill({ skill, maxWeight, hasReferenceScale, peerLabel }) {
|
||||
if (!skill) return null
|
||||
return <SkillRow skill={skill} maxWeight={maxWeight} hasReferenceScale={hasReferenceScale} />
|
||||
return (
|
||||
<SkillRow
|
||||
skill={skill}
|
||||
maxWeight={maxWeight}
|
||||
hasReferenceScale={hasReferenceScale}
|
||||
peerLabel={peerLabel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CategoryGroupedProfile({ profile, ariaLabel }) {
|
||||
function CategoryGroupedProfile({ profile, ariaLabel, peerLabel }) {
|
||||
const groups = profile?.by_main_category || []
|
||||
const hasReferenceScale = Boolean(profile?.has_reference_scale)
|
||||
const maxWeight = useMemo(() => {
|
||||
|
|
@ -104,6 +109,7 @@ function CategoryGroupedProfile({ profile, ariaLabel }) {
|
|||
skill={cat.top_skill}
|
||||
maxWeight={maxWeight}
|
||||
hasReferenceScale={hasReferenceScale}
|
||||
peerLabel={peerLabel}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -115,7 +121,7 @@ function CategoryGroupedProfile({ profile, ariaLabel }) {
|
|||
)
|
||||
}
|
||||
|
||||
function FullSkillsProfile({ profile, ariaLabel }) {
|
||||
function FullSkillsProfile({ profile, ariaLabel, peerLabel }) {
|
||||
const skills = profile?.skills || []
|
||||
const hasReferenceScale = Boolean(profile?.has_reference_scale)
|
||||
const maxWeight = useMemo(
|
||||
|
|
@ -133,6 +139,7 @@ function FullSkillsProfile({ profile, ariaLabel }) {
|
|||
skill={sk}
|
||||
maxWeight={maxWeight}
|
||||
hasReferenceScale={hasReferenceScale}
|
||||
peerLabel={peerLabel}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -166,14 +173,17 @@ export default function SkillProfilePanel({
|
|||
defaultExpanded = true,
|
||||
displayMode = 'summary',
|
||||
embedded = false,
|
||||
artifactType = 'framework_program',
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded)
|
||||
const [slotOpenId, setSlotOpenId] = useState(null)
|
||||
|
||||
const peerLabel = peerPercentSuffix(artifactType)
|
||||
|
||||
const defaultHint =
|
||||
displayMode === 'full'
|
||||
? 'Alle verknüpften Fähigkeiten nach Trainingsgewicht. Vergleich zum Vereins-Maximum je Fähigkeit (max. 100 %).'
|
||||
: 'Trainingsgewicht aus Dauer, Häufigkeit, Intensität und Stufen-Spanne. Pro Kategorie die stärkste Fähigkeit; Balken = Anteil am Vereins-Maximum.'
|
||||
? `Alle verknüpften Fähigkeiten nach Trainingsgewicht. Prozent = Anteil am stärksten sichtbaren Eintrag unter ${peerLabel} je Fähigkeit.`
|
||||
: `Trainingsgewicht aus Dauer, Häufigkeit, Intensität und Stufen-Spanne. Prozent vergleicht nur unter ${peerLabel}, nicht mit anderen Planungs-Artefakttypen.`
|
||||
|
||||
const hintText = hint || defaultHint
|
||||
const badge = useMemo(() => topCategoryBadge(profile), [profile])
|
||||
|
|
@ -237,9 +247,17 @@ export default function SkillProfilePanel({
|
|||
</div>
|
||||
|
||||
{displayMode === 'full' ? (
|
||||
<FullSkillsProfile profile={profile} ariaLabel="Alle Fähigkeiten nach Gewicht" />
|
||||
<FullSkillsProfile
|
||||
profile={profile}
|
||||
ariaLabel="Alle Fähigkeiten nach Gewicht"
|
||||
peerLabel={peerLabel}
|
||||
/>
|
||||
) : (
|
||||
<CategoryGroupedProfile profile={profile} ariaLabel="Top-Fähigkeit je Kategorie" />
|
||||
<CategoryGroupedProfile
|
||||
profile={profile}
|
||||
ariaLabel="Top-Fähigkeit je Kategorie"
|
||||
peerLabel={peerLabel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
@ -275,11 +293,13 @@ export default function SkillProfilePanel({
|
|||
<FullSkillsProfile
|
||||
profile={sl.profile}
|
||||
ariaLabel={`Alle Fähigkeiten Session ${(sl.sort_order ?? 0) + 1}`}
|
||||
peerLabel={peerLabel}
|
||||
/>
|
||||
) : (
|
||||
<CategoryGroupedProfile
|
||||
profile={sl.profile}
|
||||
ariaLabel={`Fähigkeiten Session ${(sl.sort_order ?? 0) + 1}`}
|
||||
peerLabel={peerLabel}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -975,11 +975,11 @@ export default function TrainingFrameworkProgramEditPage() {
|
|||
{!isNew ? (
|
||||
<SkillProfilePanel
|
||||
title="Fähigkeiten-Schwerpunkte (aus Übungen)"
|
||||
hint="Trainingsgewicht aus Dauer, Häufigkeit, Intensität und Stufen-Spanne. Pro Kategorie die stärkste Fähigkeit — Balken relativ zur Bibliothek, nicht 100 % innerhalb des Plans."
|
||||
profile={skillProfileData?.overall}
|
||||
slots={skillProfileData?.slots}
|
||||
loading={skillProfileLoading}
|
||||
error={skillProfileError}
|
||||
artifactType="framework_program"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -429,6 +429,7 @@ export default function TrainingModuleEditPage() {
|
|||
profile={skillProfileData?.overall}
|
||||
loading={skillProfileLoading}
|
||||
error={skillProfileError}
|
||||
artifactType="training_module"
|
||||
/>
|
||||
) : null}
|
||||
<div className="form-row">
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export default function TrainingModulesListPage() {
|
|||
Trainingsmodule
|
||||
</h1>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', maxWidth: '38rem', margin: 0 }}>
|
||||
Wiederverwendbare Übungsfolgen für die Trainingsplanung. Fähigkeiten werden im Vereinskontext verglichen.
|
||||
Wiederverwendbare Übungsfolgen für die Trainingsplanung. Prozentwerte vergleichen Module nur unter sichtbaren Modulen.
|
||||
</p>
|
||||
</div>
|
||||
<NavStateLink
|
||||
|
|
@ -144,8 +144,9 @@ export default function TrainingModulesListPage() {
|
|||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<SkillProfileCompact
|
||||
summary={skillSummaries[`training_module:${r.id}`]}
|
||||
artifactType="training_module"
|
||||
loading={summariesLoading}
|
||||
displayLimit={6}
|
||||
displayLimit={12}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ export function summarizeFrameworkImportFilters(filters = {}, catalogs = {}) {
|
|||
parts.push(`Fähigkeiten: ${names.join(', ')}`)
|
||||
}
|
||||
if (Number(f.skillMinClubPercent) > 0) {
|
||||
parts.push(`mind. ${f.skillMinClubPercent}% vom Vereins-Maximum`)
|
||||
parts.push(`mind. ${f.skillMinClubPercent}% vom Rahmenprogramm-Maximum`)
|
||||
}
|
||||
if (f.skillSort === 'skill_strength' && (f.skillIds || []).length) {
|
||||
parts.push('Sortierung: Fähigkeiten-Stärke')
|
||||
|
|
|
|||
|
|
@ -47,6 +47,26 @@ export function formatSkillWeight(value) {
|
|||
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 []
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user