aufräumen

This commit is contained in:
2026-08-12 17:45:02 +02:00
parent 0823a023c9
commit a1e722ace1
27 changed files with 542 additions and 197 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ public static class AppBootstrapper
// ── Repositories ──────────────────────────────────────────────────────
services.AddSingleton<IStudentRepository, StudentRepository>();
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<IEnrollmentRepository, EnrollmentRepository>();
services.AddSingleton<IGroupMembershipRepository, GroupMembershipRepository>();
services.AddSingleton<IExamRepository, ExamRepository>();
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
services.AddSingleton<IGradeRepository, GradeRepository>();
@@ -13,6 +13,7 @@ public partial class DashboardViewModel : ObservableObject
private static readonly CultureInfo De = new("de-DE");
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private readonly ILessonRepository _lessons;
private readonly IExamRepository _exams;
private readonly IWorkTaskRepository _tasks;
@@ -34,10 +35,10 @@ public partial class DashboardViewModel : ObservableObject
// Navigation-Callback wird von App.axaml.cs verdrahtet
public Action<Guid>? OnNavigateToGroup { get; set; }
public DashboardViewModel(IGroupRepository groups, ILessonRepository lessons,
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
IExamRepository exams, IWorkTaskRepository tasks, SchoolYearService sy)
{
_groups = groups; _lessons = lessons; _exams = exams; _tasks = tasks; _sy = sy;
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks; _sy = sy;
Load();
}
@@ -70,7 +71,12 @@ public partial class DashboardViewModel : ObservableObject
CurrentGroups.Clear();
foreach (var g in groups.Values.OrderBy(g => g.Name))
CurrentGroups.Add(new() { GroupId = g.Id, Name = g.Name, Subject = g.Subject ?? "" });
CurrentGroups.Add(new()
{
GroupId = g.Id,
Name = g.Name,
Subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "",
});
CalendarMonth = FirstOfMonth(now);
LoadCalendar();
@@ -24,24 +24,24 @@ public partial class ExamGradingDialogViewModel : ObservableObject
public ObservableCollection<ExamResultRow> Rows { get; } = [];
public ExamGradingDialogViewModel(IExamResultRepository results, IStudentRepository students,
IEnrollmentRepository enrollments, GradingService grading, Exam exam, Guid groupId, string schoolYear)
IGroupMembershipRepository memberships, GradingService grading, Exam exam, Guid groupId)
{
_results = results; _grading = grading; _exam = exam;
Tasks = exam.Tasks.OrderBy(t => t.Nr).ToList();
_examMaxPoints = Tasks.Sum(t => t.MaxPoints);
var enrolled = students.GetByGroup(groupId, schoolYear);
var enrollmentList = enrollments.GetByGroupAndYear(groupId, schoolYear);
var enrolled = students.GetByGroup(groupId);
var membershipList = memberships.GetByGroup(groupId);
var existing = results.GetByExam(exam.Id).ToDictionary(r => r.StudentId);
foreach (var s in enrolled.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
{
var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == s.Id);
if (enrollment is not null && !IsEnrolledAtDate(enrollment, exam.Date)) continue;
var membership = membershipList.FirstOrDefault(e => e.StudentId == s.Id);
if (membership is not null && !IsMemberAtDate(membership, exam.Date)) continue;
// Niveau-Klausur: nur Schüler mit passendem Niveau zeigen. Klausuren ohne
// Niveau-Zuordnung gelten weiterhin für die ganze Gruppe.
if (exam.Niveau.HasValue && enrollment?.Niveau != exam.Niveau) continue;
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue;
existing.TryGetValue(s.Id, out var result);
var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading);
@@ -52,12 +52,12 @@ public partial class ExamGradingDialogViewModel : ObservableObject
private void SaveRow(ExamResultRow row) => _results.Save(row.ToModel(_exam.Id));
private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
{
EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value)
&& (e.LeftAt is null || date <= e.LeftAt.Value),
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
_ => true,
};
}
@@ -289,7 +289,6 @@ public partial class ExamDialogViewModel : ObservableObject
Result = _editingExam ?? new Exam { GroupId = _groupId };
Result.Title = Title.Trim();
Result.Date = date;
Result.Subject = _subjectName.Trim();
Result.ExamNumber = ExamNumber;
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
Result.ReturnedAt = returnedAt;
@@ -12,7 +12,7 @@ namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class GroupListViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly SchoolYearService _sy;
private readonly ISubjectRepository _subjects;
public Action<Guid, int>? OnNavigateToDetail { get; set; }
public Func<Task>? OnAddGroup { get; set; }
@@ -37,9 +37,9 @@ public partial class GroupListViewModel : ObservableObject
public ObservableCollection<string> SchoolYears { get; } = [];
public ObservableCollection<GroupListItem> Groups { get; } = [];
public GroupListViewModel(IGroupRepository groups, SchoolYearService sy)
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy)
{
_groups = groups; _sy = sy;
_groups = groups; _subjects = subjects;
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
SelectedSchoolYear = sy.CurrentSchoolYear();
}
@@ -63,11 +63,13 @@ public partial class GroupListViewModel : ObservableObject
Groups.Clear();
var all = _groups.GetBySchoolYear(SelectedSchoolYear, includeInactive: ShowArchived)
.Where(g => g.IsActive != ShowArchived);
var subjectNames = _subjects.GetAll().ToDictionary(s => s.Id, s => s.Name);
var filtered = string.IsNullOrWhiteSpace(SearchText) ? all
: all.Where(g => g.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| (g.Subject?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false));
|| (g.SubjectId is Guid id && subjectNames.GetValueOrDefault(id, "")
.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
foreach (var g in filtered.OrderBy(g => g.Name))
Groups.Add(new GroupListItem(g));
Groups.Add(new GroupListItem(g, g.SubjectId is Guid id ? subjectNames.GetValueOrDefault(id, "") : ""));
SelectedGroup = Groups.FirstOrDefault(g => g.Id == selectedId);
OnPropertyChanged(nameof(ListSummary));
OnPropertyChanged(nameof(HasNoGroups));
@@ -136,15 +138,15 @@ public class GroupListItem
public bool IsActive { get; }
public string ArchiveActionLabel => IsActive ? "Archivieren" : "Wieder aktivieren";
public GroupListItem(LearningGroup g)
public GroupListItem(LearningGroup g, string subjectName)
{
Id = g.Id;
IsActive = g.IsActive;
Name = g.Name;
Subject = g.Subject ?? "";
Subject = subjectName;
TypeLabel = g.Type == GroupType.Class ? "Klasse" : "Kurs";
GradingLabel = g.GradingSystem == GradingSystem.Grades1To6 ? "16" : "015";
DisplayName = string.IsNullOrEmpty(g.Subject) ? g.Name : $"{g.Name} · {g.Subject}";
DisplayName = string.IsNullOrEmpty(subjectName) ? g.Name : $"{g.Name} · {subjectName}";
Subtitle = $"{TypeLabel} · Stufe {g.GradeLevel} · Noten {GradingLabel} · {g.SchoolYear}";
}
}
@@ -155,7 +157,8 @@ public partial class GroupDetailViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly IStudentRepository _students;
private readonly IEnrollmentRepository _enrollments;
private readonly IGroupMembershipRepository _memberships;
private readonly ISubjectRepository _subjects;
private readonly IExamRepository _exams;
private readonly IGradeRepository _grades;
@@ -171,6 +174,7 @@ public partial class GroupDetailViewModel : ObservableObject
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
public string SubjectName { get; private set; } = "";
partial void OnGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(IsDifferentiated));
@@ -187,10 +191,11 @@ public partial class GroupDetailViewModel : ObservableObject
public Func<Exam, Task>? OnEvaluateExam { get; set; }
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
IGroupMembershipRepository memberships, ISubjectRepository subjects,
IExamRepository exams, IGradeRepository grades,
ParticipationTabViewModel participationTab)
{
_groups = groups; _students = students; _enrollments = enrollments;
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
_exams = exams; _grades = grades;
ParticipationTab = participationTab;
}
@@ -199,6 +204,9 @@ public partial class GroupDetailViewModel : ObservableObject
{
Group = _groups.GetById(id);
if (Group is null) return;
SubjectName = Group.SubjectId is Guid subjectId
? _subjects.GetById(subjectId)?.Name ?? ""
: "";
GroupTitle = Group.Name;
GroupSubtitle = $"{Group.SchoolYear} · " +
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
@@ -221,14 +229,13 @@ public partial class GroupDetailViewModel : ObservableObject
{
if (Group is null) return;
Students.Clear();
var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear);
var enrollments = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
.ToDictionary(e => e.StudentId);
var enrolled = _students.GetByGroup(Group.Id);
var memberships = _memberships.GetByGroup(Group.Id).ToDictionary(e => e.StudentId);
StudentCount = enrolled.Count;
foreach (var s in enrolled)
{
enrollments.TryGetValue(s.Id, out var enr);
var summary = new StudentSummary(s, enr) { OnChanged = SaveStudentNiveau };
memberships.TryGetValue(s.Id, out var membership);
var summary = new StudentSummary(s, membership) { OnChanged = SaveStudentNiveau };
Students.Add(summary);
}
}
@@ -236,11 +243,10 @@ public partial class GroupDetailViewModel : ObservableObject
private void SaveStudentNiveau(StudentSummary summary)
{
if (Group is null) return;
var enrollment = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
.FirstOrDefault(e => e.StudentId == summary.Id);
if (enrollment is null) return;
enrollment.Niveau = summary.Niveau;
_enrollments.Save(enrollment);
var membership = _memberships.GetByStudentAndGroup(summary.Id, Group.Id);
if (membership is null) return;
membership.Niveau = summary.Niveau;
_memberships.Save(membership);
}
[RelayCommand]
@@ -259,10 +265,9 @@ public partial class GroupDetailViewModel : ObservableObject
private void RemoveStudent()
{
if (Group is null || SelectedStudent is null) return;
var enrollment = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
.FirstOrDefault(e => e.StudentId == SelectedStudent.Id);
if (enrollment is null) return;
_enrollments.Delete(enrollment.Id);
var membership = _memberships.GetByStudentAndGroup(SelectedStudent.Id, Group.Id);
if (membership is null) return;
_memberships.Delete(membership.Id);
LoadStudents();
SelectedStudent = null;
ParticipationTab.RefreshCurrentGrid();
@@ -422,18 +427,18 @@ public partial class StudentSummary : ObservableObject
public Action<StudentSummary>? OnChanged { get; set; }
public StudentSummary(Core.Models.Student s, Enrollment? e)
public StudentSummary(Core.Models.Student s, GroupMembership? membership)
{
Id = s.Id;
FullName = s.FullName;
PeriodLabel = e?.Period switch
PeriodLabel = membership?.Period switch
{
EnrollmentPeriod.H1Only => "H1",
EnrollmentPeriod.H2Only => "H2",
EnrollmentPeriod.Custom => BuildCustomLabel(e),
MembershipPeriod.H1Only => "H1",
MembershipPeriod.H2Only => "H2",
MembershipPeriod.Custom => BuildCustomLabel(membership),
_ => "",
};
_niveau = e?.Niveau;
_niveau = membership?.Niveau;
}
partial void OnNiveauChanged(Niveau? value)
@@ -442,11 +447,11 @@ public partial class StudentSummary : ObservableObject
OnChanged?.Invoke(this);
}
private static string BuildCustomLabel(Enrollment e)
private static string BuildCustomLabel(GroupMembership membership)
{
if (e.JoinedAt.HasValue && e.LeftAt.HasValue) return $"{e.JoinedAt:dd.MM.}{e.LeftAt:dd.MM.}";
if (e.JoinedAt.HasValue) return $"ab {e.JoinedAt:dd.MM.}";
if (e.LeftAt.HasValue) return $"bis {e.LeftAt:dd.MM.}";
if (membership.JoinedAt.HasValue && membership.LeftAt.HasValue) return $"{membership.JoinedAt:dd.MM.}{membership.LeftAt:dd.MM.}";
if (membership.JoinedAt.HasValue) return $"ab {membership.JoinedAt:dd.MM.}";
if (membership.LeftAt.HasValue) return $"bis {membership.LeftAt:dd.MM.}";
return "Datum";
}
}
@@ -491,35 +496,34 @@ public class ExamSummary
public partial class AddStudentToGroupDialogViewModel : ObservableObject
{
private readonly IStudentRepository _students;
private readonly IEnrollmentRepository _enrollments;
private readonly IGroupMembershipRepository _memberships;
private readonly Guid _groupId;
private readonly string _schoolYear;
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private StudentPickerItem? _selectedStudent;
[ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private EnrollmentPeriod _period = EnrollmentPeriod.FullYear;
[ObservableProperty] private MembershipPeriod _period = MembershipPeriod.FullYear;
[ObservableProperty] private string _joinedAtText = "";
[ObservableProperty] private string _leftAtText = "";
public bool IsFullYear { get => Period == EnrollmentPeriod.FullYear; set { if (value) Period = EnrollmentPeriod.FullYear; } }
public bool IsH1Only { get => Period == EnrollmentPeriod.H1Only; set { if (value) Period = EnrollmentPeriod.H1Only; } }
public bool IsH2Only { get => Period == EnrollmentPeriod.H2Only; set { if (value) Period = EnrollmentPeriod.H2Only; } }
public bool IsCustom { get => Period == EnrollmentPeriod.Custom; set { if (value) Period = EnrollmentPeriod.Custom; } }
public bool IsCustomPeriod => Period == EnrollmentPeriod.Custom;
public bool IsFullYear { get => Period == MembershipPeriod.FullYear; set { if (value) Period = MembershipPeriod.FullYear; } }
public bool IsH1Only { get => Period == MembershipPeriod.H1Only; set { if (value) Period = MembershipPeriod.H1Only; } }
public bool IsH2Only { get => Period == MembershipPeriod.H2Only; set { if (value) Period = MembershipPeriod.H2Only; } }
public bool IsCustom { get => Period == MembershipPeriod.Custom; set { if (value) Period = MembershipPeriod.Custom; } }
public bool IsCustomPeriod => Period == MembershipPeriod.Custom;
public ObservableCollection<StudentPickerItem> AvailableStudents { get; } = [];
public Enrollment? Result { get; private set; }
public GroupMembership? Result { get; private set; }
public AddStudentToGroupDialogViewModel(IStudentRepository students,
IEnrollmentRepository enrollments, Guid groupId, string schoolYear)
IGroupMembershipRepository memberships, Guid groupId)
{
_students = students; _enrollments = enrollments;
_groupId = groupId; _schoolYear = schoolYear;
_students = students; _memberships = memberships;
_groupId = groupId;
LoadAvailableStudents();
}
partial void OnPeriodChanged(EnrollmentPeriod value)
partial void OnPeriodChanged(MembershipPeriod value)
{
OnPropertyChanged(nameof(IsFullYear));
OnPropertyChanged(nameof(IsH1Only));
@@ -532,11 +536,11 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject
private void LoadAvailableStudents()
{
var alreadyEnrolled = _enrollments.GetByGroupAndYear(_groupId, _schoolYear)
var alreadyAssigned = _memberships.GetByGroup(_groupId)
.Select(e => e.StudentId).ToHashSet();
var all = _students.GetAll();
var available = all
.Where(s => !alreadyEnrolled.Contains(s.Id))
.Where(s => !alreadyAssigned.Contains(s.Id))
.Where(s => string.IsNullOrWhiteSpace(SearchText) ||
s.FullName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
AvailableStudents.Clear();
@@ -549,7 +553,7 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject
if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; }
DateOnly? joinedAt = null, leftAt = null;
if (Period == EnrollmentPeriod.Custom)
if (Period == MembershipPeriod.Custom)
{
if (!string.IsNullOrWhiteSpace(JoinedAtText) &&
DateOnly.TryParseExact(JoinedAtText, "dd.MM.yyyy",
@@ -561,16 +565,15 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject
leftAt = l;
}
Result = new Enrollment
Result = new GroupMembership
{
StudentId = SelectedStudent.Id,
GroupId = _groupId,
SchoolYear = _schoolYear,
Period = Period,
JoinedAt = joinedAt,
LeftAt = leftAt,
};
_enrollments.Save(Result);
_memberships.Save(Result);
}
}
@@ -587,11 +590,8 @@ public partial class AddGroupDialogViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private readonly SchoolYearService _sy;
private readonly IEnrollmentRepository _enrollments;
private List<Subject> _allSubjects = [];
private LearningGroup? _editingGroup;
private string? _originalSchoolYear;
public List<string> TypeOptions { get; } = ["Klasse", "Kurs"];
[ObservableProperty] private string _selectedTypeName = "Kurs";
@@ -625,9 +625,9 @@ public partial class AddGroupDialogViewModel : ObservableObject
public string SaveButtonText => _editingGroup is null ? "Anlegen" : "Speichern";
public AddGroupDialogViewModel(IGroupRepository groups, ISubjectRepository subjects,
SchoolYearService sy, IEnrollmentRepository enrollments)
SchoolYearService sy)
{
_groups = groups; _subjects = subjects; _sy = sy; _enrollments = enrollments;
_groups = groups; _subjects = subjects;
_allSubjects = subjects.GetAll();
KnownSubjectNames = _allSubjects.Select(s => s.Name).ToList();
SchoolYears = sy.RecentSchoolYears(3);
@@ -642,10 +642,9 @@ public partial class AddGroupDialogViewModel : ObservableObject
public void LoadForEdit(LearningGroup group)
{
_editingGroup = group;
_originalSchoolYear = group.SchoolYear;
SelectedTypeName = group.Type == GroupType.Class ? "Klasse" : "Kurs";
Name = group.Name;
Subject = group.Subject ?? "";
Subject = group.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
GradeLevel = group.GradeLevel;
SelectedGradingName = group.GradingSystem == GradingSystem.Grades1To6
? "Noten 16" : "Punkte 015";
@@ -684,7 +683,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
Result = _editingGroup ?? new LearningGroup();
Result.Name = Name.Trim();
Result.Subject = subjectName;
Result.SubjectId = subjectId;
Result.Type = IsKurs ? GroupType.Course : GroupType.Class;
Result.GradeLevel = GradeLevel;
@@ -695,14 +693,5 @@ public partial class AddGroupDialogViewModel : ObservableObject
Result.IsOwnClass = IsOwnClass;
Result.IsDifferentiated = IsDifferentiated;
_groups.Save(Result);
if (_editingGroup is not null && _originalSchoolYear != SelectedSchoolYear)
{
foreach (var enrollment in _enrollments.GetByGroup(Result.Id))
{
enrollment.SchoolYear = SelectedSchoolYear;
_enrollments.Save(enrollment);
}
}
}
}
@@ -16,7 +16,7 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
private readonly IParticipationRepository _entries;
private readonly IParticipationAspectRepository _aspects;
private readonly IStudentRepository _students;
private readonly IEnrollmentRepository _enrollments;
private readonly IGroupMembershipRepository _memberships;
private readonly IGradeRepository _grades;
private readonly GradingService _grading;
private readonly Guid _groupId;
@@ -39,11 +39,11 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
public ParticipationGradeDialogViewModel(
IParticipationSessionRepository sessions, IParticipationRepository entries,
IParticipationAspectRepository aspects, IStudentRepository students,
IEnrollmentRepository enrollments, IGradeRepository grades, GradingService grading,
IGroupMembershipRepository memberships, IGradeRepository grades, GradingService grading,
Guid groupId, string schoolYear, GradingSystem gradingSystem)
{
_sessions = sessions; _entries = entries; _aspects = aspects;
_students = students; _enrollments = enrollments; _grades = grades;
_students = students; _memberships = memberships; _grades = grades;
_grading = grading; _groupId = groupId; _schoolYear = schoolYear;
_gradingSystem = gradingSystem;
@@ -83,14 +83,14 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
.OrderBy(s => s.Date)
.ToList();
var studentList = _students.GetByGroup(_groupId, _schoolYear);
var enrollmentList = _enrollments.GetByGroupAndYear(_groupId, _schoolYear);
var studentList = _students.GetByGroup(_groupId);
var membershipList = _memberships.GetByGroup(_groupId);
foreach (var student in studentList.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
{
var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == student.Id);
var membership = membershipList.FirstOrDefault(e => e.StudentId == student.Id);
var relevantSessions = sessions
.Where(s => enrollment is null || IsEnrolledAtDate(enrollment, s.Date))
.Where(s => membership is null || IsMemberAtDate(membership, s.Date))
.ToList();
var points = new List<(DateOnly Date, double Rating)>();
@@ -132,7 +132,6 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
{
StudentId = row.StudentId,
GroupId = _groupId,
SchoolYear = _schoolYear,
Category = GradeCategory.Participation,
Note = noteTag,
};
@@ -153,12 +152,12 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
_ => true,
};
private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
{
EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value)
&& (e.LeftAt is null || date <= e.LeftAt.Value),
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
_ => true,
};
}
@@ -15,7 +15,7 @@ public partial class ParticipationTabViewModel : ObservableObject
private readonly IParticipationRepository _entries;
private readonly IParticipationAspectRepository _aspects;
private readonly IStudentRepository _students;
private readonly IEnrollmentRepository _enrollments;
private readonly IGroupMembershipRepository _memberships;
private readonly IGroupRepository _groups;
private readonly ICompetencyDomainRepository _competencyDomains;
@@ -53,13 +53,13 @@ public partial class ParticipationTabViewModel : ObservableObject
IParticipationRepository entries,
IParticipationAspectRepository aspects,
IStudentRepository students,
IEnrollmentRepository enrollments,
IGroupMembershipRepository memberships,
IGroupRepository groups,
ICompetencyDomainRepository competencyDomains)
{
_sessions = sessions; _entries = entries;
_aspects = aspects; _students = students;
_enrollments = enrollments; _groups = groups;
_memberships = memberships; _groups = groups;
_competencyDomains = competencyDomains;
}
@@ -128,18 +128,18 @@ public partial class ParticipationTabViewModel : ObservableObject
StudentRows.Clear();
var session = _sessions.GetById(sessionId);
var sessionDate = session?.Date ?? DateOnly.FromDateTime(DateTime.Today);
var students = _students.GetByGroup(_groupId, _schoolYear);
var enrollments = _enrollments.GetByGroupAndYear(_groupId, _schoolYear);
var students = _students.GetByGroup(_groupId);
var memberships = _memberships.GetByGroup(_groupId);
var entries = _entries.GetBySession(sessionId);
foreach (var s in students)
{
var enrollment = enrollments.FirstOrDefault(e => e.StudentId == s.Id);
if (enrollment is not null && !IsEnrolledAtDate(enrollment, sessionDate))
var membership = memberships.FirstOrDefault(e => e.StudentId == s.Id);
if (membership is not null && !IsMemberAtDate(membership, sessionDate))
continue;
var entry = entries.FirstOrDefault(e => e.StudentId == s.Id)
?? new ParticipationEntry { SessionId = sessionId, GroupId = _groupId, StudentId = s.Id };
?? new ParticipationEntry { SessionId = sessionId, StudentId = s.Id };
var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList(), ActiveCompetencyCodes);
row.OnRatingChanged = (sid, key, val) => SaveRating(sessionId, sid, key, val);
row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val);
@@ -149,25 +149,22 @@ public partial class ParticipationTabViewModel : ObservableObject
RebuildColumnsSignal++;
}
private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
{
EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value)
&& (e.LeftAt is null || date <= e.LeftAt.Value),
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
_ => true,
};
private void SaveRating(Guid sessionId, Guid studentId, string key, int? value)
{
var session = _sessions.GetById(sessionId);
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
?? new ParticipationEntry
{
SessionId = sessionId,
GroupId = _groupId,
StudentId = studentId,
Date = session?.Date ?? DateOnly.FromDateTime(DateTime.Today),
};
var existing = entry.Ratings.FirstOrDefault(r => r.Key == key);
@@ -280,14 +277,11 @@ public partial class ParticipationTabViewModel : ObservableObject
private void SaveCompetencyRating(Guid sessionId, Guid studentId, string code, int? value)
{
var session = _sessions.GetById(sessionId);
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
?? new ParticipationEntry
{
SessionId = sessionId,
GroupId = _groupId,
StudentId = studentId,
Date = session?.Date ?? DateOnly.FromDateTime(DateTime.Today),
};
var existing = entry.CompetencyRatings.FirstOrDefault(r => r.Code == code);
if (value is null)
@@ -110,7 +110,15 @@ public partial class SettingsViewModel : ObservableObject
private void AddSubject()
{
if (string.IsNullOrWhiteSpace(NewName)) { ValidationMessage = "Name erforderlich."; return; }
_subjects.Save(new Subject { Name = NewName.Trim(), ShortName = NewShort.Trim() });
try
{
_subjects.Save(new Subject { Name = NewName.Trim(), ShortName = NewShort.Trim() });
}
catch (InvalidOperationException ex)
{
ValidationMessage = ex.Message;
return;
}
NewName = ""; NewShort = ""; ValidationMessage = "";
LoadSubjects();
}
@@ -119,7 +127,16 @@ public partial class SettingsViewModel : ObservableObject
private void DeleteSubject(SubjectListItem? item)
{
if (item is null) return;
_subjects.Delete(item.Id);
try
{
_subjects.Delete(item.Id);
}
catch (InvalidOperationException ex)
{
ValidationMessage = ex.Message;
return;
}
ValidationMessage = "";
if (CatalogSubject?.Id == item.Id) CatalogSubject = null;
LoadSubjects();
}
@@ -68,8 +68,9 @@ public class StudentListItem
public partial class StudentDetailViewModel : ObservableObject
{
private readonly IStudentRepository _students;
private readonly IEnrollmentRepository _enrollments;
private readonly IGroupMembershipRepository _memberships;
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private readonly IDocumentationRepository _docs;
[ObservableProperty] private Student? _student;
@@ -79,7 +80,7 @@ public partial class StudentDetailViewModel : ObservableObject
[ObservableProperty] private string _editLastName = "";
[ObservableProperty] private ContactItem? _selectedContact;
public ObservableCollection<EnrollmentEntry> Enrollments { get; } = [];
public ObservableCollection<GroupMembershipEntry> GroupMemberships { get; } = [];
public ObservableCollection<DocEntry> Documentation { get; } = [];
public ObservableCollection<ContactItem> Contacts { get; } = [];
public bool HasNoContacts => Contacts.Count == 0;
@@ -87,11 +88,11 @@ public partial class StudentDetailViewModel : ObservableObject
public Action<ContactItem>? OnViewAddress { get; set; }
public StudentDetailViewModel(IStudentRepository students,
IEnrollmentRepository enrollments, IGroupRepository groups,
IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects,
IDocumentationRepository docs)
{
_students = students; _enrollments = enrollments;
_groups = groups; _docs = docs;
_students = students; _memberships = memberships;
_groups = groups; _subjects = subjects; _docs = docs;
}
public void LoadStudent(Guid id)
@@ -102,12 +103,13 @@ public partial class StudentDetailViewModel : ObservableObject
EditFirstName = Student.FirstName;
EditLastName = Student.LastName;
Enrollments.Clear();
foreach (var e in _enrollments.GetByStudent(Student.Id))
GroupMemberships.Clear();
foreach (var membership in _memberships.GetByStudent(Student.Id))
{
var g = _groups.GetById(e.GroupId);
var g = _groups.GetById(membership.GroupId);
if (g is null) continue;
Enrollments.Add(new() { SchoolYear = e.SchoolYear, GroupName = g.Name, Subject = g.Subject ?? "" });
var subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
GroupMemberships.Add(new() { SchoolYear = g.SchoolYear, GroupName = g.Name, Subject = subject });
}
LoadContacts();
@@ -195,7 +197,7 @@ public partial class StudentDetailViewModel : ObservableObject
private bool CanViewSelectedAddress() => SelectedContact?.HasAddress == true;
}
public class EnrollmentEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
public class GroupMembershipEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
public class DocEntry { public string Date { get; set; } = ""; public string Title { get; set; } = ""; public string TypeLabel { get; set; } = ""; public bool IsConfidential { get; set; } }
public class ContactItem
@@ -32,9 +32,8 @@ public partial class GroupDetailView : UserControl
var dialogVm = new AddStudentToGroupDialogViewModel(
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IEnrollmentRepository>(),
vm.Group.Id,
vm.Group.SchoolYear);
App.Services.GetRequiredService<IGroupMembershipRepository>(),
vm.Group.Id);
var dialog = new AddStudentToGroupDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
@@ -62,7 +61,7 @@ public partial class GroupDetailView : UserControl
App.Services.GetRequiredService<IGradingKeyTemplateRepository>(),
App.Services.GetRequiredService<GradingService>(),
groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem,
vm.Group.Subject ?? "", vm.Group.IsDifferentiated, editingExam, duplicateSource);
vm.SubjectName, vm.Group.IsDifferentiated, editingExam, duplicateSource);
var dialog = new ExamDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
@@ -85,9 +84,9 @@ public partial class GroupDetailView : UserControl
var dialogVm = new ExamGradingDialogViewModel(
App.Services.GetRequiredService<IExamResultRepository>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IEnrollmentRepository>(),
App.Services.GetRequiredService<IGroupMembershipRepository>(),
App.Services.GetRequiredService<GradingService>(),
exam, vm.Group.Id, vm.Group.SchoolYear);
exam, vm.Group.Id);
var dialog = new ExamGradingDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
@@ -153,7 +153,7 @@ public partial class ParticipationTabView : UserControl
App.Services.GetRequiredService<IParticipationRepository>(),
App.Services.GetRequiredService<IParticipationAspectRepository>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IEnrollmentRepository>(),
App.Services.GetRequiredService<IGroupMembershipRepository>(),
App.Services.GetRequiredService<IGradeRepository>(),
App.Services.GetRequiredService<GradingService>(),
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem);
@@ -40,9 +40,9 @@
<ScrollViewer Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="Lerngruppen" FontSize="15" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding Enrollments}">
<ItemsControl ItemsSource="{Binding GroupMemberships}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:EnrollmentEntry">
<DataTemplate DataType="vm:GroupMembershipEntry">
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="6" Padding="12,8" Margin="0,0,0,6">
<Grid ColumnDefinitions="70,*,Auto">
@@ -54,8 +54,8 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine Einschreibungen vorhanden." Opacity="0.4"
IsVisible="{Binding !Enrollments.Count}"/>
<TextBlock Text="Keine Lerngruppenzuordnungen vorhanden." Opacity="0.4"
IsVisible="{Binding !GroupMemberships.Count}"/>
</StackPanel>
</ScrollViewer>
</ContentPage>