Neuer Dialog "Deckblätter…" bei Klausuren, Punktetabelle als Drawing mit festen Spaltenbreiten, FLOWDRAWBOX-Attribut fit=content, Stapel-Rendering mit Seitenzahl je Datensatz, Kursbezeichnung und konfigurierbares Kürzel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
6.2 KiB
C#
117 lines
6.2 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using LehrerApp.Desktop.Services;
|
|
using LehrerApp.Desktop.ViewModels.Students;
|
|
using LehrerApp.Templating;
|
|
using System.Collections.ObjectModel;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
|
|
|
/// <summary>Erzeugt zu einer Klausur einen Satz individueller Deckblätter als ein Sammel-PDF.
|
|
/// Zur Auswahl stehen nur Vorlagen mit <c>report-type=exam-cover</c>, egal in welcher der beiden
|
|
/// Vorlagenbibliotheken sie importiert wurden. Mit <see cref="FillResults"/> trägt dieselbe
|
|
/// Vorlage Punkte und Note ein oder bleibt als Blanko-Deckblatt zum Ausfüllen von Hand.</summary>
|
|
public partial class ExamCoverSheetDialogViewModel : ObservableObject
|
|
{
|
|
private readonly Exam _exam;
|
|
private readonly LearningGroup _group;
|
|
private readonly string _subjectName;
|
|
private readonly GradingService _grading;
|
|
private readonly TemplateStore _loader;
|
|
private readonly ITemplateBatchRenderer _renderer;
|
|
|
|
[ObservableProperty] private LetterTemplateChoice? _selectedTemplate;
|
|
[ObservableProperty] private bool _fillResults;
|
|
[ObservableProperty] private string _statusMessage = "";
|
|
/// <summary>Vorbelegt aus den Einstellungen, hier nur für diesen Druck änderbar (z. B. zwei
|
|
/// Kürzel bei einem gemeinsam unterrichteten Kurs) — die Einstellung bleibt unberührt.</summary>
|
|
[ObservableProperty] private string _teacherAbbreviation;
|
|
|
|
public string ExamLabel => $"{_exam.Title} · {_group.Name} · {_exam.Date:dd.MM.yyyy}";
|
|
public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
|
|
public ObservableCollection<ExamCoverStudentChoice> Students { get; } = [];
|
|
public bool HasNoTemplates => Templates.Count == 0;
|
|
public string SuggestedFileName => SanitizeFileName($"Deckblätter_{_group.Name}_{_exam.Title}.pdf");
|
|
|
|
public ExamCoverSheetDialogViewModel(Exam exam, LearningGroup group, string subjectName,
|
|
IStudentRepository students, IGroupMembershipRepository memberships, IExamResultRepository results,
|
|
GradingService grading, IReadOnlyList<TemplateStore> templateStores, ITemplateBatchRenderer renderer,
|
|
string teacherAbbreviation = "")
|
|
{
|
|
_teacherAbbreviation = teacherAbbreviation;
|
|
_exam = exam; _group = group; _subjectName = subjectName; _grading = grading; _renderer = renderer;
|
|
_loader = templateStores[0];
|
|
foreach (var template in templateStores.SelectMany(x => x.GetTemplates()).Where(x => x.IsExamCover)
|
|
.DistinctBy(x => x.Id).OrderBy(x => x.Name, StringComparer.CurrentCultureIgnoreCase))
|
|
Templates.Add(new(template));
|
|
SelectedTemplate = Templates.FirstOrDefault();
|
|
|
|
var membershipList = memberships.GetByGroup(group.Id);
|
|
var existing = results.GetByExam(exam.Id).ToDictionary(r => r.StudentId);
|
|
var wroteOriginal = ExamParticipantService.WroteOriginal(exam, results.GetByExam);
|
|
foreach (var student in students.GetByGroup(group.Id).OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
|
{
|
|
var membership = membershipList.FirstOrDefault(m => m.StudentId == student.Id);
|
|
existing.TryGetValue(student.Id, out var result);
|
|
if (!ExamParticipantService.Includes(exam, membership, result is not null, wroteOriginal.Contains(student.Id)))
|
|
continue;
|
|
Students.Add(new(student, result, membership?.Niveau));
|
|
}
|
|
// Vor der Korrektur ist das Blanko-Deckblatt der Normalfall, danach das ausgefüllte.
|
|
FillResults = Students.Any(s => s.IsCorrected);
|
|
}
|
|
|
|
/// <summary>Schreibt das Sammel-PDF und meldet den Erfolg.</summary>
|
|
public bool Generate(string outputPath)
|
|
{
|
|
StatusMessage = "";
|
|
if (SelectedTemplate is null) return false;
|
|
var included = Students.Where(s => s.IsIncluded).ToList();
|
|
if (included.Count == 0) { StatusMessage = "Kein Schüler ausgewählt."; return false; }
|
|
try
|
|
{
|
|
var loaded = _loader.Load(SelectedTemplate.Model);
|
|
var today = DateOnly.FromDateTime(DateTime.Now);
|
|
var data = included.Select(s => (ITemplateDataProvider)new LetterDataProvider(
|
|
ExamCoverPlaceholderBuilder.Build(_exam, _group, _subjectName, s.Model, s.Result, FillResults,
|
|
_grading, loaded, today, s.Niveau, TeacherAbbreviation))).ToList();
|
|
var result = _renderer.RenderBatchToPdf(loaded, data);
|
|
File.WriteAllBytes(outputPath, result.Pdf);
|
|
|
|
var message = $"{included.Count} Deckblätter erzeugt.";
|
|
var blank = FillResults ? included.Count(s => !s.IsCorrected) : 0;
|
|
if (blank > 0) message += $" {blank} davon ohne Punkte und Note (noch nicht korrigiert).";
|
|
var overflowing = result.PageCounts.Count(pages => pages > 1);
|
|
if (overflowing > 0)
|
|
message += $" Achtung: {overflowing} Deckblätter passen nicht auf eine Seite — die Punktetabelle braucht mehr Platz, als die Vorlage lässt.";
|
|
StatusMessage = message;
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException)
|
|
{ StatusMessage = $"Deckblätter konnten nicht erzeugt werden: {ex.Message}"; return false; }
|
|
}
|
|
|
|
private static string SanitizeFileName(string value)
|
|
{ foreach (var character in Path.GetInvalidFileNameChars()) value = value.Replace(character, '_'); return value; }
|
|
}
|
|
|
|
public partial class ExamCoverStudentChoice : ObservableObject
|
|
{
|
|
public Student Model { get; }
|
|
public ExamResult? Result { get; }
|
|
public Niveau? Niveau { get; }
|
|
public string FullName => Model.FullName;
|
|
public bool IsCorrected => ExamCoverSheetService.IsCorrected(Result);
|
|
public string Hint => Result is { Absent: true } ? "abwesend" : "";
|
|
[ObservableProperty] private bool _isIncluded;
|
|
|
|
public ExamCoverStudentChoice(Student model, ExamResult? result, Niveau? niveau = null)
|
|
{
|
|
Model = model; Result = result; Niveau = niveau;
|
|
// Wer gefehlt hat, bekommt kein Deckblatt — lässt sich im Dialog wieder anhaken.
|
|
_isIncluded = result is not { Absent: true };
|
|
}
|
|
}
|