using Avalonia.Controls; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Platform.Storage; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Desktop.Views.Shared; using Microsoft.Extensions.DependencyInjection; using System.Globalization; namespace LehrerApp.Desktop.Views.Groups; public partial class LessonDialog : Window { public LessonDialog() => InitializeComponent(); protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is LessonDialogViewModel vm) vm.OnPickAlternativePath = ShowAlternativePathDialog; } private async Task ShowAlternativePathDialog(Guid? currentId) { var dialogVm = new AlternativePathDialogViewModel( App.Services.GetRequiredService(), currentId); var dialog = new AlternativePathDialog { DataContext = dialogVm }; var ok = await dialog.ShowDialog(this); return ok ? dialogVm.Result : null; } private void OnSave(object? s, RoutedEventArgs e) { if (DataContext is LessonDialogViewModel vm && vm.SaveCommand.CanExecute(null)) { vm.SaveCommand.Execute(null); if (vm.Result is not null) Close(true); } } private void OnCancel(object? s, RoutedEventArgs e) { if (DataContext is LessonDialogViewModel vm) vm.DiscardUnsavedAttachments(); Close(false); } /// Kopiert den beim letzten KI-"Übernehmen" gespeicherten Materialerstellungs-Prompt (4.5.36, /// Nachtrag zu 4.5.20) erneut in die Zwischenablage — derselbe Mechanismus wie im AiAssistDialog, /// hier aber für einen bereits gespeicherten, nicht mehr nur transienten Vorschlag. private async void OnCopyMaterialPrompt(object? sender, RoutedEventArgs e) { if (sender is not Button { DataContext: PhaseStepEditItem item } || string.IsNullOrWhiteSpace(item.MaterialPrompt)) return; var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; if (clipboard is null) return; await clipboard.SetTextAsync(item.MaterialPrompt); App.Services.GetRequiredService().ShowSuccess("Prompt in die Zwischenablage kopiert."); } private async void OnAddAttachment(object? sender, RoutedEventArgs e) { if (DataContext is not LessonDialogViewModel vm) return; var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = "Anhang auswählen", AllowMultiple = false, }); if (files.Count == 0) return; await using var stream = await files[0].OpenReadAsync(); vm.AddAttachment(files[0].Name, stream); } private async void OnOpenAttachment(object? sender, RoutedEventArgs e) { if (DataContext is not LessonDialogViewModel vm) return; if (sender is not Button { Tag: AttachmentItem item }) return; using var source = vm.OpenAttachment(item); if (source is null) return; var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Anhang speichern unter", SuggestedFileName = item.FileName, }); if (file is null) return; await using var target = await file.OpenWriteAsync(); await source.CopyToAsync(target); } private async void OnCreateHazardAssessment(object? sender, RoutedEventArgs e) { if (DataContext is not LessonDialogViewModel vm) return; var (unit, contextLesson, groupName) = BuildHazardAssessmentContext(vm); var dialogVm = new HazardAssessmentWizardViewModel(null, groupName, App.Services.GetRequiredService(), App.Services.GetRequiredService(), unit, contextLesson); var dialog = new HazardAssessmentWizardDialog { DataContext = dialogVm }; var ok = await dialog.ShowDialog(this); if (!ok || dialogVm.Result is null) return; UploadHazardAssessment(vm, dialogVm.Result); } private async void OnOpenHazardAssessment(object? sender, RoutedEventArgs e) { if (DataContext is not LessonDialogViewModel vm) return; if (sender is not Button { Tag: AttachmentItem item }) return; var editing = await ReadHazardAssessmentAsync(vm, item); if (editing is null) return; var (unit, contextLesson, _) = BuildHazardAssessmentContext(vm); var dialogVm = new HazardAssessmentWizardViewModel(editing, editing.GroupLabel, App.Services.GetRequiredService(), App.Services.GetRequiredService(), unit, contextLesson); var dialog = new HazardAssessmentWizardDialog { DataContext = dialogVm }; var ok = await dialog.ShowDialog(this); if (!ok || dialogVm.Result is null) return; vm.RemoveAttachmentCommand.Execute(item); UploadHazardAssessment(vm, dialogVm.Result); } private async void OnPrintHazardAssessment(object? sender, RoutedEventArgs e) { var topLevel = TopLevel.GetTopLevel(this); if (topLevel is null || DataContext is not LessonDialogViewModel vm) return; if (sender is not Button { Tag: AttachmentItem item }) return; var assessment = await ReadHazardAssessmentAsync(vm, item); if (assessment is null) return; var data = new HazardAssessmentPrintData( assessment.Title, assessment.GroupLabel, assessment.Date?.ToString("dd.MM.yyyy") ?? "", ExperimentKindDisplay.Label(assessment.Kind), assessment.Procedure, assessment.Substances.Select(s => new HazardSubstancePrintRow( s.Name, s.Cas, s.Amount, string.Join(", ", s.GhsPictograms.Select(GhsPictogramDisplay.Code)), s.SignalWord, s.HStatements, s.PStatements, s.ActivityRestriction)).ToList(), assessment.Hazards, assessment.ProtectiveMeasures, assessment.FirstAid, assessment.Disposal, assessment.Notes, assessment.IsAiAssisted); var pdf = App.Services.GetRequiredService().BuildHazardAssessmentPdf(data); await App.Services.GetRequiredService().SaveAsync(topLevel.StorageProvider, ExportFile.Pdf("Gefährdungsbeurteilung als PDF speichern", $"GBU_{assessment.Title}", pdf)); } /// Baut den Kontext für den Gefährdungsbeurteilungs-Assistenten (KI-Anfrage + Vorbelegung) aus /// dem gerade im Dialog bearbeiteten, ggf. noch nicht gespeicherten Stand — funktioniert damit /// auch beim Neuanlegen einer Stunde, bevor überhaupt gespeichert wurde. private (Unit? Unit, Lesson ContextLesson, string GroupName) BuildHazardAssessmentContext(LessonDialogViewModel vm) { var unit = App.Services.GetRequiredService().GetById(vm.UnitId); var groupName = unit is null ? "" : App.Services.GetRequiredService().GetById(unit.GroupId)?.Name ?? ""; var contextLesson = new Lesson { Topic = vm.Topic, Phases = vm.Phases.Select(p => p.ToModel()).ToList() }; return (unit, contextLesson, groupName); } private static async Task ReadHazardAssessmentAsync(LessonDialogViewModel vm, AttachmentItem item) { using var source = vm.OpenAttachment(item); if (source is null) return null; using var reader = new StreamReader(source); var json = await reader.ReadToEndAsync(); return System.Text.Json.JsonSerializer.Deserialize(json); } private static void UploadHazardAssessment(LessonDialogViewModel vm, HazardAssessment result) { var json = System.Text.Json.JsonSerializer.Serialize(result); using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json)); vm.AddAttachment(HazardAssessmentFileName(result.Title), stream); } private static string HazardAssessmentFileName(string title) { var invalid = Path.GetInvalidFileNameChars(); var safe = new string(title.Select(c => invalid.Contains(c) ? '_' : c).ToArray()).Trim(); if (safe.Length == 0) safe = "Gefaehrdungsbeurteilung"; return safe + AttachmentItem.HazardAssessmentSuffix; } /// KI-Unterstützung mit Fokus auf genau diese (bereits gespeicherte) Stunde, statt den Umweg /// über die Einheiten-Übersicht nehmen zu müssen (4.5.22, Nutzer-Feedback). Die Übernahme im /// AiAssistDialog speichert direkt ins Repository — dieser Dialog schließt sich danach mit dem /// frisch geladenen Stand, statt seine eigenen (jetzt veralteten) Feldwerte zu speichern. private async void OnAiAssist(object? s, RoutedEventArgs e) { if (DataContext is not LessonDialogViewModel vm || vm.EditingLesson is null) return; var unitRepo = App.Services.GetRequiredService(); var unit = unitRepo.GetById(vm.UnitId); if (unit is null) return; var lessonRepo = App.Services.GetRequiredService(); var currentLesson = lessonRepo.GetByUnit(vm.UnitId).FirstOrDefault(l => l.Id == vm.EditingLesson.Id); if (currentLesson is null) return; var dialogVm = new AiAssistDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), lessonRepo, unit, focusLesson: currentLesson); var dialog = new AiAssistDialog { DataContext = dialogVm }; var ok = await dialog.ShowDialog(this); if (!ok) return; var updated = lessonRepo.GetByUnit(vm.UnitId).FirstOrDefault(l => l.Id == vm.EditingLesson.Id); if (updated is not null) vm.MarkAppliedExternally(updated); Close(true); } /// Deckt die Lücke ab, dass der Stundenplan eine Stunde nur über Datum+Stundennummer findet /// (kein gespeichertes Verknüpfungsfeld) — eine per JSON-Import oder KI ohne Stundennummer /// angelegte Stunde taucht dort nie auf und würde sonst beim Klick auf den Termin dupliziert. /// Statt neu anzulegen, wird die gefundene vorhandene Stunde auf diesen Termin umgehängt. private async void OnFixIt(object? s, RoutedEventArgs e) { if (DataContext is not LessonDialogViewModel vm) return; if (!DateOnly.TryParseExact(vm.DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date)) { App.Services.GetRequiredService() .ShowError("Bitte zuerst ein gültiges Datum eintragen."); return; } var lessonRepo = App.Services.GetRequiredService(); var candidates = LessonFixItSearch.FindCandidates(lessonRepo, vm.GroupId, date); if (candidates.Count == 0) { App.Services.GetRequiredService() .ShowError("Keine passende bestehende Stunde gefunden."); return; } Lesson chosen; if (candidates.Count == 1) { var info = new ConfirmDialogInfo { Title = "Vorhandene Stunde verknüpfen?", Message = $"Vorhandene Stunde „{candidates[0].Topic}“ vom {candidates[0].Date:dd.MM.yyyy} gefunden. " + "Mit diesem Termin verknüpfen, statt eine neue Stunde anzulegen?", ConfirmText = "Verknüpfen", }; var confirm = new ConfirmDialog { DataContext = info }; if (!await confirm.ShowDialog(this)) return; chosen = candidates[0]; } else { var pickerVm = new LinkExistingLessonDialogViewModel(candidates, App.Services.GetRequiredService()); var picker = new LinkExistingLessonDialog { DataContext = pickerVm }; if (!await picker.ShowDialog(this) || pickerVm.Result is not { } picked) return; chosen = picked; } chosen.UnitId = vm.UnitId; chosen.Date = date; chosen.LessonNumber = vm.LessonNumber; lessonRepo.Save(chosen); vm.MarkAppliedExternally(chosen); Close(true); } }