diff --git a/Jugenddienst Stunden/Infrastructure/ApiClient.cs b/Jugenddienst Stunden/Infrastructure/ApiClient.cs index e5d2ca4..729965c 100644 --- a/Jugenddienst Stunden/Infrastructure/ApiClient.cs +++ b/Jugenddienst Stunden/Infrastructure/ApiClient.cs @@ -47,9 +47,11 @@ internal sealed class ApiClient : IApiClient { public async Task SendAsync(HttpMethod method, string path, object? body = null, IDictionary? query = null, CancellationToken ct = default) { + // Absolute URI aus aktuellem Settings‑BaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen. var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query); using var req = new HttpRequestMessage(method, uri); + // Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist) // Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten. // AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen. @@ -134,43 +136,44 @@ internal sealed class ApiClient : IApiClient { throw new InvalidOperationException( "ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl')."); - // Basis muss absolut sein (z. B. https://host/appapi/) - var baseUri = new Uri(baseUrl, UriKind.Absolute); + var normalizedBase = baseUrl.Trim(); // Pfad relativ zur Basis aufbauen string relativePath = path ?? string.Empty; + + if (relativePath.StartsWith('/')) + relativePath = relativePath.TrimStart('/'); + if (query is not null && query.Count > 0) { + if (normalizedBase.EndsWith('/')) + normalizedBase = normalizedBase.TrimEnd('/'); + var sb = new StringBuilder(relativePath); sb.Append(relativePath.Contains('?') ? '&' : '?'); sb.Append(string.Join('&', query .Where(kv => kv.Value is not null) .Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}"))); relativePath = sb.ToString(); + } else { + if (!normalizedBase.EndsWith('/')) + normalizedBase += "/"; } + var baseUri = new Uri(normalizedBase, UriKind.Absolute); + // Wenn path bereits absolut ist, direkt verwenden - //if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath)) - // return absoluteFromPath; - - // Sonderfall: Wenn path ein absoluter file:// URI ist, diesen relativ zur Basis behandeln - // Weiß nicht wie file:// zustande kommt, vermutlich wäre das zu verhindern - if (Uri.TryCreate(relativePath, UriKind.Absolute, out var uri)) { - if (uri.Scheme == Uri.UriSchemeFile) { - - var normalizedBase = baseUrl.Trim(); - if (!normalizedBase.EndsWith('/')) - normalizedBase += "/"; - - if (relativePath.StartsWith('/')) - relativePath = relativePath.TrimStart('/'); - - var baseUriNormalized = new Uri(normalizedBase, UriKind.Absolute); - return new Uri(baseUriNormalized, relativePath); - } - return uri; + if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath)) { + var uriString = absoluteFromPath.ToString(); + if (uriString.EndsWith('/') && absoluteFromPath.AbsolutePath.Length > 1) + return new Uri(uriString.TrimEnd('/')); + return absoluteFromPath; } + var result = new Uri(baseUri, relativePath); + var finalUriString = result.ToString(); + if (finalUriString.EndsWith('/') && result.AbsolutePath.Length > 1) + return new Uri(finalUriString.TrimEnd('/')); - return new Uri(baseUri, relativePath); + return result; } } \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/PreferencesAppSettings.cs b/Jugenddienst Stunden/Infrastructure/AppSettings.cs similarity index 56% rename from Jugenddienst Stunden/Infrastructure/PreferencesAppSettings.cs rename to Jugenddienst Stunden/Infrastructure/AppSettings.cs index 4021953..8dfe933 100644 --- a/Jugenddienst Stunden/Infrastructure/PreferencesAppSettings.cs +++ b/Jugenddienst Stunden/Infrastructure/AppSettings.cs @@ -1,8 +1,18 @@ using Jugenddienst_Stunden.Interfaces; +using Jugenddienst_Stunden.Types; namespace Jugenddienst_Stunden.Infrastructure; -internal sealed class PreferencesAppSettings : IAppSettings { +/// +/// Represents the application settings and provides access to user preferences +/// such as API URL, API key, employee ID, and personal details. +/// +/// +/// The AppSettings class implements the IAppSettings interface and manages +/// persistent configuration settings needed for the application. +/// These settings include preferences like API configuration and user identification details. +/// +internal sealed class AppSettings : IAppSettings { public string ApiUrl { get => Preferences.Default.Get("apiUrl", ""); set => Preferences.Default.Set("apiUrl", value); @@ -27,4 +37,5 @@ internal sealed class PreferencesAppSettings : IAppSettings { get => Preferences.Default.Get("surname", "Eingeloggt"); set => Preferences.Default.Set("surname", value); } + public Settings? Settings { get; set; } } \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/TokenProvider.cs b/Jugenddienst Stunden/Infrastructure/TokenProvider.cs deleted file mode 100644 index e4b5e5a..0000000 --- a/Jugenddienst Stunden/Infrastructure/TokenProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -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/IApiClient.cs b/Jugenddienst Stunden/Interfaces/IApiClient.cs index 8821d06..2867d88 100644 --- a/Jugenddienst Stunden/Interfaces/IApiClient.cs +++ b/Jugenddienst Stunden/Interfaces/IApiClient.cs @@ -1,5 +1,8 @@ namespace Jugenddienst_Stunden.Interfaces; +/// +/// Defines methods for making HTTP requests to a specified API. +/// internal interface IApiClient { Task GetAsync(string path, IDictionary? query = null, CancellationToken ct = default); diff --git a/Jugenddienst Stunden/Interfaces/IAppSettings.cs b/Jugenddienst Stunden/Interfaces/IAppSettings.cs index 0c21101..e966dd4 100644 --- a/Jugenddienst Stunden/Interfaces/IAppSettings.cs +++ b/Jugenddienst Stunden/Interfaces/IAppSettings.cs @@ -1,5 +1,10 @@ +using Jugenddienst_Stunden.Types; + namespace Jugenddienst_Stunden.Interfaces; +/// +/// Represents the application settings required for configuration and operation of the application. +/// public interface IAppSettings { string ApiUrl { get; set; } string ApiKey { get; set; } @@ -7,4 +12,6 @@ public interface IAppSettings { int EmployeeId { get; set; } string Name { get; set; } string Surname { get; set; } + + Settings? Settings { get; set; } } \ No newline at end of file diff --git a/Jugenddienst Stunden/MauiProgram.cs b/Jugenddienst Stunden/MauiProgram.cs index 6cd4347..98dadaa 100644 --- a/Jugenddienst Stunden/MauiProgram.cs +++ b/Jugenddienst Stunden/MauiProgram.cs @@ -51,7 +51,7 @@ public static class MauiProgram { builder.Services.AddSingleton(); // DI: Settings aus Preferences (Single Source of Truth bleibt Preferences) - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // DI: ApiOptions IMMER aus aktuellen Settings erzeugen (nicht beim Start einfrieren) builder.Services.AddTransient(sp => new ApiOptions { diff --git a/Jugenddienst Stunden/Models/BaseFunc.cs b/Jugenddienst Stunden/Models/BaseFunc.cs index 67d2a60..fb9a240 100644 --- a/Jugenddienst Stunden/Models/BaseFunc.cs +++ b/Jugenddienst Stunden/Models/BaseFunc.cs @@ -100,82 +100,7 @@ internal static class BaseFunc { new() { Date = File.GetLastWriteTime(filename) }; } - /// - /// 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"); - } + - 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); - } - } + } \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/GlobalVar.cs b/Jugenddienst Stunden/Models/GlobalVar.cs deleted file mode 100644 index cf1fd1c..0000000 --- a/Jugenddienst Stunden/Models/GlobalVar.cs +++ /dev/null @@ -1,32 +0,0 @@ -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; } -} \ No newline at end of file diff --git a/Jugenddienst Stunden/Models/HoursBase.cs b/Jugenddienst Stunden/Models/HoursBase.cs deleted file mode 100644 index 8b091e7..0000000 --- a/Jugenddienst Stunden/Models/HoursBase.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Jugenddienst_Stunden.Types; -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; - } - - /// - /// 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; - } - - /// - /// 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; - } - - /// - /// 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); - - return stunde; - } - - /// - /// 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/Repositories/HoursRepository.cs b/Jugenddienst Stunden/Repositories/HoursRepository.cs index 27e0631..3f5865c 100644 --- a/Jugenddienst Stunden/Repositories/HoursRepository.cs +++ b/Jugenddienst Stunden/Repositories/HoursRepository.cs @@ -62,7 +62,7 @@ internal class HoursRepository : IHoursRepository { } public Task DeleteEntry(DayTime stunde) - => _api.DeleteAsync($"/entry/{stunde.Id}"); + => _api.DeleteAsync($"entry/{stunde.Id}"); private static Dictionary QueryToDictionary(string query) { var dict = new Dictionary(); diff --git a/Jugenddienst Stunden/ViewModels/LoginViewModel.cs b/Jugenddienst Stunden/ViewModels/LoginViewModel.cs index b67742f..0c26c6c 100644 --- a/Jugenddienst Stunden/ViewModels/LoginViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/LoginViewModel.cs @@ -10,7 +10,6 @@ namespace Jugenddienst_Stunden.ViewModels; /// public partial class LoginViewModel : ObservableObject { private readonly IAuthService _auth; - private readonly IAppSettings _settings; private readonly IAlertService? _alerts; private DateTime _lastDetectionTime = DateTime.MinValue; private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5); @@ -59,9 +58,8 @@ public partial class LoginViewModel : ObservableObject { // Explizite Command-Property für den QR-Scanner-Event, damit das Binding in XAML zuverlässig greift public IAsyncRelayCommand QrDetectedCommand { get; } - public LoginViewModel(IAuthService auth, IAppSettings settings) { + public LoginViewModel(IAuthService auth) { _auth = auth; - _settings = settings; // gespeicherte Präferenz für Logintyp laden var lt = Preferences.Default.Get("logintype", "qr"); @@ -81,7 +79,7 @@ public partial class LoginViewModel : ObservableObject { } // DI-Konstruktor: AlertService anbinden und Alerts an VM-Event weiterreichen (analog StundeViewModel) - internal LoginViewModel(IAuthService auth, IAppSettings settings, IAlertService alertService) : this(auth, settings) { + internal LoginViewModel(IAuthService auth, IAlertService alertService) : this(auth) { _alerts = alertService; if (alertService is not null) { alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg); @@ -102,7 +100,7 @@ public partial class LoginViewModel : ObservableObject { var user = await _auth.LoginWithCredentials(Username?.Trim() ?? string.Empty, Password ?? string.Empty, (Server ?? string.Empty).Trim()); - + Title = $"{user.Name} {user.Surname}"; // Info zeigen und auf Bestätigung warten var args = new ConfirmationEventArgs("Information:", "Login erfolgreich"); diff --git a/Jugenddienst Stunden/ViewModels/StundeViewModel.cs b/Jugenddienst Stunden/ViewModels/StundeViewModel.cs index 1ceeb4b..d700831 100644 --- a/Jugenddienst Stunden/ViewModels/StundeViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/StundeViewModel.cs @@ -19,13 +19,13 @@ namespace Jugenddienst_Stunden.ViewModels; /// public partial class StundeViewModel : ObservableObject, IQueryAttributable { private readonly IHoursService _hoursService; + private readonly IAppSettings _settings; + private readonly IAlertService _alertService; 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(); public event EventHandler AlertEvent; public event EventHandler InfoEvent; @@ -77,36 +77,19 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable { //public ICommand LoadDataCommand { get; private set; } - public StundeViewModel(IHoursService hoursService, IAlertService alertService) { + public StundeViewModel(IHoursService hoursService, IAlertService alertService, IAppSettings settings) { _hoursService = hoursService; + _settings = settings; + _alertService = alertService; SaveCommand = new AsyncRelayCommand(Save); DeleteConfirmCommand = new Command(async () => await DeleteConfirm()); - if (alertService is not null) { - alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg); - } - - //LoadSettingsAsync(); + _alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg); } - private async void LoadSettingsAsync() { - try { - Settings = await _hoursService.GetSettingsAsync(); - GlobalVar.Settings = Settings; - OptionsGemeinde = Settings.Gemeinden; - OptionsProjekt = Settings.Projekte; - OptionsFreistellung = Settings.Freistellungen; - - GemeindeAktivSet = Settings.GemeindeAktivSet; - ProjektAktivSet = Settings.ProjektAktivSet; - } catch (Exception e) { - AlertEvent?.Invoke(this, e.Message); - } - } - - private async void UpdateSettingsAsync(Settings settings) { - GlobalVar.Settings = settings; + private void UpdateSettings(Settings settings) { + _settings.Settings = settings; OptionsGemeinde = settings.Gemeinden; OptionsProjekt = settings.Projekte; OptionsFreistellung = settings.Freistellungen; @@ -126,7 +109,7 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable { } //Projekt ist ein Pflichtfeld - if (Settings.ProjektAktivSet) { + if (_settings.Settings.ProjektAktivSet) { var projektId = DayTime.ProjektAktiv?.Id ?? 0; if (projektId == 0) { proceed = false; @@ -135,7 +118,7 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable { } //Gemeinde ist ein Pflichtfeld - if (Settings.GemeindeAktivSet) { + if (_settings.Settings.GemeindeAktivSet) { var gemeindeId = DayTime.GemeindeAktiv?.Id ?? 0; if (gemeindeId == 0) { proceed = false; @@ -200,17 +183,18 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable { //DateTime heute = DateTime.Now; try { //var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"])); - var (entry, settings, daytimes) = await _hoursService.GetEntryWithSettingsAsync(Convert.ToInt32(query["load"])); - UpdateSettingsAsync(settings); + var (entry, settings, daytimes) = + await _hoursService.GetEntryWithSettingsAsync(Convert.ToInt32(query["load"])); + UpdateSettings(settings); DayTime = entry; DayTime.TimeSpanVon = entry.Begin.ToTimeSpan(); DayTime.TimeSpanBis = entry.End.ToTimeSpan(); DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? - new Gemeinde(); + new Gemeinde(); DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? - new Projekt(); + new Projekt(); DayTime.FreistellungAktiv = OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? new Freistellung(); @@ -229,11 +213,6 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable { OnPropertyChanged(nameof(DayTimes)); } 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(DayTimes)); } //OnPropertyChanged(nameof(DayTime)); @@ -246,7 +225,7 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable { //Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen try { var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date); - UpdateSettingsAsync(settings); + UpdateSettings(settings); DayTimes = list; OnPropertyChanged(nameof(DayTimes)); } catch (Exception) { @@ -257,7 +236,7 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable { } finally { DayTime = new DayTime(); DayTime.Day = _date; - DayTime.EmployeeId = GlobalVar.EmployeeId; + DayTime.EmployeeId = _settings.EmployeeId; DayTime.GemeindeAktiv = new Gemeinde(); DayTime.ProjektAktiv = new Projekt(); DayTime.FreistellungAktiv = new Freistellung(); diff --git a/Jugenddienst Stunden/ViewModels/StundenViewModel.cs b/Jugenddienst Stunden/ViewModels/StundenViewModel.cs index ec69b57..7d41cdf 100644 --- a/Jugenddienst Stunden/ViewModels/StundenViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/StundenViewModel.cs @@ -15,6 +15,7 @@ namespace Jugenddienst_Stunden.ViewModels; /// public partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged { private readonly IHoursService _hoursService; + private readonly IAppSettings _settings; public ICommand NewEntryCommand { get; } public ICommand SelectEntryCommand { get; } @@ -50,7 +51,13 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN /// [ObservableProperty] private List dayTimes = new List(); - public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname; + /// + /// Der Titel der Stundenübersicht ist der aktuelle Benutzername + /// + public string Title { + get => _settings.Name + " " + _settings.Surname; + set; + } [ObservableProperty] private Hours hours; @@ -82,14 +89,10 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN 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 - { + MainThread.BeginInvokeOnMainThread(async () => { + try { await LoadDay(dateToday); - } - catch (Exception ex) - { + } catch (Exception ex) { AlertEvent?.Invoke(this, ex.Message); } }); @@ -162,8 +165,9 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN /// /// CTOR (DI) /// - public StundenViewModel(IHoursService hoursService) { + public StundenViewModel(IHoursService hoursService, IAppSettings appSettings) { _hoursService = hoursService; + _settings = appSettings; Hours = new Hours(); LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM"); @@ -177,19 +181,15 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN // 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 - { + MainThread.BeginInvokeOnMainThread(async () => { + try { await LoadDay(DateTime.Today); - } - catch (Exception ex) - { + } catch (Exception ex) { AlertEvent?.Invoke(this, ex.Message); } }); } - + /// @@ -244,16 +244,14 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN /// public async Task LoadDay(DateTime date) { // kleine Initialwerte sind ok, aber UI-Thread sicher setzen: - await MainThread.InvokeOnMainThreadAsync(() => - { + await MainThread.InvokeOnMainThreadAsync(() => { DayTotal = new TimeOnly(0); Sollstunden = new TimeOnly(0); }); try { var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date); - await MainThread.InvokeOnMainThreadAsync(() => - { + await MainThread.InvokeOnMainThreadAsync(() => { DayTimes = dayTimes; Settings = settings; GemeindeAktivSet = Settings.GemeindeAktivSet; @@ -275,8 +273,7 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN } _soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList(); - if (_soll.Count > 0) - { + if (_soll.Count > 0) { var soll = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit)); await MainThread.InvokeOnMainThreadAsync(() => Sollstunden = soll); } @@ -288,17 +285,15 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN //Nach der Tagessumme die anderen Tage anhängen if (DayTimes != null) { var more = await _hoursService.GetDayRangeAsync(date.AddDays(1), date.AddDays(3)); - if (more != null && more.Count > 0) - { + if (more != null && more.Count > 0) { await MainThread.InvokeOnMainThreadAsync(() => DayTimes = DayTimes.Concat(more).ToList() ); } } } catch (Exception e) { - - await MainThread.InvokeOnMainThreadAsync(() => - { + + await MainThread.InvokeOnMainThreadAsync(() => { DayTimes = new List(); //TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch. @@ -310,12 +305,11 @@ public partial class StundenViewModel : ObservableObject, IQueryAttributable, IN InfoEvent?.Invoke(this, e.Message); } }); - - + + } finally { - await MainThread.InvokeOnMainThreadAsync(() => - { + await MainThread.InvokeOnMainThreadAsync(() => { OnPropertyChanged(nameof(DayTotal)); OnPropertyChanged(nameof(Sollstunden)); OnPropertyChanged(nameof(DateToday)); diff --git a/Jugenddienst Stunden/Views/LoginPage.xaml.cs b/Jugenddienst Stunden/Views/LoginPage.xaml.cs index acbe83a..88199f0 100644 --- a/Jugenddienst Stunden/Views/LoginPage.xaml.cs +++ b/Jugenddienst Stunden/Views/LoginPage.xaml.cs @@ -1,3 +1,4 @@ +using Jugenddienst_Stunden.Interfaces; using Jugenddienst_Stunden.Models; using Jugenddienst_Stunden.Types; using Jugenddienst_Stunden.ViewModels; @@ -10,10 +11,7 @@ namespace Jugenddienst_Stunden.Views; /// Die Loginseite mit dem Barcodescanner /// public partial class LoginPage : ContentPage { - private DateTime _lastDetectionTime; - private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5); - - + /// /// CTOR /// @@ -24,7 +22,7 @@ public partial class LoginPage : ContentPage { try { if (BindingContext is null) { var sp = Application.Current?.Handler?.MauiContext?.Services - ?? throw new InvalidOperationException("DI container ist nicht verfügbar."); + ?? throw new InvalidOperationException("DI container ist nicht verfügbar."); BindingContext = sp.GetRequiredService(); } } catch (Exception) { @@ -59,68 +57,8 @@ public partial class LoginPage : ContentPage { } }; - //if (BindingContext is LoginViewModel vm) { - // vm.AlertEvent += Vm_AlertEvent; - // vm.InfoEvent += Vm_InfoEvent; - // vm.MsgEvent += Vm_MsgEvent; - //} - - //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken - // MVVM übernimmt Umschalten über IsManualMode im ViewModel; keine Code-Behind-Umschaltung mehr - } - - - /// - /// 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"); - - 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; - - 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"); - } - } catch (Exception e) { - await DisplayAlert("Fehler", e.Message, "OK"); - } - }); - } else { - MainThread.InvokeOnMainThreadAsync(() => { - DisplayAlert("Bereits eingeloggt", - Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""), - "OK"); - }); - } - } - } } + protected override void OnDisappearing() { base.OnDisappearing(); @@ -135,79 +73,7 @@ public partial class LoginPage : ContentPage { // IsDetecting wird via Binding vom ViewModel gesteuert 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; + - 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)); - - Types.User response = - await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi"); - - GlobalVar.ApiKey = response.Token; - GlobalVar.Name = response.Name; - GlobalVar.Surname = response.Surname; - GlobalVar.EmployeeId = response.Id; - GlobalVar.ApiUrl = 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; - - 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"); - } - } - - /// - /// 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; - } - - //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken - // Umschalt-Logik erfolgt über Binding an IsManualMode im ViewModel - - //private void Vm_AlertEvent(object? sender, string e) { - // DisplayAlert("Fehler:", e, "OK"); - //} - //private void Vm_InfoEvent(object? sender, string e) { - // DisplayAlert("Information:", e, "OK"); - //} - //private async Task Vm_MsgEvent(string title, string message) { - // await DisplayAlert(title, message, "OK"); - //} } \ No newline at end of file diff --git a/Jugenddienst Stunden/Views/StundenPage.xaml.cs b/Jugenddienst Stunden/Views/StundenPage.xaml.cs index 8638b1f..cc19328 100644 --- a/Jugenddienst Stunden/Views/StundenPage.xaml.cs +++ b/Jugenddienst Stunden/Views/StundenPage.xaml.cs @@ -28,24 +28,13 @@ public partial class StundenPage : ContentPage { 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(); @@ -76,9 +65,6 @@ public partial class StundenPage : ContentPage { return Preferences.Default.Get("apiKey", "") != ""; } - // private async void NavigateToTargetPage() { - // await Navigation.PushAsync(new LoginPage()); - // } private Task NavigateToTargetPage() { // Shell-Navigation statt Navigation.PushAsync