Kompetenzen fast fertig

This commit is contained in:
2026-08-16 02:32:12 +02:00
parent 7c7fe6dfcb
commit 4437f951d9
22 changed files with 979 additions and 29 deletions
@@ -0,0 +1,174 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class CompetencyOverviewTabViewModel : ObservableObject
{
private readonly IUnitRepository _units;
private readonly IExamRepository _exams;
private readonly IExamResultRepository _results;
private readonly ICompetencyDomainRepository _catalog;
private readonly IStudentRepository _students;
private readonly CompetencyAnalysisService _analysis;
private LearningGroup? _group;
[ObservableProperty] private CompetencyStudentOption? _selectedStudent;
[ObservableProperty] private double _reviewThreshold = 60;
[ObservableProperty] private string _summary = "";
[ObservableProperty] private string _emptyMessage = "";
public ObservableCollection<CompetencyStudentOption> StudentOptions { get; } = [];
public ObservableCollection<CompetencyOverviewRow> Rows { get; } = [];
public bool HasRows => Rows.Count > 0;
public bool HasStudents => StudentOptions.Count > 0;
public string ThresholdLabel => $"Wiederholungsbedarf unter {ReviewThreshold:0} %";
public CompetencyOverviewTabViewModel(IUnitRepository units, IExamRepository exams,
IExamResultRepository results, ICompetencyDomainRepository catalog,
IStudentRepository students, CompetencyAnalysisService analysis)
{
_units = units;
_exams = exams;
_results = results;
_catalog = catalog;
_students = students;
_analysis = analysis;
}
public void Initialize(LearningGroup group)
{
_group = group;
Refresh();
}
[RelayCommand]
public void Refresh()
{
if (_group is null) return;
var selectedStudentId = SelectedStudent?.Id;
var domains = _group.SubjectId is Guid subjectId
? _catalog.GetBySubjectAndGrade(subjectId, _group.GradeLevel)
: [];
var units = _units.GetByGroup(_group.Id);
var exams = _exams.GetByGroup(_group.Id);
var results = exams.SelectMany(x => _results.GetByExam(x.Id)).ToList();
var analysis = _analysis.Analyze(domains, units, exams, results);
StudentOptions.Clear();
foreach (var student in _students.GetByGroup(_group.Id)
.OrderBy(x => x.LastName).ThenBy(x => x.FirstName))
StudentOptions.Add(new CompetencyStudentOption(student.Id, student.FullName));
SelectedStudent = StudentOptions.FirstOrDefault(x => x.Id == selectedStudentId)
?? StudentOptions.FirstOrDefault();
Rows.Clear();
foreach (var item in analysis)
Rows.Add(new CompetencyOverviewRow(item, SelectedStudent?.Id, ReviewThreshold));
var treated = analysis.Count(x => x.TaughtUnitCount > 0);
var assessed = analysis.Count(x => x.AssessedExamCount > 0);
var evaluated = analysis.Count(x => x.GroupScore is not null);
Summary = $"{treated} von {analysis.Count} behandelt · {assessed} geprüft · {evaluated} mit Ergebnisdaten";
EmptyMessage = analysis.Count == 0
? "Noch keine Kompetenzen im Katalog oder in Unterrichtseinheiten und Klausuren zugeordnet."
: "";
OnPropertyChanged(nameof(HasRows));
OnPropertyChanged(nameof(HasStudents));
}
partial void OnSelectedStudentChanged(CompetencyStudentOption? value)
{
foreach (var row in Rows) row.SetStudent(value?.Id);
}
partial void OnReviewThresholdChanged(double value)
{
OnPropertyChanged(nameof(ThresholdLabel));
foreach (var row in Rows) row.SetThreshold(value);
}
}
public sealed record CompetencyStudentOption(Guid Id, string DisplayName);
public partial class CompetencyOverviewRow : ObservableObject
{
private readonly CompetencyAnalysisItem _item;
private double _threshold;
[ObservableProperty] private string _studentResult = "";
[ObservableProperty] private string _recommendation = "Noch keine Bewertung";
[ObservableProperty] private string _recommendationColor = "#6B7280";
public string Domain => string.IsNullOrWhiteSpace(_item.DomainCode)
? _item.DomainName
: $"{_item.DomainName} ({_item.DomainCode})";
public string Code => string.IsNullOrWhiteSpace(_item.Code) ? "" : _item.Code;
public string Description => _item.Description;
public string InstructionCoverage => CoverageText(
_item.TaughtUnitCount, "behandelt", _item.PlannedUnitCount, "geplant");
public string ExamCoverage => CoverageText(
_item.AssessedExamCount, "geprüft", _item.PlannedExamCount, "geplant");
public string GroupResult => ScoreText(_item.GroupScore, includeParticipants: true);
public CompetencyOverviewRow(CompetencyAnalysisItem item, Guid? studentId, double threshold)
{
_item = item;
_threshold = threshold;
SetStudent(studentId);
UpdateRecommendation();
}
public void SetStudent(Guid? studentId)
{
StudentResult = studentId is Guid id && _item.StudentScores.TryGetValue(id, out var score)
? ScoreText(score, includeParticipants: false)
: "";
}
public void SetThreshold(double threshold)
{
_threshold = threshold;
UpdateRecommendation();
}
private void UpdateRecommendation()
{
if (_item.GroupScore is null)
{
Recommendation = "Noch keine Bewertung";
RecommendationColor = "#6B7280";
}
else if (_item.GroupScore.Percent < _threshold)
{
Recommendation = "Wiederholungsbedarf";
RecommendationColor = "#C62828";
}
else
{
Recommendation = "Stand solide";
RecommendationColor = "#2E7D32";
}
}
private static string CoverageText(int completed, string completedLabel, int planned, string plannedLabel)
{
if (completed == 0 && planned == 0) return "";
if (completed == 0) return $"{planned}× {plannedLabel}";
if (planned == 0) return $"{completed}× {completedLabel}";
return $"{completed}× {completedLabel} · {planned}× {plannedLabel}";
}
private static string ScoreText(CompetencyScore? score, bool includeParticipants)
{
if (score is null) return "";
var evidence = score.EvidenceCount == 1 ? "1 Aufgabenwert" : $"{score.EvidenceCount} Aufgabenwerte";
return includeParticipants
? $"{score.Percent:0.#} % · {score.ParticipantCount} Schüler · {evidence}"
: $"{score.Percent:0.#} % · {evidence}";
}
}
@@ -206,6 +206,10 @@ public partial class GroupDetailViewModel : ObservableObject
NotifyWriteCommands();
}
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
partial void OnActiveTabIndexChanged(int value)
{
if (value == 6) CompetencyOverviewTab.Refresh();
}
public ObservableCollection<StudentSummary> Students { get; } = [];
public ObservableCollection<ExamSummary> Exams { get; } = [];
@@ -213,6 +217,7 @@ public partial class GroupDetailViewModel : ObservableObject
public ParticipationTabViewModel ParticipationTab { get; }
public GradeOverviewTabViewModel GradeOverviewTab { get; }
public PlanningTabViewModel PlanningTab { get; }
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
public Func<Task<bool>>? OnAddStudent { get; set; }
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
@@ -227,13 +232,14 @@ public partial class GroupDetailViewModel : ObservableObject
IGroupMembershipRepository memberships, ISubjectRepository subjects,
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
PlanningTabViewModel planningTab)
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab)
{
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
_exams = exams; _grades = grades; _tasks = tasks;
ParticipationTab = participationTab;
GradeOverviewTab = gradeOverviewTab;
PlanningTab = planningTab;
CompetencyOverviewTab = competencyOverviewTab;
}
public void LoadGroup(Guid id)
@@ -252,6 +258,7 @@ public partial class GroupDetailViewModel : ObservableObject
ParticipationTab.Initialize(Group.Id, Group.SchoolYear, IsReadOnly);
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
PlanningTab.Initialize(Group.Id, IsReadOnly);
CompetencyOverviewTab.Initialize(Group);
}
private void ReloadExams()
@@ -261,6 +268,7 @@ public partial class GroupDetailViewModel : ObservableObject
Exams.Clear();
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
SelectedExam = Exams.FirstOrDefault(e => e.Id == selectedId);
CompetencyOverviewTab.Refresh();
}
public void LoadStudents()
@@ -368,6 +376,7 @@ public partial class GroupDetailViewModel : ObservableObject
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
await OnGradeExam(exam);
CompetencyOverviewTab.Refresh();
}
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]