diff --git a/LehrerApp.Desktop.Tests/Fakes.cs b/LehrerApp.Desktop.Tests/Fakes.cs index 35b258d..f8ff7f7 100644 --- a/LehrerApp.Desktop.Tests/Fakes.cs +++ b/LehrerApp.Desktop.Tests/Fakes.cs @@ -280,6 +280,18 @@ public class FakeWorkTasks : IWorkTaskRepository public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id); } +public class FakeTimeEntries : ITimeEntryRepository +{ + private readonly List _all = []; + public void Add(TimeEntry e) => _all.Add(e); + public List GetByDate(DateOnly date) => _all.Where(e => e.Date == date).ToList(); + public List GetByDateRange(DateOnly from, DateOnly to) => + _all.Where(e => e.Date >= from && e.Date <= to).ToList(); + public List GetByTask(Guid taskId) => _all.Where(e => e.TaskId == taskId).ToList(); + public void Save(TimeEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); } + public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id); +} + public class FakeReportGrades : IReportGradeRepository { private readonly List _all = []; diff --git a/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs b/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs index 6487aa9..8fc75cf 100644 --- a/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs @@ -12,7 +12,7 @@ public sealed class WorkTaskListViewModelTests var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Offen", Status = WorkTaskStatus.Open }); tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])); + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); Assert.Single(vm.Tasks); Assert.Equal("Offen", vm.Tasks[0].Title); @@ -23,7 +23,7 @@ public sealed class WorkTaskListViewModelTests { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])) + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; @@ -37,7 +37,7 @@ public sealed class WorkTaskListViewModelTests var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Korrektur", Category = TaskCategory.Correction }); tasks.Add(new WorkTask { Title = "Vorbereitung", Category = TaskCategory.Preparation }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])) + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { CategoryFilter = TaskCategoryDisplay.Label(TaskCategory.Correction), }; @@ -53,7 +53,7 @@ public sealed class WorkTaskListViewModelTests var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Mit Gruppe", GroupId = group.Id }); tasks.Add(new WorkTask { Title = "Ohne Gruppe", GroupId = null }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([group])) + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([group]), new FakeTimeEntries()) { GroupFilter = WorkTaskListViewModel.NoGroupFilter, }; @@ -67,7 +67,7 @@ public sealed class WorkTaskListViewModelTests { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Open }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])) + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; @@ -82,7 +82,7 @@ public sealed class WorkTaskListViewModelTests { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Done }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])) + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; @@ -97,7 +97,7 @@ public sealed class WorkTaskListViewModelTests { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "T" }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])); + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); vm.DeleteTaskCommand.Execute(vm.Tasks[0]); @@ -108,7 +108,7 @@ public sealed class WorkTaskListViewModelTests public async Task AddTask_RuftOnEditTaskAufUndSpeichertErgebnis() { var tasks = new FakeWorkTasks(); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])); + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); vm.OnEditTask = _ => Task.FromResult(new WorkTask { Title = "Neu" }); await vm.AddTaskCommand.ExecuteAsync(null); @@ -124,7 +124,7 @@ public sealed class WorkTaskListViewModelTests tasks.Add(new WorkTask { Title = "Ohne Frist", DueDate = null }); tasks.Add(new WorkTask { Title = "Spät fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(10)) }); tasks.Add(new WorkTask { Title = "Bald fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)) }); - var vm = new WorkTaskListViewModel(tasks, new FakeGroups([])); + var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); Assert.Equal(["Bald fällig", "Spät fällig", "Ohne Frist"], vm.Tasks.Select(t => t.Title)); } @@ -193,3 +193,156 @@ public sealed class AddEditWorkTaskDialogViewModelTests Assert.Equal(WorkTaskStatus.InProgress, vm.Result.Status); } } + +public sealed class TimeTrackingViewModelTests +{ + [Fact] + public void StartStopTimer_SpeichertZeiteintragMitMindestensEinerMinute() + { + var entries = new FakeTimeEntries(); + var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks()) + { + SelectedTimerCategory = TaskCategoryDisplay.Label(TaskCategory.Correction), + }; + + vm.StartTimerCommand.Execute(null); + Assert.True(vm.IsTimerRunning); + + vm.StopTimerCommand.Execute(null); + + Assert.False(vm.IsTimerRunning); + var saved = Assert.Single(entries.GetByDate(DateOnly.FromDateTime(DateTime.Today))); + Assert.True(saved.DurationMinutes >= 1); + Assert.Equal(TaskCategoryDisplay.Label(TaskCategory.Correction), saved.Category); + } + + [Fact] + public void SelectedTimerTask_UebernimmtDessenKategorie() + { + var task = new WorkTask { Title = "T", Category = TaskCategory.Meeting }; + var tasks = new FakeWorkTasks(); + tasks.Add(task); + var vm = new TimeTrackingViewModel(new FakeTimeEntries(), tasks); + + vm.SelectedTimerTask = task; + + Assert.Equal(TaskCategoryDisplay.Label(TaskCategory.Meeting), vm.SelectedTimerCategory); + } + + [Fact] + public void Load_ZeigtNurEintraegeDerAktuellenWoche() + { + var entries = new FakeTimeEntries(); + 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()); + + Assert.Single(vm.WeekEntries); + Assert.Equal(30, vm.WeekEntries[0].Model.DurationMinutes); + } + + [Fact] + public void CategorySummaries_GruppiertUndSummiertProKategorie() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var entries = new FakeTimeEntries(); + entries.Add(new TimeEntry { Category = "Korrektur", Date = today, DurationMinutes = 30 }); + 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 korrektur = vm.CategorySummaries.Single(s => s.Category == "Korrektur"); + Assert.Equal("50 min", korrektur.MinutesDisplay); + Assert.Equal(1.0, korrektur.BarFraction); + Assert.Equal("60 min diese Woche", vm.TotalWeekMinutesDisplay); + } + + [Fact] + public void DeleteEntry_EntferntEintragAusDerWoche() + { + var entries = new FakeTimeEntries(); + entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 30 }); + var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks()); + + vm.DeleteEntryCommand.Execute(vm.WeekEntries[0]); + + Assert.Empty(vm.WeekEntries); + } + + [Fact] + public async Task AddEntry_RuftOnAddEntryAufUndSpeichertErgebnis() + { + var entries = new FakeTimeEntries(); + var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks()); + vm.OnAddEntry = () => Task.FromResult( + new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 15 }); + + await vm.AddEntryCommand.ExecuteAsync(null); + + Assert.Single(vm.WeekEntries); + } +} + +public sealed class AddTimeEntryDialogViewModelTests +{ + [Fact] + public void Save_MitDauer_ErzeugtEintrag() + { + var vm = new AddTimeEntryDialogViewModel([]) + { + DateText = "10.01.2026", + DurationText = "45", + }; + + vm.SaveCommand.Execute(null); + + Assert.NotNull(vm.Result); + Assert.Equal(45, vm.Result!.DurationMinutes); + Assert.Equal(new DateOnly(2026, 1, 10), vm.Result.Date); + } + + [Fact] + public void Save_MitVonBis_BerechnetDauer() + { + var vm = new AddTimeEntryDialogViewModel([]) + { + DateText = "10.01.2026", + StartTimeText = "14:00", + EndTimeText = "14:30", + }; + + vm.SaveCommand.Execute(null); + + Assert.NotNull(vm.Result); + Assert.Equal(30, vm.Result!.DurationMinutes); + } + + [Fact] + public void Save_OhneDauerUndOhneZeiten_SetztFehler() + { + var vm = new AddTimeEntryDialogViewModel([]) { DateText = "10.01.2026" }; + + vm.SaveCommand.Execute(null); + + Assert.Null(vm.Result); + Assert.NotEmpty(vm.TimeError); + } + + [Fact] + public void Save_EndeVorBeginn_SetztFehler() + { + var vm = new AddTimeEntryDialogViewModel([]) + { + DateText = "10.01.2026", + StartTimeText = "14:30", + EndTimeText = "14:00", + }; + + vm.SaveCommand.Execute(null); + + Assert.Null(vm.Result); + Assert.NotEmpty(vm.TimeError); + } +} diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index f573158..9cc20c3 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -189,6 +189,8 @@ public static class AppBootstrapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Transient: neue Instanz pro Navigation (für Detailseiten) services.AddTransient(); diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index 2655127..25178e0 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -89,10 +89,12 @@ public partial class MainWindowViewModel : ObservableObject return timetable; } - private WorkTaskListViewModel GetWorkload() + private WorkloadViewModel GetWorkload() { - var workload = _services.GetRequiredService(); - workload.Load(); + var workload = _services.GetRequiredService(); + workload.Tasks.Load(); + workload.TimeTracking.Load(); + workload.ActiveTabIndex = 0; return workload; } diff --git a/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs b/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs index 9cdd8bd..0369226 100644 --- a/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Workload/WorkloadViewModels.cs @@ -60,6 +60,7 @@ public partial class WorkTaskListViewModel : ObservableObject { private readonly IWorkTaskRepository _tasks; private readonly IGroupRepository _groups; + private readonly ITimeEntryRepository _timeEntries; public const string AllFilter = "Alle"; public const string ActiveFilter = "Offene Aufgaben"; @@ -81,10 +82,12 @@ public partial class WorkTaskListViewModel : ObservableObject private Dictionary _groupNames = []; - public WorkTaskListViewModel(IWorkTaskRepository tasks, IGroupRepository groups) + public WorkTaskListViewModel(IWorkTaskRepository tasks, IGroupRepository groups, + ITimeEntryRepository timeEntries) { _tasks = tasks; _groups = groups; + _timeEntries = timeEntries; Load(); } @@ -131,7 +134,11 @@ public partial class WorkTaskListViewModel : ObservableObject // Fälligkeit: fällige zuerst, unbefristete ans Ende (6.1.1). foreach (var t in all.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).ThenBy(t => t.Title)) - Tasks.Add(new WorkTaskListItem(t, _groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty))); + { + // Ist-Zeit (6.2.4): Summe aller mit der Aufgabe verknüpften Zeiteinträge. + var actualMinutes = _timeEntries.GetByTask(t.Id).Sum(e => e.DurationMinutes); + Tasks.Add(new WorkTaskListItem(t, _groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty), actualMinutes)); + } OnPropertyChanged(nameof(CountSummary)); } @@ -174,7 +181,7 @@ public partial class WorkTaskListViewModel : ObservableObject } } -public class WorkTaskListItem(WorkTask model, string? groupName) +public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinutes = 0) { public WorkTask Model { get; } = model; public string Title => Model.Title; @@ -187,6 +194,12 @@ public class WorkTaskListItem(WorkTask model, string? groupName) public string StatusLabel => WorkTaskStatusDisplay.Label(Model.Status); public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status); public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : ""; + + // 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 + ? $"{actualMinutes} / {estimate} min erfasst" + : $"{actualMinutes} min erfasst"; } // ── Dialog: Aufgabe anlegen/bearbeiten (6.1.2) ─────────────────────────────── @@ -264,3 +277,224 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject }; } } + +// ── Zeiterfassung (6.2) ────────────────────────────────────────────────────── + +public partial class TimeTrackingViewModel : ObservableObject +{ + private readonly ITimeEntryRepository _entries; + private readonly IWorkTaskRepository _tasks; + + public const string NoTaskOption = "Keine Aufgabe"; + + [ObservableProperty] private bool _isTimerRunning; + [ObservableProperty] private DateTime? _timerStartedAt; + [ObservableProperty] private WorkTask? _selectedTimerTask; + [ObservableProperty] private string _selectedTimerCategory = TaskCategoryDisplay.Options[0]; + + public List CategoryOptions { get; } = [.. TaskCategoryDisplay.Options]; + public List OpenTasks { get; private set; } = []; + public string RunningSinceDisplay => TimerStartedAt is { } started + ? $"Läuft seit {started:HH:mm} Uhr" + : ""; + + public ObservableCollection WeekEntries { get; } = []; + public ObservableCollection CategorySummaries { get; } = []; + public string TotalWeekMinutesDisplay => $"{WeekEntries.Sum(e => e.Model.DurationMinutes)} min diese Woche"; + + public Func>? OnAddEntry { get; set; } + + public TimeTrackingViewModel(ITimeEntryRepository entries, IWorkTaskRepository tasks) + { + _entries = entries; + _tasks = tasks; + Load(); + } + + partial void OnSelectedTimerTaskChanged(WorkTask? value) + { + if (value is not null) SelectedTimerCategory = TaskCategoryDisplay.Label(value.Category); + } + + public void Load() + { + OpenTasks = _tasks.GetAll().Where(t => t.Status != WorkTaskStatus.Done).ToList(); + OnPropertyChanged(nameof(OpenTasks)); + Refresh(); + } + + private void Refresh() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var monday = today.AddDays(-((int)today.DayOfWeek + 6) % 7); + var sunday = monday.AddDays(6); + + var weekEntries = _entries.GetByDateRange(monday, sunday); + var taskTitles = _tasks.GetAll().ToDictionary(t => t.Id, t => t.Title); + + WeekEntries.Clear(); + foreach (var e in weekEntries.OrderByDescending(e => e.Date).ThenByDescending(e => e.StartTime)) + WeekEntries.Add(new TimeEntryListItem(e, e.TaskId is { } id ? taskTitles.GetValueOrDefault(id) : null)); + + CategorySummaries.Clear(); + var maxMinutes = weekEntries.Count > 0 + ? weekEntries.GroupBy(e => e.Category).Max(g => g.Sum(e => e.DurationMinutes)) + : 0; + foreach (var group in weekEntries.GroupBy(e => e.Category).OrderByDescending(g => g.Sum(e => e.DurationMinutes))) + { + var minutes = group.Sum(e => e.DurationMinutes); + CategorySummaries.Add(new CategoryWeekSummary( + string.IsNullOrWhiteSpace(group.Key) ? "Ohne Kategorie" : group.Key, + minutes, maxMinutes > 0 ? minutes / (double)maxMinutes : 0)); + } + + OnPropertyChanged(nameof(TotalWeekMinutesDisplay)); + } + + [RelayCommand] + private void StartTimer() + { + IsTimerRunning = true; + TimerStartedAt = DateTime.Now; + OnPropertyChanged(nameof(RunningSinceDisplay)); + } + + [RelayCommand] + private void StopTimer() + { + if (TimerStartedAt is not { } startedAt) return; + var endedAt = DateTime.Now; + var minutes = Math.Max(1, (int)Math.Round((endedAt - startedAt).TotalMinutes)); + + _entries.Save(new TimeEntry + { + TaskId = SelectedTimerTask?.Id, + Category = SelectedTimerCategory, + GroupId = SelectedTimerTask?.GroupId, + Date = DateOnly.FromDateTime(startedAt), + StartTime = TimeOnly.FromDateTime(startedAt), + EndTime = TimeOnly.FromDateTime(endedAt), + DurationMinutes = minutes, + }); + + IsTimerRunning = false; + TimerStartedAt = null; + OnPropertyChanged(nameof(RunningSinceDisplay)); + Refresh(); + } + + [RelayCommand] + private async Task AddEntry() + { + if (OnAddEntry is null) return; + var result = await OnAddEntry(); + if (result is null) return; + _entries.Save(result); + Refresh(); + } + + [RelayCommand] + private void DeleteEntry(TimeEntryListItem? item) + { + if (item is null) return; + _entries.Delete(item.Model.Id); + Refresh(); + } +} + +public class TimeEntryListItem(TimeEntry model, string? taskTitle) +{ + public TimeEntry Model { get; } = model; + public string DateDisplay => Model.Date.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture); + public string Category => string.IsNullOrWhiteSpace(Model.Category) ? "Ohne Kategorie" : Model.Category; + public string TaskTitle => taskTitle ?? ""; + public string CategoryAndTaskDisplay => string.IsNullOrEmpty(TaskTitle) ? Category : $"{Category} · {TaskTitle}"; + public string DurationDisplay => $"{Model.DurationMinutes} min"; + public string Description => Model.Description ?? ""; +} + +public class CategoryWeekSummary(string category, int minutes, double barFraction) +{ + public string Category { get; } = category; + public string MinutesDisplay { get; } = $"{minutes} min"; + public double BarFraction { get; } = barFraction; +} + +// ── Dialog: Zeiteintrag nacherfassen (6.2.2) ───────────────────────────────── + +public partial class AddTimeEntryDialogViewModel : ObservableObject +{ + [ObservableProperty] private string _dateText = DateTime.Today.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture); + [ObservableProperty] private string _selectedCategory = TaskCategoryDisplay.Options[0]; + [ObservableProperty] private WorkTask? _selectedTask; + [ObservableProperty] private string _startTimeText = ""; + [ObservableProperty] private string _endTimeText = ""; + [ObservableProperty] private string _durationText = ""; + [ObservableProperty] private string _description = ""; + [ObservableProperty] private string _dateError = ""; + [ObservableProperty] private string _timeError = ""; + + public List CategoryOptions { get; } = [.. TaskCategoryDisplay.Options]; + public List Tasks { get; } + public TimeEntry? Result { get; private set; } + + public AddTimeEntryDialogViewModel(List tasks) => Tasks = tasks; + + partial void OnSelectedTaskChanged(WorkTask? value) + { + if (value is not null) SelectedCategory = TaskCategoryDisplay.Label(value.Category); + } + + [RelayCommand] + private void Save() + { + DateError = ""; TimeError = ""; + var valid = true; + + if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date)) + { DateError = "Format TT.MM.JJJJ."; valid = false; } + + TimeOnly? start = null, end = null; + int? duration = null; + + if (!string.IsNullOrWhiteSpace(DurationText)) + { + if (!int.TryParse(DurationText, out var m) || m <= 0) + { TimeError = "Dauer: ganze Zahl > 0 erwartet."; valid = false; } + else duration = m; + } + else if (!string.IsNullOrWhiteSpace(StartTimeText) || !string.IsNullOrWhiteSpace(EndTimeText)) + { + if (!TimeOnly.TryParseExact(StartTimeText, "HH:mm", null, DateTimeStyles.None, out var s) + || !TimeOnly.TryParseExact(EndTimeText, "HH:mm", null, DateTimeStyles.None, out var e) + || e <= s) + { TimeError = "Format HH:mm, Ende muss nach Beginn liegen."; valid = false; } + else { start = s; end = e; duration = (int)(e - s).TotalMinutes; } + } + else { TimeError = "Dauer oder Von/Bis erforderlich."; valid = false; } + + if (!valid) return; + + Result = new TimeEntry + { + TaskId = SelectedTask?.Id, + Category = SelectedCategory, + GroupId = SelectedTask?.GroupId, + Date = date, + StartTime = start, + EndTime = end, + DurationMinutes = duration!.Value, + Description = string.IsNullOrWhiteSpace(Description) ? null : Description.Trim(), + }; + } +} + +// ── Container: Arbeitszeit-Seite mit Tabs "Aufgaben"/"Zeiterfassung" ───────── + +public partial class WorkloadViewModel(WorkTaskListViewModel tasks, TimeTrackingViewModel timeTracking) : ObservableObject +{ + [ObservableProperty] private int _activeTabIndex; + + public WorkTaskListViewModel Tasks { get; } = tasks; + public TimeTrackingViewModel TimeTracking { get; } = timeTracking; +} diff --git a/LehrerApp.Desktop/Views/MainWindow.axaml b/LehrerApp.Desktop/Views/MainWindow.axaml index 308ddfd..e794a68 100644 --- a/LehrerApp.Desktop/Views/MainWindow.axaml +++ b/LehrerApp.Desktop/Views/MainWindow.axaml @@ -55,8 +55,8 @@ - - + + diff --git a/LehrerApp.Desktop/Views/Workload/AddTimeEntryDialog.axaml b/LehrerApp.Desktop/Views/Workload/AddTimeEntryDialog.axaml new file mode 100644 index 0000000..7cf6fcc --- /dev/null +++ b/LehrerApp.Desktop/Views/Workload/AddTimeEntryDialog.axaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +