Arbeitszeit: Aufgabenverwaltung (Kapitel 6.1)

Navigationspunkt "Arbeitszeit" zeigt jetzt eine echte Aufgabenliste statt eines
Placeholders: Filter nach Status/Kategorie/Gruppe (Standard blendet Erledigtes aus),
Sortierung nach Fälligkeit, Anlegen/Bearbeiten-Dialog, Status per Klick durchschalten.
This commit is contained in:
2026-08-15 22:33:40 +02:00
parent 0800832ae5
commit 5c0b6854f5
11 changed files with 692 additions and 6 deletions
@@ -6,6 +6,7 @@ using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Settings;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.ViewModels.Workload;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.ObjectModel;
@@ -66,7 +67,7 @@ public partial class MainWindowViewModel : ObservableObject
NavItem.Students => _services.GetRequiredService<StudentListViewModel>(),
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
NavItem.Planner => GetTimetable(),
NavItem.Workload => new PlaceholderViewModel { Title = "Arbeitszeit", Icon = "⏱" },
NavItem.Workload => GetWorkload(),
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
_ => CurrentPage,
};
@@ -88,6 +89,13 @@ public partial class MainWindowViewModel : ObservableObject
return timetable;
}
private WorkTaskListViewModel GetWorkload()
{
var workload = _services.GetRequiredService<WorkTaskListViewModel>();
workload.Load();
return workload;
}
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
{
ActiveNavItem = NavItem.Groups;
@@ -0,0 +1,266 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using System.Collections.ObjectModel;
using System.Globalization;
namespace LehrerApp.Desktop.ViewModels.Workload;
// ── Anzeige-Hilfen für die Enums (6.1) ──────────────────────────────────────
public static class WorkTaskStatusDisplay
{
public static string Label(WorkTaskStatus s) => s switch
{
WorkTaskStatus.Open => "Offen",
WorkTaskStatus.InProgress => "In Bearbeitung",
WorkTaskStatus.Done => "Erledigt",
_ => s.ToString(),
};
public static string ColorHex(WorkTaskStatus s) => s switch
{
WorkTaskStatus.Open => "#9E9E9E",
WorkTaskStatus.InProgress => "#1976D2",
WorkTaskStatus.Done => "#43A047",
_ => "#9E9E9E",
};
// Klick auf die Status-Kachel wechselt reihum (6.1.3).
public static WorkTaskStatus Next(WorkTaskStatus s) => s switch
{
WorkTaskStatus.Open => WorkTaskStatus.InProgress,
WorkTaskStatus.InProgress => WorkTaskStatus.Done,
_ => WorkTaskStatus.Open,
};
}
public static class TaskCategoryDisplay
{
public static string Label(TaskCategory c) => c switch
{
TaskCategory.Correction => "Korrektur",
TaskCategory.Preparation => "Vorbereitung",
TaskCategory.Admin => "Verwaltung",
TaskCategory.Meeting => "Besprechung",
TaskCategory.Other => "Sonstiges",
_ => c.ToString(),
};
public static string[] Options { get; } = Enum.GetValues<TaskCategory>().Select(Label).ToArray();
public static TaskCategory FromLabel(string? label) =>
Enum.GetValues<TaskCategory>().FirstOrDefault(c => Label(c) == label, TaskCategory.Other);
}
// ── Aufgabenliste (6.1.1, 6.1.3) ─────────────────────────────────────────────
public partial class WorkTaskListViewModel : ObservableObject
{
private readonly IWorkTaskRepository _tasks;
private readonly IGroupRepository _groups;
public const string AllFilter = "Alle";
public const string ActiveFilter = "Offene Aufgaben";
public const string DoneFilter = "Erledigt";
public const string NoGroupFilter = "Ohne Gruppe";
[ObservableProperty] private string _statusFilter = ActiveFilter;
[ObservableProperty] private string _categoryFilter = AllFilter;
[ObservableProperty] private string _groupFilter = AllFilter;
public List<string> StatusFilterOptions { get; } = [ActiveFilter, AllFilter, DoneFilter];
public List<string> CategoryFilterOptions { get; } = [AllFilter, .. TaskCategoryDisplay.Options];
public ObservableCollection<string> GroupFilterOptions { get; } = [AllFilter, NoGroupFilter];
public ObservableCollection<WorkTaskListItem> Tasks { get; } = [];
public string CountSummary => $"{Tasks.Count} Aufgabe(n)";
public Func<WorkTask?, Task<WorkTask?>>? OnEditTask { get; set; }
private Dictionary<Guid, string> _groupNames = [];
public WorkTaskListViewModel(IWorkTaskRepository tasks, IGroupRepository groups)
{
_tasks = tasks;
_groups = groups;
Load();
}
partial void OnStatusFilterChanged(string value) => Refresh();
partial void OnCategoryFilterChanged(string value) => Refresh();
partial void OnGroupFilterChanged(string value) => Refresh();
public void Load()
{
var groups = _groups.GetAll(includeInactive: true);
_groupNames = groups.ToDictionary(g => g.Id, g => g.Name);
GroupFilterOptions.Clear();
GroupFilterOptions.Add(AllFilter);
GroupFilterOptions.Add(NoGroupFilter);
foreach (var name in groups.Select(g => g.Name).OrderBy(n => n))
GroupFilterOptions.Add(name);
Refresh();
}
private void Refresh()
{
Tasks.Clear();
var all = _tasks.GetAll().AsEnumerable();
all = StatusFilter switch
{
ActiveFilter => all.Where(t => t.Status != WorkTaskStatus.Done),
DoneFilter => all.Where(t => t.Status == WorkTaskStatus.Done),
_ => all,
};
if (CategoryFilter != AllFilter)
{
var category = TaskCategoryDisplay.FromLabel(CategoryFilter);
all = all.Where(t => t.Category == category);
}
if (GroupFilter == NoGroupFilter)
all = all.Where(t => t.GroupId is null);
else if (GroupFilter != AllFilter)
all = all.Where(t => t.GroupId.HasValue && _groupNames.GetValueOrDefault(t.GroupId.Value) == GroupFilter);
// Fälligkeit: fällige zuerst, unbefristete ans Ende (6.1.1).
foreach (var t in all.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).ThenBy(t => t.Title))
Tasks.Add(new WorkTaskListItem(t, _groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty)));
OnPropertyChanged(nameof(CountSummary));
}
[RelayCommand]
private async Task AddTask()
{
if (OnEditTask is null) return;
var result = await OnEditTask(null);
if (result is null) return;
_tasks.Save(result);
Refresh();
}
[RelayCommand]
private async Task EditTask(WorkTaskListItem? item)
{
if (item is null || OnEditTask is null) return;
var result = await OnEditTask(item.Model);
if (result is null) return;
_tasks.Save(result);
Refresh();
}
[RelayCommand]
private void CycleStatus(WorkTaskListItem? item)
{
if (item is null) return;
item.Model.Status = WorkTaskStatusDisplay.Next(item.Model.Status);
_tasks.Save(item.Model);
Refresh();
}
[RelayCommand]
private void DeleteTask(WorkTaskListItem? item)
{
if (item is null) return;
_tasks.Delete(item.Model.Id);
Refresh();
}
}
public class WorkTaskListItem(WorkTask model, string? groupName)
{
public WorkTask Model { get; } = model;
public string Title => Model.Title;
public string CategoryLabel => TaskCategoryDisplay.Label(Model.Category);
public string GroupName => groupName ?? "";
public string DueDateDisplay => Model.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
public bool IsOverdue => Model.DueDate is { } d && d < DateOnly.FromDateTime(DateTime.Today)
&& Model.Status != WorkTaskStatus.Done;
public string DueDateColorHex => IsOverdue ? "#D32F2F" : "#9E9E9E";
public string StatusLabel => WorkTaskStatusDisplay.Label(Model.Status);
public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status);
public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : "";
}
// ── Dialog: Aufgabe anlegen/bearbeiten (6.1.2) ───────────────────────────────
public partial class AddEditWorkTaskDialogViewModel : ObservableObject
{
private readonly WorkTask? _source;
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _selectedCategory = TaskCategoryDisplay.Options[0];
[ObservableProperty] private LearningGroup? _selectedGroup;
[ObservableProperty] private string _dueDateText = "";
[ObservableProperty] private string _estimatedMinutesText = "";
[ObservableProperty] private string _notes = "";
[ObservableProperty] private string _titleError = "";
[ObservableProperty] private string _dueDateError = "";
[ObservableProperty] private string _estimatedMinutesError = "";
public string DialogTitle => _source is null ? "Aufgabe anlegen" : "Aufgabe bearbeiten";
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
public List<LearningGroup> Groups { get; }
public WorkTask? Result { get; private set; }
public AddEditWorkTaskDialogViewModel(WorkTask? source, List<LearningGroup> groups)
{
_source = source;
Groups = groups;
if (source is null) return;
Title = source.Title;
SelectedCategory = TaskCategoryDisplay.Label(source.Category);
SelectedGroup = source.GroupId is { } id ? groups.FirstOrDefault(g => g.Id == id) : null;
DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
Notes = source.Notes ?? "";
}
[RelayCommand]
private void Save()
{
TitleError = ""; DueDateError = ""; EstimatedMinutesError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
DateOnly? dueDate = null;
if (!string.IsNullOrWhiteSpace(DueDateText))
{
if (!DateOnly.TryParseExact(DueDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var d))
{ DueDateError = "Format TT.MM.JJJJ."; valid = false; }
else dueDate = d;
}
int? estimatedMinutes = null;
if (!string.IsNullOrWhiteSpace(EstimatedMinutesText))
{
if (!int.TryParse(EstimatedMinutesText, out var m) || m <= 0)
{ EstimatedMinutesError = "Ganze Zahl > 0 erwartet."; valid = false; }
else estimatedMinutes = m;
}
if (!valid) return;
Result = new WorkTask
{
Id = _source?.Id ?? Guid.NewGuid(),
Title = Title.Trim(),
Category = TaskCategoryDisplay.FromLabel(SelectedCategory),
GroupId = SelectedGroup?.Id,
DueDate = dueDate,
EstimatedMinutes = estimatedMinutes,
Status = _source?.Status ?? WorkTaskStatus.Open,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
};
}
}