Kurs-Übersicht: fehlende Hausaufgaben; Erinnerungen als eigene Aufgabenart; Kalender-/Sitzplan-Bugfixes

Kurs-Übersicht: neue Karte "Fehlende Hausaufgaben" listet Schüler mit nicht
gemachter Hausaufgabe aus der letzten Mitarbeitssitzung.

Aufgabenverwaltung: WorkTask.Kind unterscheidet jetzt Arbeitsaufträge von
reinen Erinnerungen ohne Zeitbezug (z.B. "morgen Ansage an die Klasse
machen") - eigener Button "🔔 Erinnerung", eigenes Icon in Liste/Dashboard,
läuft nicht in die Zeitauswertung ein.

Drei Bugfixes aus Nutzer-Feedback:
- Dashboard-Kalender zeigte eine Stunde doppelt, wenn sie über "Sitzung
  erzeugen" mit einer Mitarbeitssitzung verknüpft war.
- Der Sitzplan-Tab legte beim bloßen Öffnen einer Gruppe (nicht erst bei
  echter Nutzung) eine leere "Sitzplan"-Sitzung für heute an.
- Der "Meine Klasse"-Ring im Kalender erschien erst, sobald für den Tag
  eine Lesson existierte, statt schon laut Stundenplan.

Außerdem: Drawer-Icons pinnen jetzt explizit auf die farbige Emoji-Schrift
(Windows kann sonst je nach Font-Fallback auf eine einfarbige Variante
ausweichen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 23:17:18 +02:00
co-authored by Claude Sonnet 5
parent 4d2a89b14b
commit 52a1d45539
16 changed files with 505 additions and 57 deletions
@@ -42,6 +42,11 @@ public partial class DashboardViewModel : ObservableObject
/// genug, dass die Erinnerung nicht zu einer ignorierbaren Dauerliste wird, aber früh genug,
/// um sich abends noch vorzubereiten.
private const int UnplannedLessonsLookaheadDays = 1;
/// Nutzer-Feedback: unterhalb dieser Anzahl erfasster Anwesenheits-Einträge im laufenden
/// Schuljahr wird eine hohe Fehlquote NICHT gemeldet — wer zu Schuljahresbeginn zweimal fehlt,
/// hat rein rechnerisch schon 100 %, das ist noch kein auffälliges Muster, nur eine zu kleine
/// Stichprobe. Dieselbe Konstante wie GroupOverviewViewModel.AttendanceMinSampleSize.
private const int AttendanceMinSampleSize = 8;
[ObservableProperty] private string _greeting = "";
[ObservableProperty] private string _currentDate = "";
@@ -156,7 +161,8 @@ public partial class DashboardViewModel : ObservableObject
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(5))
OpenTasks.Add(new() { Title = t.Title,
DueDate = t.DueDate?.ToString("dd.MM.") ?? "",
IsOverdue = t.DueDate.HasValue && t.DueDate < today });
IsOverdue = t.DueDate.HasValue && t.DueDate < today,
IsReminder = t.Kind == TaskKind.Reminder });
CurrentGroups.Clear();
foreach (var g in groups.Values.OrderBy(g => g.Name))
@@ -197,7 +203,7 @@ public partial class DashboardViewModel : ObservableObject
.Select(t => (t.Item1!.Value, t.Attendance));
var balance = _attendanceBalance.Calculate(entries, from, to);
if (balance.ExceedsThreshold)
if (balance.TotalChecked >= AttendanceMinSampleSize && balance.ExceedsThreshold)
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
}
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
@@ -555,8 +561,12 @@ public partial class DashboardViewModel : ObservableObject
exam.Title, g.Name, g.Id));
}
// Sitzungen, die über "Sitzung erzeugen" (3.3.1) aus einer Stunde entstanden sind
// (LessonId gesetzt), bekommen bewusst keinen eigenen Kalendereintrag — die Stunde
// selbst ist an diesem Tag/dieser Gruppe schon als Lesson-Termin gelistet, ein
// zweiter Eintrag für dieselbe Unterrichtsstunde wäre eine Dopplung.
foreach (var session in _participationSessions.GetByGroup(g.Id)
.Where(s => s.Date >= gridStart && s.Date <= gridEnd))
.Where(s => s.LessonId is null && s.Date >= gridStart && s.Date <= gridEnd))
{
var agg = Agg(session.Date);
agg.HasSession = true;
@@ -566,6 +576,35 @@ public partial class DashboardViewModel : ObservableObject
}
}
// "Meine Klasse"-Ring: bisher nur gesetzt, wenn für den Tag schon eine Lesson/Exam/Sitzung
// existiert — ein Tag, an dem laut Stundenplan (4.3) eine eigene Klasse ansteht, für den
// aber noch keine Lesson angelegt wurde (z.B. "morgen"), zeigte den Ring fälschlich nicht.
// Dieselbe Projektion wie bei den ungeplanten Stunden (LoadUnplannedLessons), nur über den
// gesamten Kalenderraster statt nur die nächsten Tage.
var publicHolidayDatesForGrid = Enumerable.Range(gridStart.Year, gridEnd.Year - gridStart.Year + 1)
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
.Select(h => h.Date).ToHashSet();
var schoolHolidaysForGrid = _schoolHolidays.GetAll();
foreach (var g in groups.Where(g => g.IsOwnClass))
{
var slots = _timetableSlots.GetByGroup(g.Id);
if (slots.Count == 0) continue;
for (var date = gridStart; date <= gridEnd; date = date.AddDays(1))
{
if (IsFreeDay(date, schoolHolidaysForGrid, publicHolidayDatesForGrid)) continue;
var daySlots = slots.Where(s => s.Weekday == date.DayOfWeek).ToList();
if (daySlots.Count == 0) continue;
var cancelledPeriods = _substitutions.GetByDate(date)
.Where(s => s.Kind == SubstitutionKind.Cancelled)
.Select(s => s.PeriodNumber).ToHashSet();
if (daySlots.All(s => cancelledPeriods.Contains(s.PeriodNumber))) continue;
Agg(date).IsOwnClassDay = true;
}
}
for (var i = 0; i < 42; i++)
{
var date = gridStart.AddDays(i);
@@ -659,7 +698,7 @@ public class LessonItem
public string Room { get; set; } = "";
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
}
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } }
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } }
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; }
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────