feat: Komfort-Features für Unterrichtsplanung, Zeiterfassung und Dashboard
- Neue-Stunde-Dialog: Datum wird beim Anlegen anhand des Stundenplans
und der letzten Stunde der Einheit vorbelegt statt auf "heute"
(behebt eine falsch erkannte Doppelstunde, wenn "heute" nicht auf
den passenden Wochentag fiel).
- Zeiterfassung: Button "Unterrichtszeit heute übernehmen" schlägt
Start/Ende aus dem heutigen Stundenplan inkl. Puffer davor/danach vor.
- Dashboard: neue Kachel "Ungeplante Stunden" erinnert an Stunden ohne
Thema für heute/morgen, mit Opt-out je Gruppe ("Benötigt
Unterrichtsplanung"), Doppelstunden-Erkennung (keine doppelte Meldung
für die zweite Periode) und Berücksichtigung von Stundenausfall.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Workload;
|
||||
@@ -288,14 +289,16 @@ public sealed class AddEditWorkTaskDialogViewModelTests
|
||||
|
||||
public sealed class TimeTrackingViewModelTests
|
||||
{
|
||||
private static TimeTrackingViewModel BuildVm(ITimeEntryRepository entries, IWorkTaskRepository tasks,
|
||||
FakeTimetableSlots? slots = null) =>
|
||||
new(entries, tasks, slots ?? new FakeTimetableSlots(), TestSupport.BuildPeriodScheduleService());
|
||||
|
||||
[Fact]
|
||||
public void StartStopTimer_SpeichertZeiteintragMitMindestensEinerMinute()
|
||||
{
|
||||
var entries = new FakeTimeEntries();
|
||||
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks())
|
||||
{
|
||||
SelectedTimerCategory = TaskCategoryDisplay.Label(TaskCategory.Correction),
|
||||
};
|
||||
var vm = BuildVm(entries, new FakeWorkTasks());
|
||||
vm.SelectedTimerCategory = TaskCategoryDisplay.Label(TaskCategory.Correction);
|
||||
|
||||
vm.StartTimerCommand.Execute(null);
|
||||
Assert.True(vm.IsTimerRunning);
|
||||
@@ -314,7 +317,7 @@ public sealed class TimeTrackingViewModelTests
|
||||
var task = new WorkTask { Title = "T", Category = TaskCategory.Meeting };
|
||||
var tasks = new FakeWorkTasks();
|
||||
tasks.Add(task);
|
||||
var vm = new TimeTrackingViewModel(new FakeTimeEntries(), tasks);
|
||||
var vm = BuildVm(new FakeTimeEntries(), tasks);
|
||||
|
||||
vm.SelectedTimerTask = task;
|
||||
|
||||
@@ -328,7 +331,7 @@ public sealed class TimeTrackingViewModelTests
|
||||
entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 30 });
|
||||
entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today).AddDays(-30), DurationMinutes = 45 });
|
||||
|
||||
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
|
||||
var vm = BuildVm(entries, new FakeWorkTasks());
|
||||
|
||||
Assert.Single(vm.WeekEntries);
|
||||
Assert.Equal(30, vm.WeekEntries[0].Model.DurationMinutes);
|
||||
@@ -343,7 +346,7 @@ public sealed class TimeTrackingViewModelTests
|
||||
entries.Add(new TimeEntry { Category = "Korrektur", Date = today, DurationMinutes = 20 });
|
||||
entries.Add(new TimeEntry { Category = "Verwaltung", Date = today, DurationMinutes = 10 });
|
||||
|
||||
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
|
||||
var vm = BuildVm(entries, new FakeWorkTasks());
|
||||
|
||||
var korrektur = vm.CategorySummaries.Single(s => s.Category == "Korrektur");
|
||||
Assert.Equal("50 min", korrektur.MinutesDisplay);
|
||||
@@ -356,7 +359,7 @@ public sealed class TimeTrackingViewModelTests
|
||||
{
|
||||
var entries = new FakeTimeEntries();
|
||||
entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 30 });
|
||||
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
|
||||
var vm = BuildVm(entries, new FakeWorkTasks());
|
||||
|
||||
vm.DeleteEntryCommand.Execute(vm.WeekEntries[0]);
|
||||
|
||||
@@ -367,7 +370,7 @@ public sealed class TimeTrackingViewModelTests
|
||||
public async Task AddEntry_RuftOnAddEntryAufUndSpeichertErgebnis()
|
||||
{
|
||||
var entries = new FakeTimeEntries();
|
||||
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
|
||||
var vm = BuildVm(entries, new FakeWorkTasks());
|
||||
vm.OnAddEntry = () => Task.FromResult<TimeEntry?>(
|
||||
new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 15 });
|
||||
|
||||
@@ -375,6 +378,64 @@ public sealed class TimeTrackingViewModelTests
|
||||
|
||||
Assert.Single(vm.WeekEntries);
|
||||
}
|
||||
|
||||
// ── Unterrichtszeit-Vorschlag (Nutzer-Feedback: Zeiterfassung soll bei der Unterrichtszeit
|
||||
// nachfragen statt sie komplett automatisch zu erfassen) ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HasTeachingTimeSuggestionToday_OhneStundenplanHeute_IstFalse()
|
||||
{
|
||||
var vm = BuildVm(new FakeTimeEntries(), new FakeWorkTasks()); // leere FakeTimetableSlots
|
||||
|
||||
Assert.False(vm.HasTeachingTimeSuggestionToday);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuggestTeachingTime_SchlaegtBeginnMitPufferVorErsterUndNachLetzterPeriodeVor()
|
||||
{
|
||||
var today = DateTime.Today.DayOfWeek;
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { Weekday = today, PeriodNumber = 1 });
|
||||
slots.Add(new TimetableSlot { Weekday = today, PeriodNumber = 2 });
|
||||
var periodSchedule = TestSupport.BuildPeriodScheduleService();
|
||||
periodSchedule.SetPeriods([
|
||||
new PeriodTimeEntry { PeriodNumber = 1, Start = new TimeOnly(7, 50), End = new TimeOnly(8, 35) },
|
||||
new PeriodTimeEntry { PeriodNumber = 2, Start = new TimeOnly(8, 35), End = new TimeOnly(9, 20) },
|
||||
]);
|
||||
var entries = new FakeTimeEntries();
|
||||
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks(), slots, periodSchedule);
|
||||
|
||||
Assert.True(vm.HasTeachingTimeSuggestionToday);
|
||||
|
||||
(TimeOnly Start, TimeOnly End)? suggested = null;
|
||||
vm.OnSuggestTeachingTime = (start, end) =>
|
||||
{
|
||||
suggested = (start, end);
|
||||
return Task.FromResult<TimeEntry?>(new TimeEntry
|
||||
{
|
||||
Category = "Unterricht", Date = DateOnly.FromDateTime(DateTime.Today),
|
||||
StartTime = start, EndTime = end, DurationMinutes = (int)(end - start).TotalMinutes,
|
||||
});
|
||||
};
|
||||
|
||||
await vm.SuggestTeachingTimeCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(new TimeOnly(7, 35), suggested!.Value.Start); // 7:50 - 15 Min Puffer
|
||||
Assert.Equal(new TimeOnly(9, 30), suggested.Value.End); // 9:20 + 10 Min Puffer
|
||||
Assert.Single(entries.GetByDate(DateOnly.FromDateTime(DateTime.Today)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuggestTeachingTime_OhneStundenplanHeute_TutNichts()
|
||||
{
|
||||
var vm = BuildVm(new FakeTimeEntries(), new FakeWorkTasks());
|
||||
var called = false;
|
||||
vm.OnSuggestTeachingTime = (_, _) => { called = true; return Task.FromResult<TimeEntry?>(null); };
|
||||
|
||||
await vm.SuggestTeachingTimeCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(called);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AddTimeEntryDialogViewModelTests
|
||||
|
||||
Reference in New Issue
Block a user