Files
LehrerApp/LehrerApp.Desktop/ViewModels/Planning/SubstitutionEntryDialogViewModel.cs
T
adminandClaude Sonnet 5 d6dee72acc 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>
2026-08-15 21:59:07 +02:00

177 lines
7.8 KiB
C#

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using System.Collections.ObjectModel;
using System.Globalization;
namespace LehrerApp.Desktop.ViewModels.Planning;
public static class SubstitutionKindDisplay
{
public static string[] Options { get; } = ["Aufsicht", "Stunde", "Sondereinsatz", "Ausfall"];
public static string ToName(SubstitutionKind k) => k switch
{
SubstitutionKind.Lesson => "Stunde",
SubstitutionKind.SpecialAssignment => "Sondereinsatz",
SubstitutionKind.Cancelled => "Ausfall",
_ => "Aufsicht",
};
public static SubstitutionKind FromName(string? name) => name switch
{
"Stunde" => SubstitutionKind.Lesson,
"Sondereinsatz" => SubstitutionKind.SpecialAssignment,
"Ausfall" => SubstitutionKind.Cancelled,
_ => SubstitutionKind.Supervision,
};
}
/// <summary>
/// Dialog "Vertretung eintragen" (4.3 Nachtrag, Sonderfälle): einmalige Vertretungsaufsicht,
/// Vertretungsstunde in einer eigenen oder fremden Lerngruppe, ein Sondereinsatz (Ausflug,
/// Berufsmesse, Exkursion, ...), oder ein schlichter Stundenausfall (z.B. weil die betroffene
/// Gruppe selbst auf Klassenfahrt ist — nicht die eigene Abwesenheit, sondern die der Gruppe).
/// Bei eigener Gruppe bleibt der einfache Weg (nur Plan-Eintrag mit Thema) der Standard —
/// "Direkt als Stunde in der Einheit übernehmen" ist ein bewusstes Opt-in, nur bei
/// Vertretungsstunden, damit die meist beiläufigen Vertretungsstunden nicht automatisch die
/// Fortschrittsanzeige/Reihenfolge der Einheit durcheinanderbringen (siehe TODO.md, Nachtrag zu
/// 4.3). Sondereinsatz und Ausfall sind bewusst nie als Einheiten-Stunde übernehmbar — ein
/// Ausflug ist inhaltlich kein Verlaufsplan-Eintrag, und ein Ausfall ist per Definition keine
/// gehaltene Stunde.
/// </summary>
public partial class SubstitutionEntryDialogViewModel : ObservableObject
{
private readonly ISubstitutionEntryRepository _substitutions;
private readonly IUnitRepository _units;
private readonly ILessonRepository _lessons;
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
[ObservableProperty] private string _kindName = SubstitutionKindDisplay.Options[0];
[ObservableProperty] private int _afterPeriod;
[ObservableProperty] private int _periodNumber = 1;
[ObservableProperty] private bool _isAllDay = true;
[ObservableProperty] private int _fromPeriod = 1;
[ObservableProperty] private int _toPeriod = 10;
[ObservableProperty] private LearningGroup? _selectedOwnGroup;
[ObservableProperty] private string _groupLabel = "";
[ObservableProperty] private string _description = "";
[ObservableProperty] private bool _promoteToLesson;
[ObservableProperty] private Unit? _selectedUnit;
[ObservableProperty] private string _dateError = "";
[ObservableProperty] private string _validationError = "";
public string[] KindOptions => SubstitutionKindDisplay.Options;
public bool IsSupervisionKind => KindName == "Aufsicht";
public bool IsLessonKind => KindName == "Stunde";
public bool IsSpecialAssignmentKind => KindName == "Sondereinsatz";
public bool IsCancelledKind => KindName == "Ausfall";
public bool CanPromoteToLesson => IsLessonKind && SelectedOwnGroup is not null && UnitsOfSelectedGroup.Count > 0;
public ObservableCollection<LearningGroup> OwnGroups { get; } = [];
public ObservableCollection<Unit> UnitsOfSelectedGroup { get; } = [];
public SubstitutionEntry? Result { get; private set; }
public SubstitutionEntryDialogViewModel(ISubstitutionEntryRepository substitutions, IGroupRepository groups,
IUnitRepository units, ILessonRepository lessons)
{
_substitutions = substitutions; _units = units; _lessons = lessons;
foreach (var g in groups.GetAll().OrderBy(g => g.Name)) OwnGroups.Add(g);
}
partial void OnKindNameChanged(string value)
{
OnPropertyChanged(nameof(IsSupervisionKind));
OnPropertyChanged(nameof(IsLessonKind));
OnPropertyChanged(nameof(IsSpecialAssignmentKind));
OnPropertyChanged(nameof(IsCancelledKind));
OnPropertyChanged(nameof(CanPromoteToLesson));
}
partial void OnSelectedOwnGroupChanged(LearningGroup? value)
{
UnitsOfSelectedGroup.Clear();
if (value is not null)
{
GroupLabel = value.Name;
foreach (var u in _units.GetByGroup(value.Id)) UnitsOfSelectedGroup.Add(u);
}
SelectedUnit = UnitsOfSelectedGroup.FirstOrDefault();
PromoteToLesson = false;
OnPropertyChanged(nameof(CanPromoteToLesson));
}
[RelayCommand]
private void Save()
{
DateError = ""; ValidationError = "";
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
{ DateError = "Format TT.MM.JJJJ."; return; }
var kind = SubstitutionKindDisplay.FromName(KindName);
var entry = new SubstitutionEntry { Date = date, Kind = kind };
switch (kind)
{
case SubstitutionKind.Supervision:
if (AfterPeriod is < 0 or > 10) { ValidationError = "Pause muss zwischen 0 und 10 liegen."; return; }
if (string.IsNullOrWhiteSpace(Description)) { ValidationError = "Grund/Ort erforderlich."; return; }
entry.AfterPeriod = AfterPeriod;
entry.Description = Description.Trim();
break;
case SubstitutionKind.Lesson:
if (PeriodNumber is < 1 or > 10) { ValidationError = "Stunde muss zwischen 1 und 10 liegen."; return; }
if (string.IsNullOrWhiteSpace(GroupLabel)) { ValidationError = "Gruppe/Bezeichnung erforderlich."; return; }
if (string.IsNullOrWhiteSpace(Description)) { ValidationError = "Thema erforderlich."; return; }
entry.PeriodNumber = PeriodNumber;
entry.GroupId = SelectedOwnGroup?.Id;
entry.GroupLabel = GroupLabel.Trim();
entry.Description = Description.Trim();
break;
case SubstitutionKind.SpecialAssignment:
if (string.IsNullOrWhiteSpace(Description)) { ValidationError = "Bezeichnung erforderlich."; return; }
if (!IsAllDay)
{
if (FromPeriod is < 1 or > 10 || ToPeriod is < 1 or > 10)
{ ValidationError = "Stunden müssen zwischen 1 und 10 liegen."; return; }
if (ToPeriod < FromPeriod)
{ ValidationError = "Die Bis-Stunde darf nicht vor der Von-Stunde liegen."; return; }
entry.FromPeriod = FromPeriod;
entry.ToPeriod = ToPeriod;
}
entry.IsAllDay = IsAllDay;
entry.GroupId = SelectedOwnGroup?.Id;
entry.GroupLabel = GroupLabel.Trim();
entry.Description = Description.Trim();
break;
case SubstitutionKind.Cancelled:
if (PeriodNumber is < 1 or > 10) { ValidationError = "Stunde muss zwischen 1 und 10 liegen."; return; }
entry.PeriodNumber = PeriodNumber;
entry.Description = Description.Trim();
break;
}
_substitutions.Save(entry);
if (kind == SubstitutionKind.Lesson && PromoteToLesson && SelectedOwnGroup is not null && SelectedUnit is not null)
{
_lessons.Save(new Lesson
{
UnitId = SelectedUnit.Id,
GroupId = SelectedOwnGroup.Id,
Date = date,
LessonNumber = PeriodNumber,
Topic = Description.Trim(),
});
}
Result = entry;
}
}