feat: Stundenplan-Badge für unkontrollierte Hausaufgaben (4.5.4)

Lesson bekommt HomeworkChecked/HomeworkCheckDismissed. Der Stundenplan
(Tagesliste und Wochenraster im "Heute"-Tab) zeigt ein 📓-Badge, wenn die
letzte Stunde einer Gruppe eine Hausaufgabe hatte, die noch nicht
kontrolliert oder bewusst übersprungen wurde. Umschaltbar über zwei sich
gegenseitig ausschließende Checkboxen ("Kontrolliert"/"Nicht
kontrollieren") im Verlaufsplan-Editor, direkt neben dem
Hausaufgabenfeld.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 20:52:22 +02:00
co-authored by Claude Sonnet 5
parent 462080ab92
commit 41bf2c0756
8 changed files with 215 additions and 10 deletions
@@ -635,6 +635,8 @@ public partial class LessonDialogViewModel : ObservableObject
[ObservableProperty] private string _topic = "";
[ObservableProperty] private string _startTimeText = "";
[ObservableProperty] private string _homework = "";
[ObservableProperty] private bool _homeworkChecked;
[ObservableProperty] private bool _homeworkCheckDismissed;
[ObservableProperty] private string _reflection = "";
[ObservableProperty] private string _statusName = LessonStatusDisplay.Options[0];
[ObservableProperty] private string _dateTextError = "";
@@ -646,6 +648,9 @@ public partial class LessonDialogViewModel : ObservableObject
[ObservableProperty] private bool _hasTimeBudgetInfo;
public string[] StatusOptions => LessonStatusDisplay.Options;
/// Nur bei vorhandenem Hausaufgabentext sinnvoll — steuert die Sichtbarkeit von
/// HomeworkChecked/HomeworkCheckDismissed im Dialog (4.5.4).
public bool HasHomeworkText => !string.IsNullOrWhiteSpace(Homework);
public string[] MaterialSuggestions { get; }
public string[] ShorthandSuggestions { get; }
public ObservableCollection<PhaseStepEditItem> Phases { get; } = [];
@@ -707,6 +712,8 @@ public partial class LessonDialogViewModel : ObservableObject
Topic = editingLesson.Topic;
StartTimeText = editingLesson.StartTime?.ToString("HH:mm") ?? "";
Homework = editingLesson.Homework ?? "";
HomeworkChecked = editingLesson.HomeworkChecked;
HomeworkCheckDismissed = editingLesson.HomeworkCheckDismissed;
Reflection = editingLesson.Reflection ?? "";
StatusName = LessonStatusDisplay.ToName(editingLesson.Status);
foreach (var p in editingLesson.Phases) AddPhaseInternal(p);
@@ -803,6 +810,11 @@ public partial class LessonDialogViewModel : ObservableObject
RecomputeTimes();
}
partial void OnDateTextChanged(string value) => RecomputeTimes();
partial void OnHomeworkChanged(string value) => OnPropertyChanged(nameof(HasHomeworkText));
// "Kontrolliert" und "bewusst nicht kontrollieren" schließen sich gegenseitig aus — beides sind
// "erledigt"-Zustände mit unterschiedlicher Bedeutung, nie gleichzeitig sinnvoll.
partial void OnHomeworkCheckedChanged(bool value) { if (value) HomeworkCheckDismissed = false; }
partial void OnHomeworkCheckDismissedChanged(bool value) { if (value) HomeworkChecked = false; }
/// Dauer ist die primäre Eingabe je Phase; die Uhrzeit wird daraus nur zur Anzeige
/// abgeleitet — kumulativ ab "Beginn", sofern gesetzt (sonst bleibt sie leer).
@@ -907,6 +919,8 @@ public partial class LessonDialogViewModel : ObservableObject
Result.StartTime = startTime;
Result.Phases = Phases.Select(p => p.ToModel()).ToList();
Result.Homework = string.IsNullOrWhiteSpace(Homework) ? null : Homework.Trim();
Result.HomeworkChecked = HomeworkChecked;
Result.HomeworkCheckDismissed = HomeworkCheckDismissed;
Result.Reflection = string.IsNullOrWhiteSpace(Reflection) ? null : Reflection.Trim();
Result.Status = LessonStatusDisplay.FromName(StatusName);
_lessons.Save(Result);
@@ -197,7 +197,8 @@ public partial class TimetableViewModel : ObservableObject
var lesson = _lessons.GetByGroupAndDate(slot.GroupId, today).FirstOrDefault();
var exam = _exams.GetByGroup(slot.GroupId).FirstOrDefault(e => e.Date == today);
items.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title));
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title,
HasUnhandledHomework(slot.GroupId, today)));
}
foreach (var sub in substitutionsToday.Where(s => s.Kind == SubstitutionKind.Lesson &&
@@ -331,7 +332,8 @@ public partial class TimetableViewModel : ObservableObject
subject?.ShortName is { Length: > 0 } sn ? sn : subject?.Name ?? "",
group?.Name ?? "?", slot.Room ?? "", lesson?.Topic ?? "",
colorHex, holidayBadge, hasExam, isLastBeforeExam,
MentionsExperiment(lesson), slot.GroupId, isHoliday));
MentionsExperiment(lesson), slot.GroupId, isHoliday,
HasUnhandledHomework(slot.GroupId, date)));
}
AddWeekSupervisionRowIfAny(period, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
}
@@ -392,6 +394,21 @@ public partial class TimetableViewModel : ObservableObject
p.Activity?.Contains("Experiment", StringComparison.OrdinalIgnoreCase) == true ||
p.Material?.Contains("Experiment", StringComparison.OrdinalIgnoreCase) == true)) == true;
/// <summary>4.5.4: Hat die letzte vor <paramref name="date"/> liegende Lesson dieser Gruppe eine
/// Hausaufgabe, die weder als kontrolliert noch als bewusst übersprungen markiert ist? Schaut
/// bewusst nur auf die unmittelbar vorherige Lesson (nicht auf die gesamte Historie) — sobald
/// eine neuere Lesson stattfindet, ist eine noch ältere offene Hausaufgabe nicht mehr das, worauf
/// sich "letzte Stunde" bezieht. Lookback-Fenster von 120 Tagen deckt auch längere Ferienpausen
/// ab, ohne unbegrenzt weit zurückzuscannen.</summary>
private bool HasUnhandledHomework(Guid groupId, DateOnly date)
{
var previousLesson = _lessons.GetByGroupAndRange(groupId, date.AddDays(-120), date.AddDays(-1))
.OrderByDescending(l => l.Date).ThenByDescending(l => l.LessonNumber ?? 0)
.FirstOrDefault();
if (previousLesson is null || string.IsNullOrWhiteSpace(previousLesson.Homework)) return false;
return !previousLesson.HomeworkChecked && !previousLesson.HomeworkCheckDismissed;
}
/// <summary>Fällt <paramref name="date"/> auf einen gesetzlichen Feiertag oder in Schulferien?</summary>
private static bool IsFreeDay(DateOnly date, List<SchoolHoliday> schoolHolidays, HashSet<DateOnly> publicHolidayDates) =>
publicHolidayDates.Contains(date) || schoolHolidays.Any(h => date >= h.StartDate && date <= h.EndDate);
@@ -629,6 +646,7 @@ public class WeekCellItem
public bool HasExam { get; private init; }
public bool IsLastBeforeExam { get; private init; }
public bool HasExperiment { get; private init; }
public bool HasUnhandledHomework { get; private init; }
public bool IsHoliday { get; private init; }
public string SupervisionLocation { get; private init; } = "";
public bool HasSupervision => SupervisionLocation.Length > 0;
@@ -667,13 +685,14 @@ public class WeekCellItem
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()
bool hasExam, bool isLastBeforeExam, bool hasExperiment, Guid groupId, bool isHoliday,
bool hasUnhandledHomework = false) => new()
{
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday,
SubjectLabel = subjectLabel, GroupName = groupName, Room = room, Topic = topic,
ColorHex = colorHex, HolidayBadge = holidayBadge, HasExam = hasExam,
IsLastBeforeExam = isLastBeforeExam, HasExperiment = hasExperiment, GroupId = groupId,
IsHoliday = isHoliday,
IsHoliday = isHoliday, HasUnhandledHomework = hasUnhandledHomework,
};
public static WeekCellItem ForSubstitutionLesson(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry) => new()
@@ -737,12 +756,14 @@ public class TodayLessonItem
public bool HasLessonTopic => !string.IsNullOrWhiteSpace(LessonTopic);
public bool HasExam => ExamTitle is not null;
public bool HasGroupId => GroupId != Guid.Empty;
public bool HasUnhandledHomework { get; private init; }
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
string colorHex, string? lessonTopic, string? examTitle)
string colorHex, string? lessonTopic, string? examTitle, bool hasUnhandledHomework = false)
{
GroupId = groupId; PeriodNumber = periodNumber; GroupName = groupName; Room = room;
ColorHex = colorHex; LessonTopic = lessonTopic; ExamTitle = examTitle;
HasUnhandledHomework = hasUnhandledHomework;
}
public static TodayLessonItem ForSubstitution(int periodNumber, SubstitutionEntry entry) => new(