Files
LehrerApp/LehrerApp.Desktop/Services/StudentReportService.cs
T
2026-10-22 11:24:43 +02:00

108 lines
7.6 KiB
C#

using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Templating;
namespace LehrerApp.Desktop.Services;
public sealed record StudentReportOptions(DateOnly From, DateOnly To, InterimGrade? Interim,
bool IncludeInterim = true, bool IncludeParticipation = true, bool IncludeCompetencies = true,
bool IncludeHomework = true, bool IncludeDocumentation = true, bool IncludeConfidential = false,
string Comment = "");
/// <summary>Stellt die vorhandenen Bewertungen eines Schülers für eine Lerngruppe zusammen.
/// Die Berechnungen stammen aus denselben Diensten wie die interaktiven Auswertungen.</summary>
public sealed class StudentReportService(
IParticipationSessionRepository sessions, IParticipationRepository entries,
IParticipationAspectRepository aspects, IExamRepository exams, IExamResultRepository results,
IGradeRepository grades, IGroupMembershipRepository memberships,
ICompetencyDomainRepository domains, IDocumentationRepository documentation,
GradingService grading, CompetencyAnalysisService competencies)
{
public Dictionary<string, PlaceholderValue> Build(Student student, LearningGroup group,
LoadedTemplate template, StudentReportOptions options)
{
if (options.From > options.To) throw new InvalidDataException("Das Ende des Berichtszeitraums liegt vor dem Beginn.");
var values = LetterPlaceholderBuilder.BuildStandardValues(student, null, group, DateOnly.FromDateTime(DateTime.Today), "", "");
values["Report.Period"] = new TextValue($"{options.From:dd.MM.yyyy} - {options.To:dd.MM.yyyy}");
values["Report.Comment"] = new MultilineValue(options.Comment);
foreach (var name in StudentReportTemplate.Drawings) values[name] = new DrawingValue([], 0);
if (options.IncludeInterim && Uses(ReportTextDrawing.InterimGrades))
{
var interim = options.Interim;
if (interim is null || interim.StudentId != student.Id || interim.GroupId != group.Id
|| interim.Date < options.From || interim.Date > options.To)
throw new InvalidDataException("Bitte einen gespeicherten Zwischenstand dieses Schülers im Berichtszeitraum auswählen oder den Baustein abwählen.");
values[ReportTextDrawing.InterimGrades] = GradeReportDrawingBuilder.Interim(interim, template);
}
var membership = memberships.GetByStudentAndGroup(student.Id, group.Id);
bool Within(DateOnly date) => date >= options.From && date <= options.To
&& (membership is null || GroupMembershipService.IsActiveOn(membership, date));
var groupSessions = sessions.GetByGroup(group.Id).Where(s => Within(s.Date)).OrderBy(s => s.Date).ToList();
var bySession = groupSessions.ToDictionary(s => s.Id);
var studentEntries = entries.GetByStudent(student.Id).Where(e => bySession.ContainsKey(e.SessionId)).ToList();
var applicableAspects = aspects.GetDefaults().Concat(aspects.GetByGroup(group.Id))
.GroupBy(a => a.Key).Select(g => g.Last()).ToList();
if (options.IncludeParticipation && Uses(StudentReportTemplate.Participation))
{
var timeline = new ParticipationTimelineBuilder(sessions, entries, aspects, exams, results, grades,
membership is null ? new Dictionary<Guid, GroupMembership>() : new() { [student.Id] = membership },
grading, group.Id, group.GradingSystem);
var points = timeline.BuildPoints(student.Id).Where(p => !p.IsExam && Within(p.Date)).ToList();
List<ReportChartSeries> series = [new("Gewichtete Mitarbeit", "#6C5CE7",
points.Where(p => p.WeightedValue.HasValue).Select(p => new ReportChartPoint(p.Date, p.WeightedValue!.Value)).ToList())];
string[] colors = ["#2E86DE", "#E67E22", "#16A085", "#C0392B"];
foreach (var (aspect, index) in applicableAspects.Select((a, i) => (a, i)))
series.Add(new(aspect.Label, colors[index % colors.Length],
points.Where(p => p.AspectValues.ContainsKey(aspect.Key)).Select(p => new ReportChartPoint(p.Date,
ParticipationRatingScale.Normalize(aspect.ValueType, p.AspectValues[aspect.Key], aspect.MaxPoints))).ToList()));
var box = DrawingBoxLocator.Find(template, StudentReportTemplate.Participation)!.Value;
var drawing = ParticipationReportDrawing.Build(series, options.From, options.To, box.Width, box.MillimeterScale);
if (drawing.ContentHeight > box.Height)
throw new InvalidDataException("Der Mitarbeitsgraph benötigt eine höhere Zeichenbox in der Vorlage.");
values[StudentReportTemplate.Participation] = drawing;
}
if (options.IncludeCompetencies && Uses(StudentReportTemplate.Competencies))
{
var groupExams = exams.GetByGroup(group.Id).Where(e => Within(e.Date)).ToList();
var examIds = groupExams.Select(e => e.Id).ToHashSet();
var scores = competencies.Analyze(domains.GetAll(), [], groupExams,
results.GetByStudent(student.Id).Where(r => examIds.Contains(r.ExamId)).ToList(),
groupSessions, studentEntries, applicableAspects);
var lines = scores.Where(s => s.StudentScores.ContainsKey(student.Id)).Select(s =>
{
var score = s.StudentScores[student.Id];
return $"{s.Code} {s.Description}: {score.Percent.ToString("0", System.Globalization.CultureInfo.GetCultureInfo("de-DE"))} % ({score.EvidenceCount} Bewertungen)";
}).ToList();
Add(StudentReportTemplate.Competencies, "Kompetenzen", lines, "Keine Kompetenzbewertungen im gewählten Zeitraum.");
}
if (options.IncludeHomework && Uses(StudentReportTemplate.Homework))
Add(StudentReportTemplate.Homework, "Hausaufgaben", studentEntries.Where(e => HomeworkDisplay.Effective(e) is not null)
.OrderBy(e => bySession[e.SessionId].Date).Select(e =>
$"{bySession[e.SessionId].Date:dd.MM.yyyy}: {HomeworkDisplay.Label(HomeworkDisplay.Effective(e))}").ToList(),
"Keine Hausaufgabenkontrollen im gewählten Zeitraum.");
if (options.IncludeDocumentation && Uses(StudentReportTemplate.Documentation))
{
var lines = documentation.GetByStudent(student.Id)
.Where(d => !d.IsDeleted && !d.IsDraft && (options.IncludeConfidential || !d.IsConfidential)
&& d.GroupId == group.Id && Within(d.Date) && d.Concerns(student.Id))
.OrderBy(d => d.Date).Select(d =>
$"{d.Date:dd.MM.yyyy} · {DocumentationTypeDisplay.Label(d.Type)}: {d.Title}"
+ (d.IsConfidential ? " [Vertraulich]" : "") + "\n" + d.Content).ToList();
lines.AddRange(studentEntries.Where(e => !string.IsNullOrWhiteSpace(e.Note)).OrderBy(e => bySession[e.SessionId].Date)
.Select(e => $"{bySession[e.SessionId].Date:dd.MM.yyyy} · Unterrichtsnotiz: {e.Note}"));
Add(StudentReportTemplate.Documentation, "Klassenbucheinträge und Unterrichtsnotizen", lines,
"Keine freigegebenen Einträge mit Bezug zu dieser Lerngruppe im gewählten Zeitraum.");
}
return values;
bool Uses(string name) => DrawingBoxLocator.Find(template, name) is not null;
void Add(string name, string title, List<string> lines, string empty) =>
values[name] = GradeReportDrawingBuilder.Text(template, name, title, lines.Count > 0 ? lines : [empty]);
}
}