Arbeitszeit: Zeiterfassung (Kapitel 6.2)

Neuer Tab "Zeiterfassung" neben "Aufgaben" (WorkloadViewModel als Tab-Container,
gleiches Muster wie GroupDetailViewModel): Timer mit Zuordnung zu Aufgabe/Kategorie,
manuelle Nacherfassung (Von-Bis oder Dauer), Wochenübersicht nach Kategorie, sowie
"X / Y min erfasst" direkt in der Aufgabenliste als Ist-vs-Soll-Vergleich.
This commit is contained in:
2026-08-15 23:17:44 +02:00
parent 5c0b6854f5
commit d8b05702bc
14 changed files with 707 additions and 38 deletions
@@ -0,0 +1,72 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
x:Class="LehrerApp.Desktop.Views.Workload.AddTimeEntryDialog"
x:DataType="vm:AddTimeEntryDialogViewModel"
Title="Zeit nacherfassen"
Width="420" Height="440" MinWidth="380" MinHeight="400"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer Grid.Row="0">
<StackPanel Spacing="14" Margin="0,0,12,0">
<TextBlock Text="Zeit nacherfassen" Classes="dialogtitle"/>
<Grid ColumnDefinitions="*,8,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
<TextBlock Text="{Binding DateError}" Foreground="Red" FontSize="11"
IsVisible="{Binding DateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Kategorie" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding CategoryOptions}" SelectedItem="{Binding SelectedCategory}"
HorizontalAlignment="Stretch"/>
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="Aufgabe (optional)" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Tasks}" SelectedItem="{Binding SelectedTask}"
HorizontalAlignment="Stretch" PlaceholderText="Keine Aufgabe">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:WorkTask">
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<Grid ColumnDefinitions="*,8,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Von" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding StartTimeText}" PlaceholderText="HH:mm"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Bis" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding EndTimeText}" PlaceholderText="HH:mm"/>
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="Oder Dauer in min" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding DurationText}" PlaceholderText="z.B. 30"/>
<TextBlock Text="{Binding TimeError}" Foreground="Red" FontSize="11"
IsVisible="{Binding TimeError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Notiz (optional)" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Description}"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Speichern" HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Workload;
namespace LehrerApp.Desktop.Views.Workload;
public partial class AddTimeEntryDialog : Window
{
public AddTimeEntryDialog() => InitializeComponent();
private void OnSave(object? s, RoutedEventArgs e)
{
if (DataContext is AddTimeEntryDialogViewModel vm && vm.SaveCommand.CanExecute(null))
{
vm.SaveCommand.Execute(null);
if (vm.Result is not null) Close(true);
}
}
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
}
@@ -0,0 +1,101 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
x:Class="LehrerApp.Desktop.Views.Workload.TimeTrackingView"
x:DataType="vm:TimeTrackingViewModel">
<ScrollViewer>
<StackPanel Margin="20,16" Spacing="16">
<!-- Timer (6.2.1) -->
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="16">
<StackPanel Spacing="10">
<TextBlock Text="Timer" FontWeight="SemiBold" FontSize="14"/>
<Grid ColumnDefinitions="*,8,*" IsEnabled="{Binding !IsTimerRunning}">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Aufgabe (optional)" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding OpenTasks}" SelectedItem="{Binding SelectedTimerTask}"
HorizontalAlignment="Stretch" PlaceholderText="Keine Aufgabe">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:WorkTask">
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Kategorie" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding CategoryOptions}" SelectedItem="{Binding SelectedTimerCategory}"
HorizontalAlignment="Stretch"/>
</StackPanel>
</Grid>
<Grid ColumnDefinitions="Auto,*">
<Button Grid.Column="0" Content="▶ Start" Command="{Binding StartTimerCommand}"
IsVisible="{Binding !IsTimerRunning}"/>
<Button Grid.Column="0" Content="■ Stop" Command="{Binding StopTimerCommand}"
IsVisible="{Binding IsTimerRunning}"/>
<TextBlock Grid.Column="1" Text="{Binding RunningSinceDisplay}" VerticalAlignment="Center"
Margin="12,0,0,0" FontWeight="SemiBold"/>
</Grid>
</StackPanel>
</Border>
<!-- Wochenübersicht (6.2.3) -->
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="16">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Diese Woche" FontWeight="SemiBold" FontSize="14"/>
<TextBlock Grid.Column="1" Text="{Binding TotalWeekMinutesDisplay}" Opacity="0.7" FontSize="12"/>
</Grid>
<ItemsControl ItemsSource="{Binding CategorySummaries}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CategoryWeekSummary">
<Grid ColumnDefinitions="120,*,60" Margin="0,3">
<TextBlock Grid.Column="0" Text="{Binding Category}" FontSize="12" VerticalAlignment="Center"/>
<ProgressBar Grid.Column="1" Value="{Binding BarFraction}" Maximum="1" Height="8"
VerticalAlignment="Center" Margin="8,0"/>
<TextBlock Grid.Column="2" Text="{Binding MinutesDisplay}" FontSize="12"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Noch keine Zeiten diese Woche erfasst." Opacity="0.6" FontSize="12"
IsVisible="{Binding !CategorySummaries.Count}"/>
</StackPanel>
</Border>
<!-- Einträge / Nacherfassung (6.2.2) -->
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Einträge" FontWeight="SemiBold" FontSize="14" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content=" Nacherfassen" Command="{Binding AddEntryCommand}"/>
</Grid>
<ItemsControl ItemsSource="{Binding WeekEntries}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TimeEntryListItem">
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="6" Padding="12,8" Margin="0,0,0,6">
<Grid ColumnDefinitions="80,*,Auto,Auto">
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="12" Opacity="0.6"
VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Margin="8,0" VerticalAlignment="Center">
<TextBlock Text="{Binding CategoryAndTaskDisplay}" FontSize="13"/>
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.6"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding DurationDisplay}" VerticalAlignment="Center"
Margin="0,0,10,0" FontSize="12"/>
<Button Grid.Column="3" Content="Löschen" FontSize="11" Padding="8,3"
Command="{Binding $parent[ItemsControl].((vm:TimeTrackingViewModel)DataContext).DeleteEntryCommand}"
CommandParameter="{Binding}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Noch keine Einträge diese Woche." Opacity="0.6" Margin="4,4"
IsVisible="{Binding !WeekEntries.Count}"/>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,32 @@
using Avalonia.Controls;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Workload;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Workload;
public partial class TimeTrackingView : UserControl
{
public TimeTrackingView() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is TimeTrackingViewModel vm)
vm.OnAddEntry = ShowAddEntryDialog;
}
private async Task<TimeEntry?> ShowAddEntryDialog()
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
var tasks = App.Services.GetRequiredService<IWorkTaskRepository>()
.GetAll().Where(t => t.Status != WorkTaskStatus.Done).ToList();
var vm = new AddTimeEntryDialogViewModel(tasks);
var dialog = new AddTimeEntryDialog { DataContext = vm };
await dialog.ShowDialog<bool>(owner);
return vm.Result;
}
}
@@ -1,30 +1,20 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared"
x:Class="LehrerApp.Desktop.Views.Workload.WorkTaskListView"
x:DataType="vm:WorkTaskListViewModel">
<Grid RowDefinitions="Auto,Auto,*">
<Border Grid.Row="0" Padding="20,16"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto">
<shared:PageHeader Grid.Column="0" Title="Arbeitszeit" Subtitle="{Binding CountSummary}"/>
<Button Grid.Column="1" Content=" Neue Aufgabe"
Command="{Binding AddTaskCommand}" VerticalAlignment="Center"/>
</Grid>
</Border>
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto" Margin="16,10" HorizontalAlignment="Left">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,*,Auto" Margin="16,14,16,4" VerticalAlignment="Center">
<ComboBox Grid.Column="0" ItemsSource="{Binding StatusFilterOptions}"
SelectedItem="{Binding StatusFilter}" Margin="0,0,8,0"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding CategoryFilterOptions}"
SelectedItem="{Binding CategoryFilter}" Margin="0,0,8,0"/>
<ComboBox Grid.Column="2" ItemsSource="{Binding GroupFilterOptions}"
SelectedItem="{Binding GroupFilter}"/>
<Button Grid.Column="4" Content=" Neue Aufgabe" Command="{Binding AddTaskCommand}"/>
</Grid>
<ScrollViewer Grid.Row="2" Margin="16,0,16,16">
<ScrollViewer Grid.Row="1" Margin="16,10,16,16">
<StackPanel>
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
@@ -46,11 +36,15 @@
<Run Text=" · "/>
<Run Text="{Binding GroupName}"/>
</TextBlock>
<!-- Ist-Zeit vs. Schätzung (6.2.4) -->
<TextBlock Text="{Binding ActualVsEstimateDisplay}" FontSize="11" Opacity="0.6"
IsVisible="{Binding HasActualTime}"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding DueDateDisplay}" VerticalAlignment="Center"
Margin="0,0,10,0" FontSize="12" Foreground="{Binding DueDateColorHex}"/>
<TextBlock Grid.Column="3" Text="{Binding EstimatedMinutesDisplay}"
VerticalAlignment="Center" Opacity="0.6" FontSize="12" Margin="0,0,10,0"/>
VerticalAlignment="Center" Opacity="0.6" FontSize="12" Margin="0,0,10,0"
IsVisible="{Binding !HasActualTime}"/>
<Button Grid.Column="4" Content="Bearbeiten" FontSize="11" Padding="8,3" Margin="0,0,4,0"
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).EditTaskCommand}"
CommandParameter="{Binding}"/>
@@ -0,0 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.Workload"
xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared"
x:Class="LehrerApp.Desktop.Views.Workload.WorkloadView"
x:DataType="vm:WorkloadViewModel">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="20,16"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1">
<shared:PageHeader Title="Arbeitszeit" Subtitle="{Binding Tasks.CountSummary}"/>
</Border>
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
<ContentPage Header="Aufgaben">
<views:WorkTaskListView DataContext="{Binding Tasks}"/>
</ContentPage>
<ContentPage Header="Zeiterfassung">
<views:TimeTrackingView DataContext="{Binding TimeTracking}"/>
</ContentPage>
</TabbedPage>
</Grid>
</UserControl>
@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace LehrerApp.Desktop.Views.Workload;
public partial class WorkloadView : UserControl
{
public WorkloadView() => InitializeComponent();
}