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 App() {
|
||||
InitializeComponent();
|
||||
_ = new MauiIcon();
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState? activationState) {
|
||||
|
||||
@@ -3,13 +3,24 @@
|
||||
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"
|
||||
Shell.FlyoutBehavior="Flyout"
|
||||
Title="WorkTime.Mobile">
|
||||
xmlns:pages="clr-namespace:WorkTime.Mobile.Views.Pages"
|
||||
xmlns:icons="http://www.aathifmahir.com/dotnet/2022/maui/icons"
|
||||
FlyoutBehavior="Disabled"
|
||||
Title="Zeiterfassung">
|
||||
|
||||
<Shell.Resources>
|
||||
<ResourceDictionary>
|
||||
<FontImageSource x:Key="CaptureIcon" Glyph="{icons:Material Schedule}" />
|
||||
</ResourceDictionary>
|
||||
</Shell.Resources>
|
||||
|
||||
<TabBar>
|
||||
|
||||
<ShellContent
|
||||
Title="Home"
|
||||
ContentTemplate="{DataTemplate local:MainPage}"
|
||||
Route="MainPage" />
|
||||
Title="Erfassen"
|
||||
Icon="{StaticResource CaptureIcon}"
|
||||
ContentTemplate="{DataTemplate pages:CapturePage}"/>
|
||||
|
||||
</TabBar>
|
||||
|
||||
</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 WorkTime.Mobile.Repositories;
|
||||
using WorkTime.Mobile.Services;
|
||||
using WorkTime.Mobile.ViewModels;
|
||||
using WorkTime.Shared.Repositories;
|
||||
using WorkTime.Shared.Services;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace WorkTime.Mobile;
|
||||
|
||||
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() {
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
@@ -18,25 +19,37 @@ public static class MauiProgram {
|
||||
.ConfigureFonts(fonts => {
|
||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
||||
});
|
||||
})
|
||||
.UseMaterialMauiIcons();
|
||||
|
||||
builder.Services.AddScoped<HttpClient>(provider => {
|
||||
var auth = provider.GetRequiredService<IAuthService>();
|
||||
builder.Services.AddSqlite<DatabaseContext>($"Filename={Path.Combine(FileSystem.AppDataDirectory, "data.db")}");
|
||||
builder.Services.AddSingleton(SecureStorage.Default);
|
||||
builder.Services.AddScoped<HttpClient>(_ => {
|
||||
var client = new HttpClient();
|
||||
client.BaseAddress = new Uri(BackendUrl);
|
||||
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(auth.GetCurrentUserId().Result.ToString());
|
||||
return client;
|
||||
});
|
||||
|
||||
builder.Services.AddKeyedScoped<IEntryRepository, ServerEntryRepository>("server");
|
||||
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<EntryService>();
|
||||
|
||||
builder.Services.AddTransient<CaptureViewModel>();
|
||||
|
||||
#if DEBUG
|
||||
builder.Logging.AddDebug();
|
||||
BackendUrl = "https://localhost:7091/";
|
||||
#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();
|
||||
}
|
||||
|
||||
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.Repositories;
|
||||
|
||||
namespace WorkTime.Mobile.Repositories;
|
||||
|
||||
public class ServerEntryRepository(HttpClient client) : IEntryRepository {
|
||||
public class ServerEntryRepository(IHttpService client) : IEntryRepository {
|
||||
|
||||
public async Task<TimeEntry[]> GetAllEntries(Guid owner) {
|
||||
var response = await client.GetAsync($"entries/{owner}");
|
||||
if (!response.IsSuccessStatusCode) return [];
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<TimeEntry[]>() ?? [];
|
||||
var response = await client.SendRequest<TimeEntry[]>(HttpMethod.Get, $"entries/{owner}");
|
||||
return response.Result ?? [];
|
||||
}
|
||||
|
||||
public async Task<TimeEntry[]> GetEntries(Guid owner, DateOnly date) {
|
||||
var response = await client.GetAsync($"entries/{owner}/{date.ToDateTime(TimeOnly.MinValue)}");
|
||||
if (!response.IsSuccessStatusCode) return [];
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<TimeEntry[]>() ?? [];
|
||||
var response = await client.SendRequest<TimeEntry[]>(HttpMethod.Get, $"entries/{owner}/{date.ToDateTime(TimeOnly.MinValue)}");
|
||||
return response.Result ?? [];
|
||||
}
|
||||
|
||||
public Task<TimeEntry?> GetEntry(int id) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task<TimeEntry?> GetEntry(int id) => Task.FromResult<TimeEntry?>(null);
|
||||
|
||||
public async Task AddEntry(TimeEntry entry) {
|
||||
await client.PostAsJsonAsync("entries", entry);
|
||||
await client.SendRequest(HttpMethod.Post, "entries", 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;
|
||||
|
||||
public class AuthService(ISecureStorage storage) : IAuthService {
|
||||
public class AuthService(ISecureStorage storage, [FromKeyedServices("insecure")] IHttpService httpService) : IAuthService {
|
||||
|
||||
public async Task<bool> IsAuthenticated() {
|
||||
var value = await storage.GetAsync(IAuthService.HeaderName);
|
||||
@@ -13,13 +13,8 @@ public class AuthService(ISecureStorage storage) : IAuthService {
|
||||
var value = await storage.GetAsync(IAuthService.HeaderName);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value)) {
|
||||
var client = new HttpClient();
|
||||
client.BaseAddress = new Uri(MauiProgram.BackendUrl);
|
||||
|
||||
var response = await client.GetAsync("auth/register");
|
||||
if (!response.IsSuccessStatusCode) return Guid.Empty;
|
||||
|
||||
value = await response.Content.ReadAsStringAsync();
|
||||
var response = await httpService.SendRequest<string>(HttpMethod.Get, "auth/register");
|
||||
value = response.Result ?? Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
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>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<!-- Display name -->
|
||||
<ApplicationTitle>WorkTime.Mobile</ApplicationTitle>
|
||||
<ApplicationTitle>Zeiterfassung</ApplicationTitle>
|
||||
|
||||
<!-- App Identifier -->
|
||||
<ApplicationId>com.companyname.worktime.mobile</ApplicationId>
|
||||
<ApplicationId>de.leon-hoppe.worktime.mobile</ApplicationId>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
@@ -60,6 +62,8 @@
|
||||
</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.Maui.Controls" Version="$(MauiVersion)"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/>
|
||||
|
||||
Reference in New Issue
Block a user