using Avalonia.Controls; using Avalonia.Interactivity; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels; using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.Views.Groups; using LehrerApp.Desktop.Services; using Microsoft.Extensions.DependencyInjection; namespace LehrerApp.Desktop.Views.Planning; public partial class TimetableView : UserControl { public TimetableView() => InitializeComponent(); protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is TimetableViewModel vm) { vm.OnEditSlot = ShowSlotDialog; vm.OnAddSubstitution = ShowSubstitutionDialog; vm.OnImportWebUntisTimetable = ShowWebUntisTimetableDialog; vm.OnOpenLessonViewer = ShowLessonViewerDialog; vm.OnOpenTeachingMode = ShowTeachingMode; vm.OnCreateLesson = ShowCreateLessonDialog; vm.OnMoveLesson = ShowMoveLessonDialog; } } /// Nutzer-Feedback (zweite Runde — die erste Fassung hatte das Problem an der falschen /// 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 Zielen einschließlich /// Direktanlage und Verschieben als /// Alternative zum Direktklick, siehe /// für den "einheitlicheren" Standard-Klick (Unterrichtsansicht bei laufender Stunde, sonst /// 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, /// nur der normale Eltern-Wert) funktioniert dort aber sehr wohl, deshalb liest der Handler /// die Zelle über ((MenuItem)sender).DataContext statt über eine Binding. private async void OnWeekCellMenuItemClick(object? sender, RoutedEventArgs e) { if (sender is not MenuItem { Tag: TimetableLessonDestination destination } menuItem) return; if (menuItem.DataContext is not WeekCellItem cell) return; if (DataContext is not TimetableViewModel vm) return; switch (destination) { case TimetableLessonDestination.TeachingMode: if (cell.Lesson is { } teachLesson && vm.OnOpenTeachingMode is not null) await vm.OnOpenTeachingMode(teachLesson); break; case TimetableLessonDestination.Viewer: 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().NavigateToGroupDetail(cell.GroupId, 6); break; case TimetableLessonDestination.SeatingPlan: if (cell.GroupId != Guid.Empty) App.Services.GetRequiredService().NavigateToGroupDetail(cell.GroupId, 2); break; } } private async Task ShowCreateLessonDialog(TimetableLessonRequest request) { var owner = TopLevel.GetTopLevel(this) as Window; var groups = App.Services.GetRequiredService(); var group = groups.GetById(request.GroupId); if (owner is null || group is null || !group.IsActive) return; var lessons = App.Services.GetRequiredService(); if (lessons.GetByGroupAndDate(request.GroupId, request.Date) .Any(l => l.LessonNumber == request.PeriodNumber)) { App.Services.GetRequiredService() .ShowError("Für diesen Termin existiert bereits eine Stundenplanung."); return; } var units = App.Services.GetRequiredService(); var pickerVm = new TimetableUnitPickerViewModel(units, group.Id, group.Name, request.Date, request.PeriodNumber); var picker = new TimetableUnitPickerDialog { DataContext = pickerVm }; if (!await picker.ShowDialog(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().GetById(subjectId) : null; var lessonVm = new LessonDialogViewModel( lessons, App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), 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(owner) && lessonVm.Result is not null) { if (pickerVm.ResultIsNew) units.Save(unit); (DataContext as TimetableViewModel)?.Load(); App.Services.GetRequiredService().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(); var schedule = App.Services.GetRequiredService(); 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(owner) || dialogVm.Result is not { } target) return; try { var service = new LessonSchedulingService(App.Services.GetRequiredService()); 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().ShowSuccess( target.SplitDoubleLesson ? "Doppelstunde getrennt und Fortsetzung verschoben." : "Stunde verschoben."); } catch (InvalidOperationException ex) { App.Services.GetRequiredService().ShowError(ex.Message); } } private async Task ShowWebUntisTimetableDialog() { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return; var vm = new WebUntisTimetableImportViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService()); var dialog = new WebUntisTimetableImportDialog { DataContext = vm }; vm.OnCreateGroup = dialog.CreateGroupAsync; await vm.InitializeAsync(); await dialog.ShowDialog(owner); } private async Task ShowTeachingMode(Lesson lesson) { var owner = TopLevel.GetTopLevel(this) as Window; var group = App.Services.GetRequiredService().GetById(lesson.GroupId); if (owner is null || group is null) return; var teachingModeVm = new TeachingModeViewModel(lesson, group, App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService()); var window = new TeachingModeWindow { DataContext = teachingModeVm }; await window.ShowDialog(owner); } private async Task ShowLessonViewerDialog(Lesson lesson) { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return; var viewerVm = new LessonViewerViewModel(lesson, App.Services.GetRequiredService()); var dialog = new LessonViewerDialog { DataContext = viewerVm }; await dialog.ShowDialog(owner); } private async Task ShowSlotDialog(TimetableCellItem cell) { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null || cell.Weekday is null) return; var vm = new TimetableSlotDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), cell.Weekday.Value, cell.PeriodNumber, cell.Slot); var dialog = new TimetableSlotDialog { DataContext = vm }; await dialog.ShowDialog(owner); } private async Task ShowSubstitutionDialog() { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return; var vm = new SubstitutionEntryDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService()); var dialog = new SubstitutionEntryDialog { DataContext = vm }; await dialog.ShowDialog(owner); } }