Files
LehrerApp/LehrerApp.Desktop/Services/ExportService.cs
T
adminandClaude Sonnet 5 7365947edc 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>
2026-08-20 14:27:05 +02:00

81 lines
3.3 KiB
C#

using Avalonia.Platform.Storage;
using System.Text;
namespace LehrerApp.Desktop.Services;
public sealed record ExportFormat(string Label, string Extension, string MimeType)
{
public static ExportFormat Csv { get; } = new("CSV-Dateien", ".csv", "text/csv");
public static ExportFormat Json { get; } = new("JSON-Dateien", ".json", "application/json");
public static ExportFormat Pdf { get; } = new("PDF-Dateien", ".pdf", "application/pdf");
}
public sealed record ExportFile(
string DialogTitle,
string SuggestedFileName,
ExportFormat Format,
ReadOnlyMemory<byte> Content)
{
public static ExportFile Csv(string dialogTitle, string suggestedFileName, string content) =>
FromText(dialogTitle, suggestedFileName, ExportFormat.Csv, content, includeUtf8Bom: true);
public static ExportFile Json(string dialogTitle, string suggestedFileName, string content) =>
FromText(dialogTitle, suggestedFileName, ExportFormat.Json, content);
public static ExportFile Pdf(string dialogTitle, string suggestedFileName, byte[] content) =>
new(dialogTitle, EnsureExtension(SanitizeFileName(suggestedFileName), ExportFormat.Pdf.Extension),
ExportFormat.Pdf, content);
private static ExportFile FromText(string dialogTitle, string suggestedFileName,
ExportFormat format, string content, bool includeUtf8Bom = false)
{
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: includeUtf8Bom);
var preamble = encoding.GetPreamble();
var text = encoding.GetBytes(content);
var bytes = new byte[preamble.Length + text.Length];
preamble.CopyTo(bytes, 0);
text.CopyTo(bytes, preamble.Length);
return new ExportFile(dialogTitle,
EnsureExtension(SanitizeFileName(suggestedFileName), format.Extension), format, bytes);
}
private static string EnsureExtension(string fileName, string extension) =>
fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ? fileName : fileName + extension;
private static string SanitizeFileName(string fileName)
{
var invalidCharacters = Path.GetInvalidFileNameChars();
return string.Concat(fileName.Select(character =>
invalidCharacters.Contains(character) ? '_' : character));
}
}
/// <summary>Zentrale Dateiauswahl und Stream-Ausgabe für alle Exportformate.</summary>
public sealed class ExportService
{
public async Task<bool> SaveAsync(IStorageProvider storageProvider, ExportFile export)
{
var file = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = export.DialogTitle,
SuggestedFileName = export.SuggestedFileName,
DefaultExtension = export.Format.Extension.TrimStart('.'),
FileTypeChoices =
[
new FilePickerFileType(export.Format.Label)
{
Patterns = [$"*{export.Format.Extension}"],
MimeTypes = [export.Format.MimeType],
},
],
});
if (file is null) return false;
await using var stream = await file.OpenWriteAsync();
if (stream.CanSeek) stream.SetLength(0);
await stream.WriteAsync(export.Content);
return true;
}
}