using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.AiPlanning; using LehrerApp.Core.Models; using LehrerApp.Desktop.Services; using System.Collections.ObjectModel; using System.Globalization; namespace LehrerApp.Desktop.ViewModels.Groups; // ── Anzeige: Experimentart, GHS-Piktogramme (Nutzerwunsch neben 4.2 "Anhänge je Stunde") ──────── public static class ExperimentKindDisplay { public static string[] Options { get; } = ["Lehrerversuch", "Schülerversuch", "Demonstrationsversuch"]; public static string Label(ExperimentKind kind) => kind switch { ExperimentKind.Schuelerversuch => "Schülerversuch", ExperimentKind.Demonstrationsversuch => "Demonstrationsversuch", _ => "Lehrerversuch", }; public static ExperimentKind FromLabel(string? label) => label switch { "Schülerversuch" => ExperimentKind.Schuelerversuch, "Demonstrationsversuch" => ExperimentKind.Demonstrationsversuch, _ => ExperimentKind.Lehrerversuch, }; /// Für die KI-Antwort, die den unmarkierten Enum-Namen ("Schuelerversuch") statt der /// deutschen Anzeige-Bezeichnung liefert (siehe ai-backend/gbu.php "Antwortformat"). public static ExperimentKind FromWireValue(string? value) => Enum.TryParse(value, ignoreCase: true, out var kind) ? kind : ExperimentKind.Lehrerversuch; } public static class GhsPictogramDisplay { private static readonly (GhsPictogram Value, string Code, string Label)[] All = [ (GhsPictogram.Explosive, "GHS01", "Explosionsgefährlich"), (GhsPictogram.Flammable, "GHS02", "Entzündbar"), (GhsPictogram.Oxidizing, "GHS03", "Brandfördernd"), (GhsPictogram.CompressedGas, "GHS04", "Gase unter Druck"), (GhsPictogram.Corrosive, "GHS05", "Ätzend"), (GhsPictogram.Toxic, "GHS06", "Giftig"), (GhsPictogram.Harmful, "GHS07", "Gesundheitsschädlich/Reizend"), (GhsPictogram.HealthHazard, "GHS08", "Gesundheitsgefährdend"), (GhsPictogram.Environmental, "GHS09", "Umweltgefährdend"), ]; public static string Code(GhsPictogram value) => All.First(o => o.Value == value).Code; public static string Label(GhsPictogram value) => All.First(o => o.Value == value).Label; public static GhsPictogram? FromCode(string? code) { if (string.IsNullOrWhiteSpace(code)) return null; foreach (var option in All) if (string.Equals(option.Code, code.Trim(), StringComparison.OrdinalIgnoreCase)) return option.Value; return null; } public static List BuildOptions(IEnumerable? selected = null) { var selectedSet = (selected ?? []).ToHashSet(); return All.Select(o => new GhsPictogramOption(o.Value, $"{o.Code} · {o.Label}") { IsSelected = selectedSet.Contains(o.Value) }).ToList(); } } public partial class GhsPictogramOption(GhsPictogram value, string label) : ObservableObject { public GhsPictogram Value { get; } = value; public string Label { get; } = label; [ObservableProperty] private bool _isSelected; } // ── Zeile im Gefahrstoff-Editor (Wizard-Schritt 2) ─────────────────────────────────────────────── public partial class HazardSubstanceEditItem : ObservableObject { [ObservableProperty] private string _name = ""; [ObservableProperty] private string _amount = ""; [ObservableProperty] private string _hStatements = ""; [ObservableProperty] private string _pStatements = ""; public ObservableCollection PictogramOptions { get; } public Action? OnRemove { get; set; } public HazardSubstanceEditItem(HazardSubstance? source = null) { PictogramOptions = new ObservableCollection( GhsPictogramDisplay.BuildOptions(source?.GhsPictograms)); if (source is null) return; Name = source.Name; Amount = source.Amount; HStatements = source.HStatements; PStatements = source.PStatements; } [RelayCommand] private void Remove() => OnRemove?.Invoke(this); public HazardSubstance ToModel() => new() { Name = Name.Trim(), Amount = Amount.Trim(), HStatements = HStatements.Trim(), PStatements = PStatements.Trim(), GhsPictograms = PictogramOptions.Where(o => o.IsSelected).Select(o => o.Value).ToList(), }; } // ── Wizard: Gefährdungsbeurteilung anlegen/bearbeiten ──────────────────────────────────────────── /// /// Mehrschrittiger Assistent für eine Gefährdungsbeurteilung zu einem Experiment (Nutzerwunsch /// neben 4.2 "Anhänge je Stunde"). Persistiert NICHT selbst — der Aufrufer (LessonDialog) /// serialisiert zu JSON und hängt es über die bestehende /// Anhang-Infrastruktur an die Lesson an (Dateiname endet auf ".gbu.json", siehe TODO.md 4.2). /// KI-Unterstützung ist optional: ohne // /// bleibt false und der Assistent funktioniert /// rein manuell. /// public partial class HazardAssessmentWizardViewModel : ObservableObject { private static readonly string[] StepTitlesArray = ["Basisdaten", "Gefahrstoffe", "Gefährdungen & Schutzmaßnahmen", "Erste Hilfe & Entsorgung", "Zusammenfassung"]; private readonly AiPlanningService? _ai; private readonly AiSettingsService? _aiSettings; private readonly Unit? _unit; private readonly Lesson? _lesson; [ObservableProperty] private int _stepIndex; [ObservableProperty] private string _title = ""; [ObservableProperty] private string _titleError = ""; [ObservableProperty] private string _groupLabel = ""; [ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy"); [ObservableProperty] private string _kindName = ExperimentKindDisplay.Options[0]; [ObservableProperty] private string _procedure = ""; public ObservableCollection Substances { get; } = []; public bool HasSubstances => Substances.Count > 0; [ObservableProperty] private string _newHazard = ""; public ObservableCollection Hazards { get; } = []; [ObservableProperty] private string _newProtectiveMeasure = ""; public ObservableCollection ProtectiveMeasures { get; } = []; [ObservableProperty] private string _firstAid = ""; [ObservableProperty] private string _disposal = ""; [ObservableProperty] private string _notes = ""; [ObservableProperty] private bool _isAiAssisted; [ObservableProperty] private bool _isAiBusy; [ObservableProperty] private string _aiError = ""; public string AiButtonLabel => IsAiBusy ? "KI fragt..." : "🤖 KI-Entwurf erstellen"; public bool CanUseAi => _ai is not null && _aiSettings is { Enabled: true, IsLoggedIn: true } && _unit is not null && _lesson is not null; public string[] StepTitles => StepTitlesArray; public string StepTitle => StepTitles[StepIndex]; public string StepProgressLabel => $"Schritt {StepIndex + 1} von {StepTitles.Length}"; public int StepCount => StepTitles.Length; public bool IsFirstStep => StepIndex == 0; public bool IsLastStep => StepIndex == StepTitles.Length - 1; public bool IsBasicsStep => StepIndex == 0; public bool IsSubstancesStep => StepIndex == 1; public bool IsHazardsStep => StepIndex == 2; public bool IsFirstAidStep => StepIndex == 3; public bool IsSummaryStep => StepIndex == 4; public string[] KindOptions => ExperimentKindDisplay.Options; public string DialogTitle => _editing ? "Gefährdungsbeurteilung bearbeiten" : "Gefährdungsbeurteilung erstellen"; private readonly bool _editing; public HazardAssessment? Result { get; private set; } public HazardAssessmentWizardViewModel(HazardAssessment? editing, string defaultGroupLabel, AiPlanningService? ai = null, AiSettingsService? aiSettings = null, Unit? unit = null, Lesson? lesson = null) { _ai = ai; _aiSettings = aiSettings; _unit = unit; _lesson = lesson; Substances.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasSubstances)); GroupLabel = defaultGroupLabel; _editing = editing is not null; if (editing is null) return; Title = editing.Title; GroupLabel = editing.GroupLabel; DateText = editing.Date?.ToString("dd.MM.yyyy") ?? ""; KindName = ExperimentKindDisplay.Label(editing.Kind); Procedure = editing.Procedure; foreach (var s in editing.Substances) Substances.Add(NewSubstanceItem(s)); foreach (var h in editing.Hazards) Hazards.Add(h); foreach (var m in editing.ProtectiveMeasures) ProtectiveMeasures.Add(m); FirstAid = editing.FirstAid; Disposal = editing.Disposal; Notes = editing.Notes; IsAiAssisted = editing.IsAiAssisted; } partial void OnIsAiBusyChanged(bool value) => OnPropertyChanged(nameof(AiButtonLabel)); partial void OnStepIndexChanged(int value) { OnPropertyChanged(nameof(StepTitle)); OnPropertyChanged(nameof(StepProgressLabel)); OnPropertyChanged(nameof(IsFirstStep)); OnPropertyChanged(nameof(IsLastStep)); OnPropertyChanged(nameof(IsBasicsStep)); OnPropertyChanged(nameof(IsSubstancesStep)); OnPropertyChanged(nameof(IsHazardsStep)); OnPropertyChanged(nameof(IsFirstAidStep)); OnPropertyChanged(nameof(IsSummaryStep)); } private HazardSubstanceEditItem NewSubstanceItem(HazardSubstance? source = null) { var item = new HazardSubstanceEditItem(source) { OnRemove = RemoveSubstance }; return item; } [RelayCommand] private void AddSubstance() => Substances.Add(NewSubstanceItem()); private void RemoveSubstance(HazardSubstanceEditItem item) => Substances.Remove(item); [RelayCommand] private void AddHazard() { if (string.IsNullOrWhiteSpace(NewHazard)) return; Hazards.Add(NewHazard.Trim()); NewHazard = ""; } [RelayCommand] private void RemoveHazard(string? hazard) { if (hazard is not null) Hazards.Remove(hazard); } [RelayCommand] private void AddProtectiveMeasure() { if (string.IsNullOrWhiteSpace(NewProtectiveMeasure)) return; ProtectiveMeasures.Add(NewProtectiveMeasure.Trim()); NewProtectiveMeasure = ""; } [RelayCommand] private void RemoveProtectiveMeasure(string? measure) { if (measure is not null) ProtectiveMeasures.Remove(measure); } [RelayCommand] private void Next() { if (IsFirstStep && string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; return; } TitleError = ""; if (!IsLastStep) StepIndex++; } [RelayCommand] private void Back() { if (!IsFirstStep) StepIndex--; } /// /// Fragt einen KI-Entwurf zu genau dieser Lesson ab (Thema + Verlaufsplan als Kontext) und /// übernimmt ihn in die Wizard-Felder — überschreibt dabei bewusst den aktuellen Stand, damit /// nach einem erneuten Klick klar ist, was tatsächlich von der KI kommt. Die Lehrkraft prüft /// und passt danach in den einzelnen Schritten an; der Rechtssicherheits-Hinweis wird über /// in den PDF-Export übernommen. /// [RelayCommand] private async Task RequestAiDraft() { if (!CanUseAi) return; var token = _aiSettings!.GetToken(); if (token is null) { AiError = "Nicht angemeldet. Bitte in den Einstellungen bei der KI-Unterstützung anmelden."; return; } AiError = ""; IsAiBusy = true; try { var aiLesson = new AiLesson { Id = _lesson!.Id, Date = _lesson.Date, LessonNumber = _lesson.LessonNumber, Topic = _lesson.Topic, StartTime = _lesson.StartTime, Phases = _lesson.Phases.Select(p => new AiPhaseStep { Name = p.Name, DurationMinutes = p.DurationMinutes, Activity = p.Activity, Material = p.Material, Shorthand = p.Shorthand, }).ToList(), }; var response = await _ai!.RequestHazardAssessmentDraftAsync(_unit!, aiLesson, token); ApplyAiResponse(response); } catch (AiBackendException ex) { AiError = ex.Message; } finally { IsAiBusy = false; } } private void ApplyAiResponse(AiHazardAssessmentResponse response) { if (string.IsNullOrWhiteSpace(Title)) Title = _lesson?.Topic ?? ""; KindName = ExperimentKindDisplay.Label(ExperimentKindDisplay.FromWireValue(response.ExperimentKind)); Procedure = response.Procedure; Substances.Clear(); foreach (var s in response.Substances) Substances.Add(NewSubstanceItem(new HazardSubstance { Name = s.Name, Amount = s.Amount, HStatements = s.HStatements, PStatements = s.PStatements, GhsPictograms = s.GhsPictograms .Select(GhsPictogramDisplay.FromCode) .Where(p => p.HasValue) .Select(p => p!.Value) .ToList(), })); Hazards.Clear(); foreach (var h in response.Hazards) Hazards.Add(h); ProtectiveMeasures.Clear(); foreach (var m in response.ProtectiveMeasures) ProtectiveMeasures.Add(m); FirstAid = response.FirstAid; Disposal = response.Disposal; IsAiAssisted = true; } [RelayCommand] private void Save() { if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; StepIndex = 0; return; } TitleError = ""; DateOnly? date = null; if (!string.IsNullOrWhiteSpace(DateText) && DateOnly.TryParseExact(DateText, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)) date = parsed; Result = new HazardAssessment { Title = Title.Trim(), GroupLabel = GroupLabel.Trim(), Date = date, Kind = ExperimentKindDisplay.FromLabel(KindName), Procedure = Procedure.Trim(), Substances = Substances.Select(s => s.ToModel()).ToList(), Hazards = Hazards.ToList(), ProtectiveMeasures = ProtectiveMeasures.ToList(), FirstAid = FirstAid.Trim(), Disposal = Disposal.Trim(), Notes = Notes.Trim(), IsAiAssisted = IsAiAssisted, }; } }