diff --git a/LehrerApp.Core/Services/DashboardSettingsService.cs b/LehrerApp.Core/Services/DashboardSettingsService.cs
new file mode 100644
index 0000000..14fda94
--- /dev/null
+++ b/LehrerApp.Core/Services/DashboardSettingsService.cs
@@ -0,0 +1,62 @@
+using System.Text.Json;
+
+namespace LehrerApp.Core.Services;
+
+public sealed class DashboardCardSetting
+{
+ public string Key { get; set; } = "";
+ public bool IsVisible { get; set; } = true;
+ public int Order { get; set; }
+}
+
+/// Speichert Sichtbarkeit und Reihenfolge der Dashboard-Kacheln lokal.
+public sealed class DashboardSettingsService
+{
+ public static readonly string[] DefaultCardOrder =
+ [
+ "today", "tasks", "calendar", "excuses", "upcoming",
+ "corrections", "alerts", "attendance", "support", "groups",
+ ];
+
+ private readonly string _configPath;
+
+ public DashboardSettingsService(string appDataPath) =>
+ _configPath = Path.Combine(appDataPath, "dashboardsettings.json");
+
+ public List Load()
+ {
+ try
+ {
+ if (File.Exists(_configPath))
+ {
+ var saved = JsonSerializer.Deserialize>(
+ File.ReadAllText(_configPath)) ?? [];
+ var byKey = saved
+ .Where(s => DefaultCardOrder.Contains(s.Key))
+ .GroupBy(s => s.Key).ToDictionary(g => g.Key, g => g.First());
+ return DefaultCardOrder.Select((key, defaultOrder) => byKey.TryGetValue(key, out var item)
+ ? new DashboardCardSetting { Key = key, IsVisible = item.IsVisible, Order = item.Order }
+ : new DashboardCardSetting { Key = key, IsVisible = true, Order = defaultOrder })
+ .OrderBy(s => s.Order).ThenBy(s => Array.IndexOf(DefaultCardOrder, s.Key))
+ .Select((s, index) => new DashboardCardSetting
+ { Key = s.Key, IsVisible = s.IsVisible, Order = index })
+ .ToList();
+ }
+ }
+ catch { /* beschädigte Konfiguration -> Standardreihenfolge */ }
+
+ return DefaultCardOrder.Select((key, index) => new DashboardCardSetting
+ { Key = key, IsVisible = true, Order = index }).ToList();
+ }
+
+ public void Save(IEnumerable settings)
+ {
+ var normalized = settings.Select((s, index) => new DashboardCardSetting
+ {
+ Key = s.Key,
+ IsVisible = s.IsVisible,
+ Order = index,
+ }).ToList();
+ File.WriteAllText(_configPath, JsonSerializer.Serialize(normalized));
+ }
+}
diff --git a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
index 5205dca..e54e76a 100644
--- a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
@@ -18,17 +18,31 @@ public sealed class DashboardViewModelTests
return new PeriodScheduleService(tempPath);
}
+ private static DashboardSettingsService NewDashboardSettings()
+ {
+ var tempPath = System.IO.Path.Combine(
+ System.IO.Path.GetTempPath(), $"lehrerapp-dashboardsettings-tests-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(tempPath);
+ return new DashboardSettingsService(tempPath);
+ }
+
private static DashboardViewModel BuildVm(LearningGroup group, Lesson lesson,
- FakeTimetableSlots? slots = null, PeriodScheduleService? periodSchedule = null)
+ FakeTimetableSlots? slots = null, PeriodScheduleService? periodSchedule = null,
+ FakeExams? exams = null, FakeResults? results = null, FakeGrades? grades = null,
+ FakeReportGrades? reportGrades = null, FakeMemberships? memberships = null,
+ FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
+ DashboardSettingsService? dashboardSettings = null)
{
var lessons = new FakeLessons();
lessons.Add(lesson);
return new DashboardViewModel(
new FakeGroups([group]), new FakeSubjects([]), lessons,
- new FakeExams([]), new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
- new FakeStudents([]), new FakeDocumentation(),
+ exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
+ reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
+ tasks ?? new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
+ students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
- new AttendanceBalanceService(), new SchoolYearService());
+ new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings());
}
[Fact]
@@ -108,4 +122,109 @@ public sealed class DashboardViewModelTests
Assert.Equal(group.Id, navigatedTo);
}
+
+ [Fact]
+ public void UpcomingDates_BuendeltKlausurenFristenUndFoerderplanPruefungen()
+ {
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var group = new LearningGroup { Name = "9c" };
+ var exams = new FakeExams([
+ new Exam { GroupId = group.Id, Title = "Chemie-Test", Date = today.AddDays(5) },
+ ]);
+ var tasks = new FakeWorkTasks();
+ tasks.Add(new WorkTask { Title = "Notenschluss", DueDate = today.AddDays(10) });
+ var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
+ var documentation = new FakeDocumentation();
+ documentation.Add(new Documentation
+ {
+ StudentId = student.Id, Type = DocumentationType.SupportPlan, Title = "Leseförderung",
+ SupportData = new SupportData { Status = SupportStatus.Active, ReviewDate = today.AddDays(3) },
+ });
+
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams,
+ tasks: tasks, students: new FakeStudents([student]), documentation: documentation);
+
+ Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.Exam && i.Title == "Chemie-Test");
+ Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.Deadline && i.Title == "Notenschluss");
+ Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.SupportPlan && i.StudentId == student.Id);
+ }
+
+ [Fact]
+ public void OpenCorrections_ZeigtBewertungsfortschrittJeKlausur()
+ {
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var group = new LearningGroup { Name = "9c" };
+ var exam = new Exam
+ { GroupId = group.Id, Title = "Klausur 1", Date = today.AddDays(-2), Status = ExamStatus.Conducted };
+ var anna = new Student { FirstName = "Anna", LastName = "A" };
+ var ben = new Student { FirstName = "Ben", LastName = "B" };
+ var memberships = new FakeMemberships([
+ new GroupMembership { GroupId = group.Id, StudentId = anna.Id },
+ new GroupMembership { GroupId = group.Id, StudentId = ben.Id },
+ ]);
+ var results = new FakeResults();
+ results.Add(new ExamResult { ExamId = exam.Id, StudentId = anna.Id, Grade = "2" });
+
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
+ exams: new FakeExams([exam]), results: results, memberships: memberships,
+ students: new FakeStudents([anna, ben]));
+
+ var correction = Assert.Single(vm.OpenCorrections);
+ Assert.Equal(1, correction.Completed);
+ Assert.Equal(2, correction.Total);
+ Assert.Equal(50, correction.Percent);
+ }
+
+ [Fact]
+ public void Alerts_ErkenntDeutlichenNotenabfall()
+ {
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var group = new LearningGroup { Name = "9c", GradingSystem = GradingSystem.Grades1To6 };
+ var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
+ var memberships = new FakeMemberships([
+ new GroupMembership { GroupId = group.Id, StudentId = student.Id },
+ ]);
+ var grades = new FakeGrades();
+ grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-20), Value = "2" });
+ grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-15), Value = "2" });
+ grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-10), Value = "4" });
+ grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-5), Value = "4" });
+
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, grades: grades,
+ memberships: memberships, students: new FakeStudents([student]));
+
+ Assert.Contains(vm.Alerts, a => a.StudentId == student.Id && a.KindLabel == "Notenabfall");
+ }
+
+ [Fact]
+ public void Kalenderauswahl_ZeigtTermineDesGeklicktenTages()
+ {
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var group = new LearningGroup { Name = "9c" };
+ var examDate = today.AddDays(2);
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
+ exams: new FakeExams([new Exam { GroupId = group.Id, Title = "Test", Date = examDate }]));
+ var day = vm.CalendarDays.Single(d => d.Date == examDate);
+
+ vm.SelectCalendarDayCommand.Execute(day);
+
+ Assert.True(day.IsSelected);
+ Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test");
+ }
+
+ [Fact]
+ public void DashboardKacheln_SichtbarkeitUndReihenfolgeWerdenGespeichert()
+ {
+ var settings = NewDashboardSettings();
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var group = new LearningGroup { Name = "9c" };
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, dashboardSettings: settings);
+ vm.TodayCard.IsVisible = false;
+ vm.MoveCardDownCommand.Execute(vm.TasksCard);
+
+ var saved = settings.Load();
+
+ Assert.False(saved.Single(c => c.Key == "today").IsVisible);
+ Assert.True(saved.FindIndex(c => c.Key == "tasks") > 1);
+ }
}
diff --git a/LehrerApp.Desktop/App.axaml.cs b/LehrerApp.Desktop/App.axaml.cs
index ae2149f..5c25180 100644
--- a/LehrerApp.Desktop/App.axaml.cs
+++ b/LehrerApp.Desktop/App.axaml.cs
@@ -99,7 +99,8 @@ public class App : Application
var dash = Services.GetRequiredService();
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
dash.OnNavigateToStudent = id => main.NavigateToStudent(id);
- dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 2); // Tab "Mitarbeit"
+ dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 3); // Tab "Mitarbeit"
+ dash.OnNavigateToExam = id => main.NavigateToGroupDetail(id, 4); // Tab "Klausuren"
// StudentList → StudentDetail + Anlegen
var sl = Services.GetRequiredService();
@@ -109,7 +110,7 @@ public class App : Application
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
var timetable = Services.GetRequiredService();
timetable.OnNavigateToSettings = () => main.NavigateToSettings(7);
- timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5);
+ timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 6);
}
private static async Task ShowAddStudentDialog()
diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs
index f7bcdff..24af746 100644
--- a/LehrerApp.Desktop/AppBootstrapper.cs
+++ b/LehrerApp.Desktop/AppBootstrapper.cs
@@ -154,6 +154,7 @@ public static class AppBootstrapper
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
services.AddSingleton(_ => new PeriodScheduleService(appData));
services.AddSingleton(_ => new WorkloadSettingsService(appData));
+ services.AddSingleton(_ => new DashboardSettingsService(appData));
services.AddSingleton(_ => new LetterTemplateService(appData));
// ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ──────
diff --git a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
index f41d980..b7552cd 100644
--- a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
@@ -16,6 +16,10 @@ public partial class DashboardViewModel : ObservableObject
private readonly ISubjectRepository _subjects;
private readonly ILessonRepository _lessons;
private readonly IExamRepository _exams;
+ private readonly IExamResultRepository _examResults;
+ private readonly IGradeRepository _grades;
+ private readonly IReportGradeRepository _reportGrades;
+ private readonly IGroupMembershipRepository _memberships;
private readonly IWorkTaskRepository _tasks;
private readonly IParticipationSessionRepository _participationSessions;
private readonly IParticipationRepository _participationEntries;
@@ -25,14 +29,18 @@ public partial class DashboardViewModel : ObservableObject
private readonly PeriodScheduleService _periodSchedule;
private readonly AttendanceBalanceService _attendanceBalance;
private readonly SchoolYearService _sy;
+ private readonly DashboardSettingsService _dashboardSettings;
private const int OpenExcuseMaxAgeDays = 21;
private const int SupportPlanDueWithinDays = 14;
+ private const int UpcomingWithinDays = 30;
[ObservableProperty] private string _greeting = "";
[ObservableProperty] private string _currentDate = "";
[ObservableProperty] private string _currentSchoolYear = "";
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
+ [ObservableProperty] private string _selectedDayLabel = "";
+ [ObservableProperty] private bool _isDashboardSettingsOpen;
public string CalendarMonthLabel => CalendarMonth.ToString("MMMM yyyy", De);
@@ -43,6 +51,11 @@ public partial class DashboardViewModel : ObservableObject
public ObservableCollection OpenExcuses { get; } = [];
public ObservableCollection AttendanceWarnings { get; } = [];
public ObservableCollection SupportPlanReviews { get; } = [];
+ public ObservableCollection UpcomingDates { get; } = [];
+ public ObservableCollection OpenCorrections { get; } = [];
+ public ObservableCollection Alerts { get; } = [];
+ public ObservableCollection SelectedDayEvents { get; } = [];
+ public ObservableCollection DashboardCards { get; } = [];
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
// Navigation-Callback – wird von App.axaml.cs verdrahtet
@@ -52,21 +65,40 @@ public partial class DashboardViewModel : ObservableObject
// OnNavigateToGroup (Lerngruppen-Kacheln, Tab "Übersicht"), da der Sprung von einer konkreten
// Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt.
public Action? OnNavigateToLesson { get; set; }
+ public Action? OnNavigateToExam { get; set; }
+
+ public DashboardCardOption TodayCard => Card("today");
+ public DashboardCardOption TasksCard => Card("tasks");
+ public DashboardCardOption CalendarCard => Card("calendar");
+ public DashboardCardOption ExcusesCard => Card("excuses");
+ public DashboardCardOption UpcomingCard => Card("upcoming");
+ public DashboardCardOption CorrectionsCard => Card("corrections");
+ public DashboardCardOption AlertsCard => Card("alerts");
+ public DashboardCardOption AttendanceCard => Card("attendance");
+ public DashboardCardOption SupportCard => Card("support");
+ public DashboardCardOption GroupsCard => Card("groups");
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
- IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
+ IExamRepository exams, IExamResultRepository examResults, IGradeRepository grades,
+ IReportGradeRepository reportGrades, IGroupMembershipRepository memberships,
+ IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
IParticipationRepository participationEntries, IStudentRepository students,
IDocumentationRepository documentation, ITimetableSlotRepository timetableSlots,
- PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy)
+ PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy,
+ DashboardSettingsService dashboardSettings)
{
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
+ _examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
_participationSessions = participationSessions; _participationEntries = participationEntries;
_students = students; _documentation = documentation;
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
- _attendanceBalance = attendanceBalance; _sy = sy;
+ _attendanceBalance = attendanceBalance; _sy = sy; _dashboardSettings = dashboardSettings;
+ LoadDashboardCards();
Load();
}
+ private DashboardCardOption Card(string key) => DashboardCards.First(c => c.Key == key);
+
private static DateOnly FirstOfMonth(DateTime d) => new(d.Year, d.Month, 1);
private void Load()
@@ -123,6 +155,9 @@ public partial class DashboardViewModel : ObservableObject
LoadOpenExcuses(groups.Values.ToList(), today);
LoadAttendanceWarnings(today);
LoadSupportPlanReviews(today);
+ LoadUpcomingDates(groups, today);
+ LoadOpenCorrections(groups, today);
+ LoadAlerts(groups, today);
}
// ── Fehlzeiten-Warnung (5.2.3) ────────────────────────────────────────────
@@ -173,6 +208,174 @@ public partial class DashboardViewModel : ObservableObject
}
}
+ // ── Anstehende Termine (9.3) ─────────────────────────────────────────────
+
+ private void LoadUpcomingDates(IReadOnlyDictionary groups, DateOnly today)
+ {
+ UpcomingDates.Clear();
+ var dueBy = today.AddDays(UpcomingWithinDays);
+ var items = new List();
+
+ foreach (var group in groups.Values)
+ foreach (var exam in _exams.GetByGroup(group.Id)
+ .Where(e => e.Date >= today && e.Date <= dueBy && e.Status == ExamStatus.Planned))
+ items.Add(new UpcomingDateItem(UpcomingDateKind.Exam, exam.Date, exam.Title,
+ group.Name, group.Id, null, today));
+
+ foreach (var task in _tasks.GetByStatus(WorkTaskStatus.Open)
+ .Concat(_tasks.GetByStatus(WorkTaskStatus.InProgress))
+ .Where(t => t.DueDate.HasValue && t.DueDate.Value <= dueBy))
+ items.Add(new UpcomingDateItem(UpcomingDateKind.Deadline, task.DueDate!.Value,
+ task.Title, task.GroupId is Guid groupId && groups.TryGetValue(groupId, out var group)
+ ? group.Name : "Aufgabe", task.GroupId, null, today));
+
+ foreach (var doc in _documentation.GetAll()
+ .Where(d => d.Type == DocumentationType.SupportPlan
+ && d.SupportData is { Status: SupportStatus.Active, ReviewDate: not null }
+ && d.SupportData.ReviewDate.Value <= dueBy))
+ {
+ var student = _students.GetById(doc.StudentId);
+ if (student is not null)
+ items.Add(new UpcomingDateItem(UpcomingDateKind.SupportPlan,
+ doc.SupportData!.ReviewDate!.Value, doc.Title, student.FullName,
+ doc.GroupId, doc.StudentId, today));
+ }
+
+ foreach (var item in items.OrderBy(i => i.Date).ThenBy(i => i.Title).Take(8))
+ UpcomingDates.Add(item);
+ }
+
+ // ── Offene Korrekturen (9.4) ─────────────────────────────────────────────
+
+ private void LoadOpenCorrections(IReadOnlyDictionary groups, DateOnly today)
+ {
+ OpenCorrections.Clear();
+ foreach (var group in groups.Values)
+ foreach (var exam in _exams.GetByGroup(group.Id)
+ .Where(e => e.Status is ExamStatus.Conducted or ExamStatus.Graded)
+ .OrderBy(e => e.Date))
+ {
+ var expected = _memberships.GetByGroup(group.Id)
+ .Count(m => GroupMembershipService.IsActiveOn(m, exam.Date));
+ var evaluated = _examResults.GetByExam(exam.Id)
+ .Count(r => r.Absent || !string.IsNullOrWhiteSpace(r.Grade) || r.Points.Count > 0);
+ OpenCorrections.Add(new CorrectionProgressItem(exam.Id, group.Id, exam.Title,
+ group.Name, exam.Date, Math.Min(evaluated, expected), expected, today));
+ }
+ }
+
+ // ── Auffälligkeiten (9.5) ────────────────────────────────────────────────
+
+ private void LoadAlerts(IReadOnlyDictionary groups, DateOnly today)
+ {
+ Alerts.Clear();
+ foreach (var warning in AttendanceWarnings)
+ Alerts.Add(new DashboardAlertItem(warning.StudentId, null, warning.StudentName,
+ "Fehlzeiten", $"Fehlzeitenquote {warning.AbsenceRatePercent:0.#} %", AlertSeverity.High));
+
+ foreach (var group in groups.Values)
+ {
+ var groupReportGrades = _reportGrades.GetByGroup(group.Id);
+ foreach (var membership in _memberships.GetByGroup(group.Id)
+ .Where(m => GroupMembershipService.IsActiveOn(m, today)))
+ {
+ var student = _students.GetById(membership.StudentId);
+ if (student is null) continue;
+ var values = _grades.GetByStudentAndGroup(student.Id, group.Id)
+ .OrderBy(g => g.Date)
+ .Select(g => int.TryParse(g.Value, out var value) ? (int?)value : null)
+ .Where(v => v.HasValue).Select(v => v!.Value).ToList();
+
+ if (values.Count >= 4)
+ {
+ var previous = values.TakeLast(4).Take(2).Average();
+ var recent = values.TakeLast(2).Average();
+ var declined = group.GradingSystem == GradingSystem.Grades1To6
+ ? recent - previous >= 1.0
+ : previous - recent >= 3.0;
+ if (declined)
+ Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
+ "Notenabfall", $"{group.Name}: zuletzt {recent:0.0}, zuvor {previous:0.0}",
+ AlertSeverity.Medium));
+ }
+
+ var latestReport = groupReportGrades
+ .Where(r => r.StudentId == student.Id)
+ .OrderByDescending(r => r.UpdatedAt).FirstOrDefault();
+ var effective = latestReport?.OverrideValue ?? latestReport?.CalculatedValue;
+ if (int.TryParse(effective, out var reportValue)
+ && (group.GradingSystem == GradingSystem.Grades1To6 ? reportValue >= 5 : reportValue <= 4))
+ Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
+ "Versetzungsgefährdung", $"{group.Name}: aktueller Stand {reportValue}",
+ AlertSeverity.High));
+ }
+ }
+ }
+
+ // ── Konfigurierbare Kacheln (9.6) ────────────────────────────────────────
+
+ private void LoadDashboardCards()
+ {
+ DashboardCards.Clear();
+ foreach (var setting in _dashboardSettings.Load())
+ {
+ var option = new DashboardCardOption(setting.Key, CardTitle(setting.Key), setting.IsVisible);
+ option.OnVisibilityChanged = SaveAndApplyCardLayout;
+ DashboardCards.Add(option);
+ }
+ ApplyCardLayout();
+ }
+
+ private static string CardTitle(string key) => key switch
+ {
+ "today" => "Heute", "tasks" => "Offene Aufgaben", "calendar" => "Kalender",
+ "excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
+ "corrections" => "Offene Korrekturen", "alerts" => "Auffälligkeiten",
+ "attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
+ "groups" => "Meine Lerngruppen", _ => key,
+ };
+
+ private void ApplyCardLayout()
+ {
+ var visibleIndex = 0;
+ foreach (var card in DashboardCards)
+ {
+ var index = card.IsVisible ? visibleIndex++ : 0;
+ card.Row = index / 2;
+ card.Column = index % 2;
+ }
+ }
+
+ private void SaveAndApplyCardLayout()
+ {
+ ApplyCardLayout();
+ _dashboardSettings.Save(DashboardCards.Select((c, i) => new DashboardCardSetting
+ { Key = c.Key, IsVisible = c.IsVisible, Order = i }));
+ }
+
+ [RelayCommand] private void ToggleDashboardSettings() =>
+ IsDashboardSettingsOpen = !IsDashboardSettingsOpen;
+
+ [RelayCommand]
+ private void MoveCardUp(DashboardCardOption? card)
+ {
+ if (card is null) return;
+ var index = DashboardCards.IndexOf(card);
+ if (index <= 0) return;
+ DashboardCards.Move(index, index - 1);
+ SaveAndApplyCardLayout();
+ }
+
+ [RelayCommand]
+ private void MoveCardDown(DashboardCardOption? card)
+ {
+ if (card is null) return;
+ var index = DashboardCards.IndexOf(card);
+ if (index < 0 || index >= DashboardCards.Count - 1) return;
+ DashboardCards.Move(index, index + 1);
+ SaveAndApplyCardLayout();
+ }
+
[RelayCommand] private void OpenStudentAttendance(AttendanceWarningItem? item)
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
@@ -240,8 +443,8 @@ public partial class DashboardViewModel : ObservableObject
var agg = Agg(lesson.Date);
agg.HasLesson = true;
if (g.IsOwnClass) agg.IsOwnClassDay = true;
- agg.Details.Add($"Unterricht: {g.Name}" +
- (string.IsNullOrWhiteSpace(lesson.Topic) ? "" : $" – {lesson.Topic}"));
+ agg.Details.Add(new CalendarEventItem(CalendarEventKind.Lesson, lesson.Date,
+ g.Name, lesson.Topic, g.Id));
}
foreach (var exam in _exams.GetByGroup(g.Id).Where(e => e.Date >= gridStart && e.Date <= gridEnd))
@@ -249,7 +452,8 @@ public partial class DashboardViewModel : ObservableObject
var agg = Agg(exam.Date);
agg.HasExam = true;
if (g.IsOwnClass) agg.IsOwnClassDay = true;
- agg.Details.Add($"Klausur: {exam.Title} ({g.Name})");
+ agg.Details.Add(new CalendarEventItem(CalendarEventKind.Exam, exam.Date,
+ exam.Title, g.Name, g.Id));
}
}
@@ -261,6 +465,26 @@ public partial class DashboardViewModel : ObservableObject
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.IsOwnClassDay ?? false,
agg?.Details ?? []));
}
+ SelectCalendarDay(CalendarDays.FirstOrDefault(d => d.Date == today && d.IsCurrentMonth)
+ ?? CalendarDays.First(d => d.IsCurrentMonth));
+ }
+
+ [RelayCommand]
+ private void SelectCalendarDay(CalendarDayCell? day)
+ {
+ if (day is null) return;
+ foreach (var cell in CalendarDays) cell.IsSelected = cell == day;
+ SelectedDayLabel = day.Date.ToString("dddd, d. MMMM", De);
+ SelectedDayEvents.Clear();
+ foreach (var item in day.Events) SelectedDayEvents.Add(item);
+ }
+
+ [RelayCommand]
+ private void OpenCalendarEvent(CalendarEventItem? item)
+ {
+ if (item is null) return;
+ if (item.Kind == CalendarEventKind.Exam) OnNavigateToExam?.Invoke(item.GroupId);
+ else OnNavigateToLesson?.Invoke(item.GroupId);
}
[RelayCommand]
@@ -289,6 +513,19 @@ public partial class DashboardViewModel : ObservableObject
[RelayCommand] private void OpenGroup(GroupChip? c) { if (c is not null) OnNavigateToGroup?.Invoke(c.GroupId); }
[RelayCommand] private void OpenLesson(LessonItem? l) { if (l is not null) OnNavigateToLesson?.Invoke(l.GroupId); }
+ [RelayCommand] private void OpenUpcomingDate(UpcomingDateItem? item)
+ {
+ if (item?.StudentId is Guid studentId) OnNavigateToStudent?.Invoke(studentId);
+ else if (item?.GroupId is Guid groupId)
+ {
+ if (item.Kind == UpcomingDateKind.Exam) OnNavigateToExam?.Invoke(groupId);
+ else OnNavigateToGroup?.Invoke(groupId);
+ }
+ }
+ [RelayCommand] private void OpenCorrection(CorrectionProgressItem? item)
+ { if (item is not null) OnNavigateToExam?.Invoke(item.GroupId); }
+ [RelayCommand] private void OpenAlert(DashboardAlertItem? item)
+ { if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
[RelayCommand] private void Refresh() => Load();
private class DayAgg
@@ -296,7 +533,7 @@ public partial class DashboardViewModel : ObservableObject
public bool HasLesson;
public bool HasExam;
public bool IsOwnClassDay;
- public List Details { get; } = [];
+ public List Details { get; } = [];
}
}
@@ -369,8 +606,10 @@ public class SupportPlanDueItem
}
}
-public class CalendarDayCell
+public partial class CalendarDayCell : ObservableObject
{
+ [ObservableProperty] private bool _isSelected;
+ public DateOnly Date { get; }
public int DayNumber { get; }
public bool IsCurrentMonth { get; }
public bool IsToday { get; }
@@ -378,16 +617,103 @@ public class CalendarDayCell
public bool HasExam { get; }
public bool IsOwnClassDay { get; }
public string Tooltip { get; }
+ public IReadOnlyList Events { get; }
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
- bool hasLesson, bool hasExam, bool isOwnClassDay, List details)
+ bool hasLesson, bool hasExam, bool isOwnClassDay, List details)
{
+ Date = date;
DayNumber = date.Day;
IsCurrentMonth = isCurrentMonth;
IsToday = isToday;
HasLesson = hasLesson;
HasExam = hasExam;
IsOwnClassDay = isOwnClassDay;
- Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy") : string.Join("\n", details);
+ Events = details;
+ Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy")
+ : string.Join("\n", details.Select(d => $"{d.KindLabel}: {d.Title}"));
}
}
+
+public enum CalendarEventKind { Lesson, Exam }
+
+public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, string title,
+ string subtitle, Guid groupId)
+{
+ public CalendarEventKind Kind { get; } = kind;
+ public DateOnly Date { get; } = date;
+ public string Title { get; } = title;
+ public string Subtitle { get; } = subtitle;
+ public Guid GroupId { get; } = groupId;
+ public string KindLabel => Kind == CalendarEventKind.Exam ? "Klausur" : "Unterricht";
+}
+
+public enum UpcomingDateKind { Exam, SupportPlan, Deadline }
+
+public sealed class UpcomingDateItem(UpcomingDateKind kind, DateOnly date, string title,
+ string subtitle, Guid? groupId, Guid? studentId, DateOnly today)
+{
+ public UpcomingDateKind Kind { get; } = kind;
+ public DateOnly Date { get; } = date;
+ public string Title { get; } = title;
+ public string Subtitle { get; } = subtitle;
+ public Guid? GroupId { get; } = groupId;
+ public Guid? StudentId { get; } = studentId;
+ public bool IsOverdue { get; } = date < today;
+ public string DateDisplay => Date.ToString("dd.MM.");
+ public string KindLabel => Kind switch
+ {
+ UpcomingDateKind.Exam => "Klausur",
+ UpcomingDateKind.SupportPlan => "Förderplan",
+ _ => "Frist",
+ };
+}
+
+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 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
+{
+ [ObservableProperty] private bool _isVisible;
+ [ObservableProperty] private int _row;
+ [ObservableProperty] private int _column;
+ public string Key { get; }
+ public string Title { get; }
+ public Action? OnVisibilityChanged { get; set; }
+
+ public DashboardCardOption(string key, string title, bool isVisible)
+ {
+ Key = key;
+ Title = title;
+ _isVisible = isVisible;
+ }
+
+ partial void OnIsVisibleChanged(bool value) => OnVisibilityChanged?.Invoke();
+}
diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
index affe294..1c70d9c 100644
--- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
+++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
@@ -9,21 +9,56 @@
+
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -63,7 +98,8 @@
-
@@ -88,7 +124,8 @@
-
@@ -150,20 +187,24 @@
-
-
-
-
-
-
+
@@ -183,11 +224,35 @@
+
+
+
+
+
+
+
+
+
+
+
-
@@ -220,7 +285,8 @@
-
@@ -249,7 +315,8 @@
-
@@ -278,8 +345,113 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/GroupListView.axaml b/LehrerApp.Desktop/Views/Groups/GroupListView.axaml
index 74c6853..fbd32a6 100644
--- a/LehrerApp.Desktop/Views/Groups/GroupListView.axaml
+++ b/LehrerApp.Desktop/Views/Groups/GroupListView.axaml
@@ -117,31 +117,36 @@
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="1"/>
+
+ CommandParameter="4"/>
+ CommandParameter="5"/>
+ CommandParameter="6"/>
+ CommandParameter="7"/>
+ CommandParameter="8"/>
diff --git a/TODO.md b/TODO.md
index 0e3f09d..17e6a1d 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1260,20 +1260,36 @@ Hervorhebung "eigene Klasse" über `LearningGroup.IsOwnClass`, feste Kartenbreit
Stunde, für den vollständigen Tagesüberblick bleibt der Stundenplan zuständig.
- [x] **9.2** Direkter Absprung von einer Stunde in Mitarbeitserfassung bzw. Stundenplanung.
**Umsetzung:** Klick auf eine Stunde in "Heute" springt in die Lerngruppe, Tab "Mitarbeit"
- (`DashboardViewModel.OnNavigateToLesson`, `NavigateToGroupDetail(id, 2)`) — bewusst anderes
+ (`DashboardViewModel.OnNavigateToLesson`, `NavigateToGroupDetail(id, 3)`) — bewusst anderes
Sprungziel als der bereits bestehende Klick in der Stundenplan-eigenen "Heute"-Ansicht
(springt dort auf Tab "Planung", siehe 4.4.2): vom Dashboard aus ist der naheliegende nächste
Schritt morgens eher die Mitarbeitserfassung als die Planung.
-- [ ] **9.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen.
-- [ ] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
-- [ ] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.
-- [ ] **9.6** Dashboard-Kacheln ein-/ausblendbar und in der Reihenfolge konfigurierbar.
-- [ ] **9.7** Automatische Aktualisierung beim Zurücknavigieren (aktuell nur manueller Refresh).
-- [ ] **9.8** Kalender-Detailansicht: Tag im Monatskalender anklickbar/auswählbar, zeigt in
+- [x] **9.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen.
+ Bündelt geplante Klausuren, aktive Förderplan-Wiedervorlagen und offene Aufgaben mit
+ Fälligkeitsdatum für die nächsten 30 Tage; überfällige Einträge bleiben sichtbar und alle
+ Einträge springen zur passenden Lerngruppe bzw. zum Schüler.
+- [x] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
+ Durchgeführte und noch nicht zurückgegebene Klausuren zeigen den Fortschritt als Zahl und
+ Balken (`bewertete Arbeiten / am Klausurtag aktive Gruppenmitglieder`) und führen direkt in
+ den Klausuren-Tab der Lerngruppe.
+- [x] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.
+ Fehlzeiten nutzen den bestehenden konfigurierten Schwellenwert. Ein Notenabfall wird beim
+ Vergleich der letzten zwei mit den beiden vorherigen Einzelnoten erkannt (mindestens eine
+ Notenstufe bzw. drei Punkte); Versetzungsgefährdung basiert auf dem jüngsten gespeicherten
+ Zeugnisnotenstand (Note 5/6 bzw. höchstens 4 Punkte). Klick öffnet den betroffenen Schüler.
+- [x] **9.6** Dashboard-Kacheln ein-/ausblendbar und in der Reihenfolge konfigurierbar.
+ „Dashboard anpassen“ bietet für jede Kachel Sichtbarkeit sowie Hoch-/Runter-Sortierung;
+ die Konfiguration wird lokal in `dashboardsettings.json` gespeichert und das Raster ohne
+ Lücken neu angeordnet.
+- [x] **9.7** Automatische Aktualisierung beim Zurücknavigieren.
+ `MainWindowViewModel.GetDashboard()` führt beim Wechsel zurück zum Dashboard bereits den
+ `RefreshCommand` aus; damit werden Termine, Korrekturstände und Auffälligkeiten neu geladen.
+- [x] **9.8** Kalender-Detailansicht: Tag im Monatskalender anklickbar/auswählbar, zeigt in
einem angrenzenden Feld die Termine dieses Tages (Unterricht, Klausuren, perspektivisch
Konferenzen/Sondertermine) mit Details und Sprungmöglichkeit in die jeweilige Ansicht.
- Sinnvoll erst, wenn weitere Terminarten existieren — Abhängigkeit zu Kapitel 4
- (Unterrichtsplanung/Termine) sowie ggf. einem neuen Termine-Modell.
+ Unterricht und Klausuren werden jetzt im Detailbereich der Kalenderkachel angezeigt und
+ verlinkt. Weitere Terminarten können später über das vorhandene `CalendarEventItem` ergänzt
+ werden, sobald dafür ein eigenes Termine-Modell existiert.
---