Kapitel 3.1: Mitarbeit-Aspekte pro Gruppe verwalten (3.1.1/3.1.2/3.1.4)

Neuer Button "Aspekte verwalten" im Mitarbeit-Tab öffnet
ParticipationAspectsDialog: Anlegen, Bezeichnung/Typ/Gewichtung/Aktiv-Status
bearbeiten (speichert sofort, gleiches Muster wie die bestehende
Gewichtungs-Bearbeitung in 3.2.1), Hoch/Runter-Reihenfolge, Löschen mit
Rückfrage. Verwaltet bewusst nur die gruppenspezifischen Aspekte, nicht den
bislang nirgends befüllten globalen Standardkatalog.

Schlüssel ist nur beim Neuanlegen editierbar - er verknüpft AspectRating mit
dem Aspekt per Key, ein nachträgliches Umbenennen würde historische
Bewertungen unauffindbar machen.

IParticipationAspectRepository.Save validiert jetzt Pflichtfelder und
Schlüssel-Eindeutigkeit gegen globale Standards UND eigene Gruppen-Aspekte
zusammen, da beide im Bewertungsraster kombiniert verwendet werden. Neue
GetAllByGroup-Methode (inkl. inaktiver) für die Verwaltungsansicht.

3.1.3 (Scale3/Binary/Points im Bewertungsraster selbst) bewusst nicht
angefasst - eigener, größerer Eingriff in die Bewertungs-UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 23:39:44 +02:00
co-authored by Claude Sonnet 5
parent 10d82e5e2c
commit e40585801c
11 changed files with 567 additions and 11 deletions
+19 -4
View File
@@ -87,10 +87,25 @@ public class FakeEntries : IParticipationRepository
public class FakeAspects : IParticipationAspectRepository
{
public List<ParticipationAspect> GetDefaults() => [];
public List<ParticipationAspect> GetByGroup(Guid groupId) => [];
public void Save(ParticipationAspect aspect) { }
public void Delete(Guid id) { }
private readonly List<ParticipationAspect> _all = [];
public List<ParticipationAspect> GetDefaults() =>
_all.Where(a => a.GroupId == null && a.IsActive).OrderBy(a => a.SortOrder).ToList();
public List<ParticipationAspect> GetByGroup(Guid groupId) =>
_all.Where(a => a.GroupId == groupId && a.IsActive).OrderBy(a => a.SortOrder).ToList();
public List<ParticipationAspect> GetAllByGroup(Guid groupId) =>
_all.Where(a => a.GroupId == groupId).OrderBy(a => a.SortOrder).ToList();
public void Save(ParticipationAspect a)
{
a.Key = a.Key.Trim(); a.Label = a.Label.Trim();
if (a.Key.Length == 0) throw new ArgumentException("Der Schlüssel darf nicht leer sein.");
if (a.Label.Length == 0) throw new ArgumentException("Die Bezeichnung darf nicht leer sein.");
var duplicate = _all.Where(x => x.GroupId == null || x.GroupId == a.GroupId)
.FirstOrDefault(x => x.Id != a.Id && string.Equals(x.Key, a.Key, StringComparison.OrdinalIgnoreCase));
if (duplicate is not null) throw new InvalidOperationException("Ein Aspekt mit diesem Schlüssel existiert für diese Gruppe bereits.");
_all.RemoveAll(x => x.Id == a.Id);
_all.Add(a);
}
public void Delete(Guid id) => _all.RemoveAll(a => a.Id == id);
}
public class FakeGrades : IGradeRepository
@@ -0,0 +1,104 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// Tests für die Aspekt-Verwaltung (3.1.1/3.1.2/3.1.4): Anlegen, Umsortieren, Deaktivieren,
/// Löschen. FakeAspects verhält sich hier wie der echte Repository (Validierung, Duplikatprüfung),
/// damit auch Fehlerfälle über die ViewModel-Schicht hinweg realistisch getestet werden.
public sealed class ParticipationAspectsDialogViewModelTests
{
[Fact]
public void AddAspect_LegtNeuenAspektMitFortlaufenderSortOrderAn()
{
var groupId = Guid.NewGuid();
var vm = new ParticipationAspectsDialogViewModel(new FakeAspects(), groupId)
{
NewKey = "experiment", NewLabel = "Experiment",
};
vm.AddAspectCommand.Execute(null);
var item = Assert.Single(vm.Aspects);
Assert.Equal("experiment", item.Key);
Assert.Equal("Experiment", item.Label);
Assert.Equal("", vm.NewKey); // Formular wird nach Erfolg geleert
}
[Fact]
public void AddAspect_OhneSchluessel_ZeigtFehlerUndLegtNichtsAn()
{
var vm = new ParticipationAspectsDialogViewModel(new FakeAspects(), Guid.NewGuid())
{
NewKey = "", NewLabel = "Experiment",
};
vm.AddAspectCommand.Execute(null);
Assert.Empty(vm.Aspects);
Assert.NotEqual("", vm.NewAspectError);
}
[Fact]
public void AddAspect_DuplikatSchluessel_ZeigtRepositoryFehlerAnUndLegtNichtsAnderesAn()
{
var groupId = Guid.NewGuid();
var repo = new FakeAspects();
var vm = new ParticipationAspectsDialogViewModel(repo, groupId) { NewKey = "a", NewLabel = "Erste" };
vm.AddAspectCommand.Execute(null);
vm.NewKey = "a"; vm.NewLabel = "Zweite";
vm.AddAspectCommand.Execute(null);
Assert.Single(vm.Aspects);
Assert.NotEqual("", vm.NewAspectError);
}
[Fact]
public void MoveAspect_VertauschtReihenfolgeUndPersistiertSortOrder()
{
var groupId = Guid.NewGuid();
var repo = new FakeAspects();
var vm = new ParticipationAspectsDialogViewModel(repo, groupId) { NewKey = "a", NewLabel = "Erste" };
vm.AddAspectCommand.Execute(null);
vm.NewKey = "b"; vm.NewLabel = "Zweite";
vm.AddAspectCommand.Execute(null);
vm.Aspects[1].MoveUpCommand.Execute(null);
Assert.Equal("b", vm.Aspects[0].Key);
Assert.Equal("a", vm.Aspects[1].Key);
var stored = repo.GetAllByGroup(groupId).OrderBy(a => a.SortOrder).ToList();
Assert.Equal(["b", "a"], stored.Select(a => a.Key));
}
[Fact]
public void IsActive_AendernSpeichertSofortUndBleibtInGetAllByGroupSichtbar()
{
var groupId = Guid.NewGuid();
var repo = new FakeAspects();
var vm = new ParticipationAspectsDialogViewModel(repo, groupId) { NewKey = "a", NewLabel = "Erste" };
vm.AddAspectCommand.Execute(null);
vm.Aspects[0].IsActive = false;
Assert.Empty(repo.GetByGroup(groupId));
Assert.Single(repo.GetAllByGroup(groupId));
}
[Fact]
public void Label_UngueltigeAenderung_RolltZurueckUndZeigtFehler()
{
var groupId = Guid.NewGuid();
var repo = new FakeAspects();
var vm = new ParticipationAspectsDialogViewModel(repo, groupId) { NewKey = "a", NewLabel = "Erste" };
vm.AddAspectCommand.Execute(null);
vm.Aspects[0].Label = " "; // leer nach Trim -> Repository lehnt ab
Assert.Equal("Erste", vm.Aspects[0].Label);
Assert.NotEqual("", vm.Aspects[0].ErrorMessage);
Assert.Equal("Erste", repo.GetAllByGroup(groupId).Single().Label);
}
}