aufräumen
This commit is contained in:
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
<!-- API -->
|
<!-- API -->
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
|
|
||||||
|
|
||||||
<!-- Tests -->
|
<!-- Tests -->
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
|
|||||||
@@ -9,6 +9,5 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<PackageReference Include="LiteDB" />
|
<PackageReference Include="LiteDB" />
|
||||||
<PackageReference Include="System.Text.Json" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ public class ReadableSnapshot
|
|||||||
public DateTime ExportedAt { get; set; }
|
public DateTime ExportedAt { get; set; }
|
||||||
public ReadableSnapshotMeta Meta { get; set; } = new();
|
public ReadableSnapshotMeta Meta { get; set; } = new();
|
||||||
public List<LearningGroup> Groups { get; set; } = [];
|
public List<LearningGroup> Groups { get; set; } = [];
|
||||||
|
public List<Subject> Subjects { get; set; } = [];
|
||||||
public List<Student> Students { get; set; } = [];
|
public List<Student> Students { get; set; } = [];
|
||||||
public List<Enrollment> Enrollments { get; set; } = [];
|
public List<GroupMembership> Memberships { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ReadableSnapshotMeta
|
public class ReadableSnapshotMeta
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public interface IStudentRepository
|
|||||||
{
|
{
|
||||||
Student? GetById(Guid id);
|
Student? GetById(Guid id);
|
||||||
List<Student> GetAll(bool includeInactive = false);
|
List<Student> GetAll(bool includeInactive = false);
|
||||||
List<Student> GetByGroup(Guid groupId, string schoolYear);
|
List<Student> GetByGroup(Guid groupId);
|
||||||
void Save(Student student);
|
void Save(Student student);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
}
|
}
|
||||||
@@ -18,12 +18,12 @@ public interface IGroupRepository
|
|||||||
void Save(LearningGroup group);
|
void Save(LearningGroup group);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
}
|
}
|
||||||
public interface IEnrollmentRepository
|
public interface IGroupMembershipRepository
|
||||||
{
|
{
|
||||||
List<Enrollment> GetByStudent(Guid studentId);
|
List<GroupMembership> GetByStudent(Guid studentId);
|
||||||
List<Enrollment> GetByGroup(Guid groupId);
|
List<GroupMembership> GetByGroup(Guid groupId);
|
||||||
List<Enrollment> GetByGroupAndYear(Guid groupId, string schoolYear);
|
GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId);
|
||||||
void Save(Enrollment enrollment);
|
void Save(GroupMembership membership);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
}
|
}
|
||||||
public interface IExamRepository
|
public interface IExamRepository
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ public class Exam
|
|||||||
public Guid GroupId { get; set; }
|
public Guid GroupId { get; set; }
|
||||||
public string Title { get; set; } = "";
|
public string Title { get; set; } = "";
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
public string Subject { get; set; } = "";
|
|
||||||
public int? ExamNumber { get; set; }
|
public int? ExamNumber { get; set; }
|
||||||
public List<ExamTask> Tasks { get; set; } = [];
|
public List<ExamTask> Tasks { get; set; } = [];
|
||||||
public List<GradingKeyEntry> GradingKey { get; set; } = [];
|
public List<GradingKeyEntry> GradingKey { get; set; } = [];
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ public class LearningGroup
|
|||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public GroupType Type { get; set; }
|
public GroupType Type { get; set; }
|
||||||
public string? Subject { get; set; }
|
|
||||||
public Guid? SubjectId { get; set; }
|
public Guid? SubjectId { get; set; }
|
||||||
public string SchoolYear { get; set; } = "";
|
public string SchoolYear { get; set; } = "";
|
||||||
public int GradeLevel { get; set; }
|
public int GradeLevel { get; set; }
|
||||||
@@ -17,19 +16,23 @@ public class LearningGroup
|
|||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
public class Enrollment
|
/// <summary>
|
||||||
|
/// Mitgliedschaft eines Schülers in einer Lerngruppe. Das Schuljahr gehört zur
|
||||||
|
/// Lerngruppe und wird deshalb hier nicht ein zweites Mal gespeichert.
|
||||||
|
/// </summary>
|
||||||
|
public class GroupMembership
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public Guid StudentId { get; set; }
|
public Guid StudentId { get; set; }
|
||||||
public Guid GroupId { get; set; }
|
public Guid GroupId { get; set; }
|
||||||
public string SchoolYear { get; set; } = "";
|
/// <summary>Tag, an dem die Zuordnung in der App angelegt wurde.</summary>
|
||||||
public DateOnly EnrolledAt { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
public DateOnly AddedOn { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
||||||
public EnrollmentPeriod Period { get; set; } = EnrollmentPeriod.FullYear;
|
public MembershipPeriod Period { get; set; } = MembershipPeriod.FullYear;
|
||||||
public DateOnly? JoinedAt { get; set; }
|
public DateOnly? JoinedAt { get; set; }
|
||||||
public DateOnly? LeftAt { get; set; }
|
public DateOnly? LeftAt { get; set; }
|
||||||
public Niveau? Niveau { get; set; }
|
public Niveau? Niveau { get; set; }
|
||||||
}
|
}
|
||||||
public enum EnrollmentPeriod { FullYear, H1Only, H2Only, Custom }
|
public enum MembershipPeriod { FullYear, H1Only, H2Only, Custom }
|
||||||
public enum GroupType { Class, Course }
|
public enum GroupType { Class, Course }
|
||||||
public enum GradingSystem { Grades1To6, Points0To15 }
|
public enum GradingSystem { Grades1To6, Points0To15 }
|
||||||
public enum Niveau { E, G, Foerder }
|
public enum Niveau { E, G, Foerder }
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ public class ParticipationEntry
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public Guid SessionId { get; set; }
|
public Guid SessionId { get; set; }
|
||||||
public Guid GroupId { get; set; }
|
|
||||||
public Guid StudentId { get; set; }
|
public Guid StudentId { get; set; }
|
||||||
public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
|
||||||
public List<AspectRating> Ratings { get; set; } = [];
|
public List<AspectRating> Ratings { get; set; } = [];
|
||||||
public List<CompetencyRating> CompetencyRatings { get; set; } = [];
|
public List<CompetencyRating> CompetencyRatings { get; set; } = [];
|
||||||
public string? Note { get; set; }
|
public string? Note { get; set; }
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ public class Grade
|
|||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public Guid StudentId { get; set; }
|
public Guid StudentId { get; set; }
|
||||||
public Guid GroupId { get; set; }
|
public Guid GroupId { get; set; }
|
||||||
public string SchoolYear { get; set; } = "";
|
|
||||||
public GradeCategory Category { get; set; }
|
public GradeCategory Category { get; set; }
|
||||||
public string Value { get; set; } = "";
|
public string Value { get; set; } = "";
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
@@ -20,8 +19,6 @@ public class Unit
|
|||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public Guid GroupId { get; set; }
|
public Guid GroupId { get; set; }
|
||||||
public string Title { get; set; } = "";
|
public string Title { get; set; } = "";
|
||||||
public string Subject { get; set; } = "";
|
|
||||||
public string SchoolYear { get; set; } = "";
|
|
||||||
public DateOnly? StartDate { get; set; }
|
public DateOnly? StartDate { get; set; }
|
||||||
public DateOnly? EndDate { get; set; }
|
public DateOnly? EndDate { get; set; }
|
||||||
public List<string> Competencies { get; set; } = [];
|
public List<string> Competencies { get; set; } = [];
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LehrerApp.Data\LehrerApp.Data.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="xunit" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LiteDB;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Data.Tests;
|
||||||
|
|
||||||
|
public sealed class LiteDbContextTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void MigratesLegacyEnrollmentAndSubjectTextWithoutLosingData()
|
||||||
|
{
|
||||||
|
using var temp = new TempDatabase();
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var membershipId = Guid.NewGuid();
|
||||||
|
var examId = Guid.NewGuid();
|
||||||
|
var gradeId = Guid.NewGuid();
|
||||||
|
var entryId = Guid.NewGuid();
|
||||||
|
var addedOn = new DateOnly(2026, 6, 23);
|
||||||
|
|
||||||
|
using (var legacy = new LiteDatabase(temp.Path))
|
||||||
|
{
|
||||||
|
legacy.GetCollection<Subject>("subjects").Insert(new Subject { Name = "mathematik" });
|
||||||
|
legacy.GetCollection<BsonDocument>("groups").Insert(new BsonDocument
|
||||||
|
{
|
||||||
|
["_id"] = groupId,
|
||||||
|
[nameof(LearningGroup.Name)] = "Mathe G",
|
||||||
|
["Subject"] = "Mathematik",
|
||||||
|
[nameof(LearningGroup.SchoolYear)] = "2025/26",
|
||||||
|
});
|
||||||
|
legacy.GetCollection<BsonDocument>("enrollments").Insert(new BsonDocument
|
||||||
|
{
|
||||||
|
["_id"] = membershipId,
|
||||||
|
[nameof(GroupMembership.StudentId)] = studentId,
|
||||||
|
[nameof(GroupMembership.GroupId)] = groupId,
|
||||||
|
["SchoolYear"] = "2025/26",
|
||||||
|
["EnrolledAt"] = BsonMapper.Global.Serialize(addedOn),
|
||||||
|
});
|
||||||
|
legacy.GetCollection<BsonDocument>("exams").Insert(new BsonDocument
|
||||||
|
{
|
||||||
|
["_id"] = examId,
|
||||||
|
[nameof(Exam.GroupId)] = groupId,
|
||||||
|
["Subject"] = "Mathematik",
|
||||||
|
});
|
||||||
|
legacy.GetCollection<BsonDocument>("grades").Insert(new BsonDocument
|
||||||
|
{
|
||||||
|
["_id"] = gradeId,
|
||||||
|
[nameof(Grade.StudentId)] = studentId,
|
||||||
|
[nameof(Grade.GroupId)] = groupId,
|
||||||
|
["SchoolYear"] = "2025/26",
|
||||||
|
});
|
||||||
|
legacy.GetCollection<BsonDocument>("participation").Insert(new BsonDocument
|
||||||
|
{
|
||||||
|
["_id"] = entryId,
|
||||||
|
[nameof(ParticipationEntry.SessionId)] = Guid.NewGuid(),
|
||||||
|
[nameof(ParticipationEntry.StudentId)] = studentId,
|
||||||
|
["GroupId"] = groupId,
|
||||||
|
["Date"] = BsonMapper.Global.Serialize(addedOn),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var context = new LiteDbContext(temp.Path))
|
||||||
|
{
|
||||||
|
var group = context.Groups.FindById(groupId);
|
||||||
|
var membership = context.Memberships.FindById(membershipId);
|
||||||
|
Assert.NotNull(group);
|
||||||
|
Assert.NotNull(group.SubjectId);
|
||||||
|
Assert.Equal("mathematik", context.Subjects.FindById(group.SubjectId.Value)?.Name);
|
||||||
|
Assert.Equal(1, context.Subjects.Count());
|
||||||
|
Assert.NotNull(membership);
|
||||||
|
Assert.Equal(addedOn, membership.AddedOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var migrated = new LiteDatabase(temp.Path);
|
||||||
|
Assert.DoesNotContain("enrollments", migrated.GetCollectionNames());
|
||||||
|
Assert.Contains("group_memberships", migrated.GetCollectionNames());
|
||||||
|
var rawGroup = migrated.GetCollection<BsonDocument>("groups").FindById(groupId);
|
||||||
|
var rawMembership = migrated.GetCollection<BsonDocument>("group_memberships").FindById(membershipId);
|
||||||
|
var rawExam = migrated.GetCollection<BsonDocument>("exams").FindById(examId);
|
||||||
|
var rawGrade = migrated.GetCollection<BsonDocument>("grades").FindById(gradeId);
|
||||||
|
var rawEntry = migrated.GetCollection<BsonDocument>("participation").FindById(entryId);
|
||||||
|
Assert.False(rawGroup.ContainsKey("Subject"));
|
||||||
|
Assert.False(rawMembership.ContainsKey("SchoolYear"));
|
||||||
|
Assert.False(rawMembership.ContainsKey("EnrolledAt"));
|
||||||
|
Assert.True(rawMembership.ContainsKey(nameof(GroupMembership.AddedOn)));
|
||||||
|
Assert.False(rawExam.ContainsKey("Subject"));
|
||||||
|
Assert.False(rawGrade.ContainsKey("SchoolYear"));
|
||||||
|
Assert.False(rawEntry.ContainsKey("GroupId"));
|
||||||
|
Assert.False(rawEntry.ContainsKey("Date"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsASecondMembershipForTheSameStudentAndGroup()
|
||||||
|
{
|
||||||
|
using var temp = new TempDatabase();
|
||||||
|
using var context = new LiteDbContext(temp.Path);
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
context.Memberships.Insert(new GroupMembership { StudentId = studentId, GroupId = groupId });
|
||||||
|
|
||||||
|
Assert.Throws<LiteException>(() => context.Memberships.Insert(
|
||||||
|
new GroupMembership { StudentId = studentId, GroupId = groupId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsASecondExamResultForTheSameStudentAndExam()
|
||||||
|
{
|
||||||
|
using var temp = new TempDatabase();
|
||||||
|
using var context = new LiteDbContext(temp.Path);
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var examId = Guid.NewGuid();
|
||||||
|
context.ExamResults.Insert(new ExamResult { StudentId = studentId, ExamId = examId });
|
||||||
|
|
||||||
|
Assert.Throws<LiteException>(() => context.ExamResults.Insert(
|
||||||
|
new ExamResult { StudentId = studentId, ExamId = examId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TempDatabase : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _directory = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetTempPath(), $"lehrerapp-tests-{Guid.NewGuid():N}");
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public TempDatabase()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_directory);
|
||||||
|
Path = System.IO.Path.Combine(_directory, "test.db");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public class LiteDbContext : IDisposable
|
|||||||
|
|
||||||
public ILiteCollection<Student> Students => _db.GetCollection<Student>("students");
|
public ILiteCollection<Student> Students => _db.GetCollection<Student>("students");
|
||||||
public ILiteCollection<LearningGroup> Groups => _db.GetCollection<LearningGroup>("groups");
|
public ILiteCollection<LearningGroup> Groups => _db.GetCollection<LearningGroup>("groups");
|
||||||
public ILiteCollection<Enrollment> Enrollments => _db.GetCollection<Enrollment>("enrollments");
|
public ILiteCollection<GroupMembership> Memberships => _db.GetCollection<GroupMembership>("group_memberships");
|
||||||
public ILiteCollection<Exam> Exams => _db.GetCollection<Exam>("exams");
|
public ILiteCollection<Exam> Exams => _db.GetCollection<Exam>("exams");
|
||||||
public ILiteCollection<ExamResult> ExamResults => _db.GetCollection<ExamResult>("exam_results");
|
public ILiteCollection<ExamResult> ExamResults => _db.GetCollection<ExamResult>("exam_results");
|
||||||
public ILiteCollection<Grade> Grades => _db.GetCollection<Grade>("grades");
|
public ILiteCollection<Grade> Grades => _db.GetCollection<Grade>("grades");
|
||||||
@@ -42,7 +42,11 @@ public class LiteDbContext : IDisposable
|
|||||||
|
|
||||||
private void MigrateExistingData()
|
private void MigrateExistingData()
|
||||||
{
|
{
|
||||||
|
MigrateMemberships();
|
||||||
|
RemoveRedundantLegacyFields();
|
||||||
|
|
||||||
var groups = _db.GetCollection<BsonDocument>("groups");
|
var groups = _db.GetCollection<BsonDocument>("groups");
|
||||||
|
var subjects = _db.GetCollection<Subject>("subjects");
|
||||||
var missingArchiveState = groups.FindAll()
|
var missingArchiveState = groups.FindAll()
|
||||||
.Where(g => !g.ContainsKey(nameof(LearningGroup.IsActive)))
|
.Where(g => !g.ContainsKey(nameof(LearningGroup.IsActive)))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -51,6 +55,83 @@ public class LiteDbContext : IDisposable
|
|||||||
group[nameof(LearningGroup.IsActive)] = true;
|
group[nameof(LearningGroup.IsActive)] = true;
|
||||||
groups.Update(group);
|
groups.Update(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subject war anfangs nur als Text in der Gruppe gespeichert. Alte
|
||||||
|
// Werte werden einmalig an die Fachstammdaten angebunden und danach
|
||||||
|
// entfernt, damit es nur noch eine führende Fachbezeichnung gibt.
|
||||||
|
foreach (var group in groups.FindAll().ToList())
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
var legacyName = group.TryGetValue("Subject", out var value) && value.IsString
|
||||||
|
? value.AsString.Trim()
|
||||||
|
: "";
|
||||||
|
var hasSubjectId = group.TryGetValue(nameof(LearningGroup.SubjectId), out var id)
|
||||||
|
&& id.IsGuid && subjects.FindById(id.AsGuid) is not null;
|
||||||
|
|
||||||
|
if (!hasSubjectId && legacyName.Length > 0)
|
||||||
|
{
|
||||||
|
var subject = subjects.FindAll().FirstOrDefault(s =>
|
||||||
|
string.Equals(s.Name.Trim(), legacyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (subject is null)
|
||||||
|
{
|
||||||
|
subject = new Subject { Name = legacyName };
|
||||||
|
subjects.Insert(subject);
|
||||||
|
}
|
||||||
|
group[nameof(LearningGroup.SubjectId)] = subject.Id;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
else if (!hasSubjectId && group.ContainsKey(nameof(LearningGroup.SubjectId)))
|
||||||
|
{
|
||||||
|
group[nameof(LearningGroup.SubjectId)] = BsonValue.Null;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
changed |= group.Remove("Subject");
|
||||||
|
if (changed) groups.Update(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MigrateMemberships()
|
||||||
|
{
|
||||||
|
const string oldName = "enrollments";
|
||||||
|
const string newName = "group_memberships";
|
||||||
|
var names = _db.GetCollectionNames().ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (names.Contains(oldName) && !names.Contains(newName))
|
||||||
|
_db.RenameCollection(oldName, newName);
|
||||||
|
|
||||||
|
var memberships = _db.GetCollection<BsonDocument>(newName);
|
||||||
|
foreach (var membership in memberships.FindAll().ToList())
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
if (!membership.ContainsKey(nameof(GroupMembership.AddedOn)) &&
|
||||||
|
membership.TryGetValue("EnrolledAt", out var enrolledAt))
|
||||||
|
{
|
||||||
|
membership[nameof(GroupMembership.AddedOn)] = enrolledAt;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
changed |= membership.Remove("EnrolledAt");
|
||||||
|
changed |= membership.Remove("SchoolYear");
|
||||||
|
if (changed) memberships.Update(membership);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveRedundantLegacyFields()
|
||||||
|
{
|
||||||
|
RemoveFields("exams", "Subject");
|
||||||
|
RemoveFields("units", "Subject", "SchoolYear");
|
||||||
|
RemoveFields("grades", "SchoolYear");
|
||||||
|
RemoveFields("participation", "GroupId", "Date");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveFields(string collectionName, params string[] fieldNames)
|
||||||
|
{
|
||||||
|
var collection = _db.GetCollection<BsonDocument>(collectionName);
|
||||||
|
foreach (var document in collection.FindAll().ToList())
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
foreach (var fieldName in fieldNames) changed |= document.Remove(fieldName);
|
||||||
|
if (changed) collection.Update(document);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureIndexes()
|
private void EnsureIndexes()
|
||||||
@@ -59,13 +140,16 @@ public class LiteDbContext : IDisposable
|
|||||||
Students.EnsureIndex(x => x.IsActive);
|
Students.EnsureIndex(x => x.IsActive);
|
||||||
Groups.EnsureIndex(x => x.SchoolYear);
|
Groups.EnsureIndex(x => x.SchoolYear);
|
||||||
Groups.EnsureIndex(x => x.IsActive);
|
Groups.EnsureIndex(x => x.IsActive);
|
||||||
Enrollments.EnsureIndex(x => x.StudentId);
|
Memberships.EnsureIndex(x => x.StudentId);
|
||||||
Enrollments.EnsureIndex(x => x.GroupId);
|
Memberships.EnsureIndex(x => x.GroupId);
|
||||||
Enrollments.EnsureIndex(x => x.SchoolYear);
|
Memberships.EnsureIndex("ux_student_group",
|
||||||
|
BsonExpression.Create("STRING($.StudentId) + ':' + STRING($.GroupId)"), unique: true);
|
||||||
Exams.EnsureIndex(x => x.GroupId);
|
Exams.EnsureIndex(x => x.GroupId);
|
||||||
Exams.EnsureIndex(x => x.Status);
|
Exams.EnsureIndex(x => x.Status);
|
||||||
ExamResults.EnsureIndex(x => x.ExamId);
|
ExamResults.EnsureIndex(x => x.ExamId);
|
||||||
ExamResults.EnsureIndex(x => x.StudentId);
|
ExamResults.EnsureIndex(x => x.StudentId);
|
||||||
|
ExamResults.EnsureIndex("ux_exam_student",
|
||||||
|
BsonExpression.Create("STRING($.ExamId) + ':' + STRING($.StudentId)"), unique: true);
|
||||||
Grades.EnsureIndex(x => x.StudentId);
|
Grades.EnsureIndex(x => x.StudentId);
|
||||||
Grades.EnsureIndex(x => x.GroupId);
|
Grades.EnsureIndex(x => x.GroupId);
|
||||||
GradingKeyTemplates.EnsureIndex(x => x.GradingSystem);
|
GradingKeyTemplates.EnsureIndex(x => x.GradingSystem);
|
||||||
@@ -80,8 +164,11 @@ public class LiteDbContext : IDisposable
|
|||||||
ParticipationSessions.EnsureIndex(x => x.Date);
|
ParticipationSessions.EnsureIndex(x => x.Date);
|
||||||
ParticipationEntries.EnsureIndex(x => x.SessionId);
|
ParticipationEntries.EnsureIndex(x => x.SessionId);
|
||||||
ParticipationEntries.EnsureIndex(x => x.StudentId);
|
ParticipationEntries.EnsureIndex(x => x.StudentId);
|
||||||
|
ParticipationEntries.EnsureIndex("ux_session_student",
|
||||||
|
BsonExpression.Create("STRING($.SessionId) + ':' + STRING($.StudentId)"), unique: true);
|
||||||
ParticipationAspects.EnsureIndex(x => x.GroupId);
|
ParticipationAspects.EnsureIndex(x => x.GroupId);
|
||||||
Subjects.EnsureIndex(x => x.Name);
|
Subjects.EnsureIndex(x => x.Name);
|
||||||
|
Subjects.EnsureIndex("ux_subject_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true);
|
||||||
CompetencyDomains.EnsureIndex(x => x.SubjectId);
|
CompetencyDomains.EnsureIndex(x => x.SubjectId);
|
||||||
CompetencyDomains.EnsureIndex(x => x.GradeLevel);
|
CompetencyDomains.EnsureIndex(x => x.GradeLevel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ public class StudentRepository(LiteDbContext db) : IStudentRepository
|
|||||||
public List<Student> GetAll(bool includeInactive = false) =>
|
public List<Student> GetAll(bool includeInactive = false) =>
|
||||||
(includeInactive ? db.Students.FindAll() : db.Students.Find(s => s.IsActive))
|
(includeInactive ? db.Students.FindAll() : db.Students.Find(s => s.IsActive))
|
||||||
.OrderBy(s => s.LastName).ToList();
|
.OrderBy(s => s.LastName).ToList();
|
||||||
public List<Student> GetByGroup(Guid groupId, string schoolYear)
|
public List<Student> GetByGroup(Guid groupId)
|
||||||
{
|
{
|
||||||
var ids = db.Enrollments
|
var ids = db.Memberships
|
||||||
.Find(e => e.GroupId == groupId && e.SchoolYear == schoolYear)
|
.Find(e => e.GroupId == groupId)
|
||||||
.Select(e => e.StudentId).ToHashSet();
|
.Select(e => e.StudentId).ToHashSet();
|
||||||
return db.Students.Find(s => ids.Contains(s.Id)).OrderBy(s => s.LastName).ToList();
|
return db.Students.Find(s => ids.Contains(s.Id)).OrderBy(s => s.LastName).ToList();
|
||||||
}
|
}
|
||||||
@@ -31,11 +31,17 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
|
|||||||
? db.Groups.Find(g => g.SchoolYear == schoolYear)
|
? db.Groups.Find(g => g.SchoolYear == schoolYear)
|
||||||
: db.Groups.Find(g => g.SchoolYear == schoolYear && g.IsActive))
|
: db.Groups.Find(g => g.SchoolYear == schoolYear && g.IsActive))
|
||||||
.OrderBy(g => g.Name).ToList();
|
.OrderBy(g => g.Name).ToList();
|
||||||
public void Save(LearningGroup g) { g.UpdatedAt = DateTime.UtcNow; db.Groups.Upsert(g); }
|
public void Save(LearningGroup g)
|
||||||
|
{
|
||||||
|
if (g.SubjectId is Guid subjectId && db.Subjects.FindById(subjectId) is null)
|
||||||
|
throw new InvalidOperationException("Das gewählte Fach existiert nicht mehr.");
|
||||||
|
g.UpdatedAt = DateTime.UtcNow;
|
||||||
|
db.Groups.Upsert(g);
|
||||||
|
}
|
||||||
public void Delete(Guid id)
|
public void Delete(Guid id)
|
||||||
{
|
{
|
||||||
foreach (var enrollment in db.Enrollments.Find(e => e.GroupId == id).ToList())
|
foreach (var membership in db.Memberships.Find(e => e.GroupId == id).ToList())
|
||||||
db.Enrollments.Delete(enrollment.Id);
|
db.Memberships.Delete(membership.Id);
|
||||||
|
|
||||||
foreach (var exam in db.Exams.Find(e => e.GroupId == id).ToList())
|
foreach (var exam in db.Exams.Find(e => e.GroupId == id).ToList())
|
||||||
{
|
{
|
||||||
@@ -71,16 +77,22 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EnrollmentRepository(LiteDbContext db) : IEnrollmentRepository
|
public class GroupMembershipRepository(LiteDbContext db) : IGroupMembershipRepository
|
||||||
{
|
{
|
||||||
public List<Enrollment> GetByStudent(Guid id) =>
|
public List<GroupMembership> GetByStudent(Guid id) =>
|
||||||
db.Enrollments.Find(e => e.StudentId == id).ToList();
|
db.Memberships.Find(e => e.StudentId == id).ToList();
|
||||||
public List<Enrollment> GetByGroup(Guid id) =>
|
public List<GroupMembership> GetByGroup(Guid id) =>
|
||||||
db.Enrollments.Find(e => e.GroupId == id).ToList();
|
db.Memberships.Find(e => e.GroupId == id).ToList();
|
||||||
public List<Enrollment> GetByGroupAndYear(Guid groupId, string sy) =>
|
public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
||||||
db.Enrollments.Find(e => e.GroupId == groupId && e.SchoolYear == sy).ToList();
|
db.Memberships.FindOne(e => e.StudentId == studentId && e.GroupId == groupId);
|
||||||
public void Save(Enrollment e) => db.Enrollments.Upsert(e);
|
public void Save(GroupMembership membership)
|
||||||
public void Delete(Guid id) => db.Enrollments.Delete(id);
|
{
|
||||||
|
var existing = GetByStudentAndGroup(membership.StudentId, membership.GroupId);
|
||||||
|
if (existing is not null && existing.Id != membership.Id)
|
||||||
|
throw new InvalidOperationException("Der Schüler ist dieser Lerngruppe bereits zugeordnet.");
|
||||||
|
db.Memberships.Upsert(membership);
|
||||||
|
}
|
||||||
|
public void Delete(Guid id) => db.Memberships.Delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ExamRepository(LiteDbContext db) : IExamRepository
|
public class ExamRepository(LiteDbContext db) : IExamRepository
|
||||||
@@ -240,10 +252,25 @@ public class SubjectRepository(LiteDbContext db) : ISubjectRepository
|
|||||||
{
|
{
|
||||||
public List<Subject> GetAll() => db.Subjects.FindAll().OrderBy(s => s.Name).ToList();
|
public List<Subject> GetAll() => db.Subjects.FindAll().OrderBy(s => s.Name).ToList();
|
||||||
public Subject? GetById(Guid id) => db.Subjects.FindById(id);
|
public Subject? GetById(Guid id) => db.Subjects.FindById(id);
|
||||||
public Subject? GetByName(string name) =>
|
public Subject? GetByName(string name) => db.Subjects.FindAll().FirstOrDefault(s =>
|
||||||
db.Subjects.FindOne(s => s.Name == name);
|
string.Equals(s.Name.Trim(), name.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||||
public void Save(Subject s) { s.UpdatedAt = DateTime.UtcNow; db.Subjects.Upsert(s); }
|
public void Save(Subject s)
|
||||||
public void Delete(Guid id) => db.Subjects.Delete(id);
|
{
|
||||||
|
s.Name = s.Name.Trim();
|
||||||
|
s.ShortName = s.ShortName.Trim();
|
||||||
|
if (s.Name.Length == 0) throw new ArgumentException("Der Fachname darf nicht leer sein.");
|
||||||
|
var duplicate = GetByName(s.Name);
|
||||||
|
if (duplicate is not null && duplicate.Id != s.Id)
|
||||||
|
throw new InvalidOperationException("Ein Fach mit diesem Namen existiert bereits.");
|
||||||
|
s.UpdatedAt = DateTime.UtcNow;
|
||||||
|
db.Subjects.Upsert(s);
|
||||||
|
}
|
||||||
|
public void Delete(Guid id)
|
||||||
|
{
|
||||||
|
if (db.Groups.Exists(g => g.SubjectId == id) || db.CompetencyDomains.Exists(d => d.SubjectId == id))
|
||||||
|
throw new InvalidOperationException("Das Fach wird noch von einer Lerngruppe oder einem Kompetenzkatalog verwendet.");
|
||||||
|
db.Subjects.Delete(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public static class AppBootstrapper
|
|||||||
// ── Repositories ──────────────────────────────────────────────────────
|
// ── Repositories ──────────────────────────────────────────────────────
|
||||||
services.AddSingleton<IStudentRepository, StudentRepository>();
|
services.AddSingleton<IStudentRepository, StudentRepository>();
|
||||||
services.AddSingleton<IGroupRepository, GroupRepository>();
|
services.AddSingleton<IGroupRepository, GroupRepository>();
|
||||||
services.AddSingleton<IEnrollmentRepository, EnrollmentRepository>();
|
services.AddSingleton<IGroupMembershipRepository, GroupMembershipRepository>();
|
||||||
services.AddSingleton<IExamRepository, ExamRepository>();
|
services.AddSingleton<IExamRepository, ExamRepository>();
|
||||||
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
|
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
|
||||||
services.AddSingleton<IGradeRepository, GradeRepository>();
|
services.AddSingleton<IGradeRepository, GradeRepository>();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
private static readonly CultureInfo De = new("de-DE");
|
private static readonly CultureInfo De = new("de-DE");
|
||||||
|
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly ILessonRepository _lessons;
|
private readonly ILessonRepository _lessons;
|
||||||
private readonly IExamRepository _exams;
|
private readonly IExamRepository _exams;
|
||||||
private readonly IWorkTaskRepository _tasks;
|
private readonly IWorkTaskRepository _tasks;
|
||||||
@@ -34,10 +35,10 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
||||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||||
|
|
||||||
public DashboardViewModel(IGroupRepository groups, ILessonRepository lessons,
|
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
||||||
IExamRepository exams, IWorkTaskRepository tasks, SchoolYearService sy)
|
IExamRepository exams, IWorkTaskRepository tasks, SchoolYearService sy)
|
||||||
{
|
{
|
||||||
_groups = groups; _lessons = lessons; _exams = exams; _tasks = tasks; _sy = sy;
|
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks; _sy = sy;
|
||||||
Load();
|
Load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +71,12 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
CurrentGroups.Clear();
|
CurrentGroups.Clear();
|
||||||
foreach (var g in groups.Values.OrderBy(g => g.Name))
|
foreach (var g in groups.Values.OrderBy(g => g.Name))
|
||||||
CurrentGroups.Add(new() { GroupId = g.Id, Name = g.Name, Subject = g.Subject ?? "" });
|
CurrentGroups.Add(new()
|
||||||
|
{
|
||||||
|
GroupId = g.Id,
|
||||||
|
Name = g.Name,
|
||||||
|
Subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "",
|
||||||
|
});
|
||||||
|
|
||||||
CalendarMonth = FirstOfMonth(now);
|
CalendarMonth = FirstOfMonth(now);
|
||||||
LoadCalendar();
|
LoadCalendar();
|
||||||
|
|||||||
@@ -24,24 +24,24 @@ public partial class ExamGradingDialogViewModel : ObservableObject
|
|||||||
public ObservableCollection<ExamResultRow> Rows { get; } = [];
|
public ObservableCollection<ExamResultRow> Rows { get; } = [];
|
||||||
|
|
||||||
public ExamGradingDialogViewModel(IExamResultRepository results, IStudentRepository students,
|
public ExamGradingDialogViewModel(IExamResultRepository results, IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, GradingService grading, Exam exam, Guid groupId, string schoolYear)
|
IGroupMembershipRepository memberships, GradingService grading, Exam exam, Guid groupId)
|
||||||
{
|
{
|
||||||
_results = results; _grading = grading; _exam = exam;
|
_results = results; _grading = grading; _exam = exam;
|
||||||
Tasks = exam.Tasks.OrderBy(t => t.Nr).ToList();
|
Tasks = exam.Tasks.OrderBy(t => t.Nr).ToList();
|
||||||
_examMaxPoints = Tasks.Sum(t => t.MaxPoints);
|
_examMaxPoints = Tasks.Sum(t => t.MaxPoints);
|
||||||
|
|
||||||
var enrolled = students.GetByGroup(groupId, schoolYear);
|
var enrolled = students.GetByGroup(groupId);
|
||||||
var enrollmentList = enrollments.GetByGroupAndYear(groupId, schoolYear);
|
var membershipList = memberships.GetByGroup(groupId);
|
||||||
var existing = results.GetByExam(exam.Id).ToDictionary(r => r.StudentId);
|
var existing = results.GetByExam(exam.Id).ToDictionary(r => r.StudentId);
|
||||||
|
|
||||||
foreach (var s in enrolled.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
foreach (var s in enrolled.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||||
{
|
{
|
||||||
var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == s.Id);
|
var membership = membershipList.FirstOrDefault(e => e.StudentId == s.Id);
|
||||||
if (enrollment is not null && !IsEnrolledAtDate(enrollment, exam.Date)) continue;
|
if (membership is not null && !IsMemberAtDate(membership, exam.Date)) continue;
|
||||||
|
|
||||||
// Niveau-Klausur: nur Schüler mit passendem Niveau zeigen. Klausuren ohne
|
// Niveau-Klausur: nur Schüler mit passendem Niveau zeigen. Klausuren ohne
|
||||||
// Niveau-Zuordnung gelten weiterhin für die ganze Gruppe.
|
// Niveau-Zuordnung gelten weiterhin für die ganze Gruppe.
|
||||||
if (exam.Niveau.HasValue && enrollment?.Niveau != exam.Niveau) continue;
|
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue;
|
||||||
|
|
||||||
existing.TryGetValue(s.Id, out var result);
|
existing.TryGetValue(s.Id, out var result);
|
||||||
var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading);
|
var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading);
|
||||||
@@ -52,12 +52,12 @@ public partial class ExamGradingDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
private void SaveRow(ExamResultRow row) => _results.Save(row.ToModel(_exam.Id));
|
private void SaveRow(ExamResultRow row) => _results.Save(row.ToModel(_exam.Id));
|
||||||
|
|
||||||
private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch
|
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
|
||||||
{
|
{
|
||||||
EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
||||||
EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
||||||
EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value)
|
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
|
||||||
&& (e.LeftAt is null || date <= e.LeftAt.Value),
|
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ public partial class ExamDialogViewModel : ObservableObject
|
|||||||
Result = _editingExam ?? new Exam { GroupId = _groupId };
|
Result = _editingExam ?? new Exam { GroupId = _groupId };
|
||||||
Result.Title = Title.Trim();
|
Result.Title = Title.Trim();
|
||||||
Result.Date = date;
|
Result.Date = date;
|
||||||
Result.Subject = _subjectName.Trim();
|
|
||||||
Result.ExamNumber = ExamNumber;
|
Result.ExamNumber = ExamNumber;
|
||||||
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
|
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
|
||||||
Result.ReturnedAt = returnedAt;
|
Result.ReturnedAt = returnedAt;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace LehrerApp.Desktop.ViewModels.Groups;
|
|||||||
public partial class GroupListViewModel : ObservableObject
|
public partial class GroupListViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly SchoolYearService _sy;
|
private readonly ISubjectRepository _subjects;
|
||||||
|
|
||||||
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
||||||
public Func<Task>? OnAddGroup { get; set; }
|
public Func<Task>? OnAddGroup { get; set; }
|
||||||
@@ -37,9 +37,9 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
public ObservableCollection<string> SchoolYears { get; } = [];
|
public ObservableCollection<string> SchoolYears { get; } = [];
|
||||||
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
||||||
|
|
||||||
public GroupListViewModel(IGroupRepository groups, SchoolYearService sy)
|
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy)
|
||||||
{
|
{
|
||||||
_groups = groups; _sy = sy;
|
_groups = groups; _subjects = subjects;
|
||||||
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
|
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
|
||||||
SelectedSchoolYear = sy.CurrentSchoolYear();
|
SelectedSchoolYear = sy.CurrentSchoolYear();
|
||||||
}
|
}
|
||||||
@@ -63,11 +63,13 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
Groups.Clear();
|
Groups.Clear();
|
||||||
var all = _groups.GetBySchoolYear(SelectedSchoolYear, includeInactive: ShowArchived)
|
var all = _groups.GetBySchoolYear(SelectedSchoolYear, includeInactive: ShowArchived)
|
||||||
.Where(g => g.IsActive != ShowArchived);
|
.Where(g => g.IsActive != ShowArchived);
|
||||||
|
var subjectNames = _subjects.GetAll().ToDictionary(s => s.Id, s => s.Name);
|
||||||
var filtered = string.IsNullOrWhiteSpace(SearchText) ? all
|
var filtered = string.IsNullOrWhiteSpace(SearchText) ? all
|
||||||
: all.Where(g => g.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|
: all.Where(g => g.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|
||||||
|| (g.Subject?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false));
|
|| (g.SubjectId is Guid id && subjectNames.GetValueOrDefault(id, "")
|
||||||
|
.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
|
||||||
foreach (var g in filtered.OrderBy(g => g.Name))
|
foreach (var g in filtered.OrderBy(g => g.Name))
|
||||||
Groups.Add(new GroupListItem(g));
|
Groups.Add(new GroupListItem(g, g.SubjectId is Guid id ? subjectNames.GetValueOrDefault(id, "") : ""));
|
||||||
SelectedGroup = Groups.FirstOrDefault(g => g.Id == selectedId);
|
SelectedGroup = Groups.FirstOrDefault(g => g.Id == selectedId);
|
||||||
OnPropertyChanged(nameof(ListSummary));
|
OnPropertyChanged(nameof(ListSummary));
|
||||||
OnPropertyChanged(nameof(HasNoGroups));
|
OnPropertyChanged(nameof(HasNoGroups));
|
||||||
@@ -136,15 +138,15 @@ public class GroupListItem
|
|||||||
public bool IsActive { get; }
|
public bool IsActive { get; }
|
||||||
public string ArchiveActionLabel => IsActive ? "Archivieren" : "Wieder aktivieren";
|
public string ArchiveActionLabel => IsActive ? "Archivieren" : "Wieder aktivieren";
|
||||||
|
|
||||||
public GroupListItem(LearningGroup g)
|
public GroupListItem(LearningGroup g, string subjectName)
|
||||||
{
|
{
|
||||||
Id = g.Id;
|
Id = g.Id;
|
||||||
IsActive = g.IsActive;
|
IsActive = g.IsActive;
|
||||||
Name = g.Name;
|
Name = g.Name;
|
||||||
Subject = g.Subject ?? "";
|
Subject = subjectName;
|
||||||
TypeLabel = g.Type == GroupType.Class ? "Klasse" : "Kurs";
|
TypeLabel = g.Type == GroupType.Class ? "Klasse" : "Kurs";
|
||||||
GradingLabel = g.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15";
|
GradingLabel = g.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15";
|
||||||
DisplayName = string.IsNullOrEmpty(g.Subject) ? g.Name : $"{g.Name} · {g.Subject}";
|
DisplayName = string.IsNullOrEmpty(subjectName) ? g.Name : $"{g.Name} · {subjectName}";
|
||||||
Subtitle = $"{TypeLabel} · Stufe {g.GradeLevel} · Noten {GradingLabel} · {g.SchoolYear}";
|
Subtitle = $"{TypeLabel} · Stufe {g.GradeLevel} · Noten {GradingLabel} · {g.SchoolYear}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +157,8 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IEnrollmentRepository _enrollments;
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly IExamRepository _exams;
|
private readonly IExamRepository _exams;
|
||||||
private readonly IGradeRepository _grades;
|
private readonly IGradeRepository _grades;
|
||||||
|
|
||||||
@@ -171,6 +174,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
|
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
|
||||||
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
|
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
|
||||||
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
|
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
|
||||||
|
public string SubjectName { get; private set; } = "";
|
||||||
|
|
||||||
partial void OnGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(IsDifferentiated));
|
partial void OnGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(IsDifferentiated));
|
||||||
|
|
||||||
@@ -187,10 +191,11 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public Func<Exam, Task>? OnEvaluateExam { get; set; }
|
public Func<Exam, Task>? OnEvaluateExam { get; set; }
|
||||||
|
|
||||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
|
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||||
|
IExamRepository exams, IGradeRepository grades,
|
||||||
ParticipationTabViewModel participationTab)
|
ParticipationTabViewModel participationTab)
|
||||||
{
|
{
|
||||||
_groups = groups; _students = students; _enrollments = enrollments;
|
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||||
_exams = exams; _grades = grades;
|
_exams = exams; _grades = grades;
|
||||||
ParticipationTab = participationTab;
|
ParticipationTab = participationTab;
|
||||||
}
|
}
|
||||||
@@ -199,6 +204,9 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
Group = _groups.GetById(id);
|
Group = _groups.GetById(id);
|
||||||
if (Group is null) return;
|
if (Group is null) return;
|
||||||
|
SubjectName = Group.SubjectId is Guid subjectId
|
||||||
|
? _subjects.GetById(subjectId)?.Name ?? ""
|
||||||
|
: "";
|
||||||
GroupTitle = Group.Name;
|
GroupTitle = Group.Name;
|
||||||
GroupSubtitle = $"{Group.SchoolYear} · " +
|
GroupSubtitle = $"{Group.SchoolYear} · " +
|
||||||
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
|
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
|
||||||
@@ -221,14 +229,13 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (Group is null) return;
|
if (Group is null) return;
|
||||||
Students.Clear();
|
Students.Clear();
|
||||||
var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear);
|
var enrolled = _students.GetByGroup(Group.Id);
|
||||||
var enrollments = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
|
var memberships = _memberships.GetByGroup(Group.Id).ToDictionary(e => e.StudentId);
|
||||||
.ToDictionary(e => e.StudentId);
|
|
||||||
StudentCount = enrolled.Count;
|
StudentCount = enrolled.Count;
|
||||||
foreach (var s in enrolled)
|
foreach (var s in enrolled)
|
||||||
{
|
{
|
||||||
enrollments.TryGetValue(s.Id, out var enr);
|
memberships.TryGetValue(s.Id, out var membership);
|
||||||
var summary = new StudentSummary(s, enr) { OnChanged = SaveStudentNiveau };
|
var summary = new StudentSummary(s, membership) { OnChanged = SaveStudentNiveau };
|
||||||
Students.Add(summary);
|
Students.Add(summary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,11 +243,10 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
private void SaveStudentNiveau(StudentSummary summary)
|
private void SaveStudentNiveau(StudentSummary summary)
|
||||||
{
|
{
|
||||||
if (Group is null) return;
|
if (Group is null) return;
|
||||||
var enrollment = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
|
var membership = _memberships.GetByStudentAndGroup(summary.Id, Group.Id);
|
||||||
.FirstOrDefault(e => e.StudentId == summary.Id);
|
if (membership is null) return;
|
||||||
if (enrollment is null) return;
|
membership.Niveau = summary.Niveau;
|
||||||
enrollment.Niveau = summary.Niveau;
|
_memberships.Save(membership);
|
||||||
_enrollments.Save(enrollment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -259,10 +265,9 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
private void RemoveStudent()
|
private void RemoveStudent()
|
||||||
{
|
{
|
||||||
if (Group is null || SelectedStudent is null) return;
|
if (Group is null || SelectedStudent is null) return;
|
||||||
var enrollment = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
|
var membership = _memberships.GetByStudentAndGroup(SelectedStudent.Id, Group.Id);
|
||||||
.FirstOrDefault(e => e.StudentId == SelectedStudent.Id);
|
if (membership is null) return;
|
||||||
if (enrollment is null) return;
|
_memberships.Delete(membership.Id);
|
||||||
_enrollments.Delete(enrollment.Id);
|
|
||||||
LoadStudents();
|
LoadStudents();
|
||||||
SelectedStudent = null;
|
SelectedStudent = null;
|
||||||
ParticipationTab.RefreshCurrentGrid();
|
ParticipationTab.RefreshCurrentGrid();
|
||||||
@@ -422,18 +427,18 @@ public partial class StudentSummary : ObservableObject
|
|||||||
|
|
||||||
public Action<StudentSummary>? OnChanged { get; set; }
|
public Action<StudentSummary>? OnChanged { get; set; }
|
||||||
|
|
||||||
public StudentSummary(Core.Models.Student s, Enrollment? e)
|
public StudentSummary(Core.Models.Student s, GroupMembership? membership)
|
||||||
{
|
{
|
||||||
Id = s.Id;
|
Id = s.Id;
|
||||||
FullName = s.FullName;
|
FullName = s.FullName;
|
||||||
PeriodLabel = e?.Period switch
|
PeriodLabel = membership?.Period switch
|
||||||
{
|
{
|
||||||
EnrollmentPeriod.H1Only => "H1",
|
MembershipPeriod.H1Only => "H1",
|
||||||
EnrollmentPeriod.H2Only => "H2",
|
MembershipPeriod.H2Only => "H2",
|
||||||
EnrollmentPeriod.Custom => BuildCustomLabel(e),
|
MembershipPeriod.Custom => BuildCustomLabel(membership),
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
_niveau = e?.Niveau;
|
_niveau = membership?.Niveau;
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnNiveauChanged(Niveau? value)
|
partial void OnNiveauChanged(Niveau? value)
|
||||||
@@ -442,11 +447,11 @@ public partial class StudentSummary : ObservableObject
|
|||||||
OnChanged?.Invoke(this);
|
OnChanged?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildCustomLabel(Enrollment e)
|
private static string BuildCustomLabel(GroupMembership membership)
|
||||||
{
|
{
|
||||||
if (e.JoinedAt.HasValue && e.LeftAt.HasValue) return $"{e.JoinedAt:dd.MM.}–{e.LeftAt:dd.MM.}";
|
if (membership.JoinedAt.HasValue && membership.LeftAt.HasValue) return $"{membership.JoinedAt:dd.MM.}–{membership.LeftAt:dd.MM.}";
|
||||||
if (e.JoinedAt.HasValue) return $"ab {e.JoinedAt:dd.MM.}";
|
if (membership.JoinedAt.HasValue) return $"ab {membership.JoinedAt:dd.MM.}";
|
||||||
if (e.LeftAt.HasValue) return $"bis {e.LeftAt:dd.MM.}";
|
if (membership.LeftAt.HasValue) return $"bis {membership.LeftAt:dd.MM.}";
|
||||||
return "Datum";
|
return "Datum";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,35 +496,34 @@ public class ExamSummary
|
|||||||
public partial class AddStudentToGroupDialogViewModel : ObservableObject
|
public partial class AddStudentToGroupDialogViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IEnrollmentRepository _enrollments;
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
private readonly Guid _groupId;
|
private readonly Guid _groupId;
|
||||||
private readonly string _schoolYear;
|
|
||||||
|
|
||||||
[ObservableProperty] private string _searchText = "";
|
[ObservableProperty] private string _searchText = "";
|
||||||
[ObservableProperty] private StudentPickerItem? _selectedStudent;
|
[ObservableProperty] private StudentPickerItem? _selectedStudent;
|
||||||
[ObservableProperty] private string _validationMessage = "";
|
[ObservableProperty] private string _validationMessage = "";
|
||||||
[ObservableProperty] private EnrollmentPeriod _period = EnrollmentPeriod.FullYear;
|
[ObservableProperty] private MembershipPeriod _period = MembershipPeriod.FullYear;
|
||||||
[ObservableProperty] private string _joinedAtText = "";
|
[ObservableProperty] private string _joinedAtText = "";
|
||||||
[ObservableProperty] private string _leftAtText = "";
|
[ObservableProperty] private string _leftAtText = "";
|
||||||
|
|
||||||
public bool IsFullYear { get => Period == EnrollmentPeriod.FullYear; set { if (value) Period = EnrollmentPeriod.FullYear; } }
|
public bool IsFullYear { get => Period == MembershipPeriod.FullYear; set { if (value) Period = MembershipPeriod.FullYear; } }
|
||||||
public bool IsH1Only { get => Period == EnrollmentPeriod.H1Only; set { if (value) Period = EnrollmentPeriod.H1Only; } }
|
public bool IsH1Only { get => Period == MembershipPeriod.H1Only; set { if (value) Period = MembershipPeriod.H1Only; } }
|
||||||
public bool IsH2Only { get => Period == EnrollmentPeriod.H2Only; set { if (value) Period = EnrollmentPeriod.H2Only; } }
|
public bool IsH2Only { get => Period == MembershipPeriod.H2Only; set { if (value) Period = MembershipPeriod.H2Only; } }
|
||||||
public bool IsCustom { get => Period == EnrollmentPeriod.Custom; set { if (value) Period = EnrollmentPeriod.Custom; } }
|
public bool IsCustom { get => Period == MembershipPeriod.Custom; set { if (value) Period = MembershipPeriod.Custom; } }
|
||||||
public bool IsCustomPeriod => Period == EnrollmentPeriod.Custom;
|
public bool IsCustomPeriod => Period == MembershipPeriod.Custom;
|
||||||
|
|
||||||
public ObservableCollection<StudentPickerItem> AvailableStudents { get; } = [];
|
public ObservableCollection<StudentPickerItem> AvailableStudents { get; } = [];
|
||||||
public Enrollment? Result { get; private set; }
|
public GroupMembership? Result { get; private set; }
|
||||||
|
|
||||||
public AddStudentToGroupDialogViewModel(IStudentRepository students,
|
public AddStudentToGroupDialogViewModel(IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, Guid groupId, string schoolYear)
|
IGroupMembershipRepository memberships, Guid groupId)
|
||||||
{
|
{
|
||||||
_students = students; _enrollments = enrollments;
|
_students = students; _memberships = memberships;
|
||||||
_groupId = groupId; _schoolYear = schoolYear;
|
_groupId = groupId;
|
||||||
LoadAvailableStudents();
|
LoadAvailableStudents();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnPeriodChanged(EnrollmentPeriod value)
|
partial void OnPeriodChanged(MembershipPeriod value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsFullYear));
|
OnPropertyChanged(nameof(IsFullYear));
|
||||||
OnPropertyChanged(nameof(IsH1Only));
|
OnPropertyChanged(nameof(IsH1Only));
|
||||||
@@ -532,11 +536,11 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
private void LoadAvailableStudents()
|
private void LoadAvailableStudents()
|
||||||
{
|
{
|
||||||
var alreadyEnrolled = _enrollments.GetByGroupAndYear(_groupId, _schoolYear)
|
var alreadyAssigned = _memberships.GetByGroup(_groupId)
|
||||||
.Select(e => e.StudentId).ToHashSet();
|
.Select(e => e.StudentId).ToHashSet();
|
||||||
var all = _students.GetAll();
|
var all = _students.GetAll();
|
||||||
var available = all
|
var available = all
|
||||||
.Where(s => !alreadyEnrolled.Contains(s.Id))
|
.Where(s => !alreadyAssigned.Contains(s.Id))
|
||||||
.Where(s => string.IsNullOrWhiteSpace(SearchText) ||
|
.Where(s => string.IsNullOrWhiteSpace(SearchText) ||
|
||||||
s.FullName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
|
s.FullName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
|
||||||
AvailableStudents.Clear();
|
AvailableStudents.Clear();
|
||||||
@@ -549,7 +553,7 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject
|
|||||||
if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; }
|
if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; }
|
||||||
|
|
||||||
DateOnly? joinedAt = null, leftAt = null;
|
DateOnly? joinedAt = null, leftAt = null;
|
||||||
if (Period == EnrollmentPeriod.Custom)
|
if (Period == MembershipPeriod.Custom)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(JoinedAtText) &&
|
if (!string.IsNullOrWhiteSpace(JoinedAtText) &&
|
||||||
DateOnly.TryParseExact(JoinedAtText, "dd.MM.yyyy",
|
DateOnly.TryParseExact(JoinedAtText, "dd.MM.yyyy",
|
||||||
@@ -561,16 +565,15 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject
|
|||||||
leftAt = l;
|
leftAt = l;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result = new Enrollment
|
Result = new GroupMembership
|
||||||
{
|
{
|
||||||
StudentId = SelectedStudent.Id,
|
StudentId = SelectedStudent.Id,
|
||||||
GroupId = _groupId,
|
GroupId = _groupId,
|
||||||
SchoolYear = _schoolYear,
|
|
||||||
Period = Period,
|
Period = Period,
|
||||||
JoinedAt = joinedAt,
|
JoinedAt = joinedAt,
|
||||||
LeftAt = leftAt,
|
LeftAt = leftAt,
|
||||||
};
|
};
|
||||||
_enrollments.Save(Result);
|
_memberships.Save(Result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,11 +590,8 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly ISubjectRepository _subjects;
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly SchoolYearService _sy;
|
|
||||||
private readonly IEnrollmentRepository _enrollments;
|
|
||||||
private List<Subject> _allSubjects = [];
|
private List<Subject> _allSubjects = [];
|
||||||
private LearningGroup? _editingGroup;
|
private LearningGroup? _editingGroup;
|
||||||
private string? _originalSchoolYear;
|
|
||||||
|
|
||||||
public List<string> TypeOptions { get; } = ["Klasse", "Kurs"];
|
public List<string> TypeOptions { get; } = ["Klasse", "Kurs"];
|
||||||
[ObservableProperty] private string _selectedTypeName = "Kurs";
|
[ObservableProperty] private string _selectedTypeName = "Kurs";
|
||||||
@@ -625,9 +625,9 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
public string SaveButtonText => _editingGroup is null ? "Anlegen" : "Speichern";
|
public string SaveButtonText => _editingGroup is null ? "Anlegen" : "Speichern";
|
||||||
|
|
||||||
public AddGroupDialogViewModel(IGroupRepository groups, ISubjectRepository subjects,
|
public AddGroupDialogViewModel(IGroupRepository groups, ISubjectRepository subjects,
|
||||||
SchoolYearService sy, IEnrollmentRepository enrollments)
|
SchoolYearService sy)
|
||||||
{
|
{
|
||||||
_groups = groups; _subjects = subjects; _sy = sy; _enrollments = enrollments;
|
_groups = groups; _subjects = subjects;
|
||||||
_allSubjects = subjects.GetAll();
|
_allSubjects = subjects.GetAll();
|
||||||
KnownSubjectNames = _allSubjects.Select(s => s.Name).ToList();
|
KnownSubjectNames = _allSubjects.Select(s => s.Name).ToList();
|
||||||
SchoolYears = sy.RecentSchoolYears(3);
|
SchoolYears = sy.RecentSchoolYears(3);
|
||||||
@@ -642,10 +642,9 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
public void LoadForEdit(LearningGroup group)
|
public void LoadForEdit(LearningGroup group)
|
||||||
{
|
{
|
||||||
_editingGroup = group;
|
_editingGroup = group;
|
||||||
_originalSchoolYear = group.SchoolYear;
|
|
||||||
SelectedTypeName = group.Type == GroupType.Class ? "Klasse" : "Kurs";
|
SelectedTypeName = group.Type == GroupType.Class ? "Klasse" : "Kurs";
|
||||||
Name = group.Name;
|
Name = group.Name;
|
||||||
Subject = group.Subject ?? "";
|
Subject = group.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
|
||||||
GradeLevel = group.GradeLevel;
|
GradeLevel = group.GradeLevel;
|
||||||
SelectedGradingName = group.GradingSystem == GradingSystem.Grades1To6
|
SelectedGradingName = group.GradingSystem == GradingSystem.Grades1To6
|
||||||
? "Noten 1–6" : "Punkte 0–15";
|
? "Noten 1–6" : "Punkte 0–15";
|
||||||
@@ -684,7 +683,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
Result = _editingGroup ?? new LearningGroup();
|
Result = _editingGroup ?? new LearningGroup();
|
||||||
Result.Name = Name.Trim();
|
Result.Name = Name.Trim();
|
||||||
Result.Subject = subjectName;
|
|
||||||
Result.SubjectId = subjectId;
|
Result.SubjectId = subjectId;
|
||||||
Result.Type = IsKurs ? GroupType.Course : GroupType.Class;
|
Result.Type = IsKurs ? GroupType.Course : GroupType.Class;
|
||||||
Result.GradeLevel = GradeLevel;
|
Result.GradeLevel = GradeLevel;
|
||||||
@@ -695,14 +693,5 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
Result.IsOwnClass = IsOwnClass;
|
Result.IsOwnClass = IsOwnClass;
|
||||||
Result.IsDifferentiated = IsDifferentiated;
|
Result.IsDifferentiated = IsDifferentiated;
|
||||||
_groups.Save(Result);
|
_groups.Save(Result);
|
||||||
|
|
||||||
if (_editingGroup is not null && _originalSchoolYear != SelectedSchoolYear)
|
|
||||||
{
|
|
||||||
foreach (var enrollment in _enrollments.GetByGroup(Result.Id))
|
|
||||||
{
|
|
||||||
enrollment.SchoolYear = SelectedSchoolYear;
|
|
||||||
_enrollments.Save(enrollment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
|||||||
private readonly IParticipationRepository _entries;
|
private readonly IParticipationRepository _entries;
|
||||||
private readonly IParticipationAspectRepository _aspects;
|
private readonly IParticipationAspectRepository _aspects;
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IEnrollmentRepository _enrollments;
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
private readonly IGradeRepository _grades;
|
private readonly IGradeRepository _grades;
|
||||||
private readonly GradingService _grading;
|
private readonly GradingService _grading;
|
||||||
private readonly Guid _groupId;
|
private readonly Guid _groupId;
|
||||||
@@ -39,11 +39,11 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
|||||||
public ParticipationGradeDialogViewModel(
|
public ParticipationGradeDialogViewModel(
|
||||||
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
||||||
IParticipationAspectRepository aspects, IStudentRepository students,
|
IParticipationAspectRepository aspects, IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, IGradeRepository grades, GradingService grading,
|
IGroupMembershipRepository memberships, IGradeRepository grades, GradingService grading,
|
||||||
Guid groupId, string schoolYear, GradingSystem gradingSystem)
|
Guid groupId, string schoolYear, GradingSystem gradingSystem)
|
||||||
{
|
{
|
||||||
_sessions = sessions; _entries = entries; _aspects = aspects;
|
_sessions = sessions; _entries = entries; _aspects = aspects;
|
||||||
_students = students; _enrollments = enrollments; _grades = grades;
|
_students = students; _memberships = memberships; _grades = grades;
|
||||||
_grading = grading; _groupId = groupId; _schoolYear = schoolYear;
|
_grading = grading; _groupId = groupId; _schoolYear = schoolYear;
|
||||||
_gradingSystem = gradingSystem;
|
_gradingSystem = gradingSystem;
|
||||||
|
|
||||||
@@ -83,14 +83,14 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
|||||||
.OrderBy(s => s.Date)
|
.OrderBy(s => s.Date)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var studentList = _students.GetByGroup(_groupId, _schoolYear);
|
var studentList = _students.GetByGroup(_groupId);
|
||||||
var enrollmentList = _enrollments.GetByGroupAndYear(_groupId, _schoolYear);
|
var membershipList = _memberships.GetByGroup(_groupId);
|
||||||
|
|
||||||
foreach (var student in studentList.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
foreach (var student in studentList.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||||
{
|
{
|
||||||
var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == student.Id);
|
var membership = membershipList.FirstOrDefault(e => e.StudentId == student.Id);
|
||||||
var relevantSessions = sessions
|
var relevantSessions = sessions
|
||||||
.Where(s => enrollment is null || IsEnrolledAtDate(enrollment, s.Date))
|
.Where(s => membership is null || IsMemberAtDate(membership, s.Date))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var points = new List<(DateOnly Date, double Rating)>();
|
var points = new List<(DateOnly Date, double Rating)>();
|
||||||
@@ -132,7 +132,6 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
StudentId = row.StudentId,
|
StudentId = row.StudentId,
|
||||||
GroupId = _groupId,
|
GroupId = _groupId,
|
||||||
SchoolYear = _schoolYear,
|
|
||||||
Category = GradeCategory.Participation,
|
Category = GradeCategory.Participation,
|
||||||
Note = noteTag,
|
Note = noteTag,
|
||||||
};
|
};
|
||||||
@@ -153,12 +152,12 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
|||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch
|
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
|
||||||
{
|
{
|
||||||
EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
||||||
EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
||||||
EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value)
|
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
|
||||||
&& (e.LeftAt is null || date <= e.LeftAt.Value),
|
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
private readonly IParticipationRepository _entries;
|
private readonly IParticipationRepository _entries;
|
||||||
private readonly IParticipationAspectRepository _aspects;
|
private readonly IParticipationAspectRepository _aspects;
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IEnrollmentRepository _enrollments;
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly ICompetencyDomainRepository _competencyDomains;
|
private readonly ICompetencyDomainRepository _competencyDomains;
|
||||||
|
|
||||||
@@ -53,13 +53,13 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
IParticipationRepository entries,
|
IParticipationRepository entries,
|
||||||
IParticipationAspectRepository aspects,
|
IParticipationAspectRepository aspects,
|
||||||
IStudentRepository students,
|
IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments,
|
IGroupMembershipRepository memberships,
|
||||||
IGroupRepository groups,
|
IGroupRepository groups,
|
||||||
ICompetencyDomainRepository competencyDomains)
|
ICompetencyDomainRepository competencyDomains)
|
||||||
{
|
{
|
||||||
_sessions = sessions; _entries = entries;
|
_sessions = sessions; _entries = entries;
|
||||||
_aspects = aspects; _students = students;
|
_aspects = aspects; _students = students;
|
||||||
_enrollments = enrollments; _groups = groups;
|
_memberships = memberships; _groups = groups;
|
||||||
_competencyDomains = competencyDomains;
|
_competencyDomains = competencyDomains;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,18 +128,18 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
StudentRows.Clear();
|
StudentRows.Clear();
|
||||||
var session = _sessions.GetById(sessionId);
|
var session = _sessions.GetById(sessionId);
|
||||||
var sessionDate = session?.Date ?? DateOnly.FromDateTime(DateTime.Today);
|
var sessionDate = session?.Date ?? DateOnly.FromDateTime(DateTime.Today);
|
||||||
var students = _students.GetByGroup(_groupId, _schoolYear);
|
var students = _students.GetByGroup(_groupId);
|
||||||
var enrollments = _enrollments.GetByGroupAndYear(_groupId, _schoolYear);
|
var memberships = _memberships.GetByGroup(_groupId);
|
||||||
var entries = _entries.GetBySession(sessionId);
|
var entries = _entries.GetBySession(sessionId);
|
||||||
|
|
||||||
foreach (var s in students)
|
foreach (var s in students)
|
||||||
{
|
{
|
||||||
var enrollment = enrollments.FirstOrDefault(e => e.StudentId == s.Id);
|
var membership = memberships.FirstOrDefault(e => e.StudentId == s.Id);
|
||||||
if (enrollment is not null && !IsEnrolledAtDate(enrollment, sessionDate))
|
if (membership is not null && !IsMemberAtDate(membership, sessionDate))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var entry = entries.FirstOrDefault(e => e.StudentId == s.Id)
|
var entry = entries.FirstOrDefault(e => e.StudentId == s.Id)
|
||||||
?? new ParticipationEntry { SessionId = sessionId, GroupId = _groupId, StudentId = s.Id };
|
?? new ParticipationEntry { SessionId = sessionId, StudentId = s.Id };
|
||||||
var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList(), ActiveCompetencyCodes);
|
var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList(), ActiveCompetencyCodes);
|
||||||
row.OnRatingChanged = (sid, key, val) => SaveRating(sessionId, sid, key, val);
|
row.OnRatingChanged = (sid, key, val) => SaveRating(sessionId, sid, key, val);
|
||||||
row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val);
|
row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val);
|
||||||
@@ -149,25 +149,22 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
RebuildColumnsSignal++;
|
RebuildColumnsSignal++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch
|
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
|
||||||
{
|
{
|
||||||
EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
||||||
EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
||||||
EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value)
|
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
|
||||||
&& (e.LeftAt is null || date <= e.LeftAt.Value),
|
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
private void SaveRating(Guid sessionId, Guid studentId, string key, int? value)
|
private void SaveRating(Guid sessionId, Guid studentId, string key, int? value)
|
||||||
{
|
{
|
||||||
var session = _sessions.GetById(sessionId);
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
|
||||||
?? new ParticipationEntry
|
?? new ParticipationEntry
|
||||||
{
|
{
|
||||||
SessionId = sessionId,
|
SessionId = sessionId,
|
||||||
GroupId = _groupId,
|
|
||||||
StudentId = studentId,
|
StudentId = studentId,
|
||||||
Date = session?.Date ?? DateOnly.FromDateTime(DateTime.Today),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var existing = entry.Ratings.FirstOrDefault(r => r.Key == key);
|
var existing = entry.Ratings.FirstOrDefault(r => r.Key == key);
|
||||||
@@ -280,14 +277,11 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
|
|
||||||
private void SaveCompetencyRating(Guid sessionId, Guid studentId, string code, int? value)
|
private void SaveCompetencyRating(Guid sessionId, Guid studentId, string code, int? value)
|
||||||
{
|
{
|
||||||
var session = _sessions.GetById(sessionId);
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
|
||||||
?? new ParticipationEntry
|
?? new ParticipationEntry
|
||||||
{
|
{
|
||||||
SessionId = sessionId,
|
SessionId = sessionId,
|
||||||
GroupId = _groupId,
|
|
||||||
StudentId = studentId,
|
StudentId = studentId,
|
||||||
Date = session?.Date ?? DateOnly.FromDateTime(DateTime.Today),
|
|
||||||
};
|
};
|
||||||
var existing = entry.CompetencyRatings.FirstOrDefault(r => r.Code == code);
|
var existing = entry.CompetencyRatings.FirstOrDefault(r => r.Code == code);
|
||||||
if (value is null)
|
if (value is null)
|
||||||
|
|||||||
@@ -110,7 +110,15 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
private void AddSubject()
|
private void AddSubject()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(NewName)) { ValidationMessage = "Name erforderlich."; return; }
|
if (string.IsNullOrWhiteSpace(NewName)) { ValidationMessage = "Name erforderlich."; return; }
|
||||||
_subjects.Save(new Subject { Name = NewName.Trim(), ShortName = NewShort.Trim() });
|
try
|
||||||
|
{
|
||||||
|
_subjects.Save(new Subject { Name = NewName.Trim(), ShortName = NewShort.Trim() });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
ValidationMessage = ex.Message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
NewName = ""; NewShort = ""; ValidationMessage = "";
|
NewName = ""; NewShort = ""; ValidationMessage = "";
|
||||||
LoadSubjects();
|
LoadSubjects();
|
||||||
}
|
}
|
||||||
@@ -119,7 +127,16 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
private void DeleteSubject(SubjectListItem? item)
|
private void DeleteSubject(SubjectListItem? item)
|
||||||
{
|
{
|
||||||
if (item is null) return;
|
if (item is null) return;
|
||||||
_subjects.Delete(item.Id);
|
try
|
||||||
|
{
|
||||||
|
_subjects.Delete(item.Id);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
ValidationMessage = ex.Message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ValidationMessage = "";
|
||||||
if (CatalogSubject?.Id == item.Id) CatalogSubject = null;
|
if (CatalogSubject?.Id == item.Id) CatalogSubject = null;
|
||||||
LoadSubjects();
|
LoadSubjects();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ public class StudentListItem
|
|||||||
public partial class StudentDetailViewModel : ObservableObject
|
public partial class StudentDetailViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IEnrollmentRepository _enrollments;
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly IDocumentationRepository _docs;
|
private readonly IDocumentationRepository _docs;
|
||||||
|
|
||||||
[ObservableProperty] private Student? _student;
|
[ObservableProperty] private Student? _student;
|
||||||
@@ -79,7 +80,7 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _editLastName = "";
|
[ObservableProperty] private string _editLastName = "";
|
||||||
[ObservableProperty] private ContactItem? _selectedContact;
|
[ObservableProperty] private ContactItem? _selectedContact;
|
||||||
|
|
||||||
public ObservableCollection<EnrollmentEntry> Enrollments { get; } = [];
|
public ObservableCollection<GroupMembershipEntry> GroupMemberships { get; } = [];
|
||||||
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
||||||
public ObservableCollection<ContactItem> Contacts { get; } = [];
|
public ObservableCollection<ContactItem> Contacts { get; } = [];
|
||||||
public bool HasNoContacts => Contacts.Count == 0;
|
public bool HasNoContacts => Contacts.Count == 0;
|
||||||
@@ -87,11 +88,11 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
public Action<ContactItem>? OnViewAddress { get; set; }
|
public Action<ContactItem>? OnViewAddress { get; set; }
|
||||||
|
|
||||||
public StudentDetailViewModel(IStudentRepository students,
|
public StudentDetailViewModel(IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, IGroupRepository groups,
|
IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects,
|
||||||
IDocumentationRepository docs)
|
IDocumentationRepository docs)
|
||||||
{
|
{
|
||||||
_students = students; _enrollments = enrollments;
|
_students = students; _memberships = memberships;
|
||||||
_groups = groups; _docs = docs;
|
_groups = groups; _subjects = subjects; _docs = docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadStudent(Guid id)
|
public void LoadStudent(Guid id)
|
||||||
@@ -102,12 +103,13 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
EditFirstName = Student.FirstName;
|
EditFirstName = Student.FirstName;
|
||||||
EditLastName = Student.LastName;
|
EditLastName = Student.LastName;
|
||||||
|
|
||||||
Enrollments.Clear();
|
GroupMemberships.Clear();
|
||||||
foreach (var e in _enrollments.GetByStudent(Student.Id))
|
foreach (var membership in _memberships.GetByStudent(Student.Id))
|
||||||
{
|
{
|
||||||
var g = _groups.GetById(e.GroupId);
|
var g = _groups.GetById(membership.GroupId);
|
||||||
if (g is null) continue;
|
if (g is null) continue;
|
||||||
Enrollments.Add(new() { SchoolYear = e.SchoolYear, GroupName = g.Name, Subject = g.Subject ?? "" });
|
var subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
|
||||||
|
GroupMemberships.Add(new() { SchoolYear = g.SchoolYear, GroupName = g.Name, Subject = subject });
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadContacts();
|
LoadContacts();
|
||||||
@@ -195,7 +197,7 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
private bool CanViewSelectedAddress() => SelectedContact?.HasAddress == true;
|
private bool CanViewSelectedAddress() => SelectedContact?.HasAddress == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EnrollmentEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
|
public class GroupMembershipEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
|
||||||
public class DocEntry { public string Date { get; set; } = ""; public string Title { get; set; } = ""; public string TypeLabel { get; set; } = ""; public bool IsConfidential { get; set; } }
|
public class DocEntry { public string Date { get; set; } = ""; public string Title { get; set; } = ""; public string TypeLabel { get; set; } = ""; public bool IsConfidential { get; set; } }
|
||||||
|
|
||||||
public class ContactItem
|
public class ContactItem
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ public partial class GroupDetailView : UserControl
|
|||||||
|
|
||||||
var dialogVm = new AddStudentToGroupDialogViewModel(
|
var dialogVm = new AddStudentToGroupDialogViewModel(
|
||||||
App.Services.GetRequiredService<IStudentRepository>(),
|
App.Services.GetRequiredService<IStudentRepository>(),
|
||||||
App.Services.GetRequiredService<IEnrollmentRepository>(),
|
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||||
vm.Group.Id,
|
vm.Group.Id);
|
||||||
vm.Group.SchoolYear);
|
|
||||||
|
|
||||||
var dialog = new AddStudentToGroupDialog { DataContext = dialogVm };
|
var dialog = new AddStudentToGroupDialog { DataContext = dialogVm };
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
@@ -62,7 +61,7 @@ public partial class GroupDetailView : UserControl
|
|||||||
App.Services.GetRequiredService<IGradingKeyTemplateRepository>(),
|
App.Services.GetRequiredService<IGradingKeyTemplateRepository>(),
|
||||||
App.Services.GetRequiredService<GradingService>(),
|
App.Services.GetRequiredService<GradingService>(),
|
||||||
groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem,
|
groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem,
|
||||||
vm.Group.Subject ?? "", vm.Group.IsDifferentiated, editingExam, duplicateSource);
|
vm.SubjectName, vm.Group.IsDifferentiated, editingExam, duplicateSource);
|
||||||
|
|
||||||
var dialog = new ExamDialog { DataContext = dialogVm };
|
var dialog = new ExamDialog { DataContext = dialogVm };
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
@@ -85,9 +84,9 @@ public partial class GroupDetailView : UserControl
|
|||||||
var dialogVm = new ExamGradingDialogViewModel(
|
var dialogVm = new ExamGradingDialogViewModel(
|
||||||
App.Services.GetRequiredService<IExamResultRepository>(),
|
App.Services.GetRequiredService<IExamResultRepository>(),
|
||||||
App.Services.GetRequiredService<IStudentRepository>(),
|
App.Services.GetRequiredService<IStudentRepository>(),
|
||||||
App.Services.GetRequiredService<IEnrollmentRepository>(),
|
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||||
App.Services.GetRequiredService<GradingService>(),
|
App.Services.GetRequiredService<GradingService>(),
|
||||||
exam, vm.Group.Id, vm.Group.SchoolYear);
|
exam, vm.Group.Id);
|
||||||
|
|
||||||
var dialog = new ExamGradingDialog { DataContext = dialogVm };
|
var dialog = new ExamGradingDialog { DataContext = dialogVm };
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ public partial class ParticipationTabView : UserControl
|
|||||||
App.Services.GetRequiredService<IParticipationRepository>(),
|
App.Services.GetRequiredService<IParticipationRepository>(),
|
||||||
App.Services.GetRequiredService<IParticipationAspectRepository>(),
|
App.Services.GetRequiredService<IParticipationAspectRepository>(),
|
||||||
App.Services.GetRequiredService<IStudentRepository>(),
|
App.Services.GetRequiredService<IStudentRepository>(),
|
||||||
App.Services.GetRequiredService<IEnrollmentRepository>(),
|
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||||
App.Services.GetRequiredService<IGradeRepository>(),
|
App.Services.GetRequiredService<IGradeRepository>(),
|
||||||
App.Services.GetRequiredService<GradingService>(),
|
App.Services.GetRequiredService<GradingService>(),
|
||||||
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem);
|
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem);
|
||||||
|
|||||||
@@ -40,9 +40,9 @@
|
|||||||
<ScrollViewer Padding="20">
|
<ScrollViewer Padding="20">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock Text="Lerngruppen" FontSize="15" FontWeight="SemiBold"/>
|
<TextBlock Text="Lerngruppen" FontSize="15" FontWeight="SemiBold"/>
|
||||||
<ItemsControl ItemsSource="{Binding Enrollments}">
|
<ItemsControl ItemsSource="{Binding GroupMemberships}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="vm:EnrollmentEntry">
|
<DataTemplate DataType="vm:GroupMembershipEntry">
|
||||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="6" Padding="12,8" Margin="0,0,0,6">
|
CornerRadius="6" Padding="12,8" Margin="0,0,0,6">
|
||||||
<Grid ColumnDefinitions="70,*,Auto">
|
<Grid ColumnDefinitions="70,*,Auto">
|
||||||
@@ -54,8 +54,8 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
<TextBlock Text="Keine Einschreibungen vorhanden." Opacity="0.4"
|
<TextBlock Text="Keine Lerngruppenzuordnungen vorhanden." Opacity="0.4"
|
||||||
IsVisible="{Binding !Enrollments.Count}"/>
|
IsVisible="{Binding !GroupMemberships.Count}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.Api", "LehrerApp.
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.Desktop", "LehrerApp.Desktop\LehrerApp.Desktop.csproj", "{A1000005-0000-0000-0000-000000000005}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.Desktop", "LehrerApp.Desktop\LehrerApp.Desktop.csproj", "{A1000005-0000-0000-0000-000000000005}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.Data.Tests", "LehrerApp.Data.Tests\LehrerApp.Data.Tests.csproj", "{A1000006-0000-0000-0000-000000000006}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -26,5 +28,7 @@ Global
|
|||||||
{A1000004-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1000004-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000005-0000-0000-0000-000000000005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A1000005-0000-0000-0000-000000000005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000005-0000-0000-0000-000000000005}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1000005-0000-0000-0000-000000000005}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1000006-0000-0000-0000-000000000006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1000006-0000-0000-0000-000000000006}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits i
|
|||||||
|
|
||||||
**Niveaudifferenzierung (E/G/Förder) ist bereits umgesetzt** — nicht aus dieser Liste, sondern
|
**Niveaudifferenzierung (E/G/Förder) ist bereits umgesetzt** — nicht aus dieser Liste, sondern
|
||||||
ein separater Bedarf (Nachteilsausgleich/Binnendifferenzierung): `LearningGroup.IsDifferentiated`
|
ein separater Bedarf (Nachteilsausgleich/Binnendifferenzierung): `LearningGroup.IsDifferentiated`
|
||||||
(Checkbox in den Stammdaten) blendet eine Niveau-Zuordnung je Schüler ein (`Enrollment.Niveau`,
|
(Checkbox in den Stammdaten) blendet eine Niveau-Zuordnung je Schüler ein (`GroupMembership.Niveau`,
|
||||||
Schüler-Tab). Klausuren bekommen optional ein `Niveau`; Punkteeingabe und Auswertung filtern dann
|
Schüler-Tab). Klausuren bekommen optional ein `Niveau`; Punkteeingabe und Auswertung filtern dann
|
||||||
automatisch auf die passenden Schüler. Workflow: G-Klausur normal anlegen, per "Duplizieren"
|
automatisch auf die passenden Schüler. Workflow: G-Klausur normal anlegen, per "Duplizieren"
|
||||||
(1.1.4) die E-Variante mit eigenen Aufgaben/Punkten ableiten (Niveau wird beim Duplizieren bewusst
|
(1.1.4) die E-Variante mit eigenen Aufgaben/Punkten ableiten (Niveau wird beim Duplizieren bewusst
|
||||||
@@ -109,7 +109,7 @@ Der Tab "Noten" in [GroupDetailView.axaml:83](LehrerApp.Desktop/Views/Groups/Gro
|
|||||||
sonstige Noten). Zelle zeigt Note/Punkte.
|
sonstige Noten). Zelle zeigt Note/Punkte.
|
||||||
- [ ] **2.1.2** Spalte "Gesamt" mit gewichtetem Durchschnitt über `GradingService.WeightedAverage()`.
|
- [ ] **2.1.2** Spalte "Gesamt" mit gewichtetem Durchschnitt über `GradingService.WeightedAverage()`.
|
||||||
- [ ] **2.1.3** Sortierung nach Name / Gesamtnote, Umschalten Noten ↔ Punkte.
|
- [ ] **2.1.3** Sortierung nach Name / Gesamtnote, Umschalten Noten ↔ Punkte.
|
||||||
- [ ] **2.1.4** Halbjahresfilter (H1 / H2 / Gesamtjahr), berücksichtigt `Enrollment.Period`.
|
- [ ] **2.1.4** Halbjahresfilter (H1 / H2 / Gesamtjahr), berücksichtigt `GroupMembership.Period`.
|
||||||
|
|
||||||
### 2.2 Einzelnoten pflegen
|
### 2.2 Einzelnoten pflegen
|
||||||
- [ ] **2.2.1** Dialog "Note hinzufügen": Kategorie (`GradeCategory`), Wert, Datum, Gewichtung, Notiz.
|
- [ ] **2.2.1** Dialog "Note hinzufügen": Kategorie (`GradeCategory`), Wert, Datum, Gewichtung, Notiz.
|
||||||
@@ -276,7 +276,7 @@ Navigationspunkt "Arbeitszeit" ist ein `PlaceholderViewModel`.
|
|||||||
|
|
||||||
Grundfunktionen sind vorhanden ([StudentViewModels.cs](LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs),
|
Grundfunktionen sind vorhanden ([StudentViewModels.cs](LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs),
|
||||||
[GroupViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs)).
|
[GroupViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs)).
|
||||||
Niveau-Zuordnung je Schüler (E/G/Förder, `Enrollment.Niveau`) ist bereits umgesetzt, siehe
|
Niveau-Zuordnung je Schüler (E/G/Förder, `GroupMembership.Niveau`) ist bereits umgesetzt, siehe
|
||||||
Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
||||||
|
|
||||||
### 7.1 Schüler
|
### 7.1 Schüler
|
||||||
@@ -291,9 +291,9 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
|||||||
`ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit,
|
`ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit,
|
||||||
beim Review aber bestätigt.
|
beim Review aber bestätigt.
|
||||||
- [ ] **7.2.2** Gruppe ins neue Schuljahr übernehmen: Kopie mit gleicher Schülerschaft,
|
- [ ] **7.2.2** Gruppe ins neue Schuljahr übernehmen: Kopie mit gleicher Schülerschaft,
|
||||||
neues `SchoolYear`, neue `Enrollment`-Einträge.
|
neues `SchoolYear`, neue `GroupMembership`-Einträge.
|
||||||
- [ ] **7.2.3** Schüler aus einer Gruppe entfernen (`Enrollment.LeftAt` setzen statt löschen).
|
- [ ] **7.2.3** Schüler aus einer Gruppe entfernen (`GroupMembership.LeftAt` setzen statt löschen).
|
||||||
- [ ] **7.2.4** Umgang mit `EnrollmentPeriod.Custom` in allen Auswertungen prüfen
|
- [ ] **7.2.4** Umgang mit `MembershipPeriod.Custom` in allen Auswertungen prüfen
|
||||||
(Schüler zählt nur im belegten Zeitraum).
|
(Schüler zählt nur im belegten Zeitraum).
|
||||||
- [ ] **7.2.5** Archivansicht abgeschlossener Schuljahre (schreibgeschützt).
|
- [ ] **7.2.5** Archivansicht abgeschlossener Schuljahre (schreibgeschützt).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Datenmodell und Begriffe
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die fachliche Bedeutung der zentralen Datensätze.
|
||||||
|
Es soll verhindern, dass technisch ähnliche Felder als unterschiedliche
|
||||||
|
Sachverhalte interpretiert oder dieselben Informationen mehrfach gespeichert
|
||||||
|
werden.
|
||||||
|
|
||||||
|
## Lerngruppe (`LearningGroup`)
|
||||||
|
|
||||||
|
Eine Lerngruppe ist die konkrete Unterrichtsgruppe eines Fachs in genau einem
|
||||||
|
Schuljahr. Sie kann als Klasse oder Kurs organisiert sein.
|
||||||
|
|
||||||
|
- `Name`: frei gewählte Bezeichnung, zum Beispiel `5b NAT (BEN)` oder `Mathe G`
|
||||||
|
- `SubjectId`: Verweis auf das unterrichtete Fach
|
||||||
|
- `SchoolYear`: Schuljahr dieser konkreten Lerngruppe
|
||||||
|
- `GradeLevel`: Klassen- beziehungsweise Jahrgangsstufe
|
||||||
|
|
||||||
|
Der Fachname wird ausschließlich im `Subject`-Stammdatensatz gepflegt. Die
|
||||||
|
Lerngruppe speichert keine zweite Kopie des Fachnamens.
|
||||||
|
|
||||||
|
## Gruppenzuordnung (`GroupMembership`)
|
||||||
|
|
||||||
|
Eine Gruppenzuordnung verbindet einen Schüler mit einer Lerngruppe. Sie ist
|
||||||
|
keine Aufnahme oder Einschreibung an der Schule.
|
||||||
|
|
||||||
|
- `StudentId`: Schüler
|
||||||
|
- `GroupId`: Lerngruppe
|
||||||
|
- `AddedOn`: Tag, an dem die Zuordnung in der App angelegt wurde
|
||||||
|
- `Period`: ganzes Schuljahr, erstes Halbjahr, zweites Halbjahr oder eigener Zeitraum
|
||||||
|
- `JoinedAt` / `LeftAt`: Grenzen eines eigenen Teilnahmezeitraums
|
||||||
|
- `Niveau`: optionale Niveaudifferenzierung
|
||||||
|
|
||||||
|
Das Schuljahr wird über die Lerngruppe ermittelt und deshalb nicht zusätzlich
|
||||||
|
in der Gruppenzuordnung gespeichert. Für eine spätere echte Schulaufnahme wäre
|
||||||
|
ein eigenes Feld wie `Student.SchoolEntryDate` zu verwenden.
|
||||||
|
|
||||||
|
Pro Kombination aus Schüler und Lerngruppe darf es höchstens eine Zuordnung
|
||||||
|
geben.
|
||||||
|
|
||||||
|
## Bewusst gespeicherte Momentaufnahmen
|
||||||
|
|
||||||
|
Einige berechnete Werte bleiben absichtlich gespeichert:
|
||||||
|
|
||||||
|
- `ExamResult.TotalPoints` und `ExamResult.Grade` halten das zuletzt berechnete
|
||||||
|
Klausurergebnis fest.
|
||||||
|
- `Exam.GradingKey` hält den für die konkrete Klausur verwendeten Notenschlüssel
|
||||||
|
fest und ist unabhängig von später geänderten Vorlagen.
|
||||||
|
- `Exam.Tasks` hält die Aufgabenstruktur der konkreten Klausur fest.
|
||||||
|
|
||||||
|
Änderungen an Aufgaben oder Notenschlüssel müssen die betroffenen Ergebnisse
|
||||||
|
kontrolliert neu berechnen. Diese Werte sind daher fachliche Momentaufnahmen und
|
||||||
|
nicht bloß unkontrollierte Kopien.
|
||||||
|
|
||||||
|
## Bewusste Denormalisierung
|
||||||
|
|
||||||
|
`Lesson.GroupId` bleibt zusätzlich zu `Lesson.UnitId` gespeichert. Dadurch kann
|
||||||
|
der häufige Kalenderzugriff auf alle Stunden einer Lerngruppe direkt indiziert
|
||||||
|
werden. Beim späteren Ausbau der Unterrichtsplanung muss sichergestellt werden,
|
||||||
|
dass `Lesson.GroupId` mit der Lerngruppe der zugehörigen Einheit übereinstimmt.
|
||||||
|
|
||||||
|
## Eindeutige Schlüssel
|
||||||
|
|
||||||
|
Die Datenbank schützt folgende Kombinationen mit eindeutigen Indizes:
|
||||||
|
|
||||||
|
- Gruppenzuordnung: `StudentId + GroupId`
|
||||||
|
- Klausurergebnis: `ExamId + StudentId`
|
||||||
|
- Mitarbeitseintrag: `SessionId + StudentId`
|
||||||
|
- Fach: normalisierter Fachname
|
||||||
|
|
||||||
|
Altdaten werden beim Öffnen der Datenbank automatisch migriert. Die Migration
|
||||||
|
verknüpft bisherige Fachtexte mit den Fachstammdaten, benennt die bisherige
|
||||||
|
`enrollments`-Collection in `group_memberships` um und entfernt daraus das
|
||||||
|
doppelt gespeicherte Schuljahr.
|
||||||
Reference in New Issue
Block a user