Stundenplan: Wochenraster, Wochennavigation, Ferien/Feiertage (Kapitel 4.3)

Neuer Stundenplan mit "Heute"-Standardansicht (Tagesliste unten angedockt,
gruppenübergreifendes Wochenraster mit Fach/Klasse/Raum/Thema, Vor-/Zurück-
Navigation zwischen Kalenderwochen) und separatem Bearbeiten-Raster für die
wöchentliche Zuordnung. Badges für Ferien-/Klausur-Nähe und ausgegraute
Ferientage direkt im Plan statt einer separaten Liste. Ferien-/Feiertage-
Pflege (Bundesland, Schulferien) sitzt jetzt in den Einstellungen statt im
Stundenplan selbst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 02:29:18 +02:00
co-authored by Claude Sonnet 5
parent ee1a47641e
commit 99b2de6519
26 changed files with 2154 additions and 18 deletions
@@ -0,0 +1,90 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
namespace LehrerApp.Desktop.ViewModels.Planning;
/// <summary>Zuweisen/Bearbeiten/Entfernen eines Stundenplan-Termins (4.3.3).</summary>
public partial class TimetableSlotDialogViewModel : ObservableObject
{
private readonly ITimetableSlotRepository _slots;
private readonly Dictionary<string, Guid> _groupIdsByName;
private readonly TimetableSlot? _editing;
public DayOfWeek Weekday { get; }
public int PeriodNumber { get; }
public string WeekdayLabel { get; }
public string DialogTitle { get; }
public bool IsEditing => _editing is not null;
[ObservableProperty] private string _selectedGroupName = "";
[ObservableProperty] private string _room = "";
[ObservableProperty] private string _groupError = "";
public string[] GroupOptions { get; }
/// null = unverändert/Abbruch, sonst das neue/aktualisierte Ergebnis.
public TimetableSlot? Result { get; private set; }
public bool Deleted { get; private set; }
public TimetableSlotDialogViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
SchoolYearService schoolYear, DayOfWeek weekday, int periodNumber, TimetableSlot? editing)
{
_slots = slots;
_editing = editing;
Weekday = weekday;
PeriodNumber = periodNumber;
WeekdayLabel = weekday switch
{
DayOfWeek.Monday => "Montag", DayOfWeek.Tuesday => "Dienstag",
DayOfWeek.Wednesday => "Mittwoch", DayOfWeek.Thursday => "Donnerstag",
DayOfWeek.Friday => "Freitag", _ => weekday.ToString(),
};
DialogTitle = $"{WeekdayLabel}, {periodNumber}. Stunde";
var availableGroups = groups.GetBySchoolYear(schoolYear.CurrentSchoolYear()).OrderBy(g => g.Name).ToList();
_groupIdsByName = availableGroups.ToDictionary(g => g.Name, g => g.Id);
GroupOptions = availableGroups.Select(g => g.Name).ToArray();
if (editing is not null)
{
SelectedGroupName = availableGroups.FirstOrDefault(g => g.Id == editing.GroupId)?.Name ?? "";
Room = editing.Room ?? "";
}
}
[RelayCommand]
private void Save()
{
GroupError = "";
if (string.IsNullOrWhiteSpace(SelectedGroupName) || !_groupIdsByName.TryGetValue(SelectedGroupName, out var groupId))
{
GroupError = "Bitte eine Gruppe auswählen.";
return;
}
var slot = _editing ?? new TimetableSlot { Weekday = Weekday, PeriodNumber = PeriodNumber };
slot.GroupId = groupId;
slot.Room = string.IsNullOrWhiteSpace(Room) ? null : Room.Trim();
try
{
_slots.Save(slot);
Result = slot;
}
catch (InvalidOperationException ex)
{
GroupError = ex.Message;
}
}
[RelayCommand]
private void Delete()
{
if (_editing is null) return;
_slots.Delete(_editing.Id);
Deleted = true;
}
}