diff --git a/LehrerApp.Desktop.Tests/TeachingModeOutstandingHomeworkViewModelTests.cs b/LehrerApp.Desktop.Tests/TeachingModeOutstandingHomeworkViewModelTests.cs
index b495091..1f2ed88 100644
--- a/LehrerApp.Desktop.Tests/TeachingModeOutstandingHomeworkViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/TeachingModeOutstandingHomeworkViewModelTests.cs
@@ -67,4 +67,94 @@ public sealed class TeachingModeOutstandingHomeworkViewModelTests
vm.Refresh();
Assert.Equal(amelia.FullName, Assert.Single(vm.Rows).StudentName);
}
+
+ [Theory]
+ [InlineData(HomeworkStatus.MissingOpen, "submitted", HomeworkStatus.SubmittedLate)]
+ [InlineData(HomeworkStatus.MissingOpen, "final", HomeworkStatus.MissingOverdue)]
+ [InlineData(HomeworkStatus.MissingOpen, "partial", HomeworkStatus.PartialMissingOverdue)]
+ [InlineData(HomeworkStatus.MissingOverdue, "submitted", HomeworkStatus.SubmittedLate)]
+ [InlineData(HomeworkStatus.PartiallyCompleted, "submitted", HomeworkStatus.PartialSubmittedLate)]
+ [InlineData(HomeworkStatus.PartiallyCompleted, "final", HomeworkStatus.PartialMissingOverdue)]
+ [InlineData(HomeworkStatus.PartialMissingOverdue, "submitted", HomeworkStatus.PartialSubmittedLate)]
+ public void Zeilenaktionen_SetzenPassendenEndstatus(HomeworkStatus start, string action, HomeworkStatus expected)
+ {
+ var (vm, entries, entry) = CreateSingle(start);
+ vm.Refresh();
+ var row = Assert.Single(vm.Rows);
+
+ var command = action switch
+ {
+ "submitted" => row.MarkSubmittedCommand,
+ "final" => row.MarkFinalCommand,
+ _ => row.MarkPartialCommand,
+ };
+ command.Execute(null);
+
+ var saved = entries.GetBySessionAndStudent(entry.SessionId, entry.StudentId)!;
+ Assert.Equal(expected, saved.Homework);
+ Assert.Equal(HomeworkDisplay.CountsAsMissing(expected), saved.HomeworkMissing);
+ Assert.Equal(HomeworkDisplay.CountsAsMissing(expected), vm.Rows.Count == 1);
+ }
+
+ [Fact]
+ public void Zeilenaktionen_BietenNurSinnvolleOptionenAn()
+ {
+ var (vm, _, _) = CreateSingle(HomeworkStatus.MissingOverdue);
+ vm.Refresh();
+ var overdue = Assert.Single(vm.Rows);
+ Assert.False(overdue.CanMarkFinal);
+ Assert.True(overdue.CanMarkPartial);
+
+ (vm, _, _) = CreateSingle(HomeworkStatus.PartiallyCompleted);
+ vm.Refresh();
+ var partial = Assert.Single(vm.Rows);
+ Assert.True(partial.CanMarkFinal);
+ Assert.False(partial.CanMarkPartial);
+
+ (vm, _, _) = CreateSingle(HomeworkStatus.MissingOpen, isReadOnly: true);
+ vm.Refresh();
+ Assert.False(Assert.Single(vm.Rows).CanEdit);
+ }
+
+ [Fact]
+ public void Undo_StelltVorherigenStatusWiederHer()
+ {
+ var (vm, entries, entry) = CreateSingle(HomeworkStatus.MissingOpen);
+ var changed = 0;
+ vm.OnHomeworkChanged = () => changed++;
+ vm.Refresh();
+ Assert.False(vm.UndoCommand.CanExecute(null));
+
+ vm.Rows[0].MarkSubmittedCommand.Execute(null);
+ Assert.Empty(vm.Rows);
+ Assert.NotNull(vm.LastChangeLabel);
+ Assert.True(vm.UndoCommand.CanExecute(null));
+
+ vm.UndoCommand.Execute(null);
+ var saved = entries.GetBySessionAndStudent(entry.SessionId, entry.StudentId)!;
+ Assert.Equal(HomeworkStatus.MissingOpen, saved.Homework);
+ Assert.True(saved.HomeworkMissing);
+ Assert.Single(vm.Rows);
+ Assert.Null(vm.LastChangeLabel);
+ Assert.False(vm.UndoCommand.CanExecute(null));
+ Assert.Equal(2, changed);
+ }
+
+ private static (TeachingModeOutstandingHomeworkViewModel Vm, FakeEntries Entries, ParticipationEntry Entry)
+ CreateSingle(HomeworkStatus status, bool isReadOnly = false)
+ {
+ var date = new DateOnly(2026, 9, 25);
+ var group = new LearningGroup { Name = "10c Chemie" };
+ var student = new Student { FirstName = "Ben", LastName = "Muster" };
+ var session = new ParticipationSession { GroupId = group.Id, Date = date.AddDays(-5) };
+ var entries = new FakeEntries();
+ var entry = new ParticipationEntry { SessionId = session.Id, StudentId = student.Id,
+ Homework = status, HomeworkMissing = true };
+ entries.Add(entry);
+ var vm = new TeachingModeOutstandingHomeworkViewModel(group.Id, date,
+ new FakeStudents([student]),
+ new FakeMemberships([new GroupMembership { StudentId = student.Id, GroupId = group.Id }]),
+ new FakeSessions([session]), entries, isReadOnly);
+ return (vm, entries, entry);
+ }
}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/TeachingModeOutstandingHomeworkViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeOutstandingHomeworkViewModel.cs
index c5573be..1808328 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/TeachingModeOutstandingHomeworkViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeOutstandingHomeworkViewModel.cs
@@ -7,22 +7,67 @@ using LehrerApp.Core.Services;
namespace LehrerApp.Desktop.ViewModels.Groups;
-public sealed record OutstandingHomeworkRow(string StudentName, DateOnly Date, HomeworkStatus Status)
+/// Eine offene Nachreichung. Die Aktionen setzen nur die Endzustände, die beim Nachreichen
+/// sinnvoll sind: nachgereicht, endgültig nicht nachgereicht oder (bei ganz fehlender Hausaufgabe)
+/// teilweise angefertigt mit endgültig fehlendem Rest.
+public sealed class OutstandingHomeworkRow
{
+ public OutstandingHomeworkRow(Guid sessionId, Guid studentId, string studentName, DateOnly date,
+ HomeworkStatus status, bool canEdit, Action apply)
+ {
+ SessionId = sessionId;
+ StudentId = studentId;
+ StudentName = studentName;
+ Date = date;
+ Status = status;
+ CanEdit = canEdit;
+ MarkSubmittedCommand = new RelayCommand(() => apply(this, SubmittedStatus));
+ MarkFinalCommand = new RelayCommand(() => apply(this, FinalStatus));
+ MarkPartialCommand = new RelayCommand(() => apply(this, HomeworkStatus.PartialMissingOverdue));
+ }
+
+ public Guid SessionId { get; }
+ public Guid StudentId { get; }
+ public string StudentName { get; }
+ public DateOnly Date { get; }
+ public HomeworkStatus Status { get; }
public string DateLabel => Date.ToString("dd.MM.yyyy");
public string StatusLabel => HomeworkDisplay.Label(Status);
+
+ private bool IsPartial => Status is HomeworkStatus.PartiallyCompleted or HomeworkStatus.PartialMissingOverdue;
+ private bool IsFinal => Status is HomeworkStatus.MissingOverdue or HomeworkStatus.PartialMissingOverdue;
+
+ public HomeworkStatus SubmittedStatus =>
+ IsPartial ? HomeworkStatus.PartialSubmittedLate : HomeworkStatus.SubmittedLate;
+ public HomeworkStatus FinalStatus =>
+ IsPartial ? HomeworkStatus.PartialMissingOverdue : HomeworkStatus.MissingOverdue;
+
+ public string SubmittedLabel => IsPartial ? "◕ Rest nachgereicht" : "↺ Nachgereicht";
+ public string FinalLabel => IsPartial ? "✕ Rest endgültig nicht" : "✕ Endgültig nicht";
+
+ public bool CanEdit { get; }
+ public bool CanMarkFinal => CanEdit && !IsFinal;
+ public bool CanMarkPartial => CanEdit && !IsPartial;
+
+ public IRelayCommand MarkSubmittedCommand { get; }
+ public IRelayCommand MarkFinalCommand { get; }
+ public IRelayCommand MarkPartialCommand { get; }
}
/// Jüngste noch fehlende Hausaufgaben der aktuellen Lerngruppe. Liest bei jedem Öffnen
-/// und auf Wunsch erneut, damit die Anwesenheit der laufenden Stunde berücksichtigt wird.
+/// und auf Wunsch erneut, damit die Anwesenheit der laufenden Stunde berücksichtigt wird.
+/// Nachreichungen lassen sich direkt in der Liste abschließen, ohne die alte Sitzung zu öffnen;
+/// die jeweils letzte Änderung kann rückgängig gemacht werden.
public partial class TeachingModeOutstandingHomeworkViewModel(
Guid groupId, DateOnly lessonDate,
IStudentRepository students, IGroupMembershipRepository memberships,
- IParticipationSessionRepository sessions, IParticipationRepository participation) : ObservableObject
+ IParticipationSessionRepository sessions, IParticipationRepository participation,
+ bool isReadOnly = false) : ObservableObject
{
private const int LookbackDays = 120;
private const int MaxRows = 20;
private Guid? _currentSessionId;
+ private (Guid EntryId, Guid SessionId, Guid StudentId, HomeworkStatus? Homework, bool Missing)? _undo;
public ObservableCollection Rows { get; } = [];
public string Header => $"Offene Nachreichungen ({Rows.Count})";
@@ -30,6 +75,10 @@ public partial class TeachingModeOutstandingHomeworkViewModel(
[ObservableProperty] private bool _isExpanded;
[ObservableProperty] private bool _includeAbsentStudents;
+ [ObservableProperty] private string? _lastChangeLabel;
+
+ /// Wird nach jeder Statusänderung aufgerufen, damit andere Ansichten neu laden können.
+ public Action? OnHomeworkChanged { get; set; }
partial void OnIsExpandedChanged(bool value)
{
@@ -63,15 +112,55 @@ public partial class TeachingModeOutstandingHomeworkViewModel(
.Select(entry => (session.Date, Entry: entry)))
.Where(x => roster.ContainsKey(x.Entry.StudentId) &&
(IncludeAbsentStudents || !absentIds.Contains(x.Entry.StudentId)))
- .Select(x => (x.Date, x.Entry.StudentId, Status: HomeworkDisplay.Effective(x.Entry)))
+ .Select(x => (x.Date, x.Entry.SessionId, x.Entry.StudentId, Status: HomeworkDisplay.Effective(x.Entry)))
.Where(x => x.Status is { } status && HomeworkDisplay.CountsAsMissing(status))
.OrderByDescending(x => x.Date)
.ThenBy(x => roster[x.StudentId].FullName)
.Take(MaxRows);
foreach (var row in rows)
- Rows.Add(new OutstandingHomeworkRow(roster[row.StudentId].FullName, row.Date, row.Status!.Value));
+ Rows.Add(new OutstandingHomeworkRow(row.SessionId, row.StudentId, roster[row.StudentId].FullName,
+ row.Date, row.Status!.Value, !isReadOnly, Apply));
OnPropertyChanged(nameof(Header));
OnPropertyChanged(nameof(HasRows));
}
+
+ public bool CanUndo => _undo is not null;
+
+ public void Apply(OutstandingHomeworkRow row, HomeworkStatus status)
+ {
+ if (isReadOnly) return;
+ var entry = participation.GetBySessionAndStudent(row.SessionId, row.StudentId);
+ if (entry is null) return;
+ _undo = (entry.Id, entry.SessionId, entry.StudentId, entry.Homework, entry.HomeworkMissing);
+ entry.Homework = status;
+ entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(status);
+ entry.UpdatedAt = DateTime.UtcNow;
+ participation.Save(entry);
+ LastChangeLabel = $"{row.StudentName} ({row.DateLabel}): {HomeworkDisplay.Label(status)}";
+ OnPropertyChanged(nameof(CanUndo));
+ UndoCommand.NotifyCanExecuteChanged();
+ Refresh();
+ OnHomeworkChanged?.Invoke();
+ }
+
+ [RelayCommand(CanExecute = nameof(CanUndo))]
+ private void Undo()
+ {
+ if (_undo is not { } undo) return;
+ _undo = null;
+ var entry = participation.GetBySessionAndStudent(undo.SessionId, undo.StudentId);
+ if (entry is not null && entry.Id == undo.EntryId)
+ {
+ entry.Homework = undo.Homework;
+ entry.HomeworkMissing = undo.Missing;
+ entry.UpdatedAt = DateTime.UtcNow;
+ participation.Save(entry);
+ }
+ LastChangeLabel = null;
+ OnPropertyChanged(nameof(CanUndo));
+ UndoCommand.NotifyCanExecuteChanged();
+ Refresh();
+ OnHomeworkChanged?.Invoke();
+ }
}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs
index f2b0ec6..0e872d9 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs
@@ -60,6 +60,8 @@ public class TeachingModeViewModel
OutstandingHomework?.SetCurrentSessionId(SeatingPlan.SelectedSession?.Id);
OutstandingHomework?.Refresh();
SeatingPlan.OnAssessmentChanged = () => OutstandingHomework?.Refresh();
+ if (OutstandingHomework is not null)
+ OutstandingHomework.OnHomeworkChanged = Participation.RefreshCurrentGrid;
SeatingPlan.PropertyChanged += (_, args) =>
{
if (args.PropertyName != nameof(SeatingPlan.SelectedSession)) return;
diff --git a/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml
index 1af5afd..8ff6ede 100644
--- a/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml
+++ b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml
@@ -168,6 +168,12 @@
IsChecked="{Binding OutstandingHomework.IncludeAbsentStudents}" FontSize="12"/>
+
+
+
+
@@ -180,6 +186,19 @@
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml.cs b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml.cs
index a4953c0..5860dd2 100644
--- a/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml.cs
@@ -31,7 +31,8 @@ public partial class TeachingModeWindow : Window
App.Services.GetRequiredService(),
App.Services.GetRequiredService(),
App.Services.GetRequiredService(),
- App.Services.GetRequiredService());
+ App.Services.GetRequiredService(),
+ isReadOnly: !group.IsActive);
var window = new TeachingModeWindow
{
DataContext = new TeachingModeViewModel(lesson, group,
diff --git a/TODO.md b/TODO.md
index 6b6c078..8b99158 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2176,6 +2176,13 @@ Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
erfassten Anwesenheit in der aktuellen Sitzung sind standardmäßig ausgeblendet und können über
eine Checkbox eingeblendet werden. Das Panel lädt beim Öffnen und per Aktualisieren-Button neu;
nach dem Schnelldialog für Anwesenheit/Hausaufgaben ebenfalls.
+- Nutzer-Feedback: Nachreichungen ließen sich im Panel nur ansehen, nicht erfassen — dafür musste
+ man den Unterrichtsmodus verlassen und die alte Sitzung öffnen. Jede Zeile hat jetzt direkte
+ Aktionen, beschränkt auf die beim Nachreichen sinnvollen Endzustände: "Nachgereicht"
+ (`SubmittedLate` bzw. `PartialSubmittedLate` bei Teil-Hausaufgaben), "Endgültig nicht"
+ (`MissingOverdue`/`PartialMissingOverdue`, nur solange noch offen) und bei ganz fehlender
+ Hausaufgabe "Teilweise" (`PartialMissingOverdue`). Die jeweils letzte Änderung lässt sich über
+ "Rückgängig" zurücknehmen; bei archivierten Gruppen sind die Aktionen ausgeblendet.
- Regressionstests prüfen die 14-Tage-Grenze, unveränderte andere Stati und archivierte Gruppen
sowie die Panel-Filterung nach aktueller Anwesenheit, Mitgliedschaft und Nachreichstatus.