diff --git a/backend/routers/skill_profiles.py b/backend/routers/skill_profiles.py index b640213..3467ceb 100644 --- a/backend/routers/skill_profiles.py +++ b/backend/routers/skill_profiles.py @@ -14,12 +14,17 @@ from tenant_context import TenantContext, get_tenant_context, library_content_vi from skill_scoring import ( GRAPH_DEFAULT_ITEM_MINUTES, ExerciseOccurrence, + batch_compute_profiles, + batch_framework_occurrences_by_id, + batch_module_occurrences_by_id, collect_module_exercise_occurrences, collect_progression_graph_exercise_occurrences, collect_unit_exercise_occurrences, + compact_profile_summary, compute_club_corpus_reference, compute_corpus_skill_max_weights, compute_skill_profile, + fetch_exercise_skills_bulk, match_score_for_skill_ids, profile_for_occurrences, reference_scale_meta, @@ -239,33 +244,31 @@ def batch_skill_profile_summaries( include_artifact_summaries=True, ) ref_by_skill = corpus["ref_by_skill"] - all_summaries = corpus.get("artifact_summaries") or {} + allowed_fp: List[int] = [] if fp_ids: - allowed_fp = [] for fid in fp_ids: try: _framework_access(cur, fid, profile_id, role) allowed_fp.append(fid) except HTTPException: pass - for fid in allowed_fp: - key = f"framework_program:{fid}" - if key in all_summaries: - summaries[key] = all_summaries[key] + allowed_mod: List[int] = [] if mod_ids: - allowed_mod = [] for mid in mod_ids: try: _module_access(cur, mid, profile_id, role) allowed_mod.append(mid) except HTTPException: pass - for mid in allowed_mod: - key = f"training_module:{mid}" - if key in all_summaries: - summaries[key] = all_summaries[key] + + summaries = _merge_batch_summaries( + cur, + corpus=corpus, + allowed_fp=allowed_fp, + allowed_mod=allowed_mod, + ) skill_ids_seen: set[int] = set() for summary in summaries.values(): @@ -551,3 +554,100 @@ def _enrich_profile_club_best( def _empty_profile() -> Dict[str, Any]: return compute_skill_profile([], {}) + + +def _summarize_framework_program( + cur, + framework_id: int, + ref_max: Dict[int, float], + ref_by_skill: Dict[int, Dict[str, Any]], +) -> Dict[str, Any]: + cur.execute( + """ + SELECT tu.id + FROM training_framework_slots s + INNER JOIN training_units tu ON tu.framework_slot_id = s.id + WHERE s.framework_program_id = %s + """, + (int(framework_id),), + ) + occ: List[ExerciseOccurrence] = [] + for u in cur.fetchall(): + occ.extend(collect_unit_exercise_occurrences(cur, int(u["id"]))) + prof = ( + profile_for_occurrences(cur, occ, reference_max_by_skill=ref_max) + if occ + else _empty_profile() + ) + _enrich_profile_club_best(prof, ref_by_skill, "framework_program", int(framework_id)) + return compact_profile_summary(prof, ref_by_skill) + + +def _summarize_training_module( + cur, + module_id: int, + ref_max: Dict[int, float], + ref_by_skill: Dict[int, Dict[str, Any]], +) -> Dict[str, Any]: + occ = collect_module_exercise_occurrences(cur, int(module_id)) + prof = ( + profile_for_occurrences(cur, occ, reference_max_by_skill=ref_max) + if occ + else _empty_profile() + ) + _enrich_profile_club_best(prof, ref_by_skill, "training_module", int(module_id)) + return compact_profile_summary(prof, ref_by_skill) + + +def _merge_batch_summaries( + cur, + *, + corpus: 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 {} + out: Dict[str, Dict[str, Any]] = {} + + missing_fp = [fid for fid in allowed_fp if f"framework_program:{fid}" not in 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) + 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) + + missing_mod = [mid for mid in allowed_mod if f"training_module:{mid}" not in 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) + 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) + + for fid in allowed_fp: + key = f"framework_program:{fid}" + if key in cached: + out[key] = cached[key] + elif key not in out: + out[key] = _summarize_framework_program(cur, fid, ref_max, ref_by_skill) + + for mid in allowed_mod: + key = f"training_module:{mid}" + if key in cached: + out[key] = cached[key] + elif key not in out: + out[key] = _summarize_training_module(cur, mid, ref_max, ref_by_skill) + + return out diff --git a/backend/skill_scoring.py b/backend/skill_scoring.py index 9efb3e2..6bf5ce0 100644 --- a/backend/skill_scoring.py +++ b/backend/skill_scoring.py @@ -566,9 +566,10 @@ def compact_profile_summary( profile: Dict[str, Any], ref_by_skill: Optional[Dict[int, Dict[str, Any]]] = None, *, - skills_limit: int = 20, + skills_limit: int = 0, + category_limit: int = 48, ) -> Dict[str, Any]: - """Leichtgewichtiges Profil für Listen — ohne Übungsdetails.""" + """Leichtgewichtiges Profil für Listen — ohne Übungsdetails. skills_limit=0 → alle Fähigkeiten.""" skills_out: List[Dict[str, Any]] = [] for s in profile.get("skills") or []: sid = int(s["skill_id"]) @@ -579,6 +580,7 @@ def compact_profile_summary( "category_name": s.get("category_name") or s.get("category"), "main_category_name": s.get("main_category_name"), "weight": s.get("weight"), + "score": s.get("score") or s.get("weight"), "universal_percent": s.get("universal_percent"), } ref = (ref_by_skill or {}).get(sid) @@ -587,14 +589,14 @@ def compact_profile_summary( if s.get("is_club_best_for_skill"): entry["is_club_best_for_skill"] = True skills_out.append(entry) - if len(skills_out) >= skills_limit: + if skills_limit > 0 and len(skills_out) >= skills_limit: break return { "total_score": profile.get("total_score"), "total_weight": profile.get("total_weight"), "exercise_occurrence_count": profile.get("exercise_occurrence_count"), "skills_count": len(profile.get("skills") or []), - "top_by_category": top_categories_summary(profile), + "top_by_category": top_categories_summary(profile, limit=category_limit), "skills": skills_out, } diff --git a/frontend/src/app.css b/frontend/src/app.css index 1c90601..343fbee 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -2744,12 +2744,17 @@ html.modal-scroll-locked .app-main { -webkit-box-orient: vertical; overflow: hidden; } -.skill-kpi-tile__pct { - font-size: 0.72rem; +.skill-kpi-tile__score { + font-size: 0.78rem; font-weight: 700; - color: var(--accent-dark); + color: var(--text1); margin-top: 2px; } +.skill-kpi-tile__pct { + font-size: 0.68rem; + font-weight: 600; + color: var(--accent-dark); +} .skill-profile__name-cat { font-weight: 500; color: var(--text3); diff --git a/frontend/src/components/skills/SkillProfileCompact.jsx b/frontend/src/components/skills/SkillProfileCompact.jsx index 0aaa7e2..0905752 100644 --- a/frontend/src/components/skills/SkillProfileCompact.jsx +++ b/frontend/src/components/skills/SkillProfileCompact.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { formatClubPercent, kpiRowsFromSummary } from '../../utils/skillProfileListHelpers' +import { formatClubPercent, formatSkillWeight, kpiRowsFromSummary } from '../../utils/skillProfileListHelpers' /** * Kleine KPI-Kacheln: je Unterkategorie die Top-Fähigkeit (Listen/Karten). @@ -8,7 +8,7 @@ export default function SkillProfileCompact({ summary, loading = false, emptyText = 'Keine Fähigkeiten', - displayLimit = 8, + displayLimit = 24, highlightSkillIds = [], }) { if (loading) { @@ -39,10 +39,11 @@ export default function SkillProfileCompact({ (highlighted ? ' skill-kpi-tile--highlight' : '') + (isBest ? ' skill-kpi-tile--best' : '') } - title={`${row.category_name}: ${row.skill_name}`} + title={`${row.category_name}: ${row.skill_name} — Gewicht ${formatSkillWeight(row.weight ?? row.score)}, ${formatClubPercent(row.universal_percent)} vom Vereins-Maximum`} > {row.category_name} {row.skill_name} + {formatSkillWeight(row.weight ?? row.score)} {isBest ? '★ ' : ''} {formatClubPercent(row.universal_percent)} diff --git a/frontend/src/components/skills/SkillProfilePanel.jsx b/frontend/src/components/skills/SkillProfilePanel.jsx index cf87dea..8a3b45c 100644 --- a/frontend/src/components/skills/SkillProfilePanel.jsx +++ b/frontend/src/components/skills/SkillProfilePanel.jsx @@ -244,7 +244,7 @@ export default function SkillProfilePanel({ )} - {displayMode === 'summary' && slots && slots.length > 0 ? ( + {slots && slots.length > 0 ? (
Pro Session