Stundenplan verbesserung
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-08-30 00:53:32 +02:00
parent 2b299fc940
commit dade41ee8d
16 changed files with 639 additions and 75 deletions
@@ -13,7 +13,8 @@ namespace LehrerApp.Desktop.ViewModels.Groups;
// ── Ergebnisse der Verschieben-/Kopieren-/Serienerzeugungs-Dialoge (4.2.4 / 4.1.4 / 4.2.5) ───
public record MoveLessonTarget(DateOnly NewDate, bool ShiftFollowing);
public record MoveLessonTarget(DateOnly NewDate, bool ShiftFollowing, int? NewPeriod = null,
bool SplitDoubleLesson = false);
public record CopyUnitTarget(Guid TargetGroupId, DateOnly AnchorDate);
public record LessonSeriesResult(int Created, int SkippedHoliday, int SkippedExisting)
@@ -78,6 +79,7 @@ public partial class PlanningTabViewModel : ObservableObject
public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; }
public Func<Unit, Task<bool>>? OnAiAssist { get; set; }
public Action<string>? OnNotify { get; set; }
public Action<string>? OnError { get; set; }
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
IGroupRepository groups, ISubjectRepository subjects,
@@ -333,7 +335,8 @@ public partial class PlanningTabViewModel : ObservableObject
var lesson = SelectedLesson.Model;
var target = await OnPickMoveTarget(lesson);
if (target is null) return;
MoveLessonInternal(lesson, target.NewDate, target.ShiftFollowing);
try { MoveLessonInternal(lesson, target.NewDate, target.ShiftFollowing, target.NewPeriod); }
catch (InvalidOperationException ex) { OnError?.Invoke(ex.Message); return; }
LoadUnits();
}
@@ -341,33 +344,19 @@ public partial class PlanningTabViewModel : ObservableObject
/// sich alle anderen noch geplanten Stunden derselben Einheit, die ursprünglich NACH der
/// verschobenen Stunde lagen, um denselben Tages-Delta. Bereits durchgeführte Stunden werden
/// nie angefasst — nur Date ändert sich, UnitId/GroupId bleiben unverändert.
private void MoveLessonInternal(Lesson moved, DateOnly newDate, bool shiftFollowing)
private void MoveLessonInternal(Lesson moved, DateOnly newDate, bool shiftFollowing, int? newPeriod = null)
{
var oldDate = moved.Date;
var delta = newDate.DayNumber - oldDate.DayNumber;
if (shiftFollowing && delta != 0)
{
foreach (var other in _lessons.GetByUnit(moved.UnitId))
{
if (other.Id == moved.Id) continue;
if (other.Status != LessonStatus.Planned) continue;
if (other.Date <= oldDate) continue;
other.Date = other.Date.AddDays(delta);
_lessons.Save(other);
}
}
moved.Date = newDate;
_lessons.Save(moved);
new LessonSchedulingService(_lessons).Move(moved, newDate, newPeriod, shiftFollowing);
}
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
private void AdvanceLessonStatus()
{
if (SelectedLesson is null || SelectedLesson.Model.Status != LessonStatus.Planned) return;
if (SelectedLesson is null || SelectedLesson.Model.Status == LessonStatus.Conducted) return;
var lesson = SelectedLesson.Model;
lesson.Status = LessonStatus.Conducted;
lesson.Status = lesson.Status == LessonStatus.Ready
? LessonStatus.Conducted
: LessonStatus.Ready;
_lessons.Save(lesson);
LoadUnits();
}
@@ -479,8 +468,14 @@ public class LessonSummary
Topic = l.Topic;
StartTimeDisplay = l.StartTime?.ToString("HH:mm") ?? "";
Status = l.Status;
StatusLabel = l.Status == LessonStatus.Conducted ? "Durchgeführt" : "Geplant";
StatusColorHex = l.Status == LessonStatus.Conducted ? "#43A047" : "#9E9E9E";
StatusLabel = LessonStatusDisplay.ToName(l.Status);
StatusColorHex = l.Status switch
{
LessonStatus.Draft => "#78909C",
LessonStatus.Ready => "#1976D2",
LessonStatus.Conducted => "#43A047",
_ => "#9E9E9E",
};
PhaseCountLabel = l.Phases.Count == 0 ? "" : $"{l.Phases.Count} Phasen";
var totalMinutes = l.Phases.Sum(p => p.DurationMinutes);
TotalDurationLabel = totalMinutes == 0 ? "" : $"{totalMinutes} Min.";
@@ -515,12 +510,23 @@ public static class UnitStatusDisplay
public static class LessonStatusDisplay
{
public static string[] Options { get; } = ["Geplant", "Durchgeführt"];
public static string[] Options { get; } = ["Entwurf", "Geplant", "Bereit", "Durchgeführt"];
public static string ToName(LessonStatus s) => s == LessonStatus.Conducted ? "Durchgeführt" : "Geplant";
public static string ToName(LessonStatus s) => s switch
{
LessonStatus.Draft => "Entwurf",
LessonStatus.Ready => "Bereit",
LessonStatus.Conducted => "Durchgeführt",
_ => "Geplant",
};
public static LessonStatus FromName(string? name) =>
name == "Durchgeführt" ? LessonStatus.Conducted : LessonStatus.Planned;
public static LessonStatus FromName(string? name) => name switch
{
"Entwurf" => LessonStatus.Draft,
"Bereit" => LessonStatus.Ready,
"Durchgeführt" => LessonStatus.Conducted,
_ => LessonStatus.Planned,
};
}
// ── Dialog: Einheit anlegen / bearbeiten (4.1.2 / 4.1.3) ─────────────────────
@@ -722,7 +728,8 @@ public partial class LessonDialogViewModel : ObservableObject
IAlternativeLessonPathRepository alternativePaths, ITimetableSlotRepository timetableSlots,
PeriodScheduleService periodSchedule, IAttachmentStorage attachmentStorage,
Guid unitId, Guid groupId, string groupName, string subjectName,
List<string> materialSuggestions, List<string> shorthandHistorySuggestions, Lesson? editingLesson)
List<string> materialSuggestions, List<string> shorthandHistorySuggestions, Lesson? editingLesson,
DateOnly? suggestedDate = null, int? suggestedPeriod = null)
{
_lessons = lessons; _alternativePaths = alternativePaths;
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
@@ -760,9 +767,12 @@ public partial class LessonDialogViewModel : ObservableObject
}
else
{
var (date, lessonNumber) = SuggestNextLesson();
var (date, lessonNumber) = suggestedDate.HasValue
? (suggestedDate.Value, suggestedPeriod)
: SuggestNextLesson();
DateText = date.ToString("dd.MM.yyyy");
LessonNumber = lessonNumber;
StatusName = LessonStatusDisplay.ToName(LessonStatus.Draft);
}
RecomputeTimes();
}
@@ -1145,28 +1155,45 @@ public partial class AlternativePathDialogViewModel : ObservableObject
public partial class MoveLessonDialogViewModel : ObservableObject
{
[ObservableProperty] private string _newDateText;
[ObservableProperty] private int? _newPeriod;
[ObservableProperty] private bool _shiftFollowingPlanned = true;
[ObservableProperty] private bool _splitDoubleLesson;
[ObservableProperty] private string _newDateTextError = "";
[ObservableProperty] private string _newPeriodError = "";
public string CurrentDateDisplay { get; }
public string CurrentPeriodDisplay { get; }
public bool CanSplitDoubleLesson { get; }
public MoveLessonTarget? Result { get; private set; }
public MoveLessonDialogViewModel(DateOnly currentDate)
public MoveLessonDialogViewModel(DateOnly currentDate, int? currentPeriod = null,
bool canSplitDoubleLesson = false, bool splitByDefault = false)
{
CurrentDateDisplay = currentDate.ToString("dd.MM.yyyy");
CurrentPeriodDisplay = currentPeriod is null ? "nicht festgelegt" : $"{currentPeriod}. Stunde";
_newDateText = currentDate.ToString("dd.MM.yyyy");
_newPeriod = currentPeriod;
CanSplitDoubleLesson = canSplitDoubleLesson;
_splitDoubleLesson = canSplitDoubleLesson && splitByDefault;
}
[RelayCommand]
private void Save()
{
NewDateTextError = "";
NewPeriodError = "";
if (!DateOnly.TryParseExact(NewDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
{
NewDateTextError = "Format TT.MM.JJJJ.";
return;
}
Result = new MoveLessonTarget(date, ShiftFollowingPlanned);
if (NewPeriod is < 1 or > 20)
{
NewPeriodError = "Bitte eine Stundennummer zwischen 1 und 20 wählen.";
return;
}
Result = new MoveLessonTarget(date, ShiftFollowingPlanned, NewPeriod,
CanSplitDoubleLesson && SplitDoubleLesson);
}
}