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
@@ -57,6 +57,30 @@ public static class TaskCategoryDisplay
Enum.GetValues<TaskCategory>().FirstOrDefault(c => Label(c) == label, TaskCategory.Other);
}
public static class TaskPriorityDisplay
{
public static string Label(TaskPriority p) => p switch
{
TaskPriority.Low => "Niedrig",
TaskPriority.Normal => "Normal",
TaskPriority.High => "Hoch",
_ => p.ToString(),
};
public static string ColorHex(TaskPriority p) => p switch
{
TaskPriority.Low => "#9E9E9E",
TaskPriority.Normal => "#1976D2",
TaskPriority.High => "#D32F2F",
_ => "#9E9E9E",
};
public static string[] Options { get; } = Enum.GetValues<TaskPriority>().Select(Label).ToArray();
public static TaskPriority FromLabel(string? label) =>
Enum.GetValues<TaskPriority>().FirstOrDefault(p => Label(p) == label, TaskPriority.Normal);
}
public static class TaskRecurrenceDisplay
{
public static string Label(TaskRecurrence r) => r switch
@@ -218,6 +242,7 @@ public partial class WorkTaskListViewModel : ObservableObject
EstimatedMinutes = item.Model.EstimatedMinutes,
Recurrence = item.Model.Recurrence,
Kind = item.Model.Kind,
Priority = item.Model.Priority,
Notes = item.Model.Notes,
Status = WorkTaskStatus.Open,
});
@@ -250,6 +275,17 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut
public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status);
public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : "";
// Priorität (Nutzer-Feedback: pädagogische Erinnerungen farblich nach Dringlichkeit absetzen) —
// nur bei High überhaupt anzeigen, Normal/Low sollen nicht zusätzlich "schreien".
public bool ShowPriority => Model.Priority == TaskPriority.High;
public string PriorityLabel => TaskPriorityDisplay.Label(Model.Priority);
public string PriorityColorHex => TaskPriorityDisplay.ColorHex(Model.Priority);
// Abhak-Liste (Nutzer-Feedback: Namensliste/Teil-Punkte optional zuschaltbar).
public bool HasChecklist => Model.ChecklistItems.Count > 0;
public string ChecklistProgressDisplay => Model.ChecklistItems.Count == 0 ? ""
: $"{Model.ChecklistItems.Count(c => c.IsDone)} / {Model.ChecklistItems.Count} erledigt";
// Wiederkehrende Aufgabe (6.1.4).
public bool IsRecurring => Model.Recurrence != TaskRecurrence.None;
public string RecurrenceLabel => $"🔁 {TaskRecurrenceDisplay.Label(Model.Recurrence)}";
@@ -266,6 +302,10 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut
public partial class AddEditWorkTaskDialogViewModel : ObservableObject
{
private readonly WorkTask? _source;
/// Liefert die aktiven Mitglieder einer Gruppe (StudentId, Anzeigename) für "Aus Kursliste
/// befüllen" — optional/injizierbar statt eines festen Repository-Typs, damit dieses ViewModel
/// (wie bisher) auch ohne DI-Container in Tests per `new` gebaut werden kann.
private readonly Func<Guid, List<(Guid StudentId, string Name)>>? _loadRoster;
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _selectedCategory = TaskCategoryDisplay.Options[0];
@@ -273,8 +313,10 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
[ObservableProperty] private string _dueDateText = "";
[ObservableProperty] private string _estimatedMinutesText = "";
[ObservableProperty] private string _selectedRecurrence = TaskRecurrenceDisplay.Options[0];
[ObservableProperty] private string _selectedPriority = TaskPriorityDisplay.Label(TaskPriority.Normal);
[ObservableProperty] private string _notes = "";
[ObservableProperty] private bool _isReminder;
[ObservableProperty] private string _newChecklistItemText = "";
[ObservableProperty] private string _titleError = "";
[ObservableProperty] private string _dueDateError = "";
[ObservableProperty] private string _estimatedMinutesError = "";
@@ -284,15 +326,24 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
: (IsReminder ? "Erinnerung bearbeiten" : "Aufgabe bearbeiten");
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
public List<string> RecurrenceOptions { get; } = [.. TaskRecurrenceDisplay.Options];
public List<string> PriorityOptions { get; } = [.. TaskPriorityDisplay.Options];
public List<LearningGroup> Groups { get; }
public WorkTask? Result { get; private set; }
partial void OnIsReminderChanged(bool value) => OnPropertyChanged(nameof(DialogTitle));
// Abhak-Liste (Nutzer-Feedback: wahlweise Namensliste der Kurs-Schüler oder freie Teil-Punkte,
// beides über denselben Zeilentyp).
public ObservableCollection<ChecklistItemRow> ChecklistItems { get; } = [];
public bool CanFillFromRoster => SelectedGroup is not null && _loadRoster is not null;
public AddEditWorkTaskDialogViewModel(WorkTask? source, List<LearningGroup> groups, bool startAsReminder = false)
partial void OnIsReminderChanged(bool value) => OnPropertyChanged(nameof(DialogTitle));
partial void OnSelectedGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(CanFillFromRoster));
public AddEditWorkTaskDialogViewModel(WorkTask? source, List<LearningGroup> groups, bool startAsReminder = false,
Func<Guid, List<(Guid StudentId, string Name)>>? loadRoster = null)
{
_source = source;
Groups = groups;
_loadRoster = loadRoster;
if (source is null) { IsReminder = startAsReminder; return; }
Title = source.Title;
@@ -301,8 +352,37 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
SelectedRecurrence = TaskRecurrenceDisplay.Label(source.Recurrence);
SelectedPriority = TaskPriorityDisplay.Label(source.Priority);
Notes = source.Notes ?? "";
IsReminder = source.Kind == TaskKind.Reminder;
foreach (var item in source.ChecklistItems)
ChecklistItems.Add(new ChecklistItemRow(item));
}
[RelayCommand]
private void AddChecklistItem()
{
if (string.IsNullOrWhiteSpace(NewChecklistItemText)) return;
ChecklistItems.Add(new ChecklistItemRow(NewChecklistItemText.Trim()));
NewChecklistItemText = "";
}
[RelayCommand]
private void RemoveChecklistItem(ChecklistItemRow? row)
{
if (row is not null) ChecklistItems.Remove(row);
}
[RelayCommand]
private void FillFromRoster()
{
if (SelectedGroup is null || _loadRoster is null) return;
var existingStudentIds = ChecklistItems.Where(c => c.StudentId is not null).Select(c => c.StudentId!.Value).ToHashSet();
foreach (var (studentId, name) in _loadRoster(SelectedGroup.Id))
{
if (!existingStudentIds.Contains(studentId))
ChecklistItems.Add(new ChecklistItemRow(name, studentId));
}
}
[RelayCommand]
@@ -346,13 +426,39 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
EstimatedMinutes = estimatedMinutes,
Recurrence = recurrence,
Kind = IsReminder ? TaskKind.Reminder : TaskKind.WorkItem,
Priority = TaskPriorityDisplay.FromLabel(SelectedPriority),
Status = _source?.Status ?? WorkTaskStatus.Open,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
ChecklistItems = ChecklistItems.Select(c => new ChecklistItem
{ Id = c.Id, Label = c.Label, IsDone = c.IsDone, StudentId = c.StudentId }).ToList(),
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
};
}
}
public partial class ChecklistItemRow : ObservableObject
{
public Guid Id { get; }
public Guid? StudentId { get; }
[ObservableProperty] private string _label;
[ObservableProperty] private bool _isDone;
public ChecklistItemRow(ChecklistItem source)
{
Id = source.Id;
StudentId = source.StudentId;
_label = source.Label;
_isDone = source.IsDone;
}
public ChecklistItemRow(string label, Guid? studentId = null)
{
Id = Guid.NewGuid();
StudentId = studentId;
_label = label;
}
}
// ── Zeiterfassung (6.2) ──────────────────────────────────────────────────────
public partial class TimeTrackingViewModel : ObservableObject