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:
@@ -40,7 +40,8 @@ public sealed class DashboardViewModelTests
|
|||||||
FakeReportGrades? reportGrades = null, FakeMemberships? memberships = null,
|
FakeReportGrades? reportGrades = null, FakeMemberships? memberships = null,
|
||||||
FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
|
FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
|
||||||
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
|
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
|
||||||
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null)
|
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
|
||||||
|
FakeSessions? sessions = null)
|
||||||
{
|
{
|
||||||
lessons ??= new FakeLessons();
|
lessons ??= new FakeLessons();
|
||||||
lessons.Add(lesson);
|
lessons.Add(lesson);
|
||||||
@@ -48,7 +49,7 @@ public sealed class DashboardViewModelTests
|
|||||||
new FakeGroups([group]), new FakeSubjects([]), lessons,
|
new FakeGroups([group]), new FakeSubjects([]), lessons,
|
||||||
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
|
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
|
||||||
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
|
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
|
||||||
tasks ?? new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
|
tasks ?? new FakeWorkTasks(), sessions ?? new FakeSessions([]), new FakeEntries(),
|
||||||
students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
||||||
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
||||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
||||||
@@ -223,6 +224,25 @@ public sealed class DashboardViewModelTests
|
|||||||
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test");
|
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Kalender_ZeigtMitarbeitssitzungenAlsEigenenTermintyp()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var sessionDate = today.AddDays(3);
|
||||||
|
var session = new ParticipationSession { GroupId = group.Id, Date = sessionDate, Comment = "Aufsatz" };
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||||
|
sessions: new FakeSessions([session]));
|
||||||
|
|
||||||
|
var day = vm.CalendarDays.Single(d => d.Date == sessionDate);
|
||||||
|
Assert.True(day.HasSession);
|
||||||
|
|
||||||
|
vm.SelectCalendarDayCommand.Execute(day);
|
||||||
|
|
||||||
|
Assert.Contains(vm.SelectedDayEvents,
|
||||||
|
e => e.Kind == CalendarEventKind.ParticipationSession && e.Subtitle == "Aufsatz");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DashboardKacheln_SichtbarkeitUndReihenfolgeWerdenGespeichert()
|
public void DashboardKacheln_SichtbarkeitUndReihenfolgeWerdenGespeichert()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public sealed class GenerateLessonSeriesDialogViewModelTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Save_Doppelstunde_ErzeugtZweiStundenAmSelbenTagMitUnterschiedlicherStundennummer()
|
public void Save_Doppelstunde_ErzeugtNurEineStundeVerankertAnDerErstenPeriode()
|
||||||
{
|
{
|
||||||
var groupId = Guid.NewGuid();
|
var groupId = Guid.NewGuid();
|
||||||
var unitId = Guid.NewGuid();
|
var unitId = Guid.NewGuid();
|
||||||
@@ -98,9 +98,50 @@ public sealed class GenerateLessonSeriesDialogViewModelTests
|
|||||||
|
|
||||||
vm.SaveCommand.Execute(null);
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(1, vm.Result!.Created);
|
||||||
|
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).ToList();
|
||||||
|
Assert.Equal([3], created);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_Dreifachstunde_ErzeugtNurEineStundeVerankertAnDerErstenPeriode()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var unitId = Guid.NewGuid();
|
||||||
|
var from = new DateOnly(2025, 9, 1);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 3 });
|
||||||
|
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 4 });
|
||||||
|
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 5 });
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
var vm = BuildVm(slots, lessons, new FakeSchoolHolidays(), unitId, groupId, from, from);
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(1, vm.Result!.Created);
|
||||||
|
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).ToList();
|
||||||
|
Assert.Equal([3], created);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_ZweiGetrennteEinzelstundenAmSelbenTagBleibenGetrennt()
|
||||||
|
{
|
||||||
|
// Perioden 1 und 3 sind NICHT aufeinanderfolgend (Periode 2 fehlt) — keine Doppelstunde,
|
||||||
|
// beide bekommen eine eigene Lesson.
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var unitId = Guid.NewGuid();
|
||||||
|
var from = new DateOnly(2025, 9, 1);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 3 });
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
var vm = BuildVm(slots, lessons, new FakeSchoolHolidays(), unitId, groupId, from, from);
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
Assert.Equal(2, vm.Result!.Created);
|
Assert.Equal(2, vm.Result!.Created);
|
||||||
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).OrderBy(n => n).ToList();
|
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).OrderBy(n => n).ToList();
|
||||||
Assert.Equal([3, 4], created);
|
Assert.Equal([1, 3], created);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public sealed class GroupDetailViewModelTests
|
|||||||
students, memberships, groups, new FakeCompetencyDomains()),
|
students, memberships, groups, new FakeCompetencyDomains()),
|
||||||
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
||||||
new PlanningTabViewModel(new FakeUnits(), new FakeLessons(), groups, subjects,
|
new PlanningTabViewModel(new FakeUnits(), new FakeLessons(), groups, subjects,
|
||||||
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService()),
|
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), new FakeSessions([])),
|
||||||
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
||||||
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
||||||
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
||||||
|
|||||||
@@ -59,4 +59,49 @@ public sealed class ParticipationSessionEditingTests
|
|||||||
Assert.Equal("Nachbesprechung Elektrizität", sessions.GetById(session.Id)?.Comment);
|
Assert.Equal("Nachbesprechung Elektrizität", sessions.GetById(session.Id)?.Comment);
|
||||||
Assert.Contains("Nachbesprechung Elektrizität", vm.SelectedSessionDisplay);
|
Assert.Contains("Nachbesprechung Elektrizität", vm.SelectedSessionDisplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoeschenBefehl_FragtBestaetigungAbUndEntferntSessionMitsamtBewertungen()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2026/27" };
|
||||||
|
var session = new ParticipationSession { GroupId = group.Id, Date = new DateOnly(2026, 8, 19) };
|
||||||
|
var sessions = new FakeSessions([session]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = Guid.NewGuid() });
|
||||||
|
var vm = new ParticipationTabViewModel(
|
||||||
|
sessions, entries, new FakeAspects(), new FakeStudents([]),
|
||||||
|
new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains());
|
||||||
|
vm.Initialize(group.Id, group.SchoolYear);
|
||||||
|
vm.SelectedSession = vm.Sessions.Single(s => s.Id == session.Id);
|
||||||
|
|
||||||
|
int? passedEntryCount = null;
|
||||||
|
vm.OnConfirmDeleteSession = (_, count) =>
|
||||||
|
{
|
||||||
|
passedEntryCount = count;
|
||||||
|
return Task.FromResult(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.DeleteSessionCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Equal(1, passedEntryCount);
|
||||||
|
Assert.Null(sessions.GetById(session.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoeschenBefehl_BrichtBeiAbgelehnterBestaetigungAb()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2026/27" };
|
||||||
|
var session = new ParticipationSession { GroupId = group.Id, Date = new DateOnly(2026, 8, 19) };
|
||||||
|
var sessions = new FakeSessions([session]);
|
||||||
|
var vm = new ParticipationTabViewModel(
|
||||||
|
sessions, new FakeEntries(), new FakeAspects(), new FakeStudents([]),
|
||||||
|
new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains());
|
||||||
|
vm.Initialize(group.Id, group.SchoolYear);
|
||||||
|
vm.SelectedSession = vm.Sessions.Single(s => s.Id == session.Id);
|
||||||
|
vm.OnConfirmDeleteSession = (_, _) => Task.FromResult(false);
|
||||||
|
|
||||||
|
await vm.DeleteSessionCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.NotNull(sessions.GetById(session.Id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class PlanningTabViewModelTests
|
|||||||
var lessons = new FakeLessons();
|
var lessons = new FakeLessons();
|
||||||
|
|
||||||
var vm = new PlanningTabViewModel(units, lessons, groups, subjects, competencyDomains,
|
var vm = new PlanningTabViewModel(units, lessons, groups, subjects, competencyDomains,
|
||||||
TestSupport.BuildAiSettingsService());
|
TestSupport.BuildAiSettingsService(), new FakeSessions([]));
|
||||||
vm.Initialize(groupId);
|
vm.Initialize(groupId);
|
||||||
|
|
||||||
return (vm, units, lessons, groupId);
|
return (vm, units, lessons, groupId);
|
||||||
@@ -227,4 +227,71 @@ public class PlanningTabViewModelTests
|
|||||||
|
|
||||||
Assert.Equal(["L->AB01", "Plenum"], vm.KnownShorthands);
|
Assert.Equal(["L->AB01", "Plenum"], vm.KnownShorthands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateParticipationSession_LegtSitzungMitDatumUndThemaDerStundeAn()
|
||||||
|
{
|
||||||
|
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 lesson = new Lesson
|
||||||
|
{
|
||||||
|
UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "Brechung",
|
||||||
|
};
|
||||||
|
lessons.Add(lesson);
|
||||||
|
vm.RefreshPlanning(unit.Id, lesson.Id);
|
||||||
|
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == lesson.Id);
|
||||||
|
|
||||||
|
string? notified = null;
|
||||||
|
vm.OnNotify = m => notified = m;
|
||||||
|
|
||||||
|
vm.CreateParticipationSessionCommand.Execute(null);
|
||||||
|
|
||||||
|
var created = Assert.Single(sessions.GetByGroup(groupId));
|
||||||
|
Assert.Equal(lesson.Id, created.LessonId);
|
||||||
|
Assert.Equal(lesson.Date, created.Date);
|
||||||
|
Assert.Equal("Brechung", created.Comment);
|
||||||
|
Assert.Equal("Sitzung aus der Stunde erstellt.", notified);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateParticipationSession_ZweiterAufrufLegtKeineDoppelteSitzungAn()
|
||||||
|
{
|
||||||
|
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 lesson = new Lesson
|
||||||
|
{
|
||||||
|
UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "Brechung",
|
||||||
|
};
|
||||||
|
lessons.Add(lesson);
|
||||||
|
vm.RefreshPlanning(unit.Id, lesson.Id);
|
||||||
|
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == lesson.Id);
|
||||||
|
|
||||||
|
vm.CreateParticipationSessionCommand.Execute(null);
|
||||||
|
string? notified = null;
|
||||||
|
vm.OnNotify = m => notified = m;
|
||||||
|
vm.CreateParticipationSessionCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Single(sessions.GetByGroup(groupId));
|
||||||
|
Assert.Equal("Für diese Stunde existiert bereits eine Sitzung.", notified);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -554,6 +554,16 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Exam, exam.Date,
|
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Exam, exam.Date,
|
||||||
exam.Title, g.Name, g.Id));
|
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++)
|
for (var i = 0; i < 42; i++)
|
||||||
@@ -561,8 +571,8 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
var date = gridStart.AddDays(i);
|
var date = gridStart.AddDays(i);
|
||||||
byDay.TryGetValue(date, out var agg);
|
byDay.TryGetValue(date, out var agg);
|
||||||
CalendarDays.Add(new CalendarDayCell(date, date.Month == firstOfMonth.Month, date == today,
|
CalendarDays.Add(new CalendarDayCell(date, date.Month == firstOfMonth.Month, date == today,
|
||||||
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.IsOwnClassDay ?? false,
|
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.HasSession ?? false,
|
||||||
agg?.Details ?? []));
|
agg?.IsOwnClassDay ?? false, agg?.Details ?? []));
|
||||||
}
|
}
|
||||||
SelectCalendarDay(CalendarDays.FirstOrDefault(d => d.Date == today && d.IsCurrentMonth)
|
SelectCalendarDay(CalendarDays.FirstOrDefault(d => d.Date == today && d.IsCurrentMonth)
|
||||||
?? CalendarDays.First(d => d.IsCurrentMonth));
|
?? CalendarDays.First(d => d.IsCurrentMonth));
|
||||||
@@ -583,7 +593,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (item is null) return;
|
if (item is null) return;
|
||||||
if (item.Kind == CalendarEventKind.Exam) OnNavigateToExam?.Invoke(item.GroupId);
|
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]
|
[RelayCommand]
|
||||||
@@ -633,6 +643,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
public bool HasLesson;
|
public bool HasLesson;
|
||||||
public bool HasExam;
|
public bool HasExam;
|
||||||
|
public bool HasSession;
|
||||||
public bool IsOwnClassDay;
|
public bool IsOwnClassDay;
|
||||||
public List<CalendarEventItem> Details { get; } = [];
|
public List<CalendarEventItem> Details { get; } = [];
|
||||||
}
|
}
|
||||||
@@ -716,12 +727,13 @@ public partial class CalendarDayCell : ObservableObject
|
|||||||
public bool IsToday { get; }
|
public bool IsToday { get; }
|
||||||
public bool HasLesson { get; }
|
public bool HasLesson { get; }
|
||||||
public bool HasExam { get; }
|
public bool HasExam { get; }
|
||||||
|
public bool HasSession { get; }
|
||||||
public bool IsOwnClassDay { get; }
|
public bool IsOwnClassDay { get; }
|
||||||
public string Tooltip { get; }
|
public string Tooltip { get; }
|
||||||
public IReadOnlyList<CalendarEventItem> Events { get; }
|
public IReadOnlyList<CalendarEventItem> Events { get; }
|
||||||
|
|
||||||
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
|
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;
|
Date = date;
|
||||||
DayNumber = date.Day;
|
DayNumber = date.Day;
|
||||||
@@ -729,6 +741,7 @@ public partial class CalendarDayCell : ObservableObject
|
|||||||
IsToday = isToday;
|
IsToday = isToday;
|
||||||
HasLesson = hasLesson;
|
HasLesson = hasLesson;
|
||||||
HasExam = hasExam;
|
HasExam = hasExam;
|
||||||
|
HasSession = hasSession;
|
||||||
IsOwnClassDay = isOwnClassDay;
|
IsOwnClassDay = isOwnClassDay;
|
||||||
Events = details;
|
Events = details;
|
||||||
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy")
|
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,
|
public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, string title,
|
||||||
string subtitle, Guid groupId)
|
string subtitle, Guid groupId)
|
||||||
@@ -746,7 +759,12 @@ public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, str
|
|||||||
public string Title { get; } = title;
|
public string Title { get; } = title;
|
||||||
public string Subtitle { get; } = subtitle;
|
public string Subtitle { get; } = subtitle;
|
||||||
public Guid GroupId { get; } = groupId;
|
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 }
|
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>? OnComputeGrade { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnOpenWizard { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnOpenWizard { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnManageAspects { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnManageAspects { get; set; }
|
||||||
|
public Func<ParticipationSessionItem, int, Task<bool>>? OnConfirmDeleteSession { get; set; }
|
||||||
|
|
||||||
public ParticipationTabViewModel(
|
public ParticipationTabViewModel(
|
||||||
IParticipationSessionRepository sessions,
|
IParticipationSessionRepository sessions,
|
||||||
@@ -120,6 +121,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(SelectedSessionDisplay));
|
OnPropertyChanged(nameof(SelectedSessionDisplay));
|
||||||
EditSessionCommand.NotifyCanExecuteChanged();
|
EditSessionCommand.NotifyCanExecuteChanged();
|
||||||
|
DeleteSessionCommand.NotifyCanExecuteChanged();
|
||||||
if (value is null)
|
if (value is null)
|
||||||
{
|
{
|
||||||
StudentRows.Clear();
|
StudentRows.Clear();
|
||||||
@@ -282,7 +284,11 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
|
|
||||||
private bool CanEditSession() => !IsReadOnly && SelectedSession is not null;
|
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))]
|
[RelayCommand(CanExecute = nameof(CanQuickInput))]
|
||||||
private async Task QuickInput()
|
private async Task QuickInput()
|
||||||
@@ -325,14 +331,20 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
if (SelectedSession is not null) LoadGrid(SelectedSession.Id);
|
if (SelectedSession is not null) LoadGrid(SelectedSession.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand(CanExecute = nameof(CanDeleteSession))]
|
||||||
private void DeleteSession()
|
private async Task DeleteSession()
|
||||||
{
|
{
|
||||||
if (SelectedSession is null) return;
|
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);
|
_sessions.Delete(SelectedSession.Id);
|
||||||
LoadSessions();
|
LoadSessions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanDeleteSession() => !IsReadOnly && SelectedSession is not null;
|
||||||
|
|
||||||
public void SaveNote(Guid sessionId, Guid studentId, string note)
|
public void SaveNote(Guid sessionId, Guid studentId, string note)
|
||||||
{
|
{
|
||||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId);
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
private readonly ISubjectRepository _subjects;
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly ICompetencyDomainRepository _competencyDomains;
|
private readonly ICompetencyDomainRepository _competencyDomains;
|
||||||
private readonly AiSettingsService _aiSettings;
|
private readonly AiSettingsService _aiSettings;
|
||||||
|
private readonly IParticipationSessionRepository _participationSessions;
|
||||||
|
|
||||||
private Guid _groupId;
|
private Guid _groupId;
|
||||||
|
|
||||||
@@ -75,14 +76,16 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
public Func<Lesson, Task>? OnShowLesson { get; set; }
|
public Func<Lesson, Task>? OnShowLesson { get; set; }
|
||||||
public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; }
|
public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; }
|
||||||
public Func<Unit, Task<bool>>? OnAiAssist { get; set; }
|
public Func<Unit, Task<bool>>? OnAiAssist { get; set; }
|
||||||
|
public Action<string>? OnNotify { get; set; }
|
||||||
|
|
||||||
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
||||||
IGroupRepository groups, ISubjectRepository subjects,
|
IGroupRepository groups, ISubjectRepository subjects,
|
||||||
ICompetencyDomainRepository competencyDomains, AiSettingsService aiSettings)
|
ICompetencyDomainRepository competencyDomains, AiSettingsService aiSettings,
|
||||||
|
IParticipationSessionRepository participationSessions)
|
||||||
{
|
{
|
||||||
_units = units; _lessons = lessons; _groups = groups;
|
_units = units; _lessons = lessons; _groups = groups;
|
||||||
_subjects = subjects; _competencyDomains = competencyDomains;
|
_subjects = subjects; _competencyDomains = competencyDomains;
|
||||||
_aiSettings = aiSettings;
|
_aiSettings = aiSettings; _participationSessions = participationSessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize(Guid groupId, bool isReadOnly = false)
|
public void Initialize(Guid groupId, bool isReadOnly = false)
|
||||||
@@ -367,6 +370,32 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
_lessons.Save(lesson);
|
_lessons.Save(lesson);
|
||||||
LoadUnits();
|
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 ──────────────────────────────────────────────────────────────
|
// ── Anzeige-DTOs ──────────────────────────────────────────────────────────────
|
||||||
@@ -1122,6 +1151,12 @@ public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
|
|||||||
/// halten), wird nichts doppelt angelegt. Neue Stunden bekommen bewusst kein Thema — die
|
/// halten), wird nichts doppelt angelegt. Neue Stunden bekommen bewusst kein Thema — die
|
||||||
/// sonst übliche "Thema erforderlich"-Regel des manuellen "+Stunde"-Dialogs gilt hier nicht,
|
/// sonst übliche "Thema erforderlich"-Regel des manuellen "+Stunde"-Dialogs gilt hier nicht,
|
||||||
/// da diese Platzhalter zum späteren Ausfüllen gedacht sind.
|
/// 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]
|
[RelayCommand]
|
||||||
private void Save()
|
private void Save()
|
||||||
{
|
{
|
||||||
@@ -1136,6 +1171,9 @@ public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
|
|||||||
if (slotsForGroup.Count == 0)
|
if (slotsForGroup.Count == 0)
|
||||||
{ DateError = "Für diese Gruppe ist noch keine Stunde im Stundenplan eingetragen."; return; }
|
{ 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 schoolHolidays = _schoolHolidays.GetAll();
|
||||||
var publicHolidayDates = new HashSet<DateOnly>();
|
var publicHolidayDates = new HashSet<DateOnly>();
|
||||||
for (var year = from.Year; year <= to.Year; year++)
|
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))
|
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 (isFreeDay) { skippedHoliday++; continue; }
|
||||||
if (existing.Contains((date, (int?)slot.PeriodNumber))) { skippedExisting++; continue; }
|
if (existing.Contains((date, (int?)slot.PeriodNumber))) { skippedExisting++; continue; }
|
||||||
|
|
||||||
|
|||||||
@@ -199,9 +199,11 @@
|
|||||||
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
|
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
|
||||||
Text="{Binding DayNumber}" FontSize="12"
|
Text="{Binding DayNumber}" FontSize="12"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
<Ellipse Width="5" Height="5" Fill="#E53935" Margin="0,0,0,3"
|
<StackPanel Orientation="Horizontal" Spacing="2" Margin="0,0,0,3"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
HorizontalAlignment="Center" VerticalAlignment="Bottom">
|
||||||
IsVisible="{Binding HasExam}"/>
|
<Ellipse Width="5" Height="5" Fill="#1E88E5" IsVisible="{Binding HasSession}"/>
|
||||||
|
<Ellipse Width="5" Height="5" Fill="#E53935" IsVisible="{Binding HasExam}"/>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -218,6 +220,10 @@
|
|||||||
<Ellipse Width="7" Height="7" Fill="#E53935"/>
|
<Ellipse Width="7" Height="7" Fill="#E53935"/>
|
||||||
<TextBlock Text="Klausur" FontSize="10" Opacity="0.6"/>
|
<TextBlock Text="Klausur" FontSize="10" Opacity="0.6"/>
|
||||||
</StackPanel>
|
</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">
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
<Border Width="10" Height="10" CornerRadius="3" BorderThickness="2"
|
<Border Width="10" Height="10" CornerRadius="3" BorderThickness="2"
|
||||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"/>
|
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"/>
|
||||||
|
|||||||
@@ -56,6 +56,8 @@
|
|||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
||||||
<Button Content="✎ Sitzung bearbeiten" Command="{Binding EditSessionCommand}"
|
<Button Content="✎ Sitzung bearbeiten" Command="{Binding EditSessionCommand}"
|
||||||
IsVisible="{Binding !IsReadOnly}" FontSize="11" Padding="8,3"/>
|
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"
|
<ToggleButton Content="◇ Kompetenzen"
|
||||||
IsChecked="{Binding CompetencyTagsVisible}"
|
IsChecked="{Binding CompetencyTagsVisible}"
|
||||||
IsVisible="{Binding HasCompetencyCatalog}"
|
IsVisible="{Binding HasCompetencyCatalog}"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using LehrerApp.Core.Interfaces;
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using LehrerApp.Desktop.Views.Shared;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Groups;
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
@@ -29,6 +30,7 @@ public partial class ParticipationTabView : UserControl
|
|||||||
vm.OnComputeGrade = ShowComputeGradeDialog;
|
vm.OnComputeGrade = ShowComputeGradeDialog;
|
||||||
vm.OnOpenWizard = ShowWizardDialog;
|
vm.OnOpenWizard = ShowWizardDialog;
|
||||||
vm.OnManageAspects = ShowManageAspectsDialog;
|
vm.OnManageAspects = ShowManageAspectsDialog;
|
||||||
|
vm.OnConfirmDeleteSession = ConfirmDeleteSession;
|
||||||
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
||||||
vm.PropertyChanged += (_, pe) =>
|
vm.PropertyChanged += (_, pe) =>
|
||||||
{
|
{
|
||||||
@@ -367,4 +369,23 @@ public partial class ParticipationTabView : UserControl
|
|||||||
if (owner is not null)
|
if (owner is not null)
|
||||||
await dialog.ShowDialog(owner);
|
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="Bearbeiten" Command="{Binding EditLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||||
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}" 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="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}"/>
|
<Button Content="Löschen" Command="{Binding DeleteLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public partial class PlanningTabView : UserControl
|
|||||||
vm.OnShowLesson = ShowLessonViewerDialog;
|
vm.OnShowLesson = ShowLessonViewerDialog;
|
||||||
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
|
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
|
||||||
vm.OnAiAssist = ShowAiAssistDialog;
|
vm.OnAiAssist = ShowAiAssistDialog;
|
||||||
|
vm.OnNotify = Notifications.ShowSuccess;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,14 +252,35 @@ der ersten mit der zweiten Hälfte der Sitzungen. "Übernehmen" aktualisiert ein
|
|||||||
Mitarbeit-Note für denselben Zeitraum statt sie zu duplizieren (erkannt über den `Grade.Note`-Tag).
|
Mitarbeit-Note für denselben Zeitraum statt sie zu duplizieren (erkannt über den `Grade.Note`-Tag).
|
||||||
|
|
||||||
### 3.3 Sitzungen
|
### 3.3 Sitzungen
|
||||||
- [ ] **3.3.1** Sitzung automatisch aus einer geplanten `Lesson` erzeugen
|
- [x] **3.3.1** Sitzung automatisch aus einer geplanten `Lesson` erzeugen
|
||||||
(Datum + Thema übernehmen) — Abhängigkeit zu 4.2.
|
(Datum + Thema übernehmen) — Abhängigkeit zu 4.2. Neuer Button "Sitzung erzeugen" in der
|
||||||
|
Stunden-Toolbar der Planung ([PlanningTabView.axaml](LehrerApp.Desktop/Views/Groups/PlanningTabView.axaml));
|
||||||
|
`PlanningTabViewModel.CreateParticipationSessionCommand` legt eine `ParticipationSession`
|
||||||
|
mit `Date`/`Comment` aus der Stunde an und verknüpft sie über das bis dahin ungenutzte
|
||||||
|
`ParticipationSession.LessonId`-Feld. Ein zweiter Klick auf dieselbe Stunde legt keine
|
||||||
|
Dopplung an, sondern zeigt nur einen Hinweis (Prüfung über `LessonId` innerhalb der Gruppe).
|
||||||
- [x] **3.3.2** Anwesenheit in der Sitzung erfassen — als `ParticipationEntry.Attendance`
|
- [x] **3.3.2** Anwesenheit in der Sitzung erfassen — als `ParticipationEntry.Attendance`
|
||||||
(krank: Entschuldigung offen/entschuldigt/unentschuldigt), nicht als "verspätet"; siehe unten.
|
(krank: Entschuldigung offen/entschuldigt/unentschuldigt), nicht als "verspätet"; siehe unten.
|
||||||
Vorgriff auf 5.2, ersetzt dessen 5.2.1/5.2.4 aber nicht vollständig (kein Fehlzeitenmodul
|
Vorgriff auf 5.2, ersetzt dessen 5.2.1/5.2.4 aber nicht vollständig (kein Fehlzeitenmodul
|
||||||
über alle Stundentypen hinweg, nur innerhalb von Mitarbeit-Sitzungen).
|
über alle Stundentypen hinweg, nur innerhalb von Mitarbeit-Sitzungen).
|
||||||
- [ ] **3.3.3** Sitzung bearbeiten/löschen inkl. Rückfrage bei vorhandenen Bewertungen.
|
- [x] **3.3.3** Sitzung bearbeiten/löschen inkl. Rückfrage bei vorhandenen Bewertungen.
|
||||||
- [ ] **3.3.4** Sitzungen mehrerer Gruppen im Kalenderüberblick.
|
**Nachtrag:** Das Bearbeiten (`EditSessionCommand`/`AddSessionDialog`) war bereits vor
|
||||||
|
diesem Durchgang ad-hoc umgesetzt (Commit „Edit Stammdaten Mitarbeitsessions“), ohne dass
|
||||||
|
diese Liste nachgezogen wurde — daher hier nur nachträglich dokumentiert, nicht neu gebaut.
|
||||||
|
Neu ergänzt wurde das Löschen: Button "🗑 Sitzung löschen" neben "✎ Sitzung bearbeiten"
|
||||||
|
([ParticipationTabView.axaml](LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml)),
|
||||||
|
`DeleteSessionCommand` fragt über den generischen `ConfirmDialog` nach (Muster aus
|
||||||
|
`ParticipationAspectsDialog`) und nennt die Anzahl betroffener `ParticipationEntry`-Datensätze,
|
||||||
|
damit vor dem endgültigen Löschen klar ist, ob bereits Bewertungen existieren. Das Löschen
|
||||||
|
selbst kaskadiert wie zuvor über `ParticipationSessionRepository.Delete`.
|
||||||
|
- [x] **3.3.4** Sitzungen mehrerer Gruppen im Kalenderüberblick. Erweitert den bestehenden
|
||||||
|
Dashboard-Monatskalender ([DashboardView.axaml](LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml))
|
||||||
|
um einen dritten `CalendarEventKind.ParticipationSession` neben `Lesson`/`Exam` — blauer
|
||||||
|
Punkt am Tag, eigener Legenden-Eintrag, Klick navigiert wie bei Lesson-Terminen in den Tab
|
||||||
|
"Mitarbeit". Bewusst keine eigene neue Ansicht: `DashboardViewModel` hatte `IParticipationSessionRepository`
|
||||||
|
und die Kalender-Aggregation (`DayAgg`/`CalendarDayCell`/`CalendarEventItem`) bereits, sodass
|
||||||
|
sich der dritte Termintyp am Exam-Vorbild (In-Memory-Filterung nach `GetByGroup`, keine neue
|
||||||
|
Repository-Methode) ergänzen ließ.
|
||||||
|
|
||||||
### Abschnittsnoten & Mitarbeits-Assistent (nicht aus dieser Liste, eigener Workflow-Bedarf)
|
### Abschnittsnoten & Mitarbeits-Assistent (nicht aus dieser Liste, eigener Workflow-Bedarf)
|
||||||
|
|
||||||
@@ -338,6 +359,13 @@ gruppenspezifischen Tab "Planung" (`GroupDetailView`), siehe unten.
|
|||||||
`NotificationService.ShowSuccess`, bis dahin nur `ShowError` im Einsatz). Neue Stunden haben
|
`NotificationService.ShowSuccess`, bis dahin nur `ShowError` im Einsatz). Neue Stunden haben
|
||||||
bewusst kein Thema (Platzhalter zum Ausfüllen) — die "Thema erforderlich"-Pflicht des
|
bewusst kein Thema (Platzhalter zum Ausfüllen) — die "Thema erforderlich"-Pflicht des
|
||||||
manuellen "+ Stunde"-Dialogs gilt hier nicht.
|
manuellen "+ Stunde"-Dialogs gilt hier nicht.
|
||||||
|
**Bugfix (Nutzer-Feedback):** Doppelstunden (zwei direkt aufeinanderfolgende Perioden
|
||||||
|
desselben Wochentags/derselben Gruppe im Stundenplan) erzeugten zunächst zwei `Lesson`s mit
|
||||||
|
gleichem Datum statt einer — dieselbe Konvention wie bei der Doppelstunden-Erkennung in
|
||||||
|
`LessonDialogViewModel.RecomputeTimeBudget` (Zeitbudget-Anzeige) und den Ferien-Badges in
|
||||||
|
`TimetableViewModel` fehlte hier. Jetzt bekommt nur die erste Periode eines zusammenhängenden
|
||||||
|
Blocks eine `Lesson`; Folgeperioden (deren Vorgängerperiode ebenfalls im Stundenplan steht)
|
||||||
|
werden übersprungen.
|
||||||
|
|
||||||
Umgesetzt über den neuen Tab "Planung" in
|
Umgesetzt über den neuen Tab "Planung" in
|
||||||
[GroupDetailView.axaml](LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml)
|
[GroupDetailView.axaml](LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml)
|
||||||
|
|||||||
Reference in New Issue
Block a user