diff --git a/LehrerApp.Core/Interfaces/IRepositories.cs b/LehrerApp.Core/Interfaces/IRepositories.cs index 934e897..ca88deb 100644 --- a/LehrerApp.Core/Interfaces/IRepositories.cs +++ b/LehrerApp.Core/Interfaces/IRepositories.cs @@ -13,8 +13,8 @@ public interface IStudentRepository public interface IGroupRepository { LearningGroup? GetById(Guid id); - List GetAll(); - List GetBySchoolYear(string schoolYear); + List GetAll(bool includeInactive = false); + List GetBySchoolYear(string schoolYear, bool includeInactive = false); void Save(LearningGroup group); void Delete(Guid id); } diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs index d9dcc51..88adea0 100644 --- a/LehrerApp.Core/Models/LearningGroup.cs +++ b/LehrerApp.Core/Models/LearningGroup.cs @@ -11,6 +11,7 @@ public class LearningGroup public int GradeLevel { get; set; } public GradingSystem GradingSystem { get; set; } public int? HoursPerWeek { get; set; } + public bool IsActive { get; set; } = true; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } diff --git a/LehrerApp.Data/LiteDbContext.cs b/LehrerApp.Data/LiteDbContext.cs index e794b7c..2fe3c9b 100644 --- a/LehrerApp.Data/LiteDbContext.cs +++ b/LehrerApp.Data/LiteDbContext.cs @@ -16,6 +16,7 @@ public class LiteDbContext : IDisposable { Connection = ConnectionType.Shared, }); + MigrateExistingData(); EnsureIndexes(); } @@ -38,11 +39,25 @@ public class LiteDbContext : IDisposable public void Checkpoint() => _db.Checkpoint(); + private void MigrateExistingData() + { + var groups = _db.GetCollection("groups"); + var missingArchiveState = groups.FindAll() + .Where(g => !g.ContainsKey(nameof(LearningGroup.IsActive))) + .ToList(); + foreach (var group in missingArchiveState) + { + group[nameof(LearningGroup.IsActive)] = true; + groups.Update(group); + } + } + private void EnsureIndexes() { Students.EnsureIndex(x => x.LastName); Students.EnsureIndex(x => x.IsActive); Groups.EnsureIndex(x => x.SchoolYear); + Groups.EnsureIndex(x => x.IsActive); Enrollments.EnsureIndex(x => x.StudentId); Enrollments.EnsureIndex(x => x.GroupId); Enrollments.EnsureIndex(x => x.SchoolYear); diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs index 329654a..42495c1 100644 --- a/LehrerApp.Data/Repositories/AllRepositories.cs +++ b/LehrerApp.Data/Repositories/AllRepositories.cs @@ -23,12 +23,52 @@ public class StudentRepository(LiteDbContext db) : IStudentRepository public class GroupRepository(LiteDbContext db) : IGroupRepository { public LearningGroup? GetById(Guid id) => db.Groups.FindById(id); - public List GetAll() => - db.Groups.FindAll().OrderBy(g => g.SchoolYear).ThenBy(g => g.Name).ToList(); - public List GetBySchoolYear(string schoolYear) => - db.Groups.Find(g => g.SchoolYear == schoolYear).OrderBy(g => g.Name).ToList(); + public List GetAll(bool includeInactive = false) => + (includeInactive ? db.Groups.FindAll() : db.Groups.Find(g => g.IsActive)) + .OrderBy(g => g.SchoolYear).ThenBy(g => g.Name).ToList(); + public List GetBySchoolYear(string schoolYear, bool includeInactive = false) => + (includeInactive + ? db.Groups.Find(g => g.SchoolYear == schoolYear) + : db.Groups.Find(g => g.SchoolYear == schoolYear && g.IsActive)) + .OrderBy(g => g.Name).ToList(); public void Save(LearningGroup g) { g.UpdatedAt = DateTime.UtcNow; db.Groups.Upsert(g); } - public void Delete(Guid id) => db.Groups.Delete(id); + public void Delete(Guid id) + { + foreach (var enrollment in db.Enrollments.Find(e => e.GroupId == id).ToList()) + db.Enrollments.Delete(enrollment.Id); + + foreach (var exam in db.Exams.Find(e => e.GroupId == id).ToList()) + { + foreach (var result in db.ExamResults.Find(r => r.ExamId == exam.Id).ToList()) + db.ExamResults.Delete(result.Id); + db.Exams.Delete(exam.Id); + } + + foreach (var grade in db.Grades.Find(g => g.GroupId == id).ToList()) + db.Grades.Delete(grade.Id); + + foreach (var unit in db.Units.Find(u => u.GroupId == id).ToList()) + { + foreach (var lesson in db.Lessons.Find(l => l.UnitId == unit.Id).ToList()) + db.Lessons.Delete(lesson.Id); + db.Units.Delete(unit.Id); + } + + foreach (var lesson in db.Lessons.Find(l => l.GroupId == id).ToList()) + db.Lessons.Delete(lesson.Id); + + foreach (var session in db.ParticipationSessions.Find(s => s.GroupId == id).ToList()) + { + foreach (var entry in db.ParticipationEntries.Find(e => e.SessionId == session.Id).ToList()) + db.ParticipationEntries.Delete(entry.Id); + db.ParticipationSessions.Delete(session.Id); + } + + foreach (var aspect in db.ParticipationAspects.Find(a => a.GroupId == id).ToList()) + db.ParticipationAspects.Delete(aspect.Id); + + db.Groups.Delete(id); + } } public class EnrollmentRepository(LiteDbContext db) : IEnrollmentRepository diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index 65a7ab4..cd43029 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -16,13 +16,23 @@ public partial class GroupListViewModel : ObservableObject public Action? OnNavigateToDetail { get; set; } public Func? OnAddGroup { get; set; } + public Func? OnEditGroup { get; set; } + public Func>? 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 SchoolYears { get; } = []; public ObservableCollection Groups { get; } = []; @@ -36,22 +46,32 @@ public partial class GroupListViewModel : ObservableObject 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); + var all = _groups.GetBySchoolYear(SelectedSchoolYear, includeInactive: ShowArchived) + .Where(g => g.IsActive != ShowArchived); var filtered = string.IsNullOrWhiteSpace(SearchText) ? all : all.Where(g => g.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase) || (g.Subject?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false)); foreach (var g in filtered.OrderBy(g => g.Name)) Groups.Add(new GroupListItem(g)); + SelectedGroup = Groups.FirstOrDefault(g => g.Id == selectedId); + OnPropertyChanged(nameof(ListSummary)); + OnPropertyChanged(nameof(HasNoGroups)); + OnPropertyChanged(nameof(EmptyListMessage)); } [RelayCommand] @@ -63,6 +83,37 @@ public partial class GroupListViewModel : ObservableObject } [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) { @@ -82,10 +133,13 @@ public class GroupListItem 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) { Id = g.Id; + IsActive = g.IsActive; Name = g.Name; Subject = g.Subject ?? ""; TypeLabel = g.Type == GroupType.Class ? "Klasse" : "Kurs"; @@ -340,7 +394,10 @@ public partial class AddGroupDialogViewModel : ObservableObject private readonly IGroupRepository _groups; private readonly ISubjectRepository _subjects; private readonly SchoolYearService _sy; + private readonly IEnrollmentRepository _enrollments; private List _allSubjects = []; + private LearningGroup? _editingGroup; + private string? _originalSchoolYear; public List TypeOptions { get; } = ["Klasse", "Kurs"]; [ObservableProperty] private string _selectedTypeName = "Kurs"; @@ -359,17 +416,22 @@ public partial class AddGroupDialogViewModel : ObservableObject [ObservableProperty] private string _name = ""; [ObservableProperty] private string _subject = ""; [ObservableProperty] private int _gradeLevel = 10; - [ObservableProperty] private GradingSystem _gradingSystem = GradingSystem.Grades1To6; + [ObservableProperty] private string _selectedGradingName = "Noten 1–6"; [ObservableProperty] private string _selectedSchoolYear = ""; + [ObservableProperty] private int? _hoursPerWeek; [ObservableProperty] private string _validationMessage = ""; public List SchoolYears { get; } + public List GradingOptions { get; } = ["Noten 1–6", "Punkte 0–15"]; public List 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) + public AddGroupDialogViewModel(IGroupRepository groups, ISubjectRepository subjects, + SchoolYearService sy, IEnrollmentRepository enrollments) { - _groups = groups; _subjects = subjects; _sy = sy; + _groups = groups; _subjects = subjects; _sy = sy; _enrollments = enrollments; _allSubjects = subjects.GetAll(); KnownSubjectNames = _allSubjects.Select(s => s.Name).ToList(); SchoolYears = sy.RecentSchoolYears(3); @@ -377,11 +439,26 @@ public partial class AddGroupDialogViewModel : ObservableObject PropertyChanged += (_, e) => { if (e.PropertyName == nameof(GradeLevel)) - GradingSystem = GradeLevel >= 11 ? GradingSystem.Points0To15 - : GradingSystem.Grades1To6; + SelectedGradingName = GradeLevel >= 11 ? "Punkte 0–15" : "Noten 1–6"; }; } + public void LoadForEdit(LearningGroup group) + { + _editingGroup = group; + _originalSchoolYear = group.SchoolYear; + SelectedTypeName = group.Type == GroupType.Class ? "Klasse" : "Kurs"; + Name = group.Name; + Subject = group.Subject ?? ""; + GradeLevel = group.GradeLevel; + SelectedGradingName = group.GradingSystem == GradingSystem.Grades1To6 + ? "Noten 1–6" : "Punkte 0–15"; + SelectedSchoolYear = group.SchoolYear; + HoursPerWeek = group.HoursPerWeek; + OnPropertyChanged(nameof(DialogTitle)); + OnPropertyChanged(nameof(SaveButtonText)); + } + [RelayCommand] private void Save() { @@ -407,16 +484,25 @@ public partial class AddGroupDialogViewModel : ObservableObject } } - Result = new LearningGroup - { - Name = Name.Trim(), - Subject = subjectName, - SubjectId = subjectId, - Type = IsKurs ? GroupType.Course : GroupType.Class, - GradeLevel = GradeLevel, - GradingSystem = GradingSystem, - SchoolYear = SelectedSchoolYear, - }; + Result = _editingGroup ?? new LearningGroup(); + Result.Name = Name.Trim(); + Result.Subject = subjectName; + Result.SubjectId = subjectId; + Result.Type = IsKurs ? GroupType.Course : GroupType.Class; + Result.GradeLevel = GradeLevel; + Result.GradingSystem = SelectedGradingName == "Punkte 0–15" + ? GradingSystem.Points0To15 : GradingSystem.Grades1To6; + Result.SchoolYear = SelectedSchoolYear; + Result.HoursPerWeek = HoursPerWeek; _groups.Save(Result); + + if (_editingGroup is not null && _originalSchoolYear != SelectedSchoolYear) + { + foreach (var enrollment in _enrollments.GetByGroup(Result.Id)) + { + enrollment.SchoolYear = SelectedSchoolYear; + _enrollments.Save(enrollment); + } + } } } diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index 44c0745..00cf914 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -34,7 +34,7 @@ public partial class MainWindowViewModel : ObservableObject ActiveNavItem = item; CurrentPage = item switch { - NavItem.Dashboard => _services.GetRequiredService(), + NavItem.Dashboard => GetDashboard(), NavItem.Groups => _services.GetRequiredService(), NavItem.Students => _services.GetRequiredService(), NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" }, @@ -45,6 +45,13 @@ public partial class MainWindowViewModel : ObservableObject }; } + private DashboardViewModel GetDashboard() + { + var dashboard = _services.GetRequiredService(); + dashboard.RefreshCommand.Execute(null); + return dashboard; + } + public void NavigateToGroupDetail(Guid groupId, int initialTab = 0) { ActiveNavItem = NavItem.Groups; diff --git a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml index 311c10b..45c93a1 100644 --- a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml @@ -3,13 +3,13 @@ xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups" x:Class="LehrerApp.Desktop.Views.Groups.AddGroupDialog" x:DataType="vm:AddGroupDialogViewModel" - Title="Neue Lerngruppe" + Title="{Binding DialogTitle}" Width="420" SizeToContent="Height" CanResize="False" WindowStartupLocation="CenterOwner"> - + @@ -49,13 +49,28 @@ + + + + + + + + + + + + diff --git a/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs b/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs index 19829bb..718610c 100644 --- a/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs +++ b/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs @@ -12,7 +12,11 @@ public partial class GroupListView : UserControl { base.OnDataContextChanged(e); if (DataContext is GroupListViewModel vm) + { vm.OnAddGroup = ShowAddGroupDialog; + vm.OnEditGroup = ShowEditGroupDialog; + vm.OnConfirmDelete = ShowDeleteGroupDialog; + } } private async Task ShowAddGroupDialog() @@ -23,4 +27,25 @@ public partial class GroupListView : UserControl if (owner is not null) await dialog.ShowDialog(owner); } + + private async Task ShowEditGroupDialog(Guid groupId) + { + var group = App.Services.GetRequiredService() + .GetById(groupId); + if (group is null) return; + + var dialogVm = App.Services.GetRequiredService(); + dialogVm.LoadForEdit(group); + var dialog = new AddGroupDialog { DataContext = dialogVm }; + var owner = TopLevel.GetTopLevel(this) as Window; + if (owner is not null) + await dialog.ShowDialog(owner); + } + + private async Task ShowDeleteGroupDialog(GroupListItem group) + { + var dialog = new DeleteGroupDialog { DataContext = group.DisplayName }; + var owner = TopLevel.GetTopLevel(this) as Window; + return owner is not null && await dialog.ShowDialog(owner); + } }