Author SHA1 Message Date
adminandClaude Sonnet 5 2ccc3201d2 DatePicker durch CalendarDatePicker ersetzen (Zeitraum-Datumsfelder)
Avalonias Spinner-DatePicker übernimmt Änderungen nur über einen separaten
Häkchen-Button im Flyout und verwirft sie sonst stillschweigend. Betrifft
alle verbliebenen Zeitraum-Datumsfelder mit diesem Muster.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 14:46:53 +02:00
10 changed files with 23 additions and 117 deletions
@@ -294,44 +294,4 @@ public class PlanningTabViewModelTests
Assert.Single(sessions.GetByGroup(groupId)); Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal("Für diese Stunde existiert bereits eine Sitzung.", notified); Assert.Equal("Für diese Stunde existiert bereits eine Sitzung.", notified);
} }
[Fact]
public void CreateParticipationSession_AndereStundeAmSelbenTagBereitsVerknuepft_LegtKeineZweiteSitzungAn()
{
// Nutzer-Feedback: eine dritte Stunde am selben Tag (z.B. Vertretung) soll die bereits
// bestehende Sitzung des Tages weiterverwenden statt eine zweite anzulegen.
var groupId = Guid.NewGuid();
var group = new LearningGroup { Id = groupId, Name = "Testgruppe" };
var groups = new FakeGroups([group]);
var units = new FakeUnits();
var lessons = new FakeLessons();
var sessions = new FakeSessions([]);
var vm = new PlanningTabViewModel(units, lessons, groups, new FakeSubjects([]),
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), sessions);
vm.Initialize(groupId);
var unit = new Unit { GroupId = groupId, Title = "Optik" };
units.Add(unit);
var date = new DateOnly(2025, 9, 1);
var doubleLesson = new Lesson
{ UnitId = unit.Id, GroupId = groupId, Date = date, Topic = "Brechung" };
var thirdLesson = new Lesson
{ UnitId = unit.Id, GroupId = groupId, Date = date, Topic = "Vertretung" };
lessons.Add(doubleLesson);
lessons.Add(thirdLesson);
vm.RefreshPlanning(unit.Id, doubleLesson.Id);
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == doubleLesson.Id);
vm.CreateParticipationSessionCommand.Execute(null);
string? notified = null;
vm.OnNotify = m => notified = m;
vm.RefreshPlanning(unit.Id, thirdLesson.Id);
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == thirdLesson.Id);
vm.CreateParticipationSessionCommand.Execute(null);
var created = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(doubleLesson.Id, created.LessonId);
Assert.Equal("Für diesen Tag existiert bereits eine Sitzung.", notified);
}
} }
@@ -256,30 +256,6 @@ public sealed class SeatingPlanViewModelTests
Assert.Single(sessions.GetByGroup(groupId)); Assert.Single(sessions.GetByGroup(groupId));
} }
[Fact]
public void SelectOrCreateSessionForLesson_AndereStundeAmSelbenTagBereitsVerknuepft_LegtKeineZweiteSitzungAn()
{
// Nutzer-Feedback: eine dritte Stunde am selben Tag (z.B. durch Vertretung, eigene Lesson-
// Id) soll die bereits bestehende Sitzung der Doppelstunde weiterverwenden statt eine
// zweite, unabhängige Sitzung für denselben Tag anzulegen.
var groupId = Guid.NewGuid();
var today = DateOnly.FromDateTime(DateTime.Today);
var doubleLesson = new Lesson { GroupId = groupId, Date = today, Topic = "Redox" };
var thirdLesson = new Lesson { GroupId = groupId, Date = today, Topic = "Vertretung" };
var existingSession = new ParticipationSession
{ GroupId = groupId, Date = today, LessonId = doubleLesson.Id, Comment = "Redox" };
var sessions = new FakeSessions([existingSession]);
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.SelectOrCreateSessionForLesson(thirdLesson);
Assert.Equal(existingSession.Id, vm.SelectedSession?.Id);
Assert.Single(sessions.GetByGroup(groupId));
}
[Fact] [Fact]
public void SelectOrCreateSessionForLesson_KeineSitzungVorhanden_LegtVerknuepfteAnUndWaehltSieAus() public void SelectOrCreateSessionForLesson_KeineSitzungVorhanden_LegtVerknuepfteAnUndWaehltSieAus()
{ {
@@ -364,25 +364,16 @@ public partial class PlanningTabViewModel : ObservableObject
/// Übernimmt Datum + Thema der Stunde in eine neue Mitarbeitssitzung (3.3.1) — verknüpft über /// Übernimmt Datum + Thema der Stunde in eine neue Mitarbeitssitzung (3.3.1) — verknüpft über
/// das bisher ungenutzte Lesson.LessonId-Feld auf ParticipationSession, damit ein zweiter Klick /// das bisher ungenutzte Lesson.LessonId-Feld auf ParticipationSession, damit ein zweiter Klick
/// auf dieselbe Stunde keine doppelte Sitzung anlegt, sondern nur darauf hinweist. /// auf dieselbe Stunde keine doppelte Sitzung anlegt, sondern nur darauf hinweist.
///
/// Prüft dabei zusätzlich auf JEDE bereits an diesem Tag bestehende Sitzung, nicht nur eine
/// exakt mit `lesson.Id` verknüpfte (Nutzer-Feedback, analog
/// <see cref="SeatingPlanTabViewModel.SelectOrCreateSessionForLesson"/>): kommt neben einer
/// Doppelstunde noch eine dritte Stunde desselben Tages hinzu (eigene `Lesson`, z.B. durch
/// Vertretung), soll das nicht zu einer zweiten Mitarbeitssitzung für den Tag führen.
[RelayCommand(CanExecute = nameof(HasSelectedLesson))] [RelayCommand(CanExecute = nameof(HasSelectedLesson))]
private void CreateParticipationSession() private void CreateParticipationSession()
{ {
if (SelectedLesson is null) return; if (SelectedLesson is null) return;
var lesson = SelectedLesson.Model; var lesson = SelectedLesson.Model;
var sessionsForGroup = _participationSessions.GetByGroup(lesson.GroupId); var existing = _participationSessions.GetByGroup(lesson.GroupId)
var existing = sessionsForGroup.FirstOrDefault(s => s.LessonId == lesson.Id) .FirstOrDefault(s => s.LessonId == lesson.Id);
?? sessionsForGroup.FirstOrDefault(s => s.Date == lesson.Date);
if (existing is not null) if (existing is not null)
{ {
OnNotify?.Invoke(existing.LessonId == lesson.Id OnNotify?.Invoke("Für diese Stunde existiert bereits eine Sitzung.");
? "Für diese Stunde existiert bereits eine Sitzung."
: "Für diesen Tag existiert bereits eine Sitzung.");
return; return;
} }
@@ -141,18 +141,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
/// erlaubt: welche Stunde gemeint ist, steht durch die explizite Auswahl der Lehrkraft /// erlaubt: welche Stunde gemeint ist, steht durch die explizite Auswahl der Lehrkraft
/// (Klick auf "Unterrichtsmodus starten" für genau diese Stunde) bereits unzweideutig fest - /// (Klick auf "Unterrichtsmodus starten" für genau diese Stunde) bereits unzweideutig fest -
/// keine Geistersitzungs-Gefahr wie beim bloßen Öffnen eines Tabs. /// keine Geistersitzungs-Gefahr wie beim bloßen Öffnen eines Tabs.
///
/// Fällt bewusst auf JEDE an diesem Tag bereits bestehende Sitzung zurück, nicht nur auf eine
/// exakt mit `lesson.Id` verknüpfte (Nutzer-Feedback): kommt neben einer Doppelstunde noch eine
/// dritte Stunde am selben Tag hinzu (z.B. Vertretung, eigene `Lesson` mit eigener Id), soll
/// keine zweite Mitarbeitssitzung für denselben Tag entstehen — die Lehrkraft passt stattdessen
/// die Einschätzung der bereits bestehenden Sitzung an. Das entspricht dem Verhalten von
/// <see cref="EnsureTodaySession"/>, das schon immer pro Tag statt pro Stunde arbeitet.
/// </summary> /// </summary>
public void SelectOrCreateSessionForLesson(Lesson lesson) public void SelectOrCreateSessionForLesson(Lesson lesson)
{ {
var existing = TodaySessions.FirstOrDefault(s => s.LessonId == lesson.Id) var existing = TodaySessions.FirstOrDefault(s => s.LessonId == lesson.Id);
?? TodaySessions.FirstOrDefault(s => s.Date == lesson.Date);
if (existing is not null) { SelectedSession = existing; return; } if (existing is not null) { SelectedSession = existing; return; }
if (!IsEditable) return; if (!IsEditable) return;
@@ -45,9 +45,9 @@
<ComboBoxItem Content="Heute"/><ComboBoxItem Content="Letzte 7 Tage"/><ComboBoxItem Content="Letzte 30 Tage"/> <ComboBoxItem Content="Heute"/><ComboBoxItem Content="Letzte 7 Tage"/><ComboBoxItem Content="Letzte 30 Tage"/>
</ComboBox> </ComboBox>
<TextBlock Grid.Column="1" Text="von" VerticalAlignment="Center" Opacity="0.6"/> <TextBlock Grid.Column="1" Text="von" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="2" SelectedDate="{Binding StartDate}" HorizontalAlignment="Stretch"/> <CalendarDatePicker Grid.Column="2" SelectedDate="{Binding StartDate}" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Column="3" Text="bis" VerticalAlignment="Center" Opacity="0.6"/> <TextBlock Grid.Column="3" Text="bis" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="4" SelectedDate="{Binding EndDate}" HorizontalAlignment="Stretch"/> <CalendarDatePicker Grid.Column="4" SelectedDate="{Binding EndDate}" HorizontalAlignment="Stretch"/>
</Grid> </Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8"> <Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
<TextBox Grid.Column="0" Text="{Binding StudentFilter, Mode=TwoWay}" PlaceholderText="Schüler*in filtern…"/> <TextBox Grid.Column="0" Text="{Binding StudentFilter, Mode=TwoWay}" PlaceholderText="Schüler*in filtern…"/>
@@ -45,9 +45,9 @@
<ComboBoxItem Content="Heute"/><ComboBoxItem Content="Letzte 7 Tage"/><ComboBoxItem Content="Letzte 30 Tage"/> <ComboBoxItem Content="Heute"/><ComboBoxItem Content="Letzte 7 Tage"/><ComboBoxItem Content="Letzte 30 Tage"/>
</ComboBox> </ComboBox>
<TextBlock Grid.Column="1" Text="von" VerticalAlignment="Center" Opacity="0.6"/> <TextBlock Grid.Column="1" Text="von" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="2" SelectedDate="{Binding StartDate}" HorizontalAlignment="Stretch"/> <CalendarDatePicker Grid.Column="2" SelectedDate="{Binding StartDate}" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Column="3" Text="bis" VerticalAlignment="Center" Opacity="0.6"/> <TextBlock Grid.Column="3" Text="bis" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="4" SelectedDate="{Binding EndDate}" HorizontalAlignment="Stretch"/> <CalendarDatePicker Grid.Column="4" SelectedDate="{Binding EndDate}" HorizontalAlignment="Stretch"/>
</Grid> </Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8"> <Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
<TextBox Grid.Column="0" Text="{Binding StudentFilter, Mode=TwoWay}" PlaceholderText="Schüler*in filtern…"/> <TextBox Grid.Column="0" Text="{Binding StudentFilter, Mode=TwoWay}" PlaceholderText="Schüler*in filtern…"/>
@@ -12,9 +12,9 @@
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/> FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8"> <StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
<CalendarDatePicker SelectedDate="{Binding StartDate}"/> <DatePicker SelectedDate="{Binding StartDate}"/>
<TextBlock Text="bis" VerticalAlignment="Center"/> <TextBlock Text="bis" VerticalAlignment="Center"/>
<CalendarDatePicker SelectedDate="{Binding EndDate}"/> <DatePicker SelectedDate="{Binding EndDate}"/>
<Button Content="Fehlzeiten laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/> <Button Content="Fehlzeiten laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
</StackPanel> </StackPanel>
<Grid Grid.Row="2" ColumnDefinitions="Auto,1.2*,1.2*,80,80,1.1*,1.1*" ColumnSpacing="8" Margin="4,0"> <Grid Grid.Row="2" ColumnDefinitions="Auto,1.2*,1.2*,80,80,1.1*,1.1*" ColumnSpacing="8" Margin="4,0">
@@ -18,7 +18,7 @@
<DataTemplate x:DataType="svc:UntisTeacherDto"><TextBlock Text="{Binding DisplayName}"/></DataTemplate> <DataTemplate x:DataType="svc:UntisTeacherDto"><TextBlock Text="{Binding DisplayName}"/></DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<DatePicker Grid.Column="1" SelectedDate="{Binding WeekDate}"/> <CalendarDatePicker Grid.Column="1" SelectedDate="{Binding WeekDate}"/>
<Button Grid.Column="2" Content="Woche laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/> <Button Grid.Column="2" Content="Woche laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
</Grid> </Grid>
<ScrollViewer Grid.Row="2"> <ScrollViewer Grid.Row="2">
@@ -12,9 +12,9 @@
Text="Nur eigene WebUntis-Einträge (Benutzer = eigener Login). Zeilen ohne automatische Zuordnung bitte manuell einem/einer Schüler*in zuweisen. Bereits lokal vorhandene Einträge sind gesperrt."/> Text="Nur eigene WebUntis-Einträge (Benutzer = eigener Login). Zeilen ohne automatische Zuordnung bitte manuell einem/einer Schüler*in zuweisen. Bereits lokal vorhandene Einträge sind gesperrt."/>
</StackPanel> </StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8"> <StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
<DatePicker SelectedDate="{Binding StartDate}"/> <CalendarDatePicker SelectedDate="{Binding StartDate}"/>
<TextBlock Text="bis" VerticalAlignment="Center"/> <TextBlock Text="bis" VerticalAlignment="Center"/>
<DatePicker SelectedDate="{Binding EndDate}"/> <CalendarDatePicker SelectedDate="{Binding EndDate}"/>
<Button Content="Klassenbucheinträge laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/> <Button Content="Klassenbucheinträge laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
</StackPanel> </StackPanel>
+10 -23
View File
@@ -1393,13 +1393,16 @@ Zugangsdaten; Schülerdaten, Fehlzeiten und der unverschlüsselte CSV-Report pas
unentschuldigt übernommen werden; fremde/ganz­tägige Abwesenheiten erzeugen keine lokale Stunde. unentschuldigt übernommen werden; fremde/ganz­tägige Abwesenheiten erzeugen keine lokale Stunde.
**Nachtrag (September 2026, Nutzer-Feedback) — Zeitraum-Datumsfelder ließen sich nicht ändern:** **Nachtrag (September 2026, Nutzer-Feedback) — Zeitraum-Datumsfelder ließen sich nicht ändern:**
die beiden `DatePicker` oben im Dialog ([WebUntisLessonAbsenceComparisonDialog.axaml](LehrerApp.Desktop/Views/Groups/WebUntisLessonAbsenceComparisonDialog.axaml)) Avalonias WinUI-artiger Spinner-`DatePicker` übernimmt Änderungen im Tag/Monat/Jahr-Flyout nur über
sind Avalonias WinUI-artiger Spinner-`DatePicker` — Änderungen im Tag/Monat/Jahr-Flyout werden nur einen separaten Häkchen-Button; ein Klick daneben verwirft sie stillschweigend. Alle verbliebenen
über einen separaten Häkchen-Button im Flyout übernommen, ein Klick daneben verwirft sie Zeitraum-Datumsfelder mit diesem `DatePicker` auf `CalendarDatePicker` umgestellt (gleiche
stillschweigend. Ersetzt durch `CalendarDatePicker` (gleiche `DateTimeOffset?`-Bindung, kein `DateTimeOffset?`-Bindung, kein Typwechsel nötig) — bereits das etablierte Muster für frei wählbare
Typwechsel nötig) — bereits das etablierte Muster für frei wählbare Einzeldatumsfelder in dieser Einzeldatumsfelder in dieser Codebasis (`WithdrawStudentDialog`, `CreateLetterDialog`, 7.2.3), dort
Codebasis (`WithdrawStudentDialog`, `CreateLetterDialog`, 7.2.3), dort ohne dieses Problem, da ohne dieses Problem, da ein Klick auf einen Kalendertag sofort übernimmt statt einen
ein Klick auf einen Kalendertag sofort übernimmt statt einen Bestätigungsschritt zu verlangen. Bestätigungsschritt zu verlangen. Betroffen: [ClassTeacherRegisterView.axaml](LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherRegisterView.axaml),
[ClassTeacherAbsencesView.axaml](LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherAbsencesView.axaml),
[WebUntisTimetableImportDialog.axaml](LehrerApp.Desktop/Views/Planning/WebUntisTimetableImportDialog.axaml)
und [WebUntisDocumentationComparisonDialog.axaml](LehrerApp.Desktop/Views/Students/WebUntisDocumentationComparisonDialog.axaml).
**Nachtrag zu 4.3, Fehlzeiten je Unterricht (August 2026):** Der ursprüngliche Fehlzeitenabgleich **Nachtrag zu 4.3, Fehlzeiten je Unterricht (August 2026):** Der ursprüngliche Fehlzeitenabgleich
rief `getTimetableWithAbsences` ohne Element auf und bekam damit den kompletten Lehrer-Stundenplan rief `getTimetableWithAbsences` ohne Element auf und bekam damit den kompletten Lehrer-Stundenplan
@@ -2150,22 +2153,6 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
den vollen Verlaufsplan-Editor öffnen müssen. Neue kleine den vollen Verlaufsplan-Editor öffnen müssen. Neue kleine
`TeachingModeHomeworkViewModel : ObservableObject`, da `TeachingModeViewModel` selbst (wie `TeachingModeHomeworkViewModel : ObservableObject`, da `TeachingModeViewModel` selbst (wie
`LessonViewerViewModel`) keine Bindable-Basisklasse hat. `LessonViewerViewModel`) keine Bindable-Basisklasse hat.
**Nachtrag (September 2026, Nutzer-Feedback) — zweite Sitzung bei zusätzlicher Stunde am
selben Tag:** `SelectOrCreateSessionForLesson` prüfte bisher ausschließlich auf eine bereits
exakt mit `lesson.Id` verknüpfte Sitzung. Kam neben einer Doppelstunde noch eine dritte
Stunde desselben Tages hinzu (eigene `Lesson`-Id, z.B. durch Vertretung), fand die Methode
keinen Treffer und legte eine zweite, unabhängige Mitarbeitssitzung für denselben Tag an —
dadurch zeigten Unterrichtsmodus und der normale Mitarbeit-Tab der Gruppe (der über
`ParticipationTabViewModel.LoadSessions()` unabhängig davon die nach Datum jüngste Sitzung
wählt) je nach Reihenfolge unterschiedliche Sitzungen für denselben Tag. Nutzer-Entscheidung:
strikt nach Datum statt nach Stunde gehen — eine zusätzliche Stunde am selben Tag (Vertretung
o.ä.) bekommt keine eigene Sitzung, sondern die Lehrkraft passt die Einschätzung der bereits
bestehenden Sitzung des Tages an. `SelectOrCreateSessionForLesson` fällt jetzt, wenn keine
exakt verknüpfte Sitzung existiert, zusätzlich auf jede andere Sitzung desselben Tages
zurück (Verhalten analog zu `EnsureTodaySession`, das schon immer pro Tag statt pro Stunde
arbeitet). Dieselbe Lücke bestand in `PlanningTabViewModel.CreateParticipationSession`
("Sitzung aus der Stunde erstellen", 3.3.1) — gleicher Fallback dort ergänzt.
- [x] **4.5.24** Popup-Menü im Wochenraster für Unterrichtsansicht/Sitzplan/Planung/Planungsviewer - [x] **4.5.24** Popup-Menü im Wochenraster für Unterrichtsansicht/Sitzplan/Planung/Planungsviewer
(August 2026, Nutzer-Feedback, zweite Runde). Die erste Fassung hatte das Problem am (August 2026, Nutzer-Feedback, zweite Runde). Die erste Fassung hatte das Problem am
falschen Ort gelöst — ein Dropdown in der "Heute"-**Tagesliste** (unten angedockt), obwohl falschen Ort gelöst — ein Dropdown in der "Heute"-**Tagesliste** (unten angedockt), obwohl