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:
@@ -10,7 +10,10 @@
|
||||
<TextBlock Text="Kompetenzübersicht" FontSize="18" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.65"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Aktualisieren" Command="{Binding RefreshCommand}"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Top">
|
||||
<Button Content="Als PDF" Click="OnExportPdfClick" IsEnabled="{Binding HasRows}"/>
|
||||
<Button Content="Aktualisieren" Command="{Binding RefreshCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,16,210" IsVisible="{Binding HasRows}">
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class CompetencyOverviewTabView : UserControl
|
||||
{
|
||||
public CompetencyOverviewTabView() => InitializeComponent();
|
||||
|
||||
private async void OnExportPdfClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null || DataContext is not CompetencyOverviewTabViewModel { HasRows: true } vm) return;
|
||||
|
||||
var studentName = vm.SelectedStudent?.DisplayName ?? "Kein Schüler ausgewählt";
|
||||
var data = new CompetencyReportPrintData(vm.GroupLabel, studentName, vm.Summary,
|
||||
vm.ThresholdLabel,
|
||||
vm.Rows.Select(r => new CompetencyReportPrintRow(r.Domain, r.Code, r.Description,
|
||||
r.InstructionCoverage, r.ExamCoverage, r.GroupResult, r.StudentResult,
|
||||
r.Recommendation)).ToList());
|
||||
var pdf = App.Services.GetRequiredService<PdfExportService>().BuildCompetencyReportPdf(data);
|
||||
|
||||
await App.Services.GetRequiredService<ExportService>().SaveAsync(topLevel.StorageProvider,
|
||||
ExportFile.Pdf("Kompetenzbericht als PDF speichern",
|
||||
$"Kompetenzbericht_{vm.GroupLabel}_{studentName}", pdf));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +147,10 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="0,16,0,0">
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,8,Auto,*,Auto" Margin="0,16,0,0">
|
||||
<Button Grid.Column="0" Content="Als CSV exportieren" Click="OnExportClick"/>
|
||||
<Button Grid.Column="2" Content="Fertig" Click="OnClose"/>
|
||||
<Button Grid.Column="2" Content="Als PDF" Click="OnExportPdfClick"/>
|
||||
<Button Grid.Column="4" Content="Fertig" Click="OnClose"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -21,5 +21,33 @@ public partial class ExamEvaluationDialog : Window
|
||||
await App.Services.GetRequiredService<ExportService>().SaveAsync(topLevel.StorageProvider, export);
|
||||
}
|
||||
|
||||
private async void OnExportPdfClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not ExamEvaluationDialogViewModel vm) return;
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null) return;
|
||||
|
||||
var stats = new List<LabelValue>
|
||||
{
|
||||
new("Bewertet", vm.GradedCount.ToString()),
|
||||
new("Abwesend", vm.AbsentCount.ToString()),
|
||||
new("Durchschnitt", vm.AverageDisplay),
|
||||
new("Median", vm.MedianDisplay),
|
||||
};
|
||||
if (!string.IsNullOrEmpty(vm.BelowThresholdLabel1))
|
||||
stats.Add(new(vm.BelowThresholdLabel1, vm.BelowThresholdDisplay1));
|
||||
if (!string.IsNullOrEmpty(vm.BelowThresholdLabel2))
|
||||
stats.Add(new(vm.BelowThresholdLabel2, vm.BelowThresholdDisplay2));
|
||||
|
||||
var data = new ExamStatisticsPrintData(vm.ExamTitle, vm.ExamDateLabel, vm.NiveauLabel, stats,
|
||||
vm.GradeDistribution.Select(g => new GradeDistributionPrintRow(g.Grade, g.Count,
|
||||
vm.GradedCount == 0 ? 0 : g.Count * 100.0 / vm.GradedCount)).ToList(),
|
||||
vm.TaskAnalysis.Select(t => new TaskAnalysisPrintRow(t.Label, t.AvgPercent, t.IsWeak)).ToList());
|
||||
var pdf = App.Services.GetRequiredService<PdfExportService>().BuildExamStatisticsPdf(data);
|
||||
|
||||
await App.Services.GetRequiredService<ExportService>().SaveAsync(topLevel.StorageProvider,
|
||||
ExportFile.Pdf("Klausurauswertung als PDF speichern", $"Auswertung_{vm.ExamTitle}", pdf));
|
||||
}
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="+ Sammelnote" Command="{Binding CollectiveGradeCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="Zeugnisnoten" Command="{Binding ReportGradesCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="Als PDF" Click="OnExportPdfClick" Margin="12,0,0,0"
|
||||
IsEnabled="{Binding !!Rows.Count}"/>
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid Grid.Row="1"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -77,6 +79,22 @@ public partial class GradeOverviewTabView : UserControl
|
||||
if (owner is not null) await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async void OnExportPdfClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null || _vm is null) return;
|
||||
|
||||
var data = new GradeListPrintData(
|
||||
_vm.GroupLabel, _vm.SchoolYear, _vm.SelectedPeriod.Label,
|
||||
_vm.Columns.Select(c => c.Header).ToList(),
|
||||
_vm.Rows.Select(r => new GradeListPrintRow(r.Name, r.Cells, r.TotalDisplay)).ToList());
|
||||
var pdf = App.Services.GetRequiredService<PdfExportService>().BuildGradeListPdf(data);
|
||||
|
||||
await App.Services.GetRequiredService<ExportService>().SaveAsync(topLevel.StorageProvider,
|
||||
ExportFile.Pdf("Notenliste als PDF speichern",
|
||||
$"Notenliste_{_vm.GroupLabel}_{_vm.SchoolYear.Replace('/', '-')}", pdf));
|
||||
}
|
||||
|
||||
private void BuildColumns()
|
||||
{
|
||||
var grid = this.FindControl<DataGrid>("GradesGrid");
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Bottom" Spacing="4">
|
||||
<ToggleSwitch Content="Bearbeitungsmodus" IsChecked="{Binding IsEditMode}"
|
||||
IsVisible="{Binding IsEditable}" HorizontalAlignment="Right"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||
<Button Content="Als PDF" Click="OnExportPdfClick" VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Content="Bearbeitungsmodus" IsChecked="{Binding IsEditMode}"
|
||||
IsVisible="{Binding IsEditable}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding AssignmentSummary}" HorizontalAlignment="Right" FontSize="12" Opacity="0.6"/>
|
||||
<TextBlock Text="Ziehen: Platz ändern · Klicken: bewerten" HorizontalAlignment="Right"
|
||||
FontSize="11" Opacity="0.5" IsVisible="{Binding IsEditMode}"/>
|
||||
|
||||
@@ -5,8 +5,10 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.Views.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
@@ -205,6 +207,21 @@ public partial class SeatingPlanTabView : UserControl
|
||||
await vm.AssessStudent(seat);
|
||||
}
|
||||
|
||||
private async void OnExportPdfClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null || DataContext is not SeatingPlanTabViewModel { HasSelectedPlan: true } vm) return;
|
||||
|
||||
var data = new SeatingPlanPrintData(
|
||||
vm.PlanTitle, vm.PlanSubtitle, vm.PlanColumns, vm.IsBoardAtBottom, vm.ColumnGapWidths,
|
||||
vm.Seats.Select(s => new SeatPrintCell(s.Row, s.Column,
|
||||
s.IsOccupied ? s.SelectedOption.DisplayName : null, s.IsHidden)).ToList());
|
||||
var pdf = App.Services.GetRequiredService<PdfExportService>().BuildSeatingPlanPdf(data);
|
||||
|
||||
await App.Services.GetRequiredService<ExportService>().SaveAsync(topLevel.StorageProvider,
|
||||
ExportFile.Pdf("Sitzplan als PDF speichern", $"Sitzplan_{vm.PlanTitle}", pdf));
|
||||
}
|
||||
|
||||
private async Task ShowAssessmentDialog(SeatAssessmentViewModel vm)
|
||||
{
|
||||
var dialog = new SeatAssessmentDialog { DataContext = vm };
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user