Sitzungsfreigabe: McpConfirmDialog bietet zwei Checkboxen ("diese Art
von Aktion" / "alle KI-Aktionen für den Rest der Sitzung nicht mehr
nachfragen"), nur beim Bestätigen wirksam. IMcpConfirmationService.
ConfirmAsync bekommt einen per [CallerMemberName] automatisch
befüllten operationKey - kein bestehender Aufruf musste geändert
werden. Freigaben leben als In-Memory-Bookkeeping auf der
Confirmation-Service-Instanz, gelten bis App-Ende, werden aber trotzdem
geloggt, damit stillschweigende Bestätigungen nicht spurlos bleiben.
Grund: 17 identische Bestätigungen in Folge für eine neu generierte
Unterrichtseinheit sind unzumutbar.
LessonStatus.Cancelled ("Ausgefallen") als Alternative zu
delete_lesson für Stunden, die nur ausgefallen sind (Exkursion,
Feiertag), aber als Ereignis dokumentiert bleiben sollen. Als
Cancelled=4 angehängt (bestehende LiteDB-Werte 0/1 sind fix). Konsistent
wie "bereits durchgeführt" behandelt an allen Stellen, die bisher nur
auf Conducted prüften: Terminverschiebung, Fortschrittsanzeige,
Hausaufgabe-kontrollieren-Erinnerung, "nächste Stunde"-Vorschlag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
3.9 KiB
C#
100 lines
3.9 KiB
C#
using LehrerApp.Core.Interfaces;
|
||
using LehrerApp.Core.Models;
|
||
|
||
namespace LehrerApp.Core.Services;
|
||
|
||
/// <summary>Gemeinsame Terminlogik für Verschieben und Trennen von Stunden.</summary>
|
||
public sealed class LessonSchedulingService(ILessonRepository lessons)
|
||
{
|
||
public void Move(Lesson lesson, DateOnly newDate, int? newPeriod, bool shiftFollowing)
|
||
{
|
||
EnsureTargetIsFree(lesson, newDate, newPeriod);
|
||
var oldDate = lesson.Date;
|
||
var delta = newDate.DayNumber - oldDate.DayNumber;
|
||
|
||
if (shiftFollowing && delta != 0)
|
||
{
|
||
foreach (var other in lessons.GetByUnit(lesson.UnitId))
|
||
{
|
||
if (other.Id == lesson.Id || other.Status is LessonStatus.Conducted or LessonStatus.Cancelled || other.Date <= oldDate)
|
||
continue;
|
||
other.Date = other.Date.AddDays(delta);
|
||
lessons.Save(other);
|
||
}
|
||
}
|
||
|
||
lesson.Date = newDate;
|
||
if (newPeriod.HasValue) lesson.LessonNumber = newPeriod;
|
||
lessons.Save(lesson);
|
||
}
|
||
|
||
public Lesson SplitAndMoveSecondPart(Lesson source, int splitAfterMinutes, DateOnly newDate,
|
||
int newPeriod, TimeOnly? newStartTime)
|
||
{
|
||
if (splitAfterMinutes <= 0 || source.Phases.Sum(p => p.DurationMinutes) <= splitAfterMinutes)
|
||
throw new InvalidOperationException("Der Verlauf reicht nicht über die erste Stunde hinaus.");
|
||
EnsureTargetIsFree(source, newDate, newPeriod);
|
||
|
||
var first = new List<LessonPhaseStep>();
|
||
var second = new List<LessonPhaseStep>();
|
||
var elapsed = 0;
|
||
foreach (var phase in source.Phases)
|
||
{
|
||
var remainingInFirst = splitAfterMinutes - elapsed;
|
||
if (remainingInFirst <= 0)
|
||
second.Add(Clone(phase, phase.DurationMinutes));
|
||
else if (phase.DurationMinutes <= remainingInFirst)
|
||
first.Add(Clone(phase, phase.DurationMinutes));
|
||
else
|
||
{
|
||
first.Add(Clone(phase, remainingInFirst));
|
||
second.Add(Clone(phase, phase.DurationMinutes - remainingInFirst));
|
||
}
|
||
elapsed += phase.DurationMinutes;
|
||
}
|
||
|
||
source.Phases = first;
|
||
var continuation = new Lesson
|
||
{
|
||
UnitId = source.UnitId,
|
||
GroupId = source.GroupId,
|
||
Date = newDate,
|
||
LessonNumber = newPeriod,
|
||
StartTime = newStartTime,
|
||
Topic = string.IsNullOrWhiteSpace(source.Topic) ? "Fortsetzung" : $"{source.Topic} – Fortsetzung",
|
||
Phases = second,
|
||
Homework = source.Homework,
|
||
HomeworkChecked = source.HomeworkChecked,
|
||
HomeworkCheckDismissed = source.HomeworkCheckDismissed,
|
||
Reflection = source.Reflection,
|
||
Status = source.Status is LessonStatus.Conducted or LessonStatus.Cancelled ? LessonStatus.Draft : source.Status,
|
||
};
|
||
source.Homework = null;
|
||
source.HomeworkChecked = false;
|
||
source.HomeworkCheckDismissed = false;
|
||
source.Reflection = null;
|
||
lessons.Save(source);
|
||
lessons.Save(continuation);
|
||
return continuation;
|
||
}
|
||
|
||
private void EnsureTargetIsFree(Lesson source, DateOnly date, int? period)
|
||
{
|
||
if (period is null) return;
|
||
var occupied = lessons.GetByGroupAndDate(source.GroupId, date)
|
||
.Any(l => l.Id != source.Id && l.LessonNumber == period);
|
||
if (occupied)
|
||
throw new InvalidOperationException($"Für die Lerngruppe existiert am {date:dd.MM.yyyy} in der {period}. Stunde bereits eine Planung.");
|
||
}
|
||
|
||
private static LessonPhaseStep Clone(LessonPhaseStep source, int duration) => new()
|
||
{
|
||
Name = source.Name,
|
||
DurationMinutes = duration,
|
||
Activity = source.Activity,
|
||
Material = source.Material,
|
||
Shorthand = source.Shorthand,
|
||
AlternativePathId = source.AlternativePathId,
|
||
};
|
||
}
|