diff --git a/WorkTime.Database/DatabaseContext.cs b/WorkTime.Database/DatabaseContext.cs new file mode 100644 index 0000000..6103479 --- /dev/null +++ b/WorkTime.Database/DatabaseContext.cs @@ -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 options) : DbContext(options) { + + public DbSet 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(); + + await context.Database.MigrateAsync(cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/WorkTime.Database/Migrations/20251112121330_Initial.Designer.cs b/WorkTime.Database/Migrations/20251112121330_Initial.Designer.cs new file mode 100644 index 0000000..88b9458 --- /dev/null +++ b/WorkTime.Database/Migrations/20251112121330_Initial.Designer.cs @@ -0,0 +1,42 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WorkTime.Database/Migrations/20251112121330_Initial.cs b/WorkTime.Database/Migrations/20251112121330_Initial.cs new file mode 100644 index 0000000..efd929d --- /dev/null +++ b/WorkTime.Database/Migrations/20251112121330_Initial.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WorkTime.Database.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Entries", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + Timestamp = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Entries", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Entries"); + } + } +} diff --git a/WorkTime.Database/Migrations/DatabaseContextModelSnapshot.cs b/WorkTime.Database/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 0000000..a0e4c1b --- /dev/null +++ b/WorkTime.Database/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,39 @@ +// +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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WorkTime.Database/Repositories/SettingsRepository.cs b/WorkTime.Database/Repositories/SettingsRepository.cs new file mode 100644 index 0000000..cbb189d --- /dev/null +++ b/WorkTime.Database/Repositories/SettingsRepository.cs @@ -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 LoadSettings() { + if (!File.Exists(filePath)) + return new Settings(); + + var raw = await File.ReadAllTextAsync(filePath); + return JsonSerializer.Deserialize(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 {} diff --git a/WorkTime.Database/Repositories/TimeEntryRepository.cs b/WorkTime.Database/Repositories/TimeEntryRepository.cs new file mode 100644 index 0000000..2b44833 --- /dev/null +++ b/WorkTime.Database/Repositories/TimeEntryRepository.cs @@ -0,0 +1,22 @@ +using WorkTime.Models; +using WorkTime.Models.Repositories; + +namespace WorkTime.Database.Repositories; + +internal sealed class TimeEntryRepository : ITimeEntryRepository { + public Task> GetTimeEntries() { + throw new NotImplementedException(); + } + + public Task> GetTimeEntries(DateOnly date) { + throw new NotImplementedException(); + } + + public Task AddTimeEntry(TimeEntry entry) { + throw new NotImplementedException(); + } + + public Task DeleteTimeEntry(Guid id) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/WorkTime.Database/ServiceCollectionExtensions.cs b/WorkTime.Database/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..b77d58b --- /dev/null +++ b/WorkTime.Database/ServiceCollectionExtensions.cs @@ -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($"Filename={Path.Combine(basePath, "data.db")}"); + services.AddHostedService(); + services.AddTransient(); + services.AddTransient(_ => new SettingsRepository(Path.Combine(basePath, "settings.json"))); + } + +} \ No newline at end of file diff --git a/WorkTime.Database/WorkTime.Database.csproj b/WorkTime.Database/WorkTime.Database.csproj new file mode 100644 index 0000000..b67a979 --- /dev/null +++ b/WorkTime.Database/WorkTime.Database.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/WorkTime.Migrator/Program.cs b/WorkTime.Migrator/Program.cs new file mode 100644 index 0000000..5ef9eab --- /dev/null +++ b/WorkTime.Migrator/Program.cs @@ -0,0 +1,8 @@ +using WorkTime.Database; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddDatabase(Environment.CurrentDirectory); + +var host = builder.Build(); +host.Run(); \ No newline at end of file diff --git a/WorkTime.Migrator/Properties/launchSettings.json b/WorkTime.Migrator/Properties/launchSettings.json new file mode 100644 index 0000000..4edbb2e --- /dev/null +++ b/WorkTime.Migrator/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "WorkTime.Migrator": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WorkTime.Migrator/WorkTime.Migrator.csproj b/WorkTime.Migrator/WorkTime.Migrator.csproj new file mode 100644 index 0000000..5f5d431 --- /dev/null +++ b/WorkTime.Migrator/WorkTime.Migrator.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + dotnet-WorkTime.Migrator-77e5f218-8552-46f7-943e-28675db76107 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/WorkTime.Migrator/appsettings.Development.json b/WorkTime.Migrator/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/WorkTime.Migrator/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/WorkTime.Migrator/appsettings.json b/WorkTime.Migrator/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/WorkTime.Migrator/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/WorkTime.Mobile/App.xaml.cs b/WorkTime.Mobile/App.xaml.cs index b7333d9..ebba889 100644 --- a/WorkTime.Mobile/App.xaml.cs +++ b/WorkTime.Mobile/App.xaml.cs @@ -1,10 +1,12 @@ -using Microsoft.Extensions.DependencyInjection; +using MauiIcons.Core; +using Microsoft.Extensions.DependencyInjection; namespace WorkTime.Mobile; public partial class App : Application { public App() { InitializeComponent(); + _ = new MauiIcon(); } protected override Window CreateWindow(IActivationState? activationState) { diff --git a/WorkTime.Mobile/AppShell.xaml b/WorkTime.Mobile/AppShell.xaml index a80298e..6005754 100644 --- a/WorkTime.Mobile/AppShell.xaml +++ b/WorkTime.Mobile/AppShell.xaml @@ -3,12 +3,35 @@ x:Class="WorkTime.Mobile.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" - xmlns:local="clr-namespace:WorkTime.Mobile" - Title="WorkTime.Mobile"> + xmlns:pages="clr-namespace:WorkTime.Mobile.Pages" + xmlns:icons="http://www.aathifmahir.com/dotnet/2022/maui/icons" + Title="Zeiterfassung"> + + + + + + + + - + + + + + + + + + diff --git a/WorkTime.Mobile/MauiProgram.cs b/WorkTime.Mobile/MauiProgram.cs index 232802b..4ff3fe4 100644 --- a/WorkTime.Mobile/MauiProgram.cs +++ b/WorkTime.Mobile/MauiProgram.cs @@ -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; @@ -7,11 +11,17 @@ public static class MauiProgram { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() + .UseMauiCommunityToolkit() + .UseMaterialMauiIcons() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); + builder.Services.AddDatabase(FileSystem.AppDataDirectory); + + builder.Services.AddTransient(); + #if DEBUG builder.Logging.AddDebug(); #endif diff --git a/WorkTime.Mobile/Pages/AnalysisPage.xaml b/WorkTime.Mobile/Pages/AnalysisPage.xaml new file mode 100644 index 0000000..984d059 --- /dev/null +++ b/WorkTime.Mobile/Pages/AnalysisPage.xaml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/WorkTime.Mobile/Pages/AnalysisPage.xaml.cs b/WorkTime.Mobile/Pages/AnalysisPage.xaml.cs new file mode 100644 index 0000000..16a5921 --- /dev/null +++ b/WorkTime.Mobile/Pages/AnalysisPage.xaml.cs @@ -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(); + } +} \ No newline at end of file diff --git a/WorkTime.Mobile/Pages/CapturePage.xaml b/WorkTime.Mobile/Pages/CapturePage.xaml new file mode 100644 index 0000000..bcadcbc --- /dev/null +++ b/WorkTime.Mobile/Pages/CapturePage.xaml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/WorkTime.Mobile/Pages/CapturePage.xaml.cs b/WorkTime.Mobile/Pages/CapturePage.xaml.cs new file mode 100644 index 0000000..a4772c6 --- /dev/null +++ b/WorkTime.Mobile/Pages/CapturePage.xaml.cs @@ -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(); + } +} \ No newline at end of file diff --git a/WorkTime.Mobile/Pages/Components/SettingComponent.xaml b/WorkTime.Mobile/Pages/Components/SettingComponent.xaml new file mode 100644 index 0000000..99b3217 --- /dev/null +++ b/WorkTime.Mobile/Pages/Components/SettingComponent.xaml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/WorkTime.Mobile/Pages/Components/SettingComponent.xaml.cs b/WorkTime.Mobile/Pages/Components/SettingComponent.xaml.cs new file mode 100644 index 0000000..9fad078 --- /dev/null +++ b/WorkTime.Mobile/Pages/Components/SettingComponent.xaml.cs @@ -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(nameof(EntryField)).Text = string.Empty; + } +} \ No newline at end of file diff --git a/WorkTime.Mobile/Pages/SettingsPage.xaml b/WorkTime.Mobile/Pages/SettingsPage.xaml new file mode 100644 index 0000000..7a0eadd --- /dev/null +++ b/WorkTime.Mobile/Pages/SettingsPage.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +