Arbeitszeit: wiederkehrende Aufgaben + Auto-Aufgabe bei Klausurkorrektur (6.1.4/6.1.5)
Damit ist Kapitel 6 (Arbeitszeit & Aufgaben) vollständig abgeschlossen (bis auf den Export der Auswertung, der an das noch fehlende Kapitel 11 hängt). - WorkTask.Recurrence (None/Weekly/Monthly): beim Abschließen einer wiederkehrenden Aufgabe wird automatisch die nächste Instanz mit verschobenem Fälligkeitsdatum erzeugt. - GroupDetailViewModel.SetExamStatus legt beim ersten Wechsel einer Klausur von "Geplant" auf "Durchgeführt" automatisch eine Korrektur-Aufgabe an.
This commit is contained in:
@@ -85,6 +85,8 @@ public class WorkTask
|
|||||||
public int? EstimatedMinutes { get; set; }
|
public int? EstimatedMinutes { get; set; }
|
||||||
public WorkTaskStatus Status { get; set; } = WorkTaskStatus.Open;
|
public WorkTaskStatus Status { get; set; } = WorkTaskStatus.Open;
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
|
// Beim Abschließen (6.1.3) wird bei Weekly/Monthly automatisch die nächste Aufgabe erzeugt (6.1.4).
|
||||||
|
public TaskRecurrence Recurrence { get; set; } = TaskRecurrence.None;
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -103,3 +105,4 @@ public class TimeEntry
|
|||||||
}
|
}
|
||||||
public enum TaskCategory { Correction, Preparation, Admin, Meeting, Other }
|
public enum TaskCategory { Correction, Preparation, Admin, Meeting, Other }
|
||||||
public enum WorkTaskStatus { Open, InProgress, Done }
|
public enum WorkTaskStatus { Open, InProgress, Done }
|
||||||
|
public enum TaskRecurrence { None, Weekly, Monthly }
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class GroupDetailViewModelTests
|
||||||
|
{
|
||||||
|
private static (GroupDetailViewModel Vm, FakeExams Exams, FakeWorkTasks Tasks) Build(LearningGroup group, Exam exam)
|
||||||
|
{
|
||||||
|
var exams = new FakeExams([exam]);
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
var groups = new FakeGroups([group]);
|
||||||
|
var students = new FakeStudents([]);
|
||||||
|
var memberships = new FakeMemberships([]);
|
||||||
|
var subjects = new FakeSubjects([]);
|
||||||
|
var grades = new FakeGrades();
|
||||||
|
|
||||||
|
var vm = new GroupDetailViewModel(groups, students, memberships, subjects, exams, grades, tasks,
|
||||||
|
new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(),
|
||||||
|
students, memberships, groups, new FakeCompetencyDomains()),
|
||||||
|
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
||||||
|
new PlanningTabViewModel(new FakeUnits(), new FakeLessons(), groups, subjects, new FakeCompetencyDomains()));
|
||||||
|
|
||||||
|
vm.LoadGroup(group.Id);
|
||||||
|
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
||||||
|
return (vm, exams, tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetExamStatus_PlannedZuConducted_ErstelltKorrekturAufgabe()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" };
|
||||||
|
var exam = new Exam { GroupId = group.Id, Title = "Klausur 1", Date = new DateOnly(2026, 3, 10) };
|
||||||
|
var (vm, _, tasks) = Build(group, exam);
|
||||||
|
|
||||||
|
vm.SetExamStatusCommand.Execute(ExamStatus.Conducted);
|
||||||
|
|
||||||
|
var task = Assert.Single(tasks.GetAll());
|
||||||
|
Assert.Equal("Klausur korrigieren: Klausur 1", task.Title);
|
||||||
|
Assert.Equal(TaskCategory.Correction, task.Category);
|
||||||
|
Assert.Equal(group.Id, task.GroupId);
|
||||||
|
Assert.Equal(new DateOnly(2026, 3, 24), task.DueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetExamStatus_ConductedErneutGesetzt_ErstelltKeineZweiteAufgabe()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" };
|
||||||
|
var exam = new Exam { GroupId = group.Id, Title = "Klausur 1", Date = new DateOnly(2026, 3, 10) };
|
||||||
|
var (vm, _, tasks) = Build(group, exam);
|
||||||
|
|
||||||
|
vm.SetExamStatusCommand.Execute(ExamStatus.Conducted);
|
||||||
|
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
||||||
|
vm.SetExamStatusCommand.Execute(ExamStatus.Conducted);
|
||||||
|
|
||||||
|
Assert.Single(tasks.GetAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetExamStatus_DirektAufGraded_ErstelltKeineAufgabe()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" };
|
||||||
|
var exam = new Exam { GroupId = group.Id, Title = "Klausur 1", Date = new DateOnly(2026, 3, 10) };
|
||||||
|
var (vm, _, tasks) = Build(group, exam);
|
||||||
|
|
||||||
|
vm.SetExamStatusCommand.Execute(ExamStatus.Graded);
|
||||||
|
|
||||||
|
Assert.Empty(tasks.GetAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,66 @@ public sealed class WorkTaskListViewModelTests
|
|||||||
Assert.Equal(WorkTaskStatus.Open, vm.Tasks[0].Model.Status);
|
Assert.Equal(WorkTaskStatus.Open, vm.Tasks[0].Model.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CycleStatus_WoechentlicheWiederholungAufErledigt_ErzeugtNaechsteInstanz()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask
|
||||||
|
{
|
||||||
|
Title = "Wochenbericht", Status = WorkTaskStatus.InProgress,
|
||||||
|
Recurrence = TaskRecurrence.Weekly, DueDate = new DateOnly(2026, 3, 10),
|
||||||
|
Category = TaskCategory.Admin,
|
||||||
|
});
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries())
|
||||||
|
{
|
||||||
|
StatusFilter = WorkTaskListViewModel.AllFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.CycleStatusCommand.Execute(vm.Tasks[0]); // -> Done
|
||||||
|
|
||||||
|
var all = tasks.GetAll();
|
||||||
|
Assert.Equal(2, all.Count);
|
||||||
|
var next = all.Single(t => t.Status == WorkTaskStatus.Open);
|
||||||
|
Assert.Equal("Wochenbericht", next.Title);
|
||||||
|
Assert.Equal(new DateOnly(2026, 3, 17), next.DueDate);
|
||||||
|
Assert.Equal(TaskRecurrence.Weekly, next.Recurrence);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CycleStatus_MonatlicheWiederholung_AddiertEinenMonat()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask
|
||||||
|
{
|
||||||
|
Title = "Bericht", Status = WorkTaskStatus.InProgress,
|
||||||
|
Recurrence = TaskRecurrence.Monthly, DueDate = new DateOnly(2026, 3, 10),
|
||||||
|
});
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries())
|
||||||
|
{
|
||||||
|
StatusFilter = WorkTaskListViewModel.AllFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.CycleStatusCommand.Execute(vm.Tasks[0]); // -> Done
|
||||||
|
|
||||||
|
var next = tasks.GetAll().Single(t => t.Status == WorkTaskStatus.Open);
|
||||||
|
Assert.Equal(new DateOnly(2026, 4, 10), next.DueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CycleStatus_OhneWiederholung_ErzeugtKeineNeueAufgabe()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.InProgress });
|
||||||
|
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries())
|
||||||
|
{
|
||||||
|
StatusFilter = WorkTaskListViewModel.AllFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.CycleStatusCommand.Execute(vm.Tasks[0]); // -> Done
|
||||||
|
|
||||||
|
Assert.Single(tasks.GetAll());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DeleteTask_EntferntAufgabeAusDerListe()
|
public void DeleteTask_EntferntAufgabeAusDerListe()
|
||||||
{
|
{
|
||||||
@@ -193,6 +253,37 @@ public sealed class AddEditWorkTaskDialogViewModelTests
|
|||||||
Assert.Equal(source.Id, vm.Result!.Id);
|
Assert.Equal(source.Id, vm.Result!.Id);
|
||||||
Assert.Equal(WorkTaskStatus.InProgress, vm.Result.Status);
|
Assert.Equal(WorkTaskStatus.InProgress, vm.Result.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_WiederholungOhneFaelligkeitsdatum_SetztFehler()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, [])
|
||||||
|
{
|
||||||
|
Title = "T",
|
||||||
|
SelectedRecurrence = TaskRecurrenceDisplay.Label(TaskRecurrence.Weekly),
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Null(vm.Result);
|
||||||
|
Assert.NotEmpty(vm.DueDateError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_WiederholungMitFaelligkeitsdatum_UebernimmtRecurrence()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, [])
|
||||||
|
{
|
||||||
|
Title = "T",
|
||||||
|
DueDateText = "10.03.2026",
|
||||||
|
SelectedRecurrence = TaskRecurrenceDisplay.Label(TaskRecurrence.Monthly),
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.NotNull(vm.Result);
|
||||||
|
Assert.Equal(TaskRecurrence.Monthly, vm.Result!.Recurrence);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TimeTrackingViewModelTests
|
public sealed class TimeTrackingViewModelTests
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
private readonly ISubjectRepository _subjects;
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly IExamRepository _exams;
|
private readonly IExamRepository _exams;
|
||||||
private readonly IGradeRepository _grades;
|
private readonly IGradeRepository _grades;
|
||||||
|
private readonly IWorkTaskRepository _tasks;
|
||||||
|
|
||||||
[ObservableProperty] private LearningGroup? _group;
|
[ObservableProperty] private LearningGroup? _group;
|
||||||
[ObservableProperty] private string _groupTitle = "";
|
[ObservableProperty] private string _groupTitle = "";
|
||||||
@@ -224,12 +225,12 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
|
|
||||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||||
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||||
IExamRepository exams, IGradeRepository grades,
|
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
||||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
||||||
PlanningTabViewModel planningTab)
|
PlanningTabViewModel planningTab)
|
||||||
{
|
{
|
||||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||||
_exams = exams; _grades = grades;
|
_exams = exams; _grades = grades; _tasks = tasks;
|
||||||
ParticipationTab = participationTab;
|
ParticipationTab = participationTab;
|
||||||
GradeOverviewTab = gradeOverviewTab;
|
GradeOverviewTab = gradeOverviewTab;
|
||||||
PlanningTab = planningTab;
|
PlanningTab = planningTab;
|
||||||
@@ -418,12 +419,25 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
if (SelectedExam is null) return;
|
if (SelectedExam is null) return;
|
||||||
var exam = _exams.GetById(SelectedExam.Id);
|
var exam = _exams.GetById(SelectedExam.Id);
|
||||||
if (exam is null) return;
|
if (exam is null) return;
|
||||||
|
var previousStatus = exam.Status;
|
||||||
exam.Status = status;
|
exam.Status = status;
|
||||||
if (status == ExamStatus.Returned)
|
if (status == ExamStatus.Returned)
|
||||||
exam.ReturnedAt ??= DateOnly.FromDateTime(DateTime.Today);
|
exam.ReturnedAt ??= DateOnly.FromDateTime(DateTime.Today);
|
||||||
else
|
else
|
||||||
exam.ReturnedAt = null;
|
exam.ReturnedAt = null;
|
||||||
_exams.Save(exam);
|
_exams.Save(exam);
|
||||||
|
|
||||||
|
// 6.1.5: beim erstmaligen Erreichen von "Durchgeführt" automatisch eine Korrektur-Aufgabe anlegen.
|
||||||
|
if (status == ExamStatus.Conducted && previousStatus == ExamStatus.Planned)
|
||||||
|
_tasks.Save(new WorkTask
|
||||||
|
{
|
||||||
|
Title = $"Klausur korrigieren: {exam.Title}",
|
||||||
|
Category = TaskCategory.Correction,
|
||||||
|
GroupId = exam.GroupId,
|
||||||
|
DueDate = exam.Date.AddDays(14),
|
||||||
|
Status = WorkTaskStatus.Open,
|
||||||
|
});
|
||||||
|
|
||||||
var id = exam.Id;
|
var id = exam.Id;
|
||||||
ReloadExams();
|
ReloadExams();
|
||||||
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
|
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
|
||||||
|
|||||||
@@ -55,6 +55,22 @@ public static class TaskCategoryDisplay
|
|||||||
Enum.GetValues<TaskCategory>().FirstOrDefault(c => Label(c) == label, TaskCategory.Other);
|
Enum.GetValues<TaskCategory>().FirstOrDefault(c => Label(c) == label, TaskCategory.Other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TaskRecurrenceDisplay
|
||||||
|
{
|
||||||
|
public static string Label(TaskRecurrence r) => r switch
|
||||||
|
{
|
||||||
|
TaskRecurrence.None => "Keine",
|
||||||
|
TaskRecurrence.Weekly => "Wöchentlich",
|
||||||
|
TaskRecurrence.Monthly => "Monatlich",
|
||||||
|
_ => r.ToString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string[] Options { get; } = Enum.GetValues<TaskRecurrence>().Select(Label).ToArray();
|
||||||
|
|
||||||
|
public static TaskRecurrence FromLabel(string? label) =>
|
||||||
|
Enum.GetValues<TaskRecurrence>().FirstOrDefault(r => Label(r) == label, TaskRecurrence.None);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Aufgabenliste (6.1.1, 6.1.3) ─────────────────────────────────────────────
|
// ── Aufgabenliste (6.1.1, 6.1.3) ─────────────────────────────────────────────
|
||||||
|
|
||||||
public partial class WorkTaskListViewModel : ObservableObject
|
public partial class WorkTaskListViewModel : ObservableObject
|
||||||
@@ -168,8 +184,28 @@ public partial class WorkTaskListViewModel : ObservableObject
|
|||||||
private void CycleStatus(WorkTaskListItem? item)
|
private void CycleStatus(WorkTaskListItem? item)
|
||||||
{
|
{
|
||||||
if (item is null) return;
|
if (item is null) return;
|
||||||
item.Model.Status = WorkTaskStatusDisplay.Next(item.Model.Status);
|
var newStatus = WorkTaskStatusDisplay.Next(item.Model.Status);
|
||||||
|
item.Model.Status = newStatus;
|
||||||
_tasks.Save(item.Model);
|
_tasks.Save(item.Model);
|
||||||
|
|
||||||
|
// Wiederkehrende Aufgabe (6.1.4): beim Abschließen automatisch die nächste Instanz anlegen.
|
||||||
|
if (newStatus == WorkTaskStatus.Done && item.Model.Recurrence != TaskRecurrence.None
|
||||||
|
&& item.Model.DueDate is { } dueDate)
|
||||||
|
{
|
||||||
|
_tasks.Save(new WorkTask
|
||||||
|
{
|
||||||
|
Title = item.Model.Title,
|
||||||
|
Category = item.Model.Category,
|
||||||
|
GroupId = item.Model.GroupId,
|
||||||
|
DueDate = item.Model.Recurrence == TaskRecurrence.Weekly
|
||||||
|
? dueDate.AddDays(7) : dueDate.AddMonths(1),
|
||||||
|
EstimatedMinutes = item.Model.EstimatedMinutes,
|
||||||
|
Recurrence = item.Model.Recurrence,
|
||||||
|
Notes = item.Model.Notes,
|
||||||
|
Status = WorkTaskStatus.Open,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +232,10 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut
|
|||||||
public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status);
|
public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status);
|
||||||
public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : "";
|
public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : "";
|
||||||
|
|
||||||
|
// Wiederkehrende Aufgabe (6.1.4).
|
||||||
|
public bool IsRecurring => Model.Recurrence != TaskRecurrence.None;
|
||||||
|
public string RecurrenceLabel => $"🔁 {TaskRecurrenceDisplay.Label(Model.Recurrence)}";
|
||||||
|
|
||||||
// Ist-Zeit vs. Schätzung (6.2.4) — nur anzeigen, wenn tatsächlich etwas erfasst wurde.
|
// Ist-Zeit vs. Schätzung (6.2.4) — nur anzeigen, wenn tatsächlich etwas erfasst wurde.
|
||||||
public bool HasActualTime => actualMinutes > 0;
|
public bool HasActualTime => actualMinutes > 0;
|
||||||
public string ActualVsEstimateDisplay => Model.EstimatedMinutes is { } estimate
|
public string ActualVsEstimateDisplay => Model.EstimatedMinutes is { } estimate
|
||||||
@@ -214,6 +254,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
[ObservableProperty] private LearningGroup? _selectedGroup;
|
[ObservableProperty] private LearningGroup? _selectedGroup;
|
||||||
[ObservableProperty] private string _dueDateText = "";
|
[ObservableProperty] private string _dueDateText = "";
|
||||||
[ObservableProperty] private string _estimatedMinutesText = "";
|
[ObservableProperty] private string _estimatedMinutesText = "";
|
||||||
|
[ObservableProperty] private string _selectedRecurrence = TaskRecurrenceDisplay.Options[0];
|
||||||
[ObservableProperty] private string _notes = "";
|
[ObservableProperty] private string _notes = "";
|
||||||
[ObservableProperty] private string _titleError = "";
|
[ObservableProperty] private string _titleError = "";
|
||||||
[ObservableProperty] private string _dueDateError = "";
|
[ObservableProperty] private string _dueDateError = "";
|
||||||
@@ -221,6 +262,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
public string DialogTitle => _source is null ? "Aufgabe anlegen" : "Aufgabe bearbeiten";
|
public string DialogTitle => _source is null ? "Aufgabe anlegen" : "Aufgabe bearbeiten";
|
||||||
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
|
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
|
||||||
|
public List<string> RecurrenceOptions { get; } = [.. TaskRecurrenceDisplay.Options];
|
||||||
public List<LearningGroup> Groups { get; }
|
public List<LearningGroup> Groups { get; }
|
||||||
public WorkTask? Result { get; private set; }
|
public WorkTask? Result { get; private set; }
|
||||||
|
|
||||||
@@ -235,6 +277,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
SelectedGroup = source.GroupId is { } id ? groups.FirstOrDefault(g => g.Id == id) : null;
|
SelectedGroup = source.GroupId is { } id ? groups.FirstOrDefault(g => g.Id == id) : null;
|
||||||
DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
|
DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
|
||||||
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
|
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
|
||||||
|
SelectedRecurrence = TaskRecurrenceDisplay.Label(source.Recurrence);
|
||||||
Notes = source.Notes ?? "";
|
Notes = source.Notes ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +297,10 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
else dueDate = d;
|
else dueDate = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var recurrence = TaskRecurrenceDisplay.FromLabel(SelectedRecurrence);
|
||||||
|
if (recurrence != TaskRecurrence.None && dueDate is null)
|
||||||
|
{ DueDateError = "Fälligkeitsdatum erforderlich, damit die nächste Instanz geplant werden kann."; valid = false; }
|
||||||
|
|
||||||
int? estimatedMinutes = null;
|
int? estimatedMinutes = null;
|
||||||
if (!string.IsNullOrWhiteSpace(EstimatedMinutesText))
|
if (!string.IsNullOrWhiteSpace(EstimatedMinutesText))
|
||||||
{
|
{
|
||||||
@@ -272,6 +319,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
GroupId = SelectedGroup?.Id,
|
GroupId = SelectedGroup?.Id,
|
||||||
DueDate = dueDate,
|
DueDate = dueDate,
|
||||||
EstimatedMinutes = estimatedMinutes,
|
EstimatedMinutes = estimatedMinutes,
|
||||||
|
Recurrence = recurrence,
|
||||||
Status = _source?.Status ?? WorkTaskStatus.Open,
|
Status = _source?.Status ?? WorkTaskStatus.Open,
|
||||||
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
|
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
|
||||||
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
|
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
|
||||||
|
|||||||
@@ -54,6 +54,13 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</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>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Notizen (optional)" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Notizen (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
<TextBox Text="{Binding Notes}" AcceptsReturn="True" TextWrapping="Wrap" Height="80"/>
|
<TextBox Text="{Binding Notes}" AcceptsReturn="True" TextWrapping="Wrap" Height="80"/>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
<Run Text=" · "/>
|
<Run Text=" · "/>
|
||||||
<Run Text="{Binding GroupName}"/>
|
<Run Text="{Binding GroupName}"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
<!-- Wiederkehrende Aufgabe (6.1.4) -->
|
||||||
|
<TextBlock Text="{Binding RecurrenceLabel}" FontSize="11" Opacity="0.6"
|
||||||
|
IsVisible="{Binding IsRecurring}"/>
|
||||||
<!-- Ist-Zeit vs. Schätzung (6.2.4) -->
|
<!-- Ist-Zeit vs. Schätzung (6.2.4) -->
|
||||||
<TextBlock Text="{Binding ActualVsEstimateDisplay}" FontSize="11" Opacity="0.6"
|
<TextBlock Text="{Binding ActualVsEstimateDisplay}" FontSize="11" Opacity="0.6"
|
||||||
IsVisible="{Binding HasActualTime}"/>
|
IsVisible="{Binding HasActualTime}"/>
|
||||||
|
|||||||
@@ -780,8 +780,8 @@ dadurch faktisch schon (Quick-Input-Dialog bzw. "Offene Entschuldigungen" im Das
|
|||||||
Modelle `WorkTask` und `TimeEntry` existieren, Repositories ebenfalls.
|
Modelle `WorkTask` und `TimeEntry` existieren, Repositories ebenfalls.
|
||||||
Navigationspunkt "Arbeitszeit" zeigt Aufgabenverwaltung (6.1), Zeiterfassung (6.2) und Auswertung
|
Navigationspunkt "Arbeitszeit" zeigt Aufgabenverwaltung (6.1), Zeiterfassung (6.2) und Auswertung
|
||||||
(6.3) als drei Tabs (`WorkloadViewModel`/`WorkloadView`, gleiches Container-Tab-Muster wie
|
(6.3) als drei Tabs (`WorkloadViewModel`/`WorkloadView`, gleiches Container-Tab-Muster wie
|
||||||
`GroupDetailViewModel`) — nur der Export der Auswertung (6.3.3) ist noch offen, da er an das noch
|
`GroupDetailViewModel`). **Kapitel 6 ist damit vollständig abgeschlossen** bis auf den Export der
|
||||||
nicht existierende Kapitel 11 (Export-Infrastruktur) hängt.
|
Auswertung (6.3.3), der am noch nicht existierenden Kapitel 11 (Export-Infrastruktur) hängt.
|
||||||
|
|
||||||
### 6.1 Aufgabenverwaltung
|
### 6.1 Aufgabenverwaltung
|
||||||
- [x] **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 —
|
||||||
@@ -796,9 +796,17 @@ nicht existierende Kapitel 11 (Export-Infrastruktur) hängt.
|
|||||||
- [x] **6.1.3** Status per Klick wechseln (`Open → InProgress → Done → Open`) über eine farbige
|
- [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
|
Status-Kachel je Zeile; "Offene Aufgaben" (alles außer `Done`) ist der Default-Filter, damit
|
||||||
Erledigtes automatisch ausgeblendet ist, ohne separate Sichtbarkeits-Logik.
|
Erledigtes automatisch ausgeblendet ist, ohne separate Sichtbarkeits-Logik.
|
||||||
- [ ] **6.1.4** Wiederkehrende Aufgaben (wöchentlich/monatlich).
|
- [x] **6.1.4** Wiederkehrende Aufgaben (wöchentlich/monatlich) — `WorkTask.Recurrence`
|
||||||
- [ ] **6.1.5** Automatische Aufgabe "Klausur korrigieren" beim Statuswechsel einer Klausur
|
(`TaskRecurrence`: None/Weekly/Monthly). Kein eigenes Serien-/Vorlagen-Modell: Beim
|
||||||
auf `Conducted` (Anbindung an 1.1.3).
|
Abschließen (Status → `Done` in 6.1.3) wird einfach eine neue `WorkTask` mit gleichem
|
||||||
|
Titel/Kategorie/Gruppe und um 7 Tage bzw. 1 Monat verschobenem Fälligkeitsdatum erzeugt —
|
||||||
|
genau das Verhalten, das ein Lehrer von "wöchentlich wiederkehrend" erwartet, ohne
|
||||||
|
Hintergrundjob oder Kalenderlogik. Braucht deshalb ein Fälligkeitsdatum als Ausgangspunkt
|
||||||
|
(im Dialog erzwungen, sobald eine Wiederholung gewählt ist).
|
||||||
|
- [x] **6.1.5** Automatische Aufgabe "Klausur korrigieren" beim Statuswechsel einer Klausur
|
||||||
|
auf `Conducted` (Anbindung an 1.1.3) — Hook in `GroupDetailViewModel.SetExamStatus`, feuert
|
||||||
|
nur beim *erstmaligen* Wechsel von `Planned` auf `Conducted` (nicht bei jedem erneuten
|
||||||
|
Klick), Fälligkeit 14 Tage nach dem Klausurtermin, Kategorie `Correction`.
|
||||||
|
|
||||||
### 6.2 Zeiterfassung
|
### 6.2 Zeiterfassung
|
||||||
- [x] **6.2.1** Timer starten/stoppen mit Zuordnung zu Aufgabe oder Kategorie —
|
- [x] **6.2.1** Timer starten/stoppen mit Zuordnung zu Aufgabe oder Kategorie —
|
||||||
@@ -1273,4 +1281,8 @@ Die Abschnitte sind thematisch, nicht chronologisch nummeriert. Sinnvolle Bearbe
|
|||||||
Unklarheit; Desktop bekommt dort perspektivisch Graph/Kompetenz-Verknüpfung/KI-Planung, ein
|
Unklarheit; Desktop bekommt dort perspektivisch Graph/Kompetenz-Verknüpfung/KI-Planung, ein
|
||||||
möglicher Companion-Client bleibt bewusst minimal).
|
möglicher Companion-Client bleibt bewusst minimal).
|
||||||
**→ nächster sinnvoller Schritt: Kapitel 6, 10 oder 11.**
|
**→ nächster sinnvoller Schritt: Kapitel 6, 10 oder 11.**
|
||||||
7. **Kapitel 6** (Arbeitszeit), **11** (Export), **10** (Sync) — danach.
|
7. ~~**Kapitel 6** (Arbeitszeit & Aufgaben)~~ — vollständig erledigt (6.1 Aufgabenverwaltung
|
||||||
|
inkl. wiederkehrender Aufgaben, 6.2 Zeiterfassung, 6.3 Auswertung). Offen bleibt nur **6.3.3**
|
||||||
|
(Export der Auswertung), das am noch fehlenden Kapitel 11 hängt.
|
||||||
|
**→ nächster sinnvoller Schritt: Kapitel 10 (Sync) oder 11 (Export).**
|
||||||
|
8. **Kapitel 11** (Export), **10** (Sync) — danach.
|
||||||
|
|||||||
Reference in New Issue
Block a user