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:
@@ -270,6 +270,16 @@ public class FakeSubstitutionEntries : ISubstitutionEntryRepository
|
|||||||
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FakeWorkTasks : IWorkTaskRepository
|
||||||
|
{
|
||||||
|
private readonly List<WorkTask> _all = [];
|
||||||
|
public void Add(WorkTask t) => _all.Add(t);
|
||||||
|
public List<WorkTask> GetByStatus(WorkTaskStatus status) => _all.Where(t => t.Status == status).ToList();
|
||||||
|
public List<WorkTask> GetAll() => _all.ToList();
|
||||||
|
public void Save(WorkTask task) { _all.RemoveAll(t => t.Id == task.Id); _all.Add(task); }
|
||||||
|
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
public class FakeReportGrades : IReportGradeRepository
|
public class FakeReportGrades : IReportGradeRepository
|
||||||
{
|
{
|
||||||
private readonly List<ReportGrade> _all = [];
|
private readonly List<ReportGrade> _all = [];
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class WorkTaskListViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Load_BlendetErledigteAufgabenStandardmaessigAus()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "Offen", Status = WorkTaskStatus.Open });
|
||||||
|
tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]));
|
||||||
|
|
||||||
|
Assert.Single(vm.Tasks);
|
||||||
|
Assert.Equal("Offen", vm.Tasks[0].Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StatusFilter_Alle_ZeigtAuchErledigteAufgaben()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]))
|
||||||
|
{
|
||||||
|
StatusFilter = WorkTaskListViewModel.AllFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Single(vm.Tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CategoryFilter_FiltertNachKategorie()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "Korrektur", Category = TaskCategory.Correction });
|
||||||
|
tasks.Add(new WorkTask { Title = "Vorbereitung", Category = TaskCategory.Preparation });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]))
|
||||||
|
{
|
||||||
|
CategoryFilter = TaskCategoryDisplay.Label(TaskCategory.Correction),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Single(vm.Tasks);
|
||||||
|
Assert.Equal("Korrektur", vm.Tasks[0].Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GroupFilter_OhneGruppe_ZeigtNurAufgabenOhneGroupId()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a" };
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "Mit Gruppe", GroupId = group.Id });
|
||||||
|
tasks.Add(new WorkTask { Title = "Ohne Gruppe", GroupId = null });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([group]))
|
||||||
|
{
|
||||||
|
GroupFilter = WorkTaskListViewModel.NoGroupFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Single(vm.Tasks);
|
||||||
|
Assert.Equal("Ohne Gruppe", vm.Tasks[0].Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CycleStatus_WechseltVonOffenNachInBearbeitung()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Open });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]))
|
||||||
|
{
|
||||||
|
StatusFilter = WorkTaskListViewModel.AllFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.CycleStatusCommand.Execute(vm.Tasks[0]);
|
||||||
|
|
||||||
|
Assert.Equal(WorkTaskStatus.InProgress, vm.Tasks[0].Model.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CycleStatus_VonErledigtSpringtZurueckZuOffen()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Done });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]))
|
||||||
|
{
|
||||||
|
StatusFilter = WorkTaskListViewModel.AllFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.CycleStatusCommand.Execute(vm.Tasks[0]);
|
||||||
|
|
||||||
|
Assert.Equal(WorkTaskStatus.Open, vm.Tasks[0].Model.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteTask_EntferntAufgabeAusDerListe()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "T" });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]));
|
||||||
|
|
||||||
|
vm.DeleteTaskCommand.Execute(vm.Tasks[0]);
|
||||||
|
|
||||||
|
Assert.Empty(vm.Tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddTask_RuftOnEditTaskAufUndSpeichertErgebnis()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]));
|
||||||
|
vm.OnEditTask = _ => Task.FromResult<WorkTask?>(new WorkTask { Title = "Neu" });
|
||||||
|
|
||||||
|
await vm.AddTaskCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Single(vm.Tasks);
|
||||||
|
Assert.Equal("Neu", vm.Tasks[0].Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tasks_SindNachFaelligkeitSortiertUnbefristeteAmEnde()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "Ohne Frist", DueDate = null });
|
||||||
|
tasks.Add(new WorkTask { Title = "Spät fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(10)) });
|
||||||
|
tasks.Add(new WorkTask { Title = "Bald fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)) });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]));
|
||||||
|
|
||||||
|
Assert.Equal(["Bald fällig", "Spät fällig", "Ohne Frist"], vm.Tasks.Select(t => t.Title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AddEditWorkTaskDialogViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Save_OhneTitel_SetztFehlerUndErzeugtKeinResult()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, []);
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Null(vm.Result);
|
||||||
|
Assert.NotEmpty(vm.TitleError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_MitGueltigenDaten_ErzeugtWorkTask()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a" };
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, [group])
|
||||||
|
{
|
||||||
|
Title = "Klausur korrigieren",
|
||||||
|
SelectedCategory = TaskCategoryDisplay.Label(TaskCategory.Correction),
|
||||||
|
SelectedGroup = group,
|
||||||
|
DueDateText = "24.12.2026",
|
||||||
|
EstimatedMinutesText = "90",
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.NotNull(vm.Result);
|
||||||
|
Assert.Equal("Klausur korrigieren", vm.Result!.Title);
|
||||||
|
Assert.Equal(TaskCategory.Correction, vm.Result.Category);
|
||||||
|
Assert.Equal(group.Id, vm.Result.GroupId);
|
||||||
|
Assert.Equal(new DateOnly(2026, 12, 24), vm.Result.DueDate);
|
||||||
|
Assert.Equal(90, vm.Result.EstimatedMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_MitUngueltigemDatum_SetztFehler()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, [])
|
||||||
|
{
|
||||||
|
Title = "T",
|
||||||
|
DueDateText = "nicht-ein-datum",
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Null(vm.Result);
|
||||||
|
Assert.NotEmpty(vm.DueDateError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_BestehendeAufgabe_BehaeltIdUndStatus()
|
||||||
|
{
|
||||||
|
var source = new WorkTask { Title = "Alt", Status = WorkTaskStatus.InProgress };
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(source, []) { Title = "Neu" };
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(source.Id, vm.Result!.Id);
|
||||||
|
Assert.Equal(WorkTaskStatus.InProgress, vm.Result.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using LehrerApp.Desktop.ViewModels.Groups;
|
|||||||
using LehrerApp.Desktop.ViewModels.Planning;
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Settings;
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
using LehrerApp.Sync;
|
using LehrerApp.Sync;
|
||||||
using LehrerApp.Sync.Crypto;
|
using LehrerApp.Sync.Crypto;
|
||||||
using LehrerApp.Sync.Models;
|
using LehrerApp.Sync.Models;
|
||||||
@@ -187,6 +188,7 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<GroupListViewModel>();
|
services.AddSingleton<GroupListViewModel>();
|
||||||
services.AddSingleton<StudentListViewModel>();
|
services.AddSingleton<StudentListViewModel>();
|
||||||
services.AddSingleton<TimetableViewModel>();
|
services.AddSingleton<TimetableViewModel>();
|
||||||
|
services.AddSingleton<WorkTaskListViewModel>();
|
||||||
|
|
||||||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||||||
services.AddTransient<GroupDetailViewModel>();
|
services.AddTransient<GroupDetailViewModel>();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using LehrerApp.Desktop.ViewModels.Groups;
|
|||||||
using LehrerApp.Desktop.ViewModels.Planning;
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Settings;
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
NavItem.Students => _services.GetRequiredService<StudentListViewModel>(),
|
NavItem.Students => _services.GetRequiredService<StudentListViewModel>(),
|
||||||
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
|
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
|
||||||
NavItem.Planner => GetTimetable(),
|
NavItem.Planner => GetTimetable(),
|
||||||
NavItem.Workload => new PlaceholderViewModel { Title = "Arbeitszeit", Icon = "⏱" },
|
NavItem.Workload => GetWorkload(),
|
||||||
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
||||||
_ => CurrentPage,
|
_ => CurrentPage,
|
||||||
};
|
};
|
||||||
@@ -88,6 +89,13 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
return timetable;
|
return timetable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WorkTaskListViewModel GetWorkload()
|
||||||
|
{
|
||||||
|
var workload = _services.GetRequiredService<WorkTaskListViewModel>();
|
||||||
|
workload.Load();
|
||||||
|
return workload;
|
||||||
|
}
|
||||||
|
|
||||||
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
||||||
{
|
{
|
||||||
ActiveNavItem = NavItem.Groups;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
xmlns:vmset="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
xmlns:vmset="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
||||||
xmlns:vp="clr-namespace:LehrerApp.Desktop.Views.Planning"
|
xmlns:vp="clr-namespace:LehrerApp.Desktop.Views.Planning"
|
||||||
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||||
|
xmlns:vw="clr-namespace:LehrerApp.Desktop.Views.Workload"
|
||||||
|
xmlns:vmw="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
|
||||||
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
|
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
|
||||||
x:Class="LehrerApp.Desktop.Views.MainWindow"
|
x:Class="LehrerApp.Desktop.Views.MainWindow"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
@@ -53,6 +55,9 @@
|
|||||||
<DataTemplate DataType="vmp:TimetableViewModel">
|
<DataTemplate DataType="vmp:TimetableViewModel">
|
||||||
<vp:TimetableView/>
|
<vp:TimetableView/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="vmw:WorkTaskListViewModel">
|
||||||
|
<vw:WorkTaskListView/>
|
||||||
|
</DataTemplate>
|
||||||
<DataTemplate DataType="vm:PlaceholderViewModel">
|
<DataTemplate DataType="vm:PlaceholderViewModel">
|
||||||
<views:PlaceholderView/>
|
<views:PlaceholderView/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
|
||||||
|
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Workload.AddEditWorkTaskDialog"
|
||||||
|
x:DataType="vm:AddEditWorkTaskDialogViewModel"
|
||||||
|
Title="{Binding DialogTitle}"
|
||||||
|
Width="420" Height="480" MinWidth="380" MinHeight="420"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
|
<ScrollViewer Grid.Row="0">
|
||||||
|
<StackPanel Spacing="14" Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Title}" PlaceholderText="z.B. Klausur 8a korrigieren"/>
|
||||||
|
<TextBlock Text="{Binding TitleError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding TitleError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Kategorie *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding CategoryOptions}" SelectedItem="{Binding SelectedCategory}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Gruppe (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding Groups}" SelectedItem="{Binding SelectedGroup}"
|
||||||
|
HorizontalAlignment="Stretch" PlaceholderText="Keine Gruppe">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:LearningGroup">
|
||||||
|
<TextBlock Text="{Binding Name}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Fällig am (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding DueDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||||
|
<TextBlock Text="{Binding DueDateError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding DueDateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Geschätzte Dauer in min (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding EstimatedMinutesText}" PlaceholderText="z.B. 90"/>
|
||||||
|
<TextBlock Text="{Binding EstimatedMinutesError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding EstimatedMinutesError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</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>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Speichern" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Workload;
|
||||||
|
|
||||||
|
public partial class AddEditWorkTaskDialog : Window
|
||||||
|
{
|
||||||
|
public AddEditWorkTaskDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnSave(object? s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is AddEditWorkTaskDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
|
||||||
|
xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Workload.WorkTaskListView"
|
||||||
|
x:DataType="vm:WorkTaskListViewModel">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*">
|
||||||
|
<Border Grid.Row="0" Padding="20,16"
|
||||||
|
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<shared:PageHeader Grid.Column="0" Title="Arbeitszeit" Subtitle="{Binding CountSummary}"/>
|
||||||
|
<Button Grid.Column="1" Content="+ Neue Aufgabe"
|
||||||
|
Command="{Binding AddTaskCommand}" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto" Margin="16,10" HorizontalAlignment="Left">
|
||||||
|
<ComboBox Grid.Column="0" ItemsSource="{Binding StatusFilterOptions}"
|
||||||
|
SelectedItem="{Binding StatusFilter}" Margin="0,0,8,0"/>
|
||||||
|
<ComboBox Grid.Column="1" ItemsSource="{Binding CategoryFilterOptions}"
|
||||||
|
SelectedItem="{Binding CategoryFilter}" Margin="0,0,8,0"/>
|
||||||
|
<ComboBox Grid.Column="2" ItemsSource="{Binding GroupFilterOptions}"
|
||||||
|
SelectedItem="{Binding GroupFilter}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="2" Margin="16,0,16,16">
|
||||||
|
<StackPanel>
|
||||||
|
<ItemsControl ItemsSource="{Binding Tasks}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<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"
|
||||||
|
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">
|
||||||
|
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" FontSize="13"/>
|
||||||
|
<TextBlock FontSize="11" Opacity="0.6">
|
||||||
|
<Run Text="{Binding CategoryLabel}"/>
|
||||||
|
<Run Text=" · "/>
|
||||||
|
<Run Text="{Binding GroupName}"/>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding DueDateDisplay}" VerticalAlignment="Center"
|
||||||
|
Margin="0,0,10,0" FontSize="12" Foreground="{Binding DueDateColorHex}"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding EstimatedMinutesDisplay}"
|
||||||
|
VerticalAlignment="Center" Opacity="0.6" FontSize="12" Margin="0,0,10,0"/>
|
||||||
|
<Button Grid.Column="4" 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"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).DeleteTaskCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine Aufgaben in dieser Filteransicht." Opacity="0.6" Margin="4,12"
|
||||||
|
IsVisible="{Binding !Tasks.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public partial class WorkTaskListView : UserControl
|
||||||
|
{
|
||||||
|
public WorkTaskListView() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is WorkTaskListViewModel vm)
|
||||||
|
vm.OnEditTask = ShowEditDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<WorkTask?> ShowEditDialog(WorkTask? source)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
var dialog = new AddEditWorkTaskDialog { DataContext = vm };
|
||||||
|
await dialog.ShowDialog<bool>(owner);
|
||||||
|
return vm.Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -778,13 +778,22 @@ dadurch faktisch schon (Quick-Input-Dialog bzw. "Offene Entschuldigungen" im Das
|
|||||||
## 6. Arbeitszeit & Aufgaben
|
## 6. Arbeitszeit & Aufgaben
|
||||||
|
|
||||||
Modelle `WorkTask` und `TimeEntry` existieren, Repositories ebenfalls.
|
Modelle `WorkTask` und `TimeEntry` existieren, Repositories ebenfalls.
|
||||||
Navigationspunkt "Arbeitszeit" ist ein `PlaceholderViewModel`.
|
Navigationspunkt "Arbeitszeit" zeigt inzwischen die Aufgabenverwaltung (6.1) statt eines
|
||||||
|
Placeholders; Zeiterfassung (6.2/6.3) ist noch offen.
|
||||||
|
|
||||||
### 6.1 Aufgabenverwaltung
|
### 6.1 Aufgabenverwaltung
|
||||||
- [ ] **6.1.1** Aufgabenliste mit Filter nach Status, Kategorie, Gruppe und Fälligkeit.
|
- [x] **6.1.1** Aufgabenliste mit Filter nach Status, Kategorie, Gruppe und Fälligkeit —
|
||||||
- [ ] **6.1.2** Aufgabe anlegen/bearbeiten: Titel, Kategorie, Gruppe, Fälligkeit,
|
`WorkTaskListViewModel`/`WorkTaskListView`. Drei Filter-ComboBoxen (Status/Kategorie/Gruppe);
|
||||||
geschätzte Dauer, Notizen.
|
"Fälligkeit" als Sortierung (fällige zuerst, unbefristete ans Ende) statt eigenem
|
||||||
- [ ] **6.1.3** Status per Klick wechseln (`Open → InProgress → Done`), erledigte ausblenden.
|
Datumsbereich-Filter umgesetzt, da eine Liste mit wenigen Dutzend Aufgaben davon mehr
|
||||||
|
profitiert als von einer zusätzlichen Filter-UI.
|
||||||
|
- [x] **6.1.2** Aufgabe anlegen/bearbeiten: Titel, Kategorie, Gruppe, Fälligkeit,
|
||||||
|
geschätzte Dauer, Notizen — `AddEditWorkTaskDialog`, gleiches Feld-Fehler-Muster wie die
|
||||||
|
übrigen Dialoge (`{Field}Error` je Feld, Validierung sammelt statt beim ersten Fehler
|
||||||
|
abzubrechen).
|
||||||
|
- [x] **6.1.3** Status per Klick wechseln (`Open → InProgress → Done → Open`) über eine farbige
|
||||||
|
Status-Kachel je Zeile; "Offene Aufgaben" (alles außer `Done`) ist der Default-Filter, damit
|
||||||
|
Erledigtes automatisch ausgeblendet ist, ohne separate Sichtbarkeits-Logik.
|
||||||
- [ ] **6.1.4** Wiederkehrende Aufgaben (wöchentlich/monatlich).
|
- [ ] **6.1.4** Wiederkehrende Aufgaben (wöchentlich/monatlich).
|
||||||
- [ ] **6.1.5** Automatische Aufgabe "Klausur korrigieren" beim Statuswechsel einer Klausur
|
- [ ] **6.1.5** Automatische Aufgabe "Klausur korrigieren" beim Statuswechsel einer Klausur
|
||||||
auf `Conducted` (Anbindung an 1.1.3).
|
auf `Conducted` (Anbindung an 1.1.3).
|
||||||
|
|||||||
Reference in New Issue
Block a user