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:
2026-08-16 00:39:18 +02:00
parent f9e398b225
commit 2b4fda7bb3
8 changed files with 259 additions and 9 deletions
+3
View File
@@ -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}"/>
+18 -6
View File
@@ -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.