using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Jugenddienst_Stunden.Models; using Jugenddienst_Stunden.Types; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; using Jugenddienst_Stunden.Interfaces; namespace Jugenddienst_Stunden.ViewModels; /// /// ViewModel für die Stundenliste /// 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 event EventHandler AlertEvent; public event EventHandler InfoEvent; /// /// Beschriftung Button Monatsübersicht /// [ObservableProperty] private string loadOverview; //private HoursBase HoursBase = new HoursBase(); internal Settings Settings = new Settings(); /// /// Zu leistende Stunden /// [ObservableProperty] private TimeOnly sollstunden; /// /// Geleistete Stunden an einem Tag /// [ObservableProperty] private TimeOnly dayTotal; /// /// Liste der Tageszeiten /// [ObservableProperty] private List dayTimes = new List(); public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname; [ObservableProperty] private Hours hours; /// /// 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); } /// /// 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"); // 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: Geleistete Stunden /// public double? ZeitCalculated { get => Hours.zeit_total; } /// /// Monatsübersicht: Sollstunden /// public double? Nominal { get => Hours.nominal; } /// /// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden /// public double? Overtime { get => Hours.overtime; } /// /// Monatsübersicht: Restüberstunden insgesamt /// public double OvertimeMonth { get => Hours.overtime_month; } public double Zeitausgleich { get => Hours.zeitausgleich; } 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 Projekte verwendet werden? /// public bool ProjektAktivSet { get; set; } private bool doContinue = true; /// /// CTOR (DI) /// public 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); // 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 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)); } /// /// 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); } } /// /// 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); await MainThread.InvokeOnMainThreadAsync(() => { DayTimes = dayTimes; Settings = settings; GemeindeAktivSet = Settings.GemeindeAktivSet; ProjektAktivSet = Settings.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) { var soll = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit)); await MainThread.InvokeOnMainThreadAsync(() => Sollstunden = soll); } } var total = TimeOnly.FromTimeSpan(span); await MainThread.InvokeOnMainThreadAsync(() => DayTotal = total); //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) { 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. 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 { await MainThread.InvokeOnMainThreadAsync(() => { OnPropertyChanged(nameof(DayTotal)); OnPropertyChanged(nameof(Sollstunden)); OnPropertyChanged(nameof(DateToday)); OnPropertyChanged(nameof(LoadOverview)); }); } } async void IQueryAttributable.ApplyQueryAttributes(IDictionary query) { if (query.ContainsKey("date")) { await LoadDay(Convert.ToDateTime(query["date"])); } } /// /// Seite aktualisieren /// private async Task RefreshItemsAsync() { IsRefreshing = true; //await Task.Delay(2000); // Simuliert eine Datenaktualisierung await LoadDay(DateToday); 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}"); } } }