108 lines
7.6 KiB
C#
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]);
|
|
}
|
|
}
|