feat: Anhänge je Stunde, Klausur-Sitzplan mischen, Gefährdungsbeurteilungs-Assistent
Lesson bekommt dieselbe Anhang-Infrastruktur wie Documentation (Material, Arbeitsblätter, Experimentunterlagen), samt Fix einer Sync-Lücke, die Anhang- Dateibytes bisher nur für Documentation statt generisch übertragen hat (IHasAttachments). Sitzplan-Tab bekommt einen "Plätze mischen"-Button für Klausursitzpläne. Neu: mehrschrittiger Gefährdungsbeurteilungs-Assistent mit optionalem KI-Entwurf (ai-backend/gbu.php) und PDF-Export, Format bewusst als JSON-Anhang statt eigener Datenbank-Entität. Details und Architekturentscheidungen in TODO.md (4.2, 7.1.5, 10.1.8). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.HazardAssessmentWizardDialog"
|
||||
x:DataType="vm:HazardAssessmentWizardViewModel"
|
||||
Title="{Binding DialogTitle}"
|
||||
Width="720" Height="720" MinWidth="600" MinHeight="480"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="24">
|
||||
<StackPanel Grid.Row="0" Spacing="2" Margin="0,0,0,10">
|
||||
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||
<TextBlock FontSize="12" Opacity="0.7">
|
||||
<Run Text="{Binding StepProgressLabel}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding StepTitle}" FontWeight="SemiBold"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="1" IsVisible="{Binding CanUseAi}" Background="#FFF3E0" CornerRadius="6"
|
||||
Padding="10" Margin="0,0,0,10">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock FontSize="12" TextWrapping="Wrap">
|
||||
<Run Text="🤖 KI-Entwurf auf Basis von Thema und Verlaufsplan dieser Stunde." FontWeight="SemiBold"/>
|
||||
<LineBreak/>
|
||||
<Run Text="⚠️ Kein rechtssicheres Dokument — bitte jede Angabe (insbesondere H-/P-Sätze und Mengen) eigenverantwortlich gegen das Sicherheitsdatenblatt prüfen, bevor du die Gefährdungsbeurteilung verwendest."/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding AiError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding AiError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="{Binding AiButtonLabel}" Command="{Binding RequestAiDraftCommand}"
|
||||
IsEnabled="{Binding !IsAiBusy}" VerticalAlignment="Top" Margin="10,0,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<StackPanel Spacing="14" Margin="0,0,12,0">
|
||||
|
||||
<!-- Schritt 1: Basisdaten -->
|
||||
<StackPanel Spacing="10" IsVisible="{Binding IsBasicsStep}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Title}" PlaceholderText="z.B. Natrium in Wasser"/>
|
||||
<TextBlock Text="{Binding TitleError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding TitleError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,12,*,12,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Klassenstufe/Kurs" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding GroupLabel}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Datum" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="4" Spacing="4">
|
||||
<TextBlock Text="Art des Versuchs" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding KindOptions}" SelectedItem="{Binding KindName}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Durchführung (Kurzbeschreibung)" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Procedure}" AcceptsReturn="True" TextWrapping="Wrap" Height="90"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Schritt 2: Gefahrstoffe -->
|
||||
<StackPanel Spacing="10" IsVisible="{Binding IsSubstancesStep}">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Verwendete Gefahrstoffe" FontSize="13" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="+ Stoff" Command="{Binding AddSubstanceCommand}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="Noch keine Gefahrstoffe erfasst." Classes="emptyhint"
|
||||
IsVisible="{Binding !HasSubstances}"/>
|
||||
<ItemsControl ItemsSource="{Binding Substances}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HazardSubstanceEditItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,10">
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="*,140,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding Name}" PlaceholderText="Stoffname"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Amount}" PlaceholderText="Menge, z.B. ca. 2 g"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Grid.Column="2" Content="✕" Command="{Binding RemoveCommand}"
|
||||
ToolTip.Tip="Stoff entfernen"/>
|
||||
</Grid>
|
||||
<ItemsControl ItemsSource="{Binding PictogramOptions}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel ItemSpacing="10" LineSpacing="4"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GhsPictogramOption">
|
||||
<CheckBox Content="{Binding Label}" IsChecked="{Binding IsSelected}" FontSize="12"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid ColumnDefinitions="*,12,*">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="H-Sätze" FontSize="11" Opacity="0.6"/>
|
||||
<TextBox Text="{Binding HStatements}" PlaceholderText="z.B. H314, H290"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock Text="P-Sätze" FontSize="11" Opacity="0.6"/>
|
||||
<TextBox Text="{Binding PStatements}" PlaceholderText="z.B. P280, P305+P351+P338"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Schritt 3: Gefährdungen & Schutzmaßnahmen -->
|
||||
<StackPanel Spacing="14" IsVisible="{Binding IsHazardsStep}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Identifizierte Gefährdungen" FontSize="13" FontWeight="SemiBold"/>
|
||||
<ItemsControl ItemsSource="{Binding Hazards}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="×" FontSize="13" Padding="7,2"
|
||||
Command="{Binding $parent[ItemsControl].((vm:HazardAssessmentWizardViewModel)DataContext).RemoveHazardCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid ColumnDefinitions="*,8,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewHazard}" PlaceholderText="z.B. Verätzungsgefahr an Haut/Augen"/>
|
||||
<Button Grid.Column="2" Content="+" Command="{Binding AddHazardCommand}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Schutzmaßnahmen" FontSize="13" FontWeight="SemiBold"/>
|
||||
<ItemsControl ItemsSource="{Binding ProtectiveMeasures}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="×" FontSize="13" Padding="7,2"
|
||||
Command="{Binding $parent[ItemsControl].((vm:HazardAssessmentWizardViewModel)DataContext).RemoveProtectiveMeasureCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid ColumnDefinitions="*,8,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewProtectiveMeasure}" PlaceholderText="z.B. Schutzbrille, Handschuhe, Abzug"/>
|
||||
<Button Grid.Column="2" Content="+" Command="{Binding AddProtectiveMeasureCommand}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Schritt 4: Erste Hilfe & Entsorgung -->
|
||||
<StackPanel Spacing="10" IsVisible="{Binding IsFirstAidStep}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Erste Hilfe" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding FirstAid}" AcceptsReturn="True" TextWrapping="Wrap" Height="90"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Entsorgung" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Disposal}" AcceptsReturn="True" TextWrapping="Wrap" Height="70"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Weitere Hinweise" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Notes}" AcceptsReturn="True" TextWrapping="Wrap" Height="60"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Schritt 5: Zusammenfassung -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsSummaryStep}">
|
||||
<TextBlock Text="{Binding Title}" FontSize="15" FontWeight="SemiBold"/>
|
||||
<TextBlock FontSize="12" Opacity="0.7">
|
||||
<Run Text="{Binding GroupLabel}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding KindName}"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Procedure}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding Substances.Count, StringFormat='{}{0} Gefahrstoff(e) erfasst'}" FontSize="12"/>
|
||||
<TextBlock Text="Alle Angaben vor dem Einsatz noch einmal gegenprüfen." FontSize="12" Opacity="0.7"
|
||||
IsVisible="{Binding IsAiAssisted}"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="3" ColumnDefinitions="Auto,*,Auto,8,Auto,8,Auto" Margin="0,16,0,0">
|
||||
<Button Grid.Column="0" Content="Abbrechen" Click="OnCancel"/>
|
||||
<Button Grid.Column="2" Content="Zurück" Command="{Binding BackCommand}"
|
||||
IsVisible="{Binding !IsFirstStep}"/>
|
||||
<Button Grid.Column="4" Content="Weiter" Command="{Binding NextCommand}"
|
||||
IsVisible="{Binding !IsLastStep}"/>
|
||||
<Button Grid.Column="6" Content="Speichern" Click="OnSave" IsVisible="{Binding IsLastStep}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class HazardAssessmentWizardDialog : Window
|
||||
{
|
||||
public HazardAssessmentWizardDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is HazardAssessmentWizardViewModel vm)
|
||||
{
|
||||
vm.SaveCommand.Execute(null);
|
||||
if (vm.Result is not null) Close(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
xmlns:vms="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.LessonDialog"
|
||||
x:DataType="vm:LessonDialogViewModel"
|
||||
Title="{Binding DialogTitle}"
|
||||
@@ -139,6 +140,43 @@
|
||||
<TextBox Text="{Binding Reflection}" AcceptsReturn="True" Height="56" TextWrapping="Wrap"
|
||||
PlaceholderText="Nach der Stunde: was lief gut, was nicht?"/>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Anhänge (Material, Arbeitsblätter, Experiment-/Gefährdungsbeurteilung)"
|
||||
FontSize="12" Opacity="0.7" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
||||
<Button Content="🧪 Gefährdungsbeurteilung" FontSize="12" Padding="8,4" Click="OnCreateHazardAssessment"/>
|
||||
<Button Content="+ Datei" FontSize="12" Padding="8,4" Click="OnAddAttachment"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ItemsControl ItemsSource="{Binding Attachments}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vms:AttachmentItem">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto,Auto,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding FileName}" VerticalAlignment="Center" FontSize="13"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding SizeDisplay}" VerticalAlignment="Center"
|
||||
FontSize="11" Opacity="0.5" Margin="6,0"/>
|
||||
<Button Grid.Column="2" Content="Öffnen" FontSize="11" Padding="7,2" Margin="0,0,4,0"
|
||||
Click="OnOpenHazardAssessment" Tag="{Binding}" IsVisible="{Binding IsHazardAssessment}"/>
|
||||
<Button Grid.Column="2" Content="Speichern" FontSize="11" Padding="7,2" Margin="0,0,4,0"
|
||||
Click="OnOpenAttachment" Tag="{Binding}" IsVisible="{Binding !IsHazardAssessment}"/>
|
||||
<Button Grid.Column="3" Content="PDF" FontSize="11" Padding="7,2" Margin="0,0,4,0"
|
||||
Click="OnPrintHazardAssessment" Tag="{Binding}"
|
||||
IsVisible="{Binding IsHazardAssessment}"/>
|
||||
<Button Grid.Column="4" Content="×" FontSize="13" Padding="7,2"
|
||||
Command="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).RemoveAttachmentCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="{Binding AttachmentError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding AttachmentError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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;
|
||||
@@ -38,7 +40,136 @@ public partial class LessonDialog : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||
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.Amount, string.Join(", ", s.GhsPictograms.Select(GhsPictogramDisplay.Code)),
|
||||
s.HStatements, s.PStatements)).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
|
||||
|
||||
@@ -99,6 +99,7 @@ public partial class PlanningTabView : UserControl
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
||||
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
||||
App.Services.GetRequiredService<PeriodScheduleService>(),
|
||||
App.Services.GetRequiredService<IAttachmentStorage>(),
|
||||
unitId, groupId, vm.GroupLabel, vm.SubjectName,
|
||||
materialSuggestions, shorthandHistorySuggestions, editingLesson);
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
<Button Grid.Column="2" Content="Löschen" Command="{Binding DeletePlanCommand}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
<Button Content="🎲 Plätze für Klausur mischen" Command="{Binding ShuffleSeatsCommand}"
|
||||
HorizontalAlignment="Stretch" IsVisible="{Binding HasSelectedPlan}"
|
||||
ToolTip.Tip="Legt einen neuen, unabhängigen Sitzplan mit demselben Raster an, in dem die belegten Plätze zufällig neu vertauscht sind. Der ursprüngliche Plan bleibt unverändert erhalten."/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
Reference in New Issue
Block a user