fix: Schnellbewerten-Dialog - Datenverlust, Pfeiltasten, Shortcuts

- GetRating las aus einem nie aktualisierten ParticipationEntry-
  Snapshot statt aus der laufend gepflegten Cells-Liste. Beim
  Zurückwechseln zu einem vorherigen Schüler zeigte der Dialog
  dadurch fälschlich keine Bewertung, obwohl sie tatsächlich
  gespeichert war. Jetzt liest GetRating aus Cells, das ungenutzte
  _entry-Feld entfernt.
- Pfeiltasten waren unbehandelt und fielen auf Avalonias
  Standard-Fokusnavigation durch: der Fokus sprang auf den
  Schließen-Button, ein Enter danach schloss den Dialog statt zum
  nächsten Schüler zu springen. Jetzt echte Navigation: ↑/↓ voriger/
  nächster Aspekt (neue PreviousAspect-Methode), ←/→ voriger/
  nächster Schüler - zugleich rechtsseitiges Pendant zu Q/W/E/R/T
  links. Hotkey-Legende im Dialog aktualisiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 09:14:00 +02:00
co-authored by Claude Sonnet 5
parent 762c8236fe
commit 4510c56bb4
3 changed files with 70 additions and 5 deletions
@@ -0,0 +1,48 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class QuickInputViewModelTests
{
[Fact]
public void ZurueckZuVorherigemSchueler_ZeigtBereitsGesetzteBewertung()
{
var aspects = new List<AspectColumnDef>
{
new(new ParticipationAspect { Key = "mitarbeit", Label = "Mitarbeit" }),
};
var rows = new List<ParticipationStudentRow>
{
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
new(Guid.NewGuid(), "Ben", new ParticipationEntry(), aspects, []),
};
var vm = new QuickInputViewModel(rows, aspects);
vm.SetRatingByNumber(4); // Scale5, 4. Stufe -> Rohwert 1 ("+"), Anna bewerten
vm.NextStudent(); // zu Ben
vm.PreviousStudent(); // zurück zu Anna
Assert.Equal(1, vm.AspectRows[0].Value);
}
[Fact]
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
{
var aspects = new List<AspectColumnDef>
{
new(new ParticipationAspect { Key = "a", Label = "A" }),
new(new ParticipationAspect { Key = "b", Label = "B" }),
};
var rows = new List<ParticipationStudentRow>
{
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
};
var vm = new QuickInputViewModel(rows, aspects);
vm.PreviousAspect(); // von Index 0 rückwärts -> letzter Aspekt (Umlauf)
Assert.Equal(1, vm.AspectIndex);
}
}
@@ -351,7 +351,6 @@ public partial class ParticipationStudentRow : ObservableObject
public Guid StudentId { get; } public Guid StudentId { get; }
public string Name { get; } public string Name { get; }
private readonly ParticipationEntry _entry;
private readonly IReadOnlyList<AspectColumnDef> _aspectDefs; private readonly IReadOnlyList<AspectColumnDef> _aspectDefs;
public ObservableCollection<RatingCell> Cells { get; } = []; public ObservableCollection<RatingCell> Cells { get; } = [];
@@ -375,7 +374,6 @@ public partial class ParticipationStudentRow : ObservableObject
{ {
StudentId = id; StudentId = id;
Name = name; Name = name;
_entry = entry;
_aspectDefs = aspects; _aspectDefs = aspects;
_homework = HomeworkDisplay.Effective(entry); _homework = HomeworkDisplay.Effective(entry);
_attendance = entry.Attendance; _attendance = entry.Attendance;
@@ -397,8 +395,12 @@ public partial class ParticipationStudentRow : ObservableObject
} }
} }
// Liest aus Cells (per SetValue laufend aktuell gehalten), NICHT aus dem ursprünglichen
// ParticipationEntry-Snapshot: SetRating() schreibt nur in Cells + feuert OnRatingChanged
// (der Callback speichert über eine EIGENE, neu aus dem Repository geladene Entry-Instanz —
// der hier ursprünglich referenzierte Entry-Snapshot bekommt diese Änderung nie zu sehen).
public int? GetRating(string key) => public int? GetRating(string key) =>
_entry.Ratings.FirstOrDefault(r => r.Key == key)?.Value; Cells.FirstOrDefault(c => c.AspectKey == key)?.Value;
public void SetRating(string key, int? value) public void SetRating(string key, int? value)
{ {
@@ -743,8 +745,8 @@ public partial class QuickInputViewModel : ObservableObject
AspectValueType.Points => $"09 Punkte eingeben (bis {CurrentAspectMaxPoints()})", AspectValueType.Points => $"09 Punkte eingeben (bis {CurrentAspectMaxPoints()})",
_ => "15 bewerten", _ => "15 bewerten",
}; };
return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste nächster Aspekt · " + return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste/↓ nächster Aspekt · ↑ vorheriger Aspekt · " +
"Enter nächster Schüler · Backspace vorheriger · +/ anpassen · Esc schließen"; "Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/ anpassen · Esc schließen";
} }
} }
@@ -862,6 +864,12 @@ public partial class QuickInputViewModel : ObservableObject
UpdateCurrentAspect(); UpdateCurrentAspect();
} }
public void PreviousAspect()
{
AspectIndex = (AspectIndex - 1 + _aspects.Count) % _aspects.Count;
UpdateCurrentAspect();
}
public void NextStudent() public void NextStudent()
{ {
if (StudentIndex >= _rows.Count - 1) return; if (StudentIndex >= _rows.Count - 1) return;
@@ -47,6 +47,15 @@ public partial class ParticipationQuickInputDialog : Window
break; break;
case Key.Back: vm.PreviousStudent(); e.Handled = true; break; case Key.Back: vm.PreviousStudent(); e.Handled = true; break;
// Rechtsseitiges Pendant zu Q/W/E/R/T (links)/Enter/Backspace/Leertaste: ohne diese
// Fälle fallen Pfeiltasten auf Avalonias Standard-Fokusnavigation durch und der Fokus
// springt auf den Schließen-Button — ein Enter danach schließt dann den Dialog statt
// zum nächsten Schüler zu springen.
case Key.Up: vm.PreviousAspect(); e.Handled = true; break;
case Key.Down: vm.NextAspect(); e.Handled = true; break;
case Key.Right: vm.NextStudent(); e.Handled = true; break;
case Key.Left: vm.PreviousStudent(); e.Handled = true; break;
case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break; case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break;
case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break; case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break;