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