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:
@@ -554,6 +554,16 @@ public partial class DashboardViewModel : ObservableObject
|
||||
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Exam, exam.Date,
|
||||
exam.Title, g.Name, g.Id));
|
||||
}
|
||||
|
||||
foreach (var session in _participationSessions.GetByGroup(g.Id)
|
||||
.Where(s => s.Date >= gridStart && s.Date <= gridEnd))
|
||||
{
|
||||
var agg = Agg(session.Date);
|
||||
agg.HasSession = true;
|
||||
if (g.IsOwnClass) agg.IsOwnClassDay = true;
|
||||
agg.Details.Add(new CalendarEventItem(CalendarEventKind.ParticipationSession, session.Date,
|
||||
g.Name, session.Comment ?? "", g.Id));
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 42; i++)
|
||||
@@ -561,8 +571,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
var date = gridStart.AddDays(i);
|
||||
byDay.TryGetValue(date, out var agg);
|
||||
CalendarDays.Add(new CalendarDayCell(date, date.Month == firstOfMonth.Month, date == today,
|
||||
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.IsOwnClassDay ?? false,
|
||||
agg?.Details ?? []));
|
||||
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.HasSession ?? false,
|
||||
agg?.IsOwnClassDay ?? false, agg?.Details ?? []));
|
||||
}
|
||||
SelectCalendarDay(CalendarDays.FirstOrDefault(d => d.Date == today && d.IsCurrentMonth)
|
||||
?? CalendarDays.First(d => d.IsCurrentMonth));
|
||||
@@ -583,7 +593,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
{
|
||||
if (item is null) return;
|
||||
if (item.Kind == CalendarEventKind.Exam) OnNavigateToExam?.Invoke(item.GroupId);
|
||||
else OnNavigateToLesson?.Invoke(item.GroupId);
|
||||
else OnNavigateToLesson?.Invoke(item.GroupId); // auch für ParticipationSession: Tab "Mitarbeit"
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -633,6 +643,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
{
|
||||
public bool HasLesson;
|
||||
public bool HasExam;
|
||||
public bool HasSession;
|
||||
public bool IsOwnClassDay;
|
||||
public List<CalendarEventItem> Details { get; } = [];
|
||||
}
|
||||
@@ -716,12 +727,13 @@ public partial class CalendarDayCell : ObservableObject
|
||||
public bool IsToday { get; }
|
||||
public bool HasLesson { get; }
|
||||
public bool HasExam { get; }
|
||||
public bool HasSession { get; }
|
||||
public bool IsOwnClassDay { get; }
|
||||
public string Tooltip { get; }
|
||||
public IReadOnlyList<CalendarEventItem> Events { get; }
|
||||
|
||||
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
|
||||
bool hasLesson, bool hasExam, bool isOwnClassDay, List<CalendarEventItem> details)
|
||||
bool hasLesson, bool hasExam, bool hasSession, bool isOwnClassDay, List<CalendarEventItem> details)
|
||||
{
|
||||
Date = date;
|
||||
DayNumber = date.Day;
|
||||
@@ -729,6 +741,7 @@ public partial class CalendarDayCell : ObservableObject
|
||||
IsToday = isToday;
|
||||
HasLesson = hasLesson;
|
||||
HasExam = hasExam;
|
||||
HasSession = hasSession;
|
||||
IsOwnClassDay = isOwnClassDay;
|
||||
Events = details;
|
||||
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy")
|
||||
@@ -736,7 +749,7 @@ public partial class CalendarDayCell : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public enum CalendarEventKind { Lesson, Exam }
|
||||
public enum CalendarEventKind { Lesson, Exam, ParticipationSession }
|
||||
|
||||
public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, string title,
|
||||
string subtitle, Guid groupId)
|
||||
@@ -746,7 +759,12 @@ public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, str
|
||||
public string Title { get; } = title;
|
||||
public string Subtitle { get; } = subtitle;
|
||||
public Guid GroupId { get; } = groupId;
|
||||
public string KindLabel => Kind == CalendarEventKind.Exam ? "Klausur" : "Unterricht";
|
||||
public string KindLabel => Kind switch
|
||||
{
|
||||
CalendarEventKind.Exam => "Klausur",
|
||||
CalendarEventKind.ParticipationSession => "Sitzung",
|
||||
_ => "Unterricht"
|
||||
};
|
||||
}
|
||||
|
||||
public enum UpcomingDateKind { Exam, SupportPlan, Deadline }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -199,9 +199,11 @@
|
||||
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
|
||||
Text="{Binding DayNumber}" FontSize="12"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Ellipse Width="5" Height="5" Fill="#E53935" Margin="0,0,0,3"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||
IsVisible="{Binding HasExam}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="2" Margin="0,0,0,3"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Bottom">
|
||||
<Ellipse Width="5" Height="5" Fill="#1E88E5" IsVisible="{Binding HasSession}"/>
|
||||
<Ellipse Width="5" Height="5" Fill="#E53935" IsVisible="{Binding HasExam}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Button>
|
||||
@@ -218,6 +220,10 @@
|
||||
<Ellipse Width="7" Height="7" Fill="#E53935"/>
|
||||
<TextBlock Text="Klausur" FontSize="10" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Ellipse Width="7" Height="7" Fill="#1E88E5"/>
|
||||
<TextBlock Text="Sitzung" FontSize="10" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Border Width="10" Height="10" CornerRadius="3" BorderThickness="2"
|
||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"/>
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
||||
<Button Content="✎ Sitzung bearbeiten" Command="{Binding EditSessionCommand}"
|
||||
IsVisible="{Binding !IsReadOnly}" FontSize="11" Padding="8,3"/>
|
||||
<Button Content="🗑 Sitzung löschen" Command="{Binding DeleteSessionCommand}"
|
||||
IsVisible="{Binding !IsReadOnly}" FontSize="11" Padding="8,3"/>
|
||||
<ToggleButton Content="◇ Kompetenzen"
|
||||
IsChecked="{Binding CompetencyTagsVisible}"
|
||||
IsVisible="{Binding HasCompetencyCatalog}"
|
||||
|
||||
@@ -5,6 +5,7 @@ using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.Views.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
@@ -29,6 +30,7 @@ public partial class ParticipationTabView : UserControl
|
||||
vm.OnComputeGrade = ShowComputeGradeDialog;
|
||||
vm.OnOpenWizard = ShowWizardDialog;
|
||||
vm.OnManageAspects = ShowManageAspectsDialog;
|
||||
vm.OnConfirmDeleteSession = ConfirmDeleteSession;
|
||||
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
||||
vm.PropertyChanged += (_, pe) =>
|
||||
{
|
||||
@@ -367,4 +369,23 @@ public partial class ParticipationTabView : UserControl
|
||||
if (owner is not null)
|
||||
await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmDeleteSession(ParticipationSessionItem session, int entryCount)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return false;
|
||||
|
||||
var info = new ConfirmDialogInfo
|
||||
{
|
||||
Title = "Sitzung endgültig löschen?",
|
||||
Message = entryCount > 0
|
||||
? $"Die Sitzung „{session.Display}“ wird endgültig gelöscht. Dabei gehen auch " +
|
||||
$"alle {entryCount} bereits erfassten Schüler-Bewertungen dieser Sitzung " +
|
||||
"unwiderruflich verloren."
|
||||
: $"Die Sitzung „{session.Display}“ wird endgültig gelöscht.",
|
||||
ConfirmText = "Endgültig löschen",
|
||||
};
|
||||
var dialog = new ConfirmDialog { DataContext = info };
|
||||
return await dialog.ShowDialog<bool>(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@
|
||||
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="Sitzung erzeugen" Command="{Binding CreateParticipationSessionCommand}"
|
||||
IsEnabled="{Binding !IsReadOnly}"
|
||||
ToolTip.Tip="Legt eine Mitarbeitssitzung mit Datum und Thema dieser Stunde an."/>
|
||||
<Button Content="Löschen" Command="{Binding DeleteLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -41,6 +41,7 @@ public partial class PlanningTabView : UserControl
|
||||
vm.OnShowLesson = ShowLessonViewerDialog;
|
||||
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
|
||||
vm.OnAiAssist = ShowAiAssistDialog;
|
||||
vm.OnNotify = Notifications.ShowSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user