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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class CompetencyOverviewViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Initialize_ZeigtAbdeckungGruppenmittelUndVorausgewaehltesSchuelerprofil()
|
||||||
|
{
|
||||||
|
var subjectId = Guid.NewGuid();
|
||||||
|
var group = new LearningGroup { SubjectId = subjectId, GradeLevel = 8 };
|
||||||
|
var student = new Student { FirstName = "Ada", LastName = "Lovelace" };
|
||||||
|
var units = new FakeUnits();
|
||||||
|
units.Add(new Unit
|
||||||
|
{
|
||||||
|
GroupId = group.Id, Status = UnitStatus.Completed, Competencies = ["K1"],
|
||||||
|
});
|
||||||
|
var exam = new Exam
|
||||||
|
{
|
||||||
|
GroupId = group.Id,
|
||||||
|
Status = ExamStatus.Graded,
|
||||||
|
Tasks = [new ExamTask { MaxPoints = 10, CompetencyCodes = ["K1"] }],
|
||||||
|
};
|
||||||
|
var results = new FakeResults();
|
||||||
|
results.Add(new ExamResult { ExamId = exam.Id, StudentId = student.Id, Points = [5] });
|
||||||
|
var catalog = new FakeCompetencyDomains();
|
||||||
|
catalog.Add(new CompetencyDomain
|
||||||
|
{
|
||||||
|
SubjectId = subjectId,
|
||||||
|
GradeLevel = 8,
|
||||||
|
Name = "Bereich",
|
||||||
|
Items = [new CompetencyItem { Code = "K1", Description = "Kompetenz" }],
|
||||||
|
});
|
||||||
|
var vm = new CompetencyOverviewTabViewModel(units, new FakeExams([exam]), results,
|
||||||
|
catalog, new FakeStudents([student]), new CompetencyAnalysisService());
|
||||||
|
|
||||||
|
vm.Initialize(group);
|
||||||
|
|
||||||
|
var row = Assert.Single(vm.Rows);
|
||||||
|
Assert.Equal("1× behandelt", row.InstructionCoverage);
|
||||||
|
Assert.Equal("1× geprüft", row.ExamCoverage);
|
||||||
|
Assert.Contains("50 %", row.GroupResult);
|
||||||
|
Assert.Contains("50 %", row.StudentResult);
|
||||||
|
Assert.Equal("Wiederholungsbedarf", row.Recommendation);
|
||||||
|
Assert.Equal(student.Id, vm.SelectedStudent?.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReviewThreshold_Aendern_AktualisiertHinweis()
|
||||||
|
{
|
||||||
|
var item = new CompetencyAnalysisItem
|
||||||
|
{
|
||||||
|
DomainName = "Bereich",
|
||||||
|
DomainCode = "B",
|
||||||
|
Code = "K1",
|
||||||
|
Description = "Kompetenz",
|
||||||
|
GroupScore = new CompetencyScore(5, 10, 1, 1),
|
||||||
|
};
|
||||||
|
var row = new CompetencyOverviewRow(item, null, 60);
|
||||||
|
|
||||||
|
row.SetThreshold(40);
|
||||||
|
|
||||||
|
Assert.Equal("Stand solide", row.Recommendation);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,7 +234,8 @@ public class FakeCompetencyDomains : ICompetencyDomainRepository
|
|||||||
private readonly List<CompetencyDomain> _all = [];
|
private readonly List<CompetencyDomain> _all = [];
|
||||||
public void Add(CompetencyDomain d) => _all.Add(d);
|
public void Add(CompetencyDomain d) => _all.Add(d);
|
||||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||||
_all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel).ToList();
|
_all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel)
|
||||||
|
.OrderBy(d => d.SortOrder).ToList();
|
||||||
public CompetencyDomain? GetById(Guid id) => _all.FirstOrDefault(d => d.Id == id);
|
public CompetencyDomain? GetById(Guid id) => _all.FirstOrDefault(d => d.Id == id);
|
||||||
public void Save(CompetencyDomain domain) { _all.RemoveAll(d => d.Id == domain.Id); _all.Add(domain); }
|
public void Save(CompetencyDomain domain) { _all.RemoveAll(d => d.Id == domain.Id); _all.Add(domain); }
|
||||||
public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id);
|
public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ public sealed class GroupDetailViewModelTests
|
|||||||
students, memberships, groups, new FakeCompetencyDomains()),
|
students, memberships, groups, new FakeCompetencyDomains()),
|
||||||
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
||||||
new PlanningTabViewModel(new FakeUnits(), new FakeLessons(), groups, subjects,
|
new PlanningTabViewModel(new FakeUnits(), new FakeLessons(), groups, subjects,
|
||||||
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService()));
|
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService()),
|
||||||
|
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
||||||
|
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()));
|
||||||
|
|
||||||
vm.LoadGroup(group.Id);
|
vm.LoadGroup(group.Id);
|
||||||
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ namespace LehrerApp.Desktop.Tests;
|
|||||||
public sealed class SettingsViewModelTests
|
public sealed class SettingsViewModelTests
|
||||||
{
|
{
|
||||||
private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null,
|
private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null,
|
||||||
FakeSupervisionDuties? supervisionDuties = null)
|
FakeSupervisionDuties? supervisionDuties = null,
|
||||||
|
FakeSubjects? subjects = null, FakeCompetencyDomains? competencyDomains = null)
|
||||||
{
|
{
|
||||||
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState,
|
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState,
|
||||||
// das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
// das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
||||||
@@ -18,7 +19,7 @@ public sealed class SettingsViewModelTests
|
|||||||
Directory.CreateDirectory(tempPath);
|
Directory.CreateDirectory(tempPath);
|
||||||
|
|
||||||
return new SettingsViewModel(
|
return new SettingsViewModel(
|
||||||
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
|
subjects ?? new FakeSubjects([]), competencyDomains ?? new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
|
||||||
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
|
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
|
||||||
new DatabaseEncryptionService(), new AppLockService(tempPath),
|
new DatabaseEncryptionService(), new AppLockService(tempPath),
|
||||||
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
||||||
@@ -219,4 +220,82 @@ public sealed class SettingsViewModelTests
|
|||||||
Assert.Empty(duties.GetAll());
|
Assert.Empty(duties.GetAll());
|
||||||
Assert.Empty(vm.SupervisionDuties);
|
Assert.Empty(vm.SupervisionDuties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Kompetenzbereiche_Umsortieren_PersistiertSortOrder()
|
||||||
|
{
|
||||||
|
var subject = new Subject { Name = "Biologie", ShortName = "Bio" };
|
||||||
|
var repository = new FakeCompetencyDomains();
|
||||||
|
repository.Add(new CompetencyDomain
|
||||||
|
{
|
||||||
|
SubjectId = subject.Id, GradeLevel = 10, Name = "Erster Bereich", SortOrder = 0,
|
||||||
|
});
|
||||||
|
repository.Add(new CompetencyDomain
|
||||||
|
{
|
||||||
|
SubjectId = subject.Id, GradeLevel = 10, Name = "Zweiter Bereich", SortOrder = 1,
|
||||||
|
});
|
||||||
|
var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository);
|
||||||
|
vm.CatalogSubject = vm.Subjects.Single();
|
||||||
|
|
||||||
|
vm.Domains[1].MoveUpCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(["Zweiter Bereich", "Erster Bereich"], vm.Domains.Select(x => x.Name));
|
||||||
|
var stored = repository.GetBySubjectAndGrade(subject.Id, 10);
|
||||||
|
Assert.Equal(["Zweiter Bereich", "Erster Bereich"], stored.Select(x => x.Name));
|
||||||
|
Assert.Equal([0, 1], stored.Select(x => x.SortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Kompetenzen_Umsortieren_PersistiertSortOrder()
|
||||||
|
{
|
||||||
|
var repository = new FakeCompetencyDomains();
|
||||||
|
var domain = new CompetencyDomain
|
||||||
|
{
|
||||||
|
Name = "Bereich",
|
||||||
|
Items =
|
||||||
|
[
|
||||||
|
new CompetencyItem { Code = "A", Description = "Erste", SortOrder = 0 },
|
||||||
|
new CompetencyItem { Code = "B", Description = "Zweite", SortOrder = 1 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
repository.Add(domain);
|
||||||
|
var vm = new DomainEditItem(domain, repository);
|
||||||
|
|
||||||
|
vm.Items[1].MoveUpCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(["B", "A"], vm.Items.Select(x => x.Code));
|
||||||
|
var stored = repository.GetById(domain.Id)!;
|
||||||
|
Assert.Equal(["B", "A"], stored.Items.Select(x => x.Code));
|
||||||
|
Assert.Equal([0, 1], stored.Items.Select(x => x.SortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KatalogKopieren_BereitetSichereVorschauFuerZielstufeVor()
|
||||||
|
{
|
||||||
|
var subject = new Subject { Name = "Biologie", ShortName = "Bio" };
|
||||||
|
var repository = new FakeCompetencyDomains();
|
||||||
|
repository.Add(new CompetencyDomain
|
||||||
|
{
|
||||||
|
SubjectId = subject.Id,
|
||||||
|
GradeLevel = 10,
|
||||||
|
Name = "Erkenntnisgewinnung",
|
||||||
|
Code = "EG",
|
||||||
|
Items = [new CompetencyItem { Code = "EG1", Description = "Modelle verwenden" }],
|
||||||
|
});
|
||||||
|
var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository);
|
||||||
|
vm.CatalogSubject = vm.Subjects.Single();
|
||||||
|
|
||||||
|
var preview = vm.PrepareCatalogCopy(11);
|
||||||
|
|
||||||
|
Assert.NotNull(preview);
|
||||||
|
Assert.Equal(11, preview.GradeLevel);
|
||||||
|
Assert.Equal(1, preview.NewDomains);
|
||||||
|
Assert.Equal(1, preview.NewCompetencies);
|
||||||
|
|
||||||
|
vm.ApplyCatalogImport(preview, CompetencyCatalogImportMode.Merge, new HashSet<string>());
|
||||||
|
var copied = repository.GetBySubjectAndGrade(subject.Id, 11);
|
||||||
|
Assert.Single(copied);
|
||||||
|
Assert.NotEqual(repository.GetBySubjectAndGrade(subject.Id, 10)[0].Id, copied[0].Id);
|
||||||
|
Assert.Equal("EG1", copied[0].Items.Single().Code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ public static class AppBootstrapper
|
|||||||
|
|
||||||
// ── Services ──────────────────────────────────────────────────────────
|
// ── Services ──────────────────────────────────────────────────────────
|
||||||
services.AddSingleton<GradingService>();
|
services.AddSingleton<GradingService>();
|
||||||
|
services.AddSingleton<CompetencyAnalysisService>();
|
||||||
services.AddSingleton<SchoolYearService>();
|
services.AddSingleton<SchoolYearService>();
|
||||||
services.AddSingleton<GroupRolloverService>();
|
services.AddSingleton<GroupRolloverService>();
|
||||||
services.AddSingleton<PublicHolidayService>();
|
services.AddSingleton<PublicHolidayService>();
|
||||||
@@ -214,6 +215,7 @@ public static class AppBootstrapper
|
|||||||
services.AddTransient<ParticipationTabViewModel>();
|
services.AddTransient<ParticipationTabViewModel>();
|
||||||
services.AddTransient<GradeOverviewTabViewModel>();
|
services.AddTransient<GradeOverviewTabViewModel>();
|
||||||
services.AddTransient<PlanningTabViewModel>();
|
services.AddTransient<PlanningTabViewModel>();
|
||||||
|
services.AddTransient<CompetencyOverviewTabViewModel>();
|
||||||
services.AddTransient<AddGroupDialogViewModel>();
|
services.AddTransient<AddGroupDialogViewModel>();
|
||||||
services.AddTransient<SettingsViewModel>();
|
services.AddTransient<SettingsViewModel>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
public partial class CompetencyOverviewTabViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IUnitRepository _units;
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly IExamResultRepository _results;
|
||||||
|
private readonly ICompetencyDomainRepository _catalog;
|
||||||
|
private readonly IStudentRepository _students;
|
||||||
|
private readonly CompetencyAnalysisService _analysis;
|
||||||
|
private LearningGroup? _group;
|
||||||
|
|
||||||
|
[ObservableProperty] private CompetencyStudentOption? _selectedStudent;
|
||||||
|
[ObservableProperty] private double _reviewThreshold = 60;
|
||||||
|
[ObservableProperty] private string _summary = "";
|
||||||
|
[ObservableProperty] private string _emptyMessage = "";
|
||||||
|
|
||||||
|
public ObservableCollection<CompetencyStudentOption> StudentOptions { get; } = [];
|
||||||
|
public ObservableCollection<CompetencyOverviewRow> Rows { get; } = [];
|
||||||
|
public bool HasRows => Rows.Count > 0;
|
||||||
|
public bool HasStudents => StudentOptions.Count > 0;
|
||||||
|
public string ThresholdLabel => $"Wiederholungsbedarf unter {ReviewThreshold:0} %";
|
||||||
|
|
||||||
|
public CompetencyOverviewTabViewModel(IUnitRepository units, IExamRepository exams,
|
||||||
|
IExamResultRepository results, ICompetencyDomainRepository catalog,
|
||||||
|
IStudentRepository students, CompetencyAnalysisService analysis)
|
||||||
|
{
|
||||||
|
_units = units;
|
||||||
|
_exams = exams;
|
||||||
|
_results = results;
|
||||||
|
_catalog = catalog;
|
||||||
|
_students = students;
|
||||||
|
_analysis = analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(LearningGroup group)
|
||||||
|
{
|
||||||
|
_group = group;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
if (_group is null) return;
|
||||||
|
var selectedStudentId = SelectedStudent?.Id;
|
||||||
|
var domains = _group.SubjectId is Guid subjectId
|
||||||
|
? _catalog.GetBySubjectAndGrade(subjectId, _group.GradeLevel)
|
||||||
|
: [];
|
||||||
|
var units = _units.GetByGroup(_group.Id);
|
||||||
|
var exams = _exams.GetByGroup(_group.Id);
|
||||||
|
var results = exams.SelectMany(x => _results.GetByExam(x.Id)).ToList();
|
||||||
|
var analysis = _analysis.Analyze(domains, units, exams, results);
|
||||||
|
|
||||||
|
StudentOptions.Clear();
|
||||||
|
foreach (var student in _students.GetByGroup(_group.Id)
|
||||||
|
.OrderBy(x => x.LastName).ThenBy(x => x.FirstName))
|
||||||
|
StudentOptions.Add(new CompetencyStudentOption(student.Id, student.FullName));
|
||||||
|
SelectedStudent = StudentOptions.FirstOrDefault(x => x.Id == selectedStudentId)
|
||||||
|
?? StudentOptions.FirstOrDefault();
|
||||||
|
|
||||||
|
Rows.Clear();
|
||||||
|
foreach (var item in analysis)
|
||||||
|
Rows.Add(new CompetencyOverviewRow(item, SelectedStudent?.Id, ReviewThreshold));
|
||||||
|
|
||||||
|
var treated = analysis.Count(x => x.TaughtUnitCount > 0);
|
||||||
|
var assessed = analysis.Count(x => x.AssessedExamCount > 0);
|
||||||
|
var evaluated = analysis.Count(x => x.GroupScore is not null);
|
||||||
|
Summary = $"{treated} von {analysis.Count} behandelt · {assessed} geprüft · {evaluated} mit Ergebnisdaten";
|
||||||
|
EmptyMessage = analysis.Count == 0
|
||||||
|
? "Noch keine Kompetenzen im Katalog oder in Unterrichtseinheiten und Klausuren zugeordnet."
|
||||||
|
: "";
|
||||||
|
OnPropertyChanged(nameof(HasRows));
|
||||||
|
OnPropertyChanged(nameof(HasStudents));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedStudentChanged(CompetencyStudentOption? value)
|
||||||
|
{
|
||||||
|
foreach (var row in Rows) row.SetStudent(value?.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnReviewThresholdChanged(double value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(ThresholdLabel));
|
||||||
|
foreach (var row in Rows) row.SetThreshold(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CompetencyStudentOption(Guid Id, string DisplayName);
|
||||||
|
|
||||||
|
public partial class CompetencyOverviewRow : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly CompetencyAnalysisItem _item;
|
||||||
|
private double _threshold;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _studentResult = "–";
|
||||||
|
[ObservableProperty] private string _recommendation = "Noch keine Bewertung";
|
||||||
|
[ObservableProperty] private string _recommendationColor = "#6B7280";
|
||||||
|
|
||||||
|
public string Domain => string.IsNullOrWhiteSpace(_item.DomainCode)
|
||||||
|
? _item.DomainName
|
||||||
|
: $"{_item.DomainName} ({_item.DomainCode})";
|
||||||
|
public string Code => string.IsNullOrWhiteSpace(_item.Code) ? "–" : _item.Code;
|
||||||
|
public string Description => _item.Description;
|
||||||
|
public string InstructionCoverage => CoverageText(
|
||||||
|
_item.TaughtUnitCount, "behandelt", _item.PlannedUnitCount, "geplant");
|
||||||
|
public string ExamCoverage => CoverageText(
|
||||||
|
_item.AssessedExamCount, "geprüft", _item.PlannedExamCount, "geplant");
|
||||||
|
public string GroupResult => ScoreText(_item.GroupScore, includeParticipants: true);
|
||||||
|
|
||||||
|
public CompetencyOverviewRow(CompetencyAnalysisItem item, Guid? studentId, double threshold)
|
||||||
|
{
|
||||||
|
_item = item;
|
||||||
|
_threshold = threshold;
|
||||||
|
SetStudent(studentId);
|
||||||
|
UpdateRecommendation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStudent(Guid? studentId)
|
||||||
|
{
|
||||||
|
StudentResult = studentId is Guid id && _item.StudentScores.TryGetValue(id, out var score)
|
||||||
|
? ScoreText(score, includeParticipants: false)
|
||||||
|
: "–";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetThreshold(double threshold)
|
||||||
|
{
|
||||||
|
_threshold = threshold;
|
||||||
|
UpdateRecommendation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateRecommendation()
|
||||||
|
{
|
||||||
|
if (_item.GroupScore is null)
|
||||||
|
{
|
||||||
|
Recommendation = "Noch keine Bewertung";
|
||||||
|
RecommendationColor = "#6B7280";
|
||||||
|
}
|
||||||
|
else if (_item.GroupScore.Percent < _threshold)
|
||||||
|
{
|
||||||
|
Recommendation = "Wiederholungsbedarf";
|
||||||
|
RecommendationColor = "#C62828";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Recommendation = "Stand solide";
|
||||||
|
RecommendationColor = "#2E7D32";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CoverageText(int completed, string completedLabel, int planned, string plannedLabel)
|
||||||
|
{
|
||||||
|
if (completed == 0 && planned == 0) return "–";
|
||||||
|
if (completed == 0) return $"{planned}× {plannedLabel}";
|
||||||
|
if (planned == 0) return $"{completed}× {completedLabel}";
|
||||||
|
return $"{completed}× {completedLabel} · {planned}× {plannedLabel}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ScoreText(CompetencyScore? score, bool includeParticipants)
|
||||||
|
{
|
||||||
|
if (score is null) return "–";
|
||||||
|
var evidence = score.EvidenceCount == 1 ? "1 Aufgabenwert" : $"{score.EvidenceCount} Aufgabenwerte";
|
||||||
|
return includeParticipants
|
||||||
|
? $"{score.Percent:0.#} % · {score.ParticipantCount} Schüler · {evidence}"
|
||||||
|
: $"{score.Percent:0.#} % · {evidence}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -206,6 +206,10 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
NotifyWriteCommands();
|
NotifyWriteCommands();
|
||||||
}
|
}
|
||||||
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
|
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
|
||||||
|
partial void OnActiveTabIndexChanged(int value)
|
||||||
|
{
|
||||||
|
if (value == 6) CompetencyOverviewTab.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||||
@@ -213,6 +217,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public ParticipationTabViewModel ParticipationTab { get; }
|
public ParticipationTabViewModel ParticipationTab { get; }
|
||||||
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
||||||
public PlanningTabViewModel PlanningTab { get; }
|
public PlanningTabViewModel PlanningTab { get; }
|
||||||
|
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
|
||||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||||
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
||||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||||
@@ -227,13 +232,14 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||||
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
||||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
||||||
PlanningTabViewModel planningTab)
|
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab)
|
||||||
{
|
{
|
||||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||||
_exams = exams; _grades = grades; _tasks = tasks;
|
_exams = exams; _grades = grades; _tasks = tasks;
|
||||||
ParticipationTab = participationTab;
|
ParticipationTab = participationTab;
|
||||||
GradeOverviewTab = gradeOverviewTab;
|
GradeOverviewTab = gradeOverviewTab;
|
||||||
PlanningTab = planningTab;
|
PlanningTab = planningTab;
|
||||||
|
CompetencyOverviewTab = competencyOverviewTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadGroup(Guid id)
|
public void LoadGroup(Guid id)
|
||||||
@@ -252,6 +258,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear, IsReadOnly);
|
ParticipationTab.Initialize(Group.Id, Group.SchoolYear, IsReadOnly);
|
||||||
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
|
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
|
||||||
PlanningTab.Initialize(Group.Id, IsReadOnly);
|
PlanningTab.Initialize(Group.Id, IsReadOnly);
|
||||||
|
CompetencyOverviewTab.Initialize(Group);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadExams()
|
private void ReloadExams()
|
||||||
@@ -261,6 +268,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
Exams.Clear();
|
Exams.Clear();
|
||||||
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
|
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
|
||||||
SelectedExam = Exams.FirstOrDefault(e => e.Id == selectedId);
|
SelectedExam = Exams.FirstOrDefault(e => e.Id == selectedId);
|
||||||
|
CompetencyOverviewTab.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadStudents()
|
public void LoadStudents()
|
||||||
@@ -368,6 +376,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
var exam = _exams.GetById(SelectedExam.Id);
|
var exam = _exams.GetById(SelectedExam.Id);
|
||||||
if (exam is null) return;
|
if (exam is null) return;
|
||||||
await OnGradeExam(exam);
|
await OnGradeExam(exam);
|
||||||
|
CompetencyOverviewTab.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
|
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Settings;
|
||||||
|
|
||||||
|
public partial class CompetencyCatalogCopyTargetViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private int _targetGradeLevel;
|
||||||
|
[ObservableProperty] private string _error = "";
|
||||||
|
|
||||||
|
public string SourceLabel { get; }
|
||||||
|
public int SourceGradeLevel { get; }
|
||||||
|
|
||||||
|
public CompetencyCatalogCopyTargetViewModel(string subjectName, int sourceGradeLevel)
|
||||||
|
{
|
||||||
|
SourceGradeLevel = sourceGradeLevel;
|
||||||
|
SourceLabel = $"{subjectName} · Klassenstufe {sourceGradeLevel}";
|
||||||
|
_targetGradeLevel = sourceGradeLevel < 13 ? sourceGradeLevel + 1 : sourceGradeLevel - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
Error = TargetGradeLevel == SourceGradeLevel
|
||||||
|
? "Quell- und Zielklassenstufe müssen verschieden sein."
|
||||||
|
: TargetGradeLevel is < 1 or > 13
|
||||||
|
? "Die Klassenstufe muss zwischen 1 und 13 liegen."
|
||||||
|
: "";
|
||||||
|
return Error.Length == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ public partial class CompetencyCatalogImportViewModel : ObservableObject
|
|||||||
public IReadOnlyList<ImportModeOption> Modes { get; }
|
public IReadOnlyList<ImportModeOption> Modes { get; }
|
||||||
public ObservableCollection<CompetencyImportConflictItem> Conflicts { get; } = [];
|
public ObservableCollection<CompetencyImportConflictItem> Conflicts { get; } = [];
|
||||||
public IReadOnlyList<string> Warnings => _preview.Warnings;
|
public IReadOnlyList<string> Warnings => _preview.Warnings;
|
||||||
|
public string Heading { get; }
|
||||||
public string Target => $"{_preview.SubjectName} · Klassenstufe {_preview.GradeLevel}";
|
public string Target => $"{_preview.SubjectName} · Klassenstufe {_preview.GradeLevel}";
|
||||||
public string Summary =>
|
public string Summary =>
|
||||||
$"{_preview.NewDomains} neue Bereiche · {_preview.NewCompetencies} neue Kompetenzen · " +
|
$"{_preview.NewDomains} neue Bereiche · {_preview.NewCompetencies} neue Kompetenzen · " +
|
||||||
@@ -26,10 +27,12 @@ public partial class CompetencyCatalogImportViewModel : ObservableObject
|
|||||||
public bool IsReplace => SelectedMode.Mode == CompetencyCatalogImportMode.Replace;
|
public bool IsReplace => SelectedMode.Mode == CompetencyCatalogImportMode.Replace;
|
||||||
|
|
||||||
public CompetencyCatalogImportViewModel(
|
public CompetencyCatalogImportViewModel(
|
||||||
SettingsViewModel settings, CompetencyCatalogImportPreview preview)
|
SettingsViewModel settings, CompetencyCatalogImportPreview preview,
|
||||||
|
string heading = "Import prüfen")
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_preview = preview;
|
_preview = preview;
|
||||||
|
Heading = heading;
|
||||||
Modes =
|
Modes =
|
||||||
[
|
[
|
||||||
new(CompetencyCatalogImportMode.Merge, "Zusammenführen (empfohlen)"),
|
new(CompetencyCatalogImportMode.Merge, "Zusammenführen (empfohlen)"),
|
||||||
|
|||||||
@@ -733,7 +733,8 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
CatalogValidation = "";
|
CatalogValidation = "";
|
||||||
if (CatalogSubject is null) return;
|
if (CatalogSubject is null) return;
|
||||||
foreach (var d in _domainRepo.GetBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel))
|
foreach (var d in _domainRepo.GetBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel))
|
||||||
Domains.Add(new DomainEditItem(d, _domainRepo));
|
Domains.Add(CreateDomainEditItem(d));
|
||||||
|
RefreshDomainMoveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Katalog: Bereich hinzufügen / löschen ────────────────────────────────
|
// ── Katalog: Bereich hinzufügen / löschen ────────────────────────────────
|
||||||
@@ -753,7 +754,8 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
SortOrder = Domains.Count,
|
SortOrder = Domains.Count,
|
||||||
};
|
};
|
||||||
_domainRepo.Save(domain);
|
_domainRepo.Save(domain);
|
||||||
Domains.Add(new DomainEditItem(domain, _domainRepo));
|
Domains.Add(CreateDomainEditItem(domain));
|
||||||
|
RefreshDomainMoveState();
|
||||||
NewDomainName = ""; NewDomainCode = ""; CatalogValidation = "";
|
NewDomainName = ""; NewDomainCode = ""; CatalogValidation = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,6 +765,31 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
if (item is null) return;
|
if (item is null) return;
|
||||||
_domainRepo.Delete(item.Id);
|
_domainRepo.Delete(item.Id);
|
||||||
Domains.Remove(item);
|
Domains.Remove(item);
|
||||||
|
PersistDomainOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DomainEditItem CreateDomainEditItem(CompetencyDomain domain) =>
|
||||||
|
new(domain, _domainRepo, item => MoveDomain(item, -1), item => MoveDomain(item, 1));
|
||||||
|
|
||||||
|
private void MoveDomain(DomainEditItem item, int offset)
|
||||||
|
{
|
||||||
|
var oldIndex = Domains.IndexOf(item);
|
||||||
|
var newIndex = oldIndex + offset;
|
||||||
|
if (oldIndex < 0 || newIndex < 0 || newIndex >= Domains.Count) return;
|
||||||
|
Domains.Move(oldIndex, newIndex);
|
||||||
|
PersistDomainOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PersistDomainOrder()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Domains.Count; i++) Domains[i].SetSortOrder(i);
|
||||||
|
RefreshDomainMoveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshDomainMoveState()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Domains.Count; i++)
|
||||||
|
Domains[i].SetMoveState(i > 0, i < Domains.Count - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── JSON Import / Export ──────────────────────────────────────────────────
|
// ── JSON Import / Export ──────────────────────────────────────────────────
|
||||||
@@ -798,11 +825,40 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string ExportCatalog()
|
public string ExportCatalog()
|
||||||
|
=> SerializeCatalog(CatalogGradeLevel);
|
||||||
|
|
||||||
|
public CompetencyCatalogImportPreview? PrepareCatalogCopy(int targetGradeLevel)
|
||||||
|
{
|
||||||
|
if (CatalogSubject is null)
|
||||||
|
{
|
||||||
|
CatalogValidation = "Bitte zuerst ein Fach auswählen.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Domains.Count == 0)
|
||||||
|
{
|
||||||
|
CatalogValidation = "Der ausgewählte Katalog enthält keine Bereiche.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (targetGradeLevel is < 1 or > 13 || targetGradeLevel == CatalogGradeLevel)
|
||||||
|
{
|
||||||
|
CatalogValidation = "Bitte eine andere Zielklassenstufe zwischen 1 und 13 auswählen.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CatalogValidation = "";
|
||||||
|
return _catalogImport.Analyze(SerializeCatalog(targetGradeLevel), CatalogSubject.Id,
|
||||||
|
CatalogSubject.Name, targetGradeLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCatalogCopyStatus(int targetGradeLevel) =>
|
||||||
|
CatalogValidation = $"Kompetenzkatalog wurde in Klassenstufe {targetGradeLevel} kopiert.";
|
||||||
|
|
||||||
|
private string SerializeCatalog(int gradeLevel)
|
||||||
{
|
{
|
||||||
var dto = new CatalogDto
|
var dto = new CatalogDto
|
||||||
{
|
{
|
||||||
Subject = CatalogSubject?.Name ?? "",
|
Subject = CatalogSubject?.Name ?? "",
|
||||||
GradeLevel = CatalogGradeLevel,
|
GradeLevel = gradeLevel,
|
||||||
Domains = Domains.Select(d => new DomainDto
|
Domains = Domains.Select(d => new DomainDto
|
||||||
{
|
{
|
||||||
Name = d.Name,
|
Name = d.Name,
|
||||||
@@ -834,8 +890,11 @@ public partial class DomainEditItem : ObservableObject
|
|||||||
[ObservableProperty] private string _newItemDesc = "";
|
[ObservableProperty] private string _newItemDesc = "";
|
||||||
|
|
||||||
public ObservableCollection<CompetencyItemVm> Items { get; } = [];
|
public ObservableCollection<CompetencyItemVm> Items { get; } = [];
|
||||||
|
public IRelayCommand MoveUpCommand { get; }
|
||||||
|
public IRelayCommand MoveDownCommand { get; }
|
||||||
|
|
||||||
public DomainEditItem(CompetencyDomain domain, ICompetencyDomainRepository repo)
|
public DomainEditItem(CompetencyDomain domain, ICompetencyDomainRepository repo,
|
||||||
|
Action<DomainEditItem>? onMoveUp = null, Action<DomainEditItem>? onMoveDown = null)
|
||||||
{
|
{
|
||||||
_domain = domain;
|
_domain = domain;
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
@@ -846,8 +905,31 @@ public partial class DomainEditItem : ObservableObject
|
|||||||
? domain.Name
|
? domain.Name
|
||||||
: $"{domain.Name} ({domain.Code})";
|
: $"{domain.Name} ({domain.Code})";
|
||||||
|
|
||||||
foreach (var item in domain.Items.OrderBy(i => i.SortOrder))
|
MoveUpCommand = new RelayCommand(() => onMoveUp?.Invoke(this), () => _canMoveUp);
|
||||||
Items.Add(new CompetencyItemVm(item, DeleteItem));
|
MoveDownCommand = new RelayCommand(() => onMoveDown?.Invoke(this), () => _canMoveDown);
|
||||||
|
|
||||||
|
_domain.Items = domain.Items.OrderBy(i => i.SortOrder).ToList();
|
||||||
|
foreach (var item in _domain.Items)
|
||||||
|
Items.Add(CreateItemViewModel(item));
|
||||||
|
RefreshItemMoveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _canMoveUp;
|
||||||
|
private bool _canMoveDown;
|
||||||
|
|
||||||
|
internal void SetMoveState(bool canMoveUp, bool canMoveDown)
|
||||||
|
{
|
||||||
|
_canMoveUp = canMoveUp;
|
||||||
|
_canMoveDown = canMoveDown;
|
||||||
|
MoveUpCommand.NotifyCanExecuteChanged();
|
||||||
|
MoveDownCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetSortOrder(int sortOrder)
|
||||||
|
{
|
||||||
|
if (_domain.SortOrder == sortOrder) return;
|
||||||
|
_domain.SortOrder = sortOrder;
|
||||||
|
_repo.Save(_domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -862,15 +944,42 @@ public partial class DomainEditItem : ObservableObject
|
|||||||
};
|
};
|
||||||
_domain.Items.Add(item);
|
_domain.Items.Add(item);
|
||||||
_repo.Save(_domain);
|
_repo.Save(_domain);
|
||||||
Items.Add(new CompetencyItemVm(item, DeleteItem));
|
Items.Add(CreateItemViewModel(item));
|
||||||
|
RefreshItemMoveState();
|
||||||
NewItemCode = ""; NewItemDesc = "";
|
NewItemCode = ""; NewItemDesc = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteItem(CompetencyItemVm vm)
|
private void DeleteItem(CompetencyItemVm vm)
|
||||||
{
|
{
|
||||||
_domain.Items.RemoveAll(i => i.Id == vm.ItemId);
|
_domain.Items.RemoveAll(i => i.Id == vm.ItemId);
|
||||||
_repo.Save(_domain);
|
|
||||||
Items.Remove(vm);
|
Items.Remove(vm);
|
||||||
|
PersistItemOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompetencyItemVm CreateItemViewModel(CompetencyItem item) =>
|
||||||
|
new(item, DeleteItem, vm => MoveItem(vm, -1), vm => MoveItem(vm, 1));
|
||||||
|
|
||||||
|
private void MoveItem(CompetencyItemVm item, int offset)
|
||||||
|
{
|
||||||
|
var oldIndex = Items.IndexOf(item);
|
||||||
|
var newIndex = oldIndex + offset;
|
||||||
|
if (oldIndex < 0 || newIndex < 0 || newIndex >= Items.Count) return;
|
||||||
|
Items.Move(oldIndex, newIndex);
|
||||||
|
PersistItemOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PersistItemOrder()
|
||||||
|
{
|
||||||
|
_domain.Items = Items.Select(x => x.Model).ToList();
|
||||||
|
for (var i = 0; i < _domain.Items.Count; i++) _domain.Items[i].SortOrder = i;
|
||||||
|
_repo.Save(_domain);
|
||||||
|
RefreshItemMoveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshItemMoveState()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Items.Count; i++)
|
||||||
|
Items[i].SetMoveState(i > 0, i < Items.Count - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,14 +987,19 @@ public partial class DomainEditItem : ObservableObject
|
|||||||
|
|
||||||
public class CompetencyItemVm
|
public class CompetencyItemVm
|
||||||
{
|
{
|
||||||
|
internal CompetencyItem Model { get; }
|
||||||
public Guid ItemId { get; }
|
public Guid ItemId { get; }
|
||||||
public string Code { get; }
|
public string Code { get; }
|
||||||
public string Description { get; }
|
public string Description { get; }
|
||||||
public string Display { get; }
|
public string Display { get; }
|
||||||
public IRelayCommand DeleteCommand { get; }
|
public IRelayCommand DeleteCommand { get; }
|
||||||
|
public IRelayCommand MoveUpCommand { get; }
|
||||||
|
public IRelayCommand MoveDownCommand { get; }
|
||||||
|
|
||||||
public CompetencyItemVm(CompetencyItem item, Action<CompetencyItemVm> onDelete)
|
public CompetencyItemVm(CompetencyItem item, Action<CompetencyItemVm> onDelete,
|
||||||
|
Action<CompetencyItemVm>? onMoveUp = null, Action<CompetencyItemVm>? onMoveDown = null)
|
||||||
{
|
{
|
||||||
|
Model = item;
|
||||||
ItemId = item.Id;
|
ItemId = item.Id;
|
||||||
Code = item.Code;
|
Code = item.Code;
|
||||||
Description = item.Description;
|
Description = item.Description;
|
||||||
@@ -893,6 +1007,19 @@ public class CompetencyItemVm
|
|||||||
? item.Description
|
? item.Description
|
||||||
: $"[{item.Code}] {item.Description}";
|
: $"[{item.Code}] {item.Description}";
|
||||||
DeleteCommand = new RelayCommand(() => onDelete(this));
|
DeleteCommand = new RelayCommand(() => onDelete(this));
|
||||||
|
MoveUpCommand = new RelayCommand(() => onMoveUp?.Invoke(this), () => _canMoveUp);
|
||||||
|
MoveDownCommand = new RelayCommand(() => onMoveDown?.Invoke(this), () => _canMoveDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _canMoveUp;
|
||||||
|
private bool _canMoveDown;
|
||||||
|
|
||||||
|
internal void SetMoveState(bool canMoveUp, bool canMoveDown)
|
||||||
|
{
|
||||||
|
_canMoveUp = canMoveUp;
|
||||||
|
_canMoveDown = canMoveDown;
|
||||||
|
MoveUpCommand.NotifyCanExecuteChanged();
|
||||||
|
MoveDownCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.CompetencyOverviewTabView"
|
||||||
|
x:DataType="vm:CompetencyOverviewTabViewModel">
|
||||||
|
<Grid RowDefinitions="Auto,*" Margin="20">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="10" Margin="0,0,0,12">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="3">
|
||||||
|
<TextBlock Text="Kompetenzübersicht" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.65"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="Aktualisieren" Command="{Binding RefreshCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,16,210" IsVisible="{Binding HasRows}">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Schülerprofil" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding StudentOptions}" SelectedItem="{Binding SelectedStudent}"
|
||||||
|
PlaceholderText="Keine Schüler vorhanden" IsEnabled="{Binding HasStudents}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:CompetencyStudentOption">
|
||||||
|
<TextBlock Text="{Binding DisplayName}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="{Binding ThresholdLabel}" FontSize="12" Opacity="0.7"/>
|
||||||
|
<NumericUpDown Value="{Binding ReviewThreshold}" Minimum="0" Maximum="100"
|
||||||
|
Increment="5" FormatString="0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="Prozentwerte beruhen auf bewerteten Klausuraufgaben. Eine Aufgabe mit mehreren Kompetenzcodes zählt für jede dieser Kompetenzen."
|
||||||
|
TextWrapping="Wrap" FontSize="11" Opacity="0.55" IsVisible="{Binding HasRows}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<TextBlock Text="{Binding EmptyMessage}" Classes="emptyhint"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
TextWrapping="Wrap" MaxWidth="520"
|
||||||
|
IsVisible="{Binding !HasRows}"/>
|
||||||
|
|
||||||
|
<DataGrid ItemsSource="{Binding Rows}" AutoGenerateColumns="False" IsReadOnly="True"
|
||||||
|
GridLinesVisibility="Horizontal" CanUserReorderColumns="False"
|
||||||
|
CanUserResizeColumns="True" IsVisible="{Binding HasRows}">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Bereich" Binding="{Binding Domain}" Width="170"/>
|
||||||
|
<DataGridTextColumn Header="Code" Binding="{Binding Code}" Width="75"/>
|
||||||
|
<DataGridTextColumn Header="Kompetenz" Binding="{Binding Description}" Width="2*"/>
|
||||||
|
<DataGridTextColumn Header="Unterricht" Binding="{Binding InstructionCoverage}" Width="145"/>
|
||||||
|
<DataGridTextColumn Header="Klausuren" Binding="{Binding ExamCoverage}" Width="135"/>
|
||||||
|
<DataGridTextColumn Header="Gruppenmittel" Binding="{Binding GroupResult}" Width="210"/>
|
||||||
|
<DataGridTextColumn Header="Ausgewählter Schüler" Binding="{Binding StudentResult}" Width="160"/>
|
||||||
|
<DataGridTemplateColumn Header="Hinweis" Width="155">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:CompetencyOverviewRow">
|
||||||
|
<TextBlock Text="{Binding Recommendation}" Foreground="{Binding RecommendationColor}"
|
||||||
|
FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class CompetencyOverviewTabView : UserControl
|
||||||
|
{
|
||||||
|
public CompetencyOverviewTabView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -191,6 +191,11 @@
|
|||||||
<views:PlanningTabView DataContext="{Binding PlanningTab}"/>
|
<views:PlanningTabView DataContext="{Binding PlanningTab}"/>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: Kompetenzen -->
|
||||||
|
<ContentPage Header="Kompetenzen">
|
||||||
|
<views:CompetencyOverviewTabView DataContext="{Binding CompetencyOverviewTab}"/>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Dokumentation -->
|
<!-- Tab: Dokumentation -->
|
||||||
<ContentPage Header="Dokumentation">
|
<ContentPage Header="Dokumentation">
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
|||||||
@@ -132,11 +132,16 @@
|
|||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="5"/>
|
CommandParameter="5"/>
|
||||||
<Button Content="📋 Dokumentation"
|
<Button Content="🎯 Kompetenzübersicht"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="6"/>
|
CommandParameter="6"/>
|
||||||
|
<Button Content="📋 Dokumentation"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
|
Padding="12,9"
|
||||||
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
|
CommandParameter="7"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Settings.CompetencyCatalogCopyTargetDialog"
|
||||||
|
x:DataType="vm:CompetencyCatalogCopyTargetViewModel"
|
||||||
|
Title="Kompetenzkatalog kopieren"
|
||||||
|
Width="460" Height="280" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="8">
|
||||||
|
<TextBlock Text="Katalog kopieren" Classes="dialogtitle"/>
|
||||||
|
<TextBlock Text="{Binding SourceLabel}" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="Wähle die Zielklassenstufe. Anschließend kannst du vorhandene Einträge und Konflikte in einer Vorschau prüfen."
|
||||||
|
TextWrapping="Wrap" FontSize="12" Opacity="0.7"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Spacing="5" Margin="0,16,0,0">
|
||||||
|
<TextBlock Text="Zielklassenstufe" FontSize="12" Opacity="0.7"/>
|
||||||
|
<NumericUpDown Value="{Binding TargetGradeLevel}" Minimum="1" Maximum="13" FormatString="0"/>
|
||||||
|
<TextBlock Text="{Binding Error}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding Error, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" ColumnDefinitions="*,10,*" Margin="0,16,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Weiter zur Vorschau" HorizontalAlignment="Stretch" Click="OnContinue"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Settings;
|
||||||
|
|
||||||
|
public partial class CompetencyCatalogCopyTargetDialog : Window
|
||||||
|
{
|
||||||
|
public CompetencyCatalogCopyTargetDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnContinue(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is CompetencyCatalogCopyTargetViewModel vm && vm.Validate())
|
||||||
|
Close((int?)vm.TargetGradeLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||||
|
}
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
||||||
x:Class="LehrerApp.Desktop.Views.Settings.CompetencyCatalogImportDialog"
|
x:Class="LehrerApp.Desktop.Views.Settings.CompetencyCatalogImportDialog"
|
||||||
x:DataType="vm:CompetencyCatalogImportViewModel"
|
x:DataType="vm:CompetencyCatalogImportViewModel"
|
||||||
Title="Kompetenzkatalog importieren"
|
Title="{Binding Heading}"
|
||||||
Width="760" Height="720" MinWidth="620" MinHeight="560"
|
Width="760" Height="720" MinWidth="620" MinHeight="560"
|
||||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
|
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
|
||||||
<StackPanel Grid.Row="0" Spacing="10">
|
<StackPanel Grid.Row="0" Spacing="10">
|
||||||
<TextBlock Text="Import prüfen" Classes="dialogtitle"/>
|
<TextBlock Text="{Binding Heading}" Classes="dialogtitle"/>
|
||||||
<TextBlock Text="{Binding Target}" FontWeight="SemiBold"/>
|
<TextBlock Text="{Binding Target}" FontWeight="SemiBold"/>
|
||||||
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.7"/>
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="640">
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="640">
|
||||||
|
|
||||||
<!-- Fach + Klassenstufe Auswahl + Import/Export -->
|
<!-- Fach + Klassenstufe Auswahl + Import/Export -->
|
||||||
<Grid ColumnDefinitions="*,12,120,12,Auto,8,Auto">
|
<Grid ColumnDefinitions="*,12,100,12,Auto,8,Auto,8,Auto">
|
||||||
<ComboBox Grid.Column="0"
|
<ComboBox Grid.Column="0"
|
||||||
ItemsSource="{Binding Subjects}"
|
ItemsSource="{Binding Subjects}"
|
||||||
SelectedItem="{Binding CatalogSubject}"
|
SelectedItem="{Binding CatalogSubject}"
|
||||||
@@ -130,6 +130,9 @@
|
|||||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
<Button Grid.Column="6" Content="JSON exportieren" Click="OnExportClick"
|
<Button Grid.Column="6" Content="JSON exportieren" Click="OnExportClick"
|
||||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
|
<Button Grid.Column="8" Content="Kopieren…" Click="OnCopyCatalogClick"
|
||||||
|
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||||
|
ToolTip.Tip="Katalog in eine andere Klassenstufe kopieren"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Katalog-Validierungsmeldung -->
|
<!-- Katalog-Validierungsmeldung -->
|
||||||
@@ -150,10 +153,14 @@
|
|||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
|
|
||||||
<!-- Bereichs-Header -->
|
<!-- Bereichs-Header -->
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto,4,Auto,8,Auto">
|
||||||
<TextBlock Grid.Column="0" Text="{Binding DisplayName}"
|
<TextBlock Grid.Column="0" Text="{Binding DisplayName}"
|
||||||
FontWeight="SemiBold" FontSize="14" VerticalAlignment="Center"/>
|
FontWeight="SemiBold" FontSize="14" VerticalAlignment="Center"/>
|
||||||
<Button Grid.Column="1" Content="Bereich löschen" FontSize="12" Padding="10,4"
|
<Button Grid.Column="1" Content="↑" FontSize="13" Padding="8,3"
|
||||||
|
Command="{Binding MoveUpCommand}" ToolTip.Tip="Bereich nach oben"/>
|
||||||
|
<Button Grid.Column="3" Content="↓" FontSize="13" Padding="8,3"
|
||||||
|
Command="{Binding MoveDownCommand}" ToolTip.Tip="Bereich nach unten"/>
|
||||||
|
<Button Grid.Column="5" Content="Bereich löschen" FontSize="12" Padding="10,4"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteDomainCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteDomainCommand}"
|
||||||
CommandParameter="{Binding}"/>
|
CommandParameter="{Binding}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -162,11 +169,15 @@
|
|||||||
<ItemsControl ItemsSource="{Binding Items}">
|
<ItemsControl ItemsSource="{Binding Items}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="vm:CompetencyItemVm">
|
<DataTemplate DataType="vm:CompetencyItemVm">
|
||||||
<Grid ColumnDefinitions="*,Auto" Margin="0,3">
|
<Grid ColumnDefinitions="*,Auto,4,Auto,6,Auto" Margin="0,3">
|
||||||
<TextBlock Grid.Column="0" Text="{Binding Display}"
|
<TextBlock Grid.Column="0" Text="{Binding Display}"
|
||||||
TextWrapping="Wrap" VerticalAlignment="Center"
|
TextWrapping="Wrap" VerticalAlignment="Center"
|
||||||
FontSize="13"/>
|
FontSize="13"/>
|
||||||
<Button Grid.Column="1" Content="×" Padding="7,2" FontSize="13"
|
<Button Grid.Column="1" Content="↑" Padding="7,2" FontSize="12"
|
||||||
|
Command="{Binding MoveUpCommand}" ToolTip.Tip="Kompetenz nach oben"/>
|
||||||
|
<Button Grid.Column="3" Content="↓" Padding="7,2" FontSize="12"
|
||||||
|
Command="{Binding MoveDownCommand}" ToolTip.Tip="Kompetenz nach unten"/>
|
||||||
|
<Button Grid.Column="5" Content="×" Padding="7,2" FontSize="13"
|
||||||
Command="{Binding DeleteCommand}"/>
|
Command="{Binding DeleteCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@@ -93,6 +93,30 @@ public partial class SettingsView : UserControl
|
|||||||
await File.WriteAllTextAsync(file.Path.LocalPath, json);
|
await File.WriteAllTextAsync(file.Path.LocalPath, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnCopyCatalogClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not SettingsViewModel { CatalogSubject: not null } vm) return;
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return;
|
||||||
|
|
||||||
|
var targetDialog = new CompetencyCatalogCopyTargetDialog
|
||||||
|
{
|
||||||
|
DataContext = new CompetencyCatalogCopyTargetViewModel(
|
||||||
|
vm.CatalogSubject.Name, vm.CatalogGradeLevel),
|
||||||
|
};
|
||||||
|
var targetGradeLevel = await targetDialog.ShowDialog<int?>(owner);
|
||||||
|
if (targetGradeLevel is null) return;
|
||||||
|
|
||||||
|
var preview = vm.PrepareCatalogCopy(targetGradeLevel.Value);
|
||||||
|
if (preview is null) return;
|
||||||
|
var previewDialog = new CompetencyCatalogImportDialog
|
||||||
|
{
|
||||||
|
DataContext = new CompetencyCatalogImportViewModel(vm, preview, "Kopie prüfen"),
|
||||||
|
};
|
||||||
|
if (await previewDialog.ShowDialog<bool>(owner))
|
||||||
|
vm.SetCatalogCopyStatus(targetGradeLevel.Value);
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnEditGradingKeyTemplateClick(object? sender, RoutedEventArgs e)
|
private async void OnEditGradingKeyTemplateClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Button { Tag: GradingKeyTemplateEditItem item }) return;
|
if (sender is not Button { Tag: GradingKeyTemplateEditItem item }) return;
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Tests;
|
||||||
|
|
||||||
|
public sealed class CompetencyAnalysisServiceTests
|
||||||
|
{
|
||||||
|
private readonly CompetencyAnalysisService _service = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_UnterscheidetPlanungBehandlungUndPruefung()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var catalog = Catalog();
|
||||||
|
var units = new List<Unit>
|
||||||
|
{
|
||||||
|
new() { GroupId = groupId, Status = UnitStatus.Planned, Competencies = ["K1"] },
|
||||||
|
new() { GroupId = groupId, Status = UnitStatus.Active, Competencies = ["K1"] },
|
||||||
|
new() { GroupId = groupId, Status = UnitStatus.Completed, Competencies = ["K1"] },
|
||||||
|
};
|
||||||
|
var exams = new List<Exam>
|
||||||
|
{
|
||||||
|
new() { GroupId = groupId, Status = ExamStatus.Planned,
|
||||||
|
Tasks = [new ExamTask { MaxPoints = 10, CompetencyCodes = ["K1"] }] },
|
||||||
|
new() { GroupId = groupId, Status = ExamStatus.Graded,
|
||||||
|
Tasks = [new ExamTask { MaxPoints = 10, CompetencyCodes = ["K1"] }] },
|
||||||
|
};
|
||||||
|
|
||||||
|
var item = Assert.Single(_service.Analyze(catalog, units, exams, []));
|
||||||
|
|
||||||
|
Assert.Equal(1, item.PlannedUnitCount);
|
||||||
|
Assert.Equal(2, item.TaughtUnitCount);
|
||||||
|
Assert.Equal(1, item.PlannedExamCount);
|
||||||
|
Assert.Equal(1, item.AssessedExamCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_BerechnetGewichteteGruppenUndSchuelerprofileAusAufgabenpunkten()
|
||||||
|
{
|
||||||
|
var firstStudent = Guid.NewGuid();
|
||||||
|
var secondStudent = Guid.NewGuid();
|
||||||
|
var absentStudent = Guid.NewGuid();
|
||||||
|
var exam = new Exam
|
||||||
|
{
|
||||||
|
Status = ExamStatus.Graded,
|
||||||
|
Tasks =
|
||||||
|
[
|
||||||
|
new ExamTask { MaxPoints = 10, CompetencyCodes = ["K1"] },
|
||||||
|
new ExamTask { MaxPoints = 20, CompetencyCodes = ["K1"] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
var results = new List<ExamResult>
|
||||||
|
{
|
||||||
|
new() { ExamId = exam.Id, StudentId = firstStudent, Points = [5, 10] },
|
||||||
|
new() { ExamId = exam.Id, StudentId = secondStudent, Points = [10, 20] },
|
||||||
|
new() { ExamId = exam.Id, StudentId = absentStudent, Points = [0, 0], Absent = true },
|
||||||
|
};
|
||||||
|
|
||||||
|
var item = Assert.Single(_service.Analyze(Catalog(), [], [exam], results));
|
||||||
|
|
||||||
|
Assert.NotNull(item.GroupScore);
|
||||||
|
Assert.Equal(75, item.GroupScore.Percent, 6);
|
||||||
|
Assert.Equal(2, item.GroupScore.ParticipantCount);
|
||||||
|
Assert.Equal(4, item.GroupScore.EvidenceCount);
|
||||||
|
Assert.Equal(50, item.StudentScores[firstStudent].Percent, 6);
|
||||||
|
Assert.Equal(100, item.StudentScores[secondStudent].Percent, 6);
|
||||||
|
Assert.False(item.StudentScores.ContainsKey(absentStudent));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_AufgabeMitMehrerenCodes_WertetJedeZugeordneteKompetenzAus()
|
||||||
|
{
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var exam = new Exam
|
||||||
|
{
|
||||||
|
Status = ExamStatus.Returned,
|
||||||
|
Tasks = [new ExamTask { MaxPoints = 8, CompetencyCodes = ["K1", "K2"] }],
|
||||||
|
};
|
||||||
|
var result = new ExamResult { ExamId = exam.Id, StudentId = studentId, Points = [6] };
|
||||||
|
|
||||||
|
var analysis = _service.Analyze(Catalog(includeSecond: true), [], [exam], [result]);
|
||||||
|
|
||||||
|
Assert.All(analysis, item => Assert.Equal(75, item.StudentScores[studentId].Percent, 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_DurchgefuehrteAberNichtKorrigierteKlausur_LiefertNochKeinProfil()
|
||||||
|
{
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var exam = new Exam
|
||||||
|
{
|
||||||
|
Status = ExamStatus.Conducted,
|
||||||
|
Tasks = [new ExamTask { MaxPoints = 10, CompetencyCodes = ["K1"] }],
|
||||||
|
};
|
||||||
|
var result = new ExamResult { ExamId = exam.Id, StudentId = studentId, Points = [5] };
|
||||||
|
|
||||||
|
var item = Assert.Single(_service.Analyze(Catalog(), [], [exam], [result]));
|
||||||
|
|
||||||
|
Assert.Equal(1, item.AssessedExamCount);
|
||||||
|
Assert.Null(item.GroupScore);
|
||||||
|
Assert.Empty(item.StudentScores);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyze_VerwendeterUnbekannterCode_BleibtInUebersichtSichtbar()
|
||||||
|
{
|
||||||
|
var unit = new Unit { Status = UnitStatus.Active, Competencies = ["ALT1"] };
|
||||||
|
|
||||||
|
var analysis = _service.Analyze(Catalog(), [unit], [], []);
|
||||||
|
|
||||||
|
var unknown = Assert.Single(analysis, x => x.Code == "ALT1");
|
||||||
|
Assert.Equal("Nicht im Katalog", unknown.DomainName);
|
||||||
|
Assert.Equal(1, unknown.TaughtUnitCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CompetencyDomain> Catalog(bool includeSecond = false) =>
|
||||||
|
[
|
||||||
|
new CompetencyDomain
|
||||||
|
{
|
||||||
|
Name = "Bereich",
|
||||||
|
Code = "B",
|
||||||
|
Items = includeSecond
|
||||||
|
?
|
||||||
|
[
|
||||||
|
new CompetencyItem { Code = "K1", Description = "Erste Kompetenz", SortOrder = 0 },
|
||||||
|
new CompetencyItem { Code = "K2", Description = "Zweite Kompetenz", SortOrder = 1 },
|
||||||
|
]
|
||||||
|
: [new CompetencyItem { Code = "K1", Description = "Erste Kompetenz" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -990,11 +990,15 @@ Katalogverwaltung und JSON-Import existieren in
|
|||||||
Format dokumentiert in [Kompetenzkatalog-KI-Prompt.md](docs/Kompetenzkatalog-KI-Prompt.md).
|
Format dokumentiert in [Kompetenzkatalog-KI-Prompt.md](docs/Kompetenzkatalog-KI-Prompt.md).
|
||||||
|
|
||||||
### 8.1 Katalogverwaltung
|
### 8.1 Katalogverwaltung
|
||||||
- [ ] **8.1.1** Kompetenzen innerhalb eines Bereichs umsortieren (`SortOrder` bearbeitbar machen).
|
- [x] **8.1.1** Kompetenzen innerhalb eines Bereichs umsortieren (`SortOrder` bearbeitbar machen).
|
||||||
|
Bereiche und einzelne Kompetenzen lassen sich über Hoch-/Runter-Schaltflächen verschieben;
|
||||||
|
die Reihenfolge wird unmittelbar und lückenlos in `SortOrder` gespeichert.
|
||||||
- [x] **8.1.2** Katalog exportieren (JSON) — Gegenstück zum vorhandenen Import.
|
- [x] **8.1.2** Katalog exportieren (JSON) — Gegenstück zum vorhandenen Import.
|
||||||
Bereits über „JSON exportieren“ in den Einstellungen umgesetzt; Fach, Klassenstufe,
|
Bereits über „JSON exportieren“ in den Einstellungen umgesetzt; Fach, Klassenstufe,
|
||||||
Bereiche und Kompetenzen werden vollständig ausgegeben.
|
Bereiche und Kompetenzen werden vollständig ausgegeben.
|
||||||
- [ ] **8.1.3** Katalog von einer Jahrgangsstufe in eine andere kopieren.
|
- [x] **8.1.3** Katalog von einer Jahrgangsstufe in eine andere kopieren.
|
||||||
|
Zielklassenstufe wird in einem Dialog gewählt; anschließend nutzt die Kopie dieselbe sichere
|
||||||
|
Vorschau, Konfliktauswahl und Merge-/Ersetzen-Logik wie der JSON-Import.
|
||||||
- [x] **8.1.4** Import-Konflikte behandeln: Merge statt Ersetzen anbieten.
|
- [x] **8.1.4** Import-Konflikte behandeln: Merge statt Ersetzen anbieten.
|
||||||
Umgesetzt mit vollständiger Validierung vor dem Schreiben, Importvorschau, sicherem Merge
|
Umgesetzt mit vollständiger Validierung vor dem Schreiben, Importvorschau, sicherem Merge
|
||||||
als Vorauswahl, Einzelentscheidung je Konflikt und bestätigungspflichtigem atomarem Ersetzen.
|
als Vorauswahl, Einzelentscheidung je Konflikt und bestätigungspflichtigem atomarem Ersetzen.
|
||||||
@@ -1004,12 +1008,17 @@ Format dokumentiert in [Kompetenzkatalog-KI-Prompt.md](docs/Kompetenzkatalog-KI-
|
|||||||
Bereits im Unterrichtseinheiten-Dialog über die Kompetenz-Auswahl umgesetzt und in
|
Bereits im Unterrichtseinheiten-Dialog über die Kompetenz-Auswahl umgesetzt und in
|
||||||
`Unit.Competencies` gespeichert.
|
`Unit.Competencies` gespeichert.
|
||||||
- [x] **8.2.2** Kompetenzen einzelnen Klausuraufgaben zuordnen — umgesetzt mit 1.2.4.
|
- [x] **8.2.2** Kompetenzen einzelnen Klausuraufgaben zuordnen — umgesetzt mit 1.2.4.
|
||||||
- [ ] **8.2.3** Abdeckungsübersicht: welche Kompetenzen wurden im Schuljahr behandelt/geprüft?
|
- [x] **8.2.3** Abdeckungsübersicht: welche Kompetenzen wurden im Schuljahr behandelt/geprüft?
|
||||||
|
Eigener Tab je Lerngruppe unterscheidet geplante und behandelte Unterrichtseinheiten sowie
|
||||||
|
geplante und tatsächlich durchgeführte Klausuren pro Kompetenz.
|
||||||
|
|
||||||
### 8.3 Kompetenzorientierte Auswertung
|
### 8.3 Kompetenzorientierte Auswertung
|
||||||
- [ ] **8.3.1** Kompetenzprofil je Schüler aus Klausuraufgaben-Ergebnissen berechnen.
|
- [x] **8.3.1** Kompetenzprofil je Schüler aus Klausuraufgaben-Ergebnissen berechnen.
|
||||||
- [ ] **8.3.2** Gruppenübersicht: durchschnittlicher Erfüllungsgrad je Kompetenz,
|
Aufgabenpunkte korrigierter/zurückgegebener Klausuren werden nach zugeordneten Kompetenzcodes
|
||||||
Identifikation von Wiederholungsbedarf.
|
aggregiert; abwesende Schüler und Aufgaben ohne positive Maximalpunktzahl werden nicht gewertet.
|
||||||
|
- [x] **8.3.2** Gruppenübersicht: durchschnittlicher Erfüllungsgrad je Kompetenz,
|
||||||
|
Identifikation von Wiederholungsbedarf. Gruppenmittel, Zahl der beteiligten Schüler und
|
||||||
|
Aufgabenwerte werden angezeigt; die Schwelle für Wiederholungsbedarf ist frei einstellbar.
|
||||||
- [ ] **8.3.3** Kompetenzbericht je Schüler als Ausdruck/Export.
|
- [ ] **8.3.3** Kompetenzbericht je Schüler als Ausdruck/Export.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user