feat: PDF-Druck für Sitzplan, Notenliste, Klausur-Notenspiegel, Kompetenzbericht, Dokumentation

QuestPDF als PDF-Bibliothek eingeführt (11.3), bewusst nur in LehrerApp.Desktop referenziert -
zieht SkiaSharp-Native-Binaries mit, die im Server-Docker-Image nichts verloren haben. Neuer
PdfExportService mit gemeinsamem Kopf-/Fußzeilen-Layout (Titel, Stand-Datum, Seitenzahlen) und
schlanken Druck-DTOs statt ViewModels, damit die Erzeugung ohne Avalonia-Bezug testbar bleibt.
Speicherdialog läuft über den bestehenden ExportService (neues PDF-Format).

Fünf Druckvorlagen (11.4) als "Als PDF"-Button direkt in der jeweiligen Ansicht:
- Sitzplan: Raster mit Tafel-Banner, Tischabständen als echte Lücken, ausgeblendeten Plätzen als
  leere Rasterposition.
- Notenliste: druckt exakt die aktuell angezeigte Matrix (Zeitraum/Darstellung/Sortierung).
- Klausur-Notenspiegel: Kennzahlen, Notenverteilung und Aufgabenanalyse mit Balkendiagrammen,
  auffällig schwache Aufgaben rot markiert.
- Kompetenzbericht: Analyse-Tabelle für den ausgewählten Schüler (erledigt zugleich 8.3.3).
- Schülerdokumentation: alle Einträge chronologisch, vertrauliche/Entwurfs-Einträge markiert.

Layout an gerenderten Muster-PDFs visuell geprüft, nicht nur per Unit-Test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 14:27:05 +02:00
co-authored by Claude Sonnet 5
parent 4cce84c7dd
commit 7365947edc
18 changed files with 785 additions and 15 deletions
@@ -245,12 +245,16 @@
</StackPanel>
</Border>
<Grid ColumnDefinitions="*,Auto,Auto">
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<TextBlock Grid.Column="0" Text="Einträge" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Datenauskunft exportieren" FontSize="12" Padding="10,4"
<Button Grid.Column="1" Content="Als PDF" FontSize="12" Padding="10,4"
Margin="0,0,8,0" Click="OnExportDocumentationPdfClick"
IsEnabled="{Binding !!Documentation.Count}"
ToolTip.Tip="Alle sichtbaren Dokumentationseinträge dieses Schülers als PDF drucken"/>
<Button Grid.Column="2" Content="Datenauskunft exportieren" FontSize="12" Padding="10,4"
Margin="0,0,8,0" Command="{Binding ExportPersonalDataCommand}"
ToolTip.Tip="Alle gespeicherten Daten dieses Schülers als JSON-Datei exportieren (Art. 15 DSGVO)"/>
<Button Grid.Column="2" Content=" Eintrag" Command="{Binding AddDocumentationCommand}"/>
<Button Grid.Column="3" Content=" Eintrag" Command="{Binding AddDocumentationCommand}"/>
</Grid>
<TextBlock Text="{Binding ExportStatus}" Foreground="Green" FontSize="12"
IsVisible="{Binding ExportStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
@@ -1,8 +1,10 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.Views.Shared;
using Microsoft.Extensions.DependencyInjection;
@@ -119,6 +121,23 @@ public partial class StudentDetailView : UserControl
return owner is not null && await dialog.ShowDialog<bool>(owner);
}
private async void OnExportDocumentationPdfClick(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is null || DataContext is not StudentDetailViewModel { Student: not null } vm
|| vm.Documentation.Count == 0) return;
var data = new StudentDocumentationPrintData(vm.Student.FullName,
vm.Documentation.Select(d => new DocumentationPrintEntry(
d.DateDisplay, d.TypeLabel, d.Model.Title, d.Model.Content,
d.Model.Tags, d.IsConfidential, d.IsDraft)).ToList());
var pdf = App.Services.GetRequiredService<PdfExportService>().BuildStudentDocumentationPdf(data);
await App.Services.GetRequiredService<ExportService>().SaveAsync(topLevel.StorageProvider,
ExportFile.Pdf("Dokumentation als PDF speichern",
$"Dokumentation_{vm.Student.LastName}_{vm.Student.FirstName}", pdf));
}
private async Task SaveExportFile(string json)
{
var topLevel = TopLevel.GetTopLevel(this);