diff --git a/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs b/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs index aa97caf..85f9c15 100644 --- a/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs +++ b/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs @@ -120,7 +120,7 @@ public sealed class ExamCoverSheetTests : IDisposable } [Fact] - public void Generate_SchreibtEinSammelPdfMitEinerSeiteJeSchueler() + public async Task Generate_SchreibtEinSammelPdfMitEinerSeiteJeSchueler() { _results.Add(new ExamResult { ExamId = _exam.Id, StudentId = _anna.Id, Points = [4.5, 12, 1], TotalPoints = 17.5 }); @@ -129,7 +129,7 @@ public sealed class ExamCoverSheetTests : IDisposable var vm = Build([store]); var output = Path.Combine(_directory, "deckblaetter.pdf"); - Assert.True(vm.Generate(output)); + Assert.True(await vm.GenerateAsync(output)); using var document = UglyToad.PdfPig.PdfDocument.Open(output); Assert.Equal(2, document.NumberOfPages); @@ -145,10 +145,12 @@ public sealed class ExamCoverSheetTests : IDisposable // Ben ist noch nicht korrigiert: Linien zum Ausfüllen statt Punkten und Note. Assert.Contains($"Gesamtpunkte:{ExamCoverPlaceholderBuilder.BlankPoints}von17Punkte", ben); Assert.Contains("1 davon ohne Punkte und Note", vm.StatusMessage); + Assert.False(vm.IsGenerating); + Assert.True(vm.CanGenerate); } [Fact] - public void Generate_OhnePunkteEintragen_BleibtAuchBeiKorrigiertenBlanko() + public async Task Generate_OhnePunkteEintragen_BleibtAuchBeiKorrigiertenBlanko() { _results.Add(new ExamResult { ExamId = _exam.Id, StudentId = _anna.Id, Points = [4.5, 12, 1], TotalPoints = 17.5 }); @@ -158,21 +160,21 @@ public sealed class ExamCoverSheetTests : IDisposable vm.FillResults = false; var output = Path.Combine(_directory, "blanko.pdf"); - Assert.True(vm.Generate(output)); + Assert.True(await vm.GenerateAsync(output)); using var document = UglyToad.PdfPig.PdfDocument.Open(output); Assert.DoesNotContain("17,5", Normalize(document.GetPage(1).Text)); } [Fact] - public void Generate_ZuVieleAufgabenFuerEineSeite_WarntImStatus() + public async Task Generate_ZuVieleAufgabenFuerEineSeite_WarntImStatus() { _exam.Tasks = Enumerable.Range(1, 80).Select(n => new ExamTask { Nr = n, MaxPoints = 1 }).ToList(); var store = Store(); Import(store, "deckblatt", "Deckblatt", TemplateReportTypes.ExamCover); var vm = Build([store]); - Assert.True(vm.Generate(Path.Combine(_directory, "lang.pdf"))); + Assert.True(await vm.GenerateAsync(Path.Combine(_directory, "lang.pdf"))); Assert.Contains("passen nicht auf eine Seite", vm.StatusMessage); } diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamCoverSheetDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamCoverSheetDialogViewModel.cs index 02df108..70efeb2 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamCoverSheetDialogViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamCoverSheetDialogViewModel.cs @@ -31,12 +31,20 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject /// Vorbelegt mit dem internen Klausurtitel, hier nur für diesen Druck änderbar — der /// Titel der Klausur bleibt unberührt. Leer gelassen gilt wieder der interne Titel. [ObservableProperty] private string _coverTitle; + [ObservableProperty, NotifyPropertyChangedFor(nameof(CanGenerate))] private bool _isGenerating; + [ObservableProperty] private int _progressValue; + [ObservableProperty] private int _progressMaximum = 1; + /// Alle Deckblätter sind gesetzt, das Sammel-PDF entsteht — dieser Schritt meldet + /// keinen Zwischenstand, der Balken läuft deshalb unbestimmt. + [ObservableProperty] private bool _isMerging; + [ObservableProperty] private string _progressText = ""; public string ExamTitle => _exam.Title; public string ExamLabel => $"{_exam.Title} · {_group.Name} · {_exam.Date:dd.MM.yyyy}"; public ObservableCollection Templates { get; } = []; public ObservableCollection Students { get; } = []; public bool HasNoTemplates => Templates.Count == 0; + public bool CanGenerate => !HasNoTemplates && !IsGenerating; public string SuggestedFileName => SanitizeFileName($"Deckblätter_{_group.Name}_{_exam.Title}.pdf"); public ExamCoverSheetDialogViewModel(Exam exam, LearningGroup group, string subjectName, @@ -68,11 +76,12 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject FillResults = Students.Any(s => s.IsCorrected); } - /// Schreibt das Sammel-PDF und meldet den Erfolg. - public bool Generate(string outputPath) + /// Schreibt das Sammel-PDF und meldet den Erfolg. Gesetzt wird im Hintergrund, damit + /// der Dialog bei einem Klassensatz bedienbar bleibt und den Fortschritt zeigen kann. + public async Task GenerateAsync(string outputPath) { StatusMessage = ""; - if (SelectedTemplate is null) return false; + if (SelectedTemplate is null || IsGenerating) return false; var included = Students.Where(s => s.IsIncluded).ToList(); if (included.Count == 0) { StatusMessage = "Kein Schüler ausgewählt."; return false; } try @@ -82,8 +91,15 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject var data = included.Select(s => (ITemplateDataProvider)new LetterDataProvider( ExamCoverPlaceholderBuilder.Build(_exam, _group, _subjectName, s.Model, s.Result, FillResults, _grading, loaded, today, s.Niveau, TeacherAbbreviation, CoverTitle))).ToList(); - var result = _renderer.RenderBatchToPdf(loaded, data); - File.WriteAllBytes(outputPath, result.Pdf); + ReportProgress(0, included.Count); + IsGenerating = true; + var progress = new Progress(done => ReportProgress(done, included.Count)); + var result = await Task.Run(() => + { + var rendered = _renderer.RenderBatchToPdf(loaded, data, progress); + File.WriteAllBytes(outputPath, rendered.Pdf); + return rendered; + }); var message = $"{included.Count} Deckblätter erzeugt."; var blank = FillResults ? included.Count(s => !s.IsCorrected) : 0; @@ -96,6 +112,17 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject } catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException) { StatusMessage = $"Deckblätter konnten nicht erzeugt werden: {ex.Message}"; return false; } + finally { IsGenerating = false; } + } + + private void ReportProgress(int done, int total) + { + ProgressMaximum = total; + ProgressValue = done; + IsMerging = done >= total && total > 1; + ProgressText = IsMerging + ? "Sammel-PDF wird zusammengefügt …" + : $"{done} von {total} Deckblättern gesetzt …"; } private static string SanitizeFileName(string value) diff --git a/LehrerApp.Desktop/Views/Groups/ExamCoverSheetDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamCoverSheetDialog.axaml index 1dd47de..d731c9e 100644 --- a/LehrerApp.Desktop/Views/Groups/ExamCoverSheetDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/ExamCoverSheetDialog.axaml @@ -49,13 +49,21 @@ - + + + + + + + -