using LehrerApp.Core.Models; namespace LehrerApp.Core.Services; public sealed record CompetencyScore( double AchievedPoints, double MaxPoints, int EvidenceCount, int ParticipantCount) { public double Percent => MaxPoints <= 0 ? 0 : AchievedPoints / MaxPoints * 100.0; } public sealed class CompetencyAnalysisItem { public required string DomainName { get; init; } public required string DomainCode { get; init; } public required string Code { get; init; } public required string Description { get; init; } public int PlannedUnitCount { get; init; } public int TaughtUnitCount { get; init; } public int PlannedExamCount { get; init; } public int AssessedExamCount { get; init; } public CompetencyScore? GroupScore { get; init; } public IReadOnlyDictionary StudentScores { get; init; } = new Dictionary(); } public sealed class CompetencyAnalysisService { public List Analyze( IReadOnlyList catalog, IReadOnlyList units, IReadOnlyList exams, IReadOnlyList results) { var builders = new List(); var byCode = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var domain in catalog.OrderBy(x => x.SortOrder)) foreach (var item in domain.Items.OrderBy(x => x.SortOrder)) { var builder = new ItemBuilder(domain.Name, domain.Code, item.Code, item.Description); builders.Add(builder); if (!string.IsNullOrWhiteSpace(item.Code)) byCode.TryAdd(item.Code.Trim(), builder); } ItemBuilder GetOrAdd(string rawCode) { var code = rawCode.Trim(); if (byCode.TryGetValue(code, out var existing)) return existing; var added = new ItemBuilder("Nicht im Katalog", "", code, "Dieser Kompetenzcode wird noch verwendet, ist aber nicht im aktuellen Katalog enthalten."); byCode[code] = added; builders.Add(added); return added; } foreach (var unit in units) foreach (var code in NormalizeCodes(unit.Competencies)) { var builder = GetOrAdd(code); if (unit.Status == UnitStatus.Planned) builder.PlannedUnits++; else builder.TaughtUnits++; } var resultsByExam = results.GroupBy(x => x.ExamId).ToDictionary(x => x.Key, x => x.ToList()); foreach (var exam in exams) { var examCodes = NormalizeCodes(exam.Tasks.SelectMany(x => x.CompetencyCodes)); foreach (var code in examCodes) { var builder = GetOrAdd(code); if (exam.Status == ExamStatus.Planned) builder.PlannedExams++; else builder.AssessedExams++; } if (exam.Status is ExamStatus.Planned or ExamStatus.Conducted || !resultsByExam.TryGetValue(exam.Id, out var examResults)) continue; foreach (var result in examResults.Where(x => !x.Absent)) for (var taskIndex = 0; taskIndex < exam.Tasks.Count && taskIndex < result.Points.Count; taskIndex++) { var task = exam.Tasks[taskIndex]; if (task.MaxPoints <= 0) continue; var achieved = Math.Clamp(result.Points[taskIndex], 0, task.MaxPoints); foreach (var code in NormalizeCodes(task.CompetencyCodes)) GetOrAdd(code).AddScore(result.StudentId, achieved, task.MaxPoints); } } return builders.Select(x => x.Build()).ToList(); } private static bool HasCode(string? value) => !string.IsNullOrWhiteSpace(value); private static IEnumerable NormalizeCodes(IEnumerable codes) => codes.Where(HasCode).Select(x => x.Trim()).Distinct(StringComparer.OrdinalIgnoreCase); private sealed class ItemBuilder( string domainName, string domainCode, string code, string description) { private readonly Dictionary _scores = []; public int PlannedUnits { get; set; } public int TaughtUnits { get; set; } public int PlannedExams { get; set; } public int AssessedExams { get; set; } public void AddScore(Guid studentId, double achieved, double maximum) { if (!_scores.TryGetValue(studentId, out var score)) _scores[studentId] = score = new MutableScore(); score.Achieved += achieved; score.Maximum += maximum; score.EvidenceCount++; } public CompetencyAnalysisItem Build() { var studentScores = _scores.ToDictionary(x => x.Key, x => x.Value.ToScore(1)); var achieved = _scores.Values.Sum(x => x.Achieved); var maximum = _scores.Values.Sum(x => x.Maximum); var evidence = _scores.Values.Sum(x => x.EvidenceCount); return new CompetencyAnalysisItem { DomainName = domainName, DomainCode = domainCode, Code = code, Description = description, PlannedUnitCount = PlannedUnits, TaughtUnitCount = TaughtUnits, PlannedExamCount = PlannedExams, AssessedExamCount = AssessedExams, GroupScore = maximum > 0 ? new CompetencyScore(achieved, maximum, evidence, _scores.Count) : null, StudentScores = studentScores, }; } } private sealed class MutableScore { public double Achieved { get; set; } public double Maximum { get; set; } public int EvidenceCount { get; set; } public CompetencyScore ToScore(int participantCount) => new(Achieved, Maximum, EvidenceCount, participantCount); } }