Files
LehrerApp/LehrerApp.Desktop/ViewModels/Students/StudentReportDialogViewModel.cs
T
2026-10-22 11:24:43 +02:00

107 lines
6.0 KiB
C#

using System.Collections.ObjectModel;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services;
using LehrerApp.Templating;
namespace LehrerApp.Desktop.ViewModels.Students;
public partial class StudentReportDialogViewModel : ObservableObject
{
private readonly Student _student;
private readonly StudentReportService _reports;
private readonly ITemplateRenderer _renderer;
private readonly IInterimGradeRepository _interims;
private readonly SchoolYearService _schoolYears;
private readonly TemplateStore _loader;
public string StudentName => _student.FullName;
public string SuggestedFileName => string.Concat($"Schülerbericht_{_student.LastName}_{_student.FirstName}.pdf"
.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
public ObservableCollection<LetterGroupChoice> Groups { get; } = [];
public ObservableCollection<StudentReportTemplateChoice> Templates { get; } = [];
public ObservableCollection<StudentReportInterimChoice> Interims { get; } = [];
[ObservableProperty] private LetterGroupChoice? _selectedGroup;
[ObservableProperty] private StudentReportTemplateChoice? _selectedTemplate;
[ObservableProperty] private StudentReportInterimChoice? _selectedInterim;
[ObservableProperty] private string _from = "";
[ObservableProperty] private string _to = DateTime.Today.ToString("dd.MM.yyyy");
[ObservableProperty] private bool _includeInterim = true;
[ObservableProperty] private bool _includeParticipation = true;
[ObservableProperty] private bool _includeCompetencies = true;
[ObservableProperty] private bool _includeHomework = true;
[ObservableProperty] private bool _includeDocumentation = true;
[ObservableProperty] private bool _includeConfidential;
[ObservableProperty] private string _comment = "";
[ObservableProperty] private string _statusMessage = "";
[ObservableProperty, NotifyPropertyChangedFor(nameof(CanGenerate))] private bool _isGenerating;
public bool CanGenerate => !IsGenerating && SelectedGroup is not null && SelectedTemplate is not null;
public StudentReportDialogViewModel(Student student, IGroupMembershipRepository memberships,
IGroupRepository groups, IInterimGradeRepository interims, StudentReportService reports,
ITemplateRenderer renderer, SchoolYearService schoolYears, IReadOnlyList<TemplateStore> stores)
{
_student = student; _interims = interims; _reports = reports; _renderer = renderer;
_schoolYears = schoolYears; _loader = stores[0];
Templates.Add(new(null));
foreach (var template in stores.SelectMany(s => s.GetTemplates()).Where(t => t.IsStudentReport)
.DistinctBy(t => t.PackagePath).OrderBy(t => t.Name)) Templates.Add(new(template));
foreach (var membership in memberships.GetByStudent(student.Id))
if (groups.GetById(membership.GroupId) is { } group && !Groups.Any(g => g.Model.Id == group.Id))
Groups.Add(new(group));
SelectedTemplate = Templates[0];
SelectedGroup = Groups.OrderByDescending(g => g.Model.SchoolYear).FirstOrDefault();
}
partial void OnSelectedGroupChanged(LetterGroupChoice? value)
{
Interims.Clear(); SelectedInterim = null;
if (value is not null)
{
From = _schoolYears.SchoolYearStart(value.Model.SchoolYear).ToString("dd.MM.yyyy");
To = _schoolYears.SchoolYearEnd(value.Model.SchoolYear).ToString("dd.MM.yyyy");
var today = DateOnly.FromDateTime(DateTime.Today);
if (_schoolYears.SchoolYearEnd(value.Model.SchoolYear) > today) To = today.ToString("dd.MM.yyyy");
foreach (var grade in _interims.GetByStudentAndGroup(_student.Id, value.Model.Id)
.OrderByDescending(g => g.Date).ThenBy(g => g.Occasion)) Interims.Add(new(grade));
}
OnPropertyChanged(nameof(CanGenerate));
}
partial void OnSelectedTemplateChanged(StudentReportTemplateChoice? value) => OnPropertyChanged(nameof(CanGenerate));
public async Task<bool> GenerateAsync(string outputPath)
{
if (!CanGenerate) return false;
StatusMessage = "";
if (!DateOnly.TryParseExact(From, "dd.MM.yyyy", CultureInfo.GetCultureInfo("de-DE"), DateTimeStyles.None, out var from)
|| !DateOnly.TryParseExact(To, "dd.MM.yyyy", CultureInfo.GetCultureInfo("de-DE"), DateTimeStyles.None, out var to))
{ StatusMessage = "Bitte Beginn und Ende als TT.MM.JJJJ eingeben."; return false; }
try
{
var template = SelectedTemplate!.Model is { } installed ? _loader.Load(installed) : StudentReportTemplate.Create();
// Datenbankzugriffe auf dem UI-Thread; nur das Setzen und Schreiben läuft im Hintergrund.
var values = _reports.Build(_student, SelectedGroup!.Model, template,
new(from, to, SelectedInterim?.Model, IncludeInterim, IncludeParticipation, IncludeCompetencies,
IncludeHomework, IncludeDocumentation, IncludeConfidential, Comment));
IsGenerating = true;
await Task.Run(() => File.WriteAllBytes(outputPath, _renderer.RenderToPdf(template, new LetterDataProvider(values))));
StatusMessage = "Schülerbericht erzeugt.";
return true;
}
catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException or QuestPDF.Drawing.Exceptions.DocumentLayoutException)
{ StatusMessage = $"Der Schülerbericht konnte nicht erzeugt werden: {ex.Message}"; return false; }
finally { IsGenerating = false; }
}
}
public sealed record StudentReportTemplateChoice(InstalledTemplate? Model)
{
public string Name => Model?.Name ?? "Schülerbericht Standard";
}
public sealed record StudentReportInterimChoice(InterimGrade Model)
{
public string Label => $"{Model.Date:dd.MM.yyyy} · {Model.Occasion}";
}