diff --git a/backend/routers/skill_profiles.py b/backend/routers/skill_profiles.py index 3467ceb..890ef6a 100644 --- a/backend/routers/skill_profiles.py +++ b/backend/routers/skill_profiles.py @@ -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 diff --git a/backend/skill_scoring.py b/backend/skill_scoring.py index 6bf5ce0..972b55f 100644 --- a/backend/skill_scoring.py +++ b/backend/skill_scoring.py @@ -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,85 +730,91 @@ 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( - alias="fp", - profile_id=profile_id, - effective_club_id=effective_club_id, - ) - cur.execute( - f""" - SELECT fp.id, fp.title - FROM training_framework_programs fp - WHERE ({vis_clause}) - ORDER BY fp.updated_at DESC NULLS LAST - """, - 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) - all_eids = {o.exercise_id for occs in occ_map.values() for o in occs} - skills_map = fetch_exercise_skills_bulk(cur, all_eids) - profiles = batch_compute_profiles(occ_map, skills_map) - for fid, prof in profiles.items(): - ingest("framework_program", fid, titles.get(fid), prof) - - vis_clause, vis_params = club_library_visibility_sql( - alias="m", - profile_id=profile_id, - effective_club_id=effective_club_id, - ) - cur.execute( - f""" - SELECT m.id, m.title - FROM training_modules m - WHERE ({vis_clause}) - ORDER BY m.updated_at DESC NULLS LAST - """, - 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) - all_eids = {o.exercise_id for occs in occ_map.values() for o in occs} - skills_map = fetch_exercise_skills_bulk(cur, all_eids) - profiles = batch_compute_profiles(occ_map, skills_map) - for mid, prof in profiles.items(): - ingest("training_module", mid, titles.get(mid), prof) - - vis_clause, vis_params = club_library_visibility_sql( - alias="g", - profile_id=profile_id, - effective_club_id=effective_club_id, - ) - cur.execute( - f""" - SELECT g.id, g.name - FROM exercise_progression_graphs g - WHERE ({vis_clause}) - ORDER BY g.updated_at DESC NULLS LAST - """, - vis_params, - ) - for row in cur.fetchall(): - gid = int(row["id"]) - occ = collect_progression_graph_exercise_occurrences(cur, gid) - if not occ: - continue - prof = profile_for_occurrences( - cur, occ, default_item_minutes=GRAPH_DEFAULT_ITEM_MINUTES + 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, ) - ingest("progression_graph", gid, row.get("name"), prof) + cur.execute( + f""" + SELECT fp.id, fp.title + FROM training_framework_programs fp + WHERE ({vis_clause}) + ORDER BY fp.updated_at DESC NULLS LAST + """, + vis_params, + ) + 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) if all_eids else {} + profiles = batch_compute_profiles(occ_map, skills_map) + for aid, prof in profiles.items(): + ingest(aid, titles.get(aid), prof) + + 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( + f""" + SELECT m.id, m.title + FROM training_modules m + WHERE ({vis_clause}) + ORDER BY m.updated_at DESC NULLS LAST + """, + vis_params, + ) + 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) if all_eids else {} + profiles = batch_compute_profiles(occ_map, skills_map) + for aid, prof in profiles.items(): + ingest(aid, titles.get(aid), prof) + + 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( + f""" + SELECT g.id, g.name + FROM exercise_progression_graphs g + WHERE ({vis_clause}) + ORDER BY g.updated_at DESC NULLS LAST + """, + vis_params, + ) + for row in cur.fetchall(): + gid = int(row["id"]) + occ = collect_progression_graph_exercise_occurrences(cur, gid) + if not occ: + continue + prof = profile_for_occurrences( + cur, occ, default_item_minutes=GRAPH_DEFAULT_ITEM_MINUTES + ) + 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]: diff --git a/frontend/src/components/ExerciseProgressionGraphPanel.jsx b/frontend/src/components/ExerciseProgressionGraphPanel.jsx index 51c1cf8..3dceb6c 100644 --- a/frontend/src/components/ExerciseProgressionGraphPanel.jsx +++ b/frontend/src/components/ExerciseProgressionGraphPanel.jsx @@ -689,6 +689,7 @@ export default function ExerciseProgressionGraphPanel({ loading={skillProfileLoading} error={skillProfileError} defaultExpanded + artifactType="progression_graph" />

Sequenz / Reihe anlegen

diff --git a/frontend/src/components/planning/FrameworkProgramListCard.jsx b/frontend/src/components/planning/FrameworkProgramListCard.jsx index 7743774..1fbf549 100644 --- a/frontend/src/components/planning/FrameworkProgramListCard.jsx +++ b/frontend/src/components/planning/FrameworkProgramListCard.jsx @@ -137,6 +137,7 @@ export default function FrameworkProgramListCard({
0 ? (
- Fähigkeiten (Vereinsvergleich) + Fähigkeiten (Rahmenprogramme)

- 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.

{catalogSkills.map((sk) => ( @@ -312,7 +312,7 @@ export default function FrameworkProgramsFilterBlock({ {(filters.skillIds || []).length > 0 ? (
- +