Unterrichtsplanung: Serienerzeugung, Stundenraster, Aufsichten & Vertretung (Kapitel 4.2/4.3 Nachtrag)

Stunden serienweise aus dem Stundenplan erzeugen (4.2.5); Stundenraster
(Uhrzeiten je Stunde) in den Einstellungen mit Zeitbedarf-Rückmeldung im
Verlaufsplan-Editor; wiederkehrende Pausenaufsicht; neuer "Vertretung
eintragen"-Dialog für einmalige Vertretungsaufsicht, Vertretungsstunde,
Sondereinsätze (Ausflüge, Berufsmessen) und schlichten Stundenausfall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 21:59:07 +02:00
co-authored by Claude Sonnet 5
parent 99b2de6519
commit d6dee72acc
30 changed files with 2743 additions and 48 deletions
@@ -58,11 +58,15 @@ public partial class TimetableViewModel : ObservableObject
private readonly SchoolCalendarSettingsService _calendarSettings;
private readonly PublicHolidayService _publicHolidays;
private readonly SchoolYearService _schoolYear;
private readonly ISupervisionDutyRepository _supervisionDuties;
private readonly ISubstitutionEntryRepository _substitutions;
public ObservableCollection<TimetableCellItem> Cells { get; } = [];
public ObservableCollection<WeekCellItem> WeekItems { get; } = [];
public ObservableCollection<HoursWarningItem> HoursWarnings { get; } = [];
public ObservableCollection<TodayLessonItem> TodayItems { get; } = [];
public ObservableCollection<TodaySupervisionItem> TodaySupervisions { get; } = [];
public ObservableCollection<TodaySpecialAssignmentItem> TodaySpecialAssignments { get; } = [];
[ObservableProperty] private int _activeTabIndex;
[ObservableProperty] private string _todayLabel = "";
@@ -72,15 +76,18 @@ public partial class TimetableViewModel : ObservableObject
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
public Action<Guid>? OnNavigateToGroup { get; set; }
public Func<Task>? OnAddSubstitution { get; set; }
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
ISchoolHolidayRepository schoolHolidays, SchoolCalendarSettingsService calendarSettings,
PublicHolidayService publicHolidays, SchoolYearService schoolYear)
PublicHolidayService publicHolidays, SchoolYearService schoolYear,
ISupervisionDutyRepository supervisionDuties, ISubstitutionEntryRepository substitutions)
{
_slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams;
_schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings;
_publicHolidays = publicHolidays; _schoolYear = schoolYear;
_supervisionDuties = supervisionDuties; _substitutions = substitutions;
Load();
}
@@ -95,6 +102,14 @@ public partial class TimetableViewModel : ObservableObject
[RelayCommand]
private void CurrentWeek() { WeekOffset = 0; Load(); }
[RelayCommand]
private async Task AddSubstitution()
{
if (OnAddSubstitution is null) return;
await OnAddSubstitution();
Load();
}
public void Load()
{
var today = DateOnly.FromDateTime(DateTime.Today);
@@ -106,33 +121,90 @@ public partial class TimetableViewModel : ObservableObject
var holidayBadges = ComputeHolidayBadges(today, publicHolidayDates);
var examProximity = ComputeExamProximity(today, publicHolidayDates);
var duties = _supervisionDuties.GetAll();
BuildGrid(holidayBadges);
BuildWeekOverview(today, publicHolidayDates);
BuildGrid(holidayBadges, duties);
BuildWeekOverview(today, publicHolidayDates, duties);
BuildToday(today);
BuildHoursWarnings();
}
// ── "Heute": Tagesliste ──────────────────────────────────────────────────
/// <summary>
/// Vertretungsstunden (<see cref="SubstitutionEntry"/>, Kind Lesson) überschreiben für ihre
/// Stunde die normale, aus dem Stundenplan abgeleitete Anzeige — sie beschreiben, was an
/// diesem konkreten Tag tatsächlich stattfindet. Stunden ohne passenden Stundenplan-Slot (z.B.
/// Vertretung in einer fremden Gruppe) werden zusätzlich angehängt.
/// </summary>
private void BuildToday(DateOnly today)
{
TodayItems.Clear();
var slotsToday = _slots.GetAll().Where(s => s.Weekday == today.DayOfWeek).OrderBy(s => s.PeriodNumber).ToList();
var substitutionsToday = _substitutions.GetByDate(today);
var slotsToday = _slots.GetAll().Where(s => s.Weekday == today.DayOfWeek).ToList();
var items = new List<TodayLessonItem>();
var coveredPeriods = new HashSet<int>();
foreach (var slot in slotsToday)
{
coveredPeriods.Add(slot.PeriodNumber);
var lessonSub = substitutionsToday.FirstOrDefault(s => s.Kind == SubstitutionKind.Lesson && s.PeriodNumber == slot.PeriodNumber);
if (lessonSub is not null) { items.Add(TodayLessonItem.ForSubstitution(slot.PeriodNumber, lessonSub)); continue; }
var group = _groups.GetById(slot.GroupId);
if (group is null) continue;
var cancelled = substitutionsToday.FirstOrDefault(s => s.Kind == SubstitutionKind.Cancelled && s.PeriodNumber == slot.PeriodNumber);
if (cancelled is not null) { items.Add(TodayLessonItem.ForCancelled(slot.GroupId, slot.PeriodNumber, group.Name, cancelled)); continue; }
var lesson = _lessons.GetByGroupAndDate(slot.GroupId, today).FirstOrDefault();
var exam = _exams.GetByGroup(slot.GroupId).FirstOrDefault(e => e.Date == today);
TodayItems.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
items.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title));
}
foreach (var sub in substitutionsToday.Where(s => s.Kind == SubstitutionKind.Lesson &&
s.PeriodNumber is int p && !coveredPeriods.Contains(p)))
items.Add(TodayLessonItem.ForSubstitution(sub.PeriodNumber!.Value, sub));
TodayItems.Clear();
foreach (var item in items.OrderBy(i => i.PeriodNumber)) TodayItems.Add(item);
TodaySupervisions.Clear();
foreach (var item in BuildTodaySupervisionItems(today, substitutionsToday)) TodaySupervisions.Add(item);
TodaySpecialAssignments.Clear();
foreach (var sub in substitutionsToday.Where(s => s.Kind == SubstitutionKind.SpecialAssignment))
{
var periodLabel = sub.IsAllDay ? "Ganztägig" : $"{sub.FromPeriod}.{sub.ToPeriod}. Stunde";
TodaySpecialAssignments.Add(new TodaySpecialAssignmentItem(periodLabel, sub.Description, sub.GroupLabel));
}
}
private List<TodaySupervisionItem> BuildTodaySupervisionItems(DateOnly today, List<SubstitutionEntry> substitutionsToday)
{
var dutiesToday = _supervisionDuties.GetAll().Where(d => d.Weekday == today.DayOfWeek).ToList();
var subsToday = substitutionsToday.Where(s => s.Kind == SubstitutionKind.Supervision).ToList();
var afterPeriods = dutiesToday.Select(d => d.AfterPeriod)
.Concat(subsToday.Select(s => s.AfterPeriod!.Value))
.Distinct().OrderBy(p => p);
var result = new List<TodaySupervisionItem>();
foreach (var afterPeriod in afterPeriods)
{
var sub = subsToday.FirstOrDefault(s => s.AfterPeriod == afterPeriod);
if (sub is not null) { result.Add(new TodaySupervisionItem(afterPeriod, sub.Description, isSubstitution: true)); continue; }
var duty = dutiesToday.First(d => d.AfterPeriod == afterPeriod);
result.Add(new TodaySupervisionItem(afterPeriod, duty.Location, isSubstitution: false));
}
return result;
}
[RelayCommand]
private void OpenGroup(Guid groupId) => OnNavigateToGroup?.Invoke(groupId);
private void OpenGroup(Guid groupId)
{
if (groupId == Guid.Empty) return; // Vertretung in fremder Gruppe ohne echte GroupId
OnNavigateToGroup?.Invoke(groupId);
}
[RelayCommand]
private void ShowEditor() => ActiveTabIndex = 1;
@@ -146,9 +218,11 @@ public partial class TimetableViewModel : ObservableObject
/// parallele Kurs oder die nächste Stunde in der Woche auf einen Blick sichtbar sind. Die
/// Badges (Ferien-Nähe, Klausur-Nähe) werden je Zelle am dort angezeigten Datum ausgerichtet,
/// nicht am realen "heute" — sonst würde beim Blättern in andere Wochen ein Badge angezeigt,
/// das eigentlich zu einer ganz anderen Woche gehört.
/// das eigentlich zu einer ganz anderen Woche gehört. Aufsicht-Zeilen (wiederkehrend + einmalige
/// Vertretungsaufsicht) werden zwischen den betroffenen Stundenzeilen eingefügt, Vertretungsstunden
/// ersetzen für ihre Stunde die sonst aus dem Stundenplan abgeleitete Anzeige.
/// </summary>
private void BuildWeekOverview(DateOnly today, HashSet<DateOnly> publicHolidayDates)
private void BuildWeekOverview(DateOnly today, HashSet<DateOnly> publicHolidayDates, List<SupervisionDuty> duties)
{
WeekItems.Clear();
var currentWeekMonday = today.AddDays(-((int)today.DayOfWeek + 6) % 7);
@@ -160,22 +234,54 @@ public partial class TimetableViewModel : ObservableObject
var groups = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id);
var schoolHolidays = _schoolHolidays.GetAll();
var dateByWeekday = Weekdays.ToDictionary(w => w, w => monday.AddDays((int)w - (int)DayOfWeek.Monday));
var dutiesByPeriod = duties.ToLookup(d => d.AfterPeriod);
var substitutionsThisWeek = dateByWeekday.Values.SelectMany(d => _substitutions.GetByDate(d)).ToList();
WeekItems.Add(WeekCellItem.Corner());
foreach (var weekday in Weekdays)
WeekItems.Add(WeekCellItem.WeekdayHeader(weekday, dateByWeekday[weekday], dateByWeekday[weekday] == today));
AddWeekSupervisionRowIfAny(0, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
for (var period = FirstPeriod; period <= LastPeriod; period++)
{
WeekItems.Add(WeekCellItem.PeriodLabel(period));
foreach (var weekday in Weekdays)
{
var date = dateByWeekday[weekday];
var lessonSub = substitutionsThisWeek.FirstOrDefault(s =>
s.Kind == SubstitutionKind.Lesson && s.Date == date && s.PeriodNumber == period);
if (lessonSub is not null)
{
WeekItems.Add(WeekCellItem.ForSubstitutionLesson(weekday, period, date == today, lessonSub));
continue;
}
var specialAssignment = substitutionsThisWeek.FirstOrDefault(s =>
s.Kind == SubstitutionKind.SpecialAssignment && s.Date == date &&
(s.IsAllDay || (period >= s.FromPeriod && period <= s.ToPeriod)));
if (specialAssignment is not null)
{
WeekItems.Add(WeekCellItem.ForSpecialAssignment(weekday, period, date == today, specialAssignment));
continue;
}
var slot = allSlots.FirstOrDefault(s => s.Weekday == weekday && s.PeriodNumber == period);
if (slot is null) { WeekItems.Add(WeekCellItem.Empty(weekday, period)); continue; }
var date = dateByWeekday[weekday];
var group = groups.GetValueOrDefault(slot.GroupId);
var subject = group?.SubjectId is { } subjectId ? _subjects.GetById(subjectId) : null;
var cancelled = substitutionsThisWeek.FirstOrDefault(s =>
s.Kind == SubstitutionKind.Cancelled && s.Date == date && s.PeriodNumber == period);
if (cancelled is not null)
{
WeekItems.Add(WeekCellItem.ForCancelled(weekday, period, date == today, cancelled,
subject?.ShortName is { Length: > 0 } csn ? csn : subject?.Name ?? "",
group?.Name ?? "?", slot.GroupId));
continue;
}
var lesson = _lessons.GetByGroupAndDate(slot.GroupId, date).FirstOrDefault();
var hasExam = _exams.GetByGroup(slot.GroupId).Any(e => e.Date == date);
var isHoliday = IsFreeDay(date, schoolHolidays, publicHolidayDates);
@@ -189,6 +295,30 @@ public partial class TimetableViewModel : ObservableObject
colorHex, holidayBadge, hasExam, isLastBeforeExam,
MentionsExperiment(lesson), slot.GroupId, isHoliday));
}
AddWeekSupervisionRowIfAny(period, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
}
}
private void AddWeekSupervisionRowIfAny(int afterPeriod, ILookup<int, SupervisionDuty> dutiesByPeriod,
List<SubstitutionEntry> substitutionsThisWeek, Dictionary<DayOfWeek, DateOnly> dateByWeekday)
{
var hasAny = dutiesByPeriod.Contains(afterPeriod) ||
substitutionsThisWeek.Any(s => s.Kind == SubstitutionKind.Supervision && s.AfterPeriod == afterPeriod);
if (!hasAny) return;
WeekItems.Add(WeekCellItem.SupervisionRowLabel(afterPeriod));
foreach (var weekday in Weekdays)
{
var date = dateByWeekday[weekday];
var substitution = substitutionsThisWeek.FirstOrDefault(s =>
s.Kind == SubstitutionKind.Supervision && s.Date == date && s.AfterPeriod == afterPeriod);
if (substitution is not null)
{
WeekItems.Add(WeekCellItem.SupervisionCell(substitution.Description, isSubstitution: true));
continue;
}
var duty = dutiesByPeriod[afterPeriod].FirstOrDefault(d => d.Weekday == weekday);
WeekItems.Add(WeekCellItem.SupervisionCell(duty?.Location ?? "", isSubstitution: false));
}
}
@@ -226,16 +356,19 @@ public partial class TimetableViewModel : ObservableObject
// ── Bearbeiten-Raster ─────────────────────────────────────────────────────
private void BuildGrid(Dictionary<(DayOfWeek Weekday, Guid GroupId), string> badges)
private void BuildGrid(Dictionary<(DayOfWeek Weekday, Guid GroupId), string> badges, List<SupervisionDuty> duties)
{
Cells.Clear();
var allSlots = _slots.GetAll();
var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name);
var dutiesByPeriod = duties.ToLookup(d => d.AfterPeriod);
Cells.Add(TimetableCellItem.Corner());
foreach (var weekday in Weekdays)
Cells.Add(TimetableCellItem.WeekdayHeader(weekday));
AddGridSupervisionRowIfAny(0, dutiesByPeriod);
for (var period = FirstPeriod; period <= LastPeriod; period++)
{
Cells.Add(TimetableCellItem.PeriodLabel(period));
@@ -246,6 +379,21 @@ public partial class TimetableViewModel : ObservableObject
var badge = slot is not null ? badges.GetValueOrDefault((weekday, slot.GroupId), "") : "";
Cells.Add(TimetableCellItem.ForSlot(weekday, period, slot, groupName, ColorFor(groupName), badge));
}
AddGridSupervisionRowIfAny(period, dutiesByPeriod);
}
}
/// <summary>Aufsicht wird nur in den Einstellungen gepflegt (siehe SettingsView) — im
/// Bearbeiten-Raster ist die Zeile bewusst nur Anzeige, kein weiterer Klick-Dialog nötig für
/// eine so kleine, seltene Pflegeaufgabe.</summary>
private void AddGridSupervisionRowIfAny(int afterPeriod, ILookup<int, SupervisionDuty> dutiesByPeriod)
{
if (!dutiesByPeriod.Contains(afterPeriod)) return;
Cells.Add(TimetableCellItem.SupervisionRowLabel(afterPeriod));
foreach (var weekday in Weekdays)
{
var duty = dutiesByPeriod[afterPeriod].FirstOrDefault(d => d.Weekday == weekday);
Cells.Add(TimetableCellItem.SupervisionCell(duty?.Location ?? ""));
}
}
@@ -345,6 +493,8 @@ public class TimetableCellItem
{
public bool IsHeader { get; private init; }
public bool IsPeriodLabel { get; private init; }
public bool IsSupervisionRow { get; private init; }
public bool IsSupervisionCell { get; private init; }
public string Text { get; private init; } = "";
public DayOfWeek? Weekday { get; private init; }
public int PeriodNumber { get; private init; }
@@ -352,9 +502,11 @@ public class TimetableCellItem
public string GroupName { get; private init; } = "";
public string ColorHex { get; private init; } = "#9E9E9E";
public string BadgeText { get; private init; } = "";
public string SupervisionLocation { get; private init; } = "";
public bool HasSupervision => SupervisionLocation.Length > 0;
public bool HasBadge => BadgeText.Length > 0;
public bool IsAssigned => Slot is not null;
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
public bool IsSlotCell => !IsHeader && !IsPeriodLabel && !IsSupervisionRow && !IsSupervisionCell;
public static TimetableCellItem Corner() => new() { IsHeader = true, Text = "" };
@@ -370,6 +522,18 @@ public class TimetableCellItem
public static TimetableCellItem PeriodLabel(int period) => new() { IsPeriodLabel = true, Text = period.ToString() };
public static TimetableCellItem SupervisionRowLabel(int afterPeriod) => new()
{
IsSupervisionRow = true,
Text = afterPeriod == 0 ? "Aufsicht (vor 1.)" : $"Aufsicht (n. {afterPeriod}.)",
};
public static TimetableCellItem SupervisionCell(string location) => new()
{
IsSupervisionCell = true,
SupervisionLocation = location,
};
public static TimetableCellItem ForSlot(DayOfWeek day, int period, TimetableSlot? slot, string groupName,
string colorHex, string badgeText) => new()
{
@@ -384,8 +548,14 @@ public class WeekCellItem
{
public bool IsHeader { get; private init; }
public bool IsPeriodLabel { get; private init; }
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
public bool IsSupervisionRow { get; private init; }
public bool IsSupervisionCell { get; private init; }
public bool IsSlotCell => !IsHeader && !IsPeriodLabel && !IsSupervisionRow && !IsSupervisionCell;
public bool IsAssigned { get; private init; }
public bool IsSubstitutionLesson { get; private init; }
public bool IsSubstitutionSupervision { get; private init; }
public bool IsSpecialAssignment { get; private init; }
public bool IsCancelled { get; private init; }
public string Text { get; private init; } = "";
public DayOfWeek? Weekday { get; private init; }
public int PeriodNumber { get; private init; }
@@ -402,6 +572,8 @@ public class WeekCellItem
public bool IsLastBeforeExam { get; private init; }
public bool HasExperiment { get; private init; }
public bool IsHoliday { get; private init; }
public string SupervisionLocation { get; private init; } = "";
public bool HasSupervision => SupervisionLocation.Length > 0;
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic);
@@ -422,6 +594,19 @@ public class WeekCellItem
public static WeekCellItem Empty(DayOfWeek day, int period) => new() { Weekday = day, PeriodNumber = period };
public static WeekCellItem SupervisionRowLabel(int afterPeriod) => new()
{
IsSupervisionRow = true,
Text = afterPeriod == 0 ? "Aufsicht (vor 1.)" : $"Aufsicht (n. {afterPeriod}.)",
};
public static WeekCellItem SupervisionCell(string location, bool isSubstitution) => new()
{
IsSupervisionCell = true,
SupervisionLocation = location,
IsSubstitutionSupervision = isSubstitution,
};
public static WeekCellItem ForSlot(DayOfWeek day, int period, bool isToday, string subjectLabel,
string groupName, string room, string topic, string colorHex, string holidayBadge,
bool hasExam, bool isLastBeforeExam, bool hasExperiment, Guid groupId, bool isHoliday) => new()
@@ -432,6 +617,30 @@ public class WeekCellItem
IsLastBeforeExam = isLastBeforeExam, HasExperiment = hasExperiment, GroupId = groupId,
IsHoliday = isHoliday,
};
public static WeekCellItem ForSubstitutionLesson(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry) => new()
{
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday, IsSubstitutionLesson = true,
SubjectLabel = "Vertretung", GroupName = entry.GroupLabel, Topic = entry.Description,
ColorHex = "#8E24AA", GroupId = entry.GroupId ?? Guid.Empty,
};
public static WeekCellItem ForSpecialAssignment(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry) => new()
{
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday, IsSpecialAssignment = true,
SubjectLabel = "Sondereinsatz",
GroupName = string.IsNullOrWhiteSpace(entry.GroupLabel) ? "" : entry.GroupLabel,
Topic = entry.Description,
ColorHex = "#00838F", GroupId = entry.GroupId ?? Guid.Empty,
};
public static WeekCellItem ForCancelled(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry,
string subjectLabel, string groupName, Guid groupId) => new()
{
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday, IsCancelled = true,
SubjectLabel = subjectLabel, GroupName = groupName, Topic = entry.Description,
ColorHex = "#757575", GroupId = groupId,
};
}
public class HoursWarningItem(string groupName, int assigned, int expected)
@@ -440,17 +649,53 @@ public class HoursWarningItem(string groupName, int assigned, int expected)
public string Text { get; } = $"{groupName}: {assigned} von {expected} Wochenstunden eingetragen";
}
public class TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
string colorHex, string? lessonTopic, string? examTitle)
public class TodayLessonItem
{
public Guid GroupId { get; } = groupId;
public int PeriodNumber { get; } = periodNumber;
public string GroupName { get; } = groupName;
public string Room { get; } = room;
public string ColorHex { get; } = colorHex;
public string? LessonTopic { get; } = lessonTopic;
public string? ExamTitle { get; } = examTitle;
public Guid GroupId { get; private init; }
public int PeriodNumber { get; private init; }
public string GroupName { get; private init; } = "";
public string Room { get; private init; } = "";
public string ColorHex { get; private init; } = "#9E9E9E";
public string? LessonTopic { get; private init; }
public string? ExamTitle { get; private init; }
public bool IsSubstitution { get; private init; }
public bool IsCancelled { get; private init; }
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
public bool HasLessonTopic => !string.IsNullOrWhiteSpace(LessonTopic);
public bool HasExam => ExamTitle is not null;
public bool HasGroupId => GroupId != Guid.Empty;
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
string colorHex, string? lessonTopic, string? examTitle)
{
GroupId = groupId; PeriodNumber = periodNumber; GroupName = groupName; Room = room;
ColorHex = colorHex; LessonTopic = lessonTopic; ExamTitle = examTitle;
}
public static TodayLessonItem ForSubstitution(int periodNumber, SubstitutionEntry entry) => new(
entry.GroupId ?? Guid.Empty, periodNumber, entry.GroupLabel, "", "#8E24AA", entry.Description, null)
{ IsSubstitution = true };
public static TodayLessonItem ForCancelled(Guid groupId, int periodNumber, string groupName, SubstitutionEntry entry) => new(
groupId, periodNumber, groupName, "", "#757575",
string.IsNullOrWhiteSpace(entry.Description) ? null : entry.Description, null)
{ IsCancelled = true };
}
public class TodaySupervisionItem(int afterPeriod, string description, bool isSubstitution)
{
public int AfterPeriod { get; } = afterPeriod;
public string PeriodLabel { get; } = afterPeriod == 0 ? "Vor der 1. Stunde" : $"Nach der {afterPeriod}. Stunde";
public string Description { get; } = description;
public bool IsSubstitution { get; } = isSubstitution;
}
public class TodaySpecialAssignmentItem(string periodLabel, string description, string groupLabel)
{
public string PeriodLabel { get; } = periodLabel;
public string Description { get; } = description;
public string GroupLabel { get; } = groupLabel;
public bool HasGroupLabel => !string.IsNullOrWhiteSpace(GroupLabel);
public string DisplayText { get; } =
$"{periodLabel} — {description}" + (string.IsNullOrWhiteSpace(groupLabel) ? "" : $" ({groupLabel})");
}