Compare commits

6 Commits

Author SHA1 Message Date
98d6d61f16 Projekt and Gemeinde are required if active 2025-12-18 16:17:17 +01:00
5fd97deada LoginFixes 2025-12-18 15:35:39 +01:00
c11b361655 Refactor LoginPage to MVVM 2025-12-17 17:25:42 +01:00
bb5aac2944 Return Statusmesssage from API 2025-12-17 12:14:15 +01:00
76eb71946f Refactor Api-Client
Add Exceptionhandler, AlertService JSON-Converter
AppSettings via DI

Reformat Code
2025-12-17 09:34:08 +01:00
544b0c9591 Architecture
Add DI, Interfaces, Repositories
2025-12-16 15:27:09 +01:00
89 changed files with 3588 additions and 2151 deletions

247
.editorconfig Normal file
View File

@@ -0,0 +1,247 @@
# Entfernen Sie die folgende Zeile, wenn Sie EDITORCONFIG-Einstellungen von höheren Verzeichnissen vererben möchten.
root = true
# C#-Dateien
[*.cs]
#### Wichtige EditorConfig-Optionen ####
# Einzüge und Abstände
indent_size = 4
indent_style = tab
tab_width = 4
# Einstellungen für neue Zeilen
end_of_line = crlf
insert_final_newline = false
#### .NET Codeaktionen ####
# Typmitglied
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# Symbolsuche
dotnet_search_reference_assemblies = true
#### .NET-Codierungskonventionen ####
# Using-Direktiven organisieren
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this.- und Me.-Einstellungen
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Einstellungen für Sprachschlüsselwörter und BCL-Typen
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Einstellungen für Klammern
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Einstellungen für Modifizierer
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Einstellungen für Ausdrucksebene
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Einstellungen für Felder
dotnet_style_readonly_field = true
# Einstellungen für Parameter
dotnet_code_quality_unused_parameters = all
# Unterdrückungseinstellungen
dotnet_remove_unnecessary_suppression_exclusions = none
# Einstellungen für neue Zeilen
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C#-Codierungskonventionen ####
# Var-Einstellungen
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Ausdruckskörpermember
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Einstellungen für den Musterabgleich
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Einstellungen für NULL-Überprüfung
csharp_style_conditional_delegate_call = true
# Einstellungen für Modifizierer
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Einstellungen für Codeblöcke
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_prefer_system_threading_lock = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_simple_property_accessors = true
csharp_style_prefer_top_level_statements = true
# Einstellungen für Ausdrucksebene
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_implicitly_typed_lambda_expression = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_unbound_generic_type_in_nameof = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# Einstellungen für using-Anweisungen
csharp_using_directive_placement = outside_namespace
# Einstellungen für neue Zeilen
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C#-Formatierungsregeln ####
# Einstellungen für neue Zeilen
csharp_new_line_before_catch = false
csharp_new_line_before_else = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = true
# Einstellungen für Einrückung
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Einstellungen für Abstände
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Umbrucheinstellungen
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Benennungsstile ####
# Benennungsregeln
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbolspezifikationen
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Benennungsstile
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

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

@@ -0,0 +1,8 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
public sealed class EventArgsPassThroughConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value;
}

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

@@ -0,0 +1,15 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
public sealed class InverseBoolConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is bool b) return !b;
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is bool b) return !b;
return false;
}
}

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,150 @@
using Jugenddienst_Stunden.Interfaces;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using ZXing.Aztec.Internal;
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;
// Timeout nur einmalig beim Erstellen setzen spätere Änderungen an HttpClient.Timeout
// nach der ersten Verwendung führen zu InvalidOperationException.
if (_http.Timeout != options.Timeout)
_http.Timeout = options.Timeout;
// Standardmäßig JSON akzeptieren; doppelte Einträge vermeiden
if (!_http.DefaultRequestHeaders.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true))
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// KEINE globalen Header/Properties mehr dynamisch setzen. Authorization wird pro Request gesetzt.
_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());
// WICHTIG: HttpClient.BaseAddress NICHT dynamisch setzen oder ändern das verursacht Exceptions,
// sobald bereits Requests gestartet wurden. Wir bauen stattdessen absolute URIs pro Request.
}
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) {
// Absolute URI aus aktuellem SettingsBaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen.
var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query);
using var req = new HttpRequestMessage(method, uri);
// Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist)
// Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten.
// AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen.
var currentToken = _settings.ApiKey;
if (!string.IsNullOrWhiteSpace(currentToken)) {
// Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden
req.Headers.Remove("Authorization");
req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {currentToken}");
}
if (body is HttpContent httpContent) {
req.Content = httpContent;
} else if (body is not null) {
req.Content = JsonContent.Create(body, options: _json);
}
// Sicherstellen, dass Accept: application/json auch auf Request-Ebene vorhanden ist (z. B. für LoginWithToken GET)
if (!req.Headers.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) {
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/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 (res.StatusCode != System.Net.HttpStatusCode.OK) {
// Verhalten wie in BaseFunc: bei Fehlerstatus -> "message" aus Body lesen und mit dessen Inhalt eine Exception werfen.
try {
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
using var doc = JsonDocument.Parse(text, options);
var root = doc.RootElement;
// GetProperty wirft, wenn "message" fehlt — das entspricht dem bisherigen Verhalten in BaseFunc.
var messageElement = root.GetProperty("message");
if (messageElement.ValueKind != JsonValueKind.String)
throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", text);
var message = messageElement.GetString() ?? throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", text);
throw ApiException.From(res.StatusCode, message, text);
} catch (ApiException) {
throw;
} catch (Exception) {
// Fallback: Wenn Parsing fehlschlägt oder "message" fehlt, konsistente Fehlermeldung wie BaseFunc
throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", 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);
// Entfernt: EnsureBaseAddress wir ändern BaseAddress nicht mehr dynamisch.
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 BuildAbsoluteUri(string baseUrl, string path, IDictionary<string, string?>? query) {
if (string.IsNullOrWhiteSpace(baseUrl))
throw new InvalidOperationException(
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl').");
// Basis muss absolut sein (z. B. https://host/appapi/)
var baseUri = new Uri(baseUrl, UriKind.Absolute);
// Pfad relativ zur Basis aufbauen
string relativePath = path ?? string.Empty;
if (query is not null && query.Count > 0) {
var sb = new StringBuilder(relativePath);
sb.Append(relativePath.Contains('?') ? '&' : '?');
sb.Append(string.Join('&', query
.Where(kv => kv.Value is not null)
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
relativePath = sb.ToString();
}
// Wenn path bereits absolut ist, direkt verwenden
if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath))
return absoluteFromPath;
return new Uri(baseUri, relativePath);
}
}

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,33 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class RequestLoggingHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
// Log outgoing request URI + headers
Debug.WriteLine($"[Http] Request: {request.Method} {request.RequestUri}");
foreach (var h in request.Headers) {
Debug.WriteLine($"[Http] RequestHeader: {h.Key} = {string.Join(", ", h.Value)}");
}
if (request.Content is not null) {
foreach (var h in request.Content.Headers) {
Debug.WriteLine($"[Http] ContentHeader: {h.Key} = {string.Join(", ", h.Value)}");
}
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// Log response status + Location (bei Redirects) + final request URI used by handler
Debug.WriteLine($"[Http] Response: {(int)response.StatusCode} {response.ReasonPhrase}");
if (response.Headers.Location is not null)
Debug.WriteLine($"[Http] Response Location: {response.Headers.Location}");
if (response.RequestMessage?.RequestUri is not null)
Debug.WriteLine($"[Http] Final RequestUri: {response.RequestMessage.RequestUri}");
return response;
}
}

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

@@ -0,0 +1,8 @@
namespace Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Types;
public interface IAuthService {
Task<User> LoginWithCredentials(string username, string password, string serverUrl, CancellationToken ct = default);
Task<User> LoginWithToken(string token, CancellationToken ct = default);
}

View File

@@ -0,0 +1,17 @@
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Interfaces;
/// <summary>
/// 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);
}

View File

@@ -0,0 +1,16 @@
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Interfaces;
/// <summary>
/// Fachlicher Service für Stunden konsumiert Repository und stellt VMfreundliche Methoden bereit.
/// </summary>
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

@@ -1,7 +1,14 @@
using CommunityToolkit.Maui;
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;
using Jugenddienst_Stunden.ViewModels;
namespace Jugenddienst_Stunden;
@@ -9,35 +16,96 @@ 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
return builder.Build();
}
// DI: AlertService für globale Alerts (z. B. leere ApiUrl)
builder.Services.AddSingleton<IAlertService, AlertService>();
// 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
// Configure HttpClient with RequestLoggingHandler and disable automatic redirects for diagnosis
builder.Services.AddTransient<RequestLoggingHandler>();
builder.Services.AddSingleton<HttpClient>(sp => {
var nativeHandler = new HttpClientHandler { AllowAutoRedirect = false };
var logging = sp.GetRequiredService<RequestLoggingHandler>();
logging.InnerHandler = nativeHandler;
// HttpClient.Timeout will be adjusted by ApiClient if needed
return new HttpClient(logging, disposeHandler: true);
});
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>();
builder.Services.AddSingleton<IAuthService, AuthService>();
// DI: Views/ViewModels
builder.Services.AddTransient<ViewModels.StundenViewModel>();
builder.Services.AddTransient<Views.StundenPage>();
builder.Services.AddTransient<ViewModels.LoginViewModel>();
builder.Services.AddTransient<Views.LoginPage>();
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

@@ -0,0 +1,80 @@
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Repositories;
/// <summary>
/// Standard-Repository, das die bestehende API-/Model-Logik kapselt.
/// </summary>
internal class HoursRepository : IHoursRepository {
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

@@ -0,0 +1,94 @@
using System.Net.Http;
using System.Text;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Services;
internal sealed class AuthService : IAuthService {
private readonly IApiClient _api;
private readonly IAppSettings _settings;
private readonly IAlertService _alerts;
public AuthService(IApiClient api, IAppSettings settings, IAlertService alerts) {
_api = api;
_settings = settings;
_alerts = alerts;
}
public async Task<User> LoginWithCredentials(string username, string password, string serverUrl, CancellationToken ct = default) {
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
throw new Exception("Benutzername und Passwort werden benötigt.");
var apiBase = NormalizeApiUrl(serverUrl);
_settings.ApiUrl = apiBase; // BaseAddress für IApiClient setzen
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("user", username),
new KeyValuePair<string, string>("pass", password)
});
// POST ohne Pfad die API erwartet /appapi
// Wichtig: Basis-URL hat garantiert einen abschließenden Slash (…/appapi/),
// sodass ein leerer Pfad nicht zu Redirects führt (die den POST in GET verwandeln könnten).
var res = await _api.SendAsync<BaseResponse>(HttpMethod.Post, string.Empty, content, null, ct).ConfigureAwait(false);
if (res.user is null)
throw new Exception(res.message ?? "Ungültige Antwort vom Server.");
ApplyUser(res.user, apiBase);
return res.user;
}
public async Task<User> LoginWithToken(string token, CancellationToken ct = default) {
if (string.IsNullOrWhiteSpace(token)) throw new Exception("Kein Token erkannt.");
// QR-Token enthält die URL extrahiere sie
var td = new TokenData(token);
// URL aus dem Token ebenfalls normalisieren, damit sie auf "/appapi/" endet
_settings.ApiUrl = NormalizeApiUrl(td.Url);
_settings.ApiKey = token;
var res = await _api.GetAsync<BaseResponse>(string.Empty, null, ct).ConfigureAwait(false);
if (res.user is null)
throw new Exception(res.message ?? "Ungültige Antwort vom Server.");
ApplyUser(res.user, td.Url);
return res.user;
}
private void ApplyUser(User user, string apiBase) {
_settings.ApiUrl = apiBase;
// Wenn der Server keinen Token im User zurückliefert (QR-Login-Fall), bestehenden Token beibehalten
var tokenToUse = string.IsNullOrWhiteSpace(user.Token) ? _settings.ApiKey : user.Token;
_settings.ApiKey = tokenToUse;
_settings.EmployeeId = user.Id;
_settings.Name = user.Name;
_settings.Surname = user.Surname;
}
private static string NormalizeApiUrl(string input) {
if (string.IsNullOrWhiteSpace(input)) throw new Exception("Server-URL wird benötigt.");
var url = input.Trim();
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) {
url = "https://" + url;
}
// Sicherstellen, dass der Pfad auf "/appapi" endet
if (!url.EndsWith("/appapi", StringComparison.OrdinalIgnoreCase)) {
url = url.TrimEnd('/') + "/appapi";
}
// WICHTIG: Einen abschließenden Slash erzwingen, damit relative Pfade korrekt angehängt werden
// und damit POST auf Basis-URL (leerem Pfad) nicht zu einem 301/302-Redirect führt,
// der den Body (user/pass) verlieren könnte.
//if (!url.EndsWith("/", StringComparison.Ordinal)) {
// url += "/";
//}
if (url.EndsWith("/", StringComparison.Ordinal)) {
url = url.TrimEnd('/');
}
return url;
}
}

View File

@@ -0,0 +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 IHoursValidator _validator;
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}";
var baseRes = await _repo.LoadBase(q);
// 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>> 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<DayTime> GetEntryAsync(int id) => await _repo.LoadEntry(id);
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);
}

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 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

@@ -1,31 +1,149 @@
namespace Jugenddienst_Stunden.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.ViewModels;
/// <summary>
/// Die Loginseite
/// ViewModel für die Loginseite (MVVM)
/// </summary>
public class LoginViewModel {
/// <summary>
/// Name der Anwendung
/// </summary>
public string AppTitle => AppInfo.Name;
public partial class LoginViewModel : ObservableObject {
private readonly IAuthService _auth;
private readonly IAppSettings _settings;
private readonly IAlertService? _alerts;
private DateTime _lastDetectionTime = DateTime.MinValue;
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
/// <summary>
/// Programmversion
/// </summary>
public string Version => AppInfo.VersionString;
public event EventHandler<string>? AlertEvent;
public event EventHandler<string>? InfoEvent;
/// <summary>
/// Kurze Mitteilung für den Anwender
/// </summary>
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// <summary>
/// Name der Anwendung
/// </summary>
public string AppTitle => AppInfo.Name;
/// <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>
/// Programmversion
/// </summary>
public string Version => AppInfo.VersionString;
/// <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");
}
[ObservableProperty]
private string message = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
[ObservableProperty]
private string? server;
[ObservableProperty]
private string? serverLabel;
[ObservableProperty]
private string title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
[ObservableProperty]
private string? username;
[ObservableProperty]
private string? password;
[ObservableProperty]
private bool isManualMode;
[ObservableProperty]
private bool isBusy;
[ObservableProperty]
private bool isDetecting;
// Explizite Command-Property für den QR-Scanner-Event, damit das Binding in XAML zuverlässig greift
public IAsyncRelayCommand<object?> QrDetectedCommand { get; }
public LoginViewModel(IAuthService auth, IAppSettings settings) {
_auth = auth;
_settings = settings;
// gespeicherte Präferenz für Logintyp laden
var lt = Preferences.Default.Get("logintype", "qr");
isManualMode = string.Equals(lt, "manual", StringComparison.OrdinalIgnoreCase);
// Scanner standardmäßig nur im QR-Modus aktivieren
IsDetecting = !isManualMode;
// Serveranzeige vorbereiten
var apiUrl = Preferences.Default.Get("apiUrl", string.Empty);
if (!string.IsNullOrWhiteSpace(apiUrl)) {
Server = apiUrl.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
ServerLabel = "Server: " + Server;
}
// Command initialisieren
QrDetectedCommand = new AsyncRelayCommand<object?>(QrDetectedAsync);
}
// DI-Konstruktor: AlertService anbinden und Alerts an VM-Event weiterreichen (analog StundeViewModel)
internal LoginViewModel(IAuthService auth, IAppSettings settings, IAlertService alertService) : this(auth, settings) {
_alerts = alertService;
if (alertService is not null) {
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
}
}
partial void OnIsManualModeChanged(bool value) {
Preferences.Default.Set("logintype", value ? "manual" : "qr");
// Scanner nur aktiv, wenn QR-Modus aktiv ist
IsDetecting = !value;
}
[RelayCommand]
private async Task LoginAsync() {
if (IsBusy) return;
try {
IsBusy = true;
var user = await _auth.LoginWithCredentials(Username?.Trim() ?? string.Empty,
Password ?? string.Empty,
(Server ?? string.Empty).Trim());
Title = $"{user.Name} {user.Surname}";
InfoEvent?.Invoke(this, "Login erfolgreich");
await Shell.Current.GoToAsync("//StundenPage");
} catch (Exception ex) {
if (_alerts is not null) {
_alerts.Raise(ex.Message);
} else {
AlertEvent?.Invoke(this, ex.Message);
}
} finally {
IsBusy = false;
}
}
private async Task QrDetectedAsync(object? args) {
var now = DateTime.Now;
if ((now - _lastDetectionTime) <= _detectionInterval) return;
_lastDetectionTime = now;
try {
var token = ExtractFirstBarcodeValue(args);
if (string.IsNullOrWhiteSpace(token)) return;
var user = await _auth.LoginWithToken(token);
Title = $"{user.Name} {user.Surname}";
await Shell.Current.GoToAsync("//StundenPage");
} catch (Exception ex) {
if (_alerts is not null) {
_alerts.Raise(ex.Message);
} else {
AlertEvent?.Invoke(this, ex.Message);
}
}
}
private static string? ExtractFirstBarcodeValue(object? args) {
try {
if (args is ZXing.Net.Maui.BarcodeDetectionEventArgs e && e.Results is not null) {
return e.Results.FirstOrDefault()?.Value;
}
} catch { }
return null;
}
}

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

@@ -5,243 +5,275 @@ using Jugenddienst_Stunden.Types;
using System.ComponentModel;
using System.Windows.Input;
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;
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() {
SaveCommand = new AsyncRelayCommand(Save);
//DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
public StundeViewModel() : this(GetServiceOrCreate()) {
LoadSettingsAsync();
}
public StundeViewModel(DayTime stunde) {
SaveCommand = new AsyncRelayCommand(Save);
DeleteConfirmCommand = new AsyncRelayCommand(DeleteConfirm);
}
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);
}
private async void LoadSettingsAsync() {
try {
Settings = await HoursBase.LoadSettings();
GlobalVar.Settings = Settings;
internal StundeViewModel(IHoursService hoursService) {
_hoursService = hoursService;
SaveCommand = new AsyncRelayCommand(Save);
//DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
OptionsGemeinde = Settings.Gemeinden;
OptionsProjekt = Settings.Projekte;
OptionsFreistellung = Settings.Freistellungen;
// 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);
}
}
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
private async void LoadSettingsAsync() {
try {
Settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = Settings;
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
OptionsGemeinde = Settings.Gemeinden;
OptionsProjekt = Settings.Projekte;
OptionsFreistellung = Settings.Freistellungen;
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 HoursBase.SaveEntry(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")}");
}
}
}
}
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
/// <summary>
/// Löschen ohne Bestätigung
/// </summary>
private async Task Delete() {
await HoursBase.DeleteEntry(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
async Task Save() {
bool exceptionOccurred = false;
bool proceed = true;
//Arbeitszeit sollte nicht null sein
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
proceed = false;
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
}
//Projekt ist ein Pflichtfeld
if (Settings.ProjektAktivSet && DayTime.ProjektAktiv.Id == 0) {
proceed = false;
AlertEvent?.Invoke(this, "Projekt darf nicht leer sein");
}
//Gemeinde ist ein Pflichtfeld
if (Settings.GemeindeAktivSet && DayTime.GemeindeAktiv.Id == 0) {
proceed = false;
AlertEvent?.Invoke(this, "Gemeinde darf nicht leer sein");
}
/// <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 HoursBase.DeleteEntry(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} else {
//nicht Löschen
}
}
}
if (proceed) {
try {
await _hoursService.SaveEntryAsync(DayTime);
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
exceptionOccurred = true;
}
/// <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")) {
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")}");
}
}
}
}
//DateTime heute = DateTime.Now;
try {
//_dayTime = await HoursBase.LoadEntry(Convert.ToInt32(query["load"]));
BaseResponse dat = await HoursBase.LoadBase("id=" + Convert.ToInt32(query["load"]));
GlobalVar.Settings = dat.settings;
GemeindeAktivSet = dat.settings.GemeindeAktivSet;
ProjektAktivSet = dat.settings.ProjektAktivSet;
/// <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")}");
}
DayTime = dat.daytime;
DayTime.TimeSpanVon = dat.daytime.Begin.ToTimeSpan();
DayTime.TimeSpanBis = dat.daytime.End.ToTimeSpan();
/// <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
}
}
}
OptionsGemeinde = dat.settings.Gemeinden ?? new List<Gemeinde>();
OptionsProjekt = dat.settings.Projekte ?? new List<Projekt>();
OptionsFreistellung = dat.settings.Freistellungen ?? new List<Freistellung>();
/// <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;
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();
DayTime = entry;
DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
DayTime.TimeSpanBis = entry.End.ToTimeSpan();
//Evtl. noch die anderen Zeiten des gleichen Tages holen
BaseResponse dat1 = await HoursBase.LoadBase("date=" + DayTime.Day.ToString("yyyy-MM-dd"));
DayTimes = dat1.daytimes;
OnPropertyChanged(nameof(DayTime));
OnPropertyChanged(nameof(DayTimes));
// OptionsGemeinde = settings.Gemeinden ?? new List<Gemeinde>();
// OptionsProjekt = settings.Projekte ?? new List<Projekt>();
// OptionsFreistellung = settings.Freistellungen ?? new List<Freistellung>();
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
} finally {
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();
}
//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");
}
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle));
if (System.String.IsNullOrEmpty(DayTime.Description)) {
InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext");
}
FreistellungEnabled = !DayTime.Approved;
//OnPropertyChanged(nameof(DayTime));
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle));
} else if (query.ContainsKey("date")) {
Title = "Neuer Eintrag";
OnPropertyChanged(nameof(Title));
FreistellungEnabled = !DayTime.Approved;
//OnPropertyChanged(nameof(DayTime));
} 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 {
//DayTimes = await HoursBase.LoadDay(_date);
BaseResponse dat = await HoursBase.LoadBase("date=" + _date.ToString("yyyy-MM-dd"));
GlobalVar.Settings = dat.settings;
DayTimes = dat.daytimes;
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 = dat.settings.Gemeinden;
OptionsProjekt = dat.settings.Projekte;
OptionsFreistellung = dat.settings.Freistellungen;
OptionsGemeinde = settings.Gemeinden;
OptionsProjekt = settings.Projekte;
OptionsFreistellung = settings.Freistellungen;
GemeindeAktivSet = dat.settings.GemeindeAktivSet;
ProjektAktivSet = dat.settings.ProjektAktivSet;
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();
} 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;
SubTitle = _date.ToString("dddd, d. MMMM yyyy");
FreistellungEnabled = true;
OnPropertyChanged(nameof(SubTitle));
//OnPropertyChanged(nameof(DayTime));
}
}
}
}
OnPropertyChanged(nameof(SubTitle));
//OnPropertyChanged(nameof(DayTime));
}
}
}
}

View File

@@ -5,316 +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;
namespace Jugenddienst_Stunden.ViewModels;
/// <summary>
/// ViewModel für die Stundenliste
/// </summary>
internal partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged {
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() {
Hours = new Hours();
LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
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());
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 {
BaseResponse dat = await HoursBase.LoadBase("hours&year=" + DateToday.ToString("yyyy") + "&month=" + DateToday.ToString("MM"));
Hours = dat.hour;
Settings = dat.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 {
//_dayTimes = await HoursBase.LoadDay(date);
BaseResponse dat = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd"));
/// <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 = dat.daytimes;
Settings = dat.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) {
BaseResponse dat1 = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd") + "&tilldate=" + date.AddDays(3).ToString("yyyy-MM-dd"));
if (dat1.daytimes != null)
DayTimes = dat.daytimes.Concat(dat1.daytimes).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"
@@ -9,19 +10,19 @@
Title="{Binding Title}">
<ContentPage.BindingContext>
<models:LoginViewModel />
</ContentPage.BindingContext>
<!-- BindingContext wird via DI im Code-Behind gesetzt -->
<ContentPage.Resources>
<ResourceDictionary>
<conv:StringVisibilityConverter x:Key="StringVisibilityConverter" />
<conv:InverseBoolConverter x:Key="InverseBoolConverter" />
<conv:EventArgsPassThroughConverter x:Key="EventArgsPassThroughConverter" />
</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>
@@ -29,40 +30,48 @@
<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="{Binding IsManualMode}" 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 ServerLabel}"
IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0">
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0" IsVisible="{Binding IsManualMode, Converter={StaticResource InverseBoolConverter}}">
<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"
VerticalOptions="FillAndExpand"
IsDetecting="{Binding IsDetecting}">
<zxing:CameraBarcodeReaderView.Behaviors>
<toolkit:EventToCommandBehavior EventName="BarcodesDetected"
Command="{Binding QrDetectedCommand}"
EventArgsConverter="{StaticResource EventArgsPassThroughConverter}" />
</zxing:CameraBarcodeReaderView.Behaviors>
</zxing:CameraBarcodeReaderView>
</Border>
</VerticalStackLayout>
<VerticalStackLayout x:Name="LoginManual" Spacing="25">
<VerticalStackLayout x:Name="LoginManual" Spacing="25" IsVisible="{Binding IsManualMode}">
<Label Text="Manueller Login" FontSize="32" HorizontalOptions="Start" Margin="0, 20, 0, 0" />
<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}}" />
<Entry x:Name="UsernameEntry" Text="{Binding Username}" Placeholder="Benutzername (Mailadresse)" Keyboard="Email" />
<Entry x:Name="PasswordEntry" Text="{Binding Password}" Placeholder="Passwort" IsPassword="True" />
<Entry x:Name="ServerEntry" Text="{Binding Server}" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" />
<Button Text="Login" Command="{Binding LoginCommand}"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
</VerticalStackLayout>
</VerticalStackLayout>

View File

@@ -1,5 +1,6 @@
using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
using Jugenddienst_Stunden.ViewModels;
using ZXing.Net.Maui;
@@ -9,197 +10,197 @@ 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
};
// BindingContext via DI beziehen, falls nicht bereits gesetzt
try {
if (BindingContext is null) {
var sp = Application.Current?.Handler?.MauiContext?.Services
?? throw new InvalidOperationException("DI container ist nicht verfügbar.");
BindingContext = sp.GetRequiredService<LoginViewModel>();
}
} catch (Exception) {
// Ignorieren: Fallback bleibt leerer BindingContext
}
//if (BindingContext is LoginViewModel vm) {
// vm.AlertEvent += Vm_AlertEvent;
// vm.InfoEvent += Vm_InfoEvent;
// vm.MsgEvent += Vm_MsgEvent;
//}
if (BindingContext is LoginViewModel vm) {
vm.AlertEvent += async (_, msg) => await DisplayAlert("Fehler:", msg, "OK");
vm.InfoEvent += async (_, msg) => await DisplayAlert("Information:", msg, "OK");
}
//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;
}
barcodeScannerView.Options =
new BarcodeReaderOptions { Formats = BarcodeFormat.QrCode, AutoRotate = true, Multiple = false };
// Fallback-Verkabelung: Falls das EventToCommandBehavior in XAML nicht greift,
// leiten wir das Kamera-Event manuell an das ViewModel-Command weiter.
barcodeScannerView.BarcodesDetected += (s, e) => {
if (BindingContext is LoginViewModel vm && vm.QrDetectedCommand is not null) {
// Sicherstellen, dass die Command-Ausführung im UI-Thread erfolgt
MainThread.BeginInvokeOnMainThread(async () => {
if (vm.QrDetectedCommand.CanExecute(e)) {
await vm.QrDetectedCommand.ExecuteAsync(e);
}
});
}
};
//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
// MVVM übernimmt Umschalten über IsManualMode im ViewModel; keine Code-Behind-Umschaltung mehr
}
/// <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;
// IsDetecting wird via Binding vom ViewModel gesteuert
}
}
protected override void OnAppearing() {
base.OnAppearing();
protected override void OnDisappearing() {
base.OnDisappearing();
// IsDetecting wird via Binding vom ViewModel gesteuert
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
// Umschalt-Logik erfolgt über Binding an IsManualMode im ViewModel
//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,48 +59,50 @@
<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="*,*,*">
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}" SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" Grid.Column="0" IsVisible="{Binding GemeindeAktivSet}">
</Picker>
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}" SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" Grid.Column="1" IsVisible="{Binding ProjektAktivSet}">
</Picker>
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}" SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" Grid.Column="2" IsEnabled="{Binding FreistellungEnabled}">
</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 +114,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 +138,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));
}
}