Files
LehrerApp/LehrerApp.Core/Services/CompetencyAnalysisService.cs
T
2026-08-16 02:32:12 +02:00

148 lines
5.9 KiB
C#

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<Guid, CompetencyScore> StudentScores { get; init; } =
new Dictionary<Guid, CompetencyScore>();
}
public sealed class CompetencyAnalysisService
{
public List<CompetencyAnalysisItem> Analyze(
IReadOnlyList<CompetencyDomain> catalog,
IReadOnlyList<Unit> units,
IReadOnlyList<Exam> exams,
IReadOnlyList<ExamResult> results)
{
var builders = new List<ItemBuilder>();
var byCode = new Dictionary<string, ItemBuilder>(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<string> NormalizeCodes(IEnumerable<string> 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<Guid, MutableScore> _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);
}
}