Files
Jugenddienst-Stunden/Jugenddienst Stunden/ViewModels/StundenViewModel.cs
Daniel Pichler 76eb71946f Refactor Api-Client
Add Exceptionhandler, AlertService JSON-Converter
AppSettings via DI

Reformat Code
2025-12-17 09:34:08 +01:00

370 lines
9.8 KiB
C#

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;
/// <summary>
/// ViewModel für die Stundenliste
/// </summary>
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<string> AlertEvent;
public event EventHandler<string> InfoEvent;
/// <summary>
/// Beschriftung Button Monatsübersicht
/// </summary>
[ObservableProperty] private string loadOverview;
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
/// <summary>
/// Zu leistende Stunden
/// </summary>
[ObservableProperty] private TimeOnly sollstunden;
/// <summary>
/// Geleistete Stunden an einem Tag
/// </summary>
[ObservableProperty] private TimeOnly dayTotal;
/// <summary>
/// Liste der Tageszeiten
/// </summary>
[ObservableProperty] private List<DayTime> dayTimes = new List<DayTime>();
public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname;
[ObservableProperty] private Hours hours;
/// <summary>
/// Mindest-Datum für den Datepicker
/// </summary>
public DateTime MinimumDate {
get => DateTime.Today.AddDays(-365);
}
/// <summary>
/// Höchst-Datum für den Datepicker
/// </summary>
public DateTime MaximumDate {
get => DateTime.Today.AddDays(60);
}
/// <summary>
/// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen
/// </summary>
private DateTime dateToday = DateTime.Today;
public DateTime DateToday {
get => dateToday;
set {
if (dateToday != value) {
dateToday = value;
LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM yy");
// Task.Run(() => LoadDay(value));
// NICHT Task.Run: LoadDay aktualisiert UI-gebundene Properties
MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
await LoadDay(dateToday);
}
catch (Exception ex)
{
AlertEvent?.Invoke(this, ex.Message);
}
});
}
}
}
/// <summary>
/// Monatsübersicht: Geleistete Stunden
/// </summary>
public double? ZeitCalculated {
get => Hours.Zeit_total;
}
/// <summary>
/// Monatsübersicht: Sollstunden
/// </summary>
public double? Nominal {
get => Hours.Nominal;
}
/// <summary>
/// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden
/// </summary>
public double? Overtime {
get => Hours.overtime;
}
/// <summary>
/// Monatsübersicht: Restüberstunden insgesamt
/// </summary>
public double OvertimeMonth {
get => Hours.overtime_month;
}
public double Zeitausgleich {
get => Hours.zeitausgleich;
}
public double ZeitausgleichMonth {
get => Hours.zeitausgleich_month;
}
/// <summary>
/// Monatsübersicht: Resturlaub
/// </summary>
public double Holiday {
get => Hours.holiday;
}
/// <summary>
/// Seite neu laden
/// </summary>
[ObservableProperty] private bool isRefreshing;
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
public bool ProjektAktivSet { get; set; }
private bool doContinue = true;
/// <summary>
/// CTOR (DI)
/// </summary>
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<DayTime>(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);
}
});
}
/// <summary>
/// Öffnet eine neue Stundeneingabe
/// </summary>
private async Task NewEntryAsync() {
//Hier muss das Datum übergeben werden
//await Shell.Current.GoToAsync(nameof(Views.StundePage));
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?date={dateToday:yyyy-MM-dd}");
}
/// <summary>
/// Öffnet eine bestehende Stundeneingabe
/// </summary>
private async Task SelectEntryAsync(DayTime entry) {
if (entry != null && entry.Id != null) {
//var navigationParameters = new Dictionary<string, object> { { "load", entry.id } };
//await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}", navigationParameters);
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?load={entry.Id}");
} else AlertEvent?.Invoke(this, "Auswahl enthält keine Daten");
}
private async Task RefreshList() {
OnPropertyChanged(nameof(DayTimes));
}
/// <summary>
/// Lädt die Monatssummen für die Übersicht
/// </summary>
private async Task LoadData() {
try {
var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
Hours = hours;
Settings = settings;
if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this,
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
" installiert)");
}
//_hour = await HoursBase.LoadData();
RefreshProperties();
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
/// <summary>
/// Lädt die Arbeitszeiten für einen Tag
/// </summary>
public async Task LoadDay(DateTime date) {
// 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<Sollstunden> _soll;
TimeSpan span = TimeSpan.Zero;
bool merker = false;
foreach (DayTime dt in DayTimes) {
span += dt.End - dt.Begin;
//Nachtstunden dazurechnen
if (dt.Night.Ticks > 0 && !merker) {
span += dt.Night.ToTimeSpan() * .5;
merker = true;
}
_soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
if (_soll.Count > 0)
{
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, date.AddDays(3));
if (more != null && more.Count > 0)
{
await MainThread.InvokeOnMainThreadAsync(() =>
DayTimes = DayTimes.Concat(more).ToList()
);
}
}
} catch (Exception e) {
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTimes = new List<DayTime>();
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
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<string, object> query) {
if (query.ContainsKey("date")) {
await LoadDay(Convert.ToDateTime(query["date"]));
}
}
/// <summary>
/// Seite aktualisieren
/// </summary>
private async Task RefreshItemsAsync() {
IsRefreshing = true;
//await Task.Delay(2000); // Simuliert eine Datenaktualisierung
await LoadDay(DateToday);
IsRefreshing = false;
}
/// <summary>
/// Refreshes all properties
/// </summary>
private void RefreshProperties() {
OnPropertyChanged(nameof(Hours));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Nominal));
OnPropertyChanged(nameof(Overtime));
OnPropertyChanged(nameof(OvertimeMonth));
OnPropertyChanged(nameof(Zeitausgleich));
OnPropertyChanged(nameof(ZeitCalculated));
OnPropertyChanged(nameof(Holiday));
OnPropertyChanged(nameof(MinimumDate));
OnPropertyChanged(nameof(MaximumDate));
OnPropertyChanged(nameof(LoadOverview));
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
try {
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
} catch (Exception ex) {
AlertEvent?.Invoke(this, ex.Message);
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
}
}
}