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
@@ -79,6 +79,9 @@ 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<Guid>? OnNavigateToLesson { get; set; }
// Direkter Anlege-Einstieg aus der "Offene Aufgaben"-Kachel (Nutzer-Feedback), statt erst über
// "Arbeitszeit" navigieren zu müssen — gleiches Dialog-Delegate-Muster wie im Kurs-Dashboard.
public Func<bool, Task<WorkTask?>>? OnAddTask { get; set; }
public Action<Guid>? OnNavigateToExam { get; set; }
// Sprungziel für eine ungeplante Stunde — führt direkt in den Verlaufsplan-Tab der Gruppe
// (nicht den Standard-Tab von OnNavigateToGroup), damit das Thema gleich ergänzt werden kann.
@@ -162,7 +165,8 @@ public partial class DashboardViewModel : ObservableObject
OpenTasks.Add(new() { Title = t.Title,
DueDate = t.DueDate?.ToString("dd.MM.") ?? "",
IsOverdue = t.DueDate.HasValue && t.DueDate < today,
IsReminder = t.Kind == TaskKind.Reminder });
IsReminder = t.Kind == TaskKind.Reminder,
IsHighPriority = t.Priority == TaskPriority.High });
CurrentGroups.Clear();
foreach (var g in groups.Values.OrderBy(g => g.Name))
@@ -678,6 +682,18 @@ public partial class DashboardViewModel : ObservableObject
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
[RelayCommand] private void Refresh() => Load();
[RelayCommand] private Task AddTask() => AddTaskInternal(startAsReminder: false);
[RelayCommand] private Task AddReminder() => AddTaskInternal(startAsReminder: true);
private async Task AddTaskInternal(bool startAsReminder)
{
if (OnAddTask is null) return;
var result = await OnAddTask(startAsReminder);
if (result is null) return;
_tasks.Save(result);
Load();
}
private class DayAgg
{
public bool HasLesson;
@@ -698,7 +714,7 @@ public class LessonItem
public string Room { get; set; } = "";
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
}
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } }
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } public bool IsHighPriority { get; set; } }
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; }
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
@@ -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()
@@ -168,6 +168,11 @@ public partial class MainWindowViewModel : ObservableObject
}
public void NavigateToStudents() => NavigateTo(NavItem.Students);
/// Sprungziel aus dem Kurs-Dashboard ("Anstehende Aufgaben für diese Klasse") in den
/// Aufgaben-Bereich — anders als NavigateToGroupDetail/NavigateToSettings kein Tab innerhalb
/// einer Detailansicht, sondern ein eigener Top-Level-Bereich (NavItem.Workload).
public void NavigateToWorkload() => NavigateTo(NavItem.Workload);
}
public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings }
@@ -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
@@ -103,18 +103,26 @@
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="OFFENE AUFGABEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,10">
<TextBlock Grid.Column="0" Text="OFFENE AUFGABEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="🔔+" FontSize="12" Padding="7,2" Margin="0,0,4,0"
ToolTip.Tip="Erinnerung anlegen" Command="{Binding AddReminderCommand}"/>
<Button Grid.Column="2" Content="" FontSize="12" Padding="8,2"
ToolTip.Tip="Aufgabe anlegen" Command="{Binding AddTaskCommand}"/>
</Grid>
<ItemsControl ItemsSource="{Binding OpenTasks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:TaskItem">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,3">
<TextBlock Grid.Column="0" Text="🔔" FontSize="12" Margin="0,0,4,0"
<Grid ColumnDefinitions="4,Auto,*,Auto" Margin="0,3">
<Border Grid.Column="0" Background="#D32F2F" CornerRadius="2" Margin="0,0,6,0"
IsVisible="{Binding IsHighPriority}" ToolTip.Tip="Hohe Priorität"/>
<TextBlock Grid.Column="1" Text="🔔" FontSize="12" Margin="0,0,4,0"
IsVisible="{Binding IsReminder}"
ToolTip.Tip="Erinnerung — kein Zeitbezug"/>
<TextBlock Grid.Column="1" Text="{Binding Title}"
<TextBlock Grid.Column="2" Text="{Binding Title}"
FontSize="13" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="2" Text="{Binding DueDate}"
<TextBlock Grid.Column="3" Text="{Binding DueDate}"
FontSize="12" Opacity="0.6" Margin="8,0,0,0"/>
</Grid>
</DataTemplate>
@@ -1,3 +1,25 @@
using Avalonia.Controls;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.Views.Workload;
namespace LehrerApp.Desktop.Views.Dashboard;
public partial class DashboardView : UserControl { public DashboardView() => InitializeComponent(); }
public partial class DashboardView : UserControl
{
public DashboardView() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is DashboardViewModel vm)
vm.OnAddTask = ShowAddTaskDialog;
}
private async Task<WorkTask?> ShowAddTaskDialog(bool startAsReminder)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
return await WorkTaskDialogHelper.ShowDialog(owner, startAsReminder: startAsReminder);
}
}
@@ -5,11 +5,13 @@ using LehrerApp.Core.Importing;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.Views.Shared;
using LehrerApp.Desktop.Views.Students;
using LehrerApp.Desktop.Views.Workload;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
@@ -34,9 +36,19 @@ public partial class GroupDetailView : UserControl
vm.OnGradeExam = ShowGradeExamDialog;
vm.OnEvaluateExam = ShowEvaluateExamDialog;
vm.OnConfirmReactivate = ShowReactivateConfirmDialog;
vm.OverviewTab.OnNavigateToWorkload = () =>
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToWorkload();
vm.OverviewTab.OnAddGroupTask = ShowAddGroupTaskDialog;
}
}
private async Task<WorkTask?> ShowAddGroupTaskDialog(Guid groupId)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
return await WorkTaskDialogHelper.ShowDialog(owner, preselectedGroupId: groupId);
}
private async Task<bool> ShowReactivateConfirmDialog()
{
var dialog = new ConfirmDialog
@@ -5,6 +5,13 @@
x:Class="LehrerApp.Desktop.Views.Groups.GroupListView"
x:DataType="vm:GroupListViewModel">
<UserControl.Styles>
<Style Selector="TextBlock.overdue">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*">
<!-- Kopfzeile mit Schuljahr-Wähler und Neu-Button -->
@@ -108,10 +115,48 @@
<Separator/>
<!-- Schnellüberblick (Nutzer-Feedback) -->
<StackPanel Spacing="8" IsVisible="{Binding QuickHasAnything}">
<TextBlock Text="SCHNELLÜBERBLICK" FontSize="10" FontWeight="Bold" Opacity="0.4"/>
<StackPanel Spacing="1" IsVisible="{Binding QuickHasNextLesson}">
<TextBlock Text="Nächste Stunde" FontSize="11" Opacity="0.55"/>
<TextBlock Text="{Binding QuickNextLessonLabel}" FontSize="13" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Spacing="1" IsVisible="{Binding QuickHasNextExam}">
<TextBlock Text="Nächste Klausur" FontSize="11" Opacity="0.55"/>
<TextBlock Text="{Binding QuickNextExamLabel}" FontSize="13" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding QuickHasTasks}">
<TextBlock Text="Wichtige Aufgaben" FontSize="11" Opacity="0.55"/>
<ItemsControl ItemsSource="{Binding QuickTasks}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:GroupTaskItem">
<Grid ColumnDefinitions="4,Auto,*,Auto" Margin="0,2">
<Border Grid.Column="0" Background="{Binding PriorityColorHex}" CornerRadius="2"
Margin="0,0,6,0" IsVisible="{Binding IsHighPriority}"/>
<TextBlock Grid.Column="1" Text="🔔" FontSize="11" Margin="0,0,4,0"
IsVisible="{Binding IsReminder}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="{Binding Title}" FontSize="12"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<TextBlock Grid.Column="3" Text="{Binding DueDateDisplay}" FontSize="11"
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
<Separator IsVisible="{Binding QuickHasAnything}"/>
<!-- Bereichs-Navigation -->
<TextBlock Text="BEREICHE" FontSize="10" FontWeight="Bold"
Opacity="0.4" Margin="0,0,0,2"/>
<StackPanel Spacing="6">
<Button Content="📊 Übersicht"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="0"/>
<Button Content="👤 Schülerliste"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
@@ -122,6 +167,11 @@
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="2"/>
<Button Content="✋ Mitarbeit"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="3"/>
<Button Content="📝 Klausuren"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
@@ -24,6 +24,10 @@
<Setter Property="Foreground" Value="#D97706"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="TextBlock.overdue">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.cardLink">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Padding" Value="0"/>
@@ -165,6 +169,40 @@
<Button Content="Zur Mitarbeit " Classes="cardLink" Command="{Binding NavigateToParticipationCommand}"/>
</StackPanel>
</Border>
<!-- Anstehende Aufgaben für diese Klasse (pädagogische Erinnerungen/Aufgaben, Nutzer-
Feedback: "dürfen gerne auch im passenden Kurs-Dashboard stehen") — anders als die
übrigen Karten dieser Reihe bewusst immer sichtbar (statt Has…-gated), da sie zugleich
der Anlege-Einstieg für die erste Aufgabe dieser Gruppe ist. -->
<Border Classes="card" MinWidth="340" MaxWidth="420">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="ANSTEHENDE AUFGABEN" Classes="cardTitle"/>
<Button Grid.Column="1" Content="" FontSize="12" Padding="8,2" Margin="0,-4,0,0"
ToolTip.Tip="Aufgabe/Erinnerung für diese Gruppe anlegen"
Command="{Binding AddGroupTaskCommand}"/>
</Grid>
<ItemsControl ItemsSource="{Binding GroupTasks}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:GroupTaskItem">
<Grid ColumnDefinitions="4,Auto,*,Auto" Margin="0,4">
<Border Grid.Column="0" Background="{Binding PriorityColorHex}" CornerRadius="2"
Margin="0,0,6,0" IsVisible="{Binding IsHighPriority}"/>
<TextBlock Grid.Column="1" Text="🔔" FontSize="12" Margin="0,0,4,0"
IsVisible="{Binding IsReminder}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="{Binding Title}" FontSize="13"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<TextBlock Grid.Column="3" Text="{Binding DueDateDisplay}" FontSize="12"
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine anstehenden Aufgaben für diese Gruppe." Classes="emptyhint"
IsVisible="{Binding !HasGroupTasks}"/>
<Button Content="Zu den Aufgaben " Classes="cardLink" Command="{Binding NavigateToWorkloadCommand}"/>
</StackPanel>
</Border>
</WrapPanel>
</StackPanel>
</ScrollViewer>
@@ -57,17 +57,51 @@
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="Wiederholung" FontSize="12" Opacity="0.7"
ToolTip.Tip="Beim Abschließen wird automatisch die nächste Instanz mit neuem Fälligkeitsdatum angelegt. Braucht ein Fälligkeitsdatum als Ausgangspunkt."/>
<ComboBox ItemsSource="{Binding RecurrenceOptions}" SelectedItem="{Binding SelectedRecurrence}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<Grid ColumnDefinitions="*,8,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Wiederholung" FontSize="12" Opacity="0.7"
ToolTip.Tip="Beim Abschließen wird automatisch die nächste Instanz mit neuem Fälligkeitsdatum angelegt. Braucht ein Fälligkeitsdatum als Ausgangspunkt."/>
<ComboBox ItemsSource="{Binding RecurrenceOptions}" SelectedItem="{Binding SelectedRecurrence}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Priorität" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding PriorityOptions}" SelectedItem="{Binding SelectedPriority}"
HorizontalAlignment="Stretch"/>
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="Notizen (optional)" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Notes}" AcceptsReturn="True" TextWrapping="Wrap" Height="80"/>
</StackPanel>
<StackPanel Spacing="4">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Abhak-Liste (optional)" FontSize="12" Opacity="0.7"
VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Aus Kursliste befüllen" FontSize="11" Padding="7,3"
Command="{Binding FillFromRosterCommand}" IsVisible="{Binding CanFillFromRoster}"/>
</Grid>
<ItemsControl ItemsSource="{Binding ChecklistItems}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ChecklistItemRow">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,2">
<CheckBox Grid.Column="0" IsChecked="{Binding IsDone}"/>
<TextBlock Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
<Button Grid.Column="2" Content="✕" FontSize="11" Padding="6,2"
Command="{Binding $parent[ItemsControl].((vm:AddEditWorkTaskDialogViewModel)DataContext).RemoveChecklistItemCommand}"
CommandParameter="{Binding}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid ColumnDefinitions="*,8,Auto">
<TextBox Grid.Column="0" Text="{Binding NewChecklistItemText}" PlaceholderText="Eigener Punkt..."/>
<Button Grid.Column="2" Content="+" Command="{Binding AddChecklistItemCommand}"/>
</Grid>
</StackPanel>
</StackPanel>
</ScrollViewer>
@@ -0,0 +1,41 @@
using Avalonia.Controls;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Workload;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Workload;
/// Gemeinsamer Aufruf des Aufgaben-Dialogs für alle drei Entry-Points (Aufgabenliste,
/// Kurs-Dashboard, Hauptdashboard) — vermeidet drei fast identische Kopien der Gruppen-/
/// Kursliste-Ladelogik.
public static class WorkTaskDialogHelper
{
public static async Task<WorkTask?> ShowDialog(Window owner, WorkTask? source = null,
bool startAsReminder = false, Guid? preselectedGroupId = null)
{
var groups = App.Services.GetRequiredService<IGroupRepository>().GetAll(includeInactive: true);
var vm = new AddEditWorkTaskDialogViewModel(source, groups, startAsReminder, LoadActiveRoster);
if (source is null && preselectedGroupId is { } groupId)
vm.SelectedGroup = groups.FirstOrDefault(g => g.Id == groupId);
var dialog = new AddEditWorkTaskDialog { DataContext = vm };
await dialog.ShowDialog<bool>(owner);
return vm.Result;
}
// "Aus Kursliste befüllen" (Nutzer-Feedback): nur aktuell aktive Mitglieder, wie überall sonst
// im Dashboard/Übersicht-Tab (GroupMembershipService.IsActiveOn).
private static List<(Guid StudentId, string Name)> LoadActiveRoster(Guid groupId)
{
var memberships = App.Services.GetRequiredService<IGroupMembershipRepository>();
var students = App.Services.GetRequiredService<IStudentRepository>();
var today = DateOnly.FromDateTime(DateTime.Today);
return memberships.GetByGroup(groupId)
.Where(m => GroupMembershipService.IsActiveOn(m, today))
.Select(m => students.GetById(m.StudentId))
.Where(s => s is not null)
.Select(s => (s!.Id, s.FullName))
.ToList();
}
}
@@ -25,15 +25,18 @@
<DataTemplate x:DataType="vm:WorkTaskListItem">
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="6" Padding="12,10" Margin="0,0,0,8">
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
<Button Grid.Column="0" Background="{Binding StatusColorHex}" Padding="8,4"
<Grid ColumnDefinitions="4,Auto,*,Auto,Auto,Auto,Auto">
<Border Grid.Column="0" Background="{Binding PriorityColorHex}" CornerRadius="2"
Margin="0,0,8,0" IsVisible="{Binding ShowPriority}"
ToolTip.Tip="{Binding PriorityLabel}"/>
<Button Grid.Column="1" Background="{Binding StatusColorHex}" Padding="8,4"
CornerRadius="4" VerticalAlignment="Center"
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).CycleStatusCommand}"
CommandParameter="{Binding}"
ToolTip.Tip="Klicken, um den Status zu wechseln">
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
</Button>
<StackPanel Grid.Column="1" Margin="10,0" VerticalAlignment="Center">
<StackPanel Grid.Column="2" Margin="10,0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="🔔" FontSize="12" IsVisible="{Binding IsReminder}"
ToolTip.Tip="Erinnerung — kein Zeitbezug, nicht Teil der Auswertung"/>
@@ -50,16 +53,19 @@
<!-- Ist-Zeit vs. Schätzung (6.2.4) -->
<TextBlock Text="{Binding ActualVsEstimateDisplay}" FontSize="11" Opacity="0.6"
IsVisible="{Binding HasActualTime}"/>
<!-- Abhak-Liste -->
<TextBlock Text="{Binding ChecklistProgressDisplay}" FontSize="11" Opacity="0.6"
IsVisible="{Binding HasChecklist}"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding DueDateDisplay}" VerticalAlignment="Center"
<TextBlock Grid.Column="3" Text="{Binding DueDateDisplay}" VerticalAlignment="Center"
Margin="0,0,10,0" FontSize="12" Foreground="{Binding DueDateColorHex}"/>
<TextBlock Grid.Column="3" Text="{Binding EstimatedMinutesDisplay}"
<TextBlock Grid.Column="4" Text="{Binding EstimatedMinutesDisplay}"
VerticalAlignment="Center" Opacity="0.6" FontSize="12" Margin="0,0,10,0"
IsVisible="{Binding !HasActualTime}"/>
<Button Grid.Column="4" Content="Bearbeiten" FontSize="11" Padding="8,3" Margin="0,0,4,0"
<Button Grid.Column="5" Content="Bearbeiten" FontSize="11" Padding="8,3" Margin="0,0,4,0"
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).EditTaskCommand}"
CommandParameter="{Binding}"/>
<Button Grid.Column="5" Content="Löschen" FontSize="11" Padding="8,3"
<Button Grid.Column="6" Content="Löschen" FontSize="11" Padding="8,3"
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).DeleteTaskCommand}"
CommandParameter="{Binding}"/>
</Grid>
@@ -1,8 +1,6 @@
using Avalonia.Controls;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Workload;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Workload;
@@ -21,11 +19,6 @@ public partial class WorkTaskListView : UserControl
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
var groups = App.Services.GetRequiredService<IGroupRepository>().GetAll(includeInactive: true);
var vm = new AddEditWorkTaskDialogViewModel(source, groups, startAsReminder);
var dialog = new AddEditWorkTaskDialog { DataContext = vm };
await dialog.ShowDialog<bool>(owner);
return vm.Result;
return await WorkTaskDialogHelper.ShowDialog(owner, source, startAsReminder);
}
}