Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
455c61c946 | ||
|
|
6af4bee1f0 | ||
|
|
c3ce1a7204 |
@@ -12,10 +12,13 @@ public sealed class DashboardCardSetting
|
|||||||
/// <summary>Speichert Sichtbarkeit und Reihenfolge der Dashboard-Kacheln lokal.</summary>
|
/// <summary>Speichert Sichtbarkeit und Reihenfolge der Dashboard-Kacheln lokal.</summary>
|
||||||
public sealed class DashboardSettingsService
|
public sealed class DashboardSettingsService
|
||||||
{
|
{
|
||||||
|
// "attention" fasst die frueheren sieben Kacheln missingteachingtime/excuses/corrections/
|
||||||
|
// unplanned/alerts/attendance/support zusammen (siehe AttentionItem.cs im Desktop-Projekt).
|
||||||
|
// Alte gespeicherte dashboardsettings.json-Dateien mit den frueheren Keys sind unproblematisch:
|
||||||
|
// Load() unten verwirft unbekannte Keys ohnehin stillschweigend und ergaenzt neue als sichtbar.
|
||||||
public static readonly string[] DefaultCardOrder =
|
public static readonly string[] DefaultCardOrder =
|
||||||
[
|
[
|
||||||
"today", "tasks", "missingteachingtime", "calendar", "excuses", "upcoming",
|
"today", "tasks", "attention", "calendar", "upcoming", "groups", "examload",
|
||||||
"corrections", "unplanned", "alerts", "attendance", "support", "groups", "examload",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly string _configPath;
|
private readonly string _configPath;
|
||||||
|
|||||||
@@ -20,14 +20,75 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
||||||
|
|
||||||
Assert.False(vm.ExcusesCard.EffectiveIsVisible);
|
Assert.False(vm.AttentionCard.EffectiveIsVisible);
|
||||||
Assert.False(vm.CorrectionsCard.EffectiveIsVisible);
|
|
||||||
Assert.False(vm.AlertsCard.EffectiveIsVisible);
|
|
||||||
Assert.Equal("0 offene Punkte", vm.AttentionSummary);
|
Assert.Equal("0 offene Punkte", vm.AttentionSummary);
|
||||||
Assert.True(vm.TodayCard.EffectiveIsVisible);
|
Assert.True(vm.TodayCard.EffectiveIsVisible);
|
||||||
Assert.True(vm.CalendarCard.EffectiveIsVisible);
|
Assert.True(vm.CalendarCard.EffectiveIsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Regression: ApplyCardLayout zaehlte frueher IsVisible statt EffectiveIsVisible. Eine
|
||||||
|
/// eingeschaltete, aber leere HideWhenEmpty-Kachel belegte damit einen Rasterplatz, den das
|
||||||
|
/// Grid nie fuellt — im Alltag der Normalfall, weil meist mehrere Hinweiskacheln leer sind.
|
||||||
|
[Fact]
|
||||||
|
public void Kachelraster_LeereAusgeblendeteKacheln_HinterlassenKeineLuecke()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
||||||
|
|
||||||
|
// Ohne mindestens eine leer ausgeblendete Kachel wuerde der Test nichts pruefen.
|
||||||
|
Assert.False(vm.AttentionCard.EffectiveIsVisible);
|
||||||
|
|
||||||
|
var belegtePlaetze = vm.DashboardCards.Where(c => c.EffectiveIsVisible)
|
||||||
|
.Select(c => c.Row * 2 + c.Column).OrderBy(slot => slot).ToList();
|
||||||
|
Assert.Equal(Enumerable.Range(0, belegtePlaetze.Count), belegtePlaetze);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KachelMargin_FolgtDerBerechnetenSpalte()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
||||||
|
|
||||||
|
foreach (var card in vm.DashboardCards.Where(c => c.EffectiveIsVisible))
|
||||||
|
Assert.Equal(card.Column == 0
|
||||||
|
? new Avalonia.Thickness(0, 0, 8, 8)
|
||||||
|
: new Avalonia.Thickness(8, 0, 0, 8), card.Margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression: der Margin hing fest im XAML an der Kachel. Wandert sie durch Aus-/Einblenden
|
||||||
|
/// einer vorherigen Kachel in die andere Spalte, sass der Rinnstein auf der falschen Seite.
|
||||||
|
[Fact]
|
||||||
|
public void KachelAusblenden_DrehtDenMarginDerNachfolgendenKachel()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
||||||
|
Assert.Equal(1, vm.TasksCard.Column);
|
||||||
|
Assert.Equal(new Avalonia.Thickness(8, 0, 0, 8), vm.TasksCard.Margin);
|
||||||
|
|
||||||
|
vm.TodayCard.IsVisible = false;
|
||||||
|
|
||||||
|
Assert.Equal(0, vm.TasksCard.Column);
|
||||||
|
Assert.Equal(new Avalonia.Thickness(0, 0, 8, 8), vm.TasksCard.Margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Die frueheren sieben eigenen Listen (OpenExcuses, AttendanceWarnings, ...) sind zur
|
||||||
|
/// zusammengefassten Attention-Karte verschmolzen (siehe AttentionItem.cs) — Tests greifen
|
||||||
|
/// seither ueber Kind gefiltert zu statt ueber eine eigene Collection je Art.
|
||||||
|
private static IEnumerable<AttentionItem> Items(DashboardViewModel vm, AttentionKind kind) =>
|
||||||
|
vm.Attention.Where(g => g.Kind == kind).SelectMany(g => g.Items);
|
||||||
|
|
||||||
|
/// AttentionItem traegt fuer MissingTeachingTime kein eigenes Date-Feld (Title ist bereits der
|
||||||
|
/// formatierte Anzeigetext) — das genaue Datum steckt im mitgegebenen Action-Parameter
|
||||||
|
/// (derselbe MissingTeachingTimeItem, den auch der "Erfassen"-Dialog erhaelt).
|
||||||
|
private static DateOnly MissingTimeDate(AttentionItem item) =>
|
||||||
|
((MissingTeachingTimeItem)item.Actions.Single().Parameter!).Date;
|
||||||
|
|
||||||
private static PeriodScheduleService NewPeriodSchedule()
|
private static PeriodScheduleService NewPeriodSchedule()
|
||||||
{
|
{
|
||||||
var tempPath = System.IO.Path.Combine(
|
var tempPath = System.IO.Path.Combine(
|
||||||
@@ -61,7 +122,7 @@ public sealed class DashboardViewModelTests
|
|||||||
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
|
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
|
||||||
FakeSessions? sessions = null, FakeEntries? entries = null,
|
FakeSessions? sessions = null, FakeEntries? entries = null,
|
||||||
FakeAnnualPlanEvents? annualPlanEvents = null, List<LearningGroup>? allGroups = null,
|
FakeAnnualPlanEvents? annualPlanEvents = null, List<LearningGroup>? allGroups = null,
|
||||||
FakeTimeEntries? timeEntries = null)
|
FakeTimeEntries? timeEntries = null, Func<DateTime>? now = null)
|
||||||
{
|
{
|
||||||
lessons ??= new FakeLessons();
|
lessons ??= new FakeLessons();
|
||||||
lessons.Add(lesson);
|
lessons.Add(lesson);
|
||||||
@@ -75,7 +136,8 @@ public sealed class DashboardViewModelTests
|
|||||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
||||||
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(), NewCalendarSettings(),
|
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(), NewCalendarSettings(),
|
||||||
substitutions ?? new FakeSubstitutionEntries(), timeEntries ?? new FakeTimeEntries(),
|
substitutions ?? new FakeSubstitutionEntries(), timeEntries ?? new FakeTimeEntries(),
|
||||||
TestSupport.BuildUntisHubService(), TestSupport.BuildWebUntisIntegrationService(), annualPlanEvents);
|
TestSupport.BuildUntisHubService(), TestSupport.BuildWebUntisIntegrationService(), annualPlanEvents,
|
||||||
|
annualPlanSync: null, schoolWeather: null, now: now);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
|
/// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
|
||||||
@@ -153,9 +215,8 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
||||||
|
|
||||||
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
Assert.Contains(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
|
||||||
Assert.True(vm.MissingTeachingTimeCard.EffectiveIsVisible);
|
Assert.True(vm.AttentionCard.EffectiveIsVisible);
|
||||||
Assert.Equal(2, vm.AttentionCount); // fehlende Unterrichtszeit + bereits bestehende ungeplante Stunde
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -175,7 +236,7 @@ public sealed class DashboardViewModelTests
|
|||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||||
slots: slots, periodSchedule: periodSchedule, timeEntries: timeEntries);
|
slots: slots, periodSchedule: periodSchedule, timeEntries: timeEntries);
|
||||||
|
|
||||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -195,7 +256,7 @@ public sealed class DashboardViewModelTests
|
|||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||||
slots: slots, periodSchedule: periodSchedule, substitutions: substitutions);
|
slots: slots, periodSchedule: periodSchedule, substitutions: substitutions);
|
||||||
|
|
||||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -215,43 +276,55 @@ public sealed class DashboardViewModelTests
|
|||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||||
slots: slots, periodSchedule: periodSchedule, schoolHolidays: schoolHolidays);
|
slots: slots, periodSchedule: periodSchedule, schoolHolidays: schoolHolidays);
|
||||||
|
|
||||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fixer Referenzzeitpunkt fuer die beiden "heute, kurz vor/nach Ablauf der Wartezeit"-Tests
|
||||||
|
/// unten. Regression: die Tests bauten "Unterrichtsende" bisher aus TimeOnly.FromDateTime(
|
||||||
|
/// DateTime.Now).AddHours(±2) — TimeOnly wickelt bei Mitternacht um, ausgefuehrt zwischen ca.
|
||||||
|
/// 22:00 und 02:00 Uhr wurde dadurch aus "+2h" ein Ende VOR "jetzt" (oder umgekehrt), je nach
|
||||||
|
/// Tageszeit zufaellig rot. September ist bewusst gewaehlt: keiner der bundesweiten oder
|
||||||
|
/// laenderspezifischen Feiertage (PublicHolidayService) faellt in diesen Monat, ein Dienstag
|
||||||
|
/// ist garantiert kein Wochenende — die injizierte Uhr (DashboardViewModel now:-Parameter)
|
||||||
|
/// macht "jetzt" fuer den Test unabhaengig von der tatsaechlichen Ausfuehrungsuhrzeit.
|
||||||
|
private static readonly DateTime FixedNow = new(2026, 9, 8, 10, 0, 0);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MissingTeachingTime_HeuteVorAblaufDerWartezeit_WirdNichtGemeldet()
|
public void MissingTeachingTime_HeuteVorAblaufDerWartezeit_WirdNichtGemeldet()
|
||||||
{
|
{
|
||||||
var group = new LearningGroup { Name = "9c" };
|
var group = new LearningGroup { Name = "9c" };
|
||||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
var today = DateOnly.FromDateTime(FixedNow);
|
||||||
var slots = new FakeTimetableSlots();
|
var slots = new FakeTimetableSlots();
|
||||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||||
var periodSchedule = NewPeriodSchedule();
|
var periodSchedule = NewPeriodSchedule();
|
||||||
// Unterrichtsende liegt garantiert noch keine 30 Minuten zurück.
|
// Unterrichtsende liegt (relativ zur fixen Uhr FixedNow) noch keine 30 Minuten zurück.
|
||||||
var futureEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(2);
|
var futureEnd = TimeOnly.FromDateTime(FixedNow).AddHours(2);
|
||||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||||
{ PeriodNumber = 1, Start = futureEnd.AddHours(-1), End = futureEnd }]);
|
{ PeriodNumber = 1, Start = futureEnd.AddHours(-1), End = futureEnd }]);
|
||||||
|
|
||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots,
|
||||||
|
periodSchedule: periodSchedule, now: () => FixedNow);
|
||||||
|
|
||||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == today);
|
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == today);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MissingTeachingTime_HeuteNachAblaufDerWartezeit_WirdGemeldet()
|
public void MissingTeachingTime_HeuteNachAblaufDerWartezeit_WirdGemeldet()
|
||||||
{
|
{
|
||||||
var group = new LearningGroup { Name = "9c" };
|
var group = new LearningGroup { Name = "9c" };
|
||||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
var today = DateOnly.FromDateTime(FixedNow);
|
||||||
var slots = new FakeTimetableSlots();
|
var slots = new FakeTimetableSlots();
|
||||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||||
var periodSchedule = NewPeriodSchedule();
|
var periodSchedule = NewPeriodSchedule();
|
||||||
// Unterrichtsende liegt garantiert mehr als 30 Minuten zurück.
|
// Unterrichtsende liegt (relativ zur fixen Uhr FixedNow) mehr als 30 Minuten zurück.
|
||||||
var pastEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(-2);
|
var pastEnd = TimeOnly.FromDateTime(FixedNow).AddHours(-2);
|
||||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||||
{ PeriodNumber = 1, Start = pastEnd.AddHours(-1), End = pastEnd }]);
|
{ PeriodNumber = 1, Start = pastEnd.AddHours(-1), End = pastEnd }]);
|
||||||
|
|
||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots,
|
||||||
|
periodSchedule: periodSchedule, now: () => FixedNow);
|
||||||
|
|
||||||
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == today);
|
Assert.Contains(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == today);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -378,10 +451,9 @@ public sealed class DashboardViewModelTests
|
|||||||
exams: new FakeExams([exam]), results: results, memberships: memberships,
|
exams: new FakeExams([exam]), results: results, memberships: memberships,
|
||||||
students: new FakeStudents([anna, ben]));
|
students: new FakeStudents([anna, ben]));
|
||||||
|
|
||||||
var correction = Assert.Single(vm.OpenCorrections);
|
var correction = Assert.Single(Items(vm, AttentionKind.Correction));
|
||||||
Assert.Equal(1, correction.Completed);
|
Assert.Equal(50, correction.ProgressPercent);
|
||||||
Assert.Equal(2, correction.Total);
|
Assert.Equal("1 von 2 Arbeiten bewertet", correction.ProgressLabel);
|
||||||
Assert.Equal(50, correction.Percent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -402,7 +474,7 @@ public sealed class DashboardViewModelTests
|
|||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, grades: grades,
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, grades: grades,
|
||||||
memberships: memberships, students: new FakeStudents([student]));
|
memberships: memberships, students: new FakeStudents([student]));
|
||||||
|
|
||||||
Assert.Contains(vm.Alerts, a => a.StudentId == student.Id && a.KindLabel == "Notenabfall");
|
Assert.Contains(Items(vm, AttentionKind.Alert), a => a.Title == student.FullName && a.TrailingText == "Notenabfall");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -561,9 +633,8 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, pastLesson, slots: slots);
|
var vm = BuildVm(group, pastLesson, slots: slots);
|
||||||
|
|
||||||
var item = Assert.Single(vm.UnplannedLessons);
|
var item = Assert.Single(Items(vm, AttentionKind.Unplanned));
|
||||||
Assert.Equal(group.Id, item.GroupId);
|
Assert.Equal($"{group.Name} · 1. Stunde", item.Title);
|
||||||
Assert.Equal(1, item.PeriodNumber);
|
|
||||||
Assert.Equal("Heute", item.DateDisplay);
|
Assert.Equal("Heute", item.DateDisplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +649,7 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, lesson, slots: slots);
|
var vm = BuildVm(group, lesson, slots: slots);
|
||||||
|
|
||||||
Assert.Empty(vm.UnplannedLessons);
|
Assert.Empty(Items(vm, AttentionKind.Unplanned));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -592,7 +663,7 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, pastLesson, slots: slots);
|
var vm = BuildVm(group, pastLesson, slots: slots);
|
||||||
|
|
||||||
Assert.Empty(vm.UnplannedLessons);
|
Assert.Empty(Items(vm, AttentionKind.Unplanned));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -608,7 +679,7 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, pastLesson, slots: slots, schoolHolidays: schoolHolidays);
|
var vm = BuildVm(group, pastLesson, slots: slots, schoolHolidays: schoolHolidays);
|
||||||
|
|
||||||
Assert.Empty(vm.UnplannedLessons);
|
Assert.Empty(Items(vm, AttentionKind.Unplanned));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -623,7 +694,7 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, lesson, slots: slots);
|
var vm = BuildVm(group, lesson, slots: slots);
|
||||||
|
|
||||||
Assert.Empty(vm.UnplannedLessons);
|
Assert.Empty(Items(vm, AttentionKind.Unplanned));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -638,9 +709,10 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, lesson, slots: slots);
|
var vm = BuildVm(group, lesson, slots: slots);
|
||||||
|
|
||||||
Assert.Equal(2, vm.UnplannedLessons.Count);
|
var unplanned = Items(vm, AttentionKind.Unplanned).ToList();
|
||||||
Assert.Contains(vm.UnplannedLessons, i => i.PeriodNumber == 3);
|
Assert.Equal(2, unplanned.Count);
|
||||||
Assert.Contains(vm.UnplannedLessons, i => i.PeriodNumber == 4);
|
Assert.Contains(unplanned, i => i.Title.Contains("3. Stunde"));
|
||||||
|
Assert.Contains(unplanned, i => i.Title.Contains("4. Stunde"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -658,7 +730,7 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
var vm = BuildVm(group, pastLesson, slots: slots, substitutions: substitutions);
|
var vm = BuildVm(group, pastLesson, slots: slots, substitutions: substitutions);
|
||||||
|
|
||||||
Assert.Empty(vm.UnplannedLessons);
|
Assert.Empty(Items(vm, AttentionKind.Unplanned));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -681,7 +753,7 @@ public sealed class DashboardViewModelTests
|
|||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||||
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
||||||
|
|
||||||
Assert.Empty(vm.AttendanceWarnings);
|
Assert.Empty(Items(vm, AttentionKind.Attendance));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -705,9 +777,9 @@ public sealed class DashboardViewModelTests
|
|||||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||||
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
||||||
|
|
||||||
var item = Assert.Single(vm.AttendanceWarnings);
|
var item = Assert.Single(Items(vm, AttentionKind.Attendance));
|
||||||
Assert.Equal(student.FullName, item.StudentName);
|
Assert.Equal(student.FullName, item.Title);
|
||||||
Assert.Equal(30.0, item.AbsenceRatePercent);
|
Assert.Equal("30 %", item.TrailingText);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>Die sieben Dashboard-Kacheln, die tatsächlich "wo muss ich reagieren" beantworten
|
||||||
|
/// (siehe <see cref="DashboardViewModel.AttentionCount"/>), zusammengefasst zu einer Karte mit
|
||||||
|
/// Filter-Chips statt sieben eigenen Sichtbarkeits-Schaltern. Reihenfolge hier ist die Reihenfolge
|
||||||
|
/// der Gruppen in der zusammengefassten Ansicht (siehe DashboardViewModel.RebuildAttention) — sie
|
||||||
|
/// übernimmt bewusst die frühere Sortierung aus DashboardSettingsService.DefaultCardOrder, damit
|
||||||
|
/// sich nichts, was Nutzer bereits kennen, unvorhersehbar umsortiert.
|
||||||
|
/// "Klausurwochen" (examload) bleibt bewusst außen vor: die Einträge dort sind Wochen-Aggregate,
|
||||||
|
/// keine Einzelvorgänge, die sich abhaken oder anklicken lassen — sie passen nicht ins Item-Modell.</summary>
|
||||||
|
public enum AttentionKind { MissingTeachingTime, Excuse, Correction, Unplanned, Alert, Attendance, SupportPlan }
|
||||||
|
|
||||||
|
/// <summary>Eine Inline-Aktion auf einem Handlungsbedarf-Eintrag (z.B. "Entschuldigt"/"Unentschuldigt",
|
||||||
|
/// "Erfassen"). Verwendet ICommand statt Action, damit die bereits vorhandenen generierten
|
||||||
|
/// RelayCommands (OpenExcuseItem.MarkExcusedCommand, DashboardViewModel.AddMissingTeachingTimeCommand)
|
||||||
|
/// direkt weiterverwendet werden können, statt sie in einen zweiten Delegate-Typ zu verpacken.</summary>
|
||||||
|
public sealed record AttentionAction(string Label, ICommand Command, object? Parameter = null);
|
||||||
|
|
||||||
|
/// <summary>Gemeinsame Projektion für die sieben Handlungsbedarf-Arten. Trägt nur Anzeigedaten —
|
||||||
|
/// die eigentliche Beschaffung bleibt unverändert in den jeweiligen DashboardViewModel.LoadXxx-
|
||||||
|
/// Methoden; sie erzeugen am Ende ein AttentionItem statt (wie vorher) ihre eigene Item-Klasse in
|
||||||
|
/// ihre eigene ObservableCollection einzutragen.</summary>
|
||||||
|
public sealed class AttentionItem
|
||||||
|
{
|
||||||
|
public AttentionKind Kind { get; }
|
||||||
|
public string Title { get; }
|
||||||
|
public string Subtitle { get; }
|
||||||
|
/// <summary>Kurztext rechts neben dem Titel — Fehlzeitenquote, Alert-Kind-Label. Meist leer.</summary>
|
||||||
|
public string TrailingText { get; }
|
||||||
|
public string DateDisplay { get; }
|
||||||
|
public bool IsOverdue { get; }
|
||||||
|
public AlertSeverity? Severity { get; }
|
||||||
|
/// <summary>Nur bei Korrekturen gesetzt (0–100); steuert die ProgressBar. Eigenes HasProgress statt
|
||||||
|
/// Nullability, damit die ProgressBar-Bindung ein einfaches int (kompatibel mit Value:double) bleibt.</summary>
|
||||||
|
public int ProgressPercent { get; }
|
||||||
|
public bool HasProgress { get; }
|
||||||
|
/// <summary>Text unter der ProgressBar, z.B. "3 von 5 Arbeiten bewertet". Nur bei Korrekturen gesetzt.</summary>
|
||||||
|
public string ProgressLabel { get; }
|
||||||
|
/// <summary>Färbt TrailingText wie die frühere Fehlzeiten-Warnung (fest "Foreground=Red") ein —
|
||||||
|
/// nur bei Attendance gesetzt, weil ein AttentionItem dieser Art per Konstruktion nur entsteht,
|
||||||
|
/// wenn die Fehlzeitenquote den Schwellenwert bereits überschreitet.</summary>
|
||||||
|
public bool IsWarningTrailing { get; }
|
||||||
|
public IReadOnlyList<AttentionAction> Actions { get; }
|
||||||
|
public Action? Navigate { get; }
|
||||||
|
|
||||||
|
public AttentionItem(AttentionKind kind, string title, string subtitle = "", string trailingText = "",
|
||||||
|
string dateDisplay = "", bool isOverdue = false, AlertSeverity? severity = null,
|
||||||
|
int progressPercent = 0, string progressLabel = "", bool hasProgress = false,
|
||||||
|
bool isWarningTrailing = false, IReadOnlyList<AttentionAction>? actions = null, Action? navigate = null)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Title = title;
|
||||||
|
Subtitle = subtitle;
|
||||||
|
TrailingText = trailingText;
|
||||||
|
DateDisplay = dateDisplay;
|
||||||
|
IsOverdue = isOverdue;
|
||||||
|
Severity = severity;
|
||||||
|
ProgressPercent = progressPercent;
|
||||||
|
ProgressLabel = progressLabel;
|
||||||
|
HasProgress = hasProgress;
|
||||||
|
IsWarningTrailing = isWarningTrailing;
|
||||||
|
Actions = actions ?? [];
|
||||||
|
Navigate = navigate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasSubtitle => !string.IsNullOrWhiteSpace(Subtitle);
|
||||||
|
public bool HasTrailingText => !string.IsNullOrWhiteSpace(TrailingText);
|
||||||
|
public bool HasDate => !string.IsNullOrWhiteSpace(DateDisplay);
|
||||||
|
public bool HasActions => Actions.Count > 0;
|
||||||
|
public bool HasSeverity => Severity is not null;
|
||||||
|
public bool IsHighSeverity => Severity == AlertSeverity.High;
|
||||||
|
public bool IsMediumSeverity => Severity == AlertSeverity.Medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Eine Kind-Gruppe innerhalb der zusammengefassten Handlungsbedarf-Liste. Items behalten
|
||||||
|
/// die Sortierung, die ihre jeweilige LoadXxx-Methode schon immer produziert hat (z.B. Fehlzeiten
|
||||||
|
/// nach Quote absteigend, Korrekturen nach Datum) — ein zweites, generisches "Importance"-Sortieren
|
||||||
|
/// quer über alle Arten würde diese bewusst gewählten Reihenfolgen wieder zerstören.</summary>
|
||||||
|
public sealed class AttentionGroup(AttentionKind kind, string header, IReadOnlyList<AttentionItem> items)
|
||||||
|
{
|
||||||
|
public AttentionKind Kind { get; } = kind;
|
||||||
|
public string Header { get; } = header;
|
||||||
|
public IReadOnlyList<AttentionItem> Items { get; } = items;
|
||||||
|
public int Count => Items.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ein Filter-Chip für eine Handlungsbedarf-Art in der zusammengefassten Karte — ersetzt
|
||||||
|
/// die frühere Sichtbarkeit je Einzelkachel (siehe DashboardViewModel.LoadAttentionFilters).</summary>
|
||||||
|
public partial class AttentionFilterOption : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private bool _isActive = true;
|
||||||
|
public AttentionKind Kind { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
public Action? OnChanged { get; set; }
|
||||||
|
|
||||||
|
public AttentionFilterOption(AttentionKind kind, string label)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsActiveChanged(bool value) => OnChanged?.Invoke();
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
private readonly SchoolWeatherService? _schoolWeather;
|
private readonly SchoolWeatherService? _schoolWeather;
|
||||||
private readonly UntisHubService _untisHub;
|
private readonly UntisHubService _untisHub;
|
||||||
private readonly WebUntisIntegrationService _webUntis;
|
private readonly WebUntisIntegrationService _webUntis;
|
||||||
|
private readonly Func<DateTime> _now;
|
||||||
|
|
||||||
private const int OpenExcuseMaxAgeDays = 21;
|
private const int OpenExcuseMaxAgeDays = 21;
|
||||||
private const int SupportPlanDueWithinDays = 14;
|
private const int SupportPlanDueWithinDays = 14;
|
||||||
@@ -89,20 +90,43 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public ObservableCollection<TaskItem> OpenTasks { get; } = [];
|
public ObservableCollection<TaskItem> OpenTasks { get; } = [];
|
||||||
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
|
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
|
||||||
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
|
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
|
||||||
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
|
||||||
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
|
|
||||||
public ObservableCollection<ExamWeekLoadItem> ExamWeekLoads { get; } = [];
|
public ObservableCollection<ExamWeekLoadItem> ExamWeekLoads { get; } = [];
|
||||||
public ObservableCollection<MissingTeachingTimeItem> MissingTeachingTimeEntries { get; } = [];
|
|
||||||
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
|
|
||||||
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
|
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
|
||||||
public ObservableCollection<CorrectionProgressItem> OpenCorrections { get; } = [];
|
|
||||||
public ObservableCollection<UnplannedLessonItem> UnplannedLessons { get; } = [];
|
|
||||||
public ObservableCollection<DashboardAlertItem> Alerts { get; } = [];
|
|
||||||
public ObservableCollection<CalendarEventItem> SelectedDayEvents { get; } = [];
|
public ObservableCollection<CalendarEventItem> SelectedDayEvents { get; } = [];
|
||||||
public ObservableCollection<DashboardCardOption> DashboardCards { get; } = [];
|
public ObservableCollection<DashboardCardOption> DashboardCards { get; } = [];
|
||||||
public ObservableCollection<DashboardWeatherWarningItem> WeatherWarnings { get; } = [];
|
public ObservableCollection<DashboardWeatherWarningItem> WeatherWarnings { get; } = [];
|
||||||
|
// Zusammengefasste "Handlungsbedarf"-Karte (vormals sieben eigene Kacheln/Collections:
|
||||||
|
// Entschuldigungen, Fehlzeiten, Förderplan, Korrekturen, ungeplante Stunden, Auffälligkeiten,
|
||||||
|
// Unterrichtszeit-Nacherfassung — siehe AttentionItem.cs). Attention traegt nur, was nach
|
||||||
|
// Filterung (AttentionFilters) tatsaechlich angezeigt wird; die Rohdaten je Art liegen in den
|
||||||
|
// privaten _xyzItems-Feldern und werden von RebuildAttention() neu zusammengesetzt, sobald sich
|
||||||
|
// Daten oder Filter aendern — ohne die Repos erneut abzufragen.
|
||||||
|
public ObservableCollection<AttentionGroup> Attention { get; } = [];
|
||||||
|
public ObservableCollection<AttentionFilterOption> AttentionFilters { get; } = [];
|
||||||
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
|
|
||||||
|
private readonly List<OpenExcuseItem> _excuses = [];
|
||||||
|
private readonly List<AttentionItem> _attendanceItems = [];
|
||||||
|
private readonly List<AttentionItem> _supportItems = [];
|
||||||
|
private readonly List<AttentionItem> _correctionItems = [];
|
||||||
|
private readonly List<AttentionItem> _unplannedItems = [];
|
||||||
|
private readonly List<AttentionItem> _alertItems = [];
|
||||||
|
private readonly List<AttentionItem> _missingTimeItems = [];
|
||||||
|
|
||||||
|
/// Reihenfolge und Überschriften der Gruppen in der zusammengefassten Handlungsbedarf-Karte —
|
||||||
|
/// übernimmt die frühere Kachel-Reihenfolge aus DashboardSettingsService.DefaultCardOrder,
|
||||||
|
/// damit sich für Nutzer nichts unvorhersehbar umsortiert.
|
||||||
|
private static readonly (AttentionKind Kind, string Header)[] AttentionGroupOrder =
|
||||||
|
[
|
||||||
|
(AttentionKind.MissingTeachingTime, "Unterrichtszeit nacherfassen"),
|
||||||
|
(AttentionKind.Excuse, "Offene Entschuldigungen"),
|
||||||
|
(AttentionKind.Correction, "Offene Korrekturen"),
|
||||||
|
(AttentionKind.Unplanned, "Ungeplante Stunden"),
|
||||||
|
(AttentionKind.Alert, "Auffälligkeiten"),
|
||||||
|
(AttentionKind.Attendance, "Fehlzeiten-Warnung"),
|
||||||
|
(AttentionKind.SupportPlan, "Förderplan-Wiedervorlage"),
|
||||||
|
];
|
||||||
|
|
||||||
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
||||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||||
public Action<Guid>? OnNavigateToStudent { get; set; }
|
public Action<Guid>? OnNavigateToStudent { get; set; }
|
||||||
@@ -125,21 +149,15 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public DashboardCardOption TodayCard => Card("today");
|
public DashboardCardOption TodayCard => Card("today");
|
||||||
public DashboardCardOption TasksCard => Card("tasks");
|
public DashboardCardOption TasksCard => Card("tasks");
|
||||||
public DashboardCardOption CalendarCard => Card("calendar");
|
public DashboardCardOption CalendarCard => Card("calendar");
|
||||||
public DashboardCardOption ExcusesCard => Card("excuses");
|
|
||||||
public DashboardCardOption UpcomingCard => Card("upcoming");
|
public DashboardCardOption UpcomingCard => Card("upcoming");
|
||||||
public DashboardCardOption CorrectionsCard => Card("corrections");
|
|
||||||
public DashboardCardOption UnplannedCard => Card("unplanned");
|
|
||||||
public DashboardCardOption AlertsCard => Card("alerts");
|
|
||||||
public DashboardCardOption AttendanceCard => Card("attendance");
|
|
||||||
public DashboardCardOption ExamLoadCard => Card("examload");
|
public DashboardCardOption ExamLoadCard => Card("examload");
|
||||||
public DashboardCardOption MissingTeachingTimeCard => Card("missingteachingtime");
|
|
||||||
public DashboardCardOption SupportCard => Card("support");
|
|
||||||
public DashboardCardOption GroupsCard => Card("groups");
|
public DashboardCardOption GroupsCard => Card("groups");
|
||||||
|
public DashboardCardOption AttentionCard => Card("attention");
|
||||||
public int TodayLessonCount => TodaysLessons.Count;
|
public int TodayLessonCount => TodaysLessons.Count;
|
||||||
public int OpenTaskCount => OpenTasks.Count;
|
public int OpenTaskCount => OpenTasks.Count;
|
||||||
public int UpcomingCount => UpcomingDates.Count;
|
public int UpcomingCount => UpcomingDates.Count;
|
||||||
public int AttentionCount => OpenExcuses.Count + AttendanceWarnings.Count + SupportPlanReviews.Count
|
public int AttentionCount => _excuses.Count + _attendanceItems.Count + _supportItems.Count
|
||||||
+ OpenCorrections.Count + UnplannedLessons.Count + Alerts.Count + MissingTeachingTimeEntries.Count;
|
+ _correctionItems.Count + _unplannedItems.Count + _alertItems.Count + _missingTimeItems.Count;
|
||||||
public string TodayLessonSummary => TodayLessonCount == 1 ? "1 Stunde" : $"{TodayLessonCount} Stunden";
|
public string TodayLessonSummary => TodayLessonCount == 1 ? "1 Stunde" : $"{TodayLessonCount} Stunden";
|
||||||
public string OpenTaskSummary => OpenTaskCount == 1 ? "1 Aufgabe" : $"{OpenTaskCount} Aufgaben";
|
public string OpenTaskSummary => OpenTaskCount == 1 ? "1 Aufgabe" : $"{OpenTaskCount} Aufgaben";
|
||||||
public string AttentionSummary => AttentionCount == 1 ? "1 offener Punkt" : $"{AttentionCount} offene Punkte";
|
public string AttentionSummary => AttentionCount == 1 ? "1 offener Punkt" : $"{AttentionCount} offene Punkte";
|
||||||
@@ -161,7 +179,8 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
ISubstitutionEntryRepository substitutions, ITimeEntryRepository timeEntries,
|
ISubstitutionEntryRepository substitutions, ITimeEntryRepository timeEntries,
|
||||||
UntisHubService untisHub, WebUntisIntegrationService webUntis,
|
UntisHubService untisHub, WebUntisIntegrationService webUntis,
|
||||||
IAnnualPlanEventRepository? annualPlanEvents = null,
|
IAnnualPlanEventRepository? annualPlanEvents = null,
|
||||||
AnnualPlanSyncService? annualPlanSync = null, SchoolWeatherService? schoolWeather = null)
|
AnnualPlanSyncService? annualPlanSync = null, SchoolWeatherService? schoolWeather = null,
|
||||||
|
Func<DateTime>? now = null)
|
||||||
{
|
{
|
||||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||||
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
|
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
|
||||||
@@ -176,11 +195,17 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
_schoolWeather = schoolWeather;
|
_schoolWeather = schoolWeather;
|
||||||
_untisHub = untisHub;
|
_untisHub = untisHub;
|
||||||
_webUntis = webUntis;
|
_webUntis = webUntis;
|
||||||
|
// Testbare Uhr statt direkter DateTime.Now-Aufrufe (siehe Load()/LoadMissingTeachingTime):
|
||||||
|
// TimeOnly.AddHours()/AddMinutes() wickelt bei Mitternacht um, ohne injizierbares "jetzt"
|
||||||
|
// waeren Tests fuer "kurz vor/nach Ablauf einer Wartezeit" je nach Ausfuehrungsuhrzeit
|
||||||
|
// zufaellig rot oder gruen (siehe TODO.md, Abschnitt 9, Nachtrag Uhrzeit-Wraparound).
|
||||||
|
_now = now ?? (() => DateTime.Now);
|
||||||
if (annualPlanSync is not null)
|
if (annualPlanSync is not null)
|
||||||
{
|
{
|
||||||
annualPlanSync.DataChanged += () => Avalonia.Threading.Dispatcher.UIThread.Post(LoadCalendar);
|
annualPlanSync.DataChanged += () => Avalonia.Threading.Dispatcher.UIThread.Post(LoadCalendar);
|
||||||
}
|
}
|
||||||
LoadDashboardCards();
|
LoadDashboardCards();
|
||||||
|
LoadAttentionFilters();
|
||||||
Load();
|
Load();
|
||||||
RefreshWebUntisHealth();
|
RefreshWebUntisHealth();
|
||||||
}
|
}
|
||||||
@@ -204,7 +229,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
private void Load()
|
private void Load()
|
||||||
{
|
{
|
||||||
var now = DateTime.Now;
|
var now = _now();
|
||||||
var today = DateOnly.FromDateTime(now);
|
var today = DateOnly.FromDateTime(now);
|
||||||
CurrentDate = now.ToString("dddd, d. MMMM yyyy", De);
|
CurrentDate = now.ToString("dddd, d. MMMM yyyy", De);
|
||||||
CurrentSchoolYear = _sy.CurrentSchoolYear();
|
CurrentSchoolYear = _sy.CurrentSchoolYear();
|
||||||
@@ -265,6 +290,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
LoadOpenCorrections(groups, today);
|
LoadOpenCorrections(groups, today);
|
||||||
LoadUnplannedLessons(groups, today);
|
LoadUnplannedLessons(groups, today);
|
||||||
LoadAlerts(groups, today);
|
LoadAlerts(groups, today);
|
||||||
|
RebuildAttention();
|
||||||
UpdateDashboardSummary();
|
UpdateDashboardSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,16 +299,14 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
TodayCard.IsEmpty = TodaysLessons.Count == 0;
|
TodayCard.IsEmpty = TodaysLessons.Count == 0;
|
||||||
TasksCard.IsEmpty = OpenTasks.Count == 0;
|
TasksCard.IsEmpty = OpenTasks.Count == 0;
|
||||||
CalendarCard.IsEmpty = false;
|
CalendarCard.IsEmpty = false;
|
||||||
ExcusesCard.IsEmpty = OpenExcuses.Count == 0;
|
|
||||||
UpcomingCard.IsEmpty = UpcomingDates.Count == 0;
|
UpcomingCard.IsEmpty = UpcomingDates.Count == 0;
|
||||||
CorrectionsCard.IsEmpty = OpenCorrections.Count == 0;
|
|
||||||
UnplannedCard.IsEmpty = UnplannedLessons.Count == 0;
|
|
||||||
AlertsCard.IsEmpty = Alerts.Count == 0;
|
|
||||||
AttendanceCard.IsEmpty = AttendanceWarnings.Count == 0;
|
|
||||||
ExamLoadCard.IsEmpty = ExamWeekLoads.Count == 0;
|
ExamLoadCard.IsEmpty = ExamWeekLoads.Count == 0;
|
||||||
MissingTeachingTimeCard.IsEmpty = MissingTeachingTimeEntries.Count == 0;
|
|
||||||
SupportCard.IsEmpty = SupportPlanReviews.Count == 0;
|
|
||||||
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
|
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
|
||||||
|
AttentionCard.IsEmpty = AttentionCount == 0;
|
||||||
|
|
||||||
|
// Erst nachdem alle IsEmpty-Werte stehen: welche Kachel tatsaechlich gerendert wird, haengt
|
||||||
|
// ueber EffectiveIsVisible daran, und davon wiederum die Zeilen-/Spaltenzuordnung.
|
||||||
|
ApplyCardLayout();
|
||||||
|
|
||||||
OnPropertyChanged(nameof(TodayLessonCount));
|
OnPropertyChanged(nameof(TodayLessonCount));
|
||||||
OnPropertyChanged(nameof(OpenTaskCount));
|
OnPropertyChanged(nameof(OpenTaskCount));
|
||||||
@@ -349,7 +373,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
private void LoadAttendanceWarnings(DateOnly today)
|
private void LoadAttendanceWarnings(DateOnly today)
|
||||||
{
|
{
|
||||||
AttendanceWarnings.Clear();
|
_attendanceItems.Clear();
|
||||||
var schoolYear = _sy.CurrentSchoolYear();
|
var schoolYear = _sy.CurrentSchoolYear();
|
||||||
var from = _sy.SchoolYearStart(schoolYear);
|
var from = _sy.SchoolYearStart(schoolYear);
|
||||||
var to = _sy.SchoolYearEnd(schoolYear);
|
var to = _sy.SchoolYearEnd(schoolYear);
|
||||||
@@ -372,9 +396,13 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
|
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
|
||||||
}
|
}
|
||||||
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
|
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
|
||||||
AttendanceWarnings.Add(item);
|
_attendanceItems.Add(ToAttentionItem(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AttentionItem ToAttentionItem(AttendanceWarningItem w) => new(
|
||||||
|
AttentionKind.Attendance, w.StudentName, trailingText: $"{w.AbsenceRatePercent:0.#} %",
|
||||||
|
isWarningTrailing: true, navigate: () => OnNavigateToStudent?.Invoke(w.StudentId));
|
||||||
|
|
||||||
// ── Klausurwochen (Nutzer-Feedback) ───────────────────────────────────────
|
// ── Klausurwochen (Nutzer-Feedback) ───────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Persönliche Klausurlast über alle Kurse hinweg — anders als die klassenbezogene
|
// Persönliche Klausurlast über alle Kurse hinweg — anders als die klassenbezogene
|
||||||
@@ -406,13 +434,13 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
private void LoadMissingTeachingTime(DateOnly today)
|
private void LoadMissingTeachingTime(DateOnly today)
|
||||||
{
|
{
|
||||||
MissingTeachingTimeEntries.Clear();
|
_missingTimeItems.Clear();
|
||||||
var firstDay = today.AddDays(-MissingTeachingTimeLookbackDays);
|
var firstDay = today.AddDays(-MissingTeachingTimeLookbackDays);
|
||||||
var publicHolidayDates = Enumerable.Range(firstDay.Year, today.Year - firstDay.Year + 1)
|
var publicHolidayDates = Enumerable.Range(firstDay.Year, today.Year - firstDay.Year + 1)
|
||||||
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
||||||
.Select(h => h.Date).ToHashSet();
|
.Select(h => h.Date).ToHashSet();
|
||||||
var schoolHolidays = _schoolHolidays.GetAll();
|
var schoolHolidays = _schoolHolidays.GetAll();
|
||||||
var nowTime = TimeOnly.FromDateTime(DateTime.Now);
|
var nowTime = TimeOnly.FromDateTime(_now());
|
||||||
|
|
||||||
var items = new List<MissingTeachingTimeItem>();
|
var items = new List<MissingTeachingTimeItem>();
|
||||||
for (var date = firstDay; date <= today; date = date.AddDays(1))
|
for (var date = firstDay; date <= today; date = date.AddDays(1))
|
||||||
@@ -441,14 +469,15 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
items.Add(new MissingTeachingTimeItem(date, windowStart, windowEnd));
|
items.Add(new MissingTeachingTimeItem(date, windowStart, windowEnd));
|
||||||
}
|
}
|
||||||
foreach (var item in items.OrderBy(i => i.Date))
|
foreach (var item in items.OrderBy(i => i.Date))
|
||||||
MissingTeachingTimeEntries.Add(item);
|
_missingTimeItems.Add(new AttentionItem(AttentionKind.MissingTeachingTime, item.DateDisplay,
|
||||||
|
actions: [new AttentionAction("Erfassen", AddMissingTeachingTimeCommand, item)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
|
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
|
||||||
|
|
||||||
private void LoadSupportPlanReviews(DateOnly today)
|
private void LoadSupportPlanReviews(DateOnly today)
|
||||||
{
|
{
|
||||||
SupportPlanReviews.Clear();
|
_supportItems.Clear();
|
||||||
var dueBy = today.AddDays(SupportPlanDueWithinDays);
|
var dueBy = today.AddDays(SupportPlanDueWithinDays);
|
||||||
|
|
||||||
var due = _documentation.GetAll()
|
var due = _documentation.GetAll()
|
||||||
@@ -461,8 +490,11 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var student = _students.GetById(d.StudentId);
|
var student = _students.GetById(d.StudentId);
|
||||||
if (student is null) continue;
|
if (student is null) continue;
|
||||||
SupportPlanReviews.Add(new SupportPlanDueItem(
|
var reviewDate = d.SupportData!.ReviewDate!.Value;
|
||||||
d.StudentId, student.FullName, d.Title, d.SupportData!.ReviewDate!.Value, today));
|
var studentId = d.StudentId;
|
||||||
|
_supportItems.Add(new AttentionItem(AttentionKind.SupportPlan, student.FullName, subtitle: d.Title,
|
||||||
|
dateDisplay: reviewDate.ToString("dd.MM.yyyy"), isOverdue: reviewDate < today,
|
||||||
|
navigate: () => OnNavigateToStudent?.Invoke(studentId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,7 +539,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
private void LoadOpenCorrections(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
private void LoadOpenCorrections(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||||
{
|
{
|
||||||
OpenCorrections.Clear();
|
_correctionItems.Clear();
|
||||||
foreach (var group in groups.Values)
|
foreach (var group in groups.Values)
|
||||||
foreach (var exam in _exams.GetByGroup(group.Id)
|
foreach (var exam in _exams.GetByGroup(group.Id)
|
||||||
.Where(e => e.Status is ExamStatus.Conducted or ExamStatus.Graded)
|
.Where(e => e.Status is ExamStatus.Conducted or ExamStatus.Graded)
|
||||||
@@ -515,8 +547,13 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var (expected, evaluated) = ExamCorrectionCounter.Count(exam,
|
var (expected, evaluated) = ExamCorrectionCounter.Count(exam,
|
||||||
_memberships.GetByGroup(group.Id), _examResults.GetByExam(exam.Id));
|
_memberships.GetByGroup(group.Id), _examResults.GetByExam(exam.Id));
|
||||||
OpenCorrections.Add(new CorrectionProgressItem(exam.Id, group.Id, exam.Title,
|
var percent = expected == 0 ? 0 : (int)Math.Round(evaluated * 100.0 / expected);
|
||||||
group.Name, exam.Date, evaluated, expected, today));
|
var groupId = group.Id;
|
||||||
|
_correctionItems.Add(new AttentionItem(AttentionKind.Correction, exam.Title, subtitle: group.Name,
|
||||||
|
dateDisplay: exam.Date.ToString("dd.MM.yyyy"),
|
||||||
|
isOverdue: exam.Date < today.AddDays(-7) && evaluated < expected,
|
||||||
|
progressPercent: percent, progressLabel: $"{evaluated} von {expected} Arbeiten bewertet", hasProgress: true,
|
||||||
|
navigate: () => OnNavigateToExam?.Invoke(groupId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,14 +569,14 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
private void LoadUnplannedLessons(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
private void LoadUnplannedLessons(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||||
{
|
{
|
||||||
UnplannedLessons.Clear();
|
_unplannedItems.Clear();
|
||||||
var lastDay = today.AddDays(UnplannedLessonsLookaheadDays);
|
var lastDay = today.AddDays(UnplannedLessonsLookaheadDays);
|
||||||
var publicHolidayDates = Enumerable.Range(today.Year, lastDay.Year - today.Year + 1)
|
var publicHolidayDates = Enumerable.Range(today.Year, lastDay.Year - today.Year + 1)
|
||||||
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
||||||
.Select(h => h.Date).ToHashSet();
|
.Select(h => h.Date).ToHashSet();
|
||||||
var schoolHolidays = _schoolHolidays.GetAll();
|
var schoolHolidays = _schoolHolidays.GetAll();
|
||||||
|
|
||||||
var items = new List<UnplannedLessonItem>();
|
var items = new List<(DateOnly Date, int PeriodNumber, LearningGroup Group)>();
|
||||||
foreach (var group in groups.Values.Where(g => g.RequiresLessonPlanning))
|
foreach (var group in groups.Values.Where(g => g.RequiresLessonPlanning))
|
||||||
{
|
{
|
||||||
var slots = _timetableSlots.GetByGroup(group.Id);
|
var slots = _timetableSlots.GetByGroup(group.Id);
|
||||||
@@ -568,12 +605,19 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
items.Add(new UnplannedLessonItem(group.Id, group.Name, date, slot.PeriodNumber, today));
|
items.Add((date, slot.PeriodNumber, group));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach (var item in items.OrderBy(i => i.Date).ThenBy(i => i.PeriodNumber).ThenBy(i => i.GroupName))
|
foreach (var (date, periodNumber, group) in items.OrderBy(i => i.Date).ThenBy(i => i.PeriodNumber)
|
||||||
UnplannedLessons.Add(item);
|
.ThenBy(i => i.Group.Name))
|
||||||
|
{
|
||||||
|
var dateDisplay = date == today ? "Heute" : date == today.AddDays(1) ? "Morgen" : date.ToString("dd.MM.");
|
||||||
|
var groupId = group.Id;
|
||||||
|
_unplannedItems.Add(new AttentionItem(AttentionKind.Unplanned,
|
||||||
|
$"{group.Name} · {periodNumber}. Stunde", dateDisplay: dateDisplay,
|
||||||
|
navigate: () => OnNavigateToUnplannedLesson?.Invoke(groupId)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Erkennt, ob eine Stunde ohne eigene Lesson bereits Teil einer Doppelstunde ist, die
|
/// <summary>Erkennt, ob eine Stunde ohne eigene Lesson bereits Teil einer Doppelstunde ist, die
|
||||||
@@ -605,11 +649,11 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
private void LoadAlerts(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
private void LoadAlerts(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||||
{
|
{
|
||||||
Alerts.Clear();
|
_alertItems.Clear();
|
||||||
foreach (var warning in AttendanceWarnings)
|
// Fehlzeiten-Auffälligkeiten erscheinen in der zusammengefassten Handlungsbedarf-Karte
|
||||||
Alerts.Add(new DashboardAlertItem(warning.StudentId, null, warning.StudentName,
|
// bereits als eigene Gruppe "Fehlzeiten-Warnung" (LoadAttendanceWarnings) — eine weitere
|
||||||
"Fehlzeiten", $"Fehlzeitenquote {warning.AbsenceRatePercent:0.#} %", AlertSeverity.High));
|
// Kopie hier wäre jetzt eine sichtbare Dopplung derselben Schüler/Zahl, die vor dem Merge
|
||||||
|
// durch zwei getrennte Kacheln (Auffälligkeiten vs. Fehlzeiten-Warnung) nicht auffiel.
|
||||||
foreach (var group in groups.Values)
|
foreach (var group in groups.Values)
|
||||||
{
|
{
|
||||||
var groupReportGrades = _reportGrades.GetByGroup(group.Id);
|
var groupReportGrades = _reportGrades.GetByGroup(group.Id);
|
||||||
@@ -618,6 +662,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var student = _students.GetById(membership.StudentId);
|
var student = _students.GetById(membership.StudentId);
|
||||||
if (student is null) continue;
|
if (student is null) continue;
|
||||||
|
var studentId = student.Id;
|
||||||
var values = _grades.GetByStudentAndGroup(student.Id, group.Id)
|
var values = _grades.GetByStudentAndGroup(student.Id, group.Id)
|
||||||
.OrderBy(g => g.Date)
|
.OrderBy(g => g.Date)
|
||||||
.Select(g => int.TryParse(g.Value, out var value) ? (int?)value : null)
|
.Select(g => int.TryParse(g.Value, out var value) ? (int?)value : null)
|
||||||
@@ -631,9 +676,10 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
? recent - previous >= 1.0
|
? recent - previous >= 1.0
|
||||||
: previous - recent >= 3.0;
|
: previous - recent >= 3.0;
|
||||||
if (declined)
|
if (declined)
|
||||||
Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
|
_alertItems.Add(new AttentionItem(AttentionKind.Alert, student.FullName,
|
||||||
"Notenabfall", $"{group.Name}: zuletzt {recent:0.0}, zuvor {previous:0.0}",
|
subtitle: $"{group.Name}: zuletzt {recent:0.0}, zuvor {previous:0.0}",
|
||||||
AlertSeverity.Medium));
|
trailingText: "Notenabfall", severity: AlertSeverity.Medium,
|
||||||
|
navigate: () => OnNavigateToStudent?.Invoke(studentId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
var latestReport = groupReportGrades
|
var latestReport = groupReportGrades
|
||||||
@@ -642,9 +688,10 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
var effective = latestReport?.OverrideValue ?? latestReport?.CalculatedValue;
|
var effective = latestReport?.OverrideValue ?? latestReport?.CalculatedValue;
|
||||||
if (int.TryParse(effective, out var reportValue)
|
if (int.TryParse(effective, out var reportValue)
|
||||||
&& (group.GradingSystem == GradingSystem.Grades1To6 ? reportValue >= 5 : reportValue <= 4))
|
&& (group.GradingSystem == GradingSystem.Grades1To6 ? reportValue >= 5 : reportValue <= 4))
|
||||||
Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
|
_alertItems.Add(new AttentionItem(AttentionKind.Alert, student.FullName,
|
||||||
"Versetzungsgefährdung", $"{group.Name}: aktueller Stand {reportValue}",
|
subtitle: $"{group.Name}: aktueller Stand {reportValue}",
|
||||||
AlertSeverity.High));
|
trailingText: "Versetzungsgefährdung", severity: AlertSeverity.High,
|
||||||
|
navigate: () => OnNavigateToStudent?.Invoke(studentId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -666,19 +713,74 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
private static string CardTitle(string key) => key switch
|
private static string CardTitle(string key) => key switch
|
||||||
{
|
{
|
||||||
"today" => "Heute", "tasks" => "Offene Aufgaben", "calendar" => "Kalender",
|
"today" => "Heute", "tasks" => "Offene Aufgaben", "calendar" => "Kalender",
|
||||||
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
|
"upcoming" => "Anstehende Termine", "groups" => "Meine Lerngruppen", "examload" => "Klausurwochen",
|
||||||
"corrections" => "Offene Korrekturen", "unplanned" => "Ungeplante Stunden", "alerts" => "Auffälligkeiten",
|
"attention" => "Handlungsbedarf", _ => key,
|
||||||
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
|
|
||||||
"groups" => "Meine Lerngruppen", "examload" => "Klausurwochen",
|
|
||||||
"missingteachingtime" => "Unterrichtszeit nacherfassen", _ => key,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Handlungsbedarf: Filter-Chips ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Ersetzt die frühere Sichtbarkeit je Einzelkachel (sieben Schalter im "Bereiche anpassen"-
|
||||||
|
// Panel) durch Filter-Chips innerhalb der zusammengefassten Karte — dichter, und der
|
||||||
|
// naheliegende Ort, weil alle sieben jetzt eine Karte sind. Bewusst nur für die Dauer der
|
||||||
|
// Sitzung (keine Persistenz über DashboardSettingsService): das JSON-Format dort ist eine
|
||||||
|
// flache Liste von Kachel-Einstellungen, eine zweite Objektform nur für diese sieben Filter
|
||||||
|
// hätte das Dateiformat aufgespalten, ohne dass "welche Handlungsbedarf-Art blende ich
|
||||||
|
// dauerhaft aus" bisher als Bedürfnis geäußert wurde.
|
||||||
|
|
||||||
|
private void LoadAttentionFilters()
|
||||||
|
{
|
||||||
|
AttentionFilters.Clear();
|
||||||
|
foreach (var (kind, header) in AttentionGroupOrder)
|
||||||
|
{
|
||||||
|
var option = new AttentionFilterOption(kind, header) { OnChanged = RebuildAttention };
|
||||||
|
AttentionFilters.Add(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAttentionFilterActive(AttentionKind kind) =>
|
||||||
|
AttentionFilters.FirstOrDefault(f => f.Kind == kind)?.IsActive ?? true;
|
||||||
|
|
||||||
|
/// <summary>Setzt Attention aus den bereits geladenen _xyzItems-Feldern neu zusammen — reine
|
||||||
|
/// Umsortierung/Filterung im Speicher, kein Repo-Zugriff. Wird nach jedem Load() sowie nach
|
||||||
|
/// jeder punktuellen Änderung (Entschuldigung aufgelöst, Zeit nacherfasst, Filter-Chip
|
||||||
|
/// umgeschaltet) aufgerufen.</summary>
|
||||||
|
private void RebuildAttention()
|
||||||
|
{
|
||||||
|
var byKind = new Dictionary<AttentionKind, IReadOnlyList<AttentionItem>>
|
||||||
|
{
|
||||||
|
[AttentionKind.MissingTeachingTime] = _missingTimeItems,
|
||||||
|
[AttentionKind.Excuse] = _excuses.Select(ToAttentionItem).ToList(),
|
||||||
|
[AttentionKind.Correction] = _correctionItems,
|
||||||
|
[AttentionKind.Unplanned] = _unplannedItems,
|
||||||
|
[AttentionKind.Alert] = _alertItems,
|
||||||
|
[AttentionKind.Attendance] = _attendanceItems,
|
||||||
|
[AttentionKind.SupportPlan] = _supportItems,
|
||||||
|
};
|
||||||
|
|
||||||
|
Attention.Clear();
|
||||||
|
foreach (var (kind, header) in AttentionGroupOrder)
|
||||||
|
{
|
||||||
|
if (!IsAttentionFilterActive(kind)) continue;
|
||||||
|
var items = byKind[kind];
|
||||||
|
if (items.Count == 0) continue;
|
||||||
|
Attention.Add(new AttentionGroup(kind, header, items));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttentionItem ToAttentionItem(OpenExcuseItem e) => new(
|
||||||
|
AttentionKind.Excuse, e.StudentName, subtitle: $"{e.GroupName} · {e.DateDisplay}",
|
||||||
|
actions: [new AttentionAction("Entschuldigt", e.MarkExcusedCommand),
|
||||||
|
new AttentionAction("Unentschuldigt", e.MarkUnexcusedCommand)]);
|
||||||
|
|
||||||
|
// Zaehlt bewusst EffectiveIsVisible, nicht IsVisible: eine eingeschaltete, aber gerade leere
|
||||||
|
// HideWhenEmpty-Kachel wird nicht gerendert und darf deshalb auch keinen Rasterplatz belegen,
|
||||||
|
// sonst bleibt an ihrer Stelle eine Luecke im zweispaltigen Grid.
|
||||||
private void ApplyCardLayout()
|
private void ApplyCardLayout()
|
||||||
{
|
{
|
||||||
var visibleIndex = 0;
|
var visibleIndex = 0;
|
||||||
foreach (var card in DashboardCards)
|
foreach (var card in DashboardCards)
|
||||||
{
|
{
|
||||||
var index = card.IsVisible ? visibleIndex++ : 0;
|
var index = card.EffectiveIsVisible ? visibleIndex++ : 0;
|
||||||
card.Row = index / 2;
|
card.Row = index / 2;
|
||||||
card.Column = index % 2;
|
card.Column = index % 2;
|
||||||
}
|
}
|
||||||
@@ -714,11 +816,10 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
SaveAndApplyCardLayout();
|
SaveAndApplyCardLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand] private void OpenStudentAttendance(AttendanceWarningItem? item)
|
/// <summary>Ersetzt die früheren fünf eigenen Navigations-Commands (OpenStudentAttendance,
|
||||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
/// OpenStudentSupportPlan, OpenCorrection, OpenUnplannedLesson, OpenAlert) — jedes AttentionItem
|
||||||
|
/// trägt sein Sprungziel bereits als Closure in Navigate.</summary>
|
||||||
[RelayCommand] private void OpenStudentSupportPlan(SupportPlanDueItem? item)
|
[RelayCommand] private void OpenAttentionItem(AttentionItem? item) => item?.Navigate?.Invoke();
|
||||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task AddMissingTeachingTime(MissingTeachingTimeItem? item)
|
private async Task AddMissingTeachingTime(MissingTeachingTimeItem? item)
|
||||||
@@ -726,12 +827,13 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
if (item is null || OnAddMissingTeachingTime is null) return;
|
if (item is null || OnAddMissingTeachingTime is null) return;
|
||||||
await OnAddMissingTeachingTime(item);
|
await OnAddMissingTeachingTime(item);
|
||||||
LoadMissingTeachingTime(DateOnly.FromDateTime(DateTime.Today));
|
LoadMissingTeachingTime(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
RebuildAttention();
|
||||||
UpdateDashboardSummary();
|
UpdateDashboardSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadOpenExcuses(List<LearningGroup> groups, DateOnly today)
|
private void LoadOpenExcuses(List<LearningGroup> groups, DateOnly today)
|
||||||
{
|
{
|
||||||
OpenExcuses.Clear();
|
_excuses.Clear();
|
||||||
var cutoff = today.AddDays(-OpenExcuseMaxAgeDays);
|
var cutoff = today.AddDays(-OpenExcuseMaxAgeDays);
|
||||||
|
|
||||||
var items = new List<OpenExcuseItem>();
|
var items = new List<OpenExcuseItem>();
|
||||||
@@ -750,8 +852,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach (var item in items.OrderBy(i => i.Date))
|
_excuses.AddRange(items.OrderBy(i => i.Date));
|
||||||
OpenExcuses.Add(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResolveExcuse(OpenExcuseItem item, AttendanceStatus status)
|
private void ResolveExcuse(OpenExcuseItem item, AttendanceStatus status)
|
||||||
@@ -760,7 +861,8 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
if (entry is null) return;
|
if (entry is null) return;
|
||||||
entry.Attendance = status;
|
entry.Attendance = status;
|
||||||
_participationEntries.Save(entry);
|
_participationEntries.Save(entry);
|
||||||
OpenExcuses.Remove(item);
|
_excuses.Remove(item);
|
||||||
|
RebuildAttention();
|
||||||
UpdateDashboardSummary();
|
UpdateDashboardSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,12 +1063,6 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
else OnNavigateToGroup?.Invoke(groupId);
|
else OnNavigateToGroup?.Invoke(groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[RelayCommand] private void OpenCorrection(CorrectionProgressItem? item)
|
|
||||||
{ if (item is not null) OnNavigateToExam?.Invoke(item.GroupId); }
|
|
||||||
[RelayCommand] private void OpenUnplannedLesson(UnplannedLessonItem? item)
|
|
||||||
{ if (item is not null) OnNavigateToUnplannedLesson?.Invoke(item.GroupId); }
|
|
||||||
[RelayCommand] private void OpenAlert(DashboardAlertItem? item)
|
|
||||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
|
||||||
[RelayCommand] private void Refresh() => Load();
|
[RelayCommand] private void Refresh() => Load();
|
||||||
|
|
||||||
[RelayCommand] private Task AddTask() => AddTaskInternal(startAsReminder: false);
|
[RelayCommand] private Task AddTask() => AddTaskInternal(startAsReminder: false);
|
||||||
@@ -1115,26 +1211,6 @@ public class MissingTeachingTimeItem(DateOnly date, TimeOnly windowStart, TimeOn
|
|||||||
public string DateDisplay { get; } = date.ToString("dddd, dd.MM.", De);
|
public string DateDisplay { get; } = date.ToString("dddd, dd.MM.", De);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────────
|
|
||||||
|
|
||||||
public class SupportPlanDueItem
|
|
||||||
{
|
|
||||||
public Guid StudentId { get; }
|
|
||||||
public string StudentName { get; }
|
|
||||||
public string Title { get; }
|
|
||||||
public string ReviewDateDisplay { get; }
|
|
||||||
public bool IsOverdue { get; }
|
|
||||||
|
|
||||||
public SupportPlanDueItem(Guid studentId, string studentName, string title, DateOnly reviewDate, DateOnly today)
|
|
||||||
{
|
|
||||||
StudentId = studentId;
|
|
||||||
StudentName = studentName;
|
|
||||||
Title = title;
|
|
||||||
ReviewDateDisplay = reviewDate.ToString("dd.MM.yyyy");
|
|
||||||
IsOverdue = reviewDate < today;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class CalendarDayCell : ObservableObject
|
public partial class CalendarDayCell : ObservableObject
|
||||||
{
|
{
|
||||||
[ObservableProperty] private bool _isSelected;
|
[ObservableProperty] private bool _isSelected;
|
||||||
@@ -1210,46 +1286,8 @@ public sealed class UpcomingDateItem(UpcomingDateKind kind, DateOnly date, strin
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class CorrectionProgressItem(Guid examId, Guid groupId, string title, string groupName,
|
|
||||||
DateOnly date, int completed, int total, DateOnly today)
|
|
||||||
{
|
|
||||||
public Guid ExamId { get; } = examId;
|
|
||||||
public Guid GroupId { get; } = groupId;
|
|
||||||
public string Title { get; } = title;
|
|
||||||
public string GroupName { get; } = groupName;
|
|
||||||
public DateOnly Date { get; } = date;
|
|
||||||
public int Completed { get; } = completed;
|
|
||||||
public int Total { get; } = total;
|
|
||||||
public int Percent => Total == 0 ? 0 : (int)Math.Round(Completed * 100.0 / Total);
|
|
||||||
public string ProgressDisplay => $"{Completed} von {Total} Arbeiten bewertet";
|
|
||||||
public string DateDisplay => Date.ToString("dd.MM.yyyy");
|
|
||||||
public bool IsOverdue => Date < today.AddDays(-7) && Completed < Total;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class UnplannedLessonItem(Guid groupId, string groupName, DateOnly date, int periodNumber, DateOnly today)
|
|
||||||
{
|
|
||||||
public Guid GroupId { get; } = groupId;
|
|
||||||
public string GroupName { get; } = groupName;
|
|
||||||
public DateOnly Date { get; } = date;
|
|
||||||
public int PeriodNumber { get; } = periodNumber;
|
|
||||||
public string DateDisplay => Date == today ? "Heute" : Date == today.AddDays(1) ? "Morgen" : Date.ToString("dd.MM.");
|
|
||||||
public string Display => $"{GroupName} · {PeriodNumber}. Stunde";
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum AlertSeverity { Medium, High }
|
public enum AlertSeverity { Medium, High }
|
||||||
|
|
||||||
public sealed class DashboardAlertItem(Guid studentId, Guid? groupId, string studentName,
|
|
||||||
string kindLabel, string detail, AlertSeverity severity)
|
|
||||||
{
|
|
||||||
public Guid StudentId { get; } = studentId;
|
|
||||||
public Guid? GroupId { get; } = groupId;
|
|
||||||
public string StudentName { get; } = studentName;
|
|
||||||
public string KindLabel { get; } = kindLabel;
|
|
||||||
public string Detail { get; } = detail;
|
|
||||||
public AlertSeverity Severity { get; } = severity;
|
|
||||||
public string SeverityColor => Severity == AlertSeverity.High ? "#D32F2F" : "#F59E0B";
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class DashboardCardOption : ObservableObject
|
public partial class DashboardCardOption : ObservableObject
|
||||||
{
|
{
|
||||||
[ObservableProperty] private bool _isVisible;
|
[ObservableProperty] private bool _isVisible;
|
||||||
@@ -1262,12 +1300,19 @@ public partial class DashboardCardOption : ObservableObject
|
|||||||
public bool EffectiveIsVisible => IsVisible && (!HideWhenEmpty || !IsEmpty);
|
public bool EffectiveIsVisible => IsVisible && (!HideWhenEmpty || !IsEmpty);
|
||||||
public Action? OnVisibilityChanged { get; set; }
|
public Action? OnVisibilityChanged { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Rinnstein zur jeweils anderen Rasterspalte. Muss aus der berechneten
|
||||||
|
/// <see cref="Column"/> kommen und darf nicht im XAML fest an der Kachel haengen: welche Kachel
|
||||||
|
/// links und welche rechts landet, entscheidet sich erst zur Laufzeit aus Reihenfolge und
|
||||||
|
/// Sichtbarkeit, ein fester Margin sitzt dann bei jeder Umschaltung auf der falschen Seite.</summary>
|
||||||
|
public Avalonia.Thickness Margin => Column == 0
|
||||||
|
? new Avalonia.Thickness(0, 0, 8, 8)
|
||||||
|
: new Avalonia.Thickness(8, 0, 0, 8);
|
||||||
|
|
||||||
public DashboardCardOption(string key, string title, bool isVisible)
|
public DashboardCardOption(string key, string title, bool isVisible)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
Title = title;
|
Title = title;
|
||||||
HideWhenEmpty = key is "excuses" or "upcoming" or "corrections" or "unplanned"
|
HideWhenEmpty = key is "upcoming" or "attention";
|
||||||
or "alerts" or "attendance" or "support";
|
|
||||||
_isVisible = isVisible;
|
_isVisible = isVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1278,4 +1323,6 @@ public partial class DashboardCardOption : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsEmptyChanged(bool value) => OnPropertyChanged(nameof(EffectiveIsVisible));
|
partial void OnIsEmptyChanged(bool value) => OnPropertyChanged(nameof(EffectiveIsVisible));
|
||||||
|
|
||||||
|
partial void OnColumnChanged(int value) => OnPropertyChanged(nameof(Margin));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,28 @@
|
|||||||
<Setter Property="BorderThickness" Value="2"/>
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
<!-- Handlungsbedarf-Karte: Schweregrad-Streifen (nur Auffälligkeiten) und die rot
|
||||||
|
hervorgehobene Fehlzeitenquote — ersetzen die früheren fest kodierten Hex-Farben
|
||||||
|
(#D32F2F/#F59E0B, Foreground="Red") durch die validierte Status-Palette. -->
|
||||||
|
<Style Selector="Border.severitybar">
|
||||||
|
<Setter Property="Width" Value="4"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.severitybar.high">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AppStatusDangerBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.severitybar.medium">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AppStatusWarningBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.attentionTrailing">
|
||||||
|
<Setter Property="FontSize" Value="10"/>
|
||||||
|
<Setter Property="Opacity" Value="0.6"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.attentionTrailing.warning">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Opacity" Value="1"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AppStatusDangerBrush}"/>
|
||||||
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<ScrollViewer Padding="24">
|
<ScrollViewer Padding="24">
|
||||||
@@ -142,11 +164,12 @@
|
|||||||
<TextBlock Text="HEUTE UND HANDLUNGSBEDARF" FontSize="11" FontWeight="Bold" Opacity="0.5"
|
<TextBlock Text="HEUTE UND HANDLUNGSBEDARF" FontSize="11" FontWeight="Bold" Opacity="0.5"
|
||||||
Margin="2,2,0,-8"/>
|
Margin="2,2,0,-8"/>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="3*,2*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
<!-- 7 Kacheln (today/tasks/calendar/examload/upcoming/groups/attention) auf 2 Spalten -> 4 Zeilen. -->
|
||||||
|
<Grid ColumnDefinitions="3*,2*" RowDefinitions="Auto,Auto,Auto,Auto">
|
||||||
|
|
||||||
<!-- Heutige Stunden -->
|
<!-- Heutige Stunden -->
|
||||||
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
|
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
|
||||||
IsVisible="{Binding TodayCard.EffectiveIsVisible}" Margin="0,0,8,8"
|
IsVisible="{Binding TodayCard.EffectiveIsVisible}" Margin="{Binding TodayCard.Margin}"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -187,7 +210,7 @@
|
|||||||
|
|
||||||
<!-- Offene Aufgaben -->
|
<!-- Offene Aufgaben -->
|
||||||
<Border Grid.Column="{Binding TasksCard.Column}" Grid.Row="{Binding TasksCard.Row}"
|
<Border Grid.Column="{Binding TasksCard.Column}" Grid.Row="{Binding TasksCard.Row}"
|
||||||
IsVisible="{Binding TasksCard.EffectiveIsVisible}" Margin="8,0,0,8"
|
IsVisible="{Binding TasksCard.EffectiveIsVisible}" Margin="{Binding TasksCard.Margin}"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -224,7 +247,7 @@
|
|||||||
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
|
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
|
||||||
Lerngruppen-Liste darunter ihn nicht nach unten verdrängt. -->
|
Lerngruppen-Liste darunter ihn nicht nach unten verdrängt. -->
|
||||||
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
|
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
|
||||||
IsVisible="{Binding CalendarCard.EffectiveIsVisible}" Margin="0,0,8,8"
|
IsVisible="{Binding CalendarCard.EffectiveIsVisible}" Margin="{Binding CalendarCard.Margin}"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16" HorizontalAlignment="Left" MaxWidth="320">
|
CornerRadius="8" Padding="16" HorizontalAlignment="Left" MaxWidth="320">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
@@ -366,73 +389,9 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
|
|
||||||
<Border Grid.Column="{Binding ExcusesCard.Column}" Grid.Row="{Binding ExcusesCard.Row}"
|
|
||||||
IsVisible="{Binding ExcusesCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
|
||||||
CornerRadius="8" Padding="16">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="OFFENE ENTSCHULDIGUNGEN" FontSize="11" FontWeight="Bold"
|
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding OpenExcuses}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:OpenExcuseItem">
|
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,4">
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock Text="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"
|
|
||||||
TextTrimming="CharacterEllipsis"/>
|
|
||||||
<TextBlock FontSize="11" Opacity="0.6">
|
|
||||||
<Run Text="{Binding GroupName}"/>
|
|
||||||
<Run Text=" · "/>
|
|
||||||
<Run Text="{Binding DateDisplay}"/>
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
<Button Grid.Column="1" Content="Entschuldigt" FontSize="11" Padding="7,3"
|
|
||||||
Command="{Binding MarkExcusedCommand}" Margin="0,0,4,0"/>
|
|
||||||
<Button Grid.Column="2" Content="Unentschuldigt" FontSize="11" Padding="7,3"
|
|
||||||
Command="{Binding MarkUnexcusedCommand}"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="Keine offenen Entschuldigungen." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !OpenExcuses.Count}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Fehlzeiten-Warnung (5.2.3) -->
|
|
||||||
<Border Grid.Column="{Binding AttendanceCard.Column}" Grid.Row="{Binding AttendanceCard.Row}"
|
|
||||||
IsVisible="{Binding AttendanceCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
|
||||||
CornerRadius="8" Padding="16">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="FEHLZEITEN-WARNUNG" FontSize="11" FontWeight="Bold"
|
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding AttendanceWarnings}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:AttendanceWarningItem">
|
|
||||||
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
|
|
||||||
<Button Grid.Column="0" Content="{Binding StudentName}" FontSize="13"
|
|
||||||
HorizontalAlignment="Left" HorizontalContentAlignment="Left"
|
|
||||||
Background="Transparent" Padding="0"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenStudentAttendanceCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
<TextBlock Grid.Column="1" Foreground="Red" FontSize="12" VerticalAlignment="Center">
|
|
||||||
<Run Text="{Binding AbsenceRatePercent}"/>
|
|
||||||
<Run Text=" %"/>
|
|
||||||
</TextBlock>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="Keine Fehlzeiten über dem Schwellenwert." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !AttendanceWarnings.Count}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Klausurwochen (Nutzer-Feedback): eigene Klausurlast über alle Kurse hinweg -->
|
<!-- Klausurwochen (Nutzer-Feedback): eigene Klausurlast über alle Kurse hinweg -->
|
||||||
<Border Grid.Column="{Binding ExamLoadCard.Column}" Grid.Row="{Binding ExamLoadCard.Row}"
|
<Border Grid.Column="{Binding ExamLoadCard.Column}" Grid.Row="{Binding ExamLoadCard.Row}"
|
||||||
IsVisible="{Binding ExamLoadCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
IsVisible="{Binding ExamLoadCard.EffectiveIsVisible}" Margin="{Binding ExamLoadCard.Margin}" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -454,66 +413,9 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Unterrichtszeit nacherfassen (Nutzer-Feedback) -->
|
|
||||||
<Border Grid.Column="{Binding MissingTeachingTimeCard.Column}" Grid.Row="{Binding MissingTeachingTimeCard.Row}"
|
|
||||||
IsVisible="{Binding MissingTeachingTimeCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
|
||||||
CornerRadius="8" Padding="16">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="UNTERRICHTSZEIT NACHERFASSEN" FontSize="11" FontWeight="Bold"
|
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding MissingTeachingTimeEntries}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:MissingTeachingTimeItem">
|
|
||||||
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
|
|
||||||
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="13"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Button Grid.Column="1" Content="Erfassen" FontSize="12" Padding="10,4"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).AddMissingTeachingTimeCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="Keine fehlende Unterrichtszeit-Erfassung." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !MissingTeachingTimeEntries.Count}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
|
||||||
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
|
|
||||||
IsVisible="{Binding SupportCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
|
||||||
CornerRadius="8" Padding="16">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="FÖRDERPLAN-WIEDERVORLAGE" FontSize="11" FontWeight="Bold"
|
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding SupportPlanReviews}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:SupportPlanDueItem">
|
|
||||||
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<Button Content="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"
|
|
||||||
HorizontalAlignment="Left" HorizontalContentAlignment="Left"
|
|
||||||
Background="Transparent" Padding="0"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenStudentSupportPlanCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
<TextBlock Text="{Binding Title}" FontSize="11" Opacity="0.6"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding ReviewDateDisplay}" FontSize="12"
|
|
||||||
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="Keine fälligen Überprüfungen." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !SupportPlanReviews.Count}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Anstehende Termine (9.3) -->
|
<!-- Anstehende Termine (9.3) -->
|
||||||
<Border Grid.Column="{Binding UpcomingCard.Column}" Grid.Row="{Binding UpcomingCard.Row}"
|
<Border Grid.Column="{Binding UpcomingCard.Column}" Grid.Row="{Binding UpcomingCard.Row}"
|
||||||
IsVisible="{Binding UpcomingCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
IsVisible="{Binding UpcomingCard.EffectiveIsVisible}" Margin="{Binding UpcomingCard.Margin}" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -548,106 +450,107 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Offene Korrekturen (9.4) -->
|
<!-- Handlungsbedarf: zusammengefasste Karte für die frueheren sieben Kacheln
|
||||||
<Border Grid.Column="{Binding CorrectionsCard.Column}" Grid.Row="{Binding CorrectionsCard.Row}"
|
(Entschuldigungen, Fehlzeiten, Foerderplan, Korrekturen, ungeplante Stunden,
|
||||||
IsVisible="{Binding CorrectionsCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
Auffaelligkeiten, Unterrichtszeit-Nacherfassung) — siehe AttentionItem.cs.
|
||||||
|
Filter-Chips ersetzen die vorherige Sichtbarkeit je Einzelkachel. x:Name traegt den
|
||||||
|
Weg zum DashboardViewModel durch zwei ItemsControl-Verschachtelungen (Gruppe -> Item)
|
||||||
|
hindurch, ohne $parent[ItemsControl] mit Ancestor-Level zaehlen zu muessen. -->
|
||||||
|
<Border Grid.Column="{Binding AttentionCard.Column}" Grid.Row="{Binding AttentionCard.Row}"
|
||||||
|
IsVisible="{Binding AttentionCard.EffectiveIsVisible}" Margin="{Binding AttentionCard.Margin}" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel Spacing="10">
|
||||||
<TextBlock Text="OFFENE KORREKTUREN" FontSize="11" FontWeight="Bold"
|
<TextBlock Text="HANDLUNGSBEDARF" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding OpenCorrections}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:CorrectionProgressItem">
|
|
||||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,5"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenCorrectionCommand}"
|
|
||||||
CommandParameter="{Binding}">
|
|
||||||
<StackPanel Spacing="3">
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="11" Opacity="0.6"/>
|
|
||||||
</Grid>
|
|
||||||
<TextBlock Text="{Binding GroupName}" FontSize="11" Opacity="0.6"/>
|
|
||||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Percent}" Height="6"/>
|
|
||||||
<TextBlock Text="{Binding ProgressDisplay}" FontSize="10" Opacity="0.65"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="Keine offenen Korrekturen." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !OpenCorrections.Count}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Ungeplante Stunden -->
|
<ItemsControl ItemsSource="{Binding AttentionFilters}">
|
||||||
<Border Grid.Column="{Binding UnplannedCard.Column}" Grid.Row="{Binding UnplannedCard.Row}"
|
<ItemsControl.ItemsPanel>
|
||||||
IsVisible="{Binding UnplannedCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
<ItemsPanelTemplate><WrapPanel Orientation="Horizontal" ItemSpacing="6" LineSpacing="6"/></ItemsPanelTemplate>
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
</ItemsControl.ItemsPanel>
|
||||||
CornerRadius="8" Padding="16">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="UNGEPLANTE STUNDEN" FontSize="11" FontWeight="Bold"
|
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding UnplannedLessons}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:UnplannedLessonItem">
|
<DataTemplate x:DataType="vm:AttentionFilterOption">
|
||||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,5"
|
<ToggleButton Content="{Binding Label}" IsChecked="{Binding IsActive}"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
FontSize="11" Padding="8,3"/>
|
||||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenUnplannedLessonCommand}"
|
|
||||||
CommandParameter="{Binding}">
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<TextBlock Text="{Binding Display}" FontSize="13"
|
|
||||||
TextTrimming="CharacterEllipsis"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="12"
|
|
||||||
VerticalAlignment="Center" Opacity="0.6"/>
|
|
||||||
</Grid>
|
|
||||||
</Button>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
<TextBlock Text="Keine ungeplanten Stunden." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !UnplannedLessons.Count}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Auffälligkeiten (9.5) -->
|
<ItemsControl x:Name="AttentionGroupsList" ItemsSource="{Binding Attention}">
|
||||||
<Border Grid.Column="{Binding AlertsCard.Column}" Grid.Row="{Binding AlertsCard.Row}"
|
|
||||||
IsVisible="{Binding AlertsCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
|
||||||
CornerRadius="8" Padding="16">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="AUFFÄLLIGKEITEN" FontSize="11" FontWeight="Bold"
|
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding Alerts}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:DashboardAlertItem">
|
<DataTemplate x:DataType="vm:AttentionGroup">
|
||||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,4"
|
<StackPanel Margin="0,6,0,0" Spacing="4">
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
<TextBlock FontSize="11" FontWeight="SemiBold" Opacity="0.7">
|
||||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenAlertCommand}"
|
<Run Text="{Binding Header}"/>
|
||||||
CommandParameter="{Binding}">
|
<Run Text=" ("/>
|
||||||
<Grid ColumnDefinitions="4,*">
|
<Run Text="{Binding Count}"/>
|
||||||
<Border Background="{Binding SeverityColor}" CornerRadius="2" Margin="0,0,9,0"/>
|
<Run Text=")"/>
|
||||||
<StackPanel Grid.Column="1">
|
</TextBlock>
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<ItemsControl ItemsSource="{Binding Items}">
|
||||||
<TextBlock Text="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"/>
|
<ItemsControl.ItemTemplate>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding KindLabel}" FontSize="10" Opacity="0.6"/>
|
<DataTemplate x:DataType="vm:AttentionItem">
|
||||||
</Grid>
|
<StackPanel Margin="0,4">
|
||||||
<TextBlock Text="{Binding Detail}" FontSize="11" Opacity="0.65"/>
|
<Grid ColumnDefinitions="4,*">
|
||||||
</StackPanel>
|
<Border Grid.Column="0" Classes="severitybar"
|
||||||
</Grid>
|
Classes.high="{Binding IsHighSeverity}"
|
||||||
</Button>
|
Classes.medium="{Binding IsMediumSeverity}" Margin="0,0,9,0"/>
|
||||||
|
<!-- Bewusst immer als Button (statt je nach IsClickable ein anderes Root-Element):
|
||||||
|
ein einheitliches Template für alle sieben Arten. Bei Excuse/MissingTeachingTime
|
||||||
|
ist Navigate null, der Klick auf die Zeile selbst ist dann folgenlos — die
|
||||||
|
eigentliche Aktion liegt dort in Actions (Entschuldigt/Erfassen) darunter. -->
|
||||||
|
<Button Grid.Column="1" Background="Transparent" BorderThickness="0" Padding="0"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||||
|
Command="{Binding #AttentionGroupsList.((vm:DashboardViewModel)DataContext).OpenAttentionItemCommand}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding TrailingText}"
|
||||||
|
Classes="attentionTrailing" Classes.warning="{Binding IsWarningTrailing}"
|
||||||
|
VerticalAlignment="Center" Margin="8,0,0,0"
|
||||||
|
IsVisible="{Binding HasTrailingText}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="12"
|
||||||
|
VerticalAlignment="Center" Margin="8,0,0,0"
|
||||||
|
Classes.overdue="{Binding IsOverdue}"
|
||||||
|
IsVisible="{Binding HasDate}"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding Subtitle}" FontSize="11" Opacity="0.6"
|
||||||
|
IsVisible="{Binding HasSubtitle}"/>
|
||||||
|
<ProgressBar Minimum="0" Maximum="100" Value="{Binding ProgressPercent}"
|
||||||
|
Height="6" IsVisible="{Binding HasProgress}"/>
|
||||||
|
<TextBlock Text="{Binding ProgressLabel}" FontSize="10" Opacity="0.65"
|
||||||
|
IsVisible="{Binding HasProgress}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<ItemsControl ItemsSource="{Binding Actions}" Margin="13,4,0,0"
|
||||||
|
IsVisible="{Binding HasActions}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="4"/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:AttentionAction">
|
||||||
|
<Button Content="{Binding Label}" FontSize="11" Padding="7,3"
|
||||||
|
Command="{Binding Command}" CommandParameter="{Binding Parameter}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
<TextBlock Text="Keine Auffälligkeiten erkannt." Classes="emptyhint"
|
<TextBlock Text="Keine offenen Punkte." Classes="emptyhint"
|
||||||
IsVisible="{Binding !Alerts.Count}"/>
|
IsVisible="{Binding !Attention.Count}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Meine Lerngruppen -->
|
<!-- Meine Lerngruppen -->
|
||||||
<Border Grid.Column="{Binding GroupsCard.Column}" Grid.Row="{Binding GroupsCard.Row}"
|
<Border Grid.Column="{Binding GroupsCard.Column}" Grid.Row="{Binding GroupsCard.Row}"
|
||||||
IsVisible="{Binding GroupsCard.EffectiveIsVisible}"
|
IsVisible="{Binding GroupsCard.EffectiveIsVisible}" Margin="{Binding GroupsCard.Margin}"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|||||||
@@ -3228,6 +3228,56 @@ tatsächlichen Lesson oder einem an diesem Tag aktiven Stundenplan-Slot zuerst a
|
|||||||
einem Akzentpunkt markiert; innerhalb dieser Gruppe sowie für den Rest gilt alphabetische Sortierung.
|
einem Akzentpunkt markiert; innerhalb dieser Gruppe sowie für den Rest gilt alphabetische Sortierung.
|
||||||
Ferien, Feiertage und vollständig ausgefallene Stunden werden dabei berücksichtigt.
|
Ferien, Feiertage und vollständig ausgefallene Stunden werden dabei berücksichtigt.
|
||||||
|
|
||||||
|
**Nachtrag Kachelraster-Bugfix (September 2026):** `ApplyCardLayout()` zählte bislang `IsVisible`
|
||||||
|
statt `EffectiveIsVisible` für die Zeilen-/Spaltenzuordnung — eine eingeschaltete, aber gerade leere
|
||||||
|
`HideWhenEmpty`-Kachel (im Alltag der Normalfall, da meist mehrere Hinweiskacheln leer sind) belegte
|
||||||
|
dadurch weiterhin einen Rasterplatz und riss eine Lücke ins zweispaltige Grid. Zusätzlich hing der
|
||||||
|
Grid-Margin jeder Kachel fest im XAML an einer Spalte (links/rechts), obwohl Spalte und Zeile erst
|
||||||
|
zur Laufzeit aus Sichtbarkeit und Reihenfolge berechnet werden — beim Aus-/Einblenden einer Kachel
|
||||||
|
wanderten die Nachbarn in die andere Spalte, der Rinnstein saß dann auf der falschen Seite.
|
||||||
|
`DashboardCardOption.Margin` leitet den Wert jetzt aus `Column` ab, das XAML bindet darauf statt
|
||||||
|
fixer Werte, und `UpdateDashboardSummary()` ruft `ApplyCardLayout()` erst auf, nachdem alle
|
||||||
|
`IsEmpty`-Werte für den aktuellen Ladevorgang feststehen.
|
||||||
|
|
||||||
|
**Nachtrag Handlungsbedarf-Zusammenfassung (September 2026, Nutzer-Feedback):** Die sieben Kacheln,
|
||||||
|
die tatsächlich "wo muss ich reagieren" beantworten — Offene Entschuldigungen (9), Fehlzeiten-
|
||||||
|
Warnung (5.2.3), Förderplan-Wiedervorlage (5.3.2), Offene Korrekturen (9.4), Ungeplante Stunden
|
||||||
|
(9.9), Auffälligkeiten (9.5), Unterrichtszeit nacherfassen — sind zu einer Karte "Handlungsbedarf"
|
||||||
|
mit Filter-Chips zusammengefasst. Neues gemeinsames Modell `AttentionItem`/`AttentionGroup`/
|
||||||
|
`AttentionAction` ([AttentionItem.cs](LehrerApp.Desktop/ViewModels/AttentionItem.cs)) trägt nur
|
||||||
|
Anzeigedaten; die Beschaffung bleibt unverändert in den jeweiligen `DashboardViewModel.LoadXxx`-
|
||||||
|
Methoden, die am Ende ein `AttentionItem` statt ihrer eigenen Item-Klasse erzeugen.
|
||||||
|
`DashboardViewModel.RebuildAttention()` gruppiert nach Art (feste Reihenfolge, keine
|
||||||
|
kachel-übergreifende Neusortierung nach Dringlichkeit — das hätte die je Art bewusst gewählte
|
||||||
|
Sortierung, z.B. Fehlzeiten nach Quote absteigend, wieder zerstört) und wendet die Filter-Chips an,
|
||||||
|
ohne die Repos erneut abzufragen. `SupportPlanDueItem`, `CorrectionProgressItem`,
|
||||||
|
`UnplannedLessonItem` und `DashboardAlertItem` entfallen (nur dashboard-intern verwendet);
|
||||||
|
`OpenExcuseItem` und `AttendanceWarningItem` bleiben bestehen, da `GroupOverviewViewModel`
|
||||||
|
(Kurs-Dashboard) sie weiterhin direkt nutzt. Fünf Navigations-Commands (`OpenStudentAttendance`,
|
||||||
|
`OpenStudentSupportPlan`, `OpenCorrection`, `OpenUnplannedLesson`, `OpenAlert`) wurden durch ein
|
||||||
|
einziges `OpenAttentionItemCommand` ersetzt, das das im `AttentionItem` mitgegebene `Navigate`-
|
||||||
|
Delegate aufruft. **Bewusste Verhaltensänderung:** die "Auffälligkeiten"-Kachel zeigte bisher
|
||||||
|
Fehlzeiten-Überschreitungen zusätzlich zu Notenabfall/Versetzungsgefährdung — in der
|
||||||
|
zusammengefassten Karte wäre das dieselbe Zahl doppelt (einmal als eigene Gruppe "Fehlzeiten-
|
||||||
|
Warnung", einmal als "Auffälligkeit"), was vor dem Merge durch zwei getrennte Kacheln nicht auffiel;
|
||||||
|
`LoadAlerts` erzeugt diese Kopie deshalb nicht mehr. Die Filter-Chips sind bewusst nur
|
||||||
|
Sitzungszustand (keine Persistenz in `dashboardsettings.json`, das dortige Format ist eine flache
|
||||||
|
Kachel-Liste ohne Platz für Sub-Filter je Art). "Klausurwochen" bleibt weiterhin eine eigene Kachel
|
||||||
|
(Wochen-Aggregat, kein Einzelvorgang zum Abhaken/Anklicken).
|
||||||
|
|
||||||
|
**Nachtrag Uhrzeit-Wraparound-Bug (September 2026):** Zwei `MissingTeachingTime`-Tests bauten ihr
|
||||||
|
"Unterrichtsende" bisher aus `TimeOnly.FromDateTime(DateTime.Now).AddHours(±2)`. `TimeOnly` wickelt
|
||||||
|
bei Mitternacht um (kein Datumsanteil) — bei Testausführung zwischen ca. 22:00 und 02:00 Uhr wurde
|
||||||
|
aus "+2 Stunden" dadurch ein Ende, das *vor* statt nach dem echten Ablauf des 30-Minuten-Fensters
|
||||||
|
lag (oder umgekehrt bei "-2 Stunden"), je nach Ausführungsuhrzeit zufällig rot. `DashboardViewModel`
|
||||||
|
erhält deshalb einen optionalen `Func<DateTime>? now`-Konstruktorparameter (Default `DateTime.Now`,
|
||||||
|
gleiches Muster wie die bestehenden optionalen `annualPlanEvents`/`schoolWeather`-Parameter — DI
|
||||||
|
löst unregistrierte optionale Parameter über ihren Default auf) und nutzt ihn in `Load()` sowie
|
||||||
|
`LoadMissingTeachingTime` statt direkter `DateTime.Now`-Aufrufe. Die beiden betroffenen Tests
|
||||||
|
injizieren jetzt einen festen Referenzzeitpunkt (`FixedNow`, ein Dienstag im September — bewusst
|
||||||
|
gewählt, weil in diesem Monat kein bundesweiter oder länderspezifischer Feiertag liegt) und sind
|
||||||
|
damit unabhängig von der tatsächlichen Ausführungsuhrzeit deterministisch grün.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Sync & Server
|
## 10. Sync & Server
|
||||||
|
|||||||
Reference in New Issue
Block a user