Stundenplan: Popup-Menü im Wochenraster statt Dropdown in der Tagesliste
Nutzer-Feedback: die Tagesliste-Buttons waren schon in Ordnung, das Problem
lag im Wochenraster darüber — ein Klick auf eine Stunden-Kachel führt dort
entweder in den Planungsviewer oder zur Einheitenplanung, ohne dass von
außen erkennbar wäre welches Ziel man bekommt. Tagesliste auf den
ursprünglichen Stand zurückgesetzt.
Neu: ein kleiner "⋮"-Button pro Kachel öffnet ein Popup-Menü mit vier
ausdrücklich benannten Zielen (Unterrichtsansicht/Planungsviewer/Sitzplan/
Planung). MenuFlyout statt ComboBox — dabei verstanden, dass ein
$parent[ItemsControl]-Vorfahrenpfad im Flyout nicht funktioniert, eine
normale {Binding} über die DataContext-Vererbung aber sehr wohl. Der
Direktklick springt jetzt außerdem "einheitlicher": bei einer Lesson
während der eigentlichen Unterrichtszeit direkt in den Unterrichtsmodus
statt in den Viewer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,11 @@ public sealed class TimetableViewModelTests
|
|||||||
FakeSubjects? subjects = null, FakeLessons? lessons = null, FakeExams? exams = null,
|
FakeSubjects? subjects = null, FakeLessons? lessons = null, FakeExams? exams = null,
|
||||||
SchoolCalendarSettingsService? calendarSettings = null,
|
SchoolCalendarSettingsService? calendarSettings = null,
|
||||||
FakeSupervisionDuties? supervisionDuties = null, FakeSubstitutionEntries? substitutions = null,
|
FakeSupervisionDuties? supervisionDuties = null, FakeSubstitutionEntries? substitutions = null,
|
||||||
FakeUntisSlotMappings? untisMappings = null, WebUntisSettingsService? untisSettings = null)
|
FakeUntisSlotMappings? untisMappings = null, WebUntisSettingsService? untisSettings = null,
|
||||||
|
PeriodScheduleService? periodSchedule = null)
|
||||||
{
|
{
|
||||||
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf
|
// Bewusst kein "using": SchoolCalendarSettingsService/PeriodScheduleService lesen den Pfad
|
||||||
// (SetState), das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
// nur bei Bedarf, das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
||||||
var tempPath = System.IO.Path.Combine(
|
var tempPath = System.IO.Path.Combine(
|
||||||
System.IO.Path.GetTempPath(), $"lehrerapp-timetablevm-tests-{Guid.NewGuid():N}");
|
System.IO.Path.GetTempPath(), $"lehrerapp-timetablevm-tests-{Guid.NewGuid():N}");
|
||||||
Directory.CreateDirectory(tempPath);
|
Directory.CreateDirectory(tempPath);
|
||||||
@@ -28,7 +29,8 @@ public sealed class TimetableViewModelTests
|
|||||||
calendarSettings ?? new SchoolCalendarSettingsService(tempPath),
|
calendarSettings ?? new SchoolCalendarSettingsService(tempPath),
|
||||||
new PublicHolidayService(), new SchoolYearService(),
|
new PublicHolidayService(), new SchoolYearService(),
|
||||||
supervisionDuties ?? new FakeSupervisionDuties(), substitutions ?? new FakeSubstitutionEntries(),
|
supervisionDuties ?? new FakeSupervisionDuties(), substitutions ?? new FakeSubstitutionEntries(),
|
||||||
untisMappings ?? new FakeUntisSlotMappings(), untisSettings ?? TestSupport.BuildWebUntisSettingsService());
|
untisMappings ?? new FakeUntisSlotMappings(), untisSettings ?? TestSupport.BuildWebUntisSettingsService(),
|
||||||
|
periodSchedule ?? new PeriodScheduleService(tempPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nächstes Datum ab (inkl.) <paramref name="from"/>, das auf einen Wochentag Mo-Fr fällt —
|
/// Nächstes Datum ab (inkl.) <paramref name="from"/>, das auf einen Wochentag Mo-Fr fällt —
|
||||||
@@ -336,6 +338,80 @@ public sealed class TimetableViewModelTests
|
|||||||
Assert.Equal(lesson.Id, openedLesson?.Id);
|
Assert.Equal(lesson.Id, openedLesson?.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nutzer-Feedback (zweite Runde): der Direktklick auf eine Wochenraster-Kachel soll
|
||||||
|
/// "einheitlicher" springen — bei einer Lesson HEUTE, während gerade Unterrichtszeit ist,
|
||||||
|
/// direkt in den Unterrichtsmodus statt in den (schreibgeschützten) Planungsviewer.
|
||||||
|
[Fact]
|
||||||
|
public async Task OpenWeekCell_LessonHeuteWaehrendUnterrichtszeit_OeffnetUnterrichtsmodus()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 1, Topic = "Redox" };
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(lesson);
|
||||||
|
var periodSchedule = BuildPeriodSchedule(1);
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons, periodSchedule: periodSchedule);
|
||||||
|
Lesson? teachingModeLesson = null;
|
||||||
|
Lesson? viewerLesson = null;
|
||||||
|
vm.OnOpenTeachingMode = l => { teachingModeLesson = l; return Task.CompletedTask; };
|
||||||
|
vm.OnOpenLessonViewer = l => { viewerLesson = l; return Task.CompletedTask; };
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == today.DayOfWeek && c.PeriodNumber == 1);
|
||||||
|
await vm.OpenWeekCellCommand.ExecuteAsync(cell);
|
||||||
|
|
||||||
|
Assert.Equal(lesson.Id, teachingModeLesson?.Id);
|
||||||
|
Assert.Null(viewerLesson);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dieselbe Stundenzeit passt, die Lesson liegt aber nicht heute — bleibt beim Viewer.
|
||||||
|
[Fact]
|
||||||
|
public async Task OpenWeekCell_LessonNichtHeuteTrotzPassenderUhrzeit_OeffnetViewer()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var otherWeekday = today.DayOfWeek == DayOfWeek.Monday ? DayOfWeek.Tuesday : DayOfWeek.Monday;
|
||||||
|
var otherDate = DateInCurrentWeek(otherWeekday);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = otherWeekday, PeriodNumber = 1 });
|
||||||
|
var lesson = new Lesson { GroupId = group.Id, Date = otherDate, LessonNumber = 1, Topic = "Redox" };
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(lesson);
|
||||||
|
var periodSchedule = BuildPeriodSchedule(1);
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons, periodSchedule: periodSchedule);
|
||||||
|
Lesson? teachingModeLesson = null;
|
||||||
|
Lesson? viewerLesson = null;
|
||||||
|
vm.OnOpenTeachingMode = l => { teachingModeLesson = l; return Task.CompletedTask; };
|
||||||
|
vm.OnOpenLessonViewer = l => { viewerLesson = l; return Task.CompletedTask; };
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == otherWeekday && c.PeriodNumber == 1);
|
||||||
|
await vm.OpenWeekCellCommand.ExecuteAsync(cell);
|
||||||
|
|
||||||
|
Assert.Equal(lesson.Id, viewerLesson?.Id);
|
||||||
|
Assert.Null(teachingModeLesson);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weites Zeitfenster um <paramref name="around"/> herum (±2 Stunden), damit der Test nicht an
|
||||||
|
/// die exakte Toleranz von TimetableViewModel.TeachingTimeTolerance gebunden ist.
|
||||||
|
/// Start UND Ende exakt auf "jetzt" statt eines künstlich breiteren Fensters (z.B. ganzer Tag
|
||||||
|
/// via TimeOnly.MinValue/MaxValue): TimetableViewModel.IsAroundTeachingTime addiert selbst
|
||||||
|
/// noch ±10 Minuten Toleranz (TeachingTimeTolerance) auf Start/Ende — ein zusätzliches, vom
|
||||||
|
/// Test vorgegebenes Fenster würde nahe Mitternacht durch die TimeOnly-Arithmetik (wrappt bei
|
||||||
|
/// 24 Uhr) selbst kippen. "Jetzt" als Start/Ende bleibt nur dann riskant, wenn der Test
|
||||||
|
/// zufällig innerhalb der letzten/ersten 10 Minuten des Tages läuft — dasselbe inhärente
|
||||||
|
/// Randproblem hätte dann auch die Produktion, kein Testartefakt.
|
||||||
|
private static PeriodScheduleService BuildPeriodSchedule(int periodNumber)
|
||||||
|
{
|
||||||
|
var tempPath = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetTempPath(), $"lehrerapp-periodschedule-tests-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
var service = new PeriodScheduleService(tempPath);
|
||||||
|
var now = TimeOnly.FromDateTime(DateTime.Now);
|
||||||
|
service.SetPeriods([new PeriodTimeEntry { PeriodNumber = periodNumber, Start = now, End = now }]);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OpenSettings_RuftOnNavigateToSettingsAuf()
|
public void OpenSettings_RuftOnNavigateToSettingsAuf()
|
||||||
{
|
{
|
||||||
@@ -1178,44 +1254,42 @@ public sealed class TimetableViewModelTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nutzer-Feedback: unklar, wie man aus der "Heute"-Tagesliste zwischen Unterrichtsansicht,
|
/// Nutzer-Feedback (zweite Runde): das Popup-Menü je Wochenraster-Kachel (TimetableView.axaml,
|
||||||
/// Sitzplan, Planung und Planungsviewer wechselt — TodayLessonItem.DestinationOptions ersetzt die
|
/// MenuFlyout) blendet "Unterrichtsansicht"/"Planungsviewer" per IsVisible="{Binding HasLesson}"
|
||||||
/// bisherigen zwei Buttons durch ein Dropdown mit ausdrücklich benannten Zielen.
|
/// aus, wenn für den Slot noch keine Lesson existiert, und den ganzen Menü-Trigger per
|
||||||
public sealed class TodayLessonItemDestinationOptionsTests
|
/// IsVisible="{Binding HasGroupId}", wenn die Kachel gar keiner Gruppe zugeordnet ist (z.B. eine
|
||||||
|
/// GroupId-lose Vertretung). Diese beiden Properties sind die Grundlage dafür.
|
||||||
|
public sealed class WeekCellItemMenuVisibilityTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MitLesson_BietetAlleVierZiele()
|
public void ForSlot_MitLesson_HatHasLessonUndHasGroupId()
|
||||||
{
|
{
|
||||||
var lesson = new Lesson { Topic = "Redox" };
|
var lesson = new Lesson { Topic = "Redox" };
|
||||||
var item = new TodayLessonItem(Guid.NewGuid(), 3, "Q1 Chemie", "R204",
|
var cell = WeekCellItem.ForSlot(DayOfWeek.Monday, 3, isToday: true, "Che", "Q1 Chemie",
|
||||||
"#4C8DFF", "Redox", null, lesson: lesson);
|
"R204", "Redox", "#4C8DFF", "", hasExam: false, isLastBeforeExam: false,
|
||||||
|
hasExperiment: false, Guid.NewGuid(), isHoliday: false, lesson: lesson);
|
||||||
|
|
||||||
Assert.True(item.HasDestinationOptions);
|
Assert.True(cell.HasLesson);
|
||||||
Assert.Equal(
|
Assert.True(cell.HasGroupId);
|
||||||
[
|
|
||||||
TimetableLessonDestination.TeachingMode, TimetableLessonDestination.Viewer,
|
|
||||||
TimetableLessonDestination.SeatingPlan, TimetableLessonDestination.Planning,
|
|
||||||
],
|
|
||||||
item.DestinationOptions.Select(o => o.Kind));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OhneLesson_BietetNurSitzplanUndPlanung()
|
public void ForSlot_OhneLesson_HatKeinHasLesson()
|
||||||
{
|
{
|
||||||
var item = new TodayLessonItem(Guid.NewGuid(), 3, "Q1 Chemie", "R204",
|
var cell = WeekCellItem.ForSlot(DayOfWeek.Monday, 3, isToday: true, "Che", "Q1 Chemie",
|
||||||
"#4C8DFF", null, null);
|
"R204", "", "#4C8DFF", "", hasExam: false, isLastBeforeExam: false,
|
||||||
|
hasExperiment: false, Guid.NewGuid(), isHoliday: false);
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.False(cell.HasLesson);
|
||||||
[TimetableLessonDestination.SeatingPlan, TimetableLessonDestination.Planning],
|
Assert.True(cell.HasGroupId);
|
||||||
item.DestinationOptions.Select(o => o.Kind));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OhneGroupId_BietetKeineZiele()
|
public void ForSubstitutionLesson_OhneGroupId_HatKeinHasGroupId()
|
||||||
{
|
{
|
||||||
var item = TodayLessonItem.ForSubstitution(3, new SubstitutionEntry { GroupId = null, Description = "Vertretung" });
|
var cell = WeekCellItem.ForSubstitutionLesson(DayOfWeek.Monday, 3, isToday: true,
|
||||||
|
new SubstitutionEntry { GroupId = null, GroupLabel = "Fremde Klasse", Description = "Vertretung" });
|
||||||
|
|
||||||
Assert.False(item.HasDestinationOptions);
|
Assert.False(cell.HasGroupId);
|
||||||
Assert.Empty(item.DestinationOptions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public partial class TimetableViewModel : ObservableObject
|
|||||||
private readonly ISubstitutionEntryRepository _substitutions;
|
private readonly ISubstitutionEntryRepository _substitutions;
|
||||||
private readonly IUntisSlotMappingRepository _untisMappings;
|
private readonly IUntisSlotMappingRepository _untisMappings;
|
||||||
private readonly WebUntisSettingsService _untisSettings;
|
private readonly WebUntisSettingsService _untisSettings;
|
||||||
|
private readonly PeriodScheduleService _periodSchedule;
|
||||||
private readonly SchoolWeatherService? _schoolWeather;
|
private readonly SchoolWeatherService? _schoolWeather;
|
||||||
private readonly SemaphoreSlim _weatherGate = new(1, 1);
|
private readonly SemaphoreSlim _weatherGate = new(1, 1);
|
||||||
private WeatherSnapshot? _weatherSnapshot;
|
private WeatherSnapshot? _weatherSnapshot;
|
||||||
@@ -114,13 +115,14 @@ public partial class TimetableViewModel : ObservableObject
|
|||||||
PublicHolidayService publicHolidays, SchoolYearService schoolYear,
|
PublicHolidayService publicHolidays, SchoolYearService schoolYear,
|
||||||
ISupervisionDutyRepository supervisionDuties, ISubstitutionEntryRepository substitutions,
|
ISupervisionDutyRepository supervisionDuties, ISubstitutionEntryRepository substitutions,
|
||||||
IUntisSlotMappingRepository untisMappings, WebUntisSettingsService untisSettings,
|
IUntisSlotMappingRepository untisMappings, WebUntisSettingsService untisSettings,
|
||||||
SchoolWeatherService? schoolWeather = null)
|
PeriodScheduleService periodSchedule, SchoolWeatherService? schoolWeather = null)
|
||||||
{
|
{
|
||||||
_slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams;
|
_slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams;
|
||||||
_schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings;
|
_schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings;
|
||||||
_publicHolidays = publicHolidays; _schoolYear = schoolYear;
|
_publicHolidays = publicHolidays; _schoolYear = schoolYear;
|
||||||
_supervisionDuties = supervisionDuties; _substitutions = substitutions;
|
_supervisionDuties = supervisionDuties; _substitutions = substitutions;
|
||||||
_untisMappings = untisMappings; _untisSettings = untisSettings;
|
_untisMappings = untisMappings; _untisSettings = untisSettings;
|
||||||
|
_periodSchedule = periodSchedule;
|
||||||
_schoolWeather = schoolWeather;
|
_schoolWeather = schoolWeather;
|
||||||
Load();
|
Load();
|
||||||
}
|
}
|
||||||
@@ -368,14 +370,41 @@ public partial class TimetableViewModel : ObservableObject
|
|||||||
await OnOpenTeachingMode(lesson);
|
await OnOpenTeachingMode(lesson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nutzer-Feedback (zweite Runde): der Direktklick auf eine Wochenraster-Kachel soll
|
||||||
|
/// "einheitlicher" springen. Existiert eine Lesson UND ist gerade (heute, mit Toleranz vor/
|
||||||
|
/// nach der Stunde) Unterrichtszeit, geht es direkt in den Unterrichtsmodus — sonst wie
|
||||||
|
/// bisher in den (schreibgeschützten) Planungsviewer bzw., ohne Lesson, zur Einheitenplanung
|
||||||
|
/// der Gruppe. Das Popup-Menü (siehe TimetableView.axaml, MenuFlyout je Kachel) bietet
|
||||||
|
/// daneben immer alle vier Ziele explizit an, unabhängig von dieser Automatik.
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task OpenWeekCell(WeekCellItem? item)
|
private async Task OpenWeekCell(WeekCellItem? item)
|
||||||
{
|
{
|
||||||
if (item is null || item.GroupId == Guid.Empty) return;
|
if (item is null || item.GroupId == Guid.Empty) return;
|
||||||
if (item.Lesson is { } lesson && OnOpenLessonViewer is not null) await OnOpenLessonViewer(lesson);
|
if (item.Lesson is { } lesson)
|
||||||
|
{
|
||||||
|
if (IsAroundTeachingTime(lesson.Date, item.PeriodNumber) && OnOpenTeachingMode is not null)
|
||||||
|
await OnOpenTeachingMode(lesson);
|
||||||
|
else if (OnOpenLessonViewer is not null) await OnOpenLessonViewer(lesson);
|
||||||
|
}
|
||||||
else OnNavigateToGroup?.Invoke(item.GroupId);
|
else OnNavigateToGroup?.Invoke(item.GroupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bewusst mit Toleranz vor/nach der eingetragenen Stundenzeit (siehe
|
||||||
|
/// <see cref="PeriodScheduleService"/>, "Stundenraster" in den Einstellungen) statt exakt an
|
||||||
|
/// Start-/Endzeit gebunden — man klickt auch kurz vor Stundenbeginn oder in einer kurzen
|
||||||
|
/// Verzögerung danach noch typischerweise in Unterrichtsabsicht. Ohne konfiguriertes
|
||||||
|
/// Stundenraster oder an einem anderen Tag als heute bleibt es beim Planungsviewer.
|
||||||
|
/// Nimmt bewusst das Datum der Lesson selbst statt WeekCellItem.Date entgegen — Date ist dort
|
||||||
|
/// nur bei Kopfzeilen (WeekdayHeader) gesetzt, nicht bei regulären Stunden-Kacheln (ForSlot).
|
||||||
|
private static readonly TimeSpan TeachingTimeTolerance = TimeSpan.FromMinutes(10);
|
||||||
|
private bool IsAroundTeachingTime(DateOnly lessonDate, int periodNumber)
|
||||||
|
{
|
||||||
|
if (lessonDate != DateOnly.FromDateTime(DateTime.Today)) return false;
|
||||||
|
if (_periodSchedule.GetTimes(periodNumber) is not { } times) return false;
|
||||||
|
var now = TimeOnly.FromDateTime(DateTime.Now);
|
||||||
|
return now >= times.Start.Add(-TeachingTimeTolerance) && now <= times.End.Add(TeachingTimeTolerance);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenSettings() => OnNavigateToSettings?.Invoke(SettingsTab.Holidays);
|
private void OpenSettings() => OnNavigateToSettings?.Invoke(SettingsTab.Holidays);
|
||||||
|
|
||||||
@@ -784,9 +813,11 @@ public partial class WeekCellItem : ObservableObject
|
|||||||
public bool HasSupervision => SupervisionLocation.Length > 0;
|
public bool HasSupervision => SupervisionLocation.Length > 0;
|
||||||
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
||||||
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic);
|
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic);
|
||||||
|
public bool HasGroupId => GroupId != Guid.Empty;
|
||||||
/// Nur bei einer regulären, zugewiesenen Zelle mit bereits existierender Lesson gesetzt —
|
/// Nur bei einer regulären, zugewiesenen Zelle mit bereits existierender Lesson gesetzt —
|
||||||
/// Grundlage für den Direktsprung in den Verlaufsplan-Viewer (4.5.2).
|
/// Grundlage für den Direktsprung in den Verlaufsplan-Viewer (4.5.2).
|
||||||
public Lesson? Lesson { get; private init; }
|
public Lesson? Lesson { get; private init; }
|
||||||
|
public bool HasLesson => Lesson is not null;
|
||||||
public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
|
public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
|
||||||
[ObservableProperty] private string _weatherSymbol = "";
|
[ObservableProperty] private string _weatherSymbol = "";
|
||||||
[ObservableProperty] private string _weatherTooltip = "";
|
[ObservableProperty] private string _weatherTooltip = "";
|
||||||
@@ -925,33 +956,6 @@ public class TodayLessonItem
|
|||||||
public bool HasLesson => Lesson is not null;
|
public bool HasLesson => Lesson is not null;
|
||||||
public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
|
public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
|
||||||
|
|
||||||
/// Nutzer-Feedback: es war unklar, wie man aus der "Heute"-Tagesliste zwischen
|
|
||||||
/// Unterrichtsansicht, Sitzplan, Planung und Planungsviewer wechselt — bisher zwei Buttons
|
|
||||||
/// mit unterschiedlicher, teils vom Lesson-Status abhängiger Bedeutung
|
|
||||||
/// ("Verlaufsplan ansehen"/"Zur Lerngruppe"). Ein Dropdown mit ausdrücklich benannten Zielen
|
|
||||||
/// statt dessen (Wiring/Routing in TimetableView.axaml.cs). Unterrichtsansicht/Planungsviewer
|
|
||||||
/// brauchen eine existierende Lesson, Sitzplan/Planung sind auch ohne bereits möglich (Planung
|
|
||||||
/// ist ohnehin der Ort, an dem man eine Lesson für den Slot erst anlegt).
|
|
||||||
public IReadOnlyList<TimetableDestinationOption> DestinationOptions
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var options = new List<TimetableDestinationOption>();
|
|
||||||
if (HasLesson)
|
|
||||||
{
|
|
||||||
options.Add(new(TimetableLessonDestination.TeachingMode, "▶ Unterrichtsansicht"));
|
|
||||||
options.Add(new(TimetableLessonDestination.Viewer, "📋 Planungsviewer"));
|
|
||||||
}
|
|
||||||
if (HasGroupId)
|
|
||||||
{
|
|
||||||
options.Add(new(TimetableLessonDestination.SeatingPlan, "🪑 Sitzplan"));
|
|
||||||
options.Add(new(TimetableLessonDestination.Planning, "✏️ Planung"));
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public bool HasDestinationOptions => DestinationOptions.Count > 0;
|
|
||||||
|
|
||||||
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
|
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
|
||||||
string colorHex, string? lessonTopic, string? examTitle, bool hasUnhandledHomework = false,
|
string colorHex, string? lessonTopic, string? examTitle, bool hasUnhandledHomework = false,
|
||||||
Lesson? lesson = null)
|
Lesson? lesson = null)
|
||||||
|
|||||||
@@ -21,6 +21,21 @@
|
|||||||
<Style Selector="Border.supervisioncell.substitution">
|
<Style Selector="Border.supervisioncell.substitution">
|
||||||
<Setter Property="Background" Value="#8E24AA"/>
|
<Setter Property="Background" Value="#8E24AA"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="Button.weekCellMenuTrigger">
|
||||||
|
<Setter Property="Width" Value="18"/>
|
||||||
|
<Setter Property="Height" Value="16"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="Margin" Value="2"/>
|
||||||
|
<Setter Property="Background" Value="#33000000"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="CornerRadius" Value="4"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.weekCellMenuTrigger:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#55000000"/>
|
||||||
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*">
|
<Grid RowDefinitions="Auto,Auto,*">
|
||||||
@@ -111,24 +126,17 @@
|
|||||||
Foreground="#8E6C00" FontWeight="SemiBold"
|
Foreground="#8E6C00" FontWeight="SemiBold"
|
||||||
IsVisible="{Binding HasUnhandledHomework}"/>
|
IsVisible="{Binding HasUnhandledHomework}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<!-- Nutzer-Feedback: unklar, wie man aus der Tagesliste zwischen
|
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||||
Unterrichtsansicht/Sitzplan/Planung/Planungsviewer wechselt — jetzt
|
<Button Content="▶ Unterricht" FontSize="11" Padding="9,4"
|
||||||
ein Dropdown mit ausdrücklich benannten Zielen statt zweier Buttons
|
IsVisible="{Binding HasLesson}"
|
||||||
mit vom Lesson-Status abhängiger Doppelbedeutung. Wiring/Routing in
|
ToolTip.Tip="Unterrichtsmodus: Verlaufsplan und Sitzplan mit Schnellbewertung auf einem Bildschirm."
|
||||||
TimetableView.axaml.cs (SelectionChanged setzt die Auswahl danach
|
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).StartTeachingModeCommand}"
|
||||||
bewusst zurück auf null — Menü-Charakter, keine dauerhafte
|
CommandParameter="{Binding}"/>
|
||||||
Auswahl). -->
|
<Button Content="{Binding OpenButtonLabel}" FontSize="11" Padding="9,4"
|
||||||
<ComboBox Grid.Column="3" FontSize="11" MinWidth="150" VerticalAlignment="Center"
|
IsVisible="{Binding HasGroupId}"
|
||||||
IsVisible="{Binding HasDestinationOptions}"
|
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}"
|
||||||
PlaceholderText="Öffnen ▾"
|
CommandParameter="{Binding}"/>
|
||||||
ItemsSource="{Binding DestinationOptions}"
|
</StackPanel>
|
||||||
SelectionChanged="OnLessonDestinationSelected">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:TimetableDestinationOption">
|
|
||||||
<TextBlock Text="{Binding Label}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@@ -233,7 +241,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||||
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
||||||
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
|
<Panel IsVisible="{Binding IsAssigned}">
|
||||||
|
<Button Background="{Binding ColorHex}"
|
||||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
|
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
|
||||||
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).OpenWeekCellCommand}"
|
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).OpenWeekCellCommand}"
|
||||||
@@ -263,6 +272,34 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
<!-- Nutzer-Feedback (zweite Runde): Popup-Menü statt Dropdown, hier
|
||||||
|
im Wochenraster statt in der Tagesliste (dort waren die
|
||||||
|
bisherigen zwei Buttons schon in Ordnung). Sibling-Button statt
|
||||||
|
verschachteltem Button/ContextMenu — Routing über
|
||||||
|
MenuItem.Click in TimetableView.axaml.cs, siehe Kommentar dort
|
||||||
|
zu DataContext-Vererbung vs. $parent-Vorfahrensuche im Flyout. -->
|
||||||
|
<Button Classes="weekCellMenuTrigger" Content="⋮"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||||
|
IsVisible="{Binding HasGroupId}"
|
||||||
|
ToolTip.Tip="Weitere Ziele…">
|
||||||
|
<Button.Flyout>
|
||||||
|
<MenuFlyout Placement="BottomEdgeAlignedRight">
|
||||||
|
<MenuItem Header="▶ Unterrichtsansicht" IsVisible="{Binding HasLesson}"
|
||||||
|
Tag="{x:Static vm:TimetableLessonDestination.TeachingMode}"
|
||||||
|
Click="OnWeekCellMenuItemClick"/>
|
||||||
|
<MenuItem Header="📋 Planungsviewer" IsVisible="{Binding HasLesson}"
|
||||||
|
Tag="{x:Static vm:TimetableLessonDestination.Viewer}"
|
||||||
|
Click="OnWeekCellMenuItemClick"/>
|
||||||
|
<MenuItem Header="🪑 Sitzplan"
|
||||||
|
Tag="{x:Static vm:TimetableLessonDestination.SeatingPlan}"
|
||||||
|
Click="OnWeekCellMenuItemClick"/>
|
||||||
|
<MenuItem Header="✏️ Planung"
|
||||||
|
Tag="{x:Static vm:TimetableLessonDestination.Planning}"
|
||||||
|
Click="OnWeekCellMenuItemClick"/>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
@@ -28,35 +29,42 @@ public partial class TimetableView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nutzer-Feedback: unklar, wie man aus der "Heute"-Tagesliste zwischen Unterrichtsansicht,
|
/// Nutzer-Feedback (zweite Runde — die erste Fassung hatte das Problem an der falschen
|
||||||
/// Sitzplan, Planung und Planungsviewer wechselt. Statt der bisherigen zwei Buttons mit
|
/// Stelle gelöst, in der "Heute"-Tagesliste statt im Wochenraster): im Wochenraster oben
|
||||||
/// teils vom Lesson-Status abhängiger Doppelbedeutung ein Dropdown mit ausdrücklich benannten
|
/// führt ein Klick auf eine Stunden-Kachel bislang entweder in den Planungsviewer oder zur
|
||||||
/// Zielen (TodayLessonItem.DestinationOptions) — Menü-Charakter, die Auswahl wird danach
|
/// Einheitenplanung — je nachdem, ob schon eine Lesson existiert, ohne dass das von außen
|
||||||
/// bewusst zurückgesetzt statt dauerhaft angezeigt. Drei der vier Ziele nutzen unverändert
|
/// erkennbar wäre. Popup-Menü (MenuFlyout, kein ComboBox mehr) mit allen vier Zielen als
|
||||||
/// die bestehenden TimetableViewModel-Commands, nur "Sitzplan" ist neu (bisher nur über den
|
/// Alternative zum Direktklick, siehe <see cref="TimetableViewModel.OpenWeekCellCommand"/>
|
||||||
/// Unterrichtsmodus erreichbar, siehe TODO.md 4.5.23-Nachtrag).
|
/// für den "einheitlicheren" Standard-Klick (Unterrichtsansicht bei laufender Stunde, sonst
|
||||||
private void OnLessonDestinationSelected(object? sender, SelectionChangedEventArgs e)
|
/// Planungsviewer, sonst Einheitenplanung). 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 <c>((MenuItem)sender).DataContext</c> statt über eine Binding.
|
||||||
|
private async void OnWeekCellMenuItemClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not ComboBox comboBox) return;
|
if (sender is not MenuItem { Tag: TimetableLessonDestination destination } menuItem) return;
|
||||||
var item = comboBox.DataContext as TodayLessonItem;
|
if (menuItem.DataContext is not WeekCellItem cell) return;
|
||||||
var option = e.AddedItems.Count > 0 ? e.AddedItems[0] as TimetableDestinationOption : null;
|
if (DataContext is not TimetableViewModel vm) return;
|
||||||
comboBox.SelectedItem = null;
|
|
||||||
if (item is null || option is null || DataContext is not TimetableViewModel vm) return;
|
|
||||||
|
|
||||||
switch (option.Kind)
|
switch (destination)
|
||||||
{
|
{
|
||||||
case TimetableLessonDestination.TeachingMode:
|
case TimetableLessonDestination.TeachingMode:
|
||||||
vm.StartTeachingModeCommand.Execute(item);
|
if (cell.Lesson is { } teachLesson && vm.OnOpenTeachingMode is not null)
|
||||||
|
await vm.OnOpenTeachingMode(teachLesson);
|
||||||
break;
|
break;
|
||||||
case TimetableLessonDestination.Viewer:
|
case TimetableLessonDestination.Viewer:
|
||||||
vm.OpenTodayLessonCommand.Execute(item);
|
if (cell.Lesson is { } viewLesson && vm.OnOpenLessonViewer is not null)
|
||||||
|
await vm.OnOpenLessonViewer(viewLesson);
|
||||||
break;
|
break;
|
||||||
case TimetableLessonDestination.Planning:
|
case TimetableLessonDestination.Planning:
|
||||||
vm.OpenGroupCommand.Execute(item.GroupId);
|
if (cell.GroupId != Guid.Empty)
|
||||||
|
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToGroupDetail(cell.GroupId, 6);
|
||||||
break;
|
break;
|
||||||
case TimetableLessonDestination.SeatingPlan:
|
case TimetableLessonDestination.SeatingPlan:
|
||||||
App.Services.GetRequiredService<MainWindowViewModel>()
|
if (cell.GroupId != Guid.Empty)
|
||||||
.NavigateToGroupDetail(item.GroupId, 2);
|
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToGroupDetail(cell.GroupId, 2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1918,18 +1918,35 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
|
|||||||
den vollen Verlaufsplan-Editor öffnen müssen. Neue kleine
|
den vollen Verlaufsplan-Editor öffnen müssen. Neue kleine
|
||||||
`TeachingModeHomeworkViewModel : ObservableObject`, da `TeachingModeViewModel` selbst (wie
|
`TeachingModeHomeworkViewModel : ObservableObject`, da `TeachingModeViewModel` selbst (wie
|
||||||
`LessonViewerViewModel`) keine Bindable-Basisklasse hat.
|
`LessonViewerViewModel`) keine Bindable-Basisklasse hat.
|
||||||
- [x] **4.5.24** Dropdown in der "Heute"-Tagesliste für Unterrichtsansicht/Sitzplan/Planung/
|
- [x] **4.5.24** Popup-Menü im Wochenraster für Unterrichtsansicht/Sitzplan/Planung/Planungsviewer
|
||||||
Planungsviewer (August 2026, Nutzer-Feedback): unklar, wie man aus dem Stundenplan zwischen
|
(August 2026, Nutzer-Feedback, zweite Runde). Die erste Fassung hatte das Problem am
|
||||||
diesen vier Ansichten wechselt — bisher zwei Buttons mit teils vom Lesson-Status abhängiger
|
falschen Ort gelöst — ein Dropdown in der "Heute"-**Tagesliste** (unten angedockt), obwohl
|
||||||
Doppelbedeutung ("Verlaufsplan ansehen"/"Zur Lerngruppe"). Ersetzt durch eine `ComboBox` pro
|
die dortigen zwei Buttons laut Nutzer schon in Ordnung waren. Zurückgesetzt auf den
|
||||||
Zeile mit vier ausdrücklich benannten Zielen (`TodayLessonItem.DestinationOptions`,
|
ursprünglichen Stand (zwei Buttons, `TodayLessonItem` ohne `DestinationOptions`). Das
|
||||||
`TimetableLessonDestination`-Enum) — Unterrichtsansicht/Planungsviewer nur wenn für den Slot
|
eigentliche Problem lag im **Wochenraster darüber**: ein Klick auf eine Stunden-Kachel führt
|
||||||
schon eine `Lesson` existiert, Sitzplan/Planung immer. Menü-Charakter: `SelectionChanged` in
|
dort in den Planungsviewer oder zur Einheitenplanung, je nachdem ob schon eine `Lesson`
|
||||||
`TimetableView.axaml.cs` setzt `SelectedItem` nach jeder Auswahl bewusst auf `null` zurück,
|
existiert — von außen nicht erkennbar, welches Ziel man bekommt.
|
||||||
statt die Auswahl dauerhaft anzuzeigen. Drei der vier Ziele nutzen unverändert bestehende
|
- **Popup-Menü:** kleiner "⋮"-Button pro Kachel (`Button.weekCellMenuTrigger`, oben rechts in
|
||||||
`TimetableViewModel`-Commands (`StartTeachingModeCommand`/`OpenTodayLessonCommand`/
|
der Zelle überlagert) öffnet ein `MenuFlyout` mit vier ausdrücklich benannten Zielen.
|
||||||
`OpenGroupCommand` — letzterer navigiert bereits zum Planung-Tab, Index 6); "Sitzplan" ist
|
Unterrichtsansicht/Planungsviewer nur wenn schon eine `Lesson` existiert, Sitzplan/Planung
|
||||||
neu (Sitzpläne-Tab, Index 2 — bisher nur indirekt über den Unterrichtsmodus erreichbar).
|
immer (Planung ist ohnehin der Ort, an dem man eine Lesson für den Slot erst anlegt).
|
||||||
|
**Wichtiger XAML-Fallstrick, diesmal genauer verstanden:** ein `$parent[ItemsControl]`-
|
||||||
|
Vorfahrenpfad (wie beim normalen Zeilen-Button) funktioniert in einem `MenuFlyout` nicht,
|
||||||
|
weil dessen Popup nicht im normalen visuellen Baum hängt — **aber** eine normale
|
||||||
|
`{Binding}` ohne Vorfahrensuche funktioniert dort sehr wohl, weil Avalonia die
|
||||||
|
DataContext-**Vererbung** (kein Baum-Durchsuchen, nur der geerbte Wert) auch in Flyouts
|
||||||
|
korrekt weiterreicht. Trotzdem bewusst `MenuItem.Click` statt `Command`-Binding gewählt
|
||||||
|
(kein `WeekCellItem`-eigenes Command nötig): Handler in `TimetableView.axaml.cs` liest die
|
||||||
|
Zelle über `((MenuItem)sender).DataContext`, das Ziel über `Tag="{x:Static
|
||||||
|
vm:TimetableLessonDestination.…}"` — beides ohne jede Vorfahrensuche.
|
||||||
|
- **Einheitlicherer Direktklick:** `OpenWeekCellCommand` prüft jetzt zusätzlich, ob gerade
|
||||||
|
(heute, ±10 Minuten Toleranz) Unterrichtszeit der Stunde ist (neues
|
||||||
|
`TimetableViewModel.IsAroundTeachingTime`, `PeriodScheduleService`/"Stundenraster" als
|
||||||
|
Grundlage) — dann direkt in den Unterrichtsmodus statt in den Planungsviewer. Ohne Lesson
|
||||||
|
bleibt es beim Sprung zur Einheitenplanung. Das Popup-Menü bietet immer alle vier Ziele
|
||||||
|
explizit an, unabhängig von dieser Automatik. Nutzt bewusst `lesson.Date` statt
|
||||||
|
`WeekCellItem.Date` — letzteres ist nur bei Kopfzeilen gesetzt, nicht bei
|
||||||
|
Stunden-Kacheln (führte im ersten Testlauf zu einem Bug: die Automatik griff nie).
|
||||||
|
|
||||||
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
|
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
|
||||||
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
|
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
|
||||||
|
|||||||
Reference in New Issue
Block a user