diff --git a/LehrerApp.Core/Interfaces/IRepositories.cs b/LehrerApp.Core/Interfaces/IRepositories.cs index e225802..b9f2a9a 100644 --- a/LehrerApp.Core/Interfaces/IRepositories.cs +++ b/LehrerApp.Core/Interfaces/IRepositories.cs @@ -29,6 +29,7 @@ public interface IGroupMembershipRepository public interface IExamRepository { Exam? GetById(Guid id); + List GetAll(); List GetByGroup(Guid groupId); void Save(Exam exam); void Delete(Guid id); diff --git a/LehrerApp.Data.Tests/RepositoryTests.cs b/LehrerApp.Data.Tests/RepositoryTests.cs index 48f046f..93cb84e 100644 --- a/LehrerApp.Data.Tests/RepositoryTests.cs +++ b/LehrerApp.Data.Tests/RepositoryTests.cs @@ -224,6 +224,20 @@ public sealed class RepositoryTests Assert.Equal("Früh", result[1].Title); } + [Fact] + public void ExamRepository_GetAll_UeberAlleGruppenHinwegSortiertAufsteigendNachDatum() + { + using var db = NewInMemoryContext(); + var repo = new ExamRepository(db); + repo.Save(new Exam { GroupId = Guid.NewGuid(), Title = "Spät", Date = new DateOnly(2025, 11, 1) }); + repo.Save(new Exam { GroupId = Guid.NewGuid(), Title = "Früh", Date = new DateOnly(2025, 9, 1) }); + + var result = repo.GetAll(); + + Assert.Equal("Früh", result[0].Title); + Assert.Equal("Spät", result[1].Title); + } + // ── SubjectRepository ───────────────────────────────────────────────────── [Fact] diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs index 2793f0f..1d3fd6f 100644 --- a/LehrerApp.Data/Repositories/AllRepositories.cs +++ b/LehrerApp.Data/Repositories/AllRepositories.cs @@ -132,6 +132,7 @@ public class GroupMembershipRepository(LiteDbContext db) : IGroupMembershipRepos public class ExamRepository(LiteDbContext db) : IExamRepository { public Exam? GetById(Guid id) => db.Exams.FindById(id); + public List GetAll() => db.Exams.FindAll().OrderBy(e => e.Date).ToList(); public List GetByGroup(Guid groupId) => db.Exams.Find(e => e.GroupId == groupId).OrderByDescending(e => e.Date).ToList(); public void Save(Exam e) { e.UpdatedAt = DateTime.UtcNow; db.Exams.Upsert(e); } diff --git a/LehrerApp.Desktop.Tests/Fakes.cs b/LehrerApp.Desktop.Tests/Fakes.cs index 291d4bd..7ee0cdb 100644 --- a/LehrerApp.Desktop.Tests/Fakes.cs +++ b/LehrerApp.Desktop.Tests/Fakes.cs @@ -76,6 +76,7 @@ public class FakeGrades : IGradeRepository public class FakeExams(List all) : IExamRepository { public Exam? GetById(Guid id) => all.FirstOrDefault(e => e.Id == id); + public List GetAll() => all; public List GetByGroup(Guid groupId) => all.Where(e => e.GroupId == groupId).ToList(); public void Save(Exam exam) { } public void Delete(Guid id) { } diff --git a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs index c83ea8f..8b637bb 100644 --- a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs @@ -148,13 +148,15 @@ public sealed class TimetableViewModelTests } [Fact] - public void ShowEditor_SchaltetAufBearbeiten_Tab() + public void OpenSettings_RuftOnNavigateToSettingsAuf() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); + var called = false; + vm.OnNavigateToSettings = () => called = true; - vm.ShowEditorCommand.Execute(null); + vm.OpenSettingsCommand.Execute(null); - Assert.Equal(1, vm.ActiveTabIndex); + Assert.True(called); } // ── "Heute"-Ansicht: Wochenraster (Nutzer-Feedback, zweite Iteration) ──────────────────── @@ -742,4 +744,68 @@ public sealed class TimetableViewModelTests var item = Assert.Single(vm.TodayItems); Assert.False(item.HasLessonTopic); } + + // ── Anstehende Klausurtermine (4.4.3) ──────────────────────────────────── + + [Fact] + public void Load_ZeigtKlausurInnerhalbDesVorausblicks() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var group = new LearningGroup { Name = "Q1 Chemie" }; + var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur Nr. 2" }]); + var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); + + var item = Assert.Single(vm.UpcomingExams); + Assert.Equal("Q1 Chemie", item.GroupName); + Assert.Equal("Klausur Nr. 2", item.Title); + Assert.Equal(today.AddDays(10), item.Date); + } + + [Fact] + public void Load_KlausurAusserhalbDesVorausblicks_WirdNichtGezeigt() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var group = new LearningGroup { Name = "Q1 Chemie" }; + var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(30), Title = "Klausur Nr. 3" }]); + var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); + + Assert.Empty(vm.UpcomingExams); + } + + [Fact] + public void Load_VergangeneKlausur_WirdNichtGezeigt() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var group = new LearningGroup { Name = "Q1 Chemie" }; + var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(-1), Title = "Alte Klausur" }]); + var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); + + Assert.Empty(vm.UpcomingExams); + } + + [Fact] + public void Load_MehrereKlausuren_SindNachDatumSortiert() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var group = new LearningGroup { Name = "Q1 Chemie" }; + var exams = new FakeExams( + [ + new Exam { GroupId = group.Id, Date = today.AddDays(15), Title = "Später" }, + new Exam { GroupId = group.Id, Date = today.AddDays(3), Title = "Früher" }, + ]); + var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); + + Assert.Equal(["Früher", "Später"], vm.UpcomingExams.Select(e => e.Title)); + } + + [Fact] + public void Load_HeutigeKlausur_IstImVorausblickEnthalten() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var group = new LearningGroup { Name = "Q1 Chemie" }; + var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today, Title = "Heute" }]); + var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); + + Assert.Single(vm.UpcomingExams); + } } diff --git a/LehrerApp.Desktop/App.axaml.cs b/LehrerApp.Desktop/App.axaml.cs index 79bd861..9cdafac 100644 --- a/LehrerApp.Desktop/App.axaml.cs +++ b/LehrerApp.Desktop/App.axaml.cs @@ -105,8 +105,9 @@ public class App : Application sl.OnNavigateToDetail = id => main.NavigateToStudent(id); sl.OnAddStudent = () => ShowAddStudentDialog(); - // Stundenplan "Heute" → GroupDetail (Tab "Planung") + // Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage") var timetable = Services.GetRequiredService(); + timetable.OnNavigateToSettings = () => main.NavigateToSettings(7); timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5); } diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index 629727b..c9ee08f 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -97,6 +97,14 @@ public partial class MainWindowViewModel : ObservableObject vm.LoadGroup(groupId); // dann Daten laden } + public void NavigateToSettings(int initialTab = 0) + { + ActiveNavItem = NavItem.Settings; + var vm = _services.GetRequiredService(); + vm.ActiveTabIndex = initialTab; + CurrentPage = vm; + } + public void NavigateToStudent(Guid studentId) { ActiveNavItem = NavItem.Students; diff --git a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs index f4deac5..2641360 100644 --- a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs @@ -67,6 +67,7 @@ public partial class TimetableViewModel : ObservableObject public ObservableCollection TodayItems { get; } = []; public ObservableCollection TodaySupervisions { get; } = []; public ObservableCollection TodaySpecialAssignments { get; } = []; + public ObservableCollection UpcomingExams { get; } = []; [ObservableProperty] private int _activeTabIndex; [ObservableProperty] private string _todayLabel = ""; @@ -77,6 +78,7 @@ public partial class TimetableViewModel : ObservableObject public Func? OnEditSlot { get; set; } public Action? OnNavigateToGroup { get; set; } public Func? OnAddSubstitution { get; set; } + public Action? OnNavigateToSettings { get; set; } public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams, @@ -127,6 +129,31 @@ public partial class TimetableViewModel : ObservableObject BuildWeekOverview(today, publicHolidayDates, duties); BuildToday(today); BuildHoursWarnings(); + BuildUpcomingExams(today); + } + + // ── Anstehende Klausurtermine (4.4.3) ──────────────────────────────────── + + /// + /// Mehrtägiger Vorausblick über alle Gruppen hinweg (nicht nur "heute", wie die Tagesliste, + /// und unabhängig von , anders als das Wochenraster) — Abgabefristen + /// sind bewusst nicht enthalten, dafür gibt es noch kein eigenes Datenmodell (siehe TODO.md). + /// + private const int UpcomingExamsHorizonDays = 21; + + private void BuildUpcomingExams(DateOnly today) + { + UpcomingExams.Clear(); + var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name); + var horizon = today.AddDays(UpcomingExamsHorizonDays); + + var upcoming = _exams.GetAll() + .Where(e => e.Date >= today && e.Date <= horizon) + .OrderBy(e => e.Date) + .Take(10); + + foreach (var exam in upcoming) + UpcomingExams.Add(new UpcomingExamItem(exam.Date, groupNames.GetValueOrDefault(exam.GroupId, "?"), exam.Title)); } // ── "Heute": Tagesliste ────────────────────────────────────────────────── @@ -207,7 +234,7 @@ public partial class TimetableViewModel : ObservableObject } [RelayCommand] - private void ShowEditor() => ActiveTabIndex = 1; + private void OpenSettings() => OnNavigateToSettings?.Invoke(); // ── "Heute": Wochenraster (Nutzer-Feedback) — wie das Bearbeiten-Raster, aber nur Anzeige ── @@ -649,6 +676,14 @@ public class HoursWarningItem(string groupName, int assigned, int expected) public string Text { get; } = $"{groupName}: {assigned} von {expected} Wochenstunden eingetragen"; } +public class UpcomingExamItem(DateOnly date, string groupName, string title) +{ + public DateOnly Date { get; } = date; + public string DateDisplay { get; } = date.ToString("dd.MM.yyyy"); + public string GroupName { get; } = groupName; + public string Title { get; } = title; +} + public class TodayLessonItem { public Guid GroupId { get; private init; } diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs index 5ec23f9..7c4a4b6 100644 --- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs @@ -30,6 +30,8 @@ public partial class SettingsViewModel : ObservableObject private readonly IStudentRepository _students; private readonly IShorthandCodeRepository _shorthandCodes; + [ObservableProperty] private int _activeTabIndex; + // ── Fächer ──────────────────────────────────────────────────────────────── [ObservableProperty] private string _newName = ""; diff --git a/LehrerApp.Desktop/Views/Planning/SubstitutionEntryDialog.axaml b/LehrerApp.Desktop/Views/Planning/SubstitutionEntryDialog.axaml index 572495d..4ae5988 100644 --- a/LehrerApp.Desktop/Views/Planning/SubstitutionEntryDialog.axaml +++ b/LehrerApp.Desktop/Views/Planning/SubstitutionEntryDialog.axaml @@ -4,14 +4,14 @@ xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core" x:Class="LehrerApp.Desktop.Views.Planning.SubstitutionEntryDialog" x:DataType="vm:SubstitutionEntryDialogViewModel" - Title="Vertretung eintragen" + Title="Ausnahme eintragen" Width="440" Height="560" MinWidth="400" MinHeight="480" CanResize="True" WindowStartupLocation="CenterOwner"> - + diff --git a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml index f76c936..9a15e91 100644 --- a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml +++ b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml @@ -127,9 +127,10 @@