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:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user