Ersetzt den ursprünglichen KI-Websuche-Ansatz (GESTIS scheiterte sowohl über Websuche als auch über direkten API-Zugriff an fehlenden schulspezifischen Tätigkeitsbeschränkungen) durch einen reinen lokalen Datenbank-Lookup gegen die offizielle RiSU-Stoffliste (1764 Stoffe inkl. Tätigkeitsbeschränkungs-Codes, Legende zur Abfragezeit übersetzt statt in die Datenbank gebacken). Keine KI-Kosten, keine Drittanbieter-Abhängigkeit mehr für diese Funktion. HazardSubstance.AgeRestriction zu ActivityRestriction umbenannt, da die Tätigkeitsbeschränkung mehr abdeckt als reine Altersgrenzen. Details und verworfene Zwischenstände in TODO.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
204 lines
9.0 KiB
C#
204 lines
9.0 KiB
C#
using Avalonia.Controls;
|
|
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 Microsoft.Extensions.DependencyInjection;
|
|
|
|
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<AlternativeLessonPath?> ShowAlternativePathDialog(Guid? currentId)
|
|
{
|
|
var dialogVm = new AlternativePathDialogViewModel(
|
|
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(), currentId);
|
|
var dialog = new AlternativePathDialog { DataContext = dialogVm };
|
|
|
|
var ok = await dialog.ShowDialog<bool>(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);
|
|
}
|
|
|
|
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<AiPlanningService>(), App.Services.GetRequiredService<AiSettingsService>(),
|
|
unit, contextLesson);
|
|
var dialog = new HazardAssessmentWizardDialog { DataContext = dialogVm };
|
|
var ok = await dialog.ShowDialog<bool>(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<AiPlanningService>(), App.Services.GetRequiredService<AiSettingsService>(),
|
|
unit, contextLesson);
|
|
var dialog = new HazardAssessmentWizardDialog { DataContext = dialogVm };
|
|
var ok = await dialog.ShowDialog<bool>(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<PdfExportService>().BuildHazardAssessmentPdf(data);
|
|
|
|
await App.Services.GetRequiredService<ExportService>().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<IUnitRepository>().GetById(vm.UnitId);
|
|
var groupName = unit is null ? "" : App.Services.GetRequiredService<IGroupRepository>().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<HazardAssessment?> 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<HazardAssessment>(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<IUnitRepository>();
|
|
var unit = unitRepo.GetById(vm.UnitId);
|
|
if (unit is null) return;
|
|
|
|
var lessonRepo = App.Services.GetRequiredService<ILessonRepository>();
|
|
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<AiPlanningService>(),
|
|
App.Services.GetRequiredService<AiSettingsService>(),
|
|
lessonRepo, unit, focusLesson: currentLesson);
|
|
|
|
var dialog = new AiAssistDialog { DataContext = dialogVm };
|
|
var ok = await dialog.ShowDialog<bool>(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);
|
|
}
|
|
}
|