diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2555e46 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/Jugenddienst Stunden/App.xaml b/Jugenddienst Stunden/App.xaml index 2e056f0..d007323 100644 --- a/Jugenddienst Stunden/App.xaml +++ b/Jugenddienst Stunden/App.xaml @@ -1,4 +1,5 @@ - + + - + \ No newline at end of file diff --git a/Jugenddienst Stunden/App.xaml.cs b/Jugenddienst Stunden/App.xaml.cs index 533f09a..2660261 100644 --- a/Jugenddienst Stunden/App.xaml.cs +++ b/Jugenddienst Stunden/App.xaml.cs @@ -4,12 +4,11 @@ /// Die Hauptanwendungsklasse. /// public partial class App : Application { - - /// - /// Initialisiert eine neue Instanz der -Klasse. - /// - public App() { - InitializeComponent(); - MainPage = new AppShell(); - } -} + /// + /// Initialisiert eine neue Instanz der -Klasse. + /// + public App() { + InitializeComponent(); + MainPage = new AppShell(); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/AppShell.xaml b/Jugenddienst Stunden/AppShell.xaml index bc39da4..090eb41 100644 --- a/Jugenddienst Stunden/AppShell.xaml +++ b/Jugenddienst Stunden/AppShell.xaml @@ -1,4 +1,5 @@ - + + - + Icon="{OnPlatform 'icon_watch.png', iOS='icon_watch_ios.png', MacCatalyst='icon_watch_ios.png'}" + Route="StundenPage" /> + /// AppShell.xaml.cs /// @@ -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)); } - - -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Converter/CollectionVisibilityConverter.cs b/Jugenddienst Stunden/Converter/CollectionVisibilityConverter.cs index 7b7993d..be367c0 100644 --- a/Jugenddienst Stunden/Converter/CollectionVisibilityConverter.cs +++ b/Jugenddienst Stunden/Converter/CollectionVisibilityConverter.cs @@ -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 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 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(); + } + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Converter/IntBoolConverter.cs b/Jugenddienst Stunden/Converter/IntBoolConverter.cs index 7565e43..340eea4 100644 --- a/Jugenddienst Stunden/Converter/IntBoolConverter.cs +++ b/Jugenddienst Stunden/Converter/IntBoolConverter.cs @@ -1,11 +1,11 @@ using System.Globalization; namespace Jugenddienst_Stunden.Converter; + /// /// Falls ein int als bool dargestellt werden soll /// public class IntBoolConverter : IValueConverter { - /// /// Konvertiert einen int in einen bool /// @@ -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; } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Converter/SecondsTimeConverter.cs b/Jugenddienst Stunden/Converter/SecondsTimeConverter.cs index 3741302..3c1d4d0 100644 --- a/Jugenddienst Stunden/Converter/SecondsTimeConverter.cs +++ b/Jugenddienst Stunden/Converter/SecondsTimeConverter.cs @@ -1,36 +1,37 @@ using System.Globalization; namespace Jugenddienst_Stunden.Converter; + internal class SecondsTimeConverter : IValueConverter { + private int seconds; - private int seconds; + /// + /// Konvertiert eine Sekundenangabe nach Stunden:Minuten, auch bei mehr als 24 Stunden + /// + 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; + } - /// - /// Konvertiert eine Sekundenangabe nach Stunden:Minuten, auch bei mehr als 24 Stunden - /// - 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(); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Converter/StringVisibilityConverter.cs b/Jugenddienst Stunden/Converter/StringVisibilityConverter.cs index 6ff8235..b3a836a 100644 --- a/Jugenddienst Stunden/Converter/StringVisibilityConverter.cs +++ b/Jugenddienst Stunden/Converter/StringVisibilityConverter.cs @@ -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(); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Exceptions/NoDataException.cs b/Jugenddienst Stunden/Exceptions/NoDataException.cs index 7235652..5393746 100644 --- a/Jugenddienst Stunden/Exceptions/NoDataException.cs +++ b/Jugenddienst Stunden/Exceptions/NoDataException.cs @@ -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) { + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/AlertService.cs b/Jugenddienst Stunden/Infrastructure/AlertService.cs new file mode 100644 index 0000000..713eafa --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/AlertService.cs @@ -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? AlertRaised; + public void Raise(string message) { + AlertRaised?.Invoke(this, message); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/ApiClient.cs b/Jugenddienst Stunden/Infrastructure/ApiClient.cs new file mode 100644 index 0000000..ee87040 --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/ApiClient.cs @@ -0,0 +1,111 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Net.Http.Json; +using Jugenddienst_Stunden.Interfaces; + +namespace Jugenddienst_Stunden.Infrastructure; + +internal sealed class ApiClient : IApiClient { + private readonly HttpClient _http; + private readonly JsonSerializerOptions _json; + private readonly ApiOptions _options; + private readonly IAppSettings _settings; + + public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) { + _http = http; + _options = options; + _settings = settings; + + _http.Timeout = options.Timeout; + if (!_http.DefaultRequestHeaders.Accept.Any()) + _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var token = tokenProvider.GetToken(); + if (!string.IsNullOrWhiteSpace(token)) + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + _json = new JsonSerializerOptions { + PropertyNameCaseInsensitive = true, + WriteIndented = false, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + + // Globale Converter: erlauben numerische Felder auch als Strings (z.B. user.id) + _json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleInt32Converter()); + _json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleNullableInt32Converter()); + + // Stelle sicher, dass die BaseAddress sofort aus den aktuellen Settings (Preferences) gesetzt wird + // und nicht erst beim ersten Request. Dadurch steht die ApiUrl ab Initialisierung zur Verfügung. + EnsureBaseAddress(); + } + + public Task GetAsync(string path, IDictionary? query = null, CancellationToken ct = default) + => SendAsync(HttpMethod.Get, path, null, query, ct); + + public async Task SendAsync(HttpMethod method, string path, object? body = null, + IDictionary? query = null, CancellationToken ct = default) { + // Vor jedem Request sicherstellen, dass die (ggf. geänderte) BaseAddress gesetzt ist + EnsureBaseAddress(); + var uri = BuildUri(path, query); + using var req = new HttpRequestMessage(method, uri) { + Content = body is null ? null : JsonContent.Create(body, options: _json) + }; + + using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); + var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + if (!res.IsSuccessStatusCode) + throw ApiException.From(res.StatusCode, TryGetMessage(text), text); + + if (typeof(T) == typeof(void) || typeof(T) == typeof(object) || string.IsNullOrWhiteSpace(text)) + return default!; + + var obj = System.Text.Json.JsonSerializer.Deserialize(text, _json); + if (obj is null) + throw new ApiException("Fehler beim Deserialisieren der Daten."); + return obj; + } + + public Task DeleteAsync(string path, IDictionary? query = null, CancellationToken ct = default) + => SendAsync(HttpMethod.Delete, path, null, query, ct); + + private void EnsureBaseAddress() { + var baseUrl = _settings.ApiUrl; + + if (string.IsNullOrWhiteSpace(baseUrl)) { + throw new InvalidOperationException( + "ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl'), " + + "z.B. im Login/Setup, bevor API-Aufrufe stattfinden." + ); + } + + // nur setzen, wenn nötig (damit spätere Änderungen nach Login greifen) + if (_http.BaseAddress is null || !Uri.Equals(_http.BaseAddress, new Uri(baseUrl, UriKind.Absolute))) { + _http.BaseAddress = new Uri(baseUrl, UriKind.Absolute); + _http.Timeout = _options.Timeout; + } + } + + private static string TryGetMessage(string text) { + try { + using var doc = JsonDocument.Parse(text); + if (doc.RootElement.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String) + return m.GetString() ?? text; + } catch { + } + + return text; + } + + private static Uri BuildUri(string path, IDictionary? query) { + if (query is null || query.Count == 0) + return new Uri(path, UriKind.Relative); + + var sb = new StringBuilder(path); + sb.Append(path.Contains('?') ? '&' : '?'); + sb.Append(string.Join('&', query + .Where(kv => kv.Value is not null) + .Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}"))); + return new Uri(sb.ToString(), UriKind.Relative); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/ApiOptions.cs b/Jugenddienst Stunden/Infrastructure/ApiOptions.cs new file mode 100644 index 0000000..64364f2 --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/ApiOptions.cs @@ -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); +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/Exceptions.cs b/Jugenddienst Stunden/Infrastructure/Exceptions.cs new file mode 100644 index 0000000..a8ac936 --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/Exceptions.cs @@ -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) { + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/NullApiClient.cs b/Jugenddienst Stunden/Infrastructure/NullApiClient.cs new file mode 100644 index 0000000..30c5254 --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/NullApiClient.cs @@ -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 GetAsync(string path, IDictionary? query = null, CancellationToken ct = default) + => Task.FromException(new InvalidOperationException(_message)); + + public Task SendAsync(HttpMethod method, string path, object? body = null, + IDictionary? query = null, CancellationToken ct = default) + => Task.FromException(new InvalidOperationException(_message)); + + public Task DeleteAsync(string path, IDictionary? query = null, CancellationToken ct = default) + => Task.FromException(new InvalidOperationException(_message)); +} diff --git a/Jugenddienst Stunden/Infrastructure/PreferencesAppSettings.cs b/Jugenddienst Stunden/Infrastructure/PreferencesAppSettings.cs new file mode 100644 index 0000000..4021953 --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/PreferencesAppSettings.cs @@ -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); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/SettingsTokenProvider.cs b/Jugenddienst Stunden/Infrastructure/SettingsTokenProvider.cs new file mode 100644 index 0000000..2b2bbe9 --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/SettingsTokenProvider.cs @@ -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; +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/TokenProvider.cs b/Jugenddienst Stunden/Infrastructure/TokenProvider.cs new file mode 100644 index 0000000..e4b5e5a --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/TokenProvider.cs @@ -0,0 +1,7 @@ +using Jugenddienst_Stunden.Interfaces; + +namespace Jugenddienst_Stunden.Infrastructure; + +internal sealed class GlobalVarTokenProvider : ITokenProvider { + public string? GetToken() => Models.GlobalVar.ApiKey; +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Interfaces/IAlertService.cs b/Jugenddienst Stunden/Interfaces/IAlertService.cs new file mode 100644 index 0000000..3cdaf7b --- /dev/null +++ b/Jugenddienst Stunden/Interfaces/IAlertService.cs @@ -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 AlertRaised; + void Raise(string message); +} diff --git a/Jugenddienst Stunden/Interfaces/IApiClient.cs b/Jugenddienst Stunden/Interfaces/IApiClient.cs new file mode 100644 index 0000000..8821d06 --- /dev/null +++ b/Jugenddienst Stunden/Interfaces/IApiClient.cs @@ -0,0 +1,10 @@ +namespace Jugenddienst_Stunden.Interfaces; + +internal interface IApiClient { + Task GetAsync(string path, IDictionary? query = null, CancellationToken ct = default); + + Task SendAsync(HttpMethod method, string path, object? body = null, + IDictionary? query = null, CancellationToken ct = default); + + Task DeleteAsync(string path, IDictionary? query = null, CancellationToken ct = default); +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Interfaces/IAppSettings.cs b/Jugenddienst Stunden/Interfaces/IAppSettings.cs new file mode 100644 index 0000000..0c21101 --- /dev/null +++ b/Jugenddienst Stunden/Interfaces/IAppSettings.cs @@ -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; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Interfaces/IHoursRepository.cs b/Jugenddienst Stunden/Interfaces/IHoursRepository.cs index c1df1ed..01d06da 100644 --- a/Jugenddienst Stunden/Interfaces/IHoursRepository.cs +++ b/Jugenddienst Stunden/Interfaces/IHoursRepository.cs @@ -6,12 +6,12 @@ namespace Jugenddienst_Stunden.Interfaces; /// Repository‑Schnittstelle für Datenzugriff (API/Storage) rund um Stunden. /// internal interface IHoursRepository { - Task LoadBase(string query); - Task LoadSettings(); - Task LoadData(); - Task LoadUser(string apiKey); - Task> LoadDay(DateTime date); - Task LoadEntry(int id); - Task SaveEntry(DayTime stunde); - Task DeleteEntry(DayTime stunde); -} + Task LoadBase(string query); + Task LoadSettings(); + Task LoadData(); + Task LoadUser(string apiKey); + Task> LoadDay(DateTime date); + Task LoadEntry(int id); + Task SaveEntry(DayTime stunde); + Task DeleteEntry(DayTime stunde); +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Interfaces/IHoursService.cs b/Jugenddienst Stunden/Interfaces/IHoursService.cs index a24f2bc..7f72f91 100644 --- a/Jugenddienst Stunden/Interfaces/IHoursService.cs +++ b/Jugenddienst Stunden/Interfaces/IHoursService.cs @@ -5,12 +5,12 @@ namespace Jugenddienst_Stunden.Interfaces; /// /// Fachlicher Service für Stunden – konsumiert Repository und stellt VM‑freundliche Methoden bereit. /// -internal interface IHoursService { - Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate); - Task<(List dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date); - Task> GetDayRangeAsync(DateTime from, DateTime to); - Task GetSettingsAsync(); - Task GetEntryAsync(int id); - Task SaveEntryAsync(DayTime stunde); - Task DeleteEntryAsync(DayTime stunde); -} +public interface IHoursService { + Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate); + Task<(List dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date); + Task> GetDayRangeAsync(DateTime from, DateTime to); + Task GetSettingsAsync(); + Task GetEntryAsync(int id); + Task SaveEntryAsync(DayTime stunde); + Task DeleteEntryAsync(DayTime stunde); +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Interfaces/ITokenProvider.cs b/Jugenddienst Stunden/Interfaces/ITokenProvider.cs new file mode 100644 index 0000000..81fbe88 --- /dev/null +++ b/Jugenddienst Stunden/Interfaces/ITokenProvider.cs @@ -0,0 +1,5 @@ +namespace Jugenddienst_Stunden.Interfaces; + +internal interface ITokenProvider { + string? GetToken(); +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Jugenddienst Stunden.csproj b/Jugenddienst Stunden/Jugenddienst Stunden.csproj index f1d31de..1988f2e 100644 --- a/Jugenddienst Stunden/Jugenddienst Stunden.csproj +++ b/Jugenddienst Stunden/Jugenddienst Stunden.csproj @@ -1,312 +1,312 @@  - - - net9.0-android35.0 - - + + + net9.0-android35.0 + + - - + + - Exe - Jugenddienst_Stunden - true - true - enable - enable + Exe + Jugenddienst_Stunden + true + true + enable + enable - - Jugenddienst Stunden + + Jugenddienst Stunden - - com.companyname.jugenddienststunden + + com.companyname.jugenddienststunden - - 1.0.9 - 10 + + 1.0.9 + 10 - 11.0 - 13.1 - 29.0 - 10.0.17763.0 - 10.0.17763.0 - 6.5 - True - paket_icon.png - de - 1.0.9 - + 11.0 + 13.1 + 29.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + True + paket_icon.png + de + 1.0.9 + - - com.companyname.jugenddienststunden - 1.0.9 - 10 - False - True - + + com.companyname.jugenddienststunden + 1.0.9 + 10 + False + True + - - True - com.companyname.jugenddienststunden - 1.0.9 - 10 - True - + + True + com.companyname.jugenddienststunden + 1.0.9 + 10 + True + - - com.companyname.jugenddienststunden - Xamarin - True - False - True - 1.0.9 - 10 - False - + + com.companyname.jugenddienststunden + Xamarin + True + False + True + 1.0.9 + 10 + False + - - de-de - + + de-de + - - com.companyname.jugenddienststunden - 1.0.9 - 10 - $(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION - True - False - + + com.companyname.jugenddienststunden + 1.0.9 + 10 + $(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION + True + False + - - com.companyname.jugenddienststunden - Xamarin - False - True - True - 1.0.9 - 10 - False - True - False - + + com.companyname.jugenddienststunden + Xamarin + False + True + True + 1.0.9 + 10 + False + True + False + - - com.companyname.jugenddienststunden - 1.0.9 - 10 - True - True - + + com.companyname.jugenddienststunden + 1.0.9 + 10 + True + True + - - True - snupkg - AnyCPU - False - 1.0.9 - 1.0.9 - + + True + snupkg + AnyCPU + False + 1.0.9 + 1.0.9 + - - 1.0.9 - 10 - + + 1.0.9 + 10 + - - 1.0.9 - 10 - True - + + 1.0.9 + 10 + True + - - 10 - $(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION - 1.0.9 - + + 10 + $(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION + 1.0.9 + - - 10 - 1.0.9 - + + 10 + 1.0.9 + - - 8 - 1.0.9 - 10 - True - + + 8 + 1.0.9 + 10 + True + - - 8 - 1.0.9 - 10 - True - False - False - + + 8 + 1.0.9 + 10 + True + False + False + - - 8 - + + 8 + - - 8 - + + 8 + - - 8 - 1701;1702 - $(WarningsAsErrors);NU1605 - 1.0.9 - 10 - + + 8 + 1701;1702 + $(WarningsAsErrors);NU1605 + 1.0.9 + 10 + - - 8 - 1701;1702 - $(WarningsAsErrors);NU1605 - 1.0.9 - 10 - + + 8 + 1701;1702 + $(WarningsAsErrors);NU1605 + 1.0.9 + 10 + - - $(TargetFrameworks);net9.0-windows10.0.26100.0 - None - - + + $(TargetFrameworks);net9.0-windows10.0.26100.0 + None + + - - - + + + - - - - - + + - - - + + - - + + + - - - + + - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - True - \ - - + + + + + + + + + + + + + + + + + + + + + + - - - - - true - - - - - - - - - + + + True + \ + + - - - True - True - Resources.resx - - - LoginPage.xaml - - - StundePage.xaml - - + + + + + true + + + + + + + + + - - - ResXFileCodeGenerator - Resources.Designer.cs - - + + + True + True + Resources.resx + + + LoginPage.xaml + + + StundePage.xaml + + - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + diff --git a/Jugenddienst Stunden/MauiProgram.cs b/Jugenddienst Stunden/MauiProgram.cs index b432206..bc8bc6c 100644 --- a/Jugenddienst Stunden/MauiProgram.cs +++ b/Jugenddienst Stunden/MauiProgram.cs @@ -3,8 +3,11 @@ using Jugenddienst_Stunden.Models; using Jugenddienst_Stunden.Interfaces; using Jugenddienst_Stunden.Repositories; using Jugenddienst_Stunden.Services; +using Jugenddienst_Stunden.Infrastructure; +using Jugenddienst_Stunden.Validators; using Microsoft.Extensions.Logging; using ZXing.Net.Maui.Controls; +using System.Net.Http; namespace Jugenddienst_Stunden; @@ -12,39 +15,84 @@ namespace Jugenddienst_Stunden; /// Das Hauptprogramm. /// public static class MauiProgram { + public static MauiApp CreateMauiApp() { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + // 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() - // Initialize the .NET MAUI Community Toolkit by adding the below line of code - .UseMauiCommunityToolkit(options => { - options.SetShouldEnableSnackbarOnWindows(true); - }) - .ConfigureFonts(fonts => { - fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); - fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); - }) - //.UseBarcodeScanning(); - .UseBarcodeReader(); + //#if DEBUG + // if (string.IsNullOrWhiteSpace(GlobalVar.ApiKey)) { + // GlobalVar.ApiKey = Preferences.Default.Get("apiKey", + // "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk="); + // GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell"); + // GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi"); + // GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141); + // GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi"); + // } -#if DEBUG - if (GlobalVar.ApiKey == null) { - GlobalVar.ApiKey = Preferences.Default.Get("apiKey", "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk="); - GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell"); - GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi"); - GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141); - GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi"); - } - builder.Logging.AddDebug(); -#endif + // builder.Logging.AddDebug(); + //#endif - // DI: Services & Repositories - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // DI: AlertService für globale Alerts (z. B. leere ApiUrl) + builder.Services.AddSingleton(); - return builder.Build(); - } + // DI: Settings aus Preferences (Single Source of Truth bleibt Preferences) + builder.Services.AddSingleton(); + // DI: ApiOptions IMMER aus aktuellen Settings erzeugen (nicht beim Start einfrieren) + builder.Services.AddTransient(sp => new ApiOptions { + BaseUrl = sp.GetRequiredService().ApiUrl, Timeout = TimeSpan.FromSeconds(15) + }); -} + // Token Provider soll ebenfalls aus Settings/Preferences lesen + builder.Services.AddSingleton(); + + // HttpClient + ApiClient + builder.Services.AddSingleton(_ => new HttpClient()); + builder.Services.AddSingleton(sp => { + var alert = sp.GetRequiredService(); + try { + return new ApiClient( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService()); + } 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(); + //builder.Services.AddSingleton(_ => new HttpClient()); + //builder.Services.AddSingleton(sp => new ApiClient( + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // sp.GetRequiredService())); + + // DI: Validatoren + builder.Services.AddSingleton(); + + // DI: Services & Repositories + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // DI: Views/ViewModels + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + return builder.Build(); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/BaseFunc.cs b/Jugenddienst Stunden/Models/BaseFunc.cs index 2f87bfa..67d2a60 100644 --- a/Jugenddienst Stunden/Models/BaseFunc.cs +++ b/Jugenddienst Stunden/Models/BaseFunc.cs @@ -7,186 +7,175 @@ using System.Text.Json; namespace Jugenddienst_Stunden.Models; internal static class BaseFunc { + internal static async Task 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 GetApiDataWithAuthAsync(string url, string token) { + internal static async Task AuthUserPass(string user, string pass, string url) { + var values = new Dictionary { { "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(responseData) ?? + throw new Exception("Fehler beim Deserialisieren der Daten"); + //User userData = System.Text.Json.JsonSerializer.Deserialize(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; + } + /// + /// Notiz laden + /// + 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 AuthUserPass(string user, string pass, string url) { + return + new() { Date = File.GetLastWriteTime(filename) }; + } - var values = new Dictionary - { - { "user", user }, - { "pass", pass } - }; + /// + /// Stundeneintrag speichern + /// + 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(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); - //User userData = System.Text.Json.JsonSerializer.Deserialize(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); - /// - /// Notiz laden - /// - internal static Note Load(string filename) { - filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename); + //string json = JsonSerializer.Serialize(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); + } + } + } - /// - /// Stundeneintrag speichern - /// - internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) { + /// + /// Stundeneintrag löschen + /// + 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(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); - } - } - - } - - /// - /// Stundeneintrag löschen - /// - 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); + } + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/GlobalVar.cs b/Jugenddienst Stunden/Models/GlobalVar.cs index 463d0d4..cf1fd1c 100644 --- a/Jugenddienst Stunden/Models/GlobalVar.cs +++ b/Jugenddienst Stunden/Models/GlobalVar.cs @@ -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; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/HoursBase.cs b/Jugenddienst Stunden/Models/HoursBase.cs index b6c515b..8b091e7 100644 --- a/Jugenddienst Stunden/Models/HoursBase.cs +++ b/Jugenddienst Stunden/Models/HoursBase.cs @@ -4,83 +4,90 @@ using Newtonsoft.Json; namespace Jugenddienst_Stunden.Models; internal static class HoursBase { + /// + /// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id= + /// + /// Entire response + internal static async Task LoadBase(string what) { + string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?" + what, GlobalVar.ApiKey); + BaseResponse res = JsonConvert.DeserializeObject(data) ?? + throw new Exception("Fehler beim Deserialisieren der Daten"); + return res; + } - /// - /// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id= - /// - /// Entire response - internal static async Task LoadBase(string what) { - string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?"+what, GlobalVar.ApiKey); - BaseResponse res = JsonConvert.DeserializeObject(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); - return res; - } + /// + /// Einstellungen laden + /// + /// Settings only + internal static async Task LoadSettings() { + string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey); + BaseResponse res = JsonConvert.DeserializeObject(data) ?? + throw new Exception("Fehler beim Deserialisieren der Daten"); + return res.settings; + } - /// - /// Einstellungen laden - /// - /// Settings only - internal static async Task LoadSettings() { - string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey); - BaseResponse res = JsonConvert.DeserializeObject(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); - return res.settings; - } + /// + /// Daten laden + /// + /// Hours only + internal static async Task LoadData() { + string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey); + BaseResponse res = JsonConvert.DeserializeObject(data) ?? + throw new Exception("Fehler beim Deserialisieren der Daten"); + return res.hour; + } - /// - /// Daten laden - /// - /// Hours only - internal static async Task LoadData() { - string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey); - BaseResponse res = JsonConvert.DeserializeObject(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); - return res.hour; - } + /// + /// Benutzerdaten laden + /// + /// User-Object + public static async Task LoadUser(string apiKey) { + string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey); + BaseResponse res = JsonConvert.DeserializeObject(data) ?? + throw new Exception("Fehler beim Deserialisieren der Daten"); + return res.user; + } - /// - /// Benutzerdaten laden - /// - /// User-Object - public static async Task LoadUser(string apiKey) { - string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey); - BaseResponse res = JsonConvert.DeserializeObject(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); - return res.user; - } + /// + /// Zeiten eines Tages holen + /// + internal static async Task> LoadDay(DateTime date) { + string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"), + GlobalVar.ApiKey); + BaseResponse res = JsonConvert.DeserializeObject(data) ?? + throw new Exception("Fehler beim Deserialisieren der Daten"); + return res.daytimes; + } - /// - /// Zeiten eines Tages holen - /// - internal static async Task> LoadDay(DateTime date) { - string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"), GlobalVar.ApiKey); - BaseResponse res = JsonConvert.DeserializeObject(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); - return res.daytimes; - } + /// + /// Einzelnen Stundeneintrag holen + /// + internal static async Task LoadEntry(int id) { + string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey); + BaseResponse res = JsonConvert.DeserializeObject(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; + } - /// - /// Einzelnen Stundeneintrag holen - /// - internal static async Task LoadEntry(int id) { - string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey); - BaseResponse res = JsonConvert.DeserializeObject(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; - } + /// + /// Eintrag speichern + /// + internal static async Task 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); - /// - /// Eintrag speichern - /// - internal static async Task 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; - } - - /// - /// Eintrag löschen - /// - internal static async Task DeleteEntry(DayTime stunde) { - await BaseFunc.DeleteItemAsync(GlobalVar.ApiUrl + "/entry/" + stunde.Id, GlobalVar.ApiKey); - } -} + /// + /// Eintrag löschen + /// + internal static async Task DeleteEntry(DayTime stunde) { + await BaseFunc.DeleteItemAsync(GlobalVar.ApiUrl + "/entry/" + stunde.Id, GlobalVar.ApiKey); + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/JsonFlexibleIntConverters.cs b/Jugenddienst Stunden/Models/JsonFlexibleIntConverters.cs new file mode 100644 index 0000000..f616c55 --- /dev/null +++ b/Jugenddienst Stunden/Models/JsonFlexibleIntConverters.cs @@ -0,0 +1,56 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jugenddienst_Stunden.Models; + +internal sealed class JsonFlexibleInt32Converter : JsonConverter { + 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 { + 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(); + } +} diff --git a/Jugenddienst Stunden/Models/JsonSingleOrEmptyArrayConverter.cs b/Jugenddienst Stunden/Models/JsonSingleOrEmptyArrayConverter.cs index a70fae2..4ea493f 100644 --- a/Jugenddienst Stunden/Models/JsonSingleOrEmptyArrayConverter.cs +++ b/Jugenddienst Stunden/Models/JsonSingleOrEmptyArrayConverter.cs @@ -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; } } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/Note.cs b/Jugenddienst Stunden/Models/Note.cs index 6f58350..ead712f 100644 --- a/Jugenddienst Stunden/Models/Note.cs +++ b/Jugenddienst Stunden/Models/Note.cs @@ -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 LoadAll() { - // Get the folder where the notes are stored. - string appDataPath = FileSystem.AppDataDirectory; + public static IEnumerable 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 = ""; + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/Operator.cs b/Jugenddienst Stunden/Models/Operator.cs index 71b49be..6544ed2 100644 --- a/Jugenddienst Stunden/Models/Operator.cs +++ b/Jugenddienst Stunden/Models/Operator.cs @@ -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? AlertEvent; - -} + public event EventHandler? AlertEvent; +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/TokenData.cs b/Jugenddienst Stunden/Models/TokenData.cs index 887ce0d..97c7a42 100644 --- a/Jugenddienst Stunden/Models/TokenData.cs +++ b/Jugenddienst Stunden/Models/TokenData.cs @@ -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; } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Platforms/Android/AndroidManifest.xml b/Jugenddienst Stunden/Platforms/Android/AndroidManifest.xml index 199a014..0cc1628 100644 --- a/Jugenddienst Stunden/Platforms/Android/AndroidManifest.xml +++ b/Jugenddienst Stunden/Platforms/Android/AndroidManifest.xml @@ -1,14 +1,14 @@  - - - - - + android:allowBackup="true" + android:icon="@mipmap/appicon" + android:supportsRtl="true" + android:label="Stunden" + /> + + + + + \ No newline at end of file diff --git a/Jugenddienst Stunden/Platforms/Android/MainActivity.cs b/Jugenddienst Stunden/Platforms/Android/MainActivity.cs index c79b57f..1a95a7f 100644 --- a/Jugenddienst Stunden/Platforms/Android/MainActivity.cs +++ b/Jugenddienst Stunden/Platforms/Android/MainActivity.cs @@ -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 { -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Platforms/Android/MainApplication.cs b/Jugenddienst Stunden/Platforms/Android/MainApplication.cs index 10bc566..5d4d03d 100644 --- a/Jugenddienst Stunden/Platforms/Android/MainApplication.cs +++ b/Jugenddienst Stunden/Platforms/Android/MainApplication.cs @@ -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(); -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Platforms/MacCatalyst/Info.plist b/Jugenddienst Stunden/Platforms/MacCatalyst/Info.plist index 7268977..8be4e0e 100644 --- a/Jugenddienst Stunden/Platforms/MacCatalyst/Info.plist +++ b/Jugenddienst Stunden/Platforms/MacCatalyst/Info.plist @@ -1,38 +1,38 @@ - - - - - + + + + + - - - - - UIDeviceFamily - - 2 - - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - XSAppIconAssets - Assets.xcassets/appicon.appiconset - + + + + + UIDeviceFamily + + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + diff --git a/Jugenddienst Stunden/Platforms/Tizen/tizen-manifest.xml b/Jugenddienst Stunden/Platforms/Tizen/tizen-manifest.xml index 1fba88f..4e7ab9c 100644 --- a/Jugenddienst Stunden/Platforms/Tizen/tizen-manifest.xml +++ b/Jugenddienst Stunden/Platforms/Tizen/tizen-manifest.xml @@ -1,15 +1,17 @@  - - - - - maui-appicon-placeholder - - - - - http://tizen.org/privilege/internet - - - + + + + + maui-appicon-placeholder + + + + + http://tizen.org/privilege/internet + + + \ No newline at end of file diff --git a/Jugenddienst Stunden/Platforms/Windows/App.xaml b/Jugenddienst Stunden/Platforms/Windows/App.xaml index 9fce9dd..809cf08 100644 --- a/Jugenddienst Stunden/Platforms/Windows/App.xaml +++ b/Jugenddienst Stunden/Platforms/Windows/App.xaml @@ -5,4 +5,4 @@ xmlns:maui="using:Microsoft.Maui" xmlns:local="using:Jugenddienst_Stunden.WinUI"> - + \ No newline at end of file diff --git a/Jugenddienst Stunden/Platforms/Windows/App.xaml.cs b/Jugenddienst Stunden/Platforms/Windows/App.xaml.cs index 27a970b..8655865 100644 --- a/Jugenddienst Stunden/Platforms/Windows/App.xaml.cs +++ b/Jugenddienst Stunden/Platforms/Windows/App.xaml.cs @@ -19,9 +19,5 @@ namespace Jugenddienst_Stunden.WinUI { /// /// protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); - - } - - -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Platforms/Windows/Package.appxmanifest b/Jugenddienst Stunden/Platforms/Windows/Package.appxmanifest index e3df2fc..9861ce9 100644 --- a/Jugenddienst Stunden/Platforms/Windows/Package.appxmanifest +++ b/Jugenddienst Stunden/Platforms/Windows/Package.appxmanifest @@ -1,70 +1,74 @@  + 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"> - + - + - - $placeholder$ - Daniel Pichler - Resources\Windows\$placeholder$.png - + + $placeholder$ + Daniel Pichler + Resources\Windows\$placeholder$.png + - - - - + + + + - - - + + + - - - - - - + + + + + + - + - - - - + + + + - - - - - - - - - + + + + + + + + + - + - - + + - - - - - + + + + + diff --git a/Jugenddienst Stunden/Platforms/Windows/app.manifest b/Jugenddienst Stunden/Platforms/Windows/app.manifest index 5689be5..9f27d04 100644 --- a/Jugenddienst Stunden/Platforms/Windows/app.manifest +++ b/Jugenddienst Stunden/Platforms/Windows/app.manifest @@ -1,15 +1,16 @@ - + - - - - true/PM - PerMonitorV2, PerMonitor - - + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/Jugenddienst Stunden/Platforms/iOS/Info.plist b/Jugenddienst Stunden/Platforms/iOS/Info.plist index 8bdc015..c4d15c6 100644 --- a/Jugenddienst Stunden/Platforms/iOS/Info.plist +++ b/Jugenddienst Stunden/Platforms/iOS/Info.plist @@ -1,34 +1,34 @@ - - LSRequiresIPhoneOS - - UIDeviceFamily - - 1 - 2 - - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - NSCameraUsageDescription - This app uses barcode scanning to... - XSAppIconAssets - Assets.xcassets/appicon.appiconset - CFBundleIdentifier - - + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSCameraUsageDescription + This app uses barcode scanning to... + XSAppIconAssets + Assets.xcassets/appicon.appiconset + CFBundleIdentifier + + diff --git a/Jugenddienst Stunden/Properties/Resources.Designer.cs b/Jugenddienst Stunden/Properties/Resources.Designer.cs index 906d31a..419409f 100644 --- a/Jugenddienst Stunden/Properties/Resources.Designer.cs +++ b/Jugenddienst Stunden/Properties/Resources.Designer.cs @@ -1,10 +1,9 @@ //------------------------------------------------------------------------------ // -// 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. // //------------------------------------------------------------------------------ @@ -13,12 +12,12 @@ namespace Jugenddienst_Stunden.Properties { /// - /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. + /// A strongly-typed resource class, for looking up localized strings, etc. /// - // 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 { } /// - /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. + /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { @@ -47,8 +46,8 @@ namespace Jugenddienst_Stunden.Properties { } /// - /// Ü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. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { diff --git a/Jugenddienst Stunden/Properties/Resources.resx b/Jugenddienst Stunden/Properties/Resources.resx index 4fdb1b6..55c14ed 100644 --- a/Jugenddienst Stunden/Properties/Resources.resx +++ b/Jugenddienst Stunden/Properties/Resources.resx @@ -1,101 +1,106 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + 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. + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Jugenddienst Stunden/Repositories/HoursRepository.cs b/Jugenddienst Stunden/Repositories/HoursRepository.cs index f3f2f11..27e0631 100644 --- a/Jugenddienst Stunden/Repositories/HoursRepository.cs +++ b/Jugenddienst Stunden/Repositories/HoursRepository.cs @@ -1,4 +1,5 @@ using Jugenddienst_Stunden.Interfaces; +using Jugenddienst_Stunden.Infrastructure; using Jugenddienst_Stunden.Models; using Jugenddienst_Stunden.Types; @@ -8,12 +9,72 @@ namespace Jugenddienst_Stunden.Repositories; /// Standard-Repository, das die bestehende API-/Model-Logik kapselt. /// internal class HoursRepository : IHoursRepository { - public async Task LoadBase(string query) => await HoursBase.LoadBase(query); - public async Task LoadSettings() => await HoursBase.LoadSettings(); - public async Task LoadData() => await HoursBase.LoadData(); - public async Task LoadUser(string apiKey) => await HoursBase.LoadUser(apiKey); - public async Task> LoadDay(DateTime date) => await HoursBase.LoadDay(date); - public async Task LoadEntry(int id) => await HoursBase.LoadEntry(id); - public async Task SaveEntry(DayTime stunde) => await HoursBase.SaveEntry(stunde); - public async Task DeleteEntry(DayTime stunde) => await HoursBase.DeleteEntry(stunde); -} + private readonly IApiClient _api; + + public HoursRepository(IApiClient api) { + _api = api; + } + + public async Task LoadBase(string query) { + // Der bestehende Code übergab eine Query ohne führendes '?' + var dict = QueryToDictionary(query); + var res= await _api.GetAsync("", dict).ConfigureAwait(false); + return res; + } + + public async Task LoadSettings() { + var res = await _api.GetAsync("", new Dictionary { ["settings"] = "1" }) + .ConfigureAwait(false); + return res.settings; + } + + public async Task LoadData() { + var res = await _api.GetAsync("", new Dictionary { ["hours"] = "1" }) + .ConfigureAwait(false); + return res.hour; + } + + public Task LoadUser(string apiKey) { + // Für die erste Iteration bleibt das Token global; der Endpoint ohne Query liefert user + return _api.GetAsync("", null).ContinueWith(t => t.Result.user); + } + + public async Task> LoadDay(DateTime date) { + var res = await _api + .GetAsync("", new Dictionary { ["date"] = date.ToString("yyyy-MM-dd") }) + .ConfigureAwait(false); + return res.daytimes ?? new List(); + } + + public async Task LoadEntry(int id) { + var res = await _api.GetAsync("", new Dictionary { ["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 SaveEntry(DayTime stunde) { + bool isNew = stunde.Id is null; + var method = isNew ? HttpMethod.Post : HttpMethod.Put; + await _api.SendAsync(method, "", stunde).ConfigureAwait(false); + return stunde; + } + + public Task DeleteEntry(DayTime stunde) + => _api.DeleteAsync($"/entry/{stunde.Id}"); + + private static Dictionary QueryToDictionary(string query) { + var dict = new Dictionary(); + 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; + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Resources/AppIcon/appicon.svg b/Jugenddienst Stunden/Resources/AppIcon/appicon.svg index 773e284..38547b2 100644 --- a/Jugenddienst Stunden/Resources/AppIcon/appicon.svg +++ b/Jugenddienst Stunden/Resources/AppIcon/appicon.svg @@ -1,5 +1,7 @@ - + diff --git a/Jugenddienst Stunden/Resources/AppIcon/appiconfg.svg b/Jugenddienst Stunden/Resources/AppIcon/appiconfg.svg index a6a7b86..7e860c5 100644 --- a/Jugenddienst Stunden/Resources/AppIcon/appiconfg.svg +++ b/Jugenddienst Stunden/Resources/AppIcon/appiconfg.svg @@ -1,41 +1,54 @@ - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/Jugenddienst Stunden/Resources/Images/logo_splash_win.svg b/Jugenddienst Stunden/Resources/Images/logo_splash_win.svg index 3f66ad2..b6a1e65 100644 --- a/Jugenddienst Stunden/Resources/Images/logo_splash_win.svg +++ b/Jugenddienst Stunden/Resources/Images/logo_splash_win.svg @@ -1,21 +1,28 @@ - + - + - + - + - + - + diff --git a/Jugenddienst Stunden/Resources/Splash/splash.svg b/Jugenddienst Stunden/Resources/Splash/splash.svg index 3f66ad2..b6a1e65 100644 --- a/Jugenddienst Stunden/Resources/Splash/splash.svg +++ b/Jugenddienst Stunden/Resources/Splash/splash.svg @@ -1,21 +1,28 @@ - + - + - + - + - + - + diff --git a/Jugenddienst Stunden/Resources/Styles/Colors.xaml b/Jugenddienst Stunden/Resources/Styles/Colors.xaml index d69c36e..a6ecc0b 100644 --- a/Jugenddienst Stunden/Resources/Styles/Colors.xaml +++ b/Jugenddienst Stunden/Resources/Styles/Colors.xaml @@ -1,6 +1,7 @@ - + - @@ -30,18 +31,18 @@ #212121 #141414 - - - - - + + + + + - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/Jugenddienst Stunden/Resources/Styles/Styles.xaml b/Jugenddienst Stunden/Resources/Styles/Styles.xaml index 13f626b..b54d76b 100644 --- a/Jugenddienst Stunden/Resources/Styles/Styles.xaml +++ b/Jugenddienst Stunden/Resources/Styles/Styles.xaml @@ -1,54 +1,65 @@ - + - - + - + \ No newline at end of file diff --git a/Jugenddienst Stunden/Services/HoursService.cs b/Jugenddienst Stunden/Services/HoursService.cs index d516ee0..5885789 100644 --- a/Jugenddienst Stunden/Services/HoursService.cs +++ b/Jugenddienst Stunden/Services/HoursService.cs @@ -1,38 +1,58 @@ using Jugenddienst_Stunden.Interfaces; using Jugenddienst_Stunden.Types; +using Jugenddienst_Stunden.Validators; namespace Jugenddienst_Stunden.Services; internal class HoursService : IHoursService { - private readonly IHoursRepository _repo; + private readonly IHoursRepository _repo; + private readonly IHoursValidator _validator; - public HoursService(IHoursRepository repo) { - _repo = repo; - } + public HoursService(IHoursRepository repo, IHoursValidator validator) { + _repo = repo; + _validator = validator; + } - public async Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate) { - string q = $"hours&year={monthDate:yyyy}&month={monthDate:MM}"; + public async Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate) { + string q = $"hours=&year={monthDate:yyyy}&month={monthDate:MM}"; var baseRes = await _repo.LoadBase(q); - return (baseRes.hour, baseRes.settings); + + // Fallbacks, da einige Endpoints 'settings' nicht mitsenden bzw. 'hour' auslassen können + var settings = baseRes.settings ?? await _repo.LoadSettings(); + var hours = baseRes.hour ?? new Hours { + daytime = new List(), + Nominal_day_api = new List(), + Nominal_week_api = new List(), + zeit_total_daily_api = new List(), + Projekte = new System.Collections.ObjectModel.Collection(), + Gemeinden = new System.Collections.ObjectModel.Collection(), + Freistellungen = new System.Collections.ObjectModel.Collection() + }; + + return (hours, settings); } - public async Task<(List dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date) { - string q = $"date={date:yyyy-MM-dd}"; - var baseRes = await _repo.LoadBase(q); - return (baseRes.daytimes ?? new List(), baseRes.settings); - } + public async Task<(List dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date) { + string q = $"date={date:yyyy-MM-dd}"; + var baseRes = await _repo.LoadBase(q); + return (baseRes.daytimes ?? new List(), baseRes.settings); + } - public async Task> 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(); - } + public async Task> 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(); + } - public async Task GetSettingsAsync() => await _repo.LoadSettings(); + public async Task GetSettingsAsync() => await _repo.LoadSettings(); - public async Task GetEntryAsync(int id) => await _repo.LoadEntry(id); + public async Task GetEntryAsync(int id) => await _repo.LoadEntry(id); - public async Task SaveEntryAsync(DayTime stunde) => await _repo.SaveEntry(stunde); + public async Task SaveEntryAsync(DayTime stunde) { + var settings = await _repo.LoadSettings(); + _validator.Validate(stunde, settings); + return await _repo.SaveEntry(stunde); + } - public async Task DeleteEntryAsync(DayTime stunde) => await _repo.DeleteEntry(stunde); -} + public async Task DeleteEntryAsync(DayTime stunde) => await _repo.DeleteEntry(stunde); +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/BaseResponse.cs b/Jugenddienst Stunden/Types/BaseResponse.cs index b99b850..6b97982 100644 --- a/Jugenddienst Stunden/Types/BaseResponse.cs +++ b/Jugenddienst Stunden/Types/BaseResponse.cs @@ -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; } - /// - /// Monatsübersicht - /// - public Hours hour { get; set; } + /// + /// Monatsübersicht + /// + public Hours hour { get; set; } - /// - /// Stundenliste ... für die Katz? - /// - public List hours { get; set; } + /// + /// Stundenliste ... für die Katz? + /// + public List hours { get; set; } - /// - /// Liste der Stundeneinträge - /// - public List daytimes { get; set; } + /// + /// Liste der Stundeneinträge + /// + public List daytimes { get; set; } - /// - /// Einzelner Stundeneintrag - /// - public DayTime daytime { get; set; } + /// + /// Einzelner Stundeneintrag + /// + public DayTime daytime { get; set; } - /// - /// Auch irgendwie doppelt ... - /// - public Operator operatorVar { get; set; } - public User user { get; set; } + /// + /// Auch irgendwie doppelt ... + /// + 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; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/DayTime.cs b/Jugenddienst Stunden/Types/DayTime.cs index 3ac3295..98bb973 100644 --- a/Jugenddienst Stunden/Types/DayTime.cs +++ b/Jugenddienst Stunden/Types/DayTime.cs @@ -2,95 +2,95 @@ using System.Collections.ObjectModel; namespace Jugenddienst_Stunden.Types; + /// /// Represents a day time entry for an employee. /// public class DayTime { - /// - /// ID des Stundeneintrages - /// - public int? Id { get; set; } + /// + /// ID des Stundeneintrages + /// + public int? Id { get; set; } - /// - /// Mitarbeiter-ID - /// - public int EmployeeId { get; set; } + /// + /// Mitarbeiter-ID + /// + public int EmployeeId { get; set; } - /// - /// Der betreffende Tag - /// - public DateTime Day { get; set; } + /// + /// Der betreffende Tag + /// + public DateTime Day { get; set; } - /// - /// Der Wochentag - /// - public int Wday { get; set; } + /// + /// Der Wochentag + /// + public int Wday { get; set; } - /// - /// Arbeitsbeginn - /// - public TimeOnly Begin { get; set; } + /// + /// Arbeitsbeginn + /// + public TimeOnly Begin { get; set; } - /// - /// Arbeitsende - /// - public TimeOnly End { get; set; } + /// + /// Arbeitsende + /// + public TimeOnly End { get; set; } - /// - /// Beschreibung der Tätigkeit - /// - public string? Description { get; set; } + /// + /// Beschreibung der Tätigkeit + /// + public string? Description { get; set; } - /// - /// Freistellung - /// - public string? Free { get; set; } + /// + /// Freistellung + /// + public string? Free { get; set; } - /// - /// Freistellung genehmigt? - /// - public bool Approved { get; set; } + /// + /// Freistellung genehmigt? + /// + public bool Approved { get; set; } - /// - /// Das gewählte Projekt - /// - public int? Projekt { get; set; } + /// + /// Das gewählte Projekt + /// + public int? Projekt { get; set; } - /// - /// Die gewählte Gemeinde - /// - public int? Gemeinde { get; set; } + /// + /// Die gewählte Gemeinde + /// + public int? Gemeinde { get; set; } - /// - /// Nachtstunden - /// - public TimeOnly Night { get; set; } + /// + /// Nachtstunden + /// + public TimeOnly Night { get; set; } - /// - /// Summe Arbeitszeit (inklusive Nachstunden mit Faktor) - /// - public Dictionary Total { get; set; } + /// + /// Summe Arbeitszeit (inklusive Nachstunden mit Faktor) + /// + public Dictionary 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; } - /// - /// Gets the active Gemeinde based on the gemeinde ID. - /// - public Gemeinde? GemeindeAktiv { get; set; } + /// + /// Gets the active Gemeinde based on the gemeinde ID. + /// + public Gemeinde? GemeindeAktiv { get; set; } - /// - /// Gets the active Projekt based on the projekt ID. - /// - public Projekt? ProjektAktiv { get; set; } + /// + /// Gets the active Projekt based on the projekt ID. + /// + public Projekt? ProjektAktiv { get; set; } - /// - /// Gets the active Freistellung based on the Freistellung ID - /// - public Freistellung? FreistellungAktiv { get; set; } + /// + /// Gets the active Freistellung based on the Freistellung ID + /// + public Freistellung? FreistellungAktiv { get; set; } - public int TimeTable { get; set; } - -} + public int TimeTable { get; set; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/Freistellung.cs b/Jugenddienst Stunden/Types/Freistellung.cs index 6bc88dc..ad4158d 100644 --- a/Jugenddienst Stunden/Types/Freistellung.cs +++ b/Jugenddienst Stunden/Types/Freistellung.cs @@ -1,8 +1,9 @@ namespace Jugenddienst_Stunden.Types; + /// /// Freistellungen: Urlaub, Zeitausgleich, Krankheit, ... /// public class Freistellung { - public string? Id { get; set; } + public string? Id { get; set; } public string? Name { get; set; } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/Gemeinde.cs b/Jugenddienst Stunden/Types/Gemeinde.cs index f2cdf1f..aea9943 100644 --- a/Jugenddienst Stunden/Types/Gemeinde.cs +++ b/Jugenddienst Stunden/Types/Gemeinde.cs @@ -13,4 +13,4 @@ public class Gemeinde { /// Name der Gemeinde. /// public string? Name { get; set; } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/Hours.cs b/Jugenddienst Stunden/Types/Hours.cs index 82565c0..69d64d8 100644 --- a/Jugenddienst Stunden/Types/Hours.cs +++ b/Jugenddienst Stunden/Types/Hours.cs @@ -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 nominal_day_api; public List? Nominal_day_api; + //public Dictionary nominal_week_api; public List? Nominal_week_api; - //public List 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))] - //public Dictionary zeit_total_daily; + //public List time_line; + public double? Zeit_total; - public List 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))] + //public Dictionary zeit_total_daily; + + public List zeit_total_daily_api; public List? daytime; - //public List wochensumme; + //public List wochensumme; - [ObservableProperty] - public double overtime_month; + [ObservableProperty] public double overtime_month; - [ObservableProperty] - public double overtime; - //public List overtime_day; + [ObservableProperty] public double overtime; + //public List 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 Gemeinden { get; set; } public Collection Freistellungen { get; set; } public int EmployeeId { get; set; } - -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/NominalDay.cs b/Jugenddienst Stunden/Types/NominalDay.cs index 6b158b3..48dc9d0 100644 --- a/Jugenddienst Stunden/Types/NominalDay.cs +++ b/Jugenddienst Stunden/Types/NominalDay.cs @@ -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; -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/NominalWeek.cs b/Jugenddienst Stunden/Types/NominalWeek.cs index b43834a..f414e8f 100644 --- a/Jugenddienst Stunden/Types/NominalWeek.cs +++ b/Jugenddienst Stunden/Types/NominalWeek.cs @@ -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; +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/Projekt.cs b/Jugenddienst Stunden/Types/Projekt.cs index c4bdd91..e03a42f 100644 --- a/Jugenddienst Stunden/Types/Projekt.cs +++ b/Jugenddienst Stunden/Types/Projekt.cs @@ -13,4 +13,4 @@ public class Projekt { /// Holt oder setzt den Namen des Projekts. /// public string Name { get; set; } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/Settings.cs b/Jugenddienst Stunden/Types/Settings.cs index bf2e1ba..285c468 100644 --- a/Jugenddienst Stunden/Types/Settings.cs +++ b/Jugenddienst Stunden/Types/Settings.cs @@ -4,35 +4,35 @@ /// Einstellungen /// public class Settings { - /// - /// Sind Projekte aktiv? - /// - public bool ProjektAktivSet { get; set; } + /// + /// Sind Projekte aktiv? + /// + public bool ProjektAktivSet { get; set; } - /// - /// Sind Gemeinden aktiv? - /// - public bool GemeindeAktivSet { get; set; } + /// + /// Sind Gemeinden aktiv? + /// + public bool GemeindeAktivSet { get; set; } - /// - /// Liste der Projekte - /// - public List? Projekte { get; set; } + /// + /// Liste der Projekte + /// + public List? Projekte { get; set; } - /// - /// Liste der Gemeinden - /// - public List? Gemeinden { get; set; } + /// + /// Liste der Gemeinden + /// + public List? Gemeinden { get; set; } - /// - /// Liste der Freistellungen - /// - public List? Freistellungen { get; set; } + /// + /// Liste der Freistellungen + /// + public List? Freistellungen { get; set; } - public List Nominal { get; set; } + public List? Nominal { get; set; } - /// - /// Version der API - /// - public string Version { get; set; } -} + /// + /// Version der API + /// + public string? Version { get; set; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/Sollstunden.cs b/Jugenddienst Stunden/Types/Sollstunden.cs index cea4268..0f57280 100644 --- a/Jugenddienst Stunden/Types/Sollstunden.cs +++ b/Jugenddienst Stunden/Types/Sollstunden.cs @@ -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; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/TimeDay.cs b/Jugenddienst Stunden/Types/TimeDay.cs index 19957fd..5e91d9d 100644 --- a/Jugenddienst Stunden/Types/TimeDay.cs +++ b/Jugenddienst Stunden/Types/TimeDay.cs @@ -3,7 +3,7 @@ /// /// Summe der geleisteten Stunden. /// -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; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/Timetable.cs b/Jugenddienst Stunden/Types/Timetable.cs index 1c8090b..3cb4821 100644 --- a/Jugenddienst Stunden/Types/Timetable.cs +++ b/Jugenddienst Stunden/Types/Timetable.cs @@ -1,6 +1,6 @@ namespace Jugenddienst_Stunden.Types; internal class Timetable { - public List timetable; - public decimal wochensumme; -} + public List timetable; + public decimal wochensumme; +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/TimetableEntry.cs b/Jugenddienst Stunden/Types/TimetableEntry.cs index f19853e..5c300b4 100644 --- a/Jugenddienst Stunden/Types/TimetableEntry.cs +++ b/Jugenddienst Stunden/Types/TimetableEntry.cs @@ -1,7 +1,7 @@ namespace Jugenddienst_Stunden.Types; internal class TimetableEntry { - public List? Von; - public List? Bis; - public decimal Summe { get; set; } -} + public List? Von; + public List? Bis; + public decimal Summe { get; set; } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Types/User.cs b/Jugenddienst Stunden/Types/User.cs index b6d03cd..94e749b 100644 --- a/Jugenddienst Stunden/Types/User.cs +++ b/Jugenddienst Stunden/Types/User.cs @@ -1,7 +1,8 @@ namespace Jugenddienst_Stunden.Types; + internal class User { public int Id { get; set; } public string Name { get; set; } public string Surname { get; set; } public string Token { get; set; } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Validators/HoursValidator.cs b/Jugenddienst Stunden/Validators/HoursValidator.cs new file mode 100644 index 0000000..bf2f063 --- /dev/null +++ b/Jugenddienst Stunden/Validators/HoursValidator.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/ViewModels/LoginViewModel.cs b/Jugenddienst Stunden/ViewModels/LoginViewModel.cs index 6f8319c..f34abaf 100644 --- a/Jugenddienst Stunden/ViewModels/LoginViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/LoginViewModel.cs @@ -4,28 +4,30 @@ /// Die Loginseite /// public class LoginViewModel { - /// - /// Name der Anwendung - /// - public string AppTitle => AppInfo.Name; + /// + /// Name der Anwendung + /// + public string AppTitle => AppInfo.Name; - /// - /// Programmversion - /// - public string Version => AppInfo.VersionString; + /// + /// Programmversion + /// + public string Version => AppInfo.VersionString; - /// - /// Kurze Mitteilung für den Anwender - /// - public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite."; + /// + /// Kurze Mitteilung für den Anwender + /// + public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite."; - /// - /// Genutzer Server für die API - /// - public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "").Replace("https://", "").Replace("http://", ""); + /// + /// Genutzer Server für die API + /// + public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "") + .Replace("https://", "").Replace("http://", ""); - /// - /// Titel der Seite - im Moment der aktuelle Anwender - /// - public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt"); -} + /// + /// Titel der Seite - im Moment der aktuelle Anwender + /// + public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + + Preferences.Default.Get("surname", "eingeloggt"); +} \ No newline at end of file diff --git a/Jugenddienst Stunden/ViewModels/NoteViewModel.cs b/Jugenddienst Stunden/ViewModels/NoteViewModel.cs index 406cc7b..f3dd4f3 100644 --- a/Jugenddienst Stunden/ViewModels/NoteViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/NoteViewModel.cs @@ -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)); } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/ViewModels/NotesViewModel.cs b/Jugenddienst Stunden/ViewModels/NotesViewModel.cs index a154921..418cde5 100644 --- a/Jugenddienst Stunden/ViewModels/NotesViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/NotesViewModel.cs @@ -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))); + } } } } -} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/ViewModels/StundeViewModel.cs b/Jugenddienst Stunden/ViewModels/StundeViewModel.cs index d749372..ab74050 100644 --- a/Jugenddienst Stunden/ViewModels/StundeViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/StundeViewModel.cs @@ -8,243 +8,257 @@ using static System.Runtime.InteropServices.JavaScript.JSType; using Jugenddienst_Stunden.Interfaces; using Jugenddienst_Stunden.Repositories; using Jugenddienst_Stunden.Services; +using Jugenddienst_Stunden.Infrastructure; +using Jugenddienst_Stunden.Validators; +using System.Net.Http; namespace Jugenddienst_Stunden.ViewModels; + /// /// Viewmodel für die einzelnen Stundeneinträge / Bearbeitung /// public partial class StundeViewModel : ObservableObject, IQueryAttributable { - private readonly IHoursService _hoursService; + private readonly IHoursService _hoursService; - public int Id { get; set; } - public string Title { get; set; } = "Eintrag bearbeiten"; - public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy"); + public int Id { get; set; } + public string Title { get; set; } = "Eintrag bearbeiten"; + public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy"); - //private HoursBase HoursBase = new HoursBase(); - internal Settings Settings = new Settings(); + //private HoursBase HoursBase = new HoursBase(); + internal Settings Settings = new Settings(); - public event EventHandler AlertEvent; - public event EventHandler InfoEvent; - public event Func> ConfirmEvent; - //public event Func> ConfirmEvent; - //public event EventHandler ConfirmEvent; + public event EventHandler AlertEvent; + public event EventHandler InfoEvent; - /// - /// Gemeinden für die Auswahlliste - /// - [ObservableProperty] - private List optionsGemeinde; + public event Func> ConfirmEvent; + //public event Func> ConfirmEvent; + //public event EventHandler ConfirmEvent; - /// - /// Projekte für die Auswahlliste - /// - [ObservableProperty] - private List optionsProjekt; + /// + /// Gemeinden für die Auswahlliste + /// + [ObservableProperty] private List optionsGemeinde; - /// - /// Freistellungen für die Auswahlliste - /// - [ObservableProperty] - private List optionsFreistellung; + /// + /// Projekte für die Auswahlliste + /// + [ObservableProperty] private List optionsProjekt; - /// - /// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird - /// - [ObservableProperty] - private List dayTimes; + /// + /// Freistellungen für die Auswahlliste + /// + [ObservableProperty] private List optionsFreistellung; - /// - /// Aktueller Stundeneintrag - /// - [ObservableProperty] - private DayTime dayTime; + /// + /// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird + /// + [ObservableProperty] private List dayTimes; - /// - /// Dürfen Gemeinden verwendet werden? - /// - [ObservableProperty] - private bool gemeindeAktivSet; + /// + /// Aktueller Stundeneintrag + /// + [ObservableProperty] private DayTime dayTime; - /// - /// Dürfen Projekte verwendet werden? - /// - [ObservableProperty] - private bool projektAktivSet; + /// + /// Dürfen Gemeinden verwendet werden? + /// + [ObservableProperty] private bool gemeindeAktivSet; - [ObservableProperty] - private bool freistellungEnabled; + /// + /// Dürfen Projekte verwendet werden? + /// + [ObservableProperty] private bool projektAktivSet; - public ICommand SaveCommand { get; private set; } - public ICommand DeleteCommand { get; private set; } - public ICommand DeleteConfirmCommand { get; private set; } - //public ICommand LoadDataCommand { get; private set; } + [ObservableProperty] private bool freistellungEnabled; + + public ICommand SaveCommand { get; private set; } + public ICommand DeleteCommand { get; private set; } + public ICommand DeleteConfirmCommand { get; private set; } + //public ICommand LoadDataCommand { get; private set; } - public StundeViewModel() : this(GetServiceOrCreate()) { } + public StundeViewModel() : this(GetServiceOrCreate()) { + } - private static IHoursService GetServiceOrCreate() => new HoursService(new HoursRepository()); + private static IHoursService GetServiceOrCreate() { + // Fallback-Konstruktion, falls DI nicht injiziert wurde (z. B. im Designer) + var http = new HttpClient(); + var options = new Infrastructure.ApiOptions { BaseUrl = GlobalVar.ApiUrl, Timeout = TimeSpan.FromSeconds(15) }; + var tokenProvider = new GlobalVarTokenProvider(); + var api = new ApiClient(http, options, tokenProvider, new PreferencesAppSettings()); + var repo = new HoursRepository(api); + var validator = new HoursValidator(); + return new HoursService(repo, validator); + } - internal StundeViewModel(IHoursService hoursService) { - _hoursService = hoursService; - SaveCommand = new AsyncRelayCommand(Save); - //DeleteCommand = new AsyncRelayCommand(Delete); - DeleteConfirmCommand = new Command(async () => await DeleteConfirm()); - } + internal StundeViewModel(IHoursService hoursService) { + _hoursService = hoursService; + SaveCommand = new AsyncRelayCommand(Save); + //DeleteCommand = new AsyncRelayCommand(Delete); + DeleteConfirmCommand = new Command(async () => await DeleteConfirm()); + } - private async void LoadSettingsAsync() { - try { - Settings = await _hoursService.GetSettingsAsync(); - GlobalVar.Settings = Settings; + // DI-Konstruktor, der den globalen Alert-Service abonniert und Alerts an das ViewModel weiterreicht. + internal StundeViewModel(IHoursService hoursService, IAlertService alertService) : this(hoursService) { + if (alertService is not null) { + alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg); + } + } - OptionsGemeinde = Settings.Gemeinden; - OptionsProjekt = Settings.Projekte; - OptionsFreistellung = Settings.Freistellungen; + private async void LoadSettingsAsync() { + try { + Settings = await _hoursService.GetSettingsAsync(); + GlobalVar.Settings = Settings; - GemeindeAktivSet = Settings.GemeindeAktivSet; - ProjektAktivSet = Settings.ProjektAktivSet; + OptionsGemeinde = Settings.Gemeinden; + OptionsProjekt = Settings.Projekte; + OptionsFreistellung = Settings.Freistellungen; - } catch (Exception e) { - AlertEvent?.Invoke(this, e.Message); - } - } + GemeindeAktivSet = Settings.GemeindeAktivSet; + ProjektAktivSet = Settings.ProjektAktivSet; + } catch (Exception e) { + AlertEvent?.Invoke(this, e.Message); + } + } - async Task Save() { - bool exceptionOccurred = false; - bool proceed = true; - if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) { - proceed = false; - AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein"); - } - - if (proceed) { - try { - await _hoursService.SaveEntryAsync(DayTime); - } catch (Exception e) { - AlertEvent?.Invoke(this, e.Message); - exceptionOccurred = true; - } - if (!exceptionOccurred) { - if (DayTime.Id != null) { - await Shell.Current.GoToAsync($"..?saved={DayTime.Id}"); - } else { - await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); - } - } - } - } + async Task Save() { + bool exceptionOccurred = false; + bool proceed = true; + if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) { + proceed = false; + AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein"); + } - /// - /// Löschen ohne Bestätigung - /// - private async Task Delete() { - await _hoursService.DeleteEntryAsync(DayTime); - await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); - } + if (proceed) { + try { + await _hoursService.SaveEntryAsync(DayTime); + } catch (Exception e) { + AlertEvent?.Invoke(this, e.Message); + exceptionOccurred = true; + } - /// - /// Löschen mit Bestätigung - /// - private async Task DeleteConfirm() { - if (ConfirmEvent != null) { - bool answer = await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?"); - if (answer) { - //Löschen - await _hoursService.DeleteEntryAsync(DayTime); - await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); - } else { - //nicht Löschen - } - } - } + if (!exceptionOccurred) { + if (DayTime.Id != null) { + await Shell.Current.GoToAsync($"..?saved={DayTime.Id}"); + } else { + await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); + } + } + } + } - /// - /// Anwenden der Query-Parameter - /// - async void IQueryAttributable.ApplyQueryAttributes(IDictionary query) { - //load beinhaltet die ID: Eintrag bearbeiten - //date beinhaltet einen Tag: Neuen Eintrag erstellen - if (query.ContainsKey("load")) { + /// + /// Löschen ohne Bestätigung + /// + private async Task Delete() { + await _hoursService.DeleteEntryAsync(DayTime); + await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); + } - //DateTime heute = DateTime.Now; - try { - var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"])); - var settings = await _hoursService.GetSettingsAsync(); - GlobalVar.Settings = settings; - GemeindeAktivSet = settings.GemeindeAktivSet; - ProjektAktivSet = settings.ProjektAktivSet; + /// + /// Löschen mit Bestätigung + /// + private async Task DeleteConfirm() { + if (ConfirmEvent != null) { + bool answer = + await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?"); + if (answer) { + //Löschen + await _hoursService.DeleteEntryAsync(DayTime); + await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); + } else { + //nicht Löschen + } + } + } - DayTime = entry; - DayTime.TimeSpanVon = entry.Begin.ToTimeSpan(); - DayTime.TimeSpanBis = entry.End.ToTimeSpan(); + /// + /// Anwenden der Query-Parameter + /// + async void IQueryAttributable.ApplyQueryAttributes(IDictionary query) { + //load beinhaltet die ID: Eintrag bearbeiten + //date beinhaltet einen Tag: Neuen Eintrag erstellen + if (query.ContainsKey("load")) { + //DateTime heute = DateTime.Now; + try { + var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"])); + var settings = await _hoursService.GetSettingsAsync(); + GlobalVar.Settings = settings; + GemeindeAktivSet = settings.GemeindeAktivSet; + ProjektAktivSet = settings.ProjektAktivSet; - OptionsGemeinde = settings.Gemeinden ?? new List(); - OptionsProjekt = settings.Projekte ?? new List(); - OptionsFreistellung = settings.Freistellungen ?? new List(); + DayTime = entry; + DayTime.TimeSpanVon = entry.Begin.ToTimeSpan(); + DayTime.TimeSpanBis = entry.End.ToTimeSpan(); - DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? new Gemeinde(); - DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? new Projekt(); - DayTime.FreistellungAktiv = OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? new Freistellung(); + OptionsGemeinde = settings.Gemeinden ?? new List(); + OptionsProjekt = settings.Projekte ?? new List(); + OptionsFreistellung = settings.Freistellungen ?? new List(); - //Evtl. noch die anderen Zeiten des gleichen Tages holen - var day = await _hoursService.GetDayWithSettingsAsync(DayTime.Day); - DayTimes = day.dayTimes; - OnPropertyChanged(nameof(DayTime)); - OnPropertyChanged(nameof(DayTimes)); + DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? + new Gemeinde(); + DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? + new Projekt(); + DayTime.FreistellungAktiv = + OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? + new Freistellung(); - } catch (Exception e) { - AlertEvent?.Invoke(this, e.Message); - } finally { + //Evtl. noch die anderen Zeiten des gleichen Tages holen + var day = await _hoursService.GetDayWithSettingsAsync(DayTime.Day); + DayTimes = day.dayTimes; + OnPropertyChanged(nameof(DayTime)); + OnPropertyChanged(nameof(DayTimes)); + } catch (Exception e) { + AlertEvent?.Invoke(this, e.Message); + } finally { + } - } + if (System.String.IsNullOrEmpty(DayTime.Description)) { + InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext"); + } - if (System.String.IsNullOrEmpty(DayTime.Description)) { - InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext"); - } - SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy"); - OnPropertyChanged(nameof(SubTitle)); + SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy"); + OnPropertyChanged(nameof(SubTitle)); - FreistellungEnabled = !DayTime.Approved; - //OnPropertyChanged(nameof(DayTime)); + FreistellungEnabled = !DayTime.Approved; + //OnPropertyChanged(nameof(DayTime)); + } else if (query.ContainsKey("date")) { + Title = "Neuer Eintrag"; + OnPropertyChanged(nameof(Title)); - } else if (query.ContainsKey("date")) { - Title = "Neuer Eintrag"; - OnPropertyChanged(nameof(Title)); + DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd", + System.Globalization.CultureInfo.InvariantCulture); + //Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen + try { + var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date); + GlobalVar.Settings = settings; + DayTimes = list; - DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture); - //Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen - try { - var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date); - GlobalVar.Settings = settings; - DayTimes = list; + OptionsGemeinde = settings.Gemeinden; + OptionsProjekt = settings.Projekte; + OptionsFreistellung = settings.Freistellungen; - OptionsGemeinde = settings.Gemeinden; - OptionsProjekt = settings.Projekte; - OptionsFreistellung = settings.Freistellungen; + GemeindeAktivSet = settings.GemeindeAktivSet; + ProjektAktivSet = settings.ProjektAktivSet; + } catch (Exception) { + //Ein Tag ohne Einträge gibt eine Fehlermeldung, + //die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht + //In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden + LoadSettingsAsync(); + } finally { + DayTime = new DayTime(); + DayTime.Day = _date; + DayTime.EmployeeId = GlobalVar.EmployeeId; + DayTime.GemeindeAktiv = new Gemeinde(); + DayTime.ProjektAktiv = new Projekt(); + DayTime.FreistellungAktiv = new Freistellung(); - GemeindeAktivSet = settings.GemeindeAktivSet; - ProjektAktivSet = settings.ProjektAktivSet; + SubTitle = _date.ToString("dddd, d. MMMM yyyy"); + FreistellungEnabled = true; - } catch (Exception) { - //Ein Tag ohne Einträge gibt eine Fehlermeldung, - //die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht - //In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden - LoadSettingsAsync(); - } finally { - DayTime = new DayTime(); - DayTime.Day = _date; - DayTime.EmployeeId = GlobalVar.EmployeeId; - DayTime.GemeindeAktiv = new Gemeinde(); - DayTime.ProjektAktiv = new Projekt(); - DayTime.FreistellungAktiv = new Freistellung(); - - SubTitle = _date.ToString("dddd, d. MMMM yyyy"); - FreistellungEnabled = true; - - OnPropertyChanged(nameof(SubTitle)); - //OnPropertyChanged(nameof(DayTime)); - } - - } - } - -} + OnPropertyChanged(nameof(SubTitle)); + //OnPropertyChanged(nameof(DayTime)); + } + } + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/ViewModels/StundenViewModel.cs b/Jugenddienst Stunden/ViewModels/StundenViewModel.cs index 415c4ea..b394fa3 100644 --- a/Jugenddienst Stunden/ViewModels/StundenViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/StundenViewModel.cs @@ -5,325 +5,366 @@ using Jugenddienst_Stunden.Types; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; -using CommunityToolkit.Maui.Alerts; -using CommunityToolkit.Maui.Core; -using static System.Runtime.InteropServices.JavaScript.JSType; using Jugenddienst_Stunden.Interfaces; -using Jugenddienst_Stunden.Repositories; -using Jugenddienst_Stunden.Services; namespace Jugenddienst_Stunden.ViewModels; + /// /// ViewModel für die Stundenliste /// -internal partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged { - private readonly IHoursService _hoursService; +public partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged { + private readonly IHoursService _hoursService; - public ICommand NewEntryCommand { get; } - public ICommand SelectEntryCommand { get; } - public ICommand LoadDataCommand { get; private set; } - public ICommand LoadDayCommand { get; private set; } - public ICommand RefreshListCommand { get; } - public ICommand RefreshCommand { get; } + public ICommand NewEntryCommand { get; } + public ICommand SelectEntryCommand { get; } + public ICommand LoadDataCommand { get; private set; } + public ICommand LoadDayCommand { get; private set; } + public ICommand RefreshListCommand { get; } + public ICommand RefreshCommand { get; } - public event EventHandler AlertEvent; - public event EventHandler InfoEvent; + public event EventHandler AlertEvent; + public event EventHandler InfoEvent; - /// - /// Beschriftung Button Monatsübersicht - /// - [ObservableProperty] - private string loadOverview; + /// + /// Beschriftung Button Monatsübersicht + /// + [ObservableProperty] private string loadOverview; - //private HoursBase HoursBase = new HoursBase(); - internal Settings Settings = new Settings(); + //private HoursBase HoursBase = new HoursBase(); + internal Settings Settings = new Settings(); - /// - /// Zu leistende Stunden - /// - [ObservableProperty] - private TimeOnly sollstunden; + /// + /// Zu leistende Stunden + /// + [ObservableProperty] private TimeOnly sollstunden; - /// - /// Geleistete Stunden an einem Tag - /// - [ObservableProperty] - private TimeOnly dayTotal; + /// + /// Geleistete Stunden an einem Tag + /// + [ObservableProperty] private TimeOnly dayTotal; - /// - /// Liste der Tageszeiten - /// - [ObservableProperty] - private List dayTimes = new List(); + /// + /// Liste der Tageszeiten + /// + [ObservableProperty] private List dayTimes = new List(); - 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; - /// - /// Mindest-Datum für den Datepicker - /// - public DateTime MinimumDate { - get => DateTime.Today.AddDays(-365); - } + /// + /// Mindest-Datum für den Datepicker + /// + public DateTime MinimumDate { + get => DateTime.Today.AddDays(-365); + } - /// - /// Höchst-Datum für den Datepicker - /// - public DateTime MaximumDate { - get => DateTime.Today.AddDays(60); - } + /// + /// Höchst-Datum für den Datepicker + /// + public DateTime MaximumDate { + get => DateTime.Today.AddDays(60); + } - /// - /// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen - /// - 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)); - } - } - } + /// + /// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen + /// + private DateTime dateToday = DateTime.Today; - /// - /// Monatsübersicht: Geleistete Stunden - /// - 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); + } + }); + } + } + } - /// - /// Monatsübersicht: Sollstunden - /// - public double? Nominal { - get => Hours.Nominal; - } + /// + /// Monatsübersicht: Geleistete Stunden + /// + public double? ZeitCalculated { + get => Hours.Zeit_total; + } - /// - /// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden - /// - public double? Overtime { - get => Hours.overtime; - } + /// + /// Monatsübersicht: Sollstunden + /// + public double? Nominal { + get => Hours.Nominal; + } - /// - /// Monatsübersicht: Restüberstunden insgesamt - /// - public double OvertimeMonth { - get => Hours.overtime_month; - } + /// + /// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden + /// + public double? Overtime { + get => Hours.overtime; + } - public double Zeitausgleich { - get => Hours.zeitausgleich; - } - public double ZeitausgleichMonth { - get => Hours.zeitausgleich_month; - } + /// + /// Monatsübersicht: Restüberstunden insgesamt + /// + public double OvertimeMonth { + get => Hours.overtime_month; + } - /// - /// Monatsübersicht: Resturlaub - /// - public double Holiday { - get => Hours.holiday; - } + public double Zeitausgleich { + get => Hours.zeitausgleich; + } - /// - /// Seite neu laden - /// - [ObservableProperty] - private bool isRefreshing; + public double ZeitausgleichMonth { + get => Hours.zeitausgleich_month; + } + + /// + /// Monatsübersicht: Resturlaub + /// + public double Holiday { + get => Hours.holiday; + } + + /// + /// Seite neu laden + /// + [ObservableProperty] private bool isRefreshing; - /// - /// Dürfen Gemeinden verwendet werden? - /// - public bool GemeindeAktivSet { get; set; } + /// + /// Dürfen Gemeinden verwendet werden? + /// + public bool GemeindeAktivSet { get; set; } - /// - /// Dürfen Projekte verwendet werden? - /// - public bool ProjektAktivSet { get; set; } + /// + /// Dürfen Projekte verwendet werden? + /// + public bool ProjektAktivSet { get; set; } - private bool doContinue = true; + private bool doContinue = true; + /// + /// CTOR (DI) + /// + public StundenViewModel(IHoursService hoursService) { + _hoursService = hoursService; + Hours = new Hours(); - /// - /// CTOR - /// - public StundenViewModel() : this(GetServiceOrCreate()) { - } + LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM"); - private static IHoursService GetServiceOrCreate() => new HoursService(new HoursRepository()); + LoadDataCommand = new AsyncRelayCommand(LoadData); + NewEntryCommand = new AsyncRelayCommand(NewEntryAsync); + SelectEntryCommand = new AsyncRelayCommand(SelectEntryAsync); + RefreshListCommand = new AsyncRelayCommand(RefreshList); + RefreshCommand = new Command(async () => await RefreshItemsAsync()); - internal StundenViewModel(IHoursService hoursService) { - _hoursService = hoursService; - Hours = new Hours(); - - LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM"); - - LoadDataCommand = new AsyncRelayCommand(LoadData); - NewEntryCommand = new AsyncRelayCommand(NewEntryAsync); - SelectEntryCommand = new AsyncRelayCommand(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); + } + }); + } + - /// - /// Öffnet eine neue Stundeneingabe - /// - 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}"); - } + /// + /// Öffnet eine neue Stundeneingabe + /// + 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}"); + } - /// - /// Öffnet eine bestehende Stundeneingabe - /// - private async Task SelectEntryAsync(DayTime entry) { - if (entry != null && entry.Id != null) { - //var navigationParameters = new Dictionary { { "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"); - } + /// + /// Öffnet eine bestehende Stundeneingabe + /// + private async Task SelectEntryAsync(DayTime entry) { + if (entry != null && entry.Id != null) { + //var navigationParameters = new Dictionary { { "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)); + } - /// - /// Lädt die Monatssummen für die Übersicht - /// - private async Task LoadData() { - try { - var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday); - Hours = hours; - Settings = settings; + /// + /// Lädt die Monatssummen für die Übersicht + /// + 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); + } + } - /// - /// Lädt die Arbeitszeiten für einen Tag - /// - public async Task LoadDay(DateTime date) { - DayTotal = new TimeOnly(0); - Sollstunden = new TimeOnly(0); - try { - var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date); + /// + /// Lädt die Arbeitszeiten für einen Tag + /// + public async Task LoadDay(DateTime date) { + // kleine Initialwerte sind ok, aber UI-Thread sicher setzen: + await MainThread.InvokeOnMainThreadAsync(() => + { + DayTotal = new TimeOnly(0); + Sollstunden = new TimeOnly(0); + }); + try { + var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date); - DayTimes = dayTimes; - Settings = settings; - GemeindeAktivSet = Settings.GemeindeAktivSet; - ProjektAktivSet = Settings.ProjektAktivSet; + await MainThread.InvokeOnMainThreadAsync(() => + { + DayTimes = dayTimes; + Settings = settings; + GemeindeAktivSet = Settings.GemeindeAktivSet; + ProjektAktivSet = Settings.ProjektAktivSet; - OnPropertyChanged(nameof(GemeindeAktivSet)); - OnPropertyChanged(nameof(ProjektAktivSet)); + OnPropertyChanged(nameof(GemeindeAktivSet)); + OnPropertyChanged(nameof(ProjektAktivSet)); + }); - List _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 _soll; + TimeSpan span = TimeSpan.Zero; + bool merker = false; + foreach (DayTime dt in DayTimes) { + span += dt.End - dt.Begin; + //Nachtstunden dazurechnen + if (dt.Night.Ticks > 0 && !merker) { + span += dt.Night.ToTimeSpan() * .5; + merker = true; + } - //Nach der Tagessumme die anderen Tage anhängen - if (DayTimes != null) { - var more = await _hoursService.GetDayRangeAsync(date, date.AddDays(3)); - if (more != null && more.Count > 0) - DayTimes = DayTimes.Concat(more).ToList(); - } + _soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList(); + if (_soll.Count > 0) + { + var soll = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit)); + await MainThread.InvokeOnMainThreadAsync(() => Sollstunden = soll); + } + } - } catch (Exception e) { - DayTimes = new List(); - //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(); + //TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch. - async void IQueryAttributable.ApplyQueryAttributes(IDictionary 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); + } + }); + - /// - /// Seite aktualisieren - /// - 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 query) { + if (query.ContainsKey("date")) { + await LoadDay(Convert.ToDateTime(query["date"])); + } + } - IsRefreshing = false; - } + /// + /// Seite aktualisieren + /// + private async Task RefreshItemsAsync() { + IsRefreshing = true; - /// - /// Refreshes all properties - /// - 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; + } + /// + /// Refreshes all properties + /// + 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}"); + } + } +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Views/AllNotesPage.xaml b/Jugenddienst Stunden/Views/AllNotesPage.xaml index 7178cda..b09b921 100644 --- a/Jugenddienst Stunden/Views/AllNotesPage.xaml +++ b/Jugenddienst Stunden/Views/AllNotesPage.xaml @@ -1,4 +1,5 @@ - + + - + - - + + SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"> @@ -42,8 +44,8 @@ - - + + diff --git a/Jugenddienst Stunden/Views/AllNotesPage.xaml.cs b/Jugenddienst Stunden/Views/AllNotesPage.xaml.cs index 9ae500e..79741b8 100644 --- a/Jugenddienst Stunden/Views/AllNotesPage.xaml.cs +++ b/Jugenddienst Stunden/Views/AllNotesPage.xaml.cs @@ -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) { diff --git a/Jugenddienst Stunden/Views/LoginPage.xaml b/Jugenddienst Stunden/Views/LoginPage.xaml index 03510a2..0d5b820 100644 --- a/Jugenddienst Stunden/Views/LoginPage.xaml +++ b/Jugenddienst Stunden/Views/LoginPage.xaml @@ -1,4 +1,5 @@ - + + - @@ -29,31 +30,33 @@ - - + + - - + + - + - + @@ -62,7 +65,8 @@ - + diff --git a/Jugenddienst Stunden/Views/LoginPage.xaml.cs b/Jugenddienst Stunden/Views/LoginPage.xaml.cs index 97be7b1..d6913d9 100644 --- a/Jugenddienst Stunden/Views/LoginPage.xaml.cs +++ b/Jugenddienst Stunden/Views/LoginPage.xaml.cs @@ -9,197 +9,195 @@ namespace Jugenddienst_Stunden.Views; /// Die Loginseite mit dem Barcodescanner /// 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); - /// - /// CTOR - /// - public LoginPage() { - InitializeComponent(); + /// + /// CTOR + /// + public LoginPage() { + InitializeComponent(); - barcodeScannerView.Options = new BarcodeReaderOptions { - Formats = BarcodeFormat.QrCode, - AutoRotate = true, - Multiple = false - }; + barcodeScannerView.Options = + new BarcodeReaderOptions { Formats = BarcodeFormat.QrCode, AutoRotate = true, Multiple = false }; - //if (BindingContext is LoginViewModel vm) { - // vm.AlertEvent += Vm_AlertEvent; - // vm.InfoEvent += Vm_InfoEvent; - // vm.MsgEvent += Vm_MsgEvent; - //} + //if (BindingContext is LoginViewModel vm) { + // vm.AlertEvent += Vm_AlertEvent; + // vm.InfoEvent += Vm_InfoEvent; + // vm.MsgEvent += Vm_MsgEvent; + //} - //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken - bool sqr = true; - bool sma = false; - if (Preferences.Default.Get("logintype", "") == "manual") { - sqr = false; - sma = true; - LoginSwitch.IsToggled = true; - Message.IsVisible = false; - } else { - LoginSwitch.IsToggled = false; - Message.IsVisible = true; - } - LoginQR.IsVisible = sqr; - LoginManual.IsVisible = sma; - } + //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken + bool sqr = true; + bool sma = false; + if (Preferences.Default.Get("logintype", "") == "manual") { + sqr = false; + sma = true; + LoginSwitch.IsToggled = true; + Message.IsVisible = false; + } else { + LoginSwitch.IsToggled = false; + Message.IsVisible = true; + } + + LoginQR.IsVisible = sqr; + LoginManual.IsVisible = sma; + } - /// - /// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt - /// ZXing.Net.Maui.Controls 0.4.4 - /// - private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e) { + /// + /// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt + /// ZXing.Net.Maui.Controls 0.4.4 + /// + 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ück - await Navigation.PopAsync(); - } else { - //Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stundenü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�ck + await Navigation.PopAsync(); + } else { + //Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden�bersicht + await Shell.Current.GoToAsync($"//StundenPage"); + } + } catch (Exception e) { + await DisplayAlert("Fehler", e.Message, "OK"); + } + }); + } else { + MainThread.InvokeOnMainThreadAsync(() => { + DisplayAlert("Bereits eingeloggt", + Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""), + "OK"); + }); + } + } + } + } - } catch (Exception e) { - await DisplayAlert("Fehler", e.Message, "OK"); - } + protected override void OnDisappearing() { + base.OnDisappearing(); - }); - } else { - MainThread.InvokeOnMainThreadAsync(() => { - DisplayAlert("Bereits eingeloggt", - Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""), - "OK"); - }); - } - } - } + barcodeScannerView.CameraLocation = CameraLocation.Front; + barcodeScannerView.IsDetecting = false; + } - } + protected override void OnAppearing() { + base.OnAppearing(); - protected override void OnDisappearing() { - base.OnDisappearing(); + barcodeScannerView.IsDetecting = true; + barcodeScannerView.CameraLocation = CameraLocation.Rear; + } - barcodeScannerView.CameraLocation = CameraLocation.Front; - barcodeScannerView.IsDetecting = false; - } + public bool IsCameraAvailable() { + var status = Permissions.CheckStatusAsync().Result; + if (status != PermissionStatus.Granted) { + status = Permissions.RequestAsync().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().Result; - if (status != PermissionStatus.Granted) { - status = Permissions.RequestAsync().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ü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�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; - /// - /// Aus einer URL ohne Schema eine URL mit Schema machen - /// - 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; + /// + /// Aus einer URL ohne Schema eine URL mit Schema machen + /// + private static string InputUrlWithSchema(string url) { + if (!url.StartsWith("http://") && !url.StartsWith("https://")) { + url = "https://" + url; + } - if (switcher.IsToggled) { - LoginQR.IsVisible = false; - LoginManual.IsVisible = true; - Message.IsVisible = false; - Preferences.Default.Set("logintype", "manual"); - } else { - LoginQR.IsVisible = true; - LoginManual.IsVisible = false; - Message.IsVisible = true; - Preferences.Default.Set("logintype", "qr"); - } + if (url.StartsWith("http://")) { + url = url.Replace("http://", "https://"); + } - } + return url; + } - //private void Vm_AlertEvent(object? sender, string e) { - // DisplayAlert("Fehler:", e, "OK"); - //} - //private void Vm_InfoEvent(object? sender, string e) { - // DisplayAlert("Information:", e, "OK"); - //} - //private async Task Vm_MsgEvent(string title, string message) { - // await DisplayAlert(title, message, "OK"); - //} -} + //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken + private void Switch_Toggled(object sender, ToggledEventArgs e) { + var switcher = (Switch)sender; + + if (switcher.IsToggled) { + LoginQR.IsVisible = false; + LoginManual.IsVisible = true; + Message.IsVisible = false; + Preferences.Default.Set("logintype", "manual"); + } else { + LoginQR.IsVisible = true; + LoginManual.IsVisible = false; + Message.IsVisible = true; + Preferences.Default.Set("logintype", "qr"); + } + } + + //private void Vm_AlertEvent(object? sender, string e) { + // DisplayAlert("Fehler:", e, "OK"); + //} + //private void Vm_InfoEvent(object? sender, string e) { + // DisplayAlert("Information:", e, "OK"); + //} + //private async Task Vm_MsgEvent(string title, string message) { + // await DisplayAlert(title, message, "OK"); + //} +} \ No newline at end of file diff --git a/Jugenddienst Stunden/Views/NotePage.xaml b/Jugenddienst Stunden/Views/NotePage.xaml index e0ec155..f434a40 100644 --- a/Jugenddienst Stunden/Views/NotePage.xaml +++ b/Jugenddienst Stunden/Views/NotePage.xaml @@ -1,4 +1,5 @@ - + + - diff --git a/Jugenddienst Stunden/Views/NotePage.xaml.cs b/Jugenddienst Stunden/Views/NotePage.xaml.cs index 6e67f6e..bd0a5b7 100644 --- a/Jugenddienst Stunden/Views/NotePage.xaml.cs +++ b/Jugenddienst Stunden/Views/NotePage.xaml.cs @@ -4,7 +4,6 @@ namespace Jugenddienst_Stunden.Views; /// Einzelne Notiz /// public partial class NotePage : ContentPage { - /// /// CTOR /// diff --git a/Jugenddienst Stunden/Views/StundePage.xaml b/Jugenddienst Stunden/Views/StundePage.xaml index c3b95cf..eb3cfa2 100644 --- a/Jugenddienst Stunden/Views/StundePage.xaml +++ b/Jugenddienst Stunden/Views/StundePage.xaml @@ -1,4 +1,5 @@ - + + - @@ -31,20 +32,26 @@ - - + + - - + + - - + + @@ -52,8 +59,8 @@ - - + + - + - + - + - + - + TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" /> - + - + - + @@ -129,15 +146,18 @@ - - - - - - + + + + + + - + diff --git a/Jugenddienst Stunden/Views/StundePage.xaml.cs b/Jugenddienst Stunden/Views/StundePage.xaml.cs index 78a97dc..44eb8c2 100644 --- a/Jugenddienst Stunden/Views/StundePage.xaml.cs +++ b/Jugenddienst Stunden/Views/StundePage.xaml.cs @@ -10,45 +10,43 @@ namespace Jugenddienst_Stunden.Views; /// Einzelner Stundeneintrag /// public partial class StundePage : ContentPage { + /// + /// CTOR + /// + public StundePage() { + InitializeComponent(); - /// - /// CTOR - /// - 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 ShowConfirm(string title, string message) { + return await DisplayAlert(title, message, "Passt!", "Na, nor decht nit."); + } - private async Task 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 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 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; + //} } \ No newline at end of file diff --git a/Jugenddienst Stunden/Views/StundenPage.xaml b/Jugenddienst Stunden/Views/StundenPage.xaml index 4257f4c..84370ba 100644 --- a/Jugenddienst Stunden/Views/StundenPage.xaml +++ b/Jugenddienst Stunden/Views/StundenPage.xaml @@ -1,4 +1,5 @@ - + + - - - + Glyph="+" + Size="22" + Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" /> - - + - + - + + MaximumDate="{Binding MaximumDate}" + Date="{Binding DateToday}" Format="dddd, d. MMMM yyyy" /> - + - - + - + - - + - + - + @@ -101,27 +103,30 @@ - + - + - - + + - + - + - @@ -133,15 +138,23 @@ - - - - - - + + + + + + - + diff --git a/Jugenddienst Stunden/Views/StundenPage.xaml.cs b/Jugenddienst Stunden/Views/StundenPage.xaml.cs index dc85f7f..8638b1f 100644 --- a/Jugenddienst Stunden/Views/StundenPage.xaml.cs +++ b/Jugenddienst Stunden/Views/StundenPage.xaml.cs @@ -9,60 +9,80 @@ namespace Jugenddienst_Stunden.Views; /// Code-Behind für die Stunden-Übersicht /// public partial class StundenPage : ContentPage { + /// + /// CTOR (für Shell/XAML DataTemplate erforderlich) + /// + public StundenPage() : this( + (Application.Current?.Handler?.MauiContext?.Services + ?? throw new InvalidOperationException("DI container ist nicht verfügbar.")) + .GetRequiredService()) { + } - /// - /// CTOR - /// - public StundenPage() { - InitializeComponent(); + /// + /// CTOR (DI) + /// + 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ü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"); }); + } - /// - /// Beim Laden der Seite den Titel setzen - /// - 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", "") != ""; - } + /// + /// Beim Laden der Seite den Titel setzen + /// + 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)); + } } \ No newline at end of file