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
@@ -127,10 +127,29 @@ public partial class SettingsViewModel : ObservableObject
public List<string> StateOptions { get; } = GermanStateDisplay.Options.ToList();
public ObservableCollection<SchoolHolidayItem> SchoolHolidayEntries { get; } = [];
// ── Stundenraster: Uhrzeiten je Einzelstunde (4.2.2 Nachtrag) ────────────
[ObservableProperty] private string _periodTimesError = "";
[ObservableProperty] private string _periodTimesStatus = "";
public ObservableCollection<PeriodTimeEditItem> PeriodTimes { get; } = [];
// ── Aufsichten: wiederkehrende Pausenaufsicht (4.3 Nachtrag) ─────────────
[ObservableProperty] private string _newDutyWeekdayName = WeekdayDisplay.Options[0];
[ObservableProperty] private int _newDutyAfterPeriod;
[ObservableProperty] private string _newDutyLocation = "";
[ObservableProperty] private string _newDutyError = "";
public string[] WeekdayOptions { get; } = WeekdayDisplay.Options;
public ObservableCollection<SupervisionDutyItem> SupervisionDuties { get; } = [];
// ── Konstruktor ───────────────────────────────────────────────────────────
private readonly ISchoolHolidayRepository _schoolHolidays;
private readonly SchoolCalendarSettingsService _calendarSettings;
private readonly PeriodScheduleService _periodSchedule;
private readonly ISupervisionDutyRepository _supervisionDuties;
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
@@ -138,7 +157,8 @@ public partial class SettingsViewModel : ObservableObject
AppLockService appLock, LiteDbContext dbContext, PrivacySettingsService privacy,
IDocumentationRepository documentation, IStudentRepository students,
IShorthandCodeRepository shorthandCodes, ISchoolHolidayRepository schoolHolidays,
SchoolCalendarSettingsService calendarSettings)
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
ISupervisionDutyRepository supervisionDuties)
{
_subjects = subjects;
_domainRepo = domainRepo;
@@ -155,6 +175,8 @@ public partial class SettingsViewModel : ObservableObject
_shorthandCodes = shorthandCodes;
_schoolHolidays = schoolHolidays;
_calendarSettings = calendarSettings;
_periodSchedule = periodSchedule;
_supervisionDuties = supervisionDuties;
LoadSubjects();
LoadShorthandCodes();
LoadGradingKeyTemplates();
@@ -167,6 +189,84 @@ public partial class SettingsViewModel : ObservableObject
LoadExpiredDocuments();
SelectedStateName = GermanStateDisplay.Label(_calendarSettings.State);
LoadSchoolHolidays();
LoadPeriodTimes();
LoadSupervisionDuties();
}
// ── Aufsichten: Laden / Hinzufügen / Löschen ─────────────────────────────
private void LoadSupervisionDuties()
{
SupervisionDuties.Clear();
foreach (var d in _supervisionDuties.GetAll())
SupervisionDuties.Add(new SupervisionDutyItem(d));
}
[RelayCommand]
private void AddSupervisionDuty()
{
NewDutyError = "";
if (string.IsNullOrWhiteSpace(NewDutyLocation)) { NewDutyError = "Ort/Bezeichnung erforderlich."; return; }
if (NewDutyAfterPeriod is < 0 or > 10) { NewDutyError = "Muss zwischen 0 und 10 liegen."; return; }
try
{
_supervisionDuties.Save(new SupervisionDuty
{
Weekday = WeekdayDisplay.FromLabel(NewDutyWeekdayName),
AfterPeriod = NewDutyAfterPeriod,
Location = NewDutyLocation.Trim(),
});
}
catch (InvalidOperationException ex) { NewDutyError = ex.Message; return; }
NewDutyLocation = ""; NewDutyAfterPeriod = 0;
LoadSupervisionDuties();
}
[RelayCommand]
private void RemoveSupervisionDuty(SupervisionDutyItem? item)
{
if (item is null) return;
_supervisionDuties.Delete(item.Id);
SupervisionDuties.Remove(item);
}
// ── Stundenraster: Laden / Speichern ─────────────────────────────────────
private void LoadPeriodTimes()
{
PeriodTimes.Clear();
for (var period = 1; period <= 10; period++)
{
var times = _periodSchedule.GetTimes(period);
PeriodTimes.Add(new PeriodTimeEditItem(period, times?.Start, times?.End));
}
}
[RelayCommand]
private void SavePeriodTimes()
{
PeriodTimesError = ""; PeriodTimesStatus = "";
var entries = new List<PeriodTimeEntry>();
foreach (var item in PeriodTimes)
{
if (string.IsNullOrWhiteSpace(item.StartText) && string.IsNullOrWhiteSpace(item.EndText))
continue; // Stunde bewusst nicht konfiguriert — ok, keine Pflicht für alle 10.
if (!TimeOnly.TryParseExact(item.StartText, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var start) ||
!TimeOnly.TryParseExact(item.EndText, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var end))
{ PeriodTimesError = $"{item.PeriodLabel}: Format HH:MM."; return; }
if (end <= start)
{ PeriodTimesError = $"{item.PeriodLabel}: Ende muss nach dem Beginn liegen."; return; }
entries.Add(new PeriodTimeEntry { PeriodNumber = item.PeriodNumber, Start = start, End = end });
}
_periodSchedule.SetPeriods(entries);
PeriodTimesStatus = "Gespeichert.";
}
// ── Ferien & Feiertage: Bundesland / Schulferien pflegen ─────────────────
@@ -842,6 +942,47 @@ public class SchoolHolidayItem(SchoolHoliday h)
public string RangeDisplay { get; } = $"{h.StartDate:dd.MM.yyyy} {h.EndDate:dd.MM.yyyy}";
}
public partial class PeriodTimeEditItem : ObservableObject
{
public int PeriodNumber { get; }
public string PeriodLabel { get; }
[ObservableProperty] private string _startText;
[ObservableProperty] private string _endText;
public PeriodTimeEditItem(int periodNumber, TimeOnly? start, TimeOnly? end)
{
PeriodNumber = periodNumber;
PeriodLabel = $"{periodNumber}. Stunde";
_startText = start?.ToString("HH:mm") ?? "";
_endText = end?.ToString("HH:mm") ?? "";
}
}
// ── Wochentag: deutsche Anzeige (MoFr, für Aufsichten) ────────────────────────
public static class WeekdayDisplay
{
private static readonly (DayOfWeek Day, string Name)[] Entries =
[
(DayOfWeek.Monday, "Montag"), (DayOfWeek.Tuesday, "Dienstag"), (DayOfWeek.Wednesday, "Mittwoch"),
(DayOfWeek.Thursday, "Donnerstag"), (DayOfWeek.Friday, "Freitag"),
];
public static string[] Options { get; } = Entries.Select(e => e.Name).ToArray();
public static string Label(DayOfWeek d) => Entries.First(e => e.Day == d).Name;
public static DayOfWeek FromLabel(string label) => Entries.FirstOrDefault(e => e.Name == label).Day;
}
public class SupervisionDutyItem(SupervisionDuty d)
{
public Guid Id { get; } = d.Id;
public string WeekdayLabel { get; } = WeekdayDisplay.Label(d.Weekday);
public int AfterPeriod { get; } = d.AfterPeriod;
public string PeriodLabel { get; } = d.AfterPeriod == 0 ? "Vor der 1. Stunde" : $"Nach der {d.AfterPeriod}. Stunde";
public string Location { get; } = d.Location;
}
// ── JSON DTOs ─────────────────────────────────────────────────────────────────
internal class CatalogDto