Kompetenzen fast fertig
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user