Update Skill Profile Summary and Frontend Components
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Modified the `compact_profile_summary` function to allow for dynamic skill and category limits, enhancing flexibility in profile data retrieval. - Updated frontend components to display skill weights and scores more effectively, improving user interaction with skill metrics. - Adjusted CSS styles for skill KPI tiles to better differentiate between score and percentage displays, ensuring a clearer visual representation. - Refactored utility functions to streamline skill summary handling, enhancing overall code maintainability and performance.
This commit is contained in:
parent
9a0cf7f823
commit
34966b9e84
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
>
|
||||
<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)}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ export default function SkillProfilePanel({
|
|||
</>
|
||||
)}
|
||||
|
||||
{displayMode === 'summary' && slots && slots.length > 0 ? (
|
||||
{slots && slots.length > 0 ? (
|
||||
<div className="skill-profile__slots">
|
||||
<span className="skill-profile__slots-label">Pro Session</span>
|
||||
<ul className="skill-profile__slot-list">
|
||||
|
|
@ -270,11 +270,18 @@ export default function SkillProfilePanel({
|
|||
</span>
|
||||
)}
|
||||
</button>
|
||||
{open && sl.profile?.by_main_category?.length > 0 ? (
|
||||
<CategoryGroupedProfile
|
||||
profile={sl.profile}
|
||||
ariaLabel={`Fähigkeiten Session ${(sl.sort_order ?? 0) + 1}`}
|
||||
/>
|
||||
{open && sl.profile?.skills?.length > 0 ? (
|
||||
displayMode === 'full' ? (
|
||||
<FullSkillsProfile
|
||||
profile={sl.profile}
|
||||
ariaLabel={`Alle Fähigkeiten Session ${(sl.sort_order ?? 0) + 1}`}
|
||||
/>
|
||||
) : (
|
||||
<CategoryGroupedProfile
|
||||
profile={sl.profile}
|
||||
ariaLabel={`Fähigkeiten Session ${(sl.sort_order ?? 0) + 1}`}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { formatDurationDisplay, formatSessionDurationRange } from './trainingDur
|
|||
import {
|
||||
frameworkSkillSummaryKey,
|
||||
maxSelectedSkillClubPercent,
|
||||
skillEntryFromSummary,
|
||||
summaryHasSkill,
|
||||
} from './skillProfileListHelpers'
|
||||
|
||||
export function frameworkSessionDurationLabel(row) {
|
||||
|
|
@ -117,7 +117,7 @@ export const EMPTY_FRAMEWORK_IMPORT_FILTERS = {
|
|||
skillIds: [],
|
||||
skillSort: 'title',
|
||||
skillMinClubPercent: 0,
|
||||
skillDisplayLimit: 10,
|
||||
skillDisplayLimit: 24,
|
||||
}
|
||||
|
||||
export function hasActiveFrameworkImportFilters(filters = {}) {
|
||||
|
|
@ -241,14 +241,8 @@ export function filterFrameworkPrograms(rows, filters = {}, skillSummaries = nul
|
|||
if (skillIds.length && skillSummaries) {
|
||||
list = list.filter((r) => {
|
||||
const summary = skillSummaries[frameworkSkillSummaryKey(r.id)]
|
||||
if (!summary) return minClubPct === 0
|
||||
return skillIds.some((sid) => {
|
||||
const sk = skillEntryFromSummary(summary, sid)
|
||||
if (!sk) return false
|
||||
const pct = sk.universal_percent
|
||||
if (pct == null) return minClubPct === 0
|
||||
return pct >= minClubPct
|
||||
})
|
||||
if (!summary) return false
|
||||
return skillIds.some((sid) => summaryHasSkill(summary, sid, minClubPct))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,19 @@ export function moduleSkillSummaryKey(id) {
|
|||
}
|
||||
|
||||
export function skillEntryFromSummary(summary, skillId) {
|
||||
if (!summary?.skills) return null
|
||||
return summary.skills.find((s) => String(s.skill_id) === String(skillId)) || null
|
||||
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 = []) {
|
||||
|
|
@ -30,6 +41,12 @@ export function formatClubPercent(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)
|
||||
}
|
||||
|
||||
/** 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