Stundenplan verbesserung
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-08-30 00:53:32 +02:00
parent 2b299fc940
commit dade41ee8d
16 changed files with 639 additions and 75 deletions
@@ -26,6 +26,8 @@ public partial class TimetableView : UserControl
vm.OnImportWebUntisTimetable = ShowWebUntisTimetableDialog;
vm.OnOpenLessonViewer = ShowLessonViewerDialog;
vm.OnOpenTeachingMode = ShowTeachingMode;
vm.OnCreateLesson = ShowCreateLessonDialog;
vm.OnMoveLesson = ShowMoveLessonDialog;
}
}
@@ -33,10 +35,11 @@ public partial class TimetableView : UserControl
/// Stelle gelöst, in der "Heute"-Tagesliste statt im Wochenraster): im Wochenraster oben
/// führt ein Klick auf eine Stunden-Kachel bislang entweder in den Planungsviewer oder zur
/// Einheitenplanung — je nachdem, ob schon eine Lesson existiert, ohne dass das von außen
/// erkennbar wäre. Popup-Menü (MenuFlyout, kein ComboBox mehr) mit allen vier Zielen als
/// erkennbar wäre. Popup-Menü (MenuFlyout, kein ComboBox mehr) mit allen Zielen einschließlich
/// Direktanlage und Verschieben als
/// Alternative zum Direktklick, siehe <see cref="TimetableViewModel.OpenWeekCellCommand"/>
/// für den "einheitlicheren" Standard-Klick (Unterrichtsansicht bei laufender Stunde, sonst
/// Planungsviewer, sonst Einheitenplanung). MenuItem.Click statt Command-Binding: ein
/// Planungsviewer, sonst Direktanlage). MenuItem.Click statt Command-Binding: ein
/// $parent[ItemsControl]-Vorfahrenpfad (wie beim Zeilen-Button) funktioniert innerhalb eines
/// Flyouts nicht zuverlässig, weil dessen Popup nicht im normalen visuellen Baum hängt (siehe
/// TODO.md-Nachtrag zum Klassenlehrer-Bereich) — DataContext-Vererbung (kein Pfad-Suchen,
@@ -58,6 +61,14 @@ public partial class TimetableView : UserControl
if (cell.Lesson is { } viewLesson && vm.OnOpenLessonViewer is not null)
await vm.OnOpenLessonViewer(viewLesson);
break;
case TimetableLessonDestination.Create:
if (cell.Date is { } createDate && vm.OnCreateLesson is not null)
await vm.OnCreateLesson(new TimetableLessonRequest(cell.GroupId, createDate, cell.PeriodNumber));
break;
case TimetableLessonDestination.Move:
if (cell.Lesson is { } moveLesson && vm.OnMoveLesson is not null)
await vm.OnMoveLesson(new TimetableLessonMoveRequest(moveLesson, cell.PeriodNumber));
break;
case TimetableLessonDestination.Planning:
if (cell.GroupId != Guid.Empty)
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToGroupDetail(cell.GroupId, 6);
@@ -69,6 +80,96 @@ public partial class TimetableView : UserControl
}
}
private async Task ShowCreateLessonDialog(TimetableLessonRequest request)
{
var owner = TopLevel.GetTopLevel(this) as Window;
var groups = App.Services.GetRequiredService<IGroupRepository>();
var group = groups.GetById(request.GroupId);
if (owner is null || group is null || !group.IsActive) return;
var lessons = App.Services.GetRequiredService<ILessonRepository>();
if (lessons.GetByGroupAndDate(request.GroupId, request.Date)
.Any(l => l.LessonNumber == request.PeriodNumber))
{
App.Services.GetRequiredService<NotificationService>()
.ShowError("Für diesen Termin existiert bereits eine Stundenplanung.");
return;
}
var units = App.Services.GetRequiredService<IUnitRepository>();
var pickerVm = new TimetableUnitPickerViewModel(units, group.Id, group.Name,
request.Date, request.PeriodNumber);
var picker = new TimetableUnitPickerDialog { DataContext = pickerVm };
if (!await picker.ShowDialog<bool>(owner) || pickerVm.Result is not { } unit) return;
var groupLessons = units.GetByGroup(group.Id).SelectMany(u => lessons.GetByUnit(u.Id)).ToList();
var materials = groupLessons.SelectMany(l => l.Phases).Select(p => p.Material)
.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList();
var shorthands = groupLessons.SelectMany(l => l.Phases).Select(p => p.Shorthand)
.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList();
var subject = group.SubjectId is { } subjectId
? App.Services.GetRequiredService<ISubjectRepository>().GetById(subjectId) : null;
var lessonVm = new LessonDialogViewModel(
lessons,
App.Services.GetRequiredService<IShorthandCodeRepository>(),
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
App.Services.GetRequiredService<ITimetableSlotRepository>(),
App.Services.GetRequiredService<PeriodScheduleService>(),
App.Services.GetRequiredService<IAttachmentStorage>(),
unit.Id, group.Id, group.Name, subject?.Name ?? "", materials, shorthands,
editingLesson: null, suggestedDate: request.Date, suggestedPeriod: request.PeriodNumber);
var lessonDialog = new LessonDialog { DataContext = lessonVm };
if (await lessonDialog.ShowDialog<bool>(owner) && lessonVm.Result is not null)
{
if (pickerVm.ResultIsNew) units.Save(unit);
(DataContext as TimetableViewModel)?.Load();
App.Services.GetRequiredService<NotificationService>().ShowSuccess("Stunde angelegt.");
}
}
private async Task ShowMoveLessonDialog(TimetableLessonMoveRequest request)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return;
var lesson = request.Lesson;
var slots = App.Services.GetRequiredService<ITimetableSlotRepository>();
var schedule = App.Services.GetRequiredService<PeriodScheduleService>();
var anchor = lesson.LessonNumber;
var firstPartMinutes = anchor is int p ? schedule.GetDurationMinutes(p) : 0;
var isDouble = anchor is int start && firstPartMinutes > 0 &&
slots.GetByGroup(lesson.GroupId).Any(s => s.Weekday == lesson.Date.DayOfWeek &&
s.PeriodNumber == start + 1) && lesson.Phases.Sum(x => x.DurationMinutes) > firstPartMinutes;
var dialogVm = new MoveLessonDialogViewModel(lesson.Date, lesson.LessonNumber, isDouble,
isDouble && anchor is int a && request.SelectedPeriod > a);
var dialog = new MoveLessonDialog { DataContext = dialogVm };
if (!await dialog.ShowDialog<bool>(owner) || dialogVm.Result is not { } target) return;
try
{
var service = new LessonSchedulingService(App.Services.GetRequiredService<ILessonRepository>());
if (target.SplitDoubleLesson)
{
if (target.NewPeriod is not int newPeriod)
throw new InvalidOperationException("Für den zweiten Teil ist eine Zielstunde erforderlich.");
service.SplitAndMoveSecondPart(lesson, firstPartMinutes, target.NewDate, newPeriod,
schedule.GetTimes(newPeriod)?.Start);
}
else service.Move(lesson, target.NewDate, target.NewPeriod, target.ShiftFollowing);
(DataContext as TimetableViewModel)?.Load();
App.Services.GetRequiredService<NotificationService>().ShowSuccess(
target.SplitDoubleLesson ? "Doppelstunde getrennt und Fortsetzung verschoben." : "Stunde verschoben.");
}
catch (InvalidOperationException ex)
{
App.Services.GetRequiredService<NotificationService>().ShowError(ex.Message);
}
}
private async Task ShowWebUntisTimetableDialog()
{
var owner = TopLevel.GetTopLevel(this) as Window;