Refactor Api-Client

Add Exceptionhandler, AlertService JSON-Converter
AppSettings via DI

Reformat Code
This commit is contained in:
2025-12-17 09:34:08 +01:00
parent 544b0c9591
commit 76eb71946f
84 changed files with 3178 additions and 2191 deletions

View File

@@ -1,4 +1,5 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Jugenddienst_Stunden"
@@ -11,4 +12,4 @@
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
</Application>

View File

@@ -4,12 +4,11 @@
/// Die Hauptanwendungsklasse.
/// </summary>
public partial class App : Application {
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="App"/>-Klasse.
/// </summary>
public App() {
InitializeComponent();
MainPage = new AppShell();
}
}
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="App"/>-Klasse.
/// </summary>
public App() {
InitializeComponent();
MainPage = new AppShell();
}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<Shell
x:Class="Jugenddienst_Stunden.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
@@ -10,8 +11,9 @@
<ShellContent
Title="Stunden"
ContentTemplate="{DataTemplate views:StundenPage}"
Icon="{OnPlatform 'icon_watch.png', iOS='icon_watch_ios.png', MacCatalyst='icon_watch_ios.png'}" Route="StundenPage" />
Icon="{OnPlatform 'icon_watch.png', iOS='icon_watch_ios.png', MacCatalyst='icon_watch_ios.png'}"
Route="StundenPage" />
<ShellContent
Title="Notizen"
ContentTemplate="{DataTemplate views:AllNotesPage}"

View File

@@ -1,4 +1,5 @@
namespace Jugenddienst_Stunden;
/// <summary>
/// AppShell.xaml.cs
/// </summary>
@@ -12,8 +13,8 @@ public partial class AppShell : Shell {
//Seiten, die nicht in der Appshell sichtbar sind, aber trotzdem aufgerufen werden können
Routing.RegisterRoute(nameof(Views.NotePage), typeof(Views.NotePage));
Routing.RegisterRoute(nameof(Views.StundePage), typeof(Views.StundePage));
//Muss ich die registrieren?
Routing.RegisterRoute(nameof(Views.LoginPage), typeof(Views.LoginPage));
}
}
}

View File

@@ -2,20 +2,21 @@
/// Gib true zurück, wenn die Collection Werte enthält
namespace Jugenddienst_Stunden.Converter {
internal class CollectionVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is IEnumerable<object> collection) {
if ((string)parameter == "Invert")
return !collection.Any();
return collection.Any();
}
if ((string)parameter == "Invert")
return true;
return false;
}
internal class CollectionVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is IEnumerable<object> collection) {
if ((string)parameter == "Invert")
return !collection.Any();
return collection.Any();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}
if ((string)parameter == "Invert")
return true;
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View File

@@ -1,11 +1,11 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
/// <summary>
/// Falls ein int als bool dargestellt werden soll
/// </summary>
public class IntBoolConverter : IValueConverter {
/// <summary>
/// Konvertiert einen int in einen bool
/// </summary>
@@ -18,6 +18,7 @@ public class IntBoolConverter : IValueConverter {
if (value is int) {
return (int)value != 0;
}
return false;
}
@@ -33,6 +34,7 @@ public class IntBoolConverter : IValueConverter {
if (value is bool) {
return (bool)value ? 1 : 0;
}
return 0;
}
}
}

View File

@@ -1,36 +1,37 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
internal class SecondsTimeConverter : IValueConverter {
private int seconds;
private int seconds;
/// <summary>
/// Konvertiert eine Sekundenangabe nach Stunden:Minuten, auch bei mehr als 24 Stunden
/// </summary>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is null)
return "0:0";
if (value is int) {
seconds = (int)value;
}
/// <summary>
/// Konvertiert eine Sekundenangabe nach Stunden:Minuten, auch bei mehr als 24 Stunden
/// </summary>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is null)
return "0:0";
if (value is int) {
seconds = (int)value;
}
if (value is double) {
seconds = (int)Math.Round((double)value);
} else {
int.TryParse((string?)value, out seconds);
}
if (value is double) {
seconds = (int)Math.Round((double)value);
} else {
int.TryParse((string?)value, out seconds);
}
TimeSpan time = TimeSpan.FromSeconds(seconds);
TimeSpan time = TimeSpan.FromSeconds(seconds);
return (int)time.TotalHours + ":" + Math.Abs(time.Minutes);
return (int)time.TotalHours + ":" + Math.Abs(time.Minutes);
//return time.ToString(@"hh\:mm");
//return time.ToString(@"hh\:mm\:ss");
//return time.ToString(@"hh\:mm");
//return time.ToString(@"hh\:mm\:ss");
//return "00:00";
}
//return "00:00";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}

View File

@@ -1,15 +1,17 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
internal class StringVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is string strValue) {
return !string.IsNullOrEmpty(strValue.Replace("Server: ",""));
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
internal class StringVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is string strValue) {
return !string.IsNullOrEmpty(strValue.Replace("Server: ", ""));
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}

View File

@@ -4,9 +4,15 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Exceptions;
namespace Jugenddienst_Stunden.Exceptions;
public class NoDataException : Exception {
public NoDataException() : base("Keine Daten gefunden") { }
public NoDataException(string message) : base(message) { }
public NoDataException(string message, Exception inner) : base(message, inner) { }
}
public NoDataException() : base("Keine Daten gefunden") {
}
public NoDataException(string message) : base(message) {
}
public NoDataException(string message, Exception inner) : base(message, inner) {
}
}

View File

@@ -0,0 +1,15 @@
using Jugenddienst_Stunden.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class AlertService : IAlertService {
public event EventHandler<string>? AlertRaised;
public void Raise(string message) {
AlertRaised?.Invoke(this, message);
}
}

View File

@@ -0,0 +1,111 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Net.Http.Json;
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class ApiClient : IApiClient {
private readonly HttpClient _http;
private readonly JsonSerializerOptions _json;
private readonly ApiOptions _options;
private readonly IAppSettings _settings;
public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) {
_http = http;
_options = options;
_settings = settings;
_http.Timeout = options.Timeout;
if (!_http.DefaultRequestHeaders.Accept.Any())
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var token = tokenProvider.GetToken();
if (!string.IsNullOrWhiteSpace(token))
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
_json = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
// Globale Converter: erlauben numerische Felder auch als Strings (z.B. user.id)
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleInt32Converter());
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleNullableInt32Converter());
// Stelle sicher, dass die BaseAddress sofort aus den aktuellen Settings (Preferences) gesetzt wird
// und nicht erst beim ersten Request. Dadurch steht die ApiUrl ab Initialisierung zur Verfügung.
EnsureBaseAddress();
}
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> SendAsync<T>(HttpMethod.Get, path, null, query, ct);
public async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? query = null, CancellationToken ct = default) {
// Vor jedem Request sicherstellen, dass die (ggf. geänderte) BaseAddress gesetzt ist
EnsureBaseAddress();
var uri = BuildUri(path, query);
using var req = new HttpRequestMessage(method, uri) {
Content = body is null ? null : JsonContent.Create(body, options: _json)
};
using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!res.IsSuccessStatusCode)
throw ApiException.From(res.StatusCode, TryGetMessage(text), text);
if (typeof(T) == typeof(void) || typeof(T) == typeof(object) || string.IsNullOrWhiteSpace(text))
return default!;
var obj = System.Text.Json.JsonSerializer.Deserialize<T>(text, _json);
if (obj is null)
throw new ApiException("Fehler beim Deserialisieren der Daten.");
return obj;
}
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> SendAsync<object>(HttpMethod.Delete, path, null, query, ct);
private void EnsureBaseAddress() {
var baseUrl = _settings.ApiUrl;
if (string.IsNullOrWhiteSpace(baseUrl)) {
throw new InvalidOperationException(
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl'), " +
"z.B. im Login/Setup, bevor API-Aufrufe stattfinden."
);
}
// nur setzen, wenn nötig (damit spätere Änderungen nach Login greifen)
if (_http.BaseAddress is null || !Uri.Equals(_http.BaseAddress, new Uri(baseUrl, UriKind.Absolute))) {
_http.BaseAddress = new Uri(baseUrl, UriKind.Absolute);
_http.Timeout = _options.Timeout;
}
}
private static string TryGetMessage(string text) {
try {
using var doc = JsonDocument.Parse(text);
if (doc.RootElement.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String)
return m.GetString() ?? text;
} catch {
}
return text;
}
private static Uri BuildUri(string path, IDictionary<string, string?>? query) {
if (query is null || query.Count == 0)
return new Uri(path, UriKind.Relative);
var sb = new StringBuilder(path);
sb.Append(path.Contains('?') ? '&' : '?');
sb.Append(string.Join('&', query
.Where(kv => kv.Value is not null)
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
return new Uri(sb.ToString(), UriKind.Relative);
}
}

View File

@@ -0,0 +1,6 @@
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class ApiOptions {
public required string BaseUrl { get; init; }
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(15);
}

View File

@@ -0,0 +1,23 @@
using System.Net;
namespace Jugenddienst_Stunden.Infrastructure;
internal class ApiException : Exception {
public HttpStatusCode StatusCode { get; }
public string? ResponseBody { get; }
public ApiException(string message, HttpStatusCode statusCode = 0, string? responseBody = null,
Exception? inner = null)
: base(message, inner) {
StatusCode = statusCode;
ResponseBody = responseBody;
}
public static ApiException From(HttpStatusCode statusCode, string message, string? responseBody = null)
=> new ApiException(message, statusCode, responseBody);
}
internal class ValidationException : Exception {
public ValidationException(string message) : base(message) {
}
}

View File

@@ -0,0 +1,23 @@
using Jugenddienst_Stunden.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class NullApiClient : IApiClient {
private readonly string _message;
public NullApiClient(string message) => _message = message ?? "API nicht konfiguriert.";
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> Task.FromException<T>(new InvalidOperationException(_message));
public Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> Task.FromException<T>(new InvalidOperationException(_message));
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> Task.FromException(new InvalidOperationException(_message));
}

View File

@@ -0,0 +1,30 @@
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class PreferencesAppSettings : IAppSettings {
public string ApiUrl {
get => Preferences.Default.Get("apiUrl", "");
set => Preferences.Default.Set("apiUrl", value);
}
public string ApiKey {
get => Preferences.Default.Get("apiKey", "");
set => Preferences.Default.Set("apiKey", value);
}
public int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0);
set => Preferences.Default.Set("EmployeeId", value);
}
public string Name {
get => Preferences.Default.Get("name", "Nicht");
set => Preferences.Default.Set("name", value);
}
public string Surname {
get => Preferences.Default.Get("surname", "Eingeloggt");
set => Preferences.Default.Set("surname", value);
}
}

View File

@@ -0,0 +1,10 @@
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class SettingsTokenProvider : ITokenProvider {
private readonly IAppSettings _settings;
public SettingsTokenProvider(IAppSettings settings) => _settings = settings;
public string GetToken() => _settings.ApiKey;
}

View File

@@ -0,0 +1,7 @@
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class GlobalVarTokenProvider : ITokenProvider {
public string? GetToken() => Models.GlobalVar.ApiKey;
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Interfaces;
internal interface IAlertService {
event EventHandler<string> AlertRaised;
void Raise(string message);
}

View File

@@ -0,0 +1,10 @@
namespace Jugenddienst_Stunden.Interfaces;
internal interface IApiClient {
Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default);
Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? query = null, CancellationToken ct = default);
Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default);
}

View File

@@ -0,0 +1,10 @@
namespace Jugenddienst_Stunden.Interfaces;
public interface IAppSettings {
string ApiUrl { get; set; }
string ApiKey { get; set; }
int EmployeeId { get; set; }
string Name { get; set; }
string Surname { get; set; }
}

View File

@@ -6,12 +6,12 @@ namespace Jugenddienst_Stunden.Interfaces;
/// RepositorySchnittstelle für Datenzugriff (API/Storage) rund um Stunden.
/// </summary>
internal interface IHoursRepository {
Task<BaseResponse> LoadBase(string query);
Task<Settings> LoadSettings();
Task<Hours> LoadData();
Task<User> LoadUser(string apiKey);
Task<List<DayTime>> LoadDay(DateTime date);
Task<DayTime> LoadEntry(int id);
Task<DayTime> SaveEntry(DayTime stunde);
Task DeleteEntry(DayTime stunde);
}
Task<BaseResponse> LoadBase(string query);
Task<Settings> LoadSettings();
Task<Hours> LoadData();
Task<User> LoadUser(string apiKey);
Task<List<DayTime>> LoadDay(DateTime date);
Task<DayTime> LoadEntry(int id);
Task<DayTime> SaveEntry(DayTime stunde);
Task DeleteEntry(DayTime stunde);
}

View File

@@ -5,12 +5,12 @@ namespace Jugenddienst_Stunden.Interfaces;
/// <summary>
/// Fachlicher Service für Stunden konsumiert Repository und stellt VMfreundliche Methoden bereit.
/// </summary>
internal interface IHoursService {
Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate);
Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date);
Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to);
Task<Settings> GetSettingsAsync();
Task<DayTime> GetEntryAsync(int id);
Task<DayTime> SaveEntryAsync(DayTime stunde);
Task DeleteEntryAsync(DayTime stunde);
}
public interface IHoursService {
Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate);
Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date);
Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to);
Task<Settings> GetSettingsAsync();
Task<DayTime> GetEntryAsync(int id);
Task<DayTime> SaveEntryAsync(DayTime stunde);
Task DeleteEntryAsync(DayTime stunde);
}

View File

@@ -0,0 +1,5 @@
namespace Jugenddienst_Stunden.Interfaces;
internal interface ITokenProvider {
string? GetToken();
}

View File

@@ -1,312 +1,312 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- <TargetFrameworks>net8.0-maccatalyst;net9.0-android35.0</TargetFrameworks> -->
<TargetFrameworks>net9.0-android35.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<PropertyGroup>
<!-- <TargetFrameworks>net8.0-maccatalyst;net9.0-android35.0</TargetFrameworks> -->
<TargetFrameworks>net9.0-android35.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>Jugenddienst_Stunden</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<RootNamespace>Jugenddienst_Stunden</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Display name -->
<ApplicationTitle>Jugenddienst Stunden</ApplicationTitle>
<!-- Display name -->
<ApplicationTitle>Jugenddienst Stunden</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<!-- App Identifier -->
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<!-- Versions -->
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">29.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageIcon>paket_icon.png</PackageIcon>
<NeutralLanguage>de</NeutralLanguage>
<PackageVersion>1.0.9</PackageVersion>
</PropertyGroup>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">29.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageIcon>paket_icon.png</PackageIcon>
<NeutralLanguage>de</NeutralLanguage>
<PackageVersion>1.0.9</PackageVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'">
<Optimize>True</Optimize>
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'">
<Optimize>True</Optimize>
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>True</DebugSymbols>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>True</DebugSymbols>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup>
<DefaultLanguage>de-de</DefaultLanguage>
</PropertyGroup>
<PropertyGroup>
<DefaultLanguage>de-de</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<Deterministic>True</Deterministic>
<Optimize>False</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<Deterministic>True</Deterministic>
<Optimize>False</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>False</DebugSymbols>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<RunAOTCompilation>False</RunAOTCompilation>
<PublishTrimmed>True</PublishTrimmed>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>False</DebugSymbols>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<RunAOTCompilation>False</RunAOTCompilation>
<PublishTrimmed>True</PublishTrimmed>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PlatformTarget>AnyCPU</PlatformTarget>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AssemblyVersion>1.0.9</AssemblyVersion>
<FileVersion>1.0.9</FileVersion>
</PropertyGroup>
<PropertyGroup>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PlatformTarget>AnyCPU</PlatformTarget>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AssemblyVersion>1.0.9</AssemblyVersion>
<FileVersion>1.0.9</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
<DebugSymbols>False</DebugSymbols>
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
<DebugSymbols>False</DebugSymbols>
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.26100.0</TargetFrameworks>
<WindowsPackageType>None</WindowsPackageType>
<!-- <TargetFrameworks>;net9.0-android35.0</TargetFrameworks> -->
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.26100.0</TargetFrameworks>
<WindowsPackageType>None</WindowsPackageType>
<!-- <TargetFrameworks>;net9.0-android35.0</TargetFrameworks> -->
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#F7931D" BaseSize="128,128" />
<!-- Splash Screen (Windows fix) -->
<!--<MauiImage Include="Resources\Images\logo_splash_win.svg" Color="#F7931D" BaseSize="208,208" />-->
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#F7931D" BaseSize="128,128"/>
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<!-- Splash Screen (Windows fix) -->
<!--<MauiImage Include="Resources\Images\logo_splash_win.svg" Color="#F7931D" BaseSize="208,208" />-->
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Images -->
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185"/>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/>
<ItemGroup>
<None Remove="Resources\Windows\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-400.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-400.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-400.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-400.png" />
</ItemGroup>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Windows\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\$placeholder$.scale-400.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-400.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-400.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-400.png" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Windows\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-400.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-400.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-400.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-400.png"/>
</ItemGroup>
<ItemGroup>
<None Include="..\paket_icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Windows\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-400.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-400.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-400.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-400.png"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="12.2.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.110">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.110" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9" />
<PackageReference Include="Microsoft.Maui.Graphics" Version="9.0.110" />
<PackageReference Include="Microsoft.NET.Runtime.MonoAOTCompiler.Task" Version="9.0.9" />
<PackageReference Include="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk" Version="9.0.9" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ZXing.Net.Maui.Controls" Version="0.5.0" />
</ItemGroup>
<ItemGroup>
<None Include="..\paket_icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Views\LoginPage.xaml.cs">
<DependentUpon>LoginPage.xaml</DependentUpon>
</Compile>
<Compile Update="Views\StundePage.xaml.cs">
<DependentUpon>StundePage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="12.2.0"/>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0"/>
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.110">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.110"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9"/>
<PackageReference Include="Microsoft.Maui.Graphics" Version="9.0.110"/>
<PackageReference Include="Microsoft.NET.Runtime.MonoAOTCompiler.Task" Version="9.0.9"/>
<PackageReference Include="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk" Version="9.0.9"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="ZXing.Net.Maui.Controls" Version="0.5.0"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Views\LoginPage.xaml.cs">
<DependentUpon>LoginPage.xaml</DependentUpon>
</Compile>
<Compile Update="Views\StundePage.xaml.cs">
<DependentUpon>StundePage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Views\LoginPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\AllNotesPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\NotePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundenPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Views\LoginPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\AllNotesPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\NotePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundenPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
</Project>

View File

@@ -3,8 +3,11 @@ using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Repositories;
using Jugenddienst_Stunden.Services;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Validators;
using Microsoft.Extensions.Logging;
using ZXing.Net.Maui.Controls;
using System.Net.Http;
namespace Jugenddienst_Stunden;
@@ -12,39 +15,84 @@ namespace Jugenddienst_Stunden;
/// Das Hauptprogramm.
/// </summary>
public static class MauiProgram {
public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
// Initialize the .NET MAUI Community Toolkit by adding the below line of code
.UseMauiCommunityToolkit(options => { options.SetShouldEnableSnackbarOnWindows(true); })
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
//.UseBarcodeScanning();
.UseBarcodeReader();
public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
// Initialize the .NET MAUI Community Toolkit by adding the below line of code
.UseMauiCommunityToolkit(options => {
options.SetShouldEnableSnackbarOnWindows(true);
})
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
//.UseBarcodeScanning();
.UseBarcodeReader();
//#if DEBUG
// if (string.IsNullOrWhiteSpace(GlobalVar.ApiKey)) {
// GlobalVar.ApiKey = Preferences.Default.Get("apiKey",
// "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
// GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
// GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
// GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
// GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
// }
#if DEBUG
if (GlobalVar.ApiKey == null) {
GlobalVar.ApiKey = Preferences.Default.Get("apiKey", "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
}
builder.Logging.AddDebug();
#endif
// builder.Logging.AddDebug();
//#endif
// DI: Services & Repositories
builder.Services.AddSingleton<IHoursRepository, HoursRepository>();
builder.Services.AddSingleton<IHoursService, HoursService>();
// DI: AlertService für globale Alerts (z. B. leere ApiUrl)
builder.Services.AddSingleton<IAlertService, AlertService>();
return builder.Build();
}
// DI: Settings aus Preferences (Single Source of Truth bleibt Preferences)
builder.Services.AddSingleton<IAppSettings, PreferencesAppSettings>();
// DI: ApiOptions IMMER aus aktuellen Settings erzeugen (nicht beim Start einfrieren)
builder.Services.AddTransient(sp => new ApiOptions {
BaseUrl = sp.GetRequiredService<IAppSettings>().ApiUrl, Timeout = TimeSpan.FromSeconds(15)
});
}
// Token Provider soll ebenfalls aus Settings/Preferences lesen
builder.Services.AddSingleton<ITokenProvider, SettingsTokenProvider>();
// HttpClient + ApiClient
builder.Services.AddSingleton<HttpClient>(_ => new HttpClient());
builder.Services.AddSingleton<IApiClient>(sp => {
var alert = sp.GetRequiredService<IAlertService>();
try {
return new ApiClient(
sp.GetRequiredService<HttpClient>(),
sp.GetRequiredService<ApiOptions>(),
sp.GetRequiredService<ITokenProvider>(),
sp.GetRequiredService<IAppSettings>());
} catch (Exception e) {
// Alert an UI/VM weiterreichen
alert.Raise(e.Message);
// Fallback: NullApiClient liefert beim Aufruf aussagekräftige Exception
return new NullApiClient(e.Message);
}
});
// DI: Infrastruktur
//builder.Services.AddSingleton(new ApiOptions { BaseUrl = GlobalVar.ApiUrl, Timeout = TimeSpan.FromSeconds(15) });
//builder.Services.AddSingleton<ITokenProvider, GlobalVarTokenProvider>();
//builder.Services.AddSingleton<HttpClient>(_ => new HttpClient());
//builder.Services.AddSingleton<IApiClient>(sp => new ApiClient(
// sp.GetRequiredService<HttpClient>(),
// sp.GetRequiredService<ApiOptions>(),
// sp.GetRequiredService<ITokenProvider>()));
// DI: Validatoren
builder.Services.AddSingleton<IHoursValidator, HoursValidator>();
// DI: Services & Repositories
builder.Services.AddSingleton<IHoursRepository, HoursRepository>();
builder.Services.AddSingleton<IHoursService, HoursService>();
// DI: Views/ViewModels
builder.Services.AddTransient<ViewModels.StundenViewModel>();
builder.Services.AddTransient<Views.StundenPage>();
return builder.Build();
}
}

View File

@@ -7,186 +7,175 @@ using System.Text.Json;
namespace Jugenddienst_Stunden.Models;
internal static class BaseFunc {
internal static async Task<string> GetApiDataWithAuthAsync(string url, string token) {
if (Connectivity.Current.NetworkAccess == NetworkAccess.None)
throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.");
if (string.IsNullOrEmpty(token))
throw new Exception("Kein APIKEY, bitte zuerst Login durchführen");
// Erstellen eines HttpClient-Objekts
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Hinzufügen des Bearer-Tokens zum Authorization-Header
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.GetAsync(url).ConfigureAwait(false)) {
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
//using (HttpContent HttpContent = HttpResponseMessage.Content) {
// //responseData = await HttpContent.ReadAsStringAsync();
//}
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
return responseData;
} else {
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ??
throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
}
}
}
internal static async Task<string> GetApiDataWithAuthAsync(string url, string token) {
internal static async Task<User> AuthUserPass(string user, string pass, string url) {
var values = new Dictionary<string, string> { { "user", user }, { "pass", pass } };
if (Connectivity.Current.NetworkAccess == NetworkAccess.None)
throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.");
var content = new FormUrlEncodedContent(values);
if (string.IsNullOrEmpty(token))
throw new Exception("Kein APIKEY, bitte zuerst Login durchführen");
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Erstellen eines HttpClient-Objekts
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage =
await client.PostAsync(url, content).ConfigureAwait(false)) {
if (!HttpResponseMessage.IsSuccessStatusCode) {
//throw new Exception("Fehler beim Einloggen " + HttpResponseMessage.Content);
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ??
throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Überprüfen, ob die Anfrage erfolgreich war
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
using (HttpContent HttpContent = HttpResponseMessage.Content) {
// Lesen und Rückgabe der Antwort als String
// Hinzufügen des Bearer-Tokens zum Authorization-Header
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
string responseData = await HttpContent.ReadAsStringAsync();
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(responseData) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
//User userData = System.Text.Json.JsonSerializer.Deserialize<User>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
}
}
}
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.GetAsync(url).ConfigureAwait(false)) {
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
//using (HttpContent HttpContent = HttpResponseMessage.Content) {
// //responseData = await HttpContent.ReadAsStringAsync();
//}
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
return responseData;
} else {
var options = new JsonDocumentOptions {
AllowTrailingCommas = true
};
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
}
}
}
return null;
}
/// <summary>
/// Notiz laden
/// </summary>
internal static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
internal static async Task<User> AuthUserPass(string user, string pass, string url) {
return
new() { Date = File.GetLastWriteTime(filename) };
}
var values = new Dictionary<string, string>
{
{ "user", user },
{ "pass", pass }
};
/// <summary>
/// Stundeneintrag speichern
/// </summary>
internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) {
//Uhrzeiten sollten sinnvolle Werte haben - außer bei Freistellungen, da wäre eigentlich null
if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) {
throw new Exception("Beginn und Ende sind gleich");
}
var content = new FormUrlEncodedContent(values);
if (item.TimeSpanBis < item.TimeSpanVon) {
throw new Exception("Ende ist vor Beginn");
}
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
TimeSpan span = TimeSpan.Zero;
span += item.TimeSpanBis - item.TimeSpanVon;
if (span.Hours > 10) {
//Hier vielleicht eine Abfrage, ob mehr als 10 Stunden gesund sind?
//Das müsste aber das ViewModel machen
}
client.DefaultRequestHeaders.Add("Accept", "application/json");
//Gemeinde ist ein Pflichtfeld
if (item.GemeindeAktiv == null && GlobalVar.Settings.GemeindeAktivSet) {
throw new Exception("Gemeinde nicht gewählt");
}
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.PostAsync(url, content).ConfigureAwait(false)) {
if (!HttpResponseMessage.IsSuccessStatusCode)
{
//throw new Exception("Fehler beim Einloggen " + HttpResponseMessage.Content);
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
var options = new JsonDocumentOptions {
AllowTrailingCommas = true
};
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
//Projekt ist ein Pflichtfeld
if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) {
throw new Exception("Projekt nicht gewählt");
}
// Überprüfen, ob die Anfrage erfolgreich war
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
using (HttpContent HttpContent = HttpResponseMessage.Content) {
// Lesen und Rückgabe der Antwort als String
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) {
throw new Exception("Keine Beschreibung");
}
string responseData = await HttpContent.ReadAsStringAsync();
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
//User userData = System.Text.Json.JsonSerializer.Deserialize<User>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
}
}
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description)) {
item.Description = item.FreistellungAktiv.Name;
}
}
return null;
}
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
/// <summary>
/// Notiz laden
/// </summary>
internal static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
//string json = JsonSerializer.Serialize<DayTime>(item);
string json = JsonConvert.SerializeObject(item);
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
HttpResponseMessage? response = null;
if (isNewItem)
response = await client.PostAsync(url, content);
else
response = await client.PutAsync(url, content);
return
new() {
Date = File.GetLastWriteTime(filename)
};
}
if (!response.IsSuccessStatusCode) {
throw new Exception("Fehler beim Speichern " + response.Content);
}
}
}
/// <summary>
/// Stundeneintrag speichern
/// </summary>
internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) {
/// <summary>
/// Stundeneintrag löschen
/// </summary>
internal static async Task DeleteItemAsync(string url, string token) {
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//Uhrzeiten sollten sinnvolle Werte haben - außer bei Freistellungen, da wäre eigentlich null
if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) {
throw new Exception("Beginn und Ende sind gleich");
}
HttpResponseMessage response = await client.DeleteAsync(url);
if (item.TimeSpanBis < item.TimeSpanVon) {
throw new Exception("Ende ist vor Beginn");
}
TimeSpan span = TimeSpan.Zero;
span += item.TimeSpanBis - item.TimeSpanVon;
if (span.Hours > 10) {
//Hier vielleicht eine Abfrage, ob mehr als 10 Stunden gesund sind?
//Das müsste aber das ViewModel machen
}
//Gemeinde ist ein Pflichtfeld
if (item.GemeindeAktiv == null && GlobalVar.Settings.GemeindeAktivSet) {
throw new Exception("Gemeinde nicht gewählt");
}
//Projekt ist ein Pflichtfeld
if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) {
throw new Exception("Projekt nicht gewählt");
}
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) {
throw new Exception("Keine Beschreibung");
}
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description)) {
item.Description = item.FreistellungAktiv.Name;
}
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//string json = JsonSerializer.Serialize<DayTime>(item);
string json = JsonConvert.SerializeObject(item);
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage? response = null;
if (isNewItem)
response = await client.PostAsync(url, content);
else
response = await client.PutAsync(url, content);
if (!response.IsSuccessStatusCode) {
throw new Exception("Fehler beim Speichern " + response.Content);
}
}
}
/// <summary>
/// Stundeneintrag löschen
/// </summary>
internal static async Task DeleteItemAsync(string url, string token) {
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await client.DeleteAsync(url);
if (!response.IsSuccessStatusCode)
throw new Exception("Fehler beim Löschen " + response.Content);
}
}
}
if (!response.IsSuccessStatusCode)
throw new Exception("Fehler beim Löschen " + response.Content);
}
}
}

View File

@@ -1,26 +1,32 @@
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Models;
internal static class GlobalVar {
public static string ApiKey {
get => Preferences.Default.Get("apiKey", "");
set => Preferences.Default.Set("apiKey", value);
}
public static int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0);
set => Preferences.Default.Set("EmployeeId", value);
}
public static string Name {
get => Preferences.Default.Get("name", "Nicht");
set => Preferences.Default.Set("name", value);
}
public static string Surname {
get => Preferences.Default.Get("surname", "Eingeloggt");
set => Preferences.Default.Set("surname", value);
}
public static string ApiUrl {
get => Preferences.Default.Get("apiUrl", "");
set => Preferences.Default.Set("apiUrl", value);
}
public static Settings Settings { get; set; }
}
public static string ApiKey {
get => Preferences.Default.Get("apiKey", "");
set => Preferences.Default.Set("apiKey", value);
}
public static int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0);
set => Preferences.Default.Set("EmployeeId", value);
}
public static string Name {
get => Preferences.Default.Get("name", "Nicht");
set => Preferences.Default.Set("name", value);
}
public static string Surname {
get => Preferences.Default.Get("surname", "Eingeloggt");
set => Preferences.Default.Set("surname", value);
}
public static string ApiUrl {
get => Preferences.Default.Get("apiUrl", "");
set => Preferences.Default.Set("apiUrl", value);
}
public static Settings Settings { get; set; }
}

View File

@@ -4,83 +4,90 @@ using Newtonsoft.Json;
namespace Jugenddienst_Stunden.Models;
internal static class HoursBase {
/// <summary>
/// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/>
/// </summary>
/// <returns>Entire response</returns>
internal static async Task<BaseResponse> LoadBase(string what) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?" + what, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res;
}
/// <summary>
/// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/>
/// </summary>
/// <returns>Entire response</returns>
internal static async Task<BaseResponse> LoadBase(string what) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?"+what, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res;
}
/// <summary>
/// Einstellungen laden
/// </summary>
/// <returns>Settings only</returns>
internal static async Task<Settings> LoadSettings() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.settings;
}
/// <summary>
/// Einstellungen laden
/// </summary>
/// <returns>Settings only</returns>
internal static async Task<Settings> LoadSettings() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.settings;
}
/// <summary>
/// Daten laden
/// </summary>
/// <returns>Hours only</returns>
internal static async Task<Hours> LoadData() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.hour;
}
/// <summary>
/// Daten laden
/// </summary>
/// <returns>Hours only</returns>
internal static async Task<Hours> LoadData() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.hour;
}
/// <summary>
/// Benutzerdaten laden
/// </summary>
/// <returns>User-Object</returns>
public static async Task<User> LoadUser(string apiKey) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
/// <summary>
/// Benutzerdaten laden
/// </summary>
/// <returns>User-Object</returns>
public static async Task<User> LoadUser(string apiKey) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
/// <summary>
/// Zeiten eines Tages holen
/// </summary>
internal static async Task<List<DayTime>> LoadDay(DateTime date) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"),
GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.daytimes;
}
/// <summary>
/// Zeiten eines Tages holen
/// </summary>
internal static async Task<List<DayTime>> LoadDay(DateTime date) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"), GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.daytimes;
}
/// <summary>
/// Einzelnen Stundeneintrag holen
/// </summary>
internal static async Task<DayTime> LoadEntry(int id) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
return res.daytime;
}
/// <summary>
/// Einzelnen Stundeneintrag holen
/// </summary>
internal static async Task<DayTime> LoadEntry(int id) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
return res.daytime;
}
/// <summary>
/// Eintrag speichern
/// </summary>
internal static async Task<DayTime> SaveEntry(DayTime stunde) {
//, string begin, string end, string freistellung, string bemerkung) {
bool isNew = false;
if (stunde.Id == null)
isNew = true;
await BaseFunc.SaveItemAsync(GlobalVar.ApiUrl, GlobalVar.ApiKey, stunde, isNew);
/// <summary>
/// Eintrag speichern
/// </summary>
internal static async Task<DayTime> SaveEntry(DayTime stunde) { //, string begin, string end, string freistellung, string bemerkung) {
bool isNew = false;
if (stunde.Id == null)
isNew = true;
await BaseFunc.SaveItemAsync(GlobalVar.ApiUrl, GlobalVar.ApiKey, stunde, isNew);
return stunde;
}
return stunde;
}
/// <summary>
/// Eintrag löschen
/// </summary>
internal static async Task DeleteEntry(DayTime stunde) {
await BaseFunc.DeleteItemAsync(GlobalVar.ApiUrl + "/entry/" + stunde.Id, GlobalVar.ApiKey);
}
}
/// <summary>
/// Eintrag löschen
/// </summary>
internal static async Task DeleteEntry(DayTime stunde) {
await BaseFunc.DeleteItemAsync(GlobalVar.ApiUrl + "/entry/" + stunde.Id, GlobalVar.ApiKey);
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jugenddienst_Stunden.Models;
internal sealed class JsonFlexibleInt32Converter : JsonConverter<int> {
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
switch (reader.TokenType) {
case JsonTokenType.Number:
if (reader.TryGetInt32(out var n)) return n;
// Fallback via double to cover edge cases
var d = reader.GetDouble();
return (int)d;
case JsonTokenType.String:
var s = reader.GetString();
if (string.IsNullOrWhiteSpace(s)) return 0;
s = s.Trim();
// Some APIs embed id like "141|..." -> take leading numeric part
int i = 0;
int sign = 1;
int idx = 0;
if (s.StartsWith("-")) { sign = -1; idx = 1; }
for (; idx < s.Length; idx++) {
char c = s[idx];
if (c < '0' || c > '9') break;
i = i * 10 + (c - '0');
}
if (idx > 0 && (idx > 1 || sign == 1)) return i * sign;
if (int.TryParse(s, out var parsed)) return parsed;
if (long.TryParse(s, out var l)) return (int)l;
throw new JsonException($"Cannot convert string '{s}' to Int32.");
case JsonTokenType.Null:
return 0;
default:
throw new JsonException($"Token {reader.TokenType} is not valid for Int32.");
}
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
=> writer.WriteNumberValue(value);
}
internal sealed class JsonFlexibleNullableInt32Converter : JsonConverter<int?> {
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
if (reader.TokenType == JsonTokenType.Null) return null;
// Reuse non-nullable converter
var conv = new JsonFlexibleInt32Converter();
return conv.Read(ref reader, typeof(int), options);
}
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) {
if (value.HasValue) writer.WriteNumberValue(value.Value);
else writer.WriteNullValue();
}
}

View File

@@ -11,37 +11,44 @@ namespace Jugenddienst_Stunden.Models {
return typeof(T).IsAssignableFrom(objectType);
}
public override bool CanWrite { get { return false; } }
public override bool CanWrite {
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer) {
var contract = serializer.ContractResolver.ResolveContract(objectType);
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract || contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) {
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType, reader.Path));
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract ||
contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) {
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType,
reader.Path));
}
switch (reader.SkipComments().TokenType) {
case JsonToken.StartArray: {
int count = 0;
while (reader.Read()) {
switch (reader.TokenType) {
case JsonToken.Comment:
break;
case JsonToken.EndArray:
return existingValue;
default: {
count++;
if (count > 1)
throw new JsonSerializationException(string.Format("Too many objects at path {0}.", reader.Path));
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
}
break;
int count = 0;
while (reader.Read()) {
switch (reader.TokenType) {
case JsonToken.Comment:
break;
case JsonToken.EndArray:
return existingValue;
default: {
count++;
if (count > 1)
throw new JsonSerializationException(string.Format("Too many objects at path {0}.",
reader.Path));
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
}
break;
}
// Should not come here.
throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
}
// Should not come here.
throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
}
case JsonToken.Null:
return null;
@@ -67,4 +74,4 @@ namespace Jugenddienst_Stunden.Models {
return reader;
}
}
}
}

View File

@@ -1,49 +1,50 @@
namespace Jugenddienst_Stunden.Models;
internal class Note {
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
public static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
return
new() {
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
return
new() {
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
public static IEnumerable<Note> LoadAll() {
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
public static IEnumerable<Note> LoadAll() {
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
public Note() {
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
}
public Note() {
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
}

View File

@@ -2,20 +2,20 @@
using Newtonsoft.Json;
namespace Jugenddienst_Stunden.Models;
internal class Operator {
public string? id;
public string? name;
public string? surname;
public string? email;
public string? password;
public string? lang;
public string? admin;
public string? aktiv;
public string? department;
public string? department_name;
public string? num;
public string? year;
public string? id;
public string? name;
public string? surname;
public string? email;
public string? password;
public string? lang;
public string? admin;
public string? aktiv;
public string? department;
public string? department_name;
public string? num;
public string? year;
public event EventHandler<string>? AlertEvent;
}
public event EventHandler<string>? AlertEvent;
}

View File

@@ -10,19 +10,23 @@ internal class TokenData {
public string Operator_id { get; set; }
public TokenData(string ak) {
if (string.IsNullOrEmpty(ak)) {
throw new ArgumentException("API key cannot be null or empty", nameof(ak));
}
string dat = Encoding.UTF8.GetString(Convert.FromBase64String(ak));
string[] parts = dat.Split('|');
if (parts.Length < 3) {
throw new FormatException("API key format is invalid");
}
if (string.IsNullOrEmpty(ak)) {
throw new ArgumentException("API key cannot be null or empty", nameof(ak));
}
Token = dat.Split('|')[1]; ;
Url = dat.Split('|')[2]; ;
Operator_id = dat.Split('|')[0]; ;
string dat = Encoding.UTF8.GetString(Convert.FromBase64String(ak));
string[] parts = dat.Split('|');
if (parts.Length < 3) {
throw new FormatException("API key format is invalid");
}
Token = dat.Split('|')[1];
;
Url = dat.Split('|')[2];
;
Operator_id = dat.Split('|')[0];
;
ApiKey = ak;
}
}
}

View File

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/appicon"
android:supportsRtl="true"
android:label="Stunden"
/>
<!-- android:usesCleartextTraffic="true" -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
android:allowBackup="true"
android:icon="@mipmap/appicon"
android:supportsRtl="true"
android:label="Stunden"
/>
<!-- android:usesCleartextTraffic="true" -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
</manifest>

View File

@@ -2,7 +2,10 @@
using Android.Content.PM;
using Android.OS;
namespace Jugenddienst_Stunden;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
namespace Jugenddienst_Stunden;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity {
}
}

View File

@@ -1,7 +1,7 @@
using Android.App;
using Android.Runtime;
namespace Jugenddienst_Stunden;
namespace Jugenddienst_Stunden;
#if DEBUG
[Application(UsesCleartextTraffic = true)]
#else
@@ -13,4 +13,4 @@ public class MainApplication : MauiApplication {
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}

View File

@@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -1,15 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="Jugenddienst Stunden.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8"
xmlns="http://tizen.org/ns/packages">
<profile name="common"/>
<ui-application appid="maui-application-id-placeholder" exec="Jugenddienst Stunden.dll" multiple="false"
nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true"/>
</ui-application>
<shortcut-list/>
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies/>
<provides-appdefined-privileges/>
</manifest>

View File

@@ -5,4 +5,4 @@
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:Jugenddienst_Stunden.WinUI">
</maui:MauiWinUIApplication>
</maui:MauiWinUIApplication>

View File

@@ -19,9 +19,5 @@ namespace Jugenddienst_Stunden.WinUI {
/// </summary>
/// <returns></returns>
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
}

View File

@@ -1,70 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap com desktop">
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap com desktop">
<Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0" />
<Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0"/>
<mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4"
PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>Daniel Pichler</PublisherDisplayName>
<Logo>Resources\Windows\$placeholder$.png</Logo>
</Properties>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>Daniel Pichler</PublisherDisplayName>
<Logo>Resources\Windows\$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="Resources\Windows\$placeholder$.png"
Square44x44Logo="Resources\Windows\$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="Resources\Windows\Small\$placeholder$.png" Wide310x150Logo="Resources\Windows\Wide\$placeholder$.png" Square310x310Logo="Resources\Windows\$placeholder$.png" ShortName="Stunden"/>
<uap:SplashScreen Image="Resources\Windows\Splash\$placeholder$.png" BackgroundColor="#F7931D"/>
</uap:VisualElements>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="Resources\Windows\$placeholder$.png"
Square44x44Logo="Resources\Windows\$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="Resources\Windows\Small\$placeholder$.png"
Wide310x150Logo="Resources\Windows\Wide\$placeholder$.png"
Square310x310Logo="Resources\Windows\$placeholder$.png" ShortName="Stunden"/>
<uap:SplashScreen Image="Resources\Windows\Splash\$placeholder$.png" BackgroundColor="#F7931D"/>
</uap:VisualElements>
<Extensions>
<Extensions>
<!-- Specify which CLSID to activate when notification is clicked -->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="6e919706-2634-4d97-a93c-2213b2acc334" />
</desktop:Extension>
<!-- Specify which CLSID to activate when notification is clicked -->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="6e919706-2634-4d97-a93c-2213b2acc334"/>
</desktop:Extension>
<!-- Register COM CLSID -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Jugenddienst Stunden.exe" DisplayName="$targetnametoken$" Arguments="----AppNotificationActivated:">
<!-- Example path to executable: CommunityToolkit.Maui.Sample\CommunityToolkit.Maui.Sample.exe -->
<com:Class Id="6e919706-2634-4d97-a93c-2213b2acc334" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
<!-- Register COM CLSID -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Jugenddienst Stunden.exe" DisplayName="$targetnametoken$"
Arguments="----AppNotificationActivated:">
<!-- Example path to executable: CommunityToolkit.Maui.Sample\CommunityToolkit.Maui.Sample.exe -->
<com:Class Id="6e919706-2634-4d97-a93c-2213b2acc334"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Extensions>
</Application>
</Applications>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<Capability Name="internetClient"/>
<DeviceCapability Name="webcam"/>
</Capabilities>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
<Capability Name="internetClient"/>
<DeviceCapability Name="webcam"/>
</Capabilities>
</Package>

View File

@@ -1,15 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Jugenddienst Stunden.WinUI.app"/>
<assemblyIdentity version="1.0.0.0" name="Jugenddienst Stunden.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor
</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -1,34 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCameraUsageDescription</key>
<string>This app uses barcode scanning to...</string>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<key>CFBundleIdentifier</key>
<string></string>
</dict>
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCameraUsageDescription</key>
<string>This app uses barcode scanning to...</string>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<key>CFBundleIdentifier</key>
<string></string>
</dict>
</plist>

View File

@@ -1,10 +1,9 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Dieser Code wurde von einem Tool generiert.
// Laufzeitversion:4.0.30319.42000
// This code was generated by a tool.
//
// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn
// der Code erneut generiert wird.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
@@ -13,12 +12,12 @@ namespace Jugenddienst_Stunden.Properties {
/// <summary>
/// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw.
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert
// -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert.
// Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen
// mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu.
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
@@ -33,7 +32,7 @@ namespace Jugenddienst_Stunden.Properties {
}
/// <summary>
/// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird.
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
@@ -47,8 +46,8 @@ namespace Jugenddienst_Stunden.Properties {
}
/// <summary>
/// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle
/// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden.
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {

View File

@@ -1,101 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 1.3
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
</root>

View File

@@ -1,4 +1,5 @@
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
@@ -8,12 +9,72 @@ namespace Jugenddienst_Stunden.Repositories;
/// Standard-Repository, das die bestehende API-/Model-Logik kapselt.
/// </summary>
internal class HoursRepository : IHoursRepository {
public async Task<BaseResponse> LoadBase(string query) => await HoursBase.LoadBase(query);
public async Task<Settings> LoadSettings() => await HoursBase.LoadSettings();
public async Task<Hours> LoadData() => await HoursBase.LoadData();
public async Task<User> LoadUser(string apiKey) => await HoursBase.LoadUser(apiKey);
public async Task<List<DayTime>> LoadDay(DateTime date) => await HoursBase.LoadDay(date);
public async Task<DayTime> LoadEntry(int id) => await HoursBase.LoadEntry(id);
public async Task<DayTime> SaveEntry(DayTime stunde) => await HoursBase.SaveEntry(stunde);
public async Task DeleteEntry(DayTime stunde) => await HoursBase.DeleteEntry(stunde);
}
private readonly IApiClient _api;
public HoursRepository(IApiClient api) {
_api = api;
}
public async Task<BaseResponse> LoadBase(string query) {
// Der bestehende Code übergab eine Query ohne führendes '?'
var dict = QueryToDictionary(query);
var res= await _api.GetAsync<BaseResponse>("", dict).ConfigureAwait(false);
return res;
}
public async Task<Settings> LoadSettings() {
var res = await _api.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["settings"] = "1" })
.ConfigureAwait(false);
return res.settings;
}
public async Task<Hours> LoadData() {
var res = await _api.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["hours"] = "1" })
.ConfigureAwait(false);
return res.hour;
}
public Task<User> LoadUser(string apiKey) {
// Für die erste Iteration bleibt das Token global; der Endpoint ohne Query liefert user
return _api.GetAsync<BaseResponse>("", null).ContinueWith(t => t.Result.user);
}
public async Task<List<DayTime>> LoadDay(DateTime date) {
var res = await _api
.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["date"] = date.ToString("yyyy-MM-dd") })
.ConfigureAwait(false);
return res.daytimes ?? new List<DayTime>();
}
public async Task<DayTime> LoadEntry(int id) {
var res = await _api.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["id"] = id.ToString() })
.ConfigureAwait(false);
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
return res.daytime;
}
public async Task<DayTime> SaveEntry(DayTime stunde) {
bool isNew = stunde.Id is null;
var method = isNew ? HttpMethod.Post : HttpMethod.Put;
await _api.SendAsync<object>(method, "", stunde).ConfigureAwait(false);
return stunde;
}
public Task DeleteEntry(DayTime stunde)
=> _api.DeleteAsync($"/entry/{stunde.Id}");
private static Dictionary<string, string?> QueryToDictionary(string query) {
var dict = new Dictionary<string, string?>();
if (string.IsNullOrWhiteSpace(query)) return dict;
var q = query.TrimStart('?');
foreach (var part in q.Split('&', StringSplitOptions.RemoveEmptyEntries)) {
var kv = part.Split('=', 2);
var key = Uri.UnescapeDataString(kv[0]);
var val = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : null;
dict[key] = val;
}
return dict;
}
}

View File

@@ -1,5 +1,7 @@
<?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="100%" height="100%" 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;">
<svg width="100%" height="100%" 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;">
<rect x="0" y="0" width="456" height="456" style="fill:white;"/>
</svg>

Before

Width:  |  Height:  |  Size: 521 B

After

Width:  |  Height:  |  Size: 531 B

View File

@@ -1,41 +1,54 @@
<?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="100%" height="100%" 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;">
<svg width="100%" height="100%" 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;">
<g id="JD" transform="matrix(0.96485,0,0,0.96485,104.499,104.499)">
<g transform="matrix(12.0607,0,0,12.0607,101.169,256)">
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0,12.0607,12.0607,0,87.6884,17.5193)">
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,32.4324,105.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,109.437,86.7078)">
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
style="fill-rule:nonzero;"/>
</g>
</g>
<g id="Uhr" transform="matrix(0.0220493,0,0,0.0220493,267.879,222.51)">
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1638.49,981.86C1405.19,981.86 1216.06,792.734 1216.06,559.433C1216.06,326.132 1405.19,137.006 1638.49,137.006C1871.79,137.006 2060.92,326.132 2060.92,559.433C2060.92,792.734 1871.79,981.86 1638.49,981.86ZM1638.49,70.646C1368.54,70.646 1149.7,289.481 1149.7,559.433C1149.7,829.385 1368.54,1048.22 1638.49,1048.22C1908.44,1048.22 2127.28,829.385 2127.28,559.433C2127.28,289.481 1908.44,70.646 1638.49,70.646Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1638.49,981.86C1405.19,981.86 1216.06,792.734 1216.06,559.433C1216.06,326.132 1405.19,137.006 1638.49,137.006C1871.79,137.006 2060.92,326.132 2060.92,559.433C2060.92,792.734 1871.79,981.86 1638.49,981.86ZM1638.49,70.646C1368.54,70.646 1149.7,289.481 1149.7,559.433C1149.7,829.385 1368.54,1048.22 1638.49,1048.22C1908.44,1048.22 2127.28,829.385 2127.28,559.433C2127.28,289.481 1908.44,70.646 1638.49,70.646Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.14211,-0.451661,0.451661,4.14211,-6813.16,744.211)">
<path d="M1677.2,559.433C1677.2,580.825 1659.85,598.166 1638.45,598.145C1617.12,598.124 1599.75,580.168 1599.78,558.84C1599.88,474.921 1621.18,308.102 1632.14,254.433C1633.54,247.532 1643.41,247.537 1644.81,254.44C1655.78,308.457 1677.2,476.326 1677.2,559.433Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1677.2,559.433C1677.2,580.825 1659.85,598.166 1638.45,598.145C1617.12,598.124 1599.75,580.168 1599.78,558.84C1599.88,474.921 1621.18,308.102 1632.14,254.433C1633.54,247.532 1643.41,247.537 1644.81,254.44C1655.78,308.457 1677.2,476.326 1677.2,559.433Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(-4.09852,-0.750473,-0.750473,4.09852,7333.98,1287.55)">
<path d="M1595.39,590.238C1532.86,586.449 1465.21,572.938 1428.29,565.401C1422.75,564.267 1422.76,556.353 1428.29,555.229C1463.02,548.172 1526.22,535.741 1585.66,531.077C1585.22,540.595 1584.97,549.88 1584.96,558.834C1584.95,570.561 1587.38,581.586 1595.39,590.238Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1595.39,590.238C1532.86,586.449 1465.21,572.938 1428.29,565.401C1422.75,564.267 1422.76,556.353 1428.29,555.229C1463.02,548.172 1526.22,535.741 1585.66,531.077C1585.22,540.595 1584.97,549.88 1584.96,558.834C1584.95,570.561 1587.38,581.586 1595.39,590.238Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1656.19,176.808C1656.19,186.583 1648.27,194.507 1638.49,194.507C1628.72,194.507 1620.79,186.583 1620.79,176.808C1620.79,167.033 1628.72,159.108 1638.49,159.108C1648.27,159.108 1656.19,167.033 1656.19,176.808Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1656.19,176.808C1656.19,186.583 1648.27,194.507 1638.49,194.507C1628.72,194.507 1620.79,186.583 1620.79,176.808C1620.79,167.033 1628.72,159.108 1638.49,159.108C1648.27,159.108 1656.19,167.033 1656.19,176.808Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1255.87,541.734C1265.64,541.734 1273.57,549.658 1273.57,559.433C1273.57,569.208 1265.64,577.132 1255.87,577.132C1246.09,577.132 1238.17,569.208 1238.17,559.433C1238.17,549.658 1246.09,541.734 1255.87,541.734Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1255.87,541.734C1265.64,541.734 1273.57,549.658 1273.57,559.433C1273.57,569.208 1265.64,577.132 1255.87,577.132C1246.09,577.132 1238.17,569.208 1238.17,559.433C1238.17,549.658 1246.09,541.734 1255.87,541.734Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1620.79,942.059C1620.79,932.283 1628.72,924.359 1638.49,924.359C1648.27,924.359 1656.19,932.283 1656.19,942.059C1656.19,951.834 1648.27,959.758 1638.49,959.758C1628.72,959.758 1620.79,951.834 1620.79,942.059Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1620.79,942.059C1620.79,932.283 1628.72,924.359 1638.49,924.359C1648.27,924.359 1656.19,932.283 1656.19,942.059C1656.19,951.834 1648.27,959.758 1638.49,959.758C1628.72,959.758 1620.79,951.834 1620.79,942.059Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M2021.12,577.132C2011.34,577.132 2003.42,569.208 2003.42,559.433C2003.42,549.658 2011.34,541.734 2021.12,541.734C2030.89,541.734 2038.82,549.658 2038.82,559.433C2038.82,569.208 2030.89,577.132 2021.12,577.132Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M2021.12,577.132C2011.34,577.132 2003.42,569.208 2003.42,559.433C2003.42,549.658 2011.34,541.734 2021.12,541.734C2030.89,541.734 2038.82,549.658 2038.82,559.433C2038.82,569.208 2030.89,577.132 2021.12,577.132Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,21 +1,28 @@
<?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="100%" height="100%" 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;">
<svg width="100%" height="100%" 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;">
<g>
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)">
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)">
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:white;fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)">
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,21 +1,28 @@
<?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="100%" height="100%" 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;">
<svg width="100%" height="100%" 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;">
<g>
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)">
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)">
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:white;fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)">
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
@@ -30,18 +31,18 @@
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}" />
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" />
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}" />
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}" />
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}" />
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}" />
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}" />
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}" />
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}" />
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}" />
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}" />
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}" />
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}" />
</ResourceDictionary>

View File

@@ -1,54 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ToolbarItem">
<Style.Triggers>
<Trigger TargetType="ToolbarItem" Property="VisualElement.BackgroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}">
<Trigger TargetType="ToolbarItem" Property="VisualElement.BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}">
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
<Setter Property="IndicatorColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="SelectedIndicatorColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}" />
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
<Setter Property="Stroke"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="Color"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="14,10" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
@@ -59,15 +70,16 @@
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="Color"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -76,19 +88,21 @@
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -97,20 +111,23 @@
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -119,20 +136,23 @@
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -142,18 +162,20 @@
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="BorderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="BorderColor" Value="Transparent" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
@@ -170,7 +192,8 @@
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
@@ -180,7 +203,8 @@
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -189,44 +213,53 @@
</Style>
<Style TargetType="Span">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="SeparatorColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -235,14 +268,16 @@
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ProgressColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ProgressColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -251,19 +286,21 @@
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -272,26 +309,30 @@
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="RefreshColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -300,7 +341,8 @@
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
@@ -311,8 +353,10 @@
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -328,18 +372,24 @@
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumTrackColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MinimumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="MaximumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -348,11 +398,13 @@
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
@@ -360,19 +412,24 @@
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -381,19 +438,21 @@
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -402,34 +461,51 @@
</Style>
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Padding" Value="0" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.TabBarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="BarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>
</ResourceDictionary>

View File

@@ -1,38 +1,58 @@
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Types;
using Jugenddienst_Stunden.Validators;
namespace Jugenddienst_Stunden.Services;
internal class HoursService : IHoursService {
private readonly IHoursRepository _repo;
private readonly IHoursRepository _repo;
private readonly IHoursValidator _validator;
public HoursService(IHoursRepository repo) {
_repo = repo;
}
public HoursService(IHoursRepository repo, IHoursValidator validator) {
_repo = repo;
_validator = validator;
}
public async Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate) {
string q = $"hours&year={monthDate:yyyy}&month={monthDate:MM}";
public async Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate) {
string q = $"hours=&year={monthDate:yyyy}&month={monthDate:MM}";
var baseRes = await _repo.LoadBase(q);
return (baseRes.hour, baseRes.settings);
// Fallbacks, da einige Endpoints 'settings' nicht mitsenden bzw. 'hour' auslassen können
var settings = baseRes.settings ?? await _repo.LoadSettings();
var hours = baseRes.hour ?? new Hours {
daytime = new List<DayTime>(),
Nominal_day_api = new List<NominalDay>(),
Nominal_week_api = new List<NominalWeek>(),
zeit_total_daily_api = new List<TimeDay>(),
Projekte = new System.Collections.ObjectModel.Collection<Projekt>(),
Gemeinden = new System.Collections.ObjectModel.Collection<Gemeinde>(),
Freistellungen = new System.Collections.ObjectModel.Collection<Freistellung>()
};
return (hours, settings);
}
public async Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date) {
string q = $"date={date:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return (baseRes.daytimes ?? new List<DayTime>(), baseRes.settings);
}
public async Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date) {
string q = $"date={date:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return (baseRes.daytimes ?? new List<DayTime>(), baseRes.settings);
}
public async Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to) {
string q = $"date={from:yyyy-MM-dd}&tilldate={to:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return baseRes.daytimes ?? new List<DayTime>();
}
public async Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to) {
string q = $"date={from:yyyy-MM-dd}&tilldate={to:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return baseRes.daytimes ?? new List<DayTime>();
}
public async Task<Settings> GetSettingsAsync() => await _repo.LoadSettings();
public async Task<Settings> GetSettingsAsync() => await _repo.LoadSettings();
public async Task<DayTime> GetEntryAsync(int id) => await _repo.LoadEntry(id);
public async Task<DayTime> GetEntryAsync(int id) => await _repo.LoadEntry(id);
public async Task<DayTime> SaveEntryAsync(DayTime stunde) => await _repo.SaveEntry(stunde);
public async Task<DayTime> SaveEntryAsync(DayTime stunde) {
var settings = await _repo.LoadSettings();
_validator.Validate(stunde, settings);
return await _repo.SaveEntry(stunde);
}
public async Task DeleteEntryAsync(DayTime stunde) => await _repo.DeleteEntry(stunde);
}
public async Task DeleteEntryAsync(DayTime stunde) => await _repo.DeleteEntry(stunde);
}

View File

@@ -1,36 +1,37 @@
using Jugenddienst_Stunden.Models;
namespace Jugenddienst_Stunden.Types;
internal class BaseResponse {
public Settings settings { get; set; }
public Settings settings { get; set; }
/// <summary>
/// Monatsübersicht
/// </summary>
public Hours hour { get; set; }
/// <summary>
/// Monatsübersicht
/// </summary>
public Hours hour { get; set; }
/// <summary>
/// Stundenliste ... für die Katz?
/// </summary>
public List<Hours> hours { get; set; }
/// <summary>
/// Stundenliste ... für die Katz?
/// </summary>
public List<Hours> hours { get; set; }
/// <summary>
/// Liste der Stundeneinträge
/// </summary>
public List<DayTime> daytimes { get; set; }
/// <summary>
/// Liste der Stundeneinträge
/// </summary>
public List<DayTime> daytimes { get; set; }
/// <summary>
/// Einzelner Stundeneintrag
/// </summary>
public DayTime daytime { get; set; }
/// <summary>
/// Einzelner Stundeneintrag
/// </summary>
public DayTime daytime { get; set; }
/// <summary>
/// Auch irgendwie doppelt ...
/// </summary>
public Operator operatorVar { get; set; }
public User user { get; set; }
/// <summary>
/// Auch irgendwie doppelt ...
/// </summary>
public Operator operatorVar { get; set; }
public int error { get; set; }
public string message { get; set; }
}
public User user { get; set; }
public int error { get; set; }
public string message { get; set; }
}

View File

@@ -2,95 +2,95 @@
using System.Collections.ObjectModel;
namespace Jugenddienst_Stunden.Types;
/// <summary>
/// Represents a day time entry for an employee.
/// </summary>
public class DayTime {
/// <summary>
/// ID des Stundeneintrages
/// </summary>
public int? Id { get; set; }
/// <summary>
/// ID des Stundeneintrages
/// </summary>
public int? Id { get; set; }
/// <summary>
/// Mitarbeiter-ID
/// </summary>
public int EmployeeId { get; set; }
/// <summary>
/// Mitarbeiter-ID
/// </summary>
public int EmployeeId { get; set; }
/// <summary>
/// Der betreffende Tag
/// </summary>
public DateTime Day { get; set; }
/// <summary>
/// Der betreffende Tag
/// </summary>
public DateTime Day { get; set; }
/// <summary>
/// Der Wochentag
/// </summary>
public int Wday { get; set; }
/// <summary>
/// Der Wochentag
/// </summary>
public int Wday { get; set; }
/// <summary>
/// Arbeitsbeginn
/// </summary>
public TimeOnly Begin { get; set; }
/// <summary>
/// Arbeitsbeginn
/// </summary>
public TimeOnly Begin { get; set; }
/// <summary>
/// Arbeitsende
/// </summary>
public TimeOnly End { get; set; }
/// <summary>
/// Arbeitsende
/// </summary>
public TimeOnly End { get; set; }
/// <summary>
/// Beschreibung der Tätigkeit
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Beschreibung der Tätigkeit
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Freistellung
/// </summary>
public string? Free { get; set; }
/// <summary>
/// Freistellung
/// </summary>
public string? Free { get; set; }
/// <summary>
/// Freistellung genehmigt?
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// Freistellung genehmigt?
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// Das gewählte Projekt
/// </summary>
public int? Projekt { get; set; }
/// <summary>
/// Das gewählte Projekt
/// </summary>
public int? Projekt { get; set; }
/// <summary>
/// Die gewählte Gemeinde
/// </summary>
public int? Gemeinde { get; set; }
/// <summary>
/// Die gewählte Gemeinde
/// </summary>
public int? Gemeinde { get; set; }
/// <summary>
/// Nachtstunden
/// </summary>
public TimeOnly Night { get; set; }
/// <summary>
/// Nachtstunden
/// </summary>
public TimeOnly Night { get; set; }
/// <summary>
/// Summe Arbeitszeit (inklusive Nachstunden mit Faktor)
/// </summary>
public Dictionary<string, TimeOnly> Total { get; set; }
/// <summary>
/// Summe Arbeitszeit (inklusive Nachstunden mit Faktor)
/// </summary>
public Dictionary<string, TimeOnly> Total { get; set; }
public TimeOnly End_print { get; set; }
public TimeSpan TimeSpanVon { get; set; }
public TimeSpan TimeSpanBis { get; set; }
public TimeOnly End_print { get; set; }
public TimeSpan TimeSpanVon { get; set; }
public TimeSpan TimeSpanBis { get; set; }
/// <summary>
/// Gets the active Gemeinde based on the gemeinde ID.
/// </summary>
public Gemeinde? GemeindeAktiv { get; set; }
/// <summary>
/// Gets the active Gemeinde based on the gemeinde ID.
/// </summary>
public Gemeinde? GemeindeAktiv { get; set; }
/// <summary>
/// Gets the active Projekt based on the projekt ID.
/// </summary>
public Projekt? ProjektAktiv { get; set; }
/// <summary>
/// Gets the active Projekt based on the projekt ID.
/// </summary>
public Projekt? ProjektAktiv { get; set; }
/// <summary>
/// Gets the active Freistellung based on the Freistellung ID
/// </summary>
public Freistellung? FreistellungAktiv { get; set; }
/// <summary>
/// Gets the active Freistellung based on the Freistellung ID
/// </summary>
public Freistellung? FreistellungAktiv { get; set; }
public int TimeTable { get; set; }
}
public int TimeTable { get; set; }
}

View File

@@ -1,8 +1,9 @@
namespace Jugenddienst_Stunden.Types;
/// <summary>
/// Freistellungen: Urlaub, Zeitausgleich, Krankheit, ...
/// </summary>
public class Freistellung {
public string? Id { get; set; }
public string? Id { get; set; }
public string? Name { get; set; }
}
}

View File

@@ -13,4 +13,4 @@ public class Gemeinde {
/// Name der Gemeinde.
/// </summary>
public string? Name { get; set; }
}
}

View File

@@ -1,43 +1,45 @@

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace Jugenddienst_Stunden.Types;
internal partial class Hours : ObservableObject {
public partial class Hours : ObservableObject {
public double? Zeit;
public double? Nominal;
//public Dictionary<DateOnly,NominalDay> nominal_day_api;
public List<NominalDay>? Nominal_day_api;
//public Dictionary<int,NominalWeek> nominal_week_api;
public List<NominalWeek>? Nominal_week_api;
//public List<string> time_line;
public double? Zeit_total;
//https://stackoverflow.com/questions/29449641/deserialize-json-when-a-value-can-be-an-object-or-an-empty-array/29450279#29450279
//[JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Hours>))]
//public Dictionary<int,decimal> zeit_total_daily;
//public List<string> time_line;
public double? Zeit_total;
public List<TimeDay> zeit_total_daily_api;
//https://stackoverflow.com/questions/29449641/deserialize-json-when-a-value-can-be-an-object-or-an-empty-array/29450279#29450279
//[JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Hours>))]
//public Dictionary<int,decimal> zeit_total_daily;
public List<TimeDay> zeit_total_daily_api;
public List<DayTime>? daytime;
//public List<string> wochensumme;
//public List<string> wochensumme;
[ObservableProperty]
public double overtime_month;
[ObservableProperty] public double overtime_month;
[ObservableProperty]
public double overtime;
//public List<string> overtime_day;
[ObservableProperty] public double overtime;
//public List<string> overtime_day;
[ObservableProperty]
public double zeitausgleich;
[ObservableProperty] public double zeitausgleich;
public double zeitausgleich_month;
public double holiday;
public double krankheit;
public double weiterbildung;
public double bereitschaft;
public double bereitschaft_month;
//public Operator operator_api;
public DateTime Today;
public DateTime Date;
@@ -47,5 +49,4 @@ internal partial class Hours : ObservableObject {
public Collection<Gemeinde> Gemeinden { get; set; }
public Collection<Freistellung> Freistellungen { get; set; }
public int EmployeeId { get; set; }
}
}

View File

@@ -1,7 +1,8 @@
namespace Jugenddienst_Stunden.Types;
internal class NominalDay {
public class NominalDay {
public int day_number;
public int month_number;
public double hours;
public DateOnly date;
}
}

View File

@@ -1,6 +1,6 @@
namespace Jugenddienst_Stunden.Types;
internal class NominalWeek {
public int Week_number;
public double Hours;
}
public class NominalWeek {
public int Week_number;
public double Hours;
}

View File

@@ -13,4 +13,4 @@ public class Projekt {
/// Holt oder setzt den Namen des Projekts.
/// </summary>
public string Name { get; set; }
}
}

View File

@@ -4,35 +4,35 @@
/// Einstellungen
/// </summary>
public class Settings {
/// <summary>
/// Sind Projekte aktiv?
/// </summary>
public bool ProjektAktivSet { get; set; }
/// <summary>
/// Sind Projekte aktiv?
/// </summary>
public bool ProjektAktivSet { get; set; }
/// <summary>
/// Sind Gemeinden aktiv?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Sind Gemeinden aktiv?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Liste der Projekte
/// </summary>
public List<Projekt>? Projekte { get; set; }
/// <summary>
/// Liste der Projekte
/// </summary>
public List<Projekt>? Projekte { get; set; }
/// <summary>
/// Liste der Gemeinden
/// </summary>
public List<Gemeinde>? Gemeinden { get; set; }
/// <summary>
/// Liste der Gemeinden
/// </summary>
public List<Gemeinde>? Gemeinden { get; set; }
/// <summary>
/// Liste der Freistellungen
/// </summary>
public List<Freistellung>? Freistellungen { get; set; }
/// <summary>
/// Liste der Freistellungen
/// </summary>
public List<Freistellung>? Freistellungen { get; set; }
public List<Sollstunden> Nominal { get; set; }
public List<Sollstunden>? Nominal { get; set; }
/// <summary>
/// Version der API
/// </summary>
public string Version { get; set; }
}
/// <summary>
/// Version der API
/// </summary>
public string? Version { get; set; }
}

View File

@@ -1,7 +1,7 @@
namespace Jugenddienst_Stunden.Types;
public class Sollstunden {
public int Timetable { get; set; }
public int Wochentag { get; set; }
public double Zeit { get; set; }
}
public int Timetable { get; set; }
public int Wochentag { get; set; }
public double Zeit { get; set; }
}

View File

@@ -3,7 +3,7 @@
/// <summary>
/// Summe der geleisteten Stunden.
/// </summary>
internal struct TimeDay {
public int Day { get; set; }
public double Hours { get; set; }
}
public struct TimeDay {
public int Day { get; set; }
public double Hours { get; set; }
}

View File

@@ -1,6 +1,6 @@
namespace Jugenddienst_Stunden.Types;
internal class Timetable {
public List<TimetableEntry> timetable;
public decimal wochensumme;
}
public List<TimetableEntry> timetable;
public decimal wochensumme;
}

View File

@@ -1,7 +1,7 @@
namespace Jugenddienst_Stunden.Types;
internal class TimetableEntry {
public List<TimeOnly>? Von;
public List<TimeOnly>? Bis;
public decimal Summe { get; set; }
}
public List<TimeOnly>? Von;
public List<TimeOnly>? Bis;
public decimal Summe { get; set; }
}

View File

@@ -1,7 +1,8 @@
namespace Jugenddienst_Stunden.Types;
internal class User {
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string Token { get; set; }
}
}

View File

@@ -0,0 +1,31 @@
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Validators;
internal interface IHoursValidator {
void Validate(DayTime item, Settings settings);
}
internal sealed class HoursValidator : IHoursValidator {
public void Validate(DayTime item, Settings settings) {
if (item.FreistellungAktiv is null && item.TimeSpanVon == item.TimeSpanBis)
throw new ValidationException("Beginn und Ende sind gleich");
if (item.TimeSpanBis < item.TimeSpanVon)
throw new ValidationException("Ende ist vor Beginn");
if (settings.GemeindeAktivSet && item.GemeindeAktiv is null)
throw new ValidationException("Gemeinde nicht gewählt");
if (settings.ProjektAktivSet && item.ProjektAktiv is null)
throw new ValidationException("Projekt nicht gewählt");
if (string.IsNullOrWhiteSpace(item.Description)) {
if (item.FreistellungAktiv?.Name is string name && !string.IsNullOrWhiteSpace(name))
item.Description = name;
else
throw new ValidationException("Keine Beschreibung");
}
}
}

View File

@@ -4,28 +4,30 @@
/// Die Loginseite
/// </summary>
public class LoginViewModel {
/// <summary>
/// Name der Anwendung
/// </summary>
public string AppTitle => AppInfo.Name;
/// <summary>
/// Name der Anwendung
/// </summary>
public string AppTitle => AppInfo.Name;
/// <summary>
/// Programmversion
/// </summary>
public string Version => AppInfo.VersionString;
/// <summary>
/// Programmversion
/// </summary>
public string Version => AppInfo.VersionString;
/// <summary>
/// Kurze Mitteilung für den Anwender
/// </summary>
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// <summary>
/// Kurze Mitteilung für den Anwender
/// </summary>
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// <summary>
/// Genutzer Server für die API
/// </summary>
public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
/// <summary>
/// Genutzer Server für die API
/// </summary>
public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "")
.Replace("https://", "").Replace("http://", "");
/// <summary>
/// Titel der Seite - im Moment der aktuelle Anwender
/// </summary>
public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
}
/// <summary>
/// Titel der Seite - im Moment der aktuelle Anwender
/// </summary>
public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " +
Preferences.Default.Get("surname", "eingeloggt");
}

View File

@@ -2,9 +2,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input;
namespace Jugenddienst_Stunden.ViewModels;
namespace Jugenddienst_Stunden.ViewModels;
internal class NoteViewModel : ObservableObject, IQueryAttributable {
private Models.Note _note;
public string Text {
get => _note.Text;
set {
@@ -61,4 +63,4 @@ internal class NoteViewModel : ObservableObject, IQueryAttributable {
OnPropertyChanged(nameof(Text));
OnPropertyChanged(nameof(Date));
}
}
}

View File

@@ -47,8 +47,10 @@ internal class NotesViewModel : IQueryAttributable {
}
// If note isn't found, it's new; add it.
else { AllNotes.Insert(0, new NoteViewModel(Note.Load(noteId))); }
else {
AllNotes.Insert(0, new NoteViewModel(Note.Load(noteId)));
}
}
}
}
}
}

View File

@@ -8,243 +8,257 @@ using static System.Runtime.InteropServices.JavaScript.JSType;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Repositories;
using Jugenddienst_Stunden.Services;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Validators;
using System.Net.Http;
namespace Jugenddienst_Stunden.ViewModels;
/// <summary>
/// Viewmodel für die einzelnen Stundeneinträge / Bearbeitung
/// </summary>
public partial class StundeViewModel : ObservableObject, IQueryAttributable {
private readonly IHoursService _hoursService;
private readonly IHoursService _hoursService;
public int Id { get; set; }
public string Title { get; set; } = "Eintrag bearbeiten";
public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy");
public int Id { get; set; }
public string Title { get; set; } = "Eintrag bearbeiten";
public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy");
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
public event Func<string, string, Task<bool>> ConfirmEvent;
//public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent;
//public event EventHandler<ConfirmEventArgs> ConfirmEvent;
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
/// <summary>
/// Gemeinden für die Auswahlliste
/// </summary>
[ObservableProperty]
private List<Gemeinde> optionsGemeinde;
public event Func<string, string, Task<bool>> ConfirmEvent;
//public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent;
//public event EventHandler<ConfirmEventArgs> ConfirmEvent;
/// <summary>
/// Projekte für die Auswahlliste
/// </summary>
[ObservableProperty]
private List<Projekt> optionsProjekt;
/// <summary>
/// Gemeinden für die Auswahlliste
/// </summary>
[ObservableProperty] private List<Gemeinde> optionsGemeinde;
/// <summary>
/// Freistellungen für die Auswahlliste
/// </summary>
[ObservableProperty]
private List<Freistellung> optionsFreistellung;
/// <summary>
/// Projekte für die Auswahlliste
/// </summary>
[ObservableProperty] private List<Projekt> optionsProjekt;
/// <summary>
/// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird
/// </summary>
[ObservableProperty]
private List<DayTime> dayTimes;
/// <summary>
/// Freistellungen für die Auswahlliste
/// </summary>
[ObservableProperty] private List<Freistellung> optionsFreistellung;
/// <summary>
/// Aktueller Stundeneintrag
/// </summary>
[ObservableProperty]
private DayTime dayTime;
/// <summary>
/// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird
/// </summary>
[ObservableProperty] private List<DayTime> dayTimes;
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
[ObservableProperty]
private bool gemeindeAktivSet;
/// <summary>
/// Aktueller Stundeneintrag
/// </summary>
[ObservableProperty] private DayTime dayTime;
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
[ObservableProperty]
private bool projektAktivSet;
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
[ObservableProperty] private bool gemeindeAktivSet;
[ObservableProperty]
private bool freistellungEnabled;
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
[ObservableProperty] private bool projektAktivSet;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public ICommand DeleteConfirmCommand { get; private set; }
//public ICommand LoadDataCommand { get; private set; }
[ObservableProperty] private bool freistellungEnabled;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public ICommand DeleteConfirmCommand { get; private set; }
//public ICommand LoadDataCommand { get; private set; }
public StundeViewModel() : this(GetServiceOrCreate()) { }
public StundeViewModel() : this(GetServiceOrCreate()) {
}
private static IHoursService GetServiceOrCreate() => new HoursService(new HoursRepository());
private static IHoursService GetServiceOrCreate() {
// Fallback-Konstruktion, falls DI nicht injiziert wurde (z. B. im Designer)
var http = new HttpClient();
var options = new Infrastructure.ApiOptions { BaseUrl = GlobalVar.ApiUrl, Timeout = TimeSpan.FromSeconds(15) };
var tokenProvider = new GlobalVarTokenProvider();
var api = new ApiClient(http, options, tokenProvider, new PreferencesAppSettings());
var repo = new HoursRepository(api);
var validator = new HoursValidator();
return new HoursService(repo, validator);
}
internal StundeViewModel(IHoursService hoursService) {
_hoursService = hoursService;
SaveCommand = new AsyncRelayCommand(Save);
//DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
internal StundeViewModel(IHoursService hoursService) {
_hoursService = hoursService;
SaveCommand = new AsyncRelayCommand(Save);
//DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
private async void LoadSettingsAsync() {
try {
Settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = Settings;
// DI-Konstruktor, der den globalen Alert-Service abonniert und Alerts an das ViewModel weiterreicht.
internal StundeViewModel(IHoursService hoursService, IAlertService alertService) : this(hoursService) {
if (alertService is not null) {
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
}
}
OptionsGemeinde = Settings.Gemeinden;
OptionsProjekt = Settings.Projekte;
OptionsFreistellung = Settings.Freistellungen;
private async void LoadSettingsAsync() {
try {
Settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = Settings;
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
OptionsGemeinde = Settings.Gemeinden;
OptionsProjekt = Settings.Projekte;
OptionsFreistellung = Settings.Freistellungen;
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
async Task Save() {
bool exceptionOccurred = false;
bool proceed = true;
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
proceed = false;
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
}
if (proceed) {
try {
await _hoursService.SaveEntryAsync(DayTime);
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
exceptionOccurred = true;
}
if (!exceptionOccurred) {
if (DayTime.Id != null) {
await Shell.Current.GoToAsync($"..?saved={DayTime.Id}");
} else {
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
}
}
}
async Task Save() {
bool exceptionOccurred = false;
bool proceed = true;
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
proceed = false;
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
}
/// <summary>
/// Löschen ohne Bestätigung
/// </summary>
private async Task Delete() {
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
if (proceed) {
try {
await _hoursService.SaveEntryAsync(DayTime);
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
exceptionOccurred = true;
}
/// <summary>
/// Löschen mit Bestätigung
/// </summary>
private async Task DeleteConfirm() {
if (ConfirmEvent != null) {
bool answer = await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?");
if (answer) {
//Löschen
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} else {
//nicht Löschen
}
}
}
if (!exceptionOccurred) {
if (DayTime.Id != null) {
await Shell.Current.GoToAsync($"..?saved={DayTime.Id}");
} else {
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
}
}
}
/// <summary>
/// Anwenden der Query-Parameter
/// </summary>
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
//load beinhaltet die ID: Eintrag bearbeiten
//date beinhaltet einen Tag: Neuen Eintrag erstellen
if (query.ContainsKey("load")) {
/// <summary>
/// Löschen ohne Bestätigung
/// </summary>
private async Task Delete() {
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
//DateTime heute = DateTime.Now;
try {
var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"]));
var settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = settings;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
/// <summary>
/// Löschen mit Bestätigung
/// </summary>
private async Task DeleteConfirm() {
if (ConfirmEvent != null) {
bool answer =
await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?");
if (answer) {
//Löschen
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} else {
//nicht Löschen
}
}
}
DayTime = entry;
DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
DayTime.TimeSpanBis = entry.End.ToTimeSpan();
/// <summary>
/// Anwenden der Query-Parameter
/// </summary>
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
//load beinhaltet die ID: Eintrag bearbeiten
//date beinhaltet einen Tag: Neuen Eintrag erstellen
if (query.ContainsKey("load")) {
//DateTime heute = DateTime.Now;
try {
var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"]));
var settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = settings;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
OptionsGemeinde = settings.Gemeinden ?? new List<Gemeinde>();
OptionsProjekt = settings.Projekte ?? new List<Projekt>();
OptionsFreistellung = settings.Freistellungen ?? new List<Freistellung>();
DayTime = entry;
DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
DayTime.TimeSpanBis = entry.End.ToTimeSpan();
DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? new Gemeinde();
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? new Projekt();
DayTime.FreistellungAktiv = OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? new Freistellung();
OptionsGemeinde = settings.Gemeinden ?? new List<Gemeinde>();
OptionsProjekt = settings.Projekte ?? new List<Projekt>();
OptionsFreistellung = settings.Freistellungen ?? new List<Freistellung>();
//Evtl. noch die anderen Zeiten des gleichen Tages holen
var day = await _hoursService.GetDayWithSettingsAsync(DayTime.Day);
DayTimes = day.dayTimes;
OnPropertyChanged(nameof(DayTime));
OnPropertyChanged(nameof(DayTimes));
DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ??
new Gemeinde();
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ??
new Projekt();
DayTime.FreistellungAktiv =
OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ??
new Freistellung();
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
} finally {
//Evtl. noch die anderen Zeiten des gleichen Tages holen
var day = await _hoursService.GetDayWithSettingsAsync(DayTime.Day);
DayTimes = day.dayTimes;
OnPropertyChanged(nameof(DayTime));
OnPropertyChanged(nameof(DayTimes));
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
} finally {
}
}
if (System.String.IsNullOrEmpty(DayTime.Description)) {
InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext");
}
if (System.String.IsNullOrEmpty(DayTime.Description)) {
InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext");
}
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle));
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle));
FreistellungEnabled = !DayTime.Approved;
//OnPropertyChanged(nameof(DayTime));
FreistellungEnabled = !DayTime.Approved;
//OnPropertyChanged(nameof(DayTime));
} else if (query.ContainsKey("date")) {
Title = "Neuer Eintrag";
OnPropertyChanged(nameof(Title));
} else if (query.ContainsKey("date")) {
Title = "Neuer Eintrag";
OnPropertyChanged(nameof(Title));
DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd",
System.Globalization.CultureInfo.InvariantCulture);
//Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen
try {
var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date);
GlobalVar.Settings = settings;
DayTimes = list;
DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
//Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen
try {
var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date);
GlobalVar.Settings = settings;
DayTimes = list;
OptionsGemeinde = settings.Gemeinden;
OptionsProjekt = settings.Projekte;
OptionsFreistellung = settings.Freistellungen;
OptionsGemeinde = settings.Gemeinden;
OptionsProjekt = settings.Projekte;
OptionsFreistellung = settings.Freistellungen;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
} catch (Exception) {
//Ein Tag ohne Einträge gibt eine Fehlermeldung,
//die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht
//In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden
LoadSettingsAsync();
} finally {
DayTime = new DayTime();
DayTime.Day = _date;
DayTime.EmployeeId = GlobalVar.EmployeeId;
DayTime.GemeindeAktiv = new Gemeinde();
DayTime.ProjektAktiv = new Projekt();
DayTime.FreistellungAktiv = new Freistellung();
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
SubTitle = _date.ToString("dddd, d. MMMM yyyy");
FreistellungEnabled = true;
} catch (Exception) {
//Ein Tag ohne Einträge gibt eine Fehlermeldung,
//die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht
//In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden
LoadSettingsAsync();
} finally {
DayTime = new DayTime();
DayTime.Day = _date;
DayTime.EmployeeId = GlobalVar.EmployeeId;
DayTime.GemeindeAktiv = new Gemeinde();
DayTime.ProjektAktiv = new Projekt();
DayTime.FreistellungAktiv = new Freistellung();
SubTitle = _date.ToString("dddd, d. MMMM yyyy");
FreistellungEnabled = true;
OnPropertyChanged(nameof(SubTitle));
//OnPropertyChanged(nameof(DayTime));
}
}
}
}
OnPropertyChanged(nameof(SubTitle));
//OnPropertyChanged(nameof(DayTime));
}
}
}
}

View File

@@ -5,325 +5,366 @@ using Jugenddienst_Stunden.Types;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using CommunityToolkit.Maui.Alerts;
using CommunityToolkit.Maui.Core;
using static System.Runtime.InteropServices.JavaScript.JSType;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Repositories;
using Jugenddienst_Stunden.Services;
namespace Jugenddienst_Stunden.ViewModels;
/// <summary>
/// ViewModel für die Stundenliste
/// </summary>
internal partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged {
private readonly IHoursService _hoursService;
public partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged {
private readonly IHoursService _hoursService;
public ICommand NewEntryCommand { get; }
public ICommand SelectEntryCommand { get; }
public ICommand LoadDataCommand { get; private set; }
public ICommand LoadDayCommand { get; private set; }
public ICommand RefreshListCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand NewEntryCommand { get; }
public ICommand SelectEntryCommand { get; }
public ICommand LoadDataCommand { get; private set; }
public ICommand LoadDayCommand { get; private set; }
public ICommand RefreshListCommand { get; }
public ICommand RefreshCommand { get; }
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
/// <summary>
/// Beschriftung Button Monatsübersicht
/// </summary>
[ObservableProperty]
private string loadOverview;
/// <summary>
/// Beschriftung Button Monatsübersicht
/// </summary>
[ObservableProperty] private string loadOverview;
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
/// <summary>
/// Zu leistende Stunden
/// </summary>
[ObservableProperty]
private TimeOnly sollstunden;
/// <summary>
/// Zu leistende Stunden
/// </summary>
[ObservableProperty] private TimeOnly sollstunden;
/// <summary>
/// Geleistete Stunden an einem Tag
/// </summary>
[ObservableProperty]
private TimeOnly dayTotal;
/// <summary>
/// Geleistete Stunden an einem Tag
/// </summary>
[ObservableProperty] private TimeOnly dayTotal;
/// <summary>
/// Liste der Tageszeiten
/// </summary>
[ObservableProperty]
private List<DayTime> dayTimes = new List<DayTime>();
/// <summary>
/// Liste der Tageszeiten
/// </summary>
[ObservableProperty] private List<DayTime> dayTimes = new List<DayTime>();
public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname;
public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname;
[ObservableProperty]
private Hours hours;
[ObservableProperty] private Hours hours;
/// <summary>
/// Mindest-Datum für den Datepicker
/// </summary>
public DateTime MinimumDate {
get => DateTime.Today.AddDays(-365);
}
/// <summary>
/// Mindest-Datum für den Datepicker
/// </summary>
public DateTime MinimumDate {
get => DateTime.Today.AddDays(-365);
}
/// <summary>
/// Höchst-Datum für den Datepicker
/// </summary>
public DateTime MaximumDate {
get => DateTime.Today.AddDays(60);
}
/// <summary>
/// Höchst-Datum für den Datepicker
/// </summary>
public DateTime MaximumDate {
get => DateTime.Today.AddDays(60);
}
/// <summary>
/// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen
/// </summary>
private DateTime dateToday = DateTime.Today;
public DateTime DateToday {
get => dateToday;
set {
if (dateToday != value) {
dateToday = value;
LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM yy");
//OnPropertyChanged();
Task.Run(() => LoadDay(value));
}
}
}
/// <summary>
/// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen
/// </summary>
private DateTime dateToday = DateTime.Today;
/// <summary>
/// Monatsübersicht: Geleistete Stunden
/// </summary>
public double? ZeitCalculated {
get => Hours.Zeit_total;
}
public DateTime DateToday {
get => dateToday;
set {
if (dateToday != value) {
dateToday = value;
LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM yy");
// Task.Run(() => LoadDay(value));
// NICHT Task.Run: LoadDay aktualisiert UI-gebundene Properties
MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
await LoadDay(dateToday);
}
catch (Exception ex)
{
AlertEvent?.Invoke(this, ex.Message);
}
});
}
}
}
/// <summary>
/// Monatsübersicht: Sollstunden
/// </summary>
public double? Nominal {
get => Hours.Nominal;
}
/// <summary>
/// Monatsübersicht: Geleistete Stunden
/// </summary>
public double? ZeitCalculated {
get => Hours.Zeit_total;
}
/// <summary>
/// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden
/// </summary>
public double? Overtime {
get => Hours.overtime;
}
/// <summary>
/// Monatsübersicht: Sollstunden
/// </summary>
public double? Nominal {
get => Hours.Nominal;
}
/// <summary>
/// Monatsübersicht: Restüberstunden insgesamt
/// </summary>
public double OvertimeMonth {
get => Hours.overtime_month;
}
/// <summary>
/// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden
/// </summary>
public double? Overtime {
get => Hours.overtime;
}
public double Zeitausgleich {
get => Hours.zeitausgleich;
}
public double ZeitausgleichMonth {
get => Hours.zeitausgleich_month;
}
/// <summary>
/// Monatsübersicht: Restüberstunden insgesamt
/// </summary>
public double OvertimeMonth {
get => Hours.overtime_month;
}
/// <summary>
/// Monatsübersicht: Resturlaub
/// </summary>
public double Holiday {
get => Hours.holiday;
}
public double Zeitausgleich {
get => Hours.zeitausgleich;
}
/// <summary>
/// Seite neu laden
/// </summary>
[ObservableProperty]
private bool isRefreshing;
public double ZeitausgleichMonth {
get => Hours.zeitausgleich_month;
}
/// <summary>
/// Monatsübersicht: Resturlaub
/// </summary>
public double Holiday {
get => Hours.holiday;
}
/// <summary>
/// Seite neu laden
/// </summary>
[ObservableProperty] private bool isRefreshing;
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
public bool ProjektAktivSet { get; set; }
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
public bool ProjektAktivSet { get; set; }
private bool doContinue = true;
private bool doContinue = true;
/// <summary>
/// CTOR (DI)
/// </summary>
public StundenViewModel(IHoursService hoursService) {
_hoursService = hoursService;
Hours = new Hours();
/// <summary>
/// CTOR
/// </summary>
public StundenViewModel() : this(GetServiceOrCreate()) {
}
LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
private static IHoursService GetServiceOrCreate() => new HoursService(new HoursRepository());
LoadDataCommand = new AsyncRelayCommand(LoadData);
NewEntryCommand = new AsyncRelayCommand(NewEntryAsync);
SelectEntryCommand = new AsyncRelayCommand<DayTime>(SelectEntryAsync);
RefreshListCommand = new AsyncRelayCommand(RefreshList);
RefreshCommand = new Command(async () => await RefreshItemsAsync());
internal StundenViewModel(IHoursService hoursService) {
_hoursService = hoursService;
Hours = new Hours();
LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
LoadDataCommand = new AsyncRelayCommand(LoadData);
NewEntryCommand = new AsyncRelayCommand(NewEntryAsync);
SelectEntryCommand = new AsyncRelayCommand<DayTime>(SelectEntryAsync);
RefreshListCommand = new AsyncRelayCommand(RefreshList);
RefreshCommand = new Command(async () => await RefreshItemsAsync());
Task task = LoadDay(DateTime.Today);
}
// Task task = LoadDay(DateTime.Today);
// Beim Startup NICHT direkt im CTOR laden (kann Startup/Navigation blockieren)
// Stattdessen via Dispatcher "nach" dem Aufbau starten:
MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
await LoadDay(DateTime.Today);
}
catch (Exception ex)
{
AlertEvent?.Invoke(this, ex.Message);
}
});
}
/// <summary>
/// Öffnet eine neue Stundeneingabe
/// </summary>
private async Task NewEntryAsync() {
//Hier muss das Datum übergeben werden
//await Shell.Current.GoToAsync(nameof(Views.StundePage));
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?date={dateToday:yyyy-MM-dd}");
}
/// <summary>
/// Öffnet eine neue Stundeneingabe
/// </summary>
private async Task NewEntryAsync() {
//Hier muss das Datum übergeben werden
//await Shell.Current.GoToAsync(nameof(Views.StundePage));
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?date={dateToday:yyyy-MM-dd}");
}
/// <summary>
/// Öffnet eine bestehende Stundeneingabe
/// </summary>
private async Task SelectEntryAsync(DayTime entry) {
if (entry != null && entry.Id != null) {
//var navigationParameters = new Dictionary<string, object> { { "load", entry.id } };
//await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}", navigationParameters);
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?load={entry.Id}");
} else AlertEvent?.Invoke(this, "Auswahl enthält keine Daten");
}
/// <summary>
/// Öffnet eine bestehende Stundeneingabe
/// </summary>
private async Task SelectEntryAsync(DayTime entry) {
if (entry != null && entry.Id != null) {
//var navigationParameters = new Dictionary<string, object> { { "load", entry.id } };
//await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}", navigationParameters);
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?load={entry.Id}");
} else AlertEvent?.Invoke(this, "Auswahl enthält keine Daten");
}
private async Task RefreshList() {
OnPropertyChanged(nameof(DayTimes));
}
private async Task RefreshList() {
OnPropertyChanged(nameof(DayTimes));
}
/// <summary>
/// Lädt die Monatssummen für die Übersicht
/// </summary>
private async Task LoadData() {
try {
var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
Hours = hours;
Settings = settings;
/// <summary>
/// Lädt die Monatssummen für die Übersicht
/// </summary>
private async Task LoadData() {
try {
var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
Hours = hours;
Settings = settings;
if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this, "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) + " installiert)");
}
//_hour = await HoursBase.LoadData();
RefreshProperties();
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this,
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
" installiert)");
}
//_hour = await HoursBase.LoadData();
RefreshProperties();
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
/// <summary>
/// Lädt die Arbeitszeiten für einen Tag
/// </summary>
public async Task LoadDay(DateTime date) {
DayTotal = new TimeOnly(0);
Sollstunden = new TimeOnly(0);
try {
var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date);
/// <summary>
/// Lädt die Arbeitszeiten für einen Tag
/// </summary>
public async Task LoadDay(DateTime date) {
// kleine Initialwerte sind ok, aber UI-Thread sicher setzen:
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTotal = new TimeOnly(0);
Sollstunden = new TimeOnly(0);
});
try {
var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date);
DayTimes = dayTimes;
Settings = settings;
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTimes = dayTimes;
Settings = settings;
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
OnPropertyChanged(nameof(GemeindeAktivSet));
OnPropertyChanged(nameof(ProjektAktivSet));
OnPropertyChanged(nameof(GemeindeAktivSet));
OnPropertyChanged(nameof(ProjektAktivSet));
});
List<Sollstunden> _soll;
TimeSpan span = TimeSpan.Zero;
bool merker = false;
foreach (DayTime dt in DayTimes) {
span += dt.End - dt.Begin;
//Nachtstunden dazurechnen
if (dt.Night.Ticks > 0 && !merker) {
span += dt.Night.ToTimeSpan() * .5;
merker = true;
}
_soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
if (_soll.Count > 0)
Sollstunden = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit));
}
DayTotal = TimeOnly.FromTimeSpan(span);
List<Sollstunden> _soll;
TimeSpan span = TimeSpan.Zero;
bool merker = false;
foreach (DayTime dt in DayTimes) {
span += dt.End - dt.Begin;
//Nachtstunden dazurechnen
if (dt.Night.Ticks > 0 && !merker) {
span += dt.Night.ToTimeSpan() * .5;
merker = true;
}
//Nach der Tagessumme die anderen Tage anhängen
if (DayTimes != null) {
var more = await _hoursService.GetDayRangeAsync(date, date.AddDays(3));
if (more != null && more.Count > 0)
DayTimes = DayTimes.Concat(more).ToList();
}
_soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
if (_soll.Count > 0)
{
var soll = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit));
await MainThread.InvokeOnMainThreadAsync(() => Sollstunden = soll);
}
}
} catch (Exception e) {
DayTimes = new List<DayTime>();
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
var total = TimeOnly.FromTimeSpan(span);
await MainThread.InvokeOnMainThreadAsync(() => DayTotal = total);
if (Settings.Version != null && Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this, "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) + " installiert)");
} else { InfoEvent?.Invoke(this, e.Message); }
} finally {
OnPropertyChanged(nameof(DayTotal));
OnPropertyChanged(nameof(Sollstunden));
OnPropertyChanged(nameof(DateToday));
OnPropertyChanged(nameof(LoadOverview));
//OnPropertyChanged(nameof(DayTimes));
}
}
//Nach der Tagessumme die anderen Tage anhängen
if (DayTimes != null) {
var more = await _hoursService.GetDayRangeAsync(date, date.AddDays(3));
if (more != null && more.Count > 0)
{
await MainThread.InvokeOnMainThreadAsync(() =>
DayTimes = DayTimes.Concat(more).ToList()
);
}
}
} catch (Exception e) {
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTimes = new List<DayTime>();
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
if (query.ContainsKey("date")) {
await LoadDay(Convert.ToDateTime(query["date"]));
}
}
if (Settings.Version != null && Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this,
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
" installiert)");
} else {
InfoEvent?.Invoke(this, e.Message);
}
});
/// <summary>
/// Seite aktualisieren
/// </summary>
private async Task RefreshItemsAsync() {
IsRefreshing = true;
} finally {
await MainThread.InvokeOnMainThreadAsync(() =>
{
OnPropertyChanged(nameof(DayTotal));
OnPropertyChanged(nameof(Sollstunden));
OnPropertyChanged(nameof(DateToday));
OnPropertyChanged(nameof(LoadOverview));
});
}
}
//await Task.Delay(2000); // Simuliert eine Datenaktualisierung
await LoadDay(DateToday);
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
if (query.ContainsKey("date")) {
await LoadDay(Convert.ToDateTime(query["date"]));
}
}
IsRefreshing = false;
}
/// <summary>
/// Seite aktualisieren
/// </summary>
private async Task RefreshItemsAsync() {
IsRefreshing = true;
/// <summary>
/// Refreshes all properties
/// </summary>
private void RefreshProperties() {
OnPropertyChanged(nameof(Hours));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Nominal));
OnPropertyChanged(nameof(Overtime));
OnPropertyChanged(nameof(OvertimeMonth));
OnPropertyChanged(nameof(Zeitausgleich));
OnPropertyChanged(nameof(ZeitCalculated));
OnPropertyChanged(nameof(Holiday));
OnPropertyChanged(nameof(MinimumDate));
OnPropertyChanged(nameof(MaximumDate));
OnPropertyChanged(nameof(LoadOverview));
}
//await Task.Delay(2000); // Simuliert eine Datenaktualisierung
await LoadDay(DateToday);
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
try {
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
} catch (Exception ex) {
AlertEvent?.Invoke(this, ex.Message);
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
}
}
IsRefreshing = false;
}
/// <summary>
/// Refreshes all properties
/// </summary>
private void RefreshProperties() {
OnPropertyChanged(nameof(Hours));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Nominal));
OnPropertyChanged(nameof(Overtime));
OnPropertyChanged(nameof(OvertimeMonth));
OnPropertyChanged(nameof(Zeitausgleich));
OnPropertyChanged(nameof(ZeitCalculated));
OnPropertyChanged(nameof(Holiday));
OnPropertyChanged(nameof(MinimumDate));
OnPropertyChanged(nameof(MaximumDate));
OnPropertyChanged(nameof(LoadOverview));
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
try {
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
} catch (Exception ex) {
AlertEvent?.Invoke(this, ex.Message);
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
}
}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -13,17 +14,18 @@
<!-- Add an item to the toolbar -->
<ContentPage.ToolbarItems>
<ToolbarItem Text="Neue Notiz" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" />
<ToolbarItem Text="Neue Notiz" Command="{Binding NewCommand}"
IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" />
</ContentPage.ToolbarItems>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
<VerticalStackLayout Margin="20,0,0,0">
<Label Text="Werden nur lokal gespeichert"/>
<Label Text="Werden nur lokal gespeichert" />
<!-- Display notes in a list -->
<CollectionView x:Name="notesCollection"
@@ -31,7 +33,7 @@
Margin="0,20,0,0"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
<!-- Designate how the collection of items is laid out -->
<CollectionView.ItemsLayout>
@@ -42,8 +44,8 @@
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Text}" FontSize="22"/>
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/>
<Label Text="{Binding Text}" FontSize="22" />
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>

View File

@@ -1,11 +1,8 @@
namespace Jugenddienst_Stunden.Views;
public partial class AllNotesPage : ContentPage
{
public AllNotesPage()
{
public partial class AllNotesPage : ContentPage {
public AllNotesPage() {
InitializeComponent();
}
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) {

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -20,8 +21,8 @@
</ContentPage.Resources>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
@@ -29,31 +30,33 @@
<VerticalStackLayout Spacing="10" Margin="15,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<HorizontalStackLayout Spacing="5" HorizontalOptions="Start" Grid.Column="0">
<Label FontSize="20" FontAttributes="Bold" Text="{Binding AppTitle}" Margin="0,7,0,0" />
<Label FontSize="16" Text="{Binding Version}" Margin="0,11,0,0" />
</HorizontalStackLayout>
<Grid Grid.Column="1" ColumnDefinitions="*,50" ColumnSpacing="10">
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0"/>
<Switch x:Name="LoginSwitch" IsToggled="False" Toggled="Switch_Toggled" VerticalOptions="Center" Grid.Column="1"/>
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0" />
<Switch x:Name="LoginSwitch" IsToggled="False" Toggled="Switch_Toggled" VerticalOptions="Center"
Grid.Column="1" />
</Grid>
</Grid>
<Label x:Name="ServerLabel" Text="{Binding Server}" IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
<Label x:Name="ServerLabel" Text="{Binding Server}"
IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0">
<Label Text="Login mit QR-Code" FontSize="32" HorizontalOptions="Start" />
<Label x:Name="Message" Text="{Binding Message}" Margin="0,15" />
<Border HeightRequest="300" Padding="0">
<zxing:CameraBarcodeReaderView
x:Name="barcodeScannerView"
BarcodesDetected="BarcodesDetected"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"/>
<zxing:CameraBarcodeReaderView
x:Name="barcodeScannerView"
BarcodesDetected="BarcodesDetected"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
</Border>
</VerticalStackLayout>
@@ -62,7 +65,8 @@
<Entry x:Name="UsernameEntry" Placeholder="Benutzername (Mailadresse)" Keyboard="Email" />
<Entry x:Name="PasswordEntry" Placeholder="Passwort" IsPassword="True" />
<Entry x:Name="ServerEntry" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" />
<Button Text="Login" Clicked="OnLoginButtonClicked" TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
<Button Text="Login" Clicked="OnLoginButtonClicked"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
</VerticalStackLayout>
</VerticalStackLayout>

View File

@@ -9,197 +9,195 @@ namespace Jugenddienst_Stunden.Views;
/// Die Loginseite mit dem Barcodescanner
/// </summary>
public partial class LoginPage : ContentPage {
private DateTime _lastDetectionTime;
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
private DateTime _lastDetectionTime;
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
/// <summary>
/// CTOR
/// </summary>
public LoginPage() {
InitializeComponent();
/// <summary>
/// CTOR
/// </summary>
public LoginPage() {
InitializeComponent();
barcodeScannerView.Options = new BarcodeReaderOptions {
Formats = BarcodeFormat.QrCode,
AutoRotate = true,
Multiple = false
};
barcodeScannerView.Options =
new BarcodeReaderOptions { Formats = BarcodeFormat.QrCode, AutoRotate = true, Multiple = false };
//if (BindingContext is LoginViewModel vm) {
// vm.AlertEvent += Vm_AlertEvent;
// vm.InfoEvent += Vm_InfoEvent;
// vm.MsgEvent += Vm_MsgEvent;
//}
//if (BindingContext is LoginViewModel vm) {
// vm.AlertEvent += Vm_AlertEvent;
// vm.InfoEvent += Vm_InfoEvent;
// vm.MsgEvent += Vm_MsgEvent;
//}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
bool sqr = true;
bool sma = false;
if (Preferences.Default.Get("logintype", "") == "manual") {
sqr = false;
sma = true;
LoginSwitch.IsToggled = true;
Message.IsVisible = false;
} else {
LoginSwitch.IsToggled = false;
Message.IsVisible = true;
}
LoginQR.IsVisible = sqr;
LoginManual.IsVisible = sma;
}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
bool sqr = true;
bool sma = false;
if (Preferences.Default.Get("logintype", "") == "manual") {
sqr = false;
sma = true;
LoginSwitch.IsToggled = true;
Message.IsVisible = false;
} else {
LoginSwitch.IsToggled = false;
Message.IsVisible = true;
}
LoginQR.IsVisible = sqr;
LoginManual.IsVisible = sma;
}
/// <summary>
/// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt
/// ZXing.Net.Maui.Controls 0.4.4
/// </summary>
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e) {
/// <summary>
/// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt
/// ZXing.Net.Maui.Controls 0.4.4
/// </summary>
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e) {
var currentTime = DateTime.Now;
if ((currentTime - _lastDetectionTime) > _detectionInterval) {
_lastDetectionTime = currentTime;
foreach (var barcode in e.Results) {
if (GlobalVar.ApiKey != barcode.Value) {
_ = MainThread.InvokeOnMainThreadAsync(async () => {
//await DisplayAlert("Barcode erkannt", $"Barcode: {barcode.Format} - {barcode.Value}", "OK");
var currentTime = DateTime.Now;
if ((currentTime - _lastDetectionTime) > _detectionInterval) {
_lastDetectionTime = currentTime;
foreach (var barcode in e.Results) {
if (GlobalVar.ApiKey != barcode.Value) {
_ = MainThread.InvokeOnMainThreadAsync(async () => {
//await DisplayAlert("Barcode erkannt", $"Barcode: {barcode.Format} - {barcode.Value}", "OK");
try {
var tokendata = new TokenData(barcode.Value);
GlobalVar.ApiUrl = tokendata.Url;
User user = await HoursBase.LoadUser(barcode.Value);
try {
var tokendata = new TokenData(barcode.Value);
GlobalVar.ApiUrl = tokendata.Url;
User user = await HoursBase.LoadUser(barcode.Value);
GlobalVar.ApiKey = barcode.Value;
GlobalVar.Name = user.Name;
GlobalVar.Surname = user.Surname;
GlobalVar.EmployeeId = user.Id;
GlobalVar.ApiKey = barcode.Value;
GlobalVar.Name = user.Name;
GlobalVar.Surname = user.Surname;
GlobalVar.EmployeeId = user.Id;
Title = user.Name + " " + user.Surname;
//Auf der Loginseite wird der Server als Info ohne Protokoll und ohne /appapi angezeigt
ServerLabel.Text = "Server: " + tokendata.Url.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
Title = user.Name + " " + user.Surname;
//Auf der Loginseite wird der Server als Info ohne Protokoll und ohne /appapi angezeigt
ServerLabel.Text = "Server: " + tokendata.Url.Replace("/appapi", "").Replace("https://", "")
.Replace("http://", "");
await DisplayAlert("Login erfolgreich", user.Name + " " + user.Surname, "OK");
if (Navigation.NavigationStack.Count > 1) {
//Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck
await Navigation.PopAsync();
} else {
//Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht
await Shell.Current.GoToAsync($"//StundenPage");
}
await DisplayAlert("Login erfolgreich", user.Name + " " + user.Surname, "OK");
if (Navigation.NavigationStack.Count > 1) {
//Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck
await Navigation.PopAsync();
} else {
//Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht
await Shell.Current.GoToAsync($"//StundenPage");
}
} catch (Exception e) {
await DisplayAlert("Fehler", e.Message, "OK");
}
});
} else {
MainThread.InvokeOnMainThreadAsync(() => {
DisplayAlert("Bereits eingeloggt",
Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""),
"OK");
});
}
}
}
}
} catch (Exception e) {
await DisplayAlert("Fehler", e.Message, "OK");
}
protected override void OnDisappearing() {
base.OnDisappearing();
});
} else {
MainThread.InvokeOnMainThreadAsync(() => {
DisplayAlert("Bereits eingeloggt",
Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""),
"OK");
});
}
}
}
barcodeScannerView.CameraLocation = CameraLocation.Front;
barcodeScannerView.IsDetecting = false;
}
}
protected override void OnAppearing() {
base.OnAppearing();
protected override void OnDisappearing() {
base.OnDisappearing();
barcodeScannerView.IsDetecting = true;
barcodeScannerView.CameraLocation = CameraLocation.Rear;
}
barcodeScannerView.CameraLocation = CameraLocation.Front;
barcodeScannerView.IsDetecting = false;
}
public bool IsCameraAvailable() {
var status = Permissions.CheckStatusAsync<Permissions.Camera>().Result;
if (status != PermissionStatus.Granted) {
status = Permissions.RequestAsync<Permissions.Camera>().Result;
}
protected override void OnAppearing() {
base.OnAppearing();
return status != PermissionStatus.Granted;
}
barcodeScannerView.IsDetecting = true;
barcodeScannerView.CameraLocation = CameraLocation.Rear;
}
public bool IsCameraAvailable() {
var status = Permissions.CheckStatusAsync<Permissions.Camera>().Result;
if (status != PermissionStatus.Granted) {
status = Permissions.RequestAsync<Permissions.Camera>().Result;
}
return status != PermissionStatus.Granted;
}
private async void OnLoginButtonClicked(object sender, EventArgs e) {
var username = UsernameEntry.Text;
var password = PasswordEntry.Text;
var server = ServerEntry.Text;
private async void OnLoginButtonClicked(object sender, EventArgs e) {
var username = UsernameEntry.Text;
var password = PasswordEntry.Text;
var server = ServerEntry.Text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(server)) {
await DisplayAlert("Fehler", "Bitte alle Felder ausf<73>llen", "OK");
return;
}
try {
Uri uri = new Uri(InputUrlWithSchema(server));
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(server)) {
await DisplayAlert("Fehler", "Bitte alle Felder ausf<73>llen", "OK");
return;
}
Types.User response = await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi");
try {
Uri uri = new Uri(InputUrlWithSchema(server));
GlobalVar.ApiKey = response.Token;
GlobalVar.Name = response.Name;
GlobalVar.Surname = response.Surname;
GlobalVar.EmployeeId = response.Id;
GlobalVar.ApiUrl = uri.Scheme + "://" + uri.Authority + "/appapi";
Types.User response =
await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi");
Title = response.Name + " " + response.Surname;
//ServerLabel.Text = "Server: " + server.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
ServerLabel.Text = "Server: " + uri.Authority;
GlobalVar.ApiKey = response.Token;
GlobalVar.Name = response.Name;
GlobalVar.Surname = response.Surname;
GlobalVar.EmployeeId = response.Id;
GlobalVar.ApiUrl = uri.Scheme + "://" + uri.Authority + "/appapi";
await DisplayAlert("Login erfolgreich", response.Name + " " + response.Surname, "OK");
if (Navigation.NavigationStack.Count > 1)
await Navigation.PopAsync();
else {
await Shell.Current.GoToAsync($"//StundenPage");
}
} catch (Exception ex) {
await DisplayAlert("Fehler", ex.Message, "OK");
}
}
Title = response.Name + " " + response.Surname;
//ServerLabel.Text = "Server: " + server.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
ServerLabel.Text = "Server: " + uri.Authority;
/// <summary>
/// Aus einer URL ohne Schema eine URL mit Schema machen
/// </summary>
private static string InputUrlWithSchema(string url) {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) {
url = "https://" + url;
}
if (url.StartsWith("http://")) {
url = url.Replace("http://", "https://");
}
return url;
}
await DisplayAlert("Login erfolgreich", response.Name + " " + response.Surname, "OK");
if (Navigation.NavigationStack.Count > 1)
await Navigation.PopAsync();
else {
await Shell.Current.GoToAsync($"//StundenPage");
}
} catch (Exception ex) {
await DisplayAlert("Fehler", ex.Message, "OK");
}
}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
private void Switch_Toggled(object sender, ToggledEventArgs e) {
var switcher = (Switch)sender;
/// <summary>
/// Aus einer URL ohne Schema eine URL mit Schema machen
/// </summary>
private static string InputUrlWithSchema(string url) {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) {
url = "https://" + url;
}
if (switcher.IsToggled) {
LoginQR.IsVisible = false;
LoginManual.IsVisible = true;
Message.IsVisible = false;
Preferences.Default.Set("logintype", "manual");
} else {
LoginQR.IsVisible = true;
LoginManual.IsVisible = false;
Message.IsVisible = true;
Preferences.Default.Set("logintype", "qr");
}
if (url.StartsWith("http://")) {
url = url.Replace("http://", "https://");
}
}
return url;
}
//private void Vm_AlertEvent(object? sender, string e) {
// DisplayAlert("Fehler:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private async Task Vm_MsgEvent(string title, string message) {
// await DisplayAlert(title, message, "OK");
//}
}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
private void Switch_Toggled(object sender, ToggledEventArgs e) {
var switcher = (Switch)sender;
if (switcher.IsToggled) {
LoginQR.IsVisible = false;
LoginManual.IsVisible = true;
Message.IsVisible = false;
Preferences.Default.Set("logintype", "manual");
} else {
LoginQR.IsVisible = true;
LoginManual.IsVisible = false;
Message.IsVisible = true;
Preferences.Default.Set("logintype", "qr");
}
}
//private void Vm_AlertEvent(object? sender, string e) {
// DisplayAlert("Fehler:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private async Task Vm_MsgEvent(string title, string message) {
// await DisplayAlert(title, message, "OK");
//}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -11,8 +12,8 @@
</ContentPage.BindingContext>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>

View File

@@ -4,7 +4,6 @@ namespace Jugenddienst_Stunden.Views;
/// Einzelne Notiz
/// </summary>
public partial class NotePage : ContentPage {
/// <summary>
/// CTOR
/// </summary>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -19,8 +20,8 @@
</ContentPage.Resources>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
@@ -31,20 +32,26 @@
<Border>
<Border.Padding>
<OnPlatform x:TypeArguments="Thickness" Default="0,15,10,0">
<On Platform="Android" Value="0,4,10,8"/>
<On Platform="WPF" Value="0,15,10,0"/>
<On Platform="Android" Value="0,4,10,8" />
<On Platform="WPF" Value="0,15,10,0" />
</OnPlatform>
</Border.Padding>
<FlexLayout Direction="Row" AlignItems="Start" Wrap="Wrap" JustifyContent="SpaceBetween">
<HorizontalStackLayout Spacing="10">
<Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label>
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanVon}" />
<Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
MinimumWidthRequest="60">
</Label>
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80"
Time="{Binding DayTime.TimeSpanVon}" />
</HorizontalStackLayout>
<HorizontalStackLayout Spacing="10">
<Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label>
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanBis}" />
<Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
MinimumWidthRequest="60">
</Label>
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80"
Time="{Binding DayTime.TimeSpanBis}" />
</HorizontalStackLayout>
</FlexLayout>
</Border>
@@ -52,8 +59,8 @@
<Border>
<Border.Padding>
<OnPlatform x:TypeArguments="Thickness" Default="5">
<On Platform="Android" Value="5,4,5,8"/>
<On Platform="WPF" Value="5"/>
<On Platform="Android" Value="5,4,5,8" />
<On Platform="WPF" Value="5" />
</OnPlatform>
</Border.Padding>
<!--<Grid ColumnDefinitions="*,*,*">
@@ -65,35 +72,45 @@
</Picker>
</Grid>-->
<HorizontalStackLayout>
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}" SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsVisible="{Binding GemeindeAktivSet}">
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}"
SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}"
IsVisible="{Binding GemeindeAktivSet}">
</Picker>
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}" SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsVisible="{Binding ProjektAktivSet}">
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}"
SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}"
IsVisible="{Binding ProjektAktivSet}">
</Picker>
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}" SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsEnabled="{Binding FreistellungEnabled}">
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}"
SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}"
ItemDisplayBinding="{Binding Name}" IsEnabled="{Binding FreistellungEnabled}">
</Picker>
</HorizontalStackLayout>
</Border>
<Editor Placeholder="Beschreibung" Text="{Binding DayTime.Description}" MinimumHeightRequest="40" AutoSize="TextChanges" FontSize="18" />
<Editor Placeholder="Beschreibung" Text="{Binding DayTime.Description}" MinimumHeightRequest="40"
AutoSize="TextChanges" FontSize="18" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="4">
<Button Grid.Column="1" Text="Speichern"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}"
Command="{Binding SaveCommand}" />
<Button Grid.Column="0" Text="Löschen"
Command="{Binding DeleteConfirmCommand}"
IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}"
<Button Grid.Column="0" Text="Löschen"
Command="{Binding DeleteConfirmCommand}"
IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}"
IsVisible="{Binding FreistellungEnabled}"
BackgroundColor="{StaticResource Gray500}"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}"/>
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
</Grid>
<BoxView HeightRequest="1" Margin="3,10" />
<Label Text="Noch keine Einträge vorhanden" IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}, ConverterParameter=Invert}" Margin="6,0,0,0"/>
<Label Text="Noch keine Einträge vorhanden"
IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}, ConverterParameter=Invert}"
Margin="6,0,0,0" />
<StackLayout Margin="6,0,0,0" IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
<StackLayout Margin="6,0,0,0"
IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
<Label>
<Label.FormattedText>
<FormattedString>
@@ -105,13 +122,13 @@
</StackLayout>
<ScrollView IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
<CollectionView
ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0"
HeightRequest="350"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
<CollectionView
ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0"
HeightRequest="350"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="0" />
@@ -129,15 +146,18 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<HorizontalStackLayout Grid.Row="0" Grid.Column="0">
<Label Grid.Column="0" Text="{Binding Begin}"/>
<Label Text="bis" Padding="5,0,5,0"/>
<Label Text="{Binding End}"/>
<Label Text="{Binding GemeindeAktiv.Name}" Margin="10,0,0,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}"/>
<Label Text="{Binding ProjektAktiv.Name}" Margin="10,0,0,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}"/>
<Label Text="{Binding FreistellungAktiv.Name}" Margin="10,0,0,0"/>
<Label Grid.Column="0" Text="{Binding Begin}" />
<Label Text="bis" Padding="5,0,5,0" />
<Label Text="{Binding End}" />
<Label Text="{Binding GemeindeAktiv.Name}" Margin="10,0,0,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
<Label Text="{Binding ProjektAktiv.Name}" Margin="10,0,0,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
<Label Text="{Binding FreistellungAktiv.Name}" Margin="10,0,0,0" />
</HorizontalStackLayout>
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Description}" Padding="0,0,0,15"/>
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Description}"
Padding="0,0,0,15" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>

View File

@@ -10,45 +10,43 @@ namespace Jugenddienst_Stunden.Views;
/// Einzelner Stundeneintrag
/// </summary>
public partial class StundePage : ContentPage {
/// <summary>
/// CTOR
/// </summary>
public StundePage() {
InitializeComponent();
/// <summary>
/// CTOR
/// </summary>
public StundePage() {
InitializeComponent();
if (BindingContext is StundeViewModel vm) {
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
vm.ConfirmEvent += ShowConfirm;
}
}
if (BindingContext is StundeViewModel vm) {
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
vm.ConfirmEvent += ShowConfirm;
}
}
private void Vm_AlertEvent(object? sender, string e) {
DisplayAlert("Fehler:", e, "OK");
}
private void Vm_AlertEvent(object? sender, string e) {
DisplayAlert("Fehler:", e, "OK");
}
private async Task<bool> ShowConfirm(string title, string message) {
return await DisplayAlert(title, message, "Passt!", "Na, nor decht nit.");
}
private async Task<bool> ShowConfirm(string title, string message) {
return await DisplayAlert(title, message, "Passt!", "Na, nor decht nit.");
}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 20;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 20;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
//private async Task<bool> ShowConfirm(string title, string message, string ok, string not_ok) {
// return await DisplayAlert(title, message, ok, not_ok);
//}
//private async void ShowConfirm(object? sender, ConfirmEventArgs e) {
// bool result = await DisplayAlert(e.Title, e.Message, e.Ok, e.NotOk);
// e.Result = result;
//}
//private async Task<bool> ShowConfirm(string title, string message, string ok, string not_ok) {
// return await DisplayAlert(title, message, ok, not_ok);
//}
//private async void ShowConfirm(object? sender, ConfirmEventArgs e) {
// bool result = await DisplayAlert(e.Title, e.Message, e.Ok, e.NotOk);
// e.Result = result;
//}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -8,52 +9,51 @@
x:Class="Jugenddienst_Stunden.Views.StundenPage"
Title="{Binding Title}">
<ContentPage.BindingContext>
<models:StundenViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<conv:SecondsTimeConverter x:Key="secToTime" />
<FontImageSource x:Key="ToolbarIcon"
Glyph="+"
Size="22"
Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}"/>
Glyph="+"
Size="22"
Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
<ContentPage.ToolbarItems>
<!--<ToolbarItem Text="Lade Liste" Command="{Binding RefreshListCommand}"/>-->
<ToolbarItem Text="Neuer Eintrag" IconImageSource="{StaticResource ToolbarIcon}" Command="{Binding NewEntryCommand}" />
<ToolbarItem Text="Neuer Eintrag" IconImageSource="{StaticResource ToolbarIcon}"
Command="{Binding NewEntryCommand}" />
</ContentPage.ToolbarItems>
<RefreshView x:Name="MyRefreshView" Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}" Margin="10" Padding="10">
<RefreshView x:Name="MyRefreshView" Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}"
Margin="10" Padding="10">
<Grid RowDefinitions="50,*,Auto,80">
<!--<VerticalStackLayout Spacing="10" Margin="10">-->
<!--<VerticalStackLayout Spacing="10" Margin="10">-->
<Grid RowDefinitions="Auto" ColumnDefinitions="Auto,*" HeightRequest="50" Grid.Row="0">
<DatePicker Grid.Column="0" MinimumDate="{Binding MinimumDate}"
MaximumDate="{Binding MaximumDate}"
Date="{Binding DateToday}" Format="dddd, d. MMMM yyyy" />
MaximumDate="{Binding MaximumDate}"
Date="{Binding DateToday}" Format="dddd, d. MMMM yyyy" />
<Border Grid.Column="1" Margin="15,0,0,0" Padding="15,0,0,0" ToolTipProperties.Text="Tagessumme">
<HorizontalStackLayout>
<Label Text="{Binding DayTotal,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label>
<Label Text="/" VerticalOptions="Center" Margin="3,0"/>
<Label Text="/" VerticalOptions="Center" Margin="3,0" />
<Label Text="{Binding Sollstunden,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label>
</HorizontalStackLayout>
</Border>
</Grid>
<CollectionView
ItemsSource="{Binding DayTimes}"
<CollectionView
ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0,0,0,20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"
@@ -72,27 +72,29 @@
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource TransparentColor}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource TransparentColor}}" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<HorizontalStackLayout>
<HorizontalStackLayout.Triggers>
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}" Value="True">
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}"
Value="True">
<Setter Property="BackgroundColor" Value="LightCoral" />
<Setter Property="Padding" Value="4"/>
<Setter Property="Padding" Value="4" />
</DataTrigger>
</HorizontalStackLayout.Triggers>
<Label Text="{Binding Day, StringFormat='{0:dddd, dd. MMMM}'}"/>
<Label Text="{Binding Day, StringFormat='{0:dddd, dd. MMMM}'}" />
<Label Text="von" Padding="5,0,5,0" />
<Label Text="{Binding Begin}" />
<Label Text="bis" Padding="5,0,5,0" />
@@ -101,27 +103,30 @@
<HorizontalStackLayout HorizontalOptions="FillAndExpand">
<HorizontalStackLayout.Triggers>
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}" Value="True">
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}"
Value="True">
<Setter Property="BackgroundColor" Value="LightCoral" />
<Setter Property="Padding" Value="4"/>
<Setter Property="Padding" Value="4" />
</DataTrigger>
</HorizontalStackLayout.Triggers>
<Label Text="{Binding GemeindeAktiv.Name}" Margin="0,0,10,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
<Label Text="{Binding ProjektAktiv.Name}" Margin="0,0,10,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
<Label Text="{Binding GemeindeAktiv.Name}" Margin="0,0,10,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
<Label Text="{Binding ProjektAktiv.Name}" Margin="0,0,10,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
<Label Text="{Binding FreistellungAktiv.Name}" IsVisible="{Binding Approved}" />
</HorizontalStackLayout>
<Label Text="{Binding Description}" Padding="0,0,0,15"/>
<Label Text="{Binding Description}" Padding="0,0,0,15" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!--<BoxView HeightRequest="1" Grid.Row="2" Margin="0,5,0,15" />-->
<Button Text="{Binding LoadOverview}"
Grid.Row="2"
Command="{Binding LoadDataCommand}"
<Button Text="{Binding LoadOverview}"
Grid.Row="2"
Command="{Binding LoadDataCommand}"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
<Border Padding="2" Grid.Row="3" Margin="0,10,0,0">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,*" ColumnDefinitions="Auto,Auto,*,Auto" Margin="10,2">
@@ -133,15 +138,23 @@
<Label Grid.Row="1" Grid.Column="2" Text="Zeitausgleich:" Margin="15,0,0,0" />
<Label Grid.Row="2" Grid.Column="2" Text="Resturlaub:" Margin="15,0,0,0" />
<Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Nominal, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Sollstunden" />
<Label Grid.Row="1" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Geleistete Stunden" />
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" />
<Label Grid.Row="0" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Overtime, Converter={StaticResource secToTime}}" />
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" />
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Holiday, Converter={StaticResource secToTime}}" />
<Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Nominal, Converter={StaticResource secToTime}}"
ToolTipProperties.Text="Sollstunden" />
<Label Grid.Row="1" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}"
ToolTipProperties.Text="Geleistete Stunden" />
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" />
<Label Grid.Row="0" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Overtime, Converter={StaticResource secToTime}}" />
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" />
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Holiday, Converter={StaticResource secToTime}}" />
</Grid>
</Border>
</Grid>
<!--</VerticalStackLayout>-->

View File

@@ -9,60 +9,80 @@ namespace Jugenddienst_Stunden.Views;
/// Code-Behind f<>r die Stunden-<2D>bersicht
/// </summary>
public partial class StundenPage : ContentPage {
/// <summary>
/// CTOR (f<>r Shell/XAML DataTemplate erforderlich)
/// </summary>
public StundenPage() : this(
(Application.Current?.Handler?.MauiContext?.Services
?? throw new InvalidOperationException("DI container ist nicht verf<72>gbar."))
.GetRequiredService<StundenViewModel>()) {
}
/// <summary>
/// CTOR
/// </summary>
public StundenPage() {
InitializeComponent();
/// <summary>
/// CTOR (DI)
/// </summary>
public StundenPage(StundenViewModel vm) {
InitializeComponent();
BindingContext = vm;
if (BindingContext is StundenViewModel vm) {
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
}
if (!CheckLogin()) {
NavigateToTargetPage();
}
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
}
// Navigation NICHT im CTOR ausf<73>hren (Shell/Navigation-Stack ist hier oft noch nicht ?ready?)
// if (!CheckLogin()) {
// NavigateToTargetPage();
// }
}
private void Vm_AlertEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
await DisplayAlert("Fehler:", e, "OK");
});
}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// MainThread.BeginInvokeOnMainThread(async () => {
// await DisplayAlert("Information:", e, "OK");
// });
//}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 16;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
private void Vm_AlertEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => { await DisplayAlert("Fehler:", e, "OK"); });
}
/// <summary>
/// Beim Laden der Seite den Titel setzen
/// </summary>
protected override void OnAppearing() {
base.OnAppearing();
Title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// MainThread.BeginInvokeOnMainThread(async () => {
// await DisplayAlert("Information:", e, "OK");
// });
//}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 16;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
private bool CheckLogin() {
return Preferences.Default.Get("apiKey", "") != "";
}
/// <summary>
/// Beim Laden der Seite den Titel setzen
/// </summary>
protected override async void OnAppearing() {
base.OnAppearing();
Title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
private async void NavigateToTargetPage() {
await Navigation.PushAsync(new LoginPage());
}
if (!CheckLogin()) {
try {
await NavigateToTargetPage();
} catch (Exception ex) {
await DisplayAlert("Fehler:", ex.Message, "OK");
}
}
}
private bool CheckLogin() {
return Preferences.Default.Get("apiKey", "") != "";
}
// private async void NavigateToTargetPage() {
// await Navigation.PushAsync(new LoginPage());
// }
private Task NavigateToTargetPage() {
// Shell-Navigation statt Navigation.PushAsync
// Voraussetzung: LoginPage-Route ist in AppShell registriert (Routing.RegisterRoute(...))
return Shell.Current.GoToAsync(nameof(Views.LoginPage));
}
}