Finished Settings page

This commit is contained in:
2025-11-12 17:06:14 +01:00
parent a608f98497
commit e0bf95060a
38 changed files with 675 additions and 34 deletions

View File

@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WorkTime.Models;
namespace WorkTime.Database;
internal sealed class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options) {
public DbSet<TimeEntry> Entries { get; set; }
}
internal sealed class DbMigrationService(IServiceProvider provider) : IHostedService {
public async Task StartAsync(CancellationToken cancellationToken) {
await using var scope = provider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
await context.Database.MigrateAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -0,0 +1,42 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WorkTime.Database;
#nullable disable
namespace WorkTime.Database.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20251112121330_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("WorkTime.Models.TimeEntry", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Entries");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WorkTime.Database.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Entries",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Timestamp = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Entries", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Entries");
}
}
}

View File

@@ -0,0 +1,39 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WorkTime.Database;
#nullable disable
namespace WorkTime.Database.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("WorkTime.Models.TimeEntry", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Entries");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using WorkTime.Models;
using WorkTime.Models.Repositories;
namespace WorkTime.Database.Repositories;
internal sealed class SettingsRepository(string filePath) : ISettingsRepository {
public async Task<Settings> LoadSettings() {
if (!File.Exists(filePath))
return new Settings();
var raw = await File.ReadAllTextAsync(filePath);
return JsonSerializer.Deserialize<Settings>(raw, SettingsSerializer.Default.Settings) ?? new Settings();
}
public async Task SaveSettings(Settings settings) {
if (File.Exists(filePath))
File.Delete(filePath);
var json = JsonSerializer.Serialize(settings, SettingsSerializer.Default.Settings);
await File.WriteAllTextAsync(filePath, json);
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Settings))]
internal partial class SettingsSerializer : JsonSerializerContext {}

View File

@@ -0,0 +1,22 @@
using WorkTime.Models;
using WorkTime.Models.Repositories;
namespace WorkTime.Database.Repositories;
internal sealed class TimeEntryRepository : ITimeEntryRepository {
public Task<IEnumerable<TimeEntry>> GetTimeEntries() {
throw new NotImplementedException();
}
public Task<IEnumerable<TimeEntry>> GetTimeEntries(DateOnly date) {
throw new NotImplementedException();
}
public Task AddTimeEntry(TimeEntry entry) {
throw new NotImplementedException();
}
public Task DeleteTimeEntry(Guid id) {
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using WorkTime.Database.Repositories;
using WorkTime.Models.Repositories;
namespace WorkTime.Database;
public static class ServiceCollectionExtensions {
public static void AddDatabase(this IServiceCollection services, string basePath) {
services.AddSqlite<DatabaseContext>($"Filename={Path.Combine(basePath, "data.db")}");
services.AddHostedService<DbMigrationService>();
services.AddTransient<ITimeEntryRepository, TimeEntryRepository>();
services.AddTransient<ISettingsRepository, SettingsRepository>(_ => new SettingsRepository(Path.Combine(basePath, "settings.json")));
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WorkTime.Models\WorkTime.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
using WorkTime.Database;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDatabase(Environment.CurrentDirectory);
var host = builder.Build();
host.Run();

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"WorkTime.Migrator": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-WorkTime.Migrator-77e5f218-8552-46f7-943e-28675db76107</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WorkTime.Database\WorkTime.Database.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -1,10 +1,12 @@
using Microsoft.Extensions.DependencyInjection; using MauiIcons.Core;
using Microsoft.Extensions.DependencyInjection;
namespace WorkTime.Mobile; namespace WorkTime.Mobile;
public partial class App : Application { public partial class App : Application {
public App() { public App() {
InitializeComponent(); InitializeComponent();
_ = new MauiIcon();
} }
protected override Window CreateWindow(IActivationState? activationState) { protected override Window CreateWindow(IActivationState? activationState) {

View File

@@ -3,12 +3,35 @@
x:Class="WorkTime.Mobile.AppShell" x:Class="WorkTime.Mobile.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:WorkTime.Mobile" xmlns:pages="clr-namespace:WorkTime.Mobile.Pages"
Title="WorkTime.Mobile"> xmlns:icons="http://www.aathifmahir.com/dotnet/2022/maui/icons"
Title="Zeiterfassung">
<Shell.Resources>
<ResourceDictionary>
<FontImageSource x:Key="CaptureIcon" Glyph="{icons:Material Schedule}" />
<FontImageSource x:Key="AnalysisIcon" Glyph="{icons:Material Schedule}" />
<FontImageSource x:Key="SettingsIcon" Glyph="{icons:Material Settings}" />
</ResourceDictionary>
</Shell.Resources>
<ShellContent <TabBar>
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}" <ShellContent
Route="MainPage" /> Title="Erfassen"
Icon="{StaticResource CaptureIcon}"
ContentTemplate="{DataTemplate pages:CapturePage}" />
<ShellContent
Title="Auswertung"
Icon="{StaticResource AnalysisIcon}"
ContentTemplate="{DataTemplate pages:AnalysisPage}" />
<ShellContent
Title="Einstellungen"
Icon="{StaticResource SettingsIcon}"
ContentTemplate="{DataTemplate pages:SettingsPage}" />
</TabBar>
</Shell> </Shell>

View File

@@ -1,4 +1,8 @@
using Microsoft.Extensions.Logging; using CommunityToolkit.Maui;
using MauiIcons.Material;
using Microsoft.Extensions.Logging;
using WorkTime.Database;
using WorkTime.Mobile.Pages;
namespace WorkTime.Mobile; namespace WorkTime.Mobile;
@@ -7,11 +11,17 @@ public static class MauiProgram {
var builder = MauiApp.CreateBuilder(); var builder = MauiApp.CreateBuilder();
builder builder
.UseMauiApp<App>() .UseMauiApp<App>()
.UseMauiCommunityToolkit()
.UseMaterialMauiIcons()
.ConfigureFonts(fonts => { .ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
}); });
builder.Services.AddDatabase(FileSystem.AppDataDirectory);
builder.Services.AddTransient<SettingsPageModel>();
#if DEBUG #if DEBUG
builder.Logging.AddDebug(); builder.Logging.AddDebug();
#endif #endif

View File

@@ -0,0 +1,9 @@
<?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"
x:Class="WorkTime.Mobile.Pages.AnalysisPage">
<ContentPage.Content>
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WorkTime.Mobile.Pages;
public partial class AnalysisPage : ContentPage {
public AnalysisPage() {
InitializeComponent();
}
}

View File

@@ -0,0 +1,9 @@
<?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"
x:Class="WorkTime.Mobile.Pages.CapturePage">
<ContentPage.Content>
<Label Text="Hello from Capture Page" />
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WorkTime.Mobile.Pages;
public partial class CapturePage : ContentPage {
public CapturePage() {
InitializeComponent();
}
}

View File

@@ -0,0 +1,24 @@
<?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:components="clr-namespace:WorkTime.Mobile.Pages.Components"
x:Class="WorkTime.Mobile.Pages.Components.SettingComponent"
x:DataType="components:SettingComponent"
x:Name="Component">
<Grid ColumnDefinitions="*,Auto,80" ColumnSpacing="5">
<Label Text="{Binding Title, Source={x:Reference Component}}"
VerticalTextAlignment="Center"
Grid.Column="0"/>
<Entry Placeholder="{Binding Placeholder, Source={x:Reference Component}}"
Keyboard="Numeric"
Grid.Column="1"
TextChanged="OnValueChanged"
x:Name="EntryField"/>
<Label Text="{Binding Unit, Source={x:Reference Component}}"
VerticalTextAlignment="Center"
Grid.Column="2"/>
</Grid>
</ContentView>

View File

@@ -0,0 +1,59 @@
namespace WorkTime.Mobile.Pages.Components;
public partial class SettingComponent : ContentView {
public static readonly BindableProperty TitleProperty =
BindableProperty.Create(nameof(Title), typeof(string), typeof(SettingComponent), string.Empty);
public static readonly BindableProperty UnitProperty =
BindableProperty.Create(nameof(Unit), typeof(string), typeof(SettingComponent), string.Empty);
public static readonly BindableProperty PlaceholderProperty =
BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(SettingComponent), string.Empty);
public static readonly BindableProperty ValueProperty =
BindableProperty.Create(nameof(Value), typeof(TimeSpan), typeof(SettingComponent), TimeSpan.Zero,
BindingMode.TwoWay);
public string Title {
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}
public string Placeholder {
get => (string)GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
public TimeSpan Value {
get => (TimeSpan)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public SettingComponent() {
InitializeComponent();
}
private void OnValueChanged(object? sender, TextChangedEventArgs e) {
if (!int.TryParse(e.NewTextValue, out var number)) return;
switch (Unit) {
case "Stunden":
case "Uhr":
Value = TimeSpan.FromHours(number);
break;
case "Minuten":
Value = TimeSpan.FromMinutes(number);
break;
}
}
public void ClearInput() {
Content.FindByName<Entry>(nameof(EntryField)).Text = string.Empty;
}
}

View File

@@ -0,0 +1,97 @@
<?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:components="clr-namespace:WorkTime.Mobile.Pages.Components"
xmlns:pages="clr-namespace:WorkTime.Mobile.Pages"
xmlns:icons="http://www.aathifmahir.com/dotnet/2022/maui/icons"
x:Class="WorkTime.Mobile.Pages.SettingsPage"
x:DataType="pages:SettingsPageModel">
<ContentPage.Content>
<Grid Margin="20,10" RowDefinitions="*,Auto">
<VerticalStackLayout Grid.Row="0" x:Name="SettingsStack">
<HorizontalStackLayout Margin="0, 0, 0, 5" Spacing="15">
<icons:MauiIcon Icon="{icons:Material Work}"
IconSize="30"
IconColor="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Label Text="Zeiten"
FontSize="20"
HorizontalTextAlignment="Center"/>
</HorizontalStackLayout>
<components:SettingComponent
Title="Arbeitszeit"
Unit="Stunden"
Placeholder="{Binding Settings.WorkTime.TotalHours}"
Value="{Binding Settings.WorkTime, Mode=OneWayToSource}"/>
<HorizontalStackLayout Margin="0, 10, 0, 5" Spacing="15">
<icons:MauiIcon Icon="{icons:Material LocalPizza}"
IconSize="30"
IconColor="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Label Text="Pause"
FontSize="20"
HorizontalTextAlignment="Center"/>
</HorizontalStackLayout>
<components:SettingComponent
Title="Nach 0 Stunden"
Unit="Minuten"
Placeholder="{Binding Settings.BreakAfter0.TotalMinutes}"
Value="{Binding Settings.BreakAfter0, Mode=OneWayToSource}"/>
<components:SettingComponent
Title="Nach 6 Stunden"
Unit="Minuten"
Placeholder="{Binding Settings.BreakAfter6.TotalMinutes}"
Value="{Binding Settings.BreakAfter6, Mode=OneWayToSource}"/>
<components:SettingComponent
Title="Nach 9 Stunden"
Unit="Minuten"
Placeholder="{Binding Settings.BreakAfter9.TotalMinutes}"
Value="{Binding Settings.BreakAfter9, Mode=OneWayToSource}"/>
<components:SettingComponent
Title="Nicht tracken nach"
Unit="Uhr"
Placeholder="{Binding Settings.DontTrackBreakAfter.TotalHours}"
Value="{Binding Settings.DontTrackBreakAfter, Mode=OneWayToSource}"/>
<HorizontalStackLayout Margin="0, 10, 0, 5" Spacing="15">
<icons:MauiIcon Icon="{icons:Material CreditCard}"
IconSize="30"
IconColor="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Label Text="Überstunden"
FontSize="20"
HorizontalTextAlignment="Center"/>
</HorizontalStackLayout>
<components:SettingComponent
Title="Optimal"
Unit="Minuten"
Placeholder="{Binding Settings.IdealOvertime.TotalMinutes}"
Value="{Binding Settings.IdealOvertime, Mode=OneWayToSource}"/>
<components:SettingComponent
Title="Maximal"
Unit="Minuten"
Placeholder="{Binding Settings.MaxOvertime.TotalMinutes}"
Value="{Binding Settings.MaxOvertime, Mode=OneWayToSource}"/>
</VerticalStackLayout>
<Button Grid.Row="1"
Text="Speichern"
HorizontalOptions="Center"
Command="{Binding SaveSettingsCommand}"/>
</Grid>
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,50 @@
using CommunityToolkit.Maui.Alerts;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using WorkTime.Mobile.Pages.Components;
using WorkTime.Models;
using WorkTime.Models.Repositories;
namespace WorkTime.Mobile.Pages;
public partial class SettingsPage : ContentPage {
private readonly SettingsPageModel _model;
public SettingsPage(SettingsPageModel model) {
InitializeComponent();
BindingContext = model;
_model = model;
}
protected override void OnAppearing() {
base.OnAppearing();
if (_model.LoadSettingsCommand.CanExecute(null))
_model.LoadSettingsCommand.Execute(null);
foreach (var child in Content.FindByName<VerticalStackLayout>(nameof(SettingsStack)).Children) {
if (child is SettingComponent component)
component.ClearInput();
}
}
}
public partial class SettingsPageModel(ISettingsRepository settingsRepository) : ObservableObject {
[ObservableProperty]
public partial Settings Settings { get; set; }
[RelayCommand]
public async Task LoadSettings() {
Settings = await settingsRepository.LoadSettings();
}
[RelayCommand]
public async Task SaveSettings() {
await settingsRepository.SaveSettings(Settings);
await Toast.Make("Einstellungen gespeichert!").Show();
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#512BD4</color> <color name="colorPrimary">#1F1F1F</color>
<color name="colorPrimaryDark">#2B0B98</color> <color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color> <color name="colorAccent">#2B0B98</color>
</resources> </resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

Before

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -19,10 +19,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Display name --> <!-- Display name -->
<ApplicationTitle>WorkTime.Mobile</ApplicationTitle> <ApplicationTitle>Zeiterfassung</ApplicationTitle>
<!-- App Identifier --> <!-- App Identifier -->
<ApplicationId>com.companyname.worktime.mobile</ApplicationId> <ApplicationId>de.leon-hoppe.worktime</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
@@ -36,18 +36,19 @@
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion> <TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<LangVersion>preview</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- App Icon --> <!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/> <MauiIcon Include="Resources\AppIcon\appicon.png" ForegroundFile="Resources\AppIcon\appiconfg.png" Color="#FFFFFF"/>
<!-- Splash Screen --> <!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/> <MauiSplashScreen Include="Resources\Splash\splash.png" Color="#FFFFFF" BaseSize="128,128"/>
<!-- Images --> <!-- Images -->
<MauiImage Include="Resources\Images\*"/> <MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185"/> <MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="512,512"/>
<!-- Custom Fonts --> <!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/> <MauiFont Include="Resources\Fonts\*"/>
@@ -57,9 +58,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AathifMahir.Maui.MauiIcons.Material" Version="5.0.0" />
<PackageReference Include="CommunityToolkit.Maui" Version="12.3.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/> <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Maui.Essentials" Version="$(MauiVersion)"/> <PackageReference Include="Microsoft.Maui.Essentials" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0"/> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WorkTime.Database\WorkTime.Database.csproj" />
<ProjectReference Include="..\WorkTime.Models\WorkTime.Models.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,6 @@
namespace WorkTime.Models.Repositories;
public interface ISettingsRepository {
Task<Settings> LoadSettings();
Task SaveSettings(Settings settings);
}

View File

@@ -0,0 +1,9 @@
namespace WorkTime.Models.Repositories;
public interface ITimeEntryRepository {
Task<IEnumerable<TimeEntry>> GetTimeEntries();
Task<IEnumerable<TimeEntry>> GetTimeEntries(DateOnly date);
Task AddTimeEntry(TimeEntry entry);
Task DeleteTimeEntry(Guid id);
}

View File

@@ -0,0 +1,13 @@
namespace WorkTime.Models;
public sealed class Settings {
public TimeSpan WorkTime { get; set; } = TimeSpan.FromHours(7);
public TimeSpan BreakAfter0 { get; set; } = TimeSpan.Zero;
public TimeSpan BreakAfter6 { get; set; } = TimeSpan.FromMinutes(30);
public TimeSpan BreakAfter9 { get; set; } = TimeSpan.FromMinutes(45);
public TimeSpan DontTrackBreakAfter { get; set; } = TimeSpan.FromHours(14);
public TimeSpan IdealOvertime { get; set; } = TimeSpan.FromMinutes(30);
public TimeSpan MaxOvertime { get; set; } = TimeSpan.FromMinutes(60);
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace WorkTime.Models;
public sealed class TimeEntry {
[Key]
public Guid Id { get; init; } = Guid.CreateVersion7();
public required EntryType Type { get; init; }
public required DateTime Timestamp { get; set; }
}
public enum EntryType {
Login,
LoginTrip,
LoginHome,
Logout,
LogoutTrip,
LogoutHome
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -1,3 +1,8 @@
<Solution> <Solution>
<Folder Name="/Modules/">
<Project Path="WorkTime.Database/WorkTime.Database.csproj" />
<Project Path="WorkTime.Migrator/WorkTime.Migrator.csproj" />
<Project Path="WorkTime.Models/WorkTime.Models.csproj" />
</Folder>
<Project Path="WorkTime.Mobile/WorkTime.Mobile.csproj" /> <Project Path="WorkTime.Mobile/WorkTime.Mobile.csproj" />
</Solution> </Solution>