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 WorkTaskStatus Status { get; set; } = WorkTaskStatus.Open;
|
||||
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 UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -103,3 +105,4 @@ public class TimeEntry
|
||||
}
|
||||
public enum TaskCategory { Correction, Preparation, Admin, Meeting, Other }
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public void DeleteTask_EntferntAufgabeAusDerListe()
|
||||
{
|
||||
@@ -193,6 +253,37 @@ public sealed class AddEditWorkTaskDialogViewModelTests
|
||||
Assert.Equal(source.Id, vm.Result!.Id);
|
||||
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
|
||||
|
||||
@@ -179,6 +179,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
|
||||
[ObservableProperty] private LearningGroup? _group;
|
||||
[ObservableProperty] private string _groupTitle = "";
|
||||
@@ -224,12 +225,12 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
|
||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||
IExamRepository exams, IGradeRepository grades,
|
||||
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
||||
PlanningTabViewModel planningTab)
|
||||
{
|
||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||
_exams = exams; _grades = grades;
|
||||
_exams = exams; _grades = grades; _tasks = tasks;
|
||||
ParticipationTab = participationTab;
|
||||
GradeOverviewTab = gradeOverviewTab;
|
||||
PlanningTab = planningTab;
|
||||
@@ -418,12 +419,25 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
if (SelectedExam is null) return;
|
||||
var exam = _exams.GetById(SelectedExam.Id);
|
||||
if (exam is null) return;
|
||||
var previousStatus = exam.Status;
|
||||
exam.Status = status;
|
||||
if (status == ExamStatus.Returned)
|
||||
exam.ReturnedAt ??= DateOnly.FromDateTime(DateTime.Today);
|
||||
else
|
||||
exam.ReturnedAt = null;
|
||||
_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;
|
||||
ReloadExams();
|
||||
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);
|
||||
}
|
||||
|
||||
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) ─────────────────────────────────────────────
|
||||
|
||||
public partial class WorkTaskListViewModel : ObservableObject
|
||||
@@ -168,8 +184,28 @@ public partial class WorkTaskListViewModel : ObservableObject
|
||||
private void CycleStatus(WorkTaskListItem? item)
|
||||
{
|
||||
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);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -196,6 +232,10 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut
|
||||
public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status);
|
||||
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.
|
||||
public bool HasActualTime => actualMinutes > 0;
|
||||
public string ActualVsEstimateDisplay => Model.EstimatedMinutes is { } estimate
|
||||
@@ -214,6 +254,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private LearningGroup? _selectedGroup;
|
||||
[ObservableProperty] private string _dueDateText = "";
|
||||
[ObservableProperty] private string _estimatedMinutesText = "";
|
||||
[ObservableProperty] private string _selectedRecurrence = TaskRecurrenceDisplay.Options[0];
|
||||
[ObservableProperty] private string _notes = "";
|
||||
[ObservableProperty] private string _titleError = "";
|
||||
[ObservableProperty] private string _dueDateError = "";
|
||||
@@ -221,6 +262,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
||||
|
||||
public string DialogTitle => _source is null ? "Aufgabe anlegen" : "Aufgabe bearbeiten";
|
||||
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
|
||||
public List<string> RecurrenceOptions { get; } = [.. TaskRecurrenceDisplay.Options];
|
||||
public List<LearningGroup> Groups { get; }
|
||||
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;
|
||||
DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
|
||||
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
|
||||
SelectedRecurrence = TaskRecurrenceDisplay.Label(source.Recurrence);
|
||||
Notes = source.Notes ?? "";
|
||||
}
|
||||
|
||||
@@ -254,6 +297,10 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
||||
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;
|
||||
if (!string.IsNullOrWhiteSpace(EstimatedMinutesText))
|
||||
{
|
||||
@@ -272,6 +319,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
||||
GroupId = SelectedGroup?.Id,
|
||||
DueDate = dueDate,
|
||||
EstimatedMinutes = estimatedMinutes,
|
||||
Recurrence = recurrence,
|
||||
Status = _source?.Status ?? WorkTaskStatus.Open,
|
||||
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
|
||||
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
</StackPanel>
|
||||
</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">
|
||||
<TextBlock Text="Notizen (optional)" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Notes}" AcceptsReturn="True" TextWrapping="Wrap" Height="80"/>
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding GroupName}"/>
|
||||
</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) -->
|
||||
<TextBlock Text="{Binding ActualVsEstimateDisplay}" FontSize="11" Opacity="0.6"
|
||||
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.
|
||||
Navigationspunkt "Arbeitszeit" zeigt Aufgabenverwaltung (6.1), Zeiterfassung (6.2) und Auswertung
|
||||
(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
|
||||
nicht existierende Kapitel 11 (Export-Infrastruktur) hängt.
|
||||
`GroupDetailViewModel`). **Kapitel 6 ist damit vollständig abgeschlossen** bis auf den Export der
|
||||
Auswertung (6.3.3), der am noch nicht existierenden Kapitel 11 (Export-Infrastruktur) hängt.
|
||||
|
||||
### 6.1 Aufgabenverwaltung
|
||||
- [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
|
||||
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.5** Automatische Aufgabe "Klausur korrigieren" beim Statuswechsel einer Klausur
|
||||
auf `Conducted` (Anbindung an 1.1.3).
|
||||
- [x] **6.1.4** Wiederkehrende Aufgaben (wöchentlich/monatlich) — `WorkTask.Recurrence`
|
||||
(`TaskRecurrence`: None/Weekly/Monthly). Kein eigenes Serien-/Vorlagen-Modell: Beim
|
||||
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
|
||||
- [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
|
||||
möglicher Companion-Client bleibt bewusst minimal).
|
||||
**→ 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