Finished capture page
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/WorkTime.Mobile/Pages/Components/AddEntryModal.xaml
Normal file
48
src/WorkTime.Mobile/Pages/Components/AddEntryModal.xaml
Normal 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>
|
||||||
105
src/WorkTime.Mobile/Pages/Components/AddEntryModal.xaml.cs
Normal file
105
src/WorkTime.Mobile/Pages/Components/AddEntryModal.xaml.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/WorkTime.Mobile/Pages/Components/CaptureEntry.xaml
Normal file
33
src/WorkTime.Mobile/Pages/Components/CaptureEntry.xaml
Normal 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>
|
||||||
14
src/WorkTime.Mobile/Pages/Components/CaptureEntry.xaml.cs
Normal file
14
src/WorkTime.Mobile/Pages/Components/CaptureEntry.xaml.cs
Normal 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 |
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user