Finished capture page

This commit is contained in:
2025-11-21 16:16:05 +01:00
parent 969219137a
commit 617c34f5df
9 changed files with 274 additions and 14 deletions

View File

@@ -3,6 +3,7 @@ using MauiIcons.Material;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using WorkTime.Database; using WorkTime.Database;
using WorkTime.Mobile.Pages; using WorkTime.Mobile.Pages;
using WorkTime.Mobile.Pages.Components;
namespace WorkTime.Mobile; namespace WorkTime.Mobile;
@@ -22,6 +23,7 @@ public static class MauiProgram {
builder.Services.AddTransient<CapturePageModel>(); builder.Services.AddTransient<CapturePageModel>();
builder.Services.AddTransient<SettingsPageModel>(); builder.Services.AddTransient<SettingsPageModel>();
builder.Services.AddTransient<AddEntryModel>();
#if DEBUG #if DEBUG
builder.Logging.AddDebug(); builder.Logging.AddDebug();

View File

@@ -15,23 +15,28 @@
Command="{Binding LoadDateCommand}" /> Command="{Binding LoadDateCommand}" />
<ScrollView Grid.Row="1"> <ScrollView Grid.Row="1">
<CollectionView ItemsSource="{Binding Entries}"> <CollectionView ItemsSource="{Binding Entries}"
SelectionMode="Single"
SelectedItem="{Binding SelectedEntry, Mode=TwoWay}"
SelectionChangedCommand="{Binding DeleteEntryCommand}"
SelectionChangedCommandParameter="{Binding SelectedEntry}">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="20" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate> <CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:TimeEntry"> <DataTemplate x:DataType="models:TimeEntry">
<HorizontalStackLayout Spacing="5"> <components:CaptureEntry />
<Label Text="{Binding Timestamp}" />
<Label Text="{Binding Type}" />
</HorizontalStackLayout>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>
</CollectionView> </CollectionView>
</ScrollView> </ScrollView>
<HorizontalStackLayout Grid.Row="2" HorizontalOptions="Center" Spacing="20"> <HorizontalStackLayout Grid.Row="2" HorizontalOptions="Center" Spacing="20" IsVisible="{Binding IsToday}">
<Button Text="{Binding CurrentTypeName}" <Button Text="{Binding CurrentTypeName}"
Command="{Binding RegisterEntryCommand}" /> Command="{Binding RegisterEntryCommand}" />
<Button mi:MauiIcon.Value="{mi:Material Icon=Add}" /> <Button mi:MauiIcon.Value="{mi:Material Icon=Add}"
Command="{Binding OpenPopupCommand}" />
</HorizontalStackLayout> </HorizontalStackLayout>
</Grid> </Grid>
</ContentPage.Content> </ContentPage.Content>

View File

@@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using WorkTime.Mobile.Pages.Components;
using WorkTime.Models; using WorkTime.Models;
using WorkTime.Models.Repositories; using WorkTime.Models.Repositories;
@@ -17,21 +18,32 @@ public partial class CapturePage : ContentPage {
protected override void OnAppearing() { protected override void OnAppearing() {
base.OnAppearing(); base.OnAppearing();
_model.Navigation = Navigation;
_model.Window = Window!;
if (_model.AppearingCommand.CanExecute(null)) if (_model.AppearingCommand.CanExecute(null))
_model.AppearingCommand.Execute(null); _model.AppearingCommand.Execute(null);
} }
} }
public partial class CapturePageModel(ITimeEntryRepository entryRepository) : ObservableObject { public partial class CapturePageModel(ITimeEntryRepository entryRepository, IServiceProvider provider) : ObservableObject {
private DateOnly _currentDate = DateOnly.FromDateTime(DateTime.Now); private DateOnly _currentDate = DateOnly.FromDateTime(DateTime.Now);
public INavigation Navigation = null!;
public Window Window = null!;
[ObservableProperty] [ObservableProperty]
public partial ObservableCollection<TimeEntry> Entries { get; set; } = new(); public partial ObservableCollection<TimeEntry> Entries { get; set; } = new();
[ObservableProperty] [ObservableProperty]
public partial EntryType CurrentType { get; set; } = EntryType.Login; public partial EntryType CurrentType { get; set; } = EntryType.Login;
[ObservableProperty]
public partial bool IsToday { get; set; }
[ObservableProperty]
public partial TimeEntry? SelectedEntry { get; set; }
public string CurrentTypeName => CurrentType switch { public string CurrentTypeName => CurrentType switch {
EntryType.Login => "Einstempeln", EntryType.Login => "Einstempeln",
@@ -48,8 +60,9 @@ public partial class CapturePageModel(ITimeEntryRepository entryRepository) : Ob
} }
[RelayCommand] [RelayCommand]
public async Task LoadDate(DateOnly date) { private async Task LoadDate(DateOnly date) {
_currentDate = date; _currentDate = date;
IsToday = date == DateOnly.FromDateTime(DateTime.Today);
Entries.Clear(); Entries.Clear();
var result = await entryRepository.GetTimeEntries(date); var result = await entryRepository.GetTimeEntries(date);
@@ -61,7 +74,8 @@ public partial class CapturePageModel(ITimeEntryRepository entryRepository) : Ob
} }
private void UpdateCurrentType() { private void UpdateCurrentType() {
var last = Entries.LastOrDefault(); var last = Entries
.LastOrDefault(e => e.Timestamp <= DateTime.Now);
if (last is null) { if (last is null) {
CurrentType = EntryType.Login; CurrentType = EntryType.Login;
@@ -82,12 +96,12 @@ public partial class CapturePageModel(ITimeEntryRepository entryRepository) : Ob
} }
[RelayCommand] [RelayCommand]
public async Task OnAppearing() { private async Task OnAppearing() {
await LoadDate(_currentDate); await LoadDate(_currentDate);
} }
[RelayCommand] [RelayCommand]
public async Task RegisterEntry(TimeEntry? entry = null) { private async Task RegisterEntry(TimeEntry? entry = null) {
entry ??= new TimeEntry { entry ??= new TimeEntry {
Timestamp = DateTime.Now, Timestamp = DateTime.Now,
Type = CurrentType Type = CurrentType
@@ -98,9 +112,32 @@ public partial class CapturePageModel(ITimeEntryRepository entryRepository) : Ob
} }
[RelayCommand] [RelayCommand]
public async Task DeleteEntry(Guid entryId) { private async Task DeleteEntry(TimeEntry entry) {
await entryRepository.DeleteTimeEntry(entryId); SelectedEntry = null;
if (!IsToday) return;
var confirmed = await Window.Page!.DisplayAlertAsync(
"Achtung!",
"Möchten sie diesen Eintrag wirklich löschen?",
"Löschen",
"Abbrechen");
if (!confirmed) return;
await entryRepository.DeleteTimeEntry(entry.Id);
await LoadDate(_currentDate); await LoadDate(_currentDate);
} }
[RelayCommand]
private async Task OpenPopup() {
var modal = new AddEntryModal(provider.GetRequiredService<AddEntryModel>());
await Navigation.PushModalAsync(modal);
var result = await modal.Result.Task;
await Navigation.PopModalAsync();
if (result is not null)
await RegisterEntry(result);
}
} }

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:components="clr-namespace:WorkTime.Mobile.Pages.Components"
x:Class="WorkTime.Mobile.Pages.Components.AddEntryModal"
x:DataType="components:AddEntryModel">
<ContentPage.Content>
<Grid Padding="20" RowSpacing="15" RowDefinitions="Auto,Auto,*,Auto">
<Label Grid.Row="0" Text="Neuen Eintrag hinzufügen" FontAttributes="Bold" />
<VerticalStackLayout Grid.Row="1">
<Grid ColumnDefinitions="*,Auto">
<Label Grid.Column="0" Text="Uhrzeit" VerticalOptions="Center" />
<TimePicker Grid.Column="1" VerticalOptions="Center" Time="{Binding Time, Mode=TwoWay}" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<Label Grid.Column="0" Text="Stempeltyp" VerticalOptions="Center" />
<Picker Grid.Column="1" VerticalOptions="Center" SelectedIndex="{Binding SelectedType, Mode=TwoWay}">
<Picker.ItemsSource>
<x:Array Type="{x:Type system:String}">
<system:String>Einstempeln</system:String>
<system:String>Ausstempeln</system:String>
<system:String>Dienstreise starten</system:String>
<system:String>Dienstreise beenden</system:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
</Grid>
<Grid ColumnDefinitions="*,Auto" IsVisible="{Binding MobaEnabled}">
<Label Grid.Column="0" Text="Mobiles Arbeiten" VerticalOptions="Center" />
<CheckBox Grid.Column="1" VerticalOptions="Center" HorizontalOptions="End" IsChecked="{Binding IsMoba, Mode=TwoWay}" />
</Grid>
</VerticalStackLayout>
<HorizontalStackLayout Grid.Row="3" Spacing="10" HorizontalOptions="Center" Margin="0, 0, 0, 30">
<Button Text="Abbrechen" Command="{Binding CancelCommand}" />
<Button Text="Speichern" Command="{Binding SubmitCommand}" />
</HorizontalStackLayout>
</Grid>
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using WorkTime.Models;
using WorkTime.Models.Repositories;
namespace WorkTime.Mobile.Pages.Components;
public partial class AddEntryModal : ContentPage {
public TaskCompletionSource<TimeEntry?> Result = new();
private readonly AddEntryModel _model;
public AddEntryModal(AddEntryModel model) {
InitializeComponent();
BindingContext = model;
model.ResultSource = Result;
_model = model;
}
protected override void OnAppearing() {
base.OnAppearing();
if (_model.AppearingCommand.CanExecute(null))
_model.AppearingCommand.Execute(null);
}
}
public partial class AddEntryModel(ITimeEntryRepository entryRepository) : ObservableObject {
public TaskCompletionSource<TimeEntry?> ResultSource { get; set; } = null!;
[ObservableProperty]
public partial int SelectedType { get; set; } = 0;
[ObservableProperty]
public partial bool IsMoba { get; set; } = false;
[ObservableProperty]
public partial bool MobaEnabled { get; set; } = true;
[ObservableProperty]
public partial TimeSpan Time { get; set; } = DateTime.Now.TimeOfDay;
[RelayCommand]
private async Task OnAppearing() {
var entries = await entryRepository.GetTimeEntries(DateOnly.FromDateTime(DateTime.Now));
var last = entries
.OrderBy(e => e.Timestamp)
.LastOrDefault();
if (last is not null) {
SelectedType = last.Type switch {
EntryType.Login or EntryType.LoginHome => 1,
EntryType.Logout or EntryType.LogoutHome => 0,
EntryType.LoginTrip => 3,
EntryType.LogoutTrip => 0,
_ => 0
};
IsMoba = last.Type is EntryType.LoginHome or EntryType.LogoutHome;
}
}
partial void OnSelectedTypeChanged(int value) {
MobaEnabled = value < 2;
if (!MobaEnabled)
IsMoba = false;
}
[RelayCommand]
private void Submit() {
var type = SelectedType switch {
0 => EntryType.Login,
1 => EntryType.Logout,
2 => EntryType.LoginTrip,
3 => EntryType.LogoutTrip,
_ => EntryType.Login
};
if (IsMoba) {
type = type switch {
EntryType.Login => EntryType.LoginHome,
EntryType.Logout => EntryType.LogoutHome,
_ => type
};
}
var entry = new TimeEntry {
Timestamp = DateTime.Today + Time,
Type = type
};
ResultSource.SetResult(entry);
}
[RelayCommand]
private void Cancel() {
ResultSource.SetResult(null);
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:WorkTime.Models;assembly=WorkTime.Models"
xmlns:icons="http://www.aathifmahir.com/dotnet/2022/maui/icons"
x:Class="WorkTime.Mobile.Pages.Components.CaptureEntry"
x:DataType="models:TimeEntry">
<Border StrokeShape="RoundRectangle 15" StrokeThickness="0">
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto" ColumnSpacing="15"
BackgroundColor="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}"
Padding="10">
<Border
Grid.Column="0"
BackgroundColor="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}"
WidthRequest="15" HeightRequest="15"
VerticalOptions="Center"
StrokeShape="RoundRectangle 7.5"/>
<icons:MauiIcon Grid.Column="1" Icon="{icons:Material Icon=Home}" VerticalOptions="Center" IsVisible="{Binding IsHomeEntry}" />
<Label Grid.Column="2" Text="{Binding DisplayName}" VerticalOptions="Center" />
<HorizontalStackLayout Grid.Column="4" VerticalOptions="Center">
<Label Text="{Binding Timestamp.Hour}" />
<Label Text=":" />
<Label Text="{Binding Timestamp.Minute}" />
</HorizontalStackLayout>
<icons:MauiIcon Grid.Column="5" Icon="{icons:Material Icon=Delete}" VerticalOptions="Center" IsVisible="{Binding IsToday}" />
</Grid>
</Border>
</ContentView>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WorkTime.Models;
namespace WorkTime.Mobile.Pages.Components;
public partial class CaptureEntry : ContentView {
public CaptureEntry() {
InitializeComponent();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WorkTime.Models; namespace WorkTime.Models;
@@ -9,6 +10,21 @@ public sealed class TimeEntry {
public required EntryType Type { get; init; } public required EntryType Type { get; init; }
public required DateTime Timestamp { get; set; } public required DateTime Timestamp { get; set; }
[NotMapped]
public string DisplayName => Type switch {
EntryType.Login or EntryType.LoginHome => "Eingestempelt",
EntryType.Logout or EntryType.LogoutHome => "Ausgestempelt",
EntryType.LoginTrip => "Dienstreise gestartet",
EntryType.LogoutTrip => "Dienstreise beendet",
_ => string.Empty
};
[NotMapped]
public bool IsToday => Timestamp.Date == DateTime.Today;
[NotMapped]
public bool IsHomeEntry => Type is EntryType.LoginHome or EntryType.LogoutHome;
} }
public enum EntryType { public enum EntryType {