Sitzungen (3.3): aus Stunde erzeugen, löschen, Kalenderüberblick; Doppelstunden-Bugfix Serienerzeugung

Kapitel 3.3: neuer Button erzeugt eine Mitarbeitssitzung aus einer geplanten
Stunde (Datum/Thema, dupl.-sicher über Lesson.LessonId), Sitzungen lassen
sich jetzt mit Rückfrage bei vorhandenen Bewertungen löschen, und der
Dashboard-Kalender zeigt Sitzungen als dritten Termintyp neben Stunden/
Klausuren. Das Bearbeiten von Sitzungen war bereits vorhanden (undokumentiert
aus früherer Ad-hoc-Arbeit) und wird hier nur nachgezogen.

Bugfix: "Serie erzeugen" legte für Doppelstunden zwei Lessons mit gleichem
Datum an statt einer, da jede Stundenplan-Periode einzeln behandelt wurde.
Folgeperioden werden jetzt der Lesson der ersten Periode zugerechnet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 22:17:46 +02:00
co-authored by Claude Sonnet 5
parent 0faa3f54de
commit f73bd6c9ec
14 changed files with 330 additions and 24 deletions
@@ -54,6 +54,7 @@ public partial class ParticipationTabViewModel : ObservableObject
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
public Func<ParticipationTabViewModel, Task>? OnOpenWizard { get; set; }
public Func<ParticipationTabViewModel, Task>? OnManageAspects { get; set; }
public Func<ParticipationSessionItem, int, Task<bool>>? OnConfirmDeleteSession { get; set; }
public ParticipationTabViewModel(
IParticipationSessionRepository sessions,
@@ -120,6 +121,7 @@ public partial class ParticipationTabViewModel : ObservableObject
{
OnPropertyChanged(nameof(SelectedSessionDisplay));
EditSessionCommand.NotifyCanExecuteChanged();
DeleteSessionCommand.NotifyCanExecuteChanged();
if (value is null)
{
StudentRows.Clear();
@@ -282,7 +284,11 @@ public partial class ParticipationTabViewModel : ObservableObject
private bool CanEditSession() => !IsReadOnly && SelectedSession is not null;
partial void OnIsReadOnlyChanged(bool value) => EditSessionCommand.NotifyCanExecuteChanged();
partial void OnIsReadOnlyChanged(bool value)
{
EditSessionCommand.NotifyCanExecuteChanged();
DeleteSessionCommand.NotifyCanExecuteChanged();
}
[RelayCommand(CanExecute = nameof(CanQuickInput))]
private async Task QuickInput()
@@ -325,14 +331,20 @@ public partial class ParticipationTabViewModel : ObservableObject
if (SelectedSession is not null) LoadGrid(SelectedSession.Id);
}
[RelayCommand]
private void DeleteSession()
[RelayCommand(CanExecute = nameof(CanDeleteSession))]
private async Task DeleteSession()
{
if (SelectedSession is null) return;
var entryCount = _entries.GetBySession(SelectedSession.Id).Count;
if (OnConfirmDeleteSession is not null
&& !await OnConfirmDeleteSession(SelectedSession, entryCount)) return;
_sessions.Delete(SelectedSession.Id);
LoadSessions();
}
private bool CanDeleteSession() => !IsReadOnly && SelectedSession is not null;
public void SaveNote(Guid sessionId, Guid studentId, string note)
{
var entry = _entries.GetBySessionAndStudent(sessionId, studentId);
@@ -32,6 +32,7 @@ public partial class PlanningTabViewModel : ObservableObject
private readonly ISubjectRepository _subjects;
private readonly ICompetencyDomainRepository _competencyDomains;
private readonly AiSettingsService _aiSettings;
private readonly IParticipationSessionRepository _participationSessions;
private Guid _groupId;
@@ -75,14 +76,16 @@ public partial class PlanningTabViewModel : ObservableObject
public Func<Lesson, Task>? OnShowLesson { get; set; }
public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; }
public Func<Unit, Task<bool>>? OnAiAssist { get; set; }
public Action<string>? OnNotify { get; set; }
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
IGroupRepository groups, ISubjectRepository subjects,
ICompetencyDomainRepository competencyDomains, AiSettingsService aiSettings)
ICompetencyDomainRepository competencyDomains, AiSettingsService aiSettings,
IParticipationSessionRepository participationSessions)
{
_units = units; _lessons = lessons; _groups = groups;
_subjects = subjects; _competencyDomains = competencyDomains;
_aiSettings = aiSettings;
_aiSettings = aiSettings; _participationSessions = participationSessions;
}
public void Initialize(Guid groupId, bool isReadOnly = false)
@@ -367,6 +370,32 @@ public partial class PlanningTabViewModel : ObservableObject
_lessons.Save(lesson);
LoadUnits();
}
/// Ü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
/// auf dieselbe Stunde keine doppelte Sitzung anlegt, sondern nur darauf hinweist.
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
private void CreateParticipationSession()
{
if (SelectedLesson is null) return;
var lesson = SelectedLesson.Model;
var existing = _participationSessions.GetByGroup(lesson.GroupId)
.FirstOrDefault(s => s.LessonId == lesson.Id);
if (existing is not null)
{
OnNotify?.Invoke("Für diese Stunde existiert bereits eine Sitzung.");
return;
}
_participationSessions.Save(new ParticipationSession
{
GroupId = lesson.GroupId,
Date = lesson.Date,
LessonId = lesson.Id,
Comment = lesson.Topic,
});
OnNotify?.Invoke("Sitzung aus der Stunde erstellt.");
}
}
// ── Anzeige-DTOs ──────────────────────────────────────────────────────────────
@@ -1122,6 +1151,12 @@ public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
/// halten), wird nichts doppelt angelegt. Neue Stunden bekommen bewusst kein Thema — die
/// sonst übliche "Thema erforderlich"-Regel des manuellen "+Stunde"-Dialogs gilt hier nicht,
/// da diese Platzhalter zum späteren Ausfüllen gedacht sind.
///
/// Doppelstunden (zwei direkt aufeinanderfolgende Perioden desselben Wochentags/derselben
/// Gruppe im Stundenplan — dieselbe Konvention wie bei <see cref="LessonDialogViewModel.RecomputeTimeBudget"/>
/// und den Ferien-Badges in TimetableViewModel) bekommen bewusst nur EINE Lesson, verankert an
/// der ersten Periode: eine Folgeperiode, deren Vorgängerperiode ebenfalls im Stundenplan steht,
/// wird übersprungen, statt eine zweite Lesson mit gleichem Datum anzulegen.
[RelayCommand]
private void Save()
{
@@ -1136,6 +1171,9 @@ public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
if (slotsForGroup.Count == 0)
{ DateError = "Für diese Gruppe ist noch keine Stunde im Stundenplan eingetragen."; return; }
var periodsByWeekday = slotsForGroup.GroupBy(s => s.Weekday)
.ToDictionary(g => g.Key, g => g.Select(s => s.PeriodNumber).ToHashSet());
var schoolHolidays = _schoolHolidays.GetAll();
var publicHolidayDates = new HashSet<DateOnly>();
for (var year = from.Year; year <= to.Year; year++)
@@ -1152,6 +1190,10 @@ public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
foreach (var slot in slotsForGroup.Where(s => s.Weekday == date.DayOfWeek))
{
// Zweite (und weitere) Periode einer Doppelstunde: gehört zur Lesson der ersten
// Periode, keine eigene Lesson.
if (periodsByWeekday[slot.Weekday].Contains(slot.PeriodNumber - 1)) continue;
if (isFreeDay) { skippedHoliday++; continue; }
if (existing.Contains((date, (int?)slot.PeriodNumber))) { skippedExisting++; continue; }