Files
LehrerApp/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
T
admin febcb0855a Notenverwaltung (Kapitel 2) und Mitarbeits-Assistent
Notenübersicht der Gruppe (Matrix, Gesamt-Spalte, Sortierung, Halbjahresfilter),
Einzelnoten-Pflege samt Sammelerfassung, Gewichtungsschema mit Voreinstellung je
Gruppentyp, Zeugnisnoten-Berechnung mit Übersteuern/Festschreiben/Export und
Notenentwicklung im Schülerdetail.

Dazu Anwesenheits-/Hausaufgaben-Tracking je Mitarbeit-Sitzung, ein neuer
Mitarbeits-Assistent (Zeitleiste mit Abschnitten, automatischer Notenvorschlag,
Zusammenzug zur Halbjahresnote) und eine Dashboard-Kachel für offene
Entschuldigungen. Außerdem: verbliebene englische Begriffe in Auswahlfeldern
und Buttons auf Deutsch umgestellt.
2026-08-12 19:19:22 +02:00

701 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
// ── Gruppenliste ──────────────────────────────────────────────────────────────
public partial class GroupListViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
public Action<Guid, int>? OnNavigateToDetail { get; set; }
public Func<Task>? OnAddGroup { get; set; }
public Func<Guid, Task>? OnEditGroup { get; set; }
public Func<GroupListItem, Task<bool>>? OnConfirmDelete { get; set; }
[ObservableProperty] private string _selectedSchoolYear = "";
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private GroupListItem? _selectedGroup;
[ObservableProperty] private bool _showArchived;
public string SelectedGroupDisplayName => SelectedGroup?.DisplayName ?? "";
public string SelectedGroupSubtitle => SelectedGroup?.Subtitle ?? "";
public string ListSummary => ShowArchived
? $"{Groups.Count} archivierte Gruppen · {SelectedSchoolYear}"
: $"{Groups.Count} aktive Gruppen · {SelectedSchoolYear}";
public bool HasNoGroups => Groups.Count == 0;
public string EmptyListMessage => ShowArchived
? "Keine archivierten Lerngruppen in diesem Schuljahr."
: "Noch keine aktiven Lerngruppen in diesem Schuljahr.";
public ObservableCollection<string> SchoolYears { get; } = [];
public ObservableCollection<GroupListItem> Groups { get; } = [];
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy)
{
_groups = groups; _subjects = subjects;
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
SelectedSchoolYear = sy.CurrentSchoolYear();
}
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
partial void OnSearchTextChanged(string value) => LoadGroups();
partial void OnShowArchivedChanged(bool value) => LoadGroups();
partial void OnSelectedGroupChanged(GroupListItem? value)
{
OnPropertyChanged(nameof(SelectedGroupDisplayName));
OnPropertyChanged(nameof(SelectedGroupSubtitle));
NavigateToSectionCommand.NotifyCanExecuteChanged();
EditGroupCommand.NotifyCanExecuteChanged();
ToggleArchiveCommand.NotifyCanExecuteChanged();
DeleteGroupCommand.NotifyCanExecuteChanged();
}
public void LoadGroups()
{
var selectedId = SelectedGroup?.Id;
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.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, g.SubjectId is Guid id ? subjectNames.GetValueOrDefault(id, "") : ""));
SelectedGroup = Groups.FirstOrDefault(g => g.Id == selectedId);
OnPropertyChanged(nameof(ListSummary));
OnPropertyChanged(nameof(HasNoGroups));
OnPropertyChanged(nameof(EmptyListMessage));
}
[RelayCommand]
private async Task AddGroup()
{
if (OnAddGroup is null) return;
await OnAddGroup();
LoadGroups();
}
[RelayCommand] private void Refresh() => LoadGroups();
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
private async Task EditGroup()
{
if (SelectedGroup is null || OnEditGroup is null) return;
var id = SelectedGroup.Id;
await OnEditGroup(id);
LoadGroups();
SelectedGroup = Groups.FirstOrDefault(g => g.Id == id);
}
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
private void ToggleArchive()
{
if (SelectedGroup is null) return;
var group = _groups.GetById(SelectedGroup.Id);
if (group is null) return;
group.IsActive = !group.IsActive;
_groups.Save(group);
LoadGroups();
}
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
private async Task DeleteGroup()
{
if (SelectedGroup is null || OnConfirmDelete is null) return;
var selected = SelectedGroup;
if (!await OnConfirmDelete(selected)) return;
_groups.Delete(selected.Id);
LoadGroups();
}
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
private void NavigateToSection(string? tabIndex)
{
if (SelectedGroup is null || !int.TryParse(tabIndex, out var tab)) return;
OnNavigateToDetail?.Invoke(SelectedGroup.Id, tab);
}
private bool HasSelectedGroup() => SelectedGroup is not null;
}
public class GroupListItem
{
public Guid Id { get; }
public string Name { get; }
public string Subject { get; }
public string DisplayName { get; }
public string TypeLabel { get; }
public string GradingLabel { get; }
public string Subtitle { get; }
public bool IsActive { get; }
public string ArchiveActionLabel => IsActive ? "Archivieren" : "Wieder aktivieren";
public GroupListItem(LearningGroup g, string subjectName)
{
Id = g.Id;
IsActive = g.IsActive;
Name = g.Name;
Subject = subjectName;
TypeLabel = g.Type == GroupType.Class ? "Klasse" : "Kurs";
GradingLabel = g.GradingSystem == GradingSystem.Grades1To6 ? "16" : "015";
DisplayName = string.IsNullOrEmpty(subjectName) ? g.Name : $"{g.Name} · {subjectName}";
Subtitle = $"{TypeLabel} · Stufe {g.GradeLevel} · Noten {GradingLabel} · {g.SchoolYear}";
}
}
// ── Gruppendetail ─────────────────────────────────────────────────────────────
public partial class GroupDetailViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly IStudentRepository _students;
private readonly IGroupMembershipRepository _memberships;
private readonly ISubjectRepository _subjects;
private readonly IExamRepository _exams;
private readonly IGradeRepository _grades;
[ObservableProperty] private LearningGroup? _group;
[ObservableProperty] private string _groupTitle = "";
[ObservableProperty] private string _groupSubtitle = "";
[ObservableProperty] private int _studentCount;
[ObservableProperty] private int _activeTabIndex = 0;
[ObservableProperty] private StudentSummary? _selectedStudent;
[ObservableProperty] private ExamSummary? _selectedExam;
// Eigene Property statt "Group.IsDifferentiated" im Binding-Pfad: die View bindet bereits,
// 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));
public ObservableCollection<StudentSummary> Students { get; } = [];
public ObservableCollection<ExamSummary> Exams { get; } = [];
public ParticipationTabViewModel ParticipationTab { get; }
public GradeOverviewTabViewModel GradeOverviewTab { get; }
public Func<Task<bool>>? OnAddStudent { get; set; }
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
public Func<Exam, Task<bool>>? OnEditExam { get; set; }
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
public Func<ExamSummary, Task<bool>>? OnConfirmDeleteExam { get; set; }
public Func<Exam, Task>? OnGradeExam { get; set; }
public Func<Exam, Task>? OnEvaluateExam { get; set; }
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
IGroupMembershipRepository memberships, ISubjectRepository subjects,
IExamRepository exams, IGradeRepository grades,
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab)
{
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
_exams = exams; _grades = grades;
ParticipationTab = participationTab;
GradeOverviewTab = gradeOverviewTab;
}
public void LoadGroup(Guid id)
{
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")} · " +
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "16" : "015")}";
LoadStudents();
ReloadExams();
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle);
}
private void ReloadExams()
{
if (Group is null) return;
var selectedId = SelectedExam?.Id;
Exams.Clear();
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
SelectedExam = Exams.FirstOrDefault(e => e.Id == selectedId);
}
public void LoadStudents()
{
if (Group is null) return;
Students.Clear();
var enrolled = _students.GetByGroup(Group.Id);
var memberships = _memberships.GetByGroup(Group.Id).ToDictionary(e => e.StudentId);
StudentCount = enrolled.Count;
foreach (var s in enrolled)
{
memberships.TryGetValue(s.Id, out var membership);
var summary = new StudentSummary(s, membership) { OnChanged = SaveStudentNiveau };
Students.Add(summary);
}
}
private void SaveStudentNiveau(StudentSummary summary)
{
if (Group is null) return;
var membership = _memberships.GetByStudentAndGroup(summary.Id, Group.Id);
if (membership is null) return;
membership.Niveau = summary.Niveau;
_memberships.Save(membership);
}
[RelayCommand]
private async Task AddStudent()
{
if (OnAddStudent is null) return;
var confirmed = await OnAddStudent();
if (confirmed)
{
LoadStudents();
ParticipationTab.RefreshCurrentGrid();
}
}
[RelayCommand(CanExecute = nameof(HasSelectedStudent))]
private void RemoveStudent()
{
if (Group is null || SelectedStudent is null) return;
var membership = _memberships.GetByStudentAndGroup(SelectedStudent.Id, Group.Id);
if (membership is null) return;
_memberships.Delete(membership.Id);
LoadStudents();
SelectedStudent = null;
ParticipationTab.RefreshCurrentGrid();
}
partial void OnSelectedStudentChanged(StudentSummary? value) =>
RemoveStudentCommand.NotifyCanExecuteChanged();
private bool HasSelectedStudent() => SelectedStudent is not null;
[RelayCommand]
private async Task AddExam()
{
if (Group is null || OnAddExam is null) return;
var saved = await OnAddExam(Group.Id);
if (saved) ReloadExams();
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
private async Task EditExam()
{
if (SelectedExam is null || OnEditExam is null) return;
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
var id = exam.Id;
var saved = await OnEditExam(exam);
if (saved)
{
ReloadExams();
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
}
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
private async Task GradeExam()
{
if (SelectedExam is null || OnGradeExam is null) return;
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
await OnGradeExam(exam);
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
private async Task EvaluateExam()
{
if (SelectedExam is null || OnEvaluateExam is null) return;
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
await OnEvaluateExam(exam);
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
private async Task DuplicateExam()
{
if (SelectedExam is null || OnDuplicateExam is null) return;
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
var saved = await OnDuplicateExam(exam);
if (saved) ReloadExams();
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
private async Task DeleteExam()
{
if (SelectedExam is null || OnConfirmDeleteExam is null) return;
var selected = SelectedExam;
if (!await OnConfirmDeleteExam(selected)) return;
_exams.Delete(selected.Id);
SelectedExam = null;
ReloadExams();
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
private void AdvanceExamStatus()
{
if (SelectedExam is null) return;
SetExamStatus(SelectedExam.Status switch
{
ExamStatus.Planned => ExamStatus.Conducted,
ExamStatus.Conducted => ExamStatus.Graded,
ExamStatus.Graded => ExamStatus.Returned,
_ => SelectedExam.Status,
});
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
private void SetExamStatus(ExamStatus status)
{
if (SelectedExam is null) return;
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
exam.Status = status;
if (status == ExamStatus.Returned)
exam.ReturnedAt ??= DateOnly.FromDateTime(DateTime.Today);
else
exam.ReturnedAt = null;
_exams.Save(exam);
var id = exam.Id;
ReloadExams();
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
}
partial void OnSelectedExamChanged(ExamSummary? value)
{
EditExamCommand.NotifyCanExecuteChanged();
GradeExamCommand.NotifyCanExecuteChanged();
EvaluateExamCommand.NotifyCanExecuteChanged();
DuplicateExamCommand.NotifyCanExecuteChanged();
DeleteExamCommand.NotifyCanExecuteChanged();
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
SetExamStatusCommand.NotifyCanExecuteChanged();
}
private bool HasSelectedExam() => SelectedExam is not null;
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
}
// ── Niveau-Anzeige (Niveaudifferenzierung) ────────────────────────────────────
public static class NiveauDisplay
{
public static string[] Options { get; } = ["", "E-Niveau", "G-Niveau", "Förderniveau"];
public static string ToName(Niveau? n) => n switch
{
Core.Models.Niveau.E => "E-Niveau",
Core.Models.Niveau.G => "G-Niveau",
Core.Models.Niveau.Foerder => "Förderniveau",
_ => "",
};
public static Niveau? FromName(string? name) => name switch
{
"E-Niveau" => Core.Models.Niveau.E,
"G-Niveau" => Core.Models.Niveau.G,
"Förderniveau" => Core.Models.Niveau.Foerder,
_ => null,
};
}
public partial class StudentSummary : ObservableObject
{
public static string[] NiveauOptions => NiveauDisplay.Options;
public Guid Id { get; }
public string FullName { get; }
public string PeriodLabel { get; }
[ObservableProperty] private Niveau? _niveau;
public string NiveauName
{
get => NiveauDisplay.ToName(Niveau);
set => Niveau = NiveauDisplay.FromName(value);
}
public Action<StudentSummary>? OnChanged { get; set; }
public StudentSummary(Core.Models.Student s, GroupMembership? membership)
{
Id = s.Id;
FullName = s.FullName;
PeriodLabel = membership?.Period switch
{
MembershipPeriod.H1Only => "H1",
MembershipPeriod.H2Only => "H2",
MembershipPeriod.Custom => BuildCustomLabel(membership),
_ => "",
};
_niveau = membership?.Niveau;
}
partial void OnNiveauChanged(Niveau? value)
{
OnPropertyChanged(nameof(NiveauName));
OnChanged?.Invoke(this);
}
private static string BuildCustomLabel(GroupMembership membership)
{
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";
}
}
public class ExamSummary
{
public Guid Id { get; }
public string Title { get; }
public string Date { get; }
public ExamStatus Status { get; }
public string StatusLabel { get; }
public string StatusColorHex { get; }
public string ReturnedAtDisplay { get; }
public string NiveauLabel { get; }
public ExamSummary(Core.Models.Exam e)
{
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status;
ReturnedAtDisplay = e.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
NiveauLabel = e.Niveau.HasValue ? NiveauDisplay.ToName(e.Niveau) : "alle";
StatusLabel = e.Status switch
{
ExamStatus.Planned => "Geplant",
ExamStatus.Conducted => "Durchgeführt",
ExamStatus.Graded => "Korrigiert",
ExamStatus.Returned => "Zurückgegeben",
_ => "",
};
StatusColorHex = e.Status switch
{
ExamStatus.Planned => "#9E9E9E",
ExamStatus.Conducted => "#FB8C00",
ExamStatus.Graded => "#43A047",
ExamStatus.Returned => "#1E88E5",
_ => "#9E9E9E",
};
}
}
// ── Dialog: Schüler zur Gruppe hinzufügen ─────────────────────────────────────
public partial class AddStudentToGroupDialogViewModel : ObservableObject
{
private readonly IStudentRepository _students;
private readonly IGroupMembershipRepository _memberships;
private readonly Guid _groupId;
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private StudentPickerItem? _selectedStudent;
[ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private MembershipPeriod _period = MembershipPeriod.FullYear;
[ObservableProperty] private string _joinedAtText = "";
[ObservableProperty] private string _leftAtText = "";
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 GroupMembership? Result { get; private set; }
public AddStudentToGroupDialogViewModel(IStudentRepository students,
IGroupMembershipRepository memberships, Guid groupId)
{
_students = students; _memberships = memberships;
_groupId = groupId;
LoadAvailableStudents();
}
partial void OnPeriodChanged(MembershipPeriod value)
{
OnPropertyChanged(nameof(IsFullYear));
OnPropertyChanged(nameof(IsH1Only));
OnPropertyChanged(nameof(IsH2Only));
OnPropertyChanged(nameof(IsCustom));
OnPropertyChanged(nameof(IsCustomPeriod));
}
partial void OnSearchTextChanged(string value) => LoadAvailableStudents();
private void LoadAvailableStudents()
{
var alreadyAssigned = _memberships.GetByGroup(_groupId)
.Select(e => e.StudentId).ToHashSet();
var all = _students.GetAll();
var available = all
.Where(s => !alreadyAssigned.Contains(s.Id))
.Where(s => string.IsNullOrWhiteSpace(SearchText) ||
s.FullName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
AvailableStudents.Clear();
foreach (var s in available) AvailableStudents.Add(new StudentPickerItem(s));
}
[RelayCommand]
private void Save()
{
if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; }
DateOnly? joinedAt = null, leftAt = null;
if (Period == MembershipPeriod.Custom)
{
if (!string.IsNullOrWhiteSpace(JoinedAtText) &&
DateOnly.TryParseExact(JoinedAtText, "dd.MM.yyyy",
null, System.Globalization.DateTimeStyles.None, out var j))
joinedAt = j;
if (!string.IsNullOrWhiteSpace(LeftAtText) &&
DateOnly.TryParseExact(LeftAtText, "dd.MM.yyyy",
null, System.Globalization.DateTimeStyles.None, out var l))
leftAt = l;
}
Result = new GroupMembership
{
StudentId = SelectedStudent.Id,
GroupId = _groupId,
Period = Period,
JoinedAt = joinedAt,
LeftAt = leftAt,
};
_memberships.Save(Result);
}
}
public class StudentPickerItem
{
public Guid Id { get; }
public string FullName { get; }
public StudentPickerItem(Student s) { Id = s.Id; FullName = s.FullName; }
}
// ── Dialog: Neue Lerngruppe anlegen ──────────────────────────────────────────
public partial class AddGroupDialogViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private List<Subject> _allSubjects = [];
private LearningGroup? _editingGroup;
public List<string> TypeOptions { get; } = ["Klasse", "Kurs"];
[ObservableProperty] private string _selectedTypeName = "Kurs";
public bool IsKurs => SelectedTypeName == "Kurs";
public string NameLabel => IsKurs ? "Kursname *" : "Klassenname *";
public string NameHint => IsKurs ? "z.B. Q1, LK, GK" : "z.B. 10E, 5a, 11b";
partial void OnSelectedTypeNameChanged(string value)
{
OnPropertyChanged(nameof(IsKurs));
OnPropertyChanged(nameof(NameLabel));
OnPropertyChanged(nameof(NameHint));
}
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _subject = "";
[ObservableProperty] private int _gradeLevel = 10;
[ObservableProperty] private string _selectedGradingName = "Noten 16";
[ObservableProperty] private string _selectedSchoolYear = "";
[ObservableProperty] private int? _hoursPerWeek;
[ObservableProperty] private bool _isOwnClass;
[ObservableProperty] private bool _isDifferentiated;
[ObservableProperty] private string _validationMessage = "";
public List<string> SchoolYears { get; }
public List<string> GradingOptions { get; } = ["Noten 16", "Punkte 015"];
public List<string> KnownSubjectNames { get; private set; } = [];
public LearningGroup? Result { get; private set; }
public string DialogTitle => _editingGroup is null ? "Neue Lerngruppe anlegen" : "Lerngruppe bearbeiten";
public string SaveButtonText => _editingGroup is null ? "Anlegen" : "Speichern";
public AddGroupDialogViewModel(IGroupRepository groups, ISubjectRepository subjects,
SchoolYearService sy)
{
_groups = groups; _subjects = subjects;
_allSubjects = subjects.GetAll();
KnownSubjectNames = _allSubjects.Select(s => s.Name).ToList();
SchoolYears = sy.RecentSchoolYears(3);
SelectedSchoolYear = sy.CurrentSchoolYear();
PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(GradeLevel))
SelectedGradingName = GradeLevel >= 11 ? "Punkte 015" : "Noten 16";
};
}
public void LoadForEdit(LearningGroup group)
{
_editingGroup = group;
SelectedTypeName = group.Type == GroupType.Class ? "Klasse" : "Kurs";
Name = group.Name;
Subject = group.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
GradeLevel = group.GradeLevel;
SelectedGradingName = group.GradingSystem == GradingSystem.Grades1To6
? "Noten 16" : "Punkte 015";
SelectedSchoolYear = group.SchoolYear;
HoursPerWeek = group.HoursPerWeek;
IsOwnClass = group.IsOwnClass;
IsDifferentiated = group.IsDifferentiated;
OnPropertyChanged(nameof(DialogTitle));
OnPropertyChanged(nameof(SaveButtonText));
}
[RelayCommand]
private void Save()
{
if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Bezeichnung erforderlich."; return; }
if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 113."; return; }
string? subjectName = !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null;
Guid? subjectId = null;
if (subjectName is not null)
{
var existing = _allSubjects.FirstOrDefault(
s => s.Name.Equals(subjectName, StringComparison.OrdinalIgnoreCase));
if (existing is not null)
{
subjectId = existing.Id;
}
else
{
var newSubject = new Subject { Name = subjectName };
_subjects.Save(newSubject);
subjectId = newSubject.Id;
}
}
Result = _editingGroup ?? new LearningGroup();
Result.Name = Name.Trim();
Result.SubjectId = subjectId;
Result.Type = IsKurs ? GroupType.Course : GroupType.Class;
Result.GradeLevel = GradeLevel;
Result.GradingSystem = SelectedGradingName == "Punkte 015"
? GradingSystem.Points0To15 : GradingSystem.Grades1To6;
Result.SchoolYear = SelectedSchoolYear;
Result.HoursPerWeek = HoursPerWeek;
Result.IsOwnClass = IsOwnClass;
Result.IsDifferentiated = IsDifferentiated;
_groups.Save(Result);
}
}