Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0462bd2a1
commit
6c0a2dbd2e
6 files changed
+214
-6
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -7,22 +7,67 @@ using LehrerApp.Core.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
public sealed record OutstandingHomeworkRow(string StudentName, DateOnly Date, HomeworkStatus Status)
|
||||
/// <summary>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.</summary>
|
||||
public sealed class OutstandingHomeworkRow
|
||||
{
|
||||
public OutstandingHomeworkRow(Guid sessionId, Guid studentId, string studentName, DateOnly date,
|
||||
HomeworkStatus status, bool canEdit, Action<OutstandingHomeworkRow, HomeworkStatus> 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; }
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
/// 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.</summary>
|
||||
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<OutstandingHomeworkRow> 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;
|
||||
|
||||
/// <summary>Wird nach jeder Statusänderung aufgerufen, damit andere Ansichten neu laden können.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -168,6 +168,12 @@
|
||||
IsChecked="{Binding OutstandingHomework.IncludeAbsentStudents}" FontSize="12"/>
|
||||
<Button Content="Aktualisieren" Command="{Binding OutstandingHomework.RefreshCommand}"
|
||||
HorizontalAlignment="Left" FontSize="11" Padding="6,4"/>
|
||||
<Grid ColumnDefinitions="*,Auto" IsVisible="{Binding OutstandingHomework.LastChangeLabel, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Grid.Column="0" Text="{Binding OutstandingHomework.LastChangeLabel}"
|
||||
FontSize="11" Opacity="0.75" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Rückgängig" Command="{Binding OutstandingHomework.UndoCommand}"
|
||||
FontSize="11" Padding="6,2" Margin="6,0,0,0" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<TextBlock Text="Keine offenen Nachreichungen für die angezeigten Schüler."
|
||||
IsVisible="{Binding !OutstandingHomework.HasRows}" FontSize="12" TextWrapping="Wrap"/>
|
||||
<ItemsControl ItemsSource="{Binding OutstandingHomework.Rows}">
|
||||
@@ -180,6 +186,19 @@
|
||||
<TextBlock FontSize="11" Opacity="0.75" TextWrapping="Wrap">
|
||||
<Run Text="{Binding DateLabel}"/><Run Text=" · "/><Run Text="{Binding StatusLabel}"/>
|
||||
</TextBlock>
|
||||
<WrapPanel IsVisible="{Binding CanEdit}" Margin="0,3,0,0">
|
||||
<Button Content="{Binding SubmittedLabel}" Command="{Binding MarkSubmittedCommand}"
|
||||
FontSize="11" Padding="6,2" Margin="0,0,4,4"
|
||||
ToolTip.Tip="Hausaufgabe wurde nachgereicht"/>
|
||||
<Button Content="◒ Teilweise" Command="{Binding MarkPartialCommand}"
|
||||
IsVisible="{Binding CanMarkPartial}"
|
||||
FontSize="11" Padding="6,2" Margin="0,0,4,4"
|
||||
ToolTip.Tip="Teilweise angefertigt – der Rest wird nicht mehr nachgereicht"/>
|
||||
<Button Content="{Binding FinalLabel}" Command="{Binding MarkFinalCommand}"
|
||||
IsVisible="{Binding CanMarkFinal}"
|
||||
FontSize="11" Padding="6,2" Margin="0,0,4,4"
|
||||
ToolTip.Tip="Wird endgültig nicht mehr nachgereicht"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -31,7 +31,8 @@ public partial class TeachingModeWindow : Window
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>());
|
||||
App.Services.GetRequiredService<IParticipationRepository>(),
|
||||
isReadOnly: !group.IsActive);
|
||||
var window = new TeachingModeWindow
|
||||
{
|
||||
DataContext = new TeachingModeViewModel(lesson, group,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user