148 lines
5.9 KiB
C#
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);
|
|
}
|
|
}
|