From e65a729f97e553af516bfe70557e71ede5153a0e Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Fri, 21 Aug 2026 22:24:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20P=C3=A4dagogische=20Klassen-Aufgaben,?= =?UTF-8?q?=20Priorit=C3=A4ten/Checklisten,=20Dashboard-Schnellzugriffe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Aufgabenart über bestehende Felder (WorkTask.Kind=Reminder + GroupId) statt Vererbung, mit Priorität (TaskPriority) und optionaler Abhak-Liste (ChecklistItems, wahlweise Kurs- Roster oder Freitext). Kurs-Dashboard zeigt jetzt eine "Anstehende Aufgaben"-Karte samt direktem Anlege-Button; derselbe Anlege-Einstieg wurde auch ins Hauptdashboard und (als Schnellüberblick samt fehlender Übersicht/Mitarbeit-Buttons) in die Gruppenliste gezogen. Co-Authored-By: Claude Sonnet 5 --- LehrerApp.Core/Interfaces/IRepositories.cs | 2 + LehrerApp.Core/Models/Workload.cs | 20 ++++ LehrerApp.Data.Tests/RepositoryTests.cs | 41 +++++++ .../Repositories/AllRepositories.cs | 2 + .../DashboardViewModelTests.cs | 29 +++++ LehrerApp.Desktop.Tests/Fakes.cs | 1 + .../GroupDetailViewModelTests.cs | 2 +- .../GroupListViewModelTests.cs | 80 +++++++++++++ .../GroupOverviewViewModelTests.cs | 68 ++++++++++- .../WorkloadViewModelTests.cs | 84 +++++++++++++ .../ViewModels/DashboardViewModel.cs | 20 +++- .../Groups/GroupOverviewViewModel.cs | 59 +++++++++- .../ViewModels/Groups/GroupViewModels.cs | 61 +++++++++- .../ViewModels/MainWindowViewModel.cs | 5 + .../ViewModels/Workload/WorkloadViewModels.cs | 110 +++++++++++++++++- .../Views/Dashboard/DashboardView.axaml | 20 +++- .../Views/Dashboard/DashboardView.axaml.cs | 24 +++- .../Views/Groups/GroupDetailView.axaml.cs | 12 ++ .../Views/Groups/GroupListView.axaml | 50 ++++++++ .../Views/Groups/GroupOverviewTabView.axaml | 38 ++++++ .../Workload/AddEditWorkTaskDialog.axaml | 46 +++++++- .../Views/Workload/WorkTaskDialogHelper.cs | 41 +++++++ .../Views/Workload/WorkTaskListView.axaml | 20 ++-- .../Views/Workload/WorkTaskListView.axaml.cs | 9 +- TODO.md | 73 ++++++++++++ 25 files changed, 878 insertions(+), 39 deletions(-) create mode 100644 LehrerApp.Desktop.Tests/GroupListViewModelTests.cs create mode 100644 LehrerApp.Desktop/Views/Workload/WorkTaskDialogHelper.cs diff --git a/LehrerApp.Core/Interfaces/IRepositories.cs b/LehrerApp.Core/Interfaces/IRepositories.cs index 435303e..0d01f90 100644 --- a/LehrerApp.Core/Interfaces/IRepositories.cs +++ b/LehrerApp.Core/Interfaces/IRepositories.cs @@ -143,6 +143,8 @@ public interface IWorkTaskRepository { List GetByStatus(WorkTaskStatus status); List GetAll(); + /// Für das "Anstehende Aufgaben"-Widget im Kurs-Dashboard (9.2/7.2). + List GetByGroup(Guid groupId); void Save(WorkTask task); void Delete(Guid id); /// Siehe ISeatingPlanRepository.Restore (14.3). diff --git a/LehrerApp.Core/Models/Workload.cs b/LehrerApp.Core/Models/Workload.cs index a3e2af3..6b98b8f 100644 --- a/LehrerApp.Core/Models/Workload.cs +++ b/LehrerApp.Core/Models/Workload.cs @@ -110,9 +110,28 @@ public class WorkTask /// TimeEntry verknüpft und fließen deshalb schon rein datenmodell-bedingt (WorkloadEvaluationViewModel /// wertet nur TimeEntry aus, nicht WorkTask) nicht in die Zeitauswertung (6.3) ein. public TaskKind Kind { get; set; } = TaskKind.WorkItem; + /// Nutzer-Feedback: pädagogische Erinnerungen ("Ansage an die Klasse", "Material einsammeln") + /// sollen sich je nach Dringlichkeit farblich absetzen können, unabhängig vom Fälligkeitsdatum. + public TaskPriority Priority { get; set; } = TaskPriority.Normal; + /// Optionale Abhak-Liste (Nutzer-Feedback: "Namensliste aller Kurs-Schüler" ODER freie + /// Teil-Punkte, wahlweise). Beides läuft über denselben Listentyp — bei aus der Kursliste + /// befüllten Einträgen ist StudentId gesetzt (Sprungziel zum Schüler), bei frei eingegebenen + /// Einträgen bleibt es leer. Als verschachtelte LiteDB-Liste statt eigener Collection/JSON- + /// String, siehe TODO.md-Nachtrag zu 6.1/9 — synct dadurch automatisch über den bestehenden + /// Sync-Unterbau mit (EventPublisher serialisiert das ganze WorkTask-Dokument je Save()). + public List ChecklistItems { get; set; } = []; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } +public class ChecklistItem +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Label { get; set; } = ""; + public bool IsDone { get; set; } + /// Gesetzt, wenn der Eintrag aus der Kursliste befüllt wurde (Sprungziel zum Schüler); + /// bei frei eingegebenen Einträgen null. + public Guid? StudentId { get; set; } +} public class TimeEntry { public Guid Id { get; set; } = Guid.NewGuid(); @@ -130,3 +149,4 @@ public enum TaskCategory { Correction, Preparation, Admin, Meeting, Other, Teach public enum WorkTaskStatus { Open, InProgress, Done } public enum TaskRecurrence { None, Weekly, Monthly } public enum TaskKind { WorkItem, Reminder } +public enum TaskPriority { Low, Normal, High } diff --git a/LehrerApp.Data.Tests/RepositoryTests.cs b/LehrerApp.Data.Tests/RepositoryTests.cs index 94224c1..63331c7 100644 --- a/LehrerApp.Data.Tests/RepositoryTests.cs +++ b/LehrerApp.Data.Tests/RepositoryTests.cs @@ -954,4 +954,45 @@ public sealed class RepositoryTests Assert.Single(result); Assert.Equal("Vertretung 8a", result[0].Description); } + + // ── WorkTaskRepository ──────────────────────────────────────────────────── + + [Fact] + public void WorkTaskRepository_GetByGroup_FindetNurAufgabenDieserGruppe() + { + using var db = NewInMemoryContext(); + var repo = new WorkTaskRepository(db); + var groupId = Guid.NewGuid(); + repo.Save(new WorkTask { Title = "Klassenaufgabe", GroupId = groupId }); + repo.Save(new WorkTask { Title = "Andere Gruppe", GroupId = Guid.NewGuid() }); + repo.Save(new WorkTask { Title = "Ohne Gruppe", GroupId = null }); + + var result = repo.GetByGroup(groupId); + + Assert.Single(result); + Assert.Equal("Klassenaufgabe", result[0].Title); + } + + [Fact] + public void WorkTaskRepository_SaveUndLoad_RundetPrioritaetUndChecklistItemsRoundTrip() + { + using var db = NewInMemoryContext(); + var repo = new WorkTaskRepository(db); + var studentId = Guid.NewGuid(); + var task = new WorkTask + { + Title = "Material einsammeln", + Priority = TaskPriority.High, + ChecklistItems = [new ChecklistItem { Label = "Anna", StudentId = studentId, IsDone = true }], + }; + + repo.Save(task); + var loaded = repo.GetAll().Single(t => t.Id == task.Id); + + Assert.Equal(TaskPriority.High, loaded.Priority); + var item = Assert.Single(loaded.ChecklistItems); + Assert.Equal("Anna", item.Label); + Assert.Equal(studentId, item.StudentId); + Assert.True(item.IsDone); + } } diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs index 6a53e09..863757a 100644 --- a/LehrerApp.Data/Repositories/AllRepositories.cs +++ b/LehrerApp.Data/Repositories/AllRepositories.cs @@ -437,6 +437,8 @@ public class WorkTaskRepository(LiteDbContext db) : IWorkTaskRepository db.Tasks.Find(t => t.Status == s).OrderBy(t => t.DueDate).ToList(); public List GetAll() => db.Tasks.FindAll().OrderBy(t => t.Status).ThenBy(t => t.DueDate).ToList(); + public List GetByGroup(Guid groupId) => + db.Tasks.Find(t => t.GroupId == groupId).OrderBy(t => t.Status).ThenBy(t => t.DueDate).ToList(); public void Save(WorkTask t) { t.UpdatedAt = DateTime.UtcNow; diff --git a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs index 5f977de..4b47da2 100644 --- a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs @@ -462,4 +462,33 @@ public sealed class DashboardViewModelTests Assert.Equal(student.FullName, item.StudentName); Assert.Equal(30.0, item.AbsenceRatePercent); } + + [Fact] + public async Task AddTask_SpeichertErgebnisUndAktualisiertOffeneAufgaben() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var tasks = new FakeWorkTasks(); + var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, tasks: tasks); + vm.OnAddTask = startAsReminder => Task.FromResult( + new WorkTask { Title = "Ansage an die Klasse", Kind = startAsReminder ? TaskKind.Reminder : TaskKind.WorkItem }); + + await vm.AddReminderCommand.ExecuteAsync(null); + + Assert.Single(tasks.GetAll()); + Assert.Contains(vm.OpenTasks, t => t.Title == "Ansage an die Klasse" && t.IsReminder); + } + + [Fact] + public async Task AddTask_OhneDelegatMachtNichts() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var tasks = new FakeWorkTasks(); + var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, tasks: tasks); + + await vm.AddTaskCommand.ExecuteAsync(null); + + Assert.Empty(tasks.GetAll()); + } } diff --git a/LehrerApp.Desktop.Tests/Fakes.cs b/LehrerApp.Desktop.Tests/Fakes.cs index 3bcde73..fd7d549 100644 --- a/LehrerApp.Desktop.Tests/Fakes.cs +++ b/LehrerApp.Desktop.Tests/Fakes.cs @@ -432,6 +432,7 @@ public class FakeWorkTasks : IWorkTaskRepository public void Add(WorkTask t) => _all.Add(t); public List GetByStatus(WorkTaskStatus status) => _all.Where(t => t.Status == status).ToList(); public List GetAll() => _all.ToList(); + public List GetByGroup(Guid groupId) => _all.Where(t => t.GroupId == groupId).ToList(); public void Save(WorkTask task) { _all.RemoveAll(t => t.Id == task.Id); _all.Add(task); } public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id); public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore diff --git a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs index a607970..6847b14 100644 --- a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs @@ -19,7 +19,7 @@ public sealed class GroupDetailViewModelTests var vm = new GroupDetailViewModel(groups, students, memberships, subjects, exams, grades, tasks, new GroupOverviewViewModel(new FakeLessons(), exams, new FakeSessions([]), new FakeEntries(), students, - new FakeDocumentation(), new AttendanceBalanceService(), new SchoolYearService()), + new FakeDocumentation(), tasks, new AttendanceBalanceService(), new SchoolYearService()), new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(), students, memberships, groups, new FakeCompetencyDomains()), new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()), diff --git a/LehrerApp.Desktop.Tests/GroupListViewModelTests.cs b/LehrerApp.Desktop.Tests/GroupListViewModelTests.cs new file mode 100644 index 0000000..fb50301 --- /dev/null +++ b/LehrerApp.Desktop.Tests/GroupListViewModelTests.cs @@ -0,0 +1,80 @@ +using LehrerApp.Core.Models; +using LehrerApp.Core.Services; +using LehrerApp.Desktop.ViewModels.Groups; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +/// Tests für den Schnellüberblick im Auswahl-Panel der Gruppenliste (Nutzer-Feedback: +/// "oberhalb der Buttonliste ein paar Daten auswerfen. Nächste Stunde, nächste Arbeit, +/// wichtige Todos"). +public sealed class GroupListViewModelTests +{ + private static GroupListViewModel BuildVm(LearningGroup group, FakeLessons? lessons = null, + FakeExams? exams = null, FakeWorkTasks? tasks = null) => + new(new FakeGroups([group]), new FakeSubjects([]), new SchoolYearService(), + lessons ?? new FakeLessons(), exams ?? new FakeExams([]), tasks ?? new FakeWorkTasks()); + + [Fact] + public void SelectedGroup_OhneDatenZeigtKeinenSchnellueberblick() + { + var group = new LearningGroup { Name = "9c" }; + var vm = BuildVm(group); + + vm.SelectedGroup = vm.Groups.Count > 0 ? vm.Groups[0] : new GroupListItem(group, ""); + + Assert.False(vm.QuickHasAnything); + } + + [Fact] + public void SelectedGroup_ZeigtNaechsteGeplanteStundeUndKlausur() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var lessons = new FakeLessons(); + lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(3), Topic = "Redox", Status = LessonStatus.Planned }); + var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur 1" }]); + var vm = BuildVm(group, lessons: lessons, exams: exams); + + vm.SelectedGroup = new GroupListItem(group, ""); + + Assert.True(vm.QuickHasNextLesson); + Assert.Contains("Redox", vm.QuickNextLessonLabel); + Assert.True(vm.QuickHasNextExam); + Assert.Contains("Klausur 1", vm.QuickNextExamLabel); + Assert.True(vm.QuickHasAnything); + } + + [Fact] + public void SelectedGroup_ZeigtOffeneAufgabenDieserGruppeSortiertNachFaelligkeit() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var tasks = new FakeWorkTasks(); + tasks.Add(new WorkTask { GroupId = group.Id, Title = "Bald fällig", DueDate = today.AddDays(1), Status = WorkTaskStatus.Open }); + tasks.Add(new WorkTask { GroupId = group.Id, Title = "Erledigt", DueDate = today, Status = WorkTaskStatus.Done }); + tasks.Add(new WorkTask { GroupId = Guid.NewGuid(), Title = "Andere Gruppe", DueDate = today, Status = WorkTaskStatus.Open }); + var vm = BuildVm(group, tasks: tasks); + + vm.SelectedGroup = new GroupListItem(group, ""); + + Assert.True(vm.QuickHasTasks); + Assert.Equal("Bald fällig", Assert.Single(vm.QuickTasks).Title); + } + + [Fact] + public void SelectedGroup_Zuruecksetzen_LeertSchnellueberblick() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur 1" }]); + var vm = BuildVm(group, exams: exams); + vm.SelectedGroup = new GroupListItem(group, ""); + Assert.True(vm.QuickHasAnything); + + vm.SelectedGroup = null; + + Assert.False(vm.QuickHasAnything); + Assert.Empty(vm.QuickTasks); + } +} diff --git a/LehrerApp.Desktop.Tests/GroupOverviewViewModelTests.cs b/LehrerApp.Desktop.Tests/GroupOverviewViewModelTests.cs index 1217087..5d49ea2 100644 --- a/LehrerApp.Desktop.Tests/GroupOverviewViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/GroupOverviewViewModelTests.cs @@ -12,10 +12,10 @@ public sealed class GroupOverviewViewModelTests { private static GroupOverviewViewModel NewVm(FakeLessons? lessons = null, FakeExams? exams = null, FakeSessions? sessions = null, FakeEntries? entries = null, FakeStudents? students = null, - FakeDocumentation? documentation = null) => + FakeDocumentation? documentation = null, FakeWorkTasks? tasks = null) => new(lessons ?? new FakeLessons(), exams ?? new FakeExams([]), sessions ?? new FakeSessions([]), entries ?? new FakeEntries(), students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(), - new AttendanceBalanceService(), new SchoolYearService()); + tasks ?? new FakeWorkTasks(), new AttendanceBalanceService(), new SchoolYearService()); private static (GroupOverviewViewModel Vm, FakeLessons Lessons, FakeExams Exams, FakeSessions Sessions, FakeEntries Entries, FakeStudents Students, Guid GroupId) BuildScenario( @@ -301,4 +301,68 @@ public sealed class GroupOverviewViewModelTests var (vm, _, _, _, _, _, _) = BuildScenario(); Assert.False(vm.HasMissingHomework); } + + [Fact] + public void AnstehendeAufgaben_ZeigtNurOffeneAufgabenDieserGruppeSortiertNachFaelligkeit() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var groupId = Guid.NewGuid(); + var otherGroupId = Guid.NewGuid(); + var tasks = new FakeWorkTasks(); + tasks.Add(new WorkTask { GroupId = groupId, Title = "Später fällig", DueDate = today.AddDays(10), Status = WorkTaskStatus.Open }); + tasks.Add(new WorkTask { GroupId = groupId, Title = "Bald fällig", DueDate = today.AddDays(1), Status = WorkTaskStatus.Open }); + tasks.Add(new WorkTask { GroupId = groupId, Title = "Erledigt", DueDate = today, Status = WorkTaskStatus.Done }); + tasks.Add(new WorkTask { GroupId = otherGroupId, Title = "Anderer Kurs", DueDate = today, Status = WorkTaskStatus.Open }); + + var vm = NewVm(tasks: tasks); + vm.Initialize(groupId, "Testkurs"); + + Assert.True(vm.HasGroupTasks); + Assert.Equal(["Bald fällig", "Später fällig"], vm.GroupTasks.Select(t => t.Title)); + } + + [Fact] + public void AnstehendeAufgaben_OhneAufgabenIstLeer() + { + var (vm, _, _, _, _, _, _) = BuildScenario(); + Assert.False(vm.HasGroupTasks); + } + + [Fact] + public void NavigateToWorkload_RuftDelegatAuf() + { + var (vm, _, _, _, _, _, _) = BuildScenario(); + var called = false; + vm.OnNavigateToWorkload = () => called = true; + + vm.NavigateToWorkloadCommand.Execute(null); + + Assert.True(called); + } + + [Fact] + public async Task AddGroupTask_SpeichertErgebnisMitGruppeUndAktualisiertListe() + { + var tasks = new FakeWorkTasks(); + var (vm, _, _, _, _, _, groupId) = BuildScenario(); + var vmWithTasks = NewVm(tasks: tasks); + vmWithTasks.Initialize(groupId, "Testkurs"); + vmWithTasks.OnAddGroupTask = requestedGroupId => + Task.FromResult(new WorkTask { Title = "Material einsammeln", GroupId = requestedGroupId }); + + await vmWithTasks.AddGroupTaskCommand.ExecuteAsync(null); + + var saved = Assert.Single(tasks.GetByGroup(groupId)); + Assert.Equal("Material einsammeln", saved.Title); + Assert.True(vmWithTasks.HasGroupTasks); + Assert.Equal("Material einsammeln", Assert.Single(vmWithTasks.GroupTasks).Title); + } + + [Fact] + public async Task AddGroupTask_OhneDelegatMachtNichts() + { + var (vm, _, _, _, _, _, _) = BuildScenario(); + await vm.AddGroupTaskCommand.ExecuteAsync(null); + Assert.False(vm.HasGroupTasks); + } } diff --git a/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs b/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs index ca2c15f..d03e9b3 100644 --- a/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs @@ -337,6 +337,90 @@ public sealed class AddEditWorkTaskDialogViewModelTests var vm = new AddEditWorkTaskDialogViewModel(source, []); Assert.True(vm.IsReminder); } + + [Fact] + public void Save_MitPrioritaet_UebernimmtSieInsResult() + { + var vm = new AddEditWorkTaskDialogViewModel(null, []) + { + Title = "Wichtig", + SelectedPriority = TaskPriorityDisplay.Label(TaskPriority.High), + }; + + vm.SaveCommand.Execute(null); + + Assert.Equal(TaskPriority.High, vm.Result!.Priority); + } + + [Fact] + public void Konstruktor_OhneQuelle_StartetMitNormalerPrioritaet() + { + var vm = new AddEditWorkTaskDialogViewModel(null, []); + Assert.Equal(TaskPriorityDisplay.Label(TaskPriority.Normal), vm.SelectedPriority); + } + + [Fact] + public void ChecklistItems_HinzufuegenUndEntfernen_WirdInResultUebernommen() + { + var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "Material einsammeln" }; + vm.NewChecklistItemText = "Erste Position"; + vm.AddChecklistItemCommand.Execute(null); + vm.NewChecklistItemText = "Zweite Position"; + vm.AddChecklistItemCommand.Execute(null); + + Assert.Equal(["Erste Position", "Zweite Position"], vm.ChecklistItems.Select(c => c.Label)); + Assert.Equal("", vm.NewChecklistItemText); + + vm.RemoveChecklistItemCommand.Execute(vm.ChecklistItems[0]); + vm.SaveCommand.Execute(null); + + var item = Assert.Single(vm.Result!.ChecklistItems); + Assert.Equal("Zweite Position", item.Label); + Assert.Null(item.StudentId); + } + + [Fact] + public void Konstruktor_BestehendeAufgabeMitChecklist_UebernimmtEintraege() + { + var source = new WorkTask + { + Title = "Alt", + ChecklistItems = [new ChecklistItem { Label = "Schon da", IsDone = true }], + }; + var vm = new AddEditWorkTaskDialogViewModel(source, []); + + var row = Assert.Single(vm.ChecklistItems); + Assert.Equal("Schon da", row.Label); + Assert.True(row.IsDone); + } + + [Fact] + public void FillFromRoster_OhneGruppeOderLader_MachtNichts() + { + var vm = new AddEditWorkTaskDialogViewModel(null, []); + vm.FillFromRosterCommand.Execute(null); + Assert.Empty(vm.ChecklistItems); + } + + [Fact] + public void FillFromRoster_BefuelltAusRosterUndUeberspringtDoppelteSchueler() + { + var group = new LearningGroup { Name = "8a" }; + var studentId = Guid.NewGuid(); + var otherStudentId = Guid.NewGuid(); + var vm = new AddEditWorkTaskDialogViewModel(null, [group], loadRoster: _ => + [(studentId, "Anna Beispiel"), (otherStudentId, "Ben Muster")]) + { + SelectedGroup = group, + }; + + Assert.True(vm.CanFillFromRoster); + vm.FillFromRosterCommand.Execute(null); + Assert.Equal(2, vm.ChecklistItems.Count); + + vm.FillFromRosterCommand.Execute(null); + Assert.Equal(2, vm.ChecklistItems.Count); // kein Duplikat beim zweiten Aufruf + } } public sealed class TimeTrackingViewModelTests diff --git a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs index 4d96ca1..ebba00a 100644 --- a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs @@ -79,6 +79,9 @@ public partial class DashboardViewModel : ObservableObject // OnNavigateToGroup (Lerngruppen-Kacheln, Tab "Übersicht"), da der Sprung von einer konkreten // Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt. public Action? OnNavigateToLesson { get; set; } + // Direkter Anlege-Einstieg aus der "Offene Aufgaben"-Kachel (Nutzer-Feedback), statt erst über + // "Arbeitszeit" navigieren zu müssen — gleiches Dialog-Delegate-Muster wie im Kurs-Dashboard. + public Func>? OnAddTask { get; set; } public Action? OnNavigateToExam { get; set; } // Sprungziel für eine ungeplante Stunde — führt direkt in den Verlaufsplan-Tab der Gruppe // (nicht den Standard-Tab von OnNavigateToGroup), damit das Thema gleich ergänzt werden kann. @@ -162,7 +165,8 @@ public partial class DashboardViewModel : ObservableObject OpenTasks.Add(new() { Title = t.Title, DueDate = t.DueDate?.ToString("dd.MM.") ?? "", IsOverdue = t.DueDate.HasValue && t.DueDate < today, - IsReminder = t.Kind == TaskKind.Reminder }); + IsReminder = t.Kind == TaskKind.Reminder, + IsHighPriority = t.Priority == TaskPriority.High }); CurrentGroups.Clear(); foreach (var g in groups.Values.OrderBy(g => g.Name)) @@ -678,6 +682,18 @@ public partial class DashboardViewModel : ObservableObject { if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); } [RelayCommand] private void Refresh() => Load(); + [RelayCommand] private Task AddTask() => AddTaskInternal(startAsReminder: false); + [RelayCommand] private Task AddReminder() => AddTaskInternal(startAsReminder: true); + + private async Task AddTaskInternal(bool startAsReminder) + { + if (OnAddTask is null) return; + var result = await OnAddTask(startAsReminder); + if (result is null) return; + _tasks.Save(result); + Load(); + } + private class DayAgg { public bool HasLesson; @@ -698,7 +714,7 @@ public class LessonItem public string Room { get; set; } = ""; public bool HasRoom => !string.IsNullOrWhiteSpace(Room); } -public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } } +public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } public bool IsHighPriority { get; set; } } public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; } // ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ──────────────────────── diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs index de5bf4e..911b63d 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs @@ -4,6 +4,7 @@ using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels; +using LehrerApp.Desktop.ViewModels.Workload; using System.Collections.ObjectModel; namespace LehrerApp.Desktop.ViewModels.Groups; @@ -36,6 +37,7 @@ public partial class GroupOverviewViewModel : ObservableObject private readonly IParticipationRepository _entries; private readonly IStudentRepository _students; private readonly IDocumentationRepository _documentation; + private readonly IWorkTaskRepository _tasks; private readonly AttendanceBalanceService _attendanceBalance; private readonly SchoolYearService _schoolYear; @@ -69,18 +71,46 @@ public partial class GroupOverviewViewModel : ObservableObject /// 6 Planung, 7 Kompetenzen, 8 Dokumentation). public Action? OnNavigateToTab { get; set; } + /// Anders als OnNavigateToTab kein Tab innerhalb dieser Detailansicht, sondern ein Sprung in + /// den eigenständigen Top-Level-Aufgabenbereich (MainWindowViewModel.NavigateToWorkload) — von + /// hier aus nicht direkt aufrufbar (keine ViewModel-zu-ViewModel-Referenz, siehe + /// GroupDetailView.axaml.cs, gleiches Muster wie LessonViewerDialog/TeachingModeWindow). + public Action? OnNavigateToWorkload { get; set; } + [RelayCommand] private void NavigateToPlanning() => OnNavigateToTab?.Invoke(6); [RelayCommand] private void NavigateToExams() => OnNavigateToTab?.Invoke(4); [RelayCommand] private void NavigateToParticipation() => OnNavigateToTab?.Invoke(3); [RelayCommand] private void NavigateToDocumentation() => OnNavigateToTab?.Invoke(8); + [RelayCommand] private void NavigateToWorkload() => OnNavigateToWorkload?.Invoke(); + + // ── Anstehende Aufgaben für diese Klasse (Nutzer-Feedback: pädagogische Erinnerungen/Aufgaben + // sollen "gleichberechtigt" auch im Kurs-Dashboard stehen, nicht nur im Hauptdashboard) ─────── + private const int GroupTasksMaxCount = 5; + public ObservableCollection GroupTasks { get; } = []; + public bool HasGroupTasks => GroupTasks.Count > 0; + + /// Öffnet den Aufgaben-Dialog mit dieser Gruppe vorbelegt (Nutzer-Feedback: direkter + /// Anlege-Einstieg aus dem Kurs-Dashboard, statt erst über "Zu den Aufgaben" springen zu + /// müssen). Gleiches View-Code-Behind-Delegate-Muster wie OnNavigateToWorkload. + public Func>? OnAddGroupTask { get; set; } + + [RelayCommand] + private async Task AddGroupTask() + { + if (OnAddGroupTask is null) return; + var result = await OnAddGroupTask(_groupId); + if (result is null) return; + _tasks.Save(result); + LoadGroupTasks(DateOnly.FromDateTime(DateTime.Today)); + } public GroupOverviewViewModel(ILessonRepository lessons, IExamRepository exams, IParticipationSessionRepository sessions, IParticipationRepository entries, - IStudentRepository students, IDocumentationRepository documentation, + IStudentRepository students, IDocumentationRepository documentation, IWorkTaskRepository tasks, AttendanceBalanceService attendanceBalance, SchoolYearService schoolYear) { _lessons = lessons; _exams = exams; _sessions = sessions; - _entries = entries; _students = students; _documentation = documentation; + _entries = entries; _students = students; _documentation = documentation; _tasks = tasks; _attendanceBalance = attendanceBalance; _schoolYear = schoolYear; } @@ -102,6 +132,7 @@ public partial class GroupOverviewViewModel : ObservableObject LoadDraftDocumentationCount(); LoadAttendanceNotices(today); LoadMissingHomework(); + LoadGroupTasks(today); } private void LoadNextLesson(DateOnly today) @@ -260,6 +291,19 @@ public partial class GroupOverviewViewModel : ObservableObject } OnPropertyChanged(nameof(HasMissingHomework)); } + + private void LoadGroupTasks(DateOnly today) + { + GroupTasks.Clear(); + foreach (var t in _tasks.GetByGroup(_groupId) + .Where(t => t.Status != WorkTaskStatus.Done) + .OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(GroupTasksMaxCount)) + GroupTasks.Add(new GroupTaskItem(t.Title, t.Kind == TaskKind.Reminder, + t.DueDate?.ToString("dd.MM.yyyy") ?? "", + t.DueDate is { } d && d < today, TaskPriorityDisplay.ColorHex(t.Priority), + t.Priority == TaskPriority.High)); + OnPropertyChanged(nameof(HasGroupTasks)); + } } public sealed class MissingHomeworkItem(string studentName, string statusLabel) @@ -267,3 +311,14 @@ public sealed class MissingHomeworkItem(string studentName, string statusLabel) public string StudentName { get; } = studentName; public string StatusLabel { get; } = statusLabel; } + +public sealed class GroupTaskItem(string title, bool isReminder, string dueDateDisplay, bool isOverdue, + string priorityColorHex, bool isHighPriority) +{ + public string Title { get; } = title; + public bool IsReminder { get; } = isReminder; + public string DueDateDisplay { get; } = dueDateDisplay; + public bool IsOverdue { get; } = isOverdue; + public string PriorityColorHex { get; } = priorityColorHex; + public bool IsHighPriority { get; } = isHighPriority; +} diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index 3fe7104..da02dca 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -4,6 +4,7 @@ using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels.Students; +using LehrerApp.Desktop.ViewModels.Workload; using System.Collections.ObjectModel; namespace LehrerApp.Desktop.ViewModels.Groups; @@ -12,8 +13,13 @@ namespace LehrerApp.Desktop.ViewModels.Groups; public partial class GroupListViewModel : ObservableObject { + private const int QuickTasksMaxCount = 3; + private readonly IGroupRepository _groups; private readonly ISubjectRepository _subjects; + private readonly ILessonRepository _lessons; + private readonly IExamRepository _exams; + private readonly IWorkTaskRepository _tasks; public Action? OnNavigateToDetail { get; set; } public Func? OnAddGroup { get; set; } @@ -39,9 +45,22 @@ public partial class GroupListViewModel : ObservableObject public ObservableCollection SchoolYears { get; } = []; public ObservableCollection Groups { get; } = []; - public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy) + // ── Schnellüberblick im Auswahl-Panel (Nutzer-Feedback: "oberhalb der Buttonliste ein paar + // Daten auswerfen. Nächste Stunde, nächste Arbeit, wichtige Todos") — bewusst dieselben + // kompakten Kennzahlen wie die obersten Karten des Kurs-Dashboards (GroupOverviewViewModel), + // hier nur ohne eigenen Tab-Wechsel, da man ohnehin schon auf der Gruppenliste steht. + [ObservableProperty] private bool _quickHasNextLesson; + [ObservableProperty] private string _quickNextLessonLabel = ""; + [ObservableProperty] private bool _quickHasNextExam; + [ObservableProperty] private string _quickNextExamLabel = ""; + public ObservableCollection QuickTasks { get; } = []; + public bool QuickHasTasks => QuickTasks.Count > 0; + public bool QuickHasAnything => QuickHasNextLesson || QuickHasNextExam || QuickHasTasks; + + public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy, + ILessonRepository lessons, IExamRepository exams, IWorkTaskRepository tasks) { - _groups = groups; _subjects = subjects; + _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks; foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y); SelectedSchoolYear = sy.CurrentSchoolYear(); } @@ -58,6 +77,44 @@ public partial class GroupListViewModel : ObservableObject RollOverGroupCommand.NotifyCanExecuteChanged(); ToggleArchiveCommand.NotifyCanExecuteChanged(); DeleteGroupCommand.NotifyCanExecuteChanged(); + LoadQuickInfo(); + } + + private void LoadQuickInfo() + { + QuickTasks.Clear(); + if (SelectedGroup is null) + { + QuickHasNextLesson = false; QuickNextLessonLabel = ""; + QuickHasNextExam = false; QuickNextExamLabel = ""; + OnPropertyChanged(nameof(QuickHasTasks)); + OnPropertyChanged(nameof(QuickHasAnything)); + return; + } + + var groupId = SelectedGroup.Id; + var today = DateOnly.FromDateTime(DateTime.Today); + + var nextLesson = _lessons.GetByGroupAndRange(groupId, today, today.AddDays(90)) + .Where(l => l.Status == LessonStatus.Planned) + .OrderBy(l => l.Date).ThenBy(l => l.LessonNumber ?? 0).FirstOrDefault(); + QuickHasNextLesson = nextLesson is not null; + QuickNextLessonLabel = nextLesson is null ? "" + : string.IsNullOrWhiteSpace(nextLesson.Topic) + ? nextLesson.Date.ToString("dd.MM.yyyy") + : $"{nextLesson.Date:dd.MM.yyyy} — {nextLesson.Topic}"; + + var nextExam = _exams.GetByGroup(groupId).Where(e => e.Date >= today).MinBy(e => e.Date); + QuickHasNextExam = nextExam is not null; + QuickNextExamLabel = nextExam is null ? "" : $"{nextExam.Date:dd.MM.yyyy} — {nextExam.Title}"; + + foreach (var t in _tasks.GetByGroup(groupId).Where(t => t.Status != WorkTaskStatus.Done) + .OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(QuickTasksMaxCount)) + QuickTasks.Add(new GroupTaskItem(t.Title, t.Kind == TaskKind.Reminder, + t.DueDate?.ToString("dd.MM.yyyy") ?? "", t.DueDate is { } d && d < today, + TaskPriorityDisplay.ColorHex(t.Priority), t.Priority == TaskPriority.High)); + OnPropertyChanged(nameof(QuickHasTasks)); + OnPropertyChanged(nameof(QuickHasAnything)); } public void LoadGroups() diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index bdd2e4f..3007bfa 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -168,6 +168,11 @@ public partial class MainWindowViewModel : ObservableObject } public void NavigateToStudents() => NavigateTo(NavItem.Students); + + /// Sprungziel aus dem Kurs-Dashboard ("Anstehende Aufgaben für diese Klasse") in den + /// Aufgaben-Bereich — anders als NavigateToGroupDetail/NavigateToSettings kein Tab innerhalb + /// einer Detailansicht, sondern ein eigener Top-Level-Bereich (NavItem.Workload). + public void NavigateToWorkload() => NavigateTo(NavItem.Workload); } public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings } diff --git a/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs b/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs index aaf0f6a..cc3610d 100644 --- a/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs @@ -57,6 +57,30 @@ public static class TaskCategoryDisplay Enum.GetValues().FirstOrDefault(c => Label(c) == label, TaskCategory.Other); } +public static class TaskPriorityDisplay +{ + public static string Label(TaskPriority p) => p switch + { + TaskPriority.Low => "Niedrig", + TaskPriority.Normal => "Normal", + TaskPriority.High => "Hoch", + _ => p.ToString(), + }; + + public static string ColorHex(TaskPriority p) => p switch + { + TaskPriority.Low => "#9E9E9E", + TaskPriority.Normal => "#1976D2", + TaskPriority.High => "#D32F2F", + _ => "#9E9E9E", + }; + + public static string[] Options { get; } = Enum.GetValues().Select(Label).ToArray(); + + public static TaskPriority FromLabel(string? label) => + Enum.GetValues().FirstOrDefault(p => Label(p) == label, TaskPriority.Normal); +} + public static class TaskRecurrenceDisplay { public static string Label(TaskRecurrence r) => r switch @@ -218,6 +242,7 @@ public partial class WorkTaskListViewModel : ObservableObject EstimatedMinutes = item.Model.EstimatedMinutes, Recurrence = item.Model.Recurrence, Kind = item.Model.Kind, + Priority = item.Model.Priority, Notes = item.Model.Notes, Status = WorkTaskStatus.Open, }); @@ -250,6 +275,17 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status); public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : ""; + // Priorität (Nutzer-Feedback: pädagogische Erinnerungen farblich nach Dringlichkeit absetzen) — + // nur bei High überhaupt anzeigen, Normal/Low sollen nicht zusätzlich "schreien". + public bool ShowPriority => Model.Priority == TaskPriority.High; + public string PriorityLabel => TaskPriorityDisplay.Label(Model.Priority); + public string PriorityColorHex => TaskPriorityDisplay.ColorHex(Model.Priority); + + // Abhak-Liste (Nutzer-Feedback: Namensliste/Teil-Punkte optional zuschaltbar). + public bool HasChecklist => Model.ChecklistItems.Count > 0; + public string ChecklistProgressDisplay => Model.ChecklistItems.Count == 0 ? "" + : $"{Model.ChecklistItems.Count(c => c.IsDone)} / {Model.ChecklistItems.Count} erledigt"; + // Wiederkehrende Aufgabe (6.1.4). public bool IsRecurring => Model.Recurrence != TaskRecurrence.None; public string RecurrenceLabel => $"🔁 {TaskRecurrenceDisplay.Label(Model.Recurrence)}"; @@ -266,6 +302,10 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut public partial class AddEditWorkTaskDialogViewModel : ObservableObject { private readonly WorkTask? _source; + /// Liefert die aktiven Mitglieder einer Gruppe (StudentId, Anzeigename) für "Aus Kursliste + /// befüllen" — optional/injizierbar statt eines festen Repository-Typs, damit dieses ViewModel + /// (wie bisher) auch ohne DI-Container in Tests per `new` gebaut werden kann. + private readonly Func>? _loadRoster; [ObservableProperty] private string _title = ""; [ObservableProperty] private string _selectedCategory = TaskCategoryDisplay.Options[0]; @@ -273,8 +313,10 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject [ObservableProperty] private string _dueDateText = ""; [ObservableProperty] private string _estimatedMinutesText = ""; [ObservableProperty] private string _selectedRecurrence = TaskRecurrenceDisplay.Options[0]; + [ObservableProperty] private string _selectedPriority = TaskPriorityDisplay.Label(TaskPriority.Normal); [ObservableProperty] private string _notes = ""; [ObservableProperty] private bool _isReminder; + [ObservableProperty] private string _newChecklistItemText = ""; [ObservableProperty] private string _titleError = ""; [ObservableProperty] private string _dueDateError = ""; [ObservableProperty] private string _estimatedMinutesError = ""; @@ -284,15 +326,24 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject : (IsReminder ? "Erinnerung bearbeiten" : "Aufgabe bearbeiten"); public List CategoryOptions { get; } = [.. TaskCategoryDisplay.Options]; public List RecurrenceOptions { get; } = [.. TaskRecurrenceDisplay.Options]; + public List PriorityOptions { get; } = [.. TaskPriorityDisplay.Options]; public List Groups { get; } public WorkTask? Result { get; private set; } - partial void OnIsReminderChanged(bool value) => OnPropertyChanged(nameof(DialogTitle)); + // Abhak-Liste (Nutzer-Feedback: wahlweise Namensliste der Kurs-Schüler oder freie Teil-Punkte, + // beides über denselben Zeilentyp). + public ObservableCollection ChecklistItems { get; } = []; + public bool CanFillFromRoster => SelectedGroup is not null && _loadRoster is not null; - public AddEditWorkTaskDialogViewModel(WorkTask? source, List groups, bool startAsReminder = false) + partial void OnIsReminderChanged(bool value) => OnPropertyChanged(nameof(DialogTitle)); + partial void OnSelectedGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(CanFillFromRoster)); + + public AddEditWorkTaskDialogViewModel(WorkTask? source, List groups, bool startAsReminder = false, + Func>? loadRoster = null) { _source = source; Groups = groups; + _loadRoster = loadRoster; if (source is null) { IsReminder = startAsReminder; return; } Title = source.Title; @@ -301,8 +352,37 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? ""; EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? ""; SelectedRecurrence = TaskRecurrenceDisplay.Label(source.Recurrence); + SelectedPriority = TaskPriorityDisplay.Label(source.Priority); Notes = source.Notes ?? ""; IsReminder = source.Kind == TaskKind.Reminder; + foreach (var item in source.ChecklistItems) + ChecklistItems.Add(new ChecklistItemRow(item)); + } + + [RelayCommand] + private void AddChecklistItem() + { + if (string.IsNullOrWhiteSpace(NewChecklistItemText)) return; + ChecklistItems.Add(new ChecklistItemRow(NewChecklistItemText.Trim())); + NewChecklistItemText = ""; + } + + [RelayCommand] + private void RemoveChecklistItem(ChecklistItemRow? row) + { + if (row is not null) ChecklistItems.Remove(row); + } + + [RelayCommand] + private void FillFromRoster() + { + if (SelectedGroup is null || _loadRoster is null) return; + var existingStudentIds = ChecklistItems.Where(c => c.StudentId is not null).Select(c => c.StudentId!.Value).ToHashSet(); + foreach (var (studentId, name) in _loadRoster(SelectedGroup.Id)) + { + if (!existingStudentIds.Contains(studentId)) + ChecklistItems.Add(new ChecklistItemRow(name, studentId)); + } } [RelayCommand] @@ -346,13 +426,39 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject EstimatedMinutes = estimatedMinutes, Recurrence = recurrence, Kind = IsReminder ? TaskKind.Reminder : TaskKind.WorkItem, + Priority = TaskPriorityDisplay.FromLabel(SelectedPriority), Status = _source?.Status ?? WorkTaskStatus.Open, Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(), + ChecklistItems = ChecklistItems.Select(c => new ChecklistItem + { Id = c.Id, Label = c.Label, IsDone = c.IsDone, StudentId = c.StudentId }).ToList(), CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow, }; } } +public partial class ChecklistItemRow : ObservableObject +{ + public Guid Id { get; } + public Guid? StudentId { get; } + [ObservableProperty] private string _label; + [ObservableProperty] private bool _isDone; + + public ChecklistItemRow(ChecklistItem source) + { + Id = source.Id; + StudentId = source.StudentId; + _label = source.Label; + _isDone = source.IsDone; + } + + public ChecklistItemRow(string label, Guid? studentId = null) + { + Id = Guid.NewGuid(); + StudentId = studentId; + _label = label; + } +} + // ── Zeiterfassung (6.2) ────────────────────────────────────────────────────── public partial class TimeTrackingViewModel : ObservableObject diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml index 6a71820..ba1819e 100644 --- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml +++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml @@ -103,18 +103,26 @@ Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="16"> - + + +