feat: Pädagogische Klassen-Aufgaben, Prioritäten/Checklisten, Dashboard-Schnellzugriffe

Neue Aufgabenart über bestehende Felder (WorkTask.Kind=Reminder + GroupId) statt Vererbung,
mit Priorität (TaskPriority) und optionaler Abhak-Liste (ChecklistItems, wahlweise Kurs-
Roster oder Freitext). Kurs-Dashboard zeigt jetzt eine "Anstehende Aufgaben"-Karte samt
direktem Anlege-Button; derselbe Anlege-Einstieg wurde auch ins Hauptdashboard und (als
Schnellüberblick samt fehlender Übersicht/Mitarbeit-Buttons) in die Gruppenliste gezogen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 22:24:38 +02:00
co-authored by Claude Sonnet 5
parent e7c3fdd4ec
commit e65a729f97
25 changed files with 878 additions and 39 deletions
@@ -4,6 +4,7 @@ using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.ViewModels.Workload;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
@@ -36,6 +37,7 @@ public partial class GroupOverviewViewModel : ObservableObject
private readonly IParticipationRepository _entries;
private readonly IStudentRepository _students;
private readonly IDocumentationRepository _documentation;
private readonly IWorkTaskRepository _tasks;
private readonly AttendanceBalanceService _attendanceBalance;
private readonly SchoolYearService _schoolYear;
@@ -69,18 +71,46 @@ public partial class GroupOverviewViewModel : ObservableObject
/// 6 Planung, 7 Kompetenzen, 8 Dokumentation).
public Action<int>? OnNavigateToTab { get; set; }
/// Anders als OnNavigateToTab kein Tab innerhalb dieser Detailansicht, sondern ein Sprung in
/// den eigenständigen Top-Level-Aufgabenbereich (MainWindowViewModel.NavigateToWorkload) — von
/// hier aus nicht direkt aufrufbar (keine ViewModel-zu-ViewModel-Referenz, siehe
/// GroupDetailView.axaml.cs, gleiches Muster wie LessonViewerDialog/TeachingModeWindow).
public Action? OnNavigateToWorkload { get; set; }
[RelayCommand] private void NavigateToPlanning() => OnNavigateToTab?.Invoke(6);
[RelayCommand] private void NavigateToExams() => OnNavigateToTab?.Invoke(4);
[RelayCommand] private void NavigateToParticipation() => OnNavigateToTab?.Invoke(3);
[RelayCommand] private void NavigateToDocumentation() => OnNavigateToTab?.Invoke(8);
[RelayCommand] private void NavigateToWorkload() => OnNavigateToWorkload?.Invoke();
// ── Anstehende Aufgaben für diese Klasse (Nutzer-Feedback: pädagogische Erinnerungen/Aufgaben
// sollen "gleichberechtigt" auch im Kurs-Dashboard stehen, nicht nur im Hauptdashboard) ───────
private const int GroupTasksMaxCount = 5;
public ObservableCollection<GroupTaskItem> GroupTasks { get; } = [];
public bool HasGroupTasks => GroupTasks.Count > 0;
/// Öffnet den Aufgaben-Dialog mit dieser Gruppe vorbelegt (Nutzer-Feedback: direkter
/// Anlege-Einstieg aus dem Kurs-Dashboard, statt erst über "Zu den Aufgaben" springen zu
/// müssen). Gleiches View-Code-Behind-Delegate-Muster wie OnNavigateToWorkload.
public Func<Guid, Task<WorkTask?>>? OnAddGroupTask { get; set; }
[RelayCommand]
private async Task AddGroupTask()
{
if (OnAddGroupTask is null) return;
var result = await OnAddGroupTask(_groupId);
if (result is null) return;
_tasks.Save(result);
LoadGroupTasks(DateOnly.FromDateTime(DateTime.Today));
}
public GroupOverviewViewModel(ILessonRepository lessons, IExamRepository exams,
IParticipationSessionRepository sessions, IParticipationRepository entries,
IStudentRepository students, IDocumentationRepository documentation,
IStudentRepository students, IDocumentationRepository documentation, IWorkTaskRepository tasks,
AttendanceBalanceService attendanceBalance, SchoolYearService schoolYear)
{
_lessons = lessons; _exams = exams; _sessions = sessions;
_entries = entries; _students = students; _documentation = documentation;
_entries = entries; _students = students; _documentation = documentation; _tasks = tasks;
_attendanceBalance = attendanceBalance; _schoolYear = schoolYear;
}
@@ -102,6 +132,7 @@ public partial class GroupOverviewViewModel : ObservableObject
LoadDraftDocumentationCount();
LoadAttendanceNotices(today);
LoadMissingHomework();
LoadGroupTasks(today);
}
private void LoadNextLesson(DateOnly today)
@@ -260,6 +291,19 @@ public partial class GroupOverviewViewModel : ObservableObject
}
OnPropertyChanged(nameof(HasMissingHomework));
}
private void LoadGroupTasks(DateOnly today)
{
GroupTasks.Clear();
foreach (var t in _tasks.GetByGroup(_groupId)
.Where(t => t.Status != WorkTaskStatus.Done)
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(GroupTasksMaxCount))
GroupTasks.Add(new GroupTaskItem(t.Title, t.Kind == TaskKind.Reminder,
t.DueDate?.ToString("dd.MM.yyyy") ?? "",
t.DueDate is { } d && d < today, TaskPriorityDisplay.ColorHex(t.Priority),
t.Priority == TaskPriority.High));
OnPropertyChanged(nameof(HasGroupTasks));
}
}
public sealed class MissingHomeworkItem(string studentName, string statusLabel)
@@ -267,3 +311,14 @@ public sealed class MissingHomeworkItem(string studentName, string statusLabel)
public string StudentName { get; } = studentName;
public string StatusLabel { get; } = statusLabel;
}
public sealed class GroupTaskItem(string title, bool isReminder, string dueDateDisplay, bool isOverdue,
string priorityColorHex, bool isHighPriority)
{
public string Title { get; } = title;
public bool IsReminder { get; } = isReminder;
public string DueDateDisplay { get; } = dueDateDisplay;
public bool IsOverdue { get; } = isOverdue;
public string PriorityColorHex { get; } = priorityColorHex;
public bool IsHighPriority { get; } = isHighPriority;
}
@@ -4,6 +4,7 @@ using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.ViewModels.Workload;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
@@ -12,8 +13,13 @@ namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class GroupListViewModel : ObservableObject
{
private const int QuickTasksMaxCount = 3;
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private readonly ILessonRepository _lessons;
private readonly IExamRepository _exams;
private readonly IWorkTaskRepository _tasks;
public Action<Guid, int>? OnNavigateToDetail { get; set; }
public Func<Task>? OnAddGroup { get; set; }
@@ -39,9 +45,22 @@ public partial class GroupListViewModel : ObservableObject
public ObservableCollection<string> SchoolYears { get; } = [];
public ObservableCollection<GroupListItem> Groups { get; } = [];
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy)
// ── Schnellüberblick im Auswahl-Panel (Nutzer-Feedback: "oberhalb der Buttonliste ein paar
// Daten auswerfen. Nächste Stunde, nächste Arbeit, wichtige Todos") — bewusst dieselben
// kompakten Kennzahlen wie die obersten Karten des Kurs-Dashboards (GroupOverviewViewModel),
// hier nur ohne eigenen Tab-Wechsel, da man ohnehin schon auf der Gruppenliste steht.
[ObservableProperty] private bool _quickHasNextLesson;
[ObservableProperty] private string _quickNextLessonLabel = "";
[ObservableProperty] private bool _quickHasNextExam;
[ObservableProperty] private string _quickNextExamLabel = "";
public ObservableCollection<GroupTaskItem> QuickTasks { get; } = [];
public bool QuickHasTasks => QuickTasks.Count > 0;
public bool QuickHasAnything => QuickHasNextLesson || QuickHasNextExam || QuickHasTasks;
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy,
ILessonRepository lessons, IExamRepository exams, IWorkTaskRepository tasks)
{
_groups = groups; _subjects = subjects;
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
SelectedSchoolYear = sy.CurrentSchoolYear();
}
@@ -58,6 +77,44 @@ public partial class GroupListViewModel : ObservableObject
RollOverGroupCommand.NotifyCanExecuteChanged();
ToggleArchiveCommand.NotifyCanExecuteChanged();
DeleteGroupCommand.NotifyCanExecuteChanged();
LoadQuickInfo();
}
private void LoadQuickInfo()
{
QuickTasks.Clear();
if (SelectedGroup is null)
{
QuickHasNextLesson = false; QuickNextLessonLabel = "";
QuickHasNextExam = false; QuickNextExamLabel = "";
OnPropertyChanged(nameof(QuickHasTasks));
OnPropertyChanged(nameof(QuickHasAnything));
return;
}
var groupId = SelectedGroup.Id;
var today = DateOnly.FromDateTime(DateTime.Today);
var nextLesson = _lessons.GetByGroupAndRange(groupId, today, today.AddDays(90))
.Where(l => l.Status == LessonStatus.Planned)
.OrderBy(l => l.Date).ThenBy(l => l.LessonNumber ?? 0).FirstOrDefault();
QuickHasNextLesson = nextLesson is not null;
QuickNextLessonLabel = nextLesson is null ? ""
: string.IsNullOrWhiteSpace(nextLesson.Topic)
? nextLesson.Date.ToString("dd.MM.yyyy")
: $"{nextLesson.Date:dd.MM.yyyy} — {nextLesson.Topic}";
var nextExam = _exams.GetByGroup(groupId).Where(e => e.Date >= today).MinBy(e => e.Date);
QuickHasNextExam = nextExam is not null;
QuickNextExamLabel = nextExam is null ? "" : $"{nextExam.Date:dd.MM.yyyy} — {nextExam.Title}";
foreach (var t in _tasks.GetByGroup(groupId).Where(t => t.Status != WorkTaskStatus.Done)
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(QuickTasksMaxCount))
QuickTasks.Add(new GroupTaskItem(t.Title, t.Kind == TaskKind.Reminder,
t.DueDate?.ToString("dd.MM.yyyy") ?? "", t.DueDate is { } d && d < today,
TaskPriorityDisplay.ColorHex(t.Priority), t.Priority == TaskPriority.High));
OnPropertyChanged(nameof(QuickHasTasks));
OnPropertyChanged(nameof(QuickHasAnything));
}
public void LoadGroups()