Started adding capture page logic
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
namespace WorkTime.Mobile;
|
using MauiIcons.Core;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -3,13 +3,24 @@
|
|||||||
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.Views.Pages"
|
||||||
Shell.FlyoutBehavior="Flyout"
|
xmlns:icons="http://www.aathifmahir.com/dotnet/2022/maui/icons"
|
||||||
Title="WorkTime.Mobile">
|
FlyoutBehavior="Disabled"
|
||||||
|
Title="Zeiterfassung">
|
||||||
|
|
||||||
<ShellContent
|
<Shell.Resources>
|
||||||
Title="Home"
|
<ResourceDictionary>
|
||||||
ContentTemplate="{DataTemplate local:MainPage}"
|
<FontImageSource x:Key="CaptureIcon" Glyph="{icons:Material Schedule}" />
|
||||||
Route="MainPage" />
|
</ResourceDictionary>
|
||||||
|
</Shell.Resources>
|
||||||
|
|
||||||
|
<TabBar>
|
||||||
|
|
||||||
|
<ShellContent
|
||||||
|
Title="Erfassen"
|
||||||
|
Icon="{StaticResource CaptureIcon}"
|
||||||
|
ContentTemplate="{DataTemplate pages:CapturePage}"/>
|
||||||
|
|
||||||
|
</TabBar>
|
||||||
|
|
||||||
</Shell>
|
</Shell>
|
||||||
|
|||||||
17
src/WorkTime.Mobile/Extensions.cs
Normal file
17
src/WorkTime.Mobile/Extensions.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using WorkTime.Shared.Models;
|
||||||
|
|
||||||
|
namespace WorkTime.Mobile;
|
||||||
|
|
||||||
|
public static class Extensions {
|
||||||
|
|
||||||
|
public static string GetActionName(this TimeEntryType type) {
|
||||||
|
return type switch {
|
||||||
|
TimeEntryType.Login => "Einstempeln",
|
||||||
|
TimeEntryType.Logout => "Ausstempeln",
|
||||||
|
TimeEntryType.LoginDrive => "Dienstreise starten",
|
||||||
|
TimeEntryType.LogoutDrive => "Dienstreise beenden",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?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.MainPage">
|
|
||||||
|
|
||||||
<ScrollView>
|
|
||||||
<VerticalStackLayout
|
|
||||||
Padding="30,0"
|
|
||||||
Spacing="25">
|
|
||||||
<Image
|
|
||||||
Source="dotnet_bot.png"
|
|
||||||
HeightRequest="185"
|
|
||||||
Aspect="AspectFit"
|
|
||||||
SemanticProperties.Description="dot net bot in a hovercraft number nine" />
|
|
||||||
|
|
||||||
<Label
|
|
||||||
Text="Hello, World!"
|
|
||||||
Style="{StaticResource Headline}"
|
|
||||||
SemanticProperties.HeadingLevel="Level1" />
|
|
||||||
|
|
||||||
<Label
|
|
||||||
Text="Welcome to .NET Multi-platform App UI"
|
|
||||||
Style="{StaticResource SubHeadline}"
|
|
||||||
SemanticProperties.HeadingLevel="Level2"
|
|
||||||
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
x:Name="CounterBtn"
|
|
||||||
Text="Click me"
|
|
||||||
SemanticProperties.Hint="Counts the number of times you click"
|
|
||||||
Clicked="OnCounterClicked"
|
|
||||||
HorizontalOptions="Fill" />
|
|
||||||
</VerticalStackLayout>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</ContentPage>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
namespace WorkTime.Mobile;
|
|
||||||
|
|
||||||
public partial class MainPage : ContentPage {
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
public MainPage() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCounterClicked(object sender, EventArgs e) {
|
|
||||||
count++;
|
|
||||||
|
|
||||||
if (count == 1)
|
|
||||||
CounterBtn.Text = $"Clicked {count} time";
|
|
||||||
else
|
|
||||||
CounterBtn.Text = $"Clicked {count} times";
|
|
||||||
|
|
||||||
SemanticScreenReader.Announce(CounterBtn.Text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
using System.Net.Http.Headers;
|
using MauiIcons.Material;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using WorkTime.Mobile.Repositories;
|
using WorkTime.Mobile.Repositories;
|
||||||
using WorkTime.Mobile.Services;
|
using WorkTime.Mobile.Services;
|
||||||
|
using WorkTime.Mobile.ViewModels;
|
||||||
using WorkTime.Shared.Repositories;
|
using WorkTime.Shared.Repositories;
|
||||||
using WorkTime.Shared.Services;
|
using WorkTime.Shared.Services;
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ namespace WorkTime.Mobile;
|
|||||||
|
|
||||||
public static class MauiProgram {
|
public static class MauiProgram {
|
||||||
|
|
||||||
public static string BackendUrl { get; private set; } = string.Empty; //TODO: Set production endpoint
|
private static string BackendUrl { get; set; } = string.Empty; //TODO: Set production endpoint
|
||||||
|
|
||||||
public static MauiApp CreateMauiApp() {
|
public static MauiApp CreateMauiApp() {
|
||||||
var builder = MauiApp.CreateBuilder();
|
var builder = MauiApp.CreateBuilder();
|
||||||
@@ -18,25 +19,37 @@ public static class MauiProgram {
|
|||||||
.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");
|
||||||
});
|
})
|
||||||
|
.UseMaterialMauiIcons();
|
||||||
|
|
||||||
builder.Services.AddScoped<HttpClient>(provider => {
|
builder.Services.AddSqlite<DatabaseContext>($"Filename={Path.Combine(FileSystem.AppDataDirectory, "data.db")}");
|
||||||
var auth = provider.GetRequiredService<IAuthService>();
|
builder.Services.AddSingleton(SecureStorage.Default);
|
||||||
|
builder.Services.AddScoped<HttpClient>(_ => {
|
||||||
var client = new HttpClient();
|
var client = new HttpClient();
|
||||||
client.BaseAddress = new Uri(BackendUrl);
|
client.BaseAddress = new Uri(BackendUrl);
|
||||||
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(auth.GetCurrentUserId().Result.ToString());
|
|
||||||
return client;
|
return client;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddKeyedScoped<IEntryRepository, ServerEntryRepository>("server");
|
builder.Services.AddKeyedScoped<IEntryRepository, ServerEntryRepository>("server");
|
||||||
builder.Services.AddKeyedScoped<IEntryRepository, ClientEntryRepository>("client");
|
builder.Services.AddKeyedScoped<IEntryRepository, ClientEntryRepository>("client");
|
||||||
|
builder.Services.AddKeyedScoped<IHttpService, InsecureHttpService>("insecure");
|
||||||
|
builder.Services.AddScoped<IHttpService, HttpService>();
|
||||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
|
builder.Services.AddScoped<EntryService>();
|
||||||
|
|
||||||
|
builder.Services.AddTransient<CaptureViewModel>();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
builder.Logging.AddDebug();
|
builder.Logging.AddDebug();
|
||||||
BackendUrl = "https://localhost:7091/";
|
BackendUrl = "https://localhost:7091/";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
using var scope = app.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,4 +36,14 @@ public class ClientEntryRepository(DatabaseContext context) : IEntryRepository {
|
|||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ReplaceEntries(IEnumerable<TimeEntry> entries, DateOnly date) {
|
||||||
|
var oldEntries = await context.Entries
|
||||||
|
.Where(entry => DateOnly.FromDateTime(entry.Timestamp) == date)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
context.Entries.RemoveRange(oldEntries);
|
||||||
|
await context.Entries.AddRangeAsync(entries);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,29 @@
|
|||||||
using System.Net.Http.Json;
|
using WorkTime.Mobile.Services;
|
||||||
using WorkTime.Shared.Models;
|
using WorkTime.Shared.Models;
|
||||||
using WorkTime.Shared.Repositories;
|
using WorkTime.Shared.Repositories;
|
||||||
|
|
||||||
namespace WorkTime.Mobile.Repositories;
|
namespace WorkTime.Mobile.Repositories;
|
||||||
|
|
||||||
public class ServerEntryRepository(HttpClient client) : IEntryRepository {
|
public class ServerEntryRepository(IHttpService client) : IEntryRepository {
|
||||||
|
|
||||||
public async Task<TimeEntry[]> GetAllEntries(Guid owner) {
|
public async Task<TimeEntry[]> GetAllEntries(Guid owner) {
|
||||||
var response = await client.GetAsync($"entries/{owner}");
|
var response = await client.SendRequest<TimeEntry[]>(HttpMethod.Get, $"entries/{owner}");
|
||||||
if (!response.IsSuccessStatusCode) return [];
|
return response.Result ?? [];
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<TimeEntry[]>() ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TimeEntry[]> GetEntries(Guid owner, DateOnly date) {
|
public async Task<TimeEntry[]> GetEntries(Guid owner, DateOnly date) {
|
||||||
var response = await client.GetAsync($"entries/{owner}/{date.ToDateTime(TimeOnly.MinValue)}");
|
var response = await client.SendRequest<TimeEntry[]>(HttpMethod.Get, $"entries/{owner}/{date.ToDateTime(TimeOnly.MinValue)}");
|
||||||
if (!response.IsSuccessStatusCode) return [];
|
return response.Result ?? [];
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<TimeEntry[]>() ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TimeEntry?> GetEntry(int id) {
|
public Task<TimeEntry?> GetEntry(int id) => Task.FromResult<TimeEntry?>(null);
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AddEntry(TimeEntry entry) {
|
public async Task AddEntry(TimeEntry entry) {
|
||||||
await client.PostAsJsonAsync("entries", entry);
|
await client.SendRequest(HttpMethod.Post, "entries", entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteEntry(TimeEntry entry) {
|
public async Task DeleteEntry(TimeEntry entry) {
|
||||||
await client.DeleteAsync($"entries/{entry.Id}");
|
await client.SendRequest(HttpMethod.Delete, $"entries/{entry.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace WorkTime.Mobile.Services;
|
namespace WorkTime.Mobile.Services;
|
||||||
|
|
||||||
public class AuthService(ISecureStorage storage) : IAuthService {
|
public class AuthService(ISecureStorage storage, [FromKeyedServices("insecure")] IHttpService httpService) : IAuthService {
|
||||||
|
|
||||||
public async Task<bool> IsAuthenticated() {
|
public async Task<bool> IsAuthenticated() {
|
||||||
var value = await storage.GetAsync(IAuthService.HeaderName);
|
var value = await storage.GetAsync(IAuthService.HeaderName);
|
||||||
@@ -13,13 +13,8 @@ public class AuthService(ISecureStorage storage) : IAuthService {
|
|||||||
var value = await storage.GetAsync(IAuthService.HeaderName);
|
var value = await storage.GetAsync(IAuthService.HeaderName);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(value)) {
|
if (string.IsNullOrWhiteSpace(value)) {
|
||||||
var client = new HttpClient();
|
var response = await httpService.SendRequest<string>(HttpMethod.Get, "auth/register");
|
||||||
client.BaseAddress = new Uri(MauiProgram.BackendUrl);
|
value = response.Result ?? Guid.NewGuid().ToString();
|
||||||
|
|
||||||
var response = await client.GetAsync("auth/register");
|
|
||||||
if (!response.IsSuccessStatusCode) return Guid.Empty;
|
|
||||||
|
|
||||||
value = await response.Content.ReadAsStringAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Guid.Parse(value);
|
return Guid.Parse(value);
|
||||||
|
|||||||
66
src/WorkTime.Mobile/Services/EntryService.cs
Normal file
66
src/WorkTime.Mobile/Services/EntryService.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using WorkTime.Mobile.Repositories;
|
||||||
|
using WorkTime.Shared.Models;
|
||||||
|
using WorkTime.Shared.Repositories;
|
||||||
|
using WorkTime.Shared.Services;
|
||||||
|
|
||||||
|
namespace WorkTime.Mobile.Services;
|
||||||
|
|
||||||
|
public class EntryService(
|
||||||
|
[FromKeyedServices("server")] IEntryRepository serverRepository,
|
||||||
|
[FromKeyedServices("client")] IEntryRepository clientRepository,
|
||||||
|
IAuthService authService) {
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<UpdatingEntriesResponse> GetEntries(DateOnly date) {
|
||||||
|
var userId = await authService.GetCurrentUserId();
|
||||||
|
var clientEntries = await clientRepository.GetEntries(userId, date);
|
||||||
|
yield return new() {
|
||||||
|
NewBatch = true
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var entry in clientEntries) {
|
||||||
|
yield return new() {
|
||||||
|
Entry = entry
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverEntries = await serverRepository.GetEntries(userId, date);
|
||||||
|
if (serverEntries.Length == 0) yield break;
|
||||||
|
|
||||||
|
yield return new() {
|
||||||
|
NewBatch = true
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var entry in serverEntries) {
|
||||||
|
yield return new() {
|
||||||
|
Entry = entry
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await ((ClientEntryRepository)clientRepository).ReplaceEntries(serverEntries, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddEntry(TimeEntry entry) {
|
||||||
|
var userId = await authService.GetCurrentUserId();
|
||||||
|
entry.Owner = userId;
|
||||||
|
|
||||||
|
await Task.WhenAll(
|
||||||
|
clientRepository.AddEntry(entry),
|
||||||
|
serverRepository.AddEntry(entry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteEntry(TimeEntry entry) {
|
||||||
|
await Task.WhenAll(
|
||||||
|
clientRepository.DeleteEntry(entry),
|
||||||
|
serverRepository.DeleteEntry(entry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct UpdatingEntriesResponse {
|
||||||
|
|
||||||
|
public bool NewBatch { get; init; }
|
||||||
|
|
||||||
|
public TimeEntry? Entry { get; init; }
|
||||||
|
|
||||||
|
}
|
||||||
101
src/WorkTime.Mobile/Services/HttpService.cs
Normal file
101
src/WorkTime.Mobile/Services/HttpService.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using WorkTime.Shared.Services;
|
||||||
|
|
||||||
|
namespace WorkTime.Mobile.Services;
|
||||||
|
|
||||||
|
public interface IHttpService {
|
||||||
|
Task<HttpResponse<TResult>> SendRequest<TResult>(HttpMethod method, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, object? body = null);
|
||||||
|
|
||||||
|
Task<HttpResponse> SendRequest(HttpMethod method, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, object? body = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class HttpService(HttpClient client, IAuthService authService) : IHttpService {
|
||||||
|
|
||||||
|
private InsecureHttpService? _service;
|
||||||
|
|
||||||
|
public async Task<HttpResponse<TResult>> SendRequest<TResult>(HttpMethod method, string uri, object? body = null) {
|
||||||
|
var service = await GetInternalService();
|
||||||
|
return await service.SendRequest<TResult>(method, uri, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponse> SendRequest(HttpMethod method, string uri, object? body = null) {
|
||||||
|
var service = await GetInternalService();
|
||||||
|
return await service.SendRequest(method, uri, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IHttpService> GetInternalService() {
|
||||||
|
if (_service is null) {
|
||||||
|
var id = await authService.GetCurrentUserId();
|
||||||
|
client.DefaultRequestHeaders.Add(IAuthService.HeaderName, id.ToString());
|
||||||
|
_service = new InsecureHttpService(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _service;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class InsecureHttpService(HttpClient client) : IHttpService {
|
||||||
|
|
||||||
|
public async Task<HttpResponse<TResult>> SendRequest<TResult>(HttpMethod method, string uri, object? body = null) {
|
||||||
|
try {
|
||||||
|
var request = new HttpRequestMessage(method, uri);
|
||||||
|
if (body is not null)
|
||||||
|
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
return new HttpResponse<TResult> {
|
||||||
|
Result = typeof(TResult) == typeof(string) ? (TResult)(object)responseContent : JsonSerializer.Deserialize<TResult>(responseContent),
|
||||||
|
ResponseCode = response.StatusCode,
|
||||||
|
IsSuccessful = response.IsSuccessStatusCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception) {
|
||||||
|
return new() {
|
||||||
|
IsSuccessful = false,
|
||||||
|
ResponseCode = HttpStatusCode.InternalServerError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponse> SendRequest(HttpMethod method, string uri, object? body = null) {
|
||||||
|
try {
|
||||||
|
var request = new HttpRequestMessage(method, uri);
|
||||||
|
if (body is not null)
|
||||||
|
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
return new HttpResponse {
|
||||||
|
ResponseCode = response.StatusCode,
|
||||||
|
IsSuccessful = response.IsSuccessStatusCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception) {
|
||||||
|
return new() {
|
||||||
|
IsSuccessful = false,
|
||||||
|
ResponseCode = HttpStatusCode.InternalServerError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct HttpResponse<TResult> {
|
||||||
|
public TResult? Result { get; init; }
|
||||||
|
public HttpStatusCode ResponseCode { get; init; }
|
||||||
|
public bool IsSuccessful { get; init; }
|
||||||
|
|
||||||
|
public static implicit operator TResult?(HttpResponse<TResult> response) {
|
||||||
|
return response.Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct HttpResponse {
|
||||||
|
public HttpStatusCode ResponseCode { get; init; }
|
||||||
|
public bool IsSuccessful { get; init; }
|
||||||
|
}
|
||||||
77
src/WorkTime.Mobile/ViewModels/CaptureViewModel.cs
Normal file
77
src/WorkTime.Mobile/ViewModels/CaptureViewModel.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using WorkTime.Mobile.Services;
|
||||||
|
using WorkTime.Shared.Models;
|
||||||
|
using WorkTime.Shared.Services;
|
||||||
|
|
||||||
|
namespace WorkTime.Mobile.ViewModels;
|
||||||
|
|
||||||
|
public partial class CaptureViewModel(EntryService entryService, IAuthService authService) : ObservableObject {
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial ObservableCollection<TimeEntry> Entries { get; set; } = new();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial TimeEntryType CurrentAction { get; set; } = TimeEntryType.Login;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool CurrentlyMoba { get; set; }
|
||||||
|
|
||||||
|
public string CurrentActionName => CurrentAction.GetActionName();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OnDateSelected(DateOnly date) {
|
||||||
|
await foreach (var entryResponse in entryService.GetEntries(date)) {
|
||||||
|
if (entryResponse.NewBatch)
|
||||||
|
Entries.Clear();
|
||||||
|
|
||||||
|
if (entryResponse.Entry is not null)
|
||||||
|
Entries.Add(entryResponse.Entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCurrentAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RegisterEntry(TimeEntry? entry = null) {
|
||||||
|
entry ??= new TimeEntry {
|
||||||
|
Type = CurrentAction,
|
||||||
|
IsMoba = CurrentlyMoba,
|
||||||
|
Owner = await authService.GetCurrentUserId()
|
||||||
|
};
|
||||||
|
|
||||||
|
var insertIndex = Entries.Index()
|
||||||
|
.LastOrDefault(e => e.Item.Timestamp < entry.Timestamp)
|
||||||
|
.Index;
|
||||||
|
|
||||||
|
if (Entries.Count == insertIndex + 1 || Entries.Count == 0)
|
||||||
|
Entries.Add(entry);
|
||||||
|
else Entries.Insert(insertIndex + 1, entry);
|
||||||
|
|
||||||
|
await entryService.AddEntry(entry);
|
||||||
|
UpdateCurrentAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCurrentAction() {
|
||||||
|
if (Entries.Count == 0) {
|
||||||
|
CurrentAction = TimeEntryType.Login;
|
||||||
|
CurrentlyMoba = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastEntry = Entries[^1];
|
||||||
|
CurrentAction = lastEntry.Type switch {
|
||||||
|
TimeEntryType.Login => TimeEntryType.Logout,
|
||||||
|
TimeEntryType.Logout => TimeEntryType.Login,
|
||||||
|
TimeEntryType.LoginDrive => TimeEntryType.LogoutDrive,
|
||||||
|
TimeEntryType.LogoutDrive => TimeEntryType.Login,
|
||||||
|
_ => TimeEntryType.Login
|
||||||
|
};
|
||||||
|
CurrentlyMoba = lastEntry is { Type: TimeEntryType.Login, IsMoba: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnCurrentActionChanged(TimeEntryType value) {
|
||||||
|
OnPropertyChanged(nameof(CurrentActionName));
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/WorkTime.Mobile/Views/Components/DateSelector.xaml
Normal file
25
src/WorkTime.Mobile/Views/Components/DateSelector.xaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?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.Views.Components"
|
||||||
|
x:Class="WorkTime.Mobile.Views.Components.DateSelector"
|
||||||
|
x:DataType="components:DateSelector"
|
||||||
|
x:Name="Component">
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
|
||||||
|
<Label
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="Tag"
|
||||||
|
VerticalOptions="Center"/>
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
Grid.Column="1"
|
||||||
|
Date="{Binding CurrentDate, Source={x:Reference Component}}"
|
||||||
|
MaximumDate="{Binding MaxDate, Source={x:Reference Component}}"
|
||||||
|
DateSelected="OnDateSelected"/>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</ContentView>
|
||||||
47
src/WorkTime.Mobile/Views/Components/DateSelector.xaml.cs
Normal file
47
src/WorkTime.Mobile/Views/Components/DateSelector.xaml.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace WorkTime.Mobile.Views.Components;
|
||||||
|
|
||||||
|
public partial class DateSelector : ContentView {
|
||||||
|
|
||||||
|
public static readonly BindableProperty CurrentDateProperty = BindableProperty.Create(
|
||||||
|
nameof(CurrentDate),
|
||||||
|
typeof(DateTime),
|
||||||
|
typeof(DateSelector),
|
||||||
|
DateTime.Now);
|
||||||
|
|
||||||
|
public static readonly BindableProperty MaxDateProperty = BindableProperty.Create(
|
||||||
|
nameof(MaxDate),
|
||||||
|
typeof(DateTime),
|
||||||
|
typeof(DateSelector),
|
||||||
|
DateTime.Now);
|
||||||
|
|
||||||
|
public static readonly BindableProperty CommandProperty = BindableProperty.Create(
|
||||||
|
nameof(Command),
|
||||||
|
typeof(IRelayCommand<DateOnly>),
|
||||||
|
typeof(DateSelector));
|
||||||
|
|
||||||
|
public DateTime CurrentDate {
|
||||||
|
get => (DateTime)GetValue(CurrentDateProperty);
|
||||||
|
set => SetValue(CurrentDateProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime MaxDate {
|
||||||
|
get => (DateTime)GetValue(MaxDateProperty);
|
||||||
|
set => SetValue(MaxDateProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRelayCommand<DateOnly>? Command {
|
||||||
|
get => (IRelayCommand<DateOnly>)GetValue(CommandProperty);
|
||||||
|
set => SetValue(CommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateSelector() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDateSelected(object? sender, DateChangedEventArgs e) {
|
||||||
|
var date = DateOnly.FromDateTime(CurrentDate);
|
||||||
|
Command?.Execute(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/WorkTime.Mobile/Views/Pages/CapturePage.xaml
Normal file
39
src/WorkTime.Mobile/Views/Pages/CapturePage.xaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?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.Views.Components"
|
||||||
|
xmlns:viewModels="clr-namespace:WorkTime.Mobile.ViewModels"
|
||||||
|
xmlns:models="clr-namespace:WorkTime.Shared.Models;assembly=WorkTime.Shared"
|
||||||
|
x:Class="WorkTime.Mobile.Views.Pages.CapturePage"
|
||||||
|
x:DataType="viewModels:CaptureViewModel"
|
||||||
|
Padding="24, 0, 24, 24">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
|
<components:DateSelector
|
||||||
|
Grid.Row="0"
|
||||||
|
Command="{Binding DateSelectedCommand}"/>
|
||||||
|
|
||||||
|
<CollectionView
|
||||||
|
Grid.Row="1"
|
||||||
|
ItemsSource="{Binding Entries}">
|
||||||
|
<CollectionView.ItemTemplate>
|
||||||
|
<DataTemplate
|
||||||
|
x:DataType="models:TimeEntry">
|
||||||
|
<HorizontalStackLayout Spacing="5">
|
||||||
|
<Label Text="{Binding Timestamp}" />
|
||||||
|
<Label Text="{Binding Type}" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</DataTemplate>
|
||||||
|
</CollectionView.ItemTemplate>
|
||||||
|
</CollectionView>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Grid.Row="2"
|
||||||
|
Text="{Binding CurrentActionName}"
|
||||||
|
Command="{Binding RegisterEntryCommand}"/>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</ContentPage>
|
||||||
15
src/WorkTime.Mobile/Views/Pages/CapturePage.xaml.cs
Normal file
15
src/WorkTime.Mobile/Views/Pages/CapturePage.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using WorkTime.Mobile.ViewModels;
|
||||||
|
|
||||||
|
namespace WorkTime.Mobile.Views.Pages;
|
||||||
|
|
||||||
|
public partial class CapturePage : ContentPage {
|
||||||
|
public CapturePage(CaptureViewModel model) {
|
||||||
|
InitializeComponent();
|
||||||
|
BindingContext = model;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,12 +19,14 @@
|
|||||||
<SingleProject>true</SingleProject>
|
<SingleProject>true</SingleProject>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
|
||||||
<!-- 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.mobile</ApplicationId>
|
||||||
|
|
||||||
<!-- Versions -->
|
<!-- Versions -->
|
||||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||||
@@ -60,6 +62,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AathifMahir.Maui.MauiIcons.Material" Version="4.0.0" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/>
|
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user