Neue Funktion Schüler austragen statt aus Kurs löschen.
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
|
||||||
|
namespace LehrerApp.Core.Services;
|
||||||
|
|
||||||
|
public enum SchoolYearPeriodKind { FullYear, H1, H2 }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gemeinsame Regeln für zeitlich begrenzte Gruppenzugehörigkeiten.
|
||||||
|
/// Eintritt und Austritt gelten unabhängig vom gewählten Halbjahresmodus.
|
||||||
|
/// </summary>
|
||||||
|
public static class GroupMembershipService
|
||||||
|
{
|
||||||
|
public static bool IsActiveOn(GroupMembership membership, DateOnly date)
|
||||||
|
{
|
||||||
|
if (membership.JoinedAt is { } joinedAt && date < joinedAt) return false;
|
||||||
|
if (membership.LeftAt is { } leftAt && date > leftAt) return false;
|
||||||
|
|
||||||
|
return membership.Period switch
|
||||||
|
{
|
||||||
|
MembershipPeriod.H1Only => IsFirstSemester(date),
|
||||||
|
MembershipPeriod.H2Only => !IsFirstSemester(date),
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Overlaps(GroupMembership membership, DateOnly from, DateOnly to)
|
||||||
|
{
|
||||||
|
if (to < from) return false;
|
||||||
|
|
||||||
|
var effectiveFrom = membership.JoinedAt is { } joinedAt && joinedAt > from ? joinedAt : from;
|
||||||
|
var effectiveTo = membership.LeftAt is { } leftAt && leftAt < to ? leftAt : to;
|
||||||
|
if (effectiveTo < effectiveFrom) return false;
|
||||||
|
|
||||||
|
if (membership.Period is MembershipPeriod.FullYear or MembershipPeriod.Custom) return true;
|
||||||
|
|
||||||
|
for (var month = new DateOnly(effectiveFrom.Year, effectiveFrom.Month, 1);
|
||||||
|
month <= effectiveTo;
|
||||||
|
month = month.AddMonths(1))
|
||||||
|
{
|
||||||
|
if (membership.Period == MembershipPeriod.H1Only && IsFirstSemester(month)) return true;
|
||||||
|
if (membership.Period == MembershipPeriod.H2Only && !IsFirstSemester(month)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (DateOnly From, DateOnly To) SchoolYearPeriod(
|
||||||
|
string schoolYear, SchoolYearPeriodKind period)
|
||||||
|
{
|
||||||
|
var slash = schoolYear.IndexOf('/');
|
||||||
|
var startText = slash >= 0 ? schoolYear[..slash] : schoolYear;
|
||||||
|
if (!int.TryParse(startText, out var startYear))
|
||||||
|
throw new ArgumentException("Ungültiges Schuljahr.", nameof(schoolYear));
|
||||||
|
|
||||||
|
return period switch
|
||||||
|
{
|
||||||
|
SchoolYearPeriodKind.H1 => (new DateOnly(startYear, 8, 1), new DateOnly(startYear + 1, 1, 31)),
|
||||||
|
SchoolYearPeriodKind.H2 => (new DateOnly(startYear + 1, 2, 1), new DateOnly(startYear + 1, 7, 31)),
|
||||||
|
_ => (new DateOnly(startYear, 8, 1), new DateOnly(startYear + 1, 7, 31)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFirstSemester(DateOnly date) => date.Month >= 8 || date.Month <= 1;
|
||||||
|
}
|
||||||
@@ -21,8 +21,12 @@ public class FakeMemberships(List<GroupMembership> all) : IGroupMembershipReposi
|
|||||||
public List<GroupMembership> GetByGroup(Guid groupId) => all.Where(m => m.GroupId == groupId).ToList();
|
public List<GroupMembership> GetByGroup(Guid groupId) => all.Where(m => m.GroupId == groupId).ToList();
|
||||||
public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
||||||
all.FirstOrDefault(m => m.StudentId == studentId && m.GroupId == groupId);
|
all.FirstOrDefault(m => m.StudentId == studentId && m.GroupId == groupId);
|
||||||
public void Save(GroupMembership membership) { }
|
public void Save(GroupMembership membership)
|
||||||
public void Delete(Guid id) { }
|
{
|
||||||
|
all.RemoveAll(m => m.Id == membership.Id);
|
||||||
|
all.Add(membership);
|
||||||
|
}
|
||||||
|
public void Delete(Guid id) => all.RemoveAll(m => m.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FakeSessions(List<ParticipationSession> all) : IParticipationSessionRepository
|
public class FakeSessions(List<ParticipationSession> all) : IParticipationSessionRepository
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ public class ReportGradeCalculationTests
|
|||||||
var grading = new GradingService();
|
var grading = new GradingService();
|
||||||
|
|
||||||
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
|
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
|
||||||
schemes, reportGrades, grading, GroupId, GroupType.Class, GradingSystem.Grades1To6, "Testgruppe");
|
schemes, reportGrades, grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
|
||||||
|
"Testgruppe", "2025/26");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (Exam Exam1, Exam Exam2, FakeExams Exams, FakeResults Results) BuildExams()
|
private static (Exam Exam1, Exam Exam2, FakeExams Exams, FakeResults Results) BuildExams()
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public class WithdrawStudentDialogViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void InitialisiertMitHeutigemDatumUndSpeichertAustritt()
|
||||||
|
{
|
||||||
|
var membership = new GroupMembership { JoinedAt = new DateOnly(2025, 9, 1) };
|
||||||
|
var memberships = new FakeMemberships([membership]);
|
||||||
|
var vm = new WithdrawStudentDialogViewModel(
|
||||||
|
memberships, membership, "Anna Beispiel", "7a", new DateOnly(2025, 10, 5));
|
||||||
|
|
||||||
|
Assert.Equal(new DateOnly(2025, 10, 5), DateOnly.FromDateTime(vm.SelectedDate!.Value.LocalDateTime));
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.True(vm.WasSaved);
|
||||||
|
Assert.Equal(new DateOnly(2025, 10, 5), membership.LeftAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VerhindertAustrittVorEintritt()
|
||||||
|
{
|
||||||
|
var membership = new GroupMembership { JoinedAt = new DateOnly(2025, 9, 1) };
|
||||||
|
var vm = new WithdrawStudentDialogViewModel(
|
||||||
|
new FakeMemberships([membership]), membership, "Anna Beispiel", "7a");
|
||||||
|
vm.SelectedDate = new DateTimeOffset(2025, 8, 31, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.False(vm.WasSaved);
|
||||||
|
Assert.Null(membership.LeftAt);
|
||||||
|
Assert.NotEmpty(vm.DateError);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ public partial class ExamGradingDialogViewModel : ObservableObject
|
|||||||
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 membership = membershipList.FirstOrDefault(e => e.StudentId == s.Id);
|
var membership = membershipList.FirstOrDefault(e => e.StudentId == s.Id);
|
||||||
if (membership is not null && !IsMemberAtDate(membership, exam.Date)) continue;
|
if (membership is not null && !GroupMembershipService.IsActiveOn(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.
|
||||||
@@ -52,14 +52,6 @@ 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 IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
|
|
||||||
{
|
|
||||||
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
|
||||||
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
|
||||||
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
|
|
||||||
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zeile im Punkteraster ──────────────────────────────────────────────────
|
// ── Zeile im Punkteraster ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
private GradingSystem _gradingSystem;
|
private GradingSystem _gradingSystem;
|
||||||
private GroupType _groupType;
|
private GroupType _groupType;
|
||||||
private string _groupLabel = "";
|
private string _groupLabel = "";
|
||||||
|
private string _schoolYear = "";
|
||||||
private bool _sortByTotal;
|
private bool _sortByTotal;
|
||||||
private bool _sortDescending;
|
private bool _sortDescending;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
public GradingSystem GradingSystem => _gradingSystem;
|
public GradingSystem GradingSystem => _gradingSystem;
|
||||||
public GroupType GroupType => _groupType;
|
public GroupType GroupType => _groupType;
|
||||||
public string GroupLabel => _groupLabel;
|
public string GroupLabel => _groupLabel;
|
||||||
|
public string SchoolYear => _schoolYear;
|
||||||
|
|
||||||
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
||||||
[ObservableProperty] private bool _showAsPoints = true;
|
[ObservableProperty] private bool _showAsPoints = true;
|
||||||
@@ -61,12 +63,14 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
_selectedPeriod = PeriodOptions[0];
|
_selectedPeriod = PeriodOptions[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize(Guid groupId, GradingSystem gradingSystem, GroupType groupType, string groupLabel)
|
public void Initialize(Guid groupId, GradingSystem gradingSystem, GroupType groupType,
|
||||||
|
string groupLabel, string schoolYear)
|
||||||
{
|
{
|
||||||
_groupId = groupId;
|
_groupId = groupId;
|
||||||
_gradingSystem = gradingSystem;
|
_gradingSystem = gradingSystem;
|
||||||
_groupType = groupType;
|
_groupType = groupType;
|
||||||
_groupLabel = groupLabel;
|
_groupLabel = groupLabel;
|
||||||
|
_schoolYear = schoolYear;
|
||||||
ShowAsPoints = gradingSystem == GradingSystem.Points0To15;
|
ShowAsPoints = gradingSystem == GradingSystem.Points0To15;
|
||||||
OnPropertyChanged(nameof(CanTogglePointsView));
|
OnPropertyChanged(nameof(CanTogglePointsView));
|
||||||
Recompute();
|
Recompute();
|
||||||
@@ -119,18 +123,24 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
private void Recompute()
|
private void Recompute()
|
||||||
{
|
{
|
||||||
var period = SelectedPeriod.Period;
|
var period = SelectedPeriod.Period;
|
||||||
|
var (periodFrom, periodTo) = GroupMembershipService.SchoolYearPeriod(_schoolYear, period switch
|
||||||
|
{
|
||||||
|
ParticipationPeriod.H1 => SchoolYearPeriodKind.H1,
|
||||||
|
ParticipationPeriod.H2 => SchoolYearPeriodKind.H2,
|
||||||
|
_ => SchoolYearPeriodKind.FullYear,
|
||||||
|
});
|
||||||
|
|
||||||
var students = _students.GetByGroup(_groupId);
|
var students = _students.GetByGroup(_groupId);
|
||||||
var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
||||||
|
|
||||||
var exams = _exams.GetByGroup(_groupId)
|
var exams = _exams.GetByGroup(_groupId)
|
||||||
.Where(e => InPeriod(e.Date, period))
|
.Where(e => e.Date >= periodFrom && e.Date <= periodTo)
|
||||||
.OrderBy(e => e.Date)
|
.OrderBy(e => e.Date)
|
||||||
.ToList();
|
.ToList();
|
||||||
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
||||||
|
|
||||||
var otherGrades = _grades.GetByGroup(_groupId)
|
var otherGrades = _grades.GetByGroup(_groupId)
|
||||||
.Where(g => InPeriod(g.Date, period))
|
.Where(g => g.Date >= periodFrom && g.Date <= periodTo)
|
||||||
.ToList();
|
.ToList();
|
||||||
var gradeColumnKeys = otherGrades
|
var gradeColumnKeys = otherGrades
|
||||||
.Select(g => (g.Category, Note: g.Note ?? "", g.Date))
|
.Select(g => (g.Category, Note: g.Note ?? "", g.Date))
|
||||||
@@ -153,13 +163,15 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
foreach (var student in students)
|
foreach (var student in students)
|
||||||
{
|
{
|
||||||
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
||||||
if (!StudentActiveInPeriod(membership, period)) continue;
|
if (membership is not null && !GroupMembershipService.Overlaps(membership, periodFrom, periodTo)) continue;
|
||||||
|
|
||||||
var cells = new List<string>();
|
var cells = new List<string>();
|
||||||
var numeric = new List<(string Grade, double Weight)>();
|
var numeric = new List<(string Grade, double Weight)>();
|
||||||
|
|
||||||
foreach (var exam in exams)
|
foreach (var exam in exams)
|
||||||
{
|
{
|
||||||
|
if (membership is not null && !GroupMembershipService.IsActiveOn(membership, exam.Date))
|
||||||
|
{ cells.Add(""); continue; }
|
||||||
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) { cells.Add(""); continue; }
|
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) { cells.Add(""); continue; }
|
||||||
resultsByExam[exam.Id].TryGetValue(student.Id, out var result);
|
resultsByExam[exam.Id].TryGetValue(student.Id, out var result);
|
||||||
if (result is null) { cells.Add(""); continue; }
|
if (result is null) { cells.Add(""); continue; }
|
||||||
@@ -171,6 +183,8 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
|
|
||||||
foreach (var key in gradeColumnKeys)
|
foreach (var key in gradeColumnKeys)
|
||||||
{
|
{
|
||||||
|
if (membership is not null && !GroupMembershipService.IsActiveOn(membership, key.Date))
|
||||||
|
{ cells.Add(""); continue; }
|
||||||
var grade = otherGrades.FirstOrDefault(g =>
|
var grade = otherGrades.FirstOrDefault(g =>
|
||||||
g.StudentId == student.Id && g.Category == key.Category &&
|
g.StudentId == student.Id && g.Category == key.Category &&
|
||||||
(g.Note ?? "") == key.Note && g.Date == key.Date);
|
(g.Note ?? "") == key.Note && g.Date == key.Date);
|
||||||
@@ -206,23 +220,6 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
for (var i = 0; i < sorted.Count; i++) Rows.Move(Rows.IndexOf(sorted[i]), i);
|
for (var i = 0; i < sorted.Count; i++) Rows.Move(Rows.IndexOf(sorted[i]), i);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InPeriod(DateOnly date, ParticipationPeriod period) => period switch
|
|
||||||
{
|
|
||||||
ParticipationPeriod.H1 => date.Month >= 8 || date.Month <= 1,
|
|
||||||
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool StudentActiveInPeriod(GroupMembership? m, ParticipationPeriod period)
|
|
||||||
{
|
|
||||||
if (period == ParticipationPeriod.FullYear || m is null) return true;
|
|
||||||
return m.Period switch
|
|
||||||
{
|
|
||||||
MembershipPeriod.H1Only => period == ParticipationPeriod.H1,
|
|
||||||
MembershipPeriod.H2Only => period == ParticipationPeriod.H2,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GradeOverviewColumnDef(string header)
|
public class GradeOverviewColumnDef(string header)
|
||||||
@@ -404,6 +401,8 @@ public partial class GradeEditItem : ObservableObject
|
|||||||
public partial class CollectiveGradeDialogViewModel : ObservableObject
|
public partial class CollectiveGradeDialogViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IGradeRepository _grades;
|
private readonly IGradeRepository _grades;
|
||||||
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
|
private readonly List<Student> _students;
|
||||||
private readonly Guid _groupId;
|
private readonly Guid _groupId;
|
||||||
|
|
||||||
[ObservableProperty] private GradeCategory _category = GradeCategory.Other;
|
[ObservableProperty] private GradeCategory _category = GradeCategory.Other;
|
||||||
@@ -422,11 +421,36 @@ public partial class CollectiveGradeDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
public ObservableCollection<CollectiveGradeStudentRow> Rows { get; } = [];
|
public ObservableCollection<CollectiveGradeStudentRow> Rows { get; } = [];
|
||||||
|
|
||||||
public CollectiveGradeDialogViewModel(IGradeRepository grades, IStudentRepository students, Guid groupId)
|
public CollectiveGradeDialogViewModel(IGradeRepository grades, IStudentRepository students,
|
||||||
|
IGroupMembershipRepository memberships, Guid groupId)
|
||||||
{
|
{
|
||||||
_grades = grades; _groupId = groupId;
|
_grades = grades; _memberships = memberships; _groupId = groupId;
|
||||||
foreach (var s in students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
_students = students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToList();
|
||||||
Rows.Add(new CollectiveGradeStudentRow(s.Id, s.FullName));
|
RebuildRows(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnDateTextChanged(string value)
|
||||||
|
{
|
||||||
|
if (DateOnly.TryParseExact(value, "dd.MM.yyyy", null,
|
||||||
|
System.Globalization.DateTimeStyles.None, out var date))
|
||||||
|
RebuildRows(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildRows(DateOnly date)
|
||||||
|
{
|
||||||
|
var previousValues = Rows.ToDictionary(r => r.StudentId, r => r.Value);
|
||||||
|
var membershipByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
||||||
|
Rows.Clear();
|
||||||
|
foreach (var student in _students)
|
||||||
|
{
|
||||||
|
if (membershipByStudent.TryGetValue(student.Id, out var membership)
|
||||||
|
&& !GroupMembershipService.IsActiveOn(membership, date))
|
||||||
|
continue;
|
||||||
|
Rows.Add(new CollectiveGradeStudentRow(student.Id, student.FullName)
|
||||||
|
{
|
||||||
|
Value = previousValues.GetValueOrDefault(student.Id, ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
[ObservableProperty] private int _activeTabIndex = 0;
|
[ObservableProperty] private int _activeTabIndex = 0;
|
||||||
[ObservableProperty] private StudentSummary? _selectedStudent;
|
[ObservableProperty] private StudentSummary? _selectedStudent;
|
||||||
[ObservableProperty] private ExamSummary? _selectedExam;
|
[ObservableProperty] private ExamSummary? _selectedExam;
|
||||||
|
[ObservableProperty] private bool _showFormerStudents;
|
||||||
|
|
||||||
// Eigene Property statt "Group.IsDifferentiated" im Binding-Pfad: die View bindet bereits,
|
// Eigene Property statt "Group.IsDifferentiated" im Binding-Pfad: die View bindet bereits,
|
||||||
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
|
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
|
||||||
@@ -177,6 +178,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public string SubjectName { get; private set; } = "";
|
public string SubjectName { get; private set; } = "";
|
||||||
|
|
||||||
partial void OnGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(IsDifferentiated));
|
partial void OnGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(IsDifferentiated));
|
||||||
|
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
|
||||||
|
|
||||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||||
@@ -185,6 +187,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
||||||
public PlanningTabViewModel PlanningTab { get; }
|
public PlanningTabViewModel PlanningTab { get; }
|
||||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||||
|
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
||||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||||
public Func<Exam, Task<bool>>? OnEditExam { get; set; }
|
public Func<Exam, Task<bool>>? OnEditExam { get; set; }
|
||||||
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
|
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
|
||||||
@@ -219,7 +222,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
LoadStudents();
|
LoadStudents();
|
||||||
ReloadExams();
|
ReloadExams();
|
||||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||||
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle);
|
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear);
|
||||||
PlanningTab.Initialize(Group.Id);
|
PlanningTab.Initialize(Group.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,11 +241,14 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
Students.Clear();
|
Students.Clear();
|
||||||
var enrolled = _students.GetByGroup(Group.Id);
|
var enrolled = _students.GetByGroup(Group.Id);
|
||||||
var memberships = _memberships.GetByGroup(Group.Id).ToDictionary(e => e.StudentId);
|
var memberships = _memberships.GetByGroup(Group.Id).ToDictionary(e => e.StudentId);
|
||||||
StudentCount = enrolled.Count;
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
foreach (var s in enrolled)
|
StudentCount = memberships.Values.Count(m => GroupMembershipService.IsActiveOn(m, today));
|
||||||
|
foreach (var s in enrolled.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||||
{
|
{
|
||||||
memberships.TryGetValue(s.Id, out var membership);
|
memberships.TryGetValue(s.Id, out var membership);
|
||||||
var summary = new StudentSummary(s, membership) { OnChanged = SaveStudentNiveau };
|
if (membership is null) continue;
|
||||||
|
var summary = new StudentSummary(s, membership, today) { OnChanged = SaveStudentNiveau };
|
||||||
|
if (summary.IsFormer && !ShowFormerStudents) continue;
|
||||||
Students.Add(summary);
|
Students.Add(summary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,21 +275,36 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(HasSelectedStudent))]
|
[RelayCommand(CanExecute = nameof(HasSelectedStudent))]
|
||||||
private void RemoveStudent()
|
private async Task WithdrawStudent()
|
||||||
{
|
{
|
||||||
if (Group is null || SelectedStudent is null) return;
|
if (SelectedStudent is null || OnWithdrawStudent is null) return;
|
||||||
var membership = _memberships.GetByStudentAndGroup(SelectedStudent.Id, Group.Id);
|
if (!await OnWithdrawStudent(SelectedStudent)) return;
|
||||||
if (membership is null) return;
|
|
||||||
_memberships.Delete(membership.Id);
|
|
||||||
LoadStudents();
|
LoadStudents();
|
||||||
SelectedStudent = null;
|
SelectedStudent = null;
|
||||||
ParticipationTab.RefreshCurrentGrid();
|
ParticipationTab.RefreshCurrentGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedStudentChanged(StudentSummary? value) =>
|
[RelayCommand(CanExecute = nameof(CanReinstateSelectedStudent))]
|
||||||
RemoveStudentCommand.NotifyCanExecuteChanged();
|
private void ReinstateStudent()
|
||||||
|
{
|
||||||
|
if (Group is null || SelectedStudent is null) return;
|
||||||
|
var membership = _memberships.GetByStudentAndGroup(SelectedStudent.Id, Group.Id);
|
||||||
|
if (membership is null) return;
|
||||||
|
membership.LeftAt = null;
|
||||||
|
_memberships.Save(membership);
|
||||||
|
LoadStudents();
|
||||||
|
SelectedStudent = null;
|
||||||
|
ParticipationTab.RefreshCurrentGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedStudentChanged(StudentSummary? value)
|
||||||
|
{
|
||||||
|
WithdrawStudentCommand.NotifyCanExecuteChanged();
|
||||||
|
ReinstateStudentCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private bool HasSelectedStudent() => SelectedStudent is not null;
|
private bool HasSelectedStudent() => SelectedStudent is not null;
|
||||||
|
private bool CanReinstateSelectedStudent() => SelectedStudent?.HasExitDate == true;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task AddExam()
|
private async Task AddExam()
|
||||||
@@ -423,6 +444,10 @@ public partial class StudentSummary : ObservableObject
|
|||||||
public Guid Id { get; }
|
public Guid Id { get; }
|
||||||
public string FullName { get; }
|
public string FullName { get; }
|
||||||
public string PeriodLabel { get; }
|
public string PeriodLabel { get; }
|
||||||
|
public bool IsFormer { get; }
|
||||||
|
public bool HasExitDate { get; }
|
||||||
|
public string MembershipStatus { get; }
|
||||||
|
public string WithdrawActionLabel => HasExitDate ? "Austrittsdatum ändern" : "Austragen";
|
||||||
|
|
||||||
[ObservableProperty] private Niveau? _niveau;
|
[ObservableProperty] private Niveau? _niveau;
|
||||||
|
|
||||||
@@ -434,17 +459,17 @@ public partial class StudentSummary : ObservableObject
|
|||||||
|
|
||||||
public Action<StudentSummary>? OnChanged { get; set; }
|
public Action<StudentSummary>? OnChanged { get; set; }
|
||||||
|
|
||||||
public StudentSummary(Core.Models.Student s, GroupMembership? membership)
|
public StudentSummary(Core.Models.Student s, GroupMembership? membership, DateOnly? today = null)
|
||||||
{
|
{
|
||||||
Id = s.Id;
|
Id = s.Id;
|
||||||
FullName = s.FullName;
|
FullName = s.FullName;
|
||||||
PeriodLabel = membership?.Period switch
|
PeriodLabel = membership is null ? "" : BuildPeriodLabel(membership);
|
||||||
{
|
HasExitDate = membership?.LeftAt.HasValue == true;
|
||||||
MembershipPeriod.H1Only => "H1",
|
IsFormer = membership?.LeftAt is { } leftAt
|
||||||
MembershipPeriod.H2Only => "H2",
|
&& leftAt < (today ?? DateOnly.FromDateTime(DateTime.Today));
|
||||||
MembershipPeriod.Custom => BuildCustomLabel(membership),
|
MembershipStatus = membership?.LeftAt is { } exitDate
|
||||||
_ => "",
|
? IsFormer ? $"Ausgetreten am {exitDate:dd.MM.yyyy}" : $"Austritt am {exitDate:dd.MM.yyyy}"
|
||||||
};
|
: "Aktiv";
|
||||||
_niveau = membership?.Niveau;
|
_niveau = membership?.Niveau;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,12 +479,20 @@ public partial class StudentSummary : ObservableObject
|
|||||||
OnChanged?.Invoke(this);
|
OnChanged?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildCustomLabel(GroupMembership membership)
|
private static string BuildPeriodLabel(GroupMembership membership)
|
||||||
{
|
{
|
||||||
if (membership.JoinedAt.HasValue && membership.LeftAt.HasValue) return $"{membership.JoinedAt:dd.MM.}–{membership.LeftAt:dd.MM.}";
|
var period = membership.Period switch
|
||||||
if (membership.JoinedAt.HasValue) return $"ab {membership.JoinedAt:dd.MM.}";
|
{
|
||||||
if (membership.LeftAt.HasValue) return $"bis {membership.LeftAt:dd.MM.}";
|
MembershipPeriod.H1Only => "H1",
|
||||||
return "Datum";
|
MembershipPeriod.H2Only => "H2",
|
||||||
|
MembershipPeriod.Custom => "Datum",
|
||||||
|
_ => "Ganzes Jahr",
|
||||||
|
};
|
||||||
|
if (membership.JoinedAt.HasValue && membership.LeftAt.HasValue)
|
||||||
|
return $"{period} · {membership.JoinedAt:dd.MM.}–{membership.LeftAt:dd.MM.}";
|
||||||
|
if (membership.JoinedAt.HasValue) return $"{period} · ab {membership.JoinedAt:dd.MM.}";
|
||||||
|
if (membership.LeftAt.HasValue) return $"{period} · bis {membership.LeftAt:dd.MM.}";
|
||||||
|
return period;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,6 +624,63 @@ public class StudentPickerItem
|
|||||||
public StudentPickerItem(Student s) { Id = s.Id; FullName = s.FullName; }
|
public StudentPickerItem(Student s) { Id = s.Id; FullName = s.FullName; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Dialog: Schüler aus einer Lerngruppe austragen ───────────────────────────
|
||||||
|
|
||||||
|
public partial class WithdrawStudentDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
|
private readonly GroupMembership _membership;
|
||||||
|
|
||||||
|
[ObservableProperty] private DateTimeOffset? _selectedDate;
|
||||||
|
[ObservableProperty] private string _dateError = "";
|
||||||
|
|
||||||
|
public string StudentName { get; }
|
||||||
|
public string GroupName { get; }
|
||||||
|
public DateTimeOffset? EarliestDate { get; }
|
||||||
|
public bool WasSaved { get; private set; }
|
||||||
|
|
||||||
|
public WithdrawStudentDialogViewModel(IGroupMembershipRepository memberships,
|
||||||
|
GroupMembership membership, string studentName, string groupName, DateOnly? today = null)
|
||||||
|
{
|
||||||
|
_memberships = memberships;
|
||||||
|
_membership = membership;
|
||||||
|
StudentName = studentName;
|
||||||
|
GroupName = groupName;
|
||||||
|
|
||||||
|
var initialDate = membership.LeftAt ?? today ?? DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
_selectedDate = ToDateTimeOffset(initialDate);
|
||||||
|
EarliestDate = membership.JoinedAt is { } joinedAt ? ToDateTimeOffset(joinedAt) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
DateError = "";
|
||||||
|
if (SelectedDate is null)
|
||||||
|
{
|
||||||
|
DateError = "Bitte ein Austrittsdatum auswählen.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitDate = DateOnly.FromDateTime(SelectedDate.Value.LocalDateTime);
|
||||||
|
if (_membership.JoinedAt is { } joinedAt && exitDate < joinedAt)
|
||||||
|
{
|
||||||
|
DateError = $"Das Austrittsdatum darf nicht vor dem Eintritt am {joinedAt:dd.MM.yyyy} liegen.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_membership.LeftAt = exitDate;
|
||||||
|
_memberships.Save(_membership);
|
||||||
|
WasSaved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset ToDateTimeOffset(DateOnly date)
|
||||||
|
{
|
||||||
|
var localDateTime = date.ToDateTime(TimeOnly.MinValue);
|
||||||
|
return new DateTimeOffset(localDateTime, TimeZoneInfo.Local.GetUtcOffset(localDateTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dialog: Neue Lerngruppe anlegen ──────────────────────────────────────────
|
// ── Dialog: Neue Lerngruppe anlegen ──────────────────────────────────────────
|
||||||
|
|
||||||
public partial class AddGroupDialogViewModel : ObservableObject
|
public partial class AddGroupDialogViewModel : ObservableObject
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var membership = membershipList.FirstOrDefault(e => e.StudentId == student.Id);
|
var membership = membershipList.FirstOrDefault(e => e.StudentId == student.Id);
|
||||||
var relevantSessions = sessions
|
var relevantSessions = sessions
|
||||||
.Where(s => membership is null || IsMemberAtDate(membership, s.Date))
|
.Where(s => membership is null || GroupMembershipService.IsActiveOn(membership, s.Date))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var points = new List<(DateOnly Date, double Rating)>();
|
var points = new List<(DateOnly Date, double Rating)>();
|
||||||
@@ -152,14 +152,6 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
|||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
|
|
||||||
{
|
|
||||||
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
|
||||||
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
|
||||||
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
|
|
||||||
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Gewichtung eines Aspekts (3.2.1) ─────────────────────────────────────────
|
// ── Gewichtung eines Aspekts (3.2.1) ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
foreach (var s in students)
|
foreach (var s in students)
|
||||||
{
|
{
|
||||||
var membership = memberships.FirstOrDefault(e => e.StudentId == s.Id);
|
var membership = memberships.FirstOrDefault(e => e.StudentId == s.Id);
|
||||||
if (membership is not null && !IsMemberAtDate(membership, sessionDate))
|
if (membership is not null && !GroupMembershipService.IsActiveOn(membership, sessionDate))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var entry = entries.FirstOrDefault(e => e.StudentId == s.Id)
|
var entry = entries.FirstOrDefault(e => e.StudentId == s.Id)
|
||||||
@@ -157,15 +158,6 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
RebuildColumnsSignal++;
|
RebuildColumnsSignal++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
|
|
||||||
{
|
|
||||||
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
|
||||||
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
|
||||||
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
|
|
||||||
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
private void SaveRating(Guid sessionId, Guid studentId, string key, int? value)
|
private void SaveRating(Guid sessionId, Guid studentId, string key, int? value)
|
||||||
{
|
{
|
||||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
private readonly IExamRepository _exams;
|
private readonly IExamRepository _exams;
|
||||||
private readonly IExamResultRepository _results;
|
private readonly IExamResultRepository _results;
|
||||||
private readonly IGradeRepository _grades;
|
private readonly IGradeRepository _grades;
|
||||||
|
private readonly Dictionary<Guid, GroupMembership> _membershipsByStudent;
|
||||||
private readonly GradingService _grading;
|
private readonly GradingService _grading;
|
||||||
private readonly Guid _groupId;
|
private readonly Guid _groupId;
|
||||||
private readonly string _schoolYear;
|
private readonly string _schoolYear;
|
||||||
@@ -69,7 +70,8 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
public ParticipationWizardDialogViewModel(
|
public ParticipationWizardDialogViewModel(
|
||||||
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
||||||
IParticipationAspectRepository aspects, IParticipationSectionRepository sectionRepo,
|
IParticipationAspectRepository aspects, IParticipationSectionRepository sectionRepo,
|
||||||
IStudentRepository students, IExamRepository exams, IExamResultRepository results,
|
IStudentRepository students, IGroupMembershipRepository memberships,
|
||||||
|
IExamRepository exams, IExamResultRepository results,
|
||||||
IGradeRepository grades, GradingService grading,
|
IGradeRepository grades, GradingService grading,
|
||||||
Guid groupId, string schoolYear, GradingSystem gradingSystem, string groupLabel)
|
Guid groupId, string schoolYear, GradingSystem gradingSystem, string groupLabel)
|
||||||
{
|
{
|
||||||
@@ -77,12 +79,17 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
_exams = exams; _results = results; _grades = grades; _grading = grading;
|
_exams = exams; _results = results; _grades = grades; _grading = grading;
|
||||||
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
||||||
GroupLabel = groupLabel;
|
GroupLabel = groupLabel;
|
||||||
|
_membershipsByStudent = memberships.GetByGroup(groupId).ToDictionary(m => m.StudentId);
|
||||||
|
|
||||||
_aspectWeights = aspects.GetDefaults()
|
_aspectWeights = aspects.GetDefaults()
|
||||||
.Concat(aspects.GetByGroup(groupId))
|
.Concat(aspects.GetByGroup(groupId))
|
||||||
.GroupBy(a => a.Key)
|
.GroupBy(a => a.Key)
|
||||||
.ToDictionary(g => g.Key, g => g.Last().Weight);
|
.ToDictionary(g => g.Key, g => g.Last().Weight);
|
||||||
_students = students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToList();
|
var schoolYearRange = GroupMembershipService.SchoolYearPeriod(schoolYear, SchoolYearPeriodKind.FullYear);
|
||||||
|
_students = students.GetByGroup(groupId)
|
||||||
|
.Where(s => !_membershipsByStudent.TryGetValue(s.Id, out var membership)
|
||||||
|
|| GroupMembershipService.Overlaps(membership, schoolYearRange.From, schoolYearRange.To))
|
||||||
|
.OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToList();
|
||||||
_allSessions = sessions.GetByGroup(groupId).OrderBy(s => s.Date).ToList();
|
_allSessions = sessions.GetByGroup(groupId).OrderBy(s => s.Date).ToList();
|
||||||
_sectionList = sectionRepo.GetByGroup(groupId).OrderBy(s => s.StartDate).ToList();
|
_sectionList = sectionRepo.GetByGroup(groupId).OrderBy(s => s.StartDate).ToList();
|
||||||
|
|
||||||
@@ -123,17 +130,20 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
foreach (var session in _allSessions)
|
foreach (var session in _allSessions)
|
||||||
{
|
{
|
||||||
|
if (!IsStudentActiveOn(studentId, session.Date)) continue;
|
||||||
var entry = _entries.GetBySessionAndStudent(session.Id, studentId);
|
var entry = _entries.GetBySessionAndStudent(session.Id, studentId);
|
||||||
points.Add((session.Date, BuildSessionPoint(session, entry, studentId)));
|
points.Add((session.Date, BuildSessionPoint(session, entry, studentId)));
|
||||||
}
|
}
|
||||||
foreach (var exam in _exams.GetByGroup(_groupId).OrderBy(e => e.Date))
|
foreach (var exam in _exams.GetByGroup(_groupId).OrderBy(e => e.Date))
|
||||||
{
|
{
|
||||||
|
if (!IsStudentActiveOn(studentId, exam.Date)) continue;
|
||||||
var result = _results.GetByExamAndStudent(exam.Id, studentId);
|
var result = _results.GetByExamAndStudent(exam.Id, studentId);
|
||||||
if (result is null) continue;
|
if (result is null) continue;
|
||||||
points.Add((exam.Date, BuildExamPoint(exam, result)));
|
points.Add((exam.Date, BuildExamPoint(exam, result)));
|
||||||
}
|
}
|
||||||
foreach (var grade in _grades.GetByStudentAndGroup(studentId, _groupId)
|
foreach (var grade in _grades.GetByStudentAndGroup(studentId, _groupId)
|
||||||
.Where(g => g.Category != GradeCategory.Participation))
|
.Where(g => g.Category != GradeCategory.Participation)
|
||||||
|
.Where(g => IsStudentActiveOn(studentId, g.Date)))
|
||||||
{
|
{
|
||||||
points.Add((grade.Date, BuildOtherGradePoint(grade)));
|
points.Add((grade.Date, BuildOtherGradePoint(grade)));
|
||||||
}
|
}
|
||||||
@@ -269,6 +279,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
foreach (var section in _sectionList)
|
foreach (var section in _sectionList)
|
||||||
{
|
{
|
||||||
|
if (!StudentOverlaps(studentId, section.StartDate, section.EndDate)) continue;
|
||||||
var grade = studentGrades.FirstOrDefault(g => g.Note == AbschnittPrefix + section.Label);
|
var grade = studentGrades.FirstOrDefault(g => g.Note == AbschnittPrefix + section.Label);
|
||||||
var row = new WizardSectionRow(section.Label, section.StartDate, section.EndDate,
|
var row = new WizardSectionRow(section.Label, section.StartDate, section.EndDate,
|
||||||
grade?.Value ?? "", isOpen: false);
|
grade?.Value ?? "", isOpen: false);
|
||||||
@@ -285,7 +296,8 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
private string? ComputeSuggestion(Guid studentId, DateOnly start, DateOnly end)
|
private string? ComputeSuggestion(Guid studentId, DateOnly start, DateOnly end)
|
||||||
{
|
{
|
||||||
var points = new List<double>();
|
var points = new List<double>();
|
||||||
foreach (var session in _allSessions.Where(s => s.Date >= start && s.Date <= end))
|
foreach (var session in _allSessions.Where(s => s.Date >= start && s.Date <= end)
|
||||||
|
.Where(s => IsStudentActiveOn(studentId, s.Date)))
|
||||||
{
|
{
|
||||||
var entry = _entries.GetBySessionAndStudent(session.Id, studentId);
|
var entry = _entries.GetBySessionAndStudent(session.Id, studentId);
|
||||||
if (entry is null || entry.Ratings.Count == 0) continue;
|
if (entry is null || entry.Ratings.Count == 0) continue;
|
||||||
@@ -375,6 +387,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
var sectionGrades = _grades.GetByStudentAndGroup(student.Id, _groupId)
|
var sectionGrades = _grades.GetByStudentAndGroup(student.Id, _groupId)
|
||||||
.Where(g => g.Category == GradeCategory.Participation && g.Note is not null && g.Note.StartsWith(AbschnittPrefix))
|
.Where(g => g.Category == GradeCategory.Participation && g.Note is not null && g.Note.StartsWith(AbschnittPrefix))
|
||||||
.Where(g => InPeriod(g.Date, RollupPeriod.Period))
|
.Where(g => InPeriod(g.Date, RollupPeriod.Period))
|
||||||
|
.Where(g => IsStudentActiveOn(student.Id, g.Date))
|
||||||
.Select(g => (g.Value, g.Weight))
|
.Select(g => (g.Value, g.Weight))
|
||||||
.ToList();
|
.ToList();
|
||||||
if (sectionGrades.Count == 0) continue;
|
if (sectionGrades.Count == 0) continue;
|
||||||
@@ -401,6 +414,14 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private bool IsStudentActiveOn(Guid studentId, DateOnly date) =>
|
||||||
|
!_membershipsByStudent.TryGetValue(studentId, out var membership)
|
||||||
|
|| GroupMembershipService.IsActiveOn(membership, date);
|
||||||
|
|
||||||
|
private bool StudentOverlaps(Guid studentId, DateOnly from, DateOnly to) =>
|
||||||
|
!_membershipsByStudent.TryGetValue(studentId, out var membership)
|
||||||
|
|| GroupMembershipService.Overlaps(membership, from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zeitleisten-Bausteine ────────────────────────────────────────────────────
|
// ── Zeitleisten-Bausteine ────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
private readonly GroupType _groupType;
|
private readonly GroupType _groupType;
|
||||||
private readonly GradingSystem _gradingSystem;
|
private readonly GradingSystem _gradingSystem;
|
||||||
private readonly string _groupLabel;
|
private readonly string _groupLabel;
|
||||||
|
private readonly string _schoolYear;
|
||||||
|
|
||||||
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
||||||
[ObservableProperty] private RoundingRule _roundingRule = RoundingRule.Commercial;
|
[ObservableProperty] private RoundingRule _roundingRule = RoundingRule.Commercial;
|
||||||
@@ -51,11 +52,12 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
public ReportGradeDialogViewModel(IGradeRepository grades, IExamRepository exams,
|
public ReportGradeDialogViewModel(IGradeRepository grades, IExamRepository exams,
|
||||||
IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships,
|
IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships,
|
||||||
IGradingSchemeRepository schemes, IReportGradeRepository reportGrades, GradingService grading,
|
IGradingSchemeRepository schemes, IReportGradeRepository reportGrades, GradingService grading,
|
||||||
Guid groupId, GroupType groupType, GradingSystem gradingSystem, string groupLabel)
|
Guid groupId, GroupType groupType, GradingSystem gradingSystem, string groupLabel, string schoolYear)
|
||||||
{
|
{
|
||||||
_grades = grades; _exams = exams; _results = results; _students = students;
|
_grades = grades; _exams = exams; _results = results; _students = students;
|
||||||
_memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading;
|
_memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading;
|
||||||
_groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel;
|
_groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel;
|
||||||
|
_schoolYear = schoolYear;
|
||||||
|
|
||||||
_selectedPeriod = PeriodOptions[0];
|
_selectedPeriod = PeriodOptions[0];
|
||||||
Recompute();
|
Recompute();
|
||||||
@@ -77,18 +79,24 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
var period = SelectedPeriod.Period;
|
var period = SelectedPeriod.Period;
|
||||||
var periodTag = SelectedPeriod.Label;
|
var periodTag = SelectedPeriod.Label;
|
||||||
|
var (periodFrom, periodTo) = GroupMembershipService.SchoolYearPeriod(_schoolYear, period switch
|
||||||
|
{
|
||||||
|
ParticipationPeriod.H1 => SchoolYearPeriodKind.H1,
|
||||||
|
ParticipationPeriod.H2 => SchoolYearPeriodKind.H2,
|
||||||
|
_ => SchoolYearPeriodKind.FullYear,
|
||||||
|
});
|
||||||
|
|
||||||
var students = _students.GetByGroup(_groupId);
|
var students = _students.GetByGroup(_groupId);
|
||||||
var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
||||||
var exams = _exams.GetByGroup(_groupId).Where(e => InPeriod(e.Date, period)).ToList();
|
var exams = _exams.GetByGroup(_groupId).Where(e => e.Date >= periodFrom && e.Date <= periodTo).ToList();
|
||||||
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
||||||
var allGrades = _grades.GetByGroup(_groupId).Where(g => InPeriod(g.Date, period)).ToList();
|
var allGrades = _grades.GetByGroup(_groupId).Where(g => g.Date >= periodFrom && g.Date <= periodTo).ToList();
|
||||||
|
|
||||||
Rows.Clear();
|
Rows.Clear();
|
||||||
foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||||
{
|
{
|
||||||
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
||||||
if (!StudentActiveInPeriod(membership, period)) continue;
|
if (membership is not null && !GroupMembershipService.Overlaps(membership, periodFrom, periodTo)) continue;
|
||||||
|
|
||||||
var existing = _reportGrades.GetByStudentGroupPeriod(student.Id, _groupId, periodTag);
|
var existing = _reportGrades.GetByStudentGroupPeriod(student.Id, _groupId, periodTag);
|
||||||
|
|
||||||
@@ -101,6 +109,7 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
var examGrades = new List<(string Grade, double Weight)>();
|
var examGrades = new List<(string Grade, double Weight)>();
|
||||||
foreach (var exam in exams)
|
foreach (var exam in exams)
|
||||||
{
|
{
|
||||||
|
if (membership is not null && !GroupMembershipService.IsActiveOn(membership, exam.Date)) continue;
|
||||||
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue;
|
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue;
|
||||||
if (!resultsByExam[exam.Id].TryGetValue(student.Id, out var result)) continue;
|
if (!resultsByExam[exam.Id].TryGetValue(student.Id, out var result)) continue;
|
||||||
if (result.Absent || result.Grade is null) continue;
|
if (result.Absent || result.Grade is null) continue;
|
||||||
@@ -108,10 +117,12 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
var participationGrades = allGrades
|
var participationGrades = allGrades
|
||||||
.Where(g => g.StudentId == student.Id && g.Category == GradeCategory.Participation)
|
.Where(g => g.StudentId == student.Id && g.Category == GradeCategory.Participation
|
||||||
|
&& (membership is null || GroupMembershipService.IsActiveOn(membership, g.Date)))
|
||||||
.Select(g => (g.Value, g.Weight)).ToList();
|
.Select(g => (g.Value, g.Weight)).ToList();
|
||||||
var otherGrades = allGrades
|
var otherGrades = allGrades
|
||||||
.Where(g => g.StudentId == student.Id && g.Category != GradeCategory.Participation)
|
.Where(g => g.StudentId == student.Id && g.Category != GradeCategory.Participation
|
||||||
|
&& (membership is null || GroupMembershipService.IsActiveOn(membership, g.Date)))
|
||||||
.Select(g => (g.Value, g.Weight)).ToList();
|
.Select(g => (g.Value, g.Weight)).ToList();
|
||||||
|
|
||||||
var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades,
|
var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades,
|
||||||
@@ -168,23 +179,6 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InPeriod(DateOnly date, ParticipationPeriod period) => period switch
|
|
||||||
{
|
|
||||||
ParticipationPeriod.H1 => date.Month >= 8 || date.Month <= 1,
|
|
||||||
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool StudentActiveInPeriod(GroupMembership? m, ParticipationPeriod period)
|
|
||||||
{
|
|
||||||
if (period == ParticipationPeriod.FullYear || m is null) return true;
|
|
||||||
return m.Period switch
|
|
||||||
{
|
|
||||||
MembershipPeriod.H1Only => period == ParticipationPeriod.H1,
|
|
||||||
MembershipPeriod.H2Only => period == ParticipationPeriod.H2,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rundungsregel-Anzeige ─────────────────────────────────────────────────────
|
// ── Rundungsregel-Anzeige ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public partial class GradeOverviewTabView : UserControl
|
|||||||
var dialogVm = new CollectiveGradeDialogViewModel(
|
var dialogVm = new CollectiveGradeDialogViewModel(
|
||||||
App.Services.GetRequiredService<IGradeRepository>(),
|
App.Services.GetRequiredService<IGradeRepository>(),
|
||||||
App.Services.GetRequiredService<IStudentRepository>(),
|
App.Services.GetRequiredService<IStudentRepository>(),
|
||||||
|
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||||
_vm!.GroupId);
|
_vm!.GroupId);
|
||||||
|
|
||||||
var dialog = new CollectiveGradeDialog { DataContext = dialogVm };
|
var dialog = new CollectiveGradeDialog { DataContext = dialogVm };
|
||||||
@@ -66,7 +67,7 @@ public partial class GradeOverviewTabView : UserControl
|
|||||||
App.Services.GetRequiredService<IGradingSchemeRepository>(),
|
App.Services.GetRequiredService<IGradingSchemeRepository>(),
|
||||||
App.Services.GetRequiredService<IReportGradeRepository>(),
|
App.Services.GetRequiredService<IReportGradeRepository>(),
|
||||||
App.Services.GetRequiredService<GradingService>(),
|
App.Services.GetRequiredService<GradingService>(),
|
||||||
_vm!.GroupId, _vm.GroupType, _vm.GradingSystem, _vm.GroupLabel);
|
_vm!.GroupId, _vm.GroupType, _vm.GradingSystem, _vm.GroupLabel, _vm.SchoolYear);
|
||||||
|
|
||||||
var dialog = new ReportGradeDialog { DataContext = dialogVm };
|
var dialog = new ReportGradeDialog { DataContext = dialogVm };
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
|||||||
@@ -20,9 +20,14 @@
|
|||||||
<Run Text="{Binding StudentCount}"/>
|
<Run Text="{Binding StudentCount}"/>
|
||||||
<Run Text=" Schüler"/>
|
<Run Text=" Schüler"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
<CheckBox Content="Ausgetretene anzeigen" IsChecked="{Binding ShowFormerStudents}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
<Button Content="+ Schüler" Command="{Binding AddStudentCommand}"/>
|
<Button Content="+ Schüler" Command="{Binding AddStudentCommand}"/>
|
||||||
<Button Content="− Austragen" Command="{Binding RemoveStudentCommand}"
|
<Button Content="{Binding SelectedStudent.WithdrawActionLabel}"
|
||||||
|
Command="{Binding WithdrawStudentCommand}"
|
||||||
IsVisible="{Binding SelectedStudent, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
IsVisible="{Binding SelectedStudent, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
|
<Button Content="Austragung zurücknehmen" Command="{Binding ReinstateStudentCommand}"
|
||||||
|
IsVisible="{Binding SelectedStudent.HasExitDate}"/>
|
||||||
<Button Content="+ Klausur" Command="{Binding AddExamCommand}"/>
|
<Button Content="+ Klausur" Command="{Binding AddExamCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -52,8 +57,9 @@
|
|||||||
CanUserResizeColumns="True"
|
CanUserResizeColumns="True"
|
||||||
Margin="0">
|
Margin="0">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Name" Binding="{Binding FullName}" Width="*"/>
|
<DataGridTextColumn Header="Name" Binding="{Binding FullName}" Width="*"/>
|
||||||
<DataGridTextColumn Header="Zeitraum" Binding="{Binding PeriodLabel}" Width="90"/>
|
<DataGridTextColumn Header="Zeitraum" Binding="{Binding PeriodLabel}" Width="190"/>
|
||||||
|
<DataGridTextColumn Header="Status" Binding="{Binding MembershipStatus}" Width="170"/>
|
||||||
<DataGridTemplateColumn Header="Niveau" Width="130"
|
<DataGridTemplateColumn Header="Niveau" Width="130"
|
||||||
IsVisible="{Binding IsDifferentiated}">
|
IsVisible="{Binding IsDifferentiated}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public partial class GroupDetailView : UserControl
|
|||||||
if (DataContext is GroupDetailViewModel vm)
|
if (DataContext is GroupDetailViewModel vm)
|
||||||
{
|
{
|
||||||
vm.OnAddStudent = ShowAddStudentDialog;
|
vm.OnAddStudent = ShowAddStudentDialog;
|
||||||
|
vm.OnWithdrawStudent = ShowWithdrawStudentDialog;
|
||||||
vm.OnAddExam = ShowAddExamDialog;
|
vm.OnAddExam = ShowAddExamDialog;
|
||||||
vm.OnEditExam = ShowEditExamDialog;
|
vm.OnEditExam = ShowEditExamDialog;
|
||||||
vm.OnDuplicateExam = ShowDuplicateExamDialog;
|
vm.OnDuplicateExam = ShowDuplicateExamDialog;
|
||||||
@@ -42,6 +43,20 @@ public partial class GroupDetailView : UserControl
|
|||||||
return await dialog.ShowDialog<bool>(owner);
|
return await dialog.ShowDialog<bool>(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShowWithdrawStudentDialog(StudentSummary student)
|
||||||
|
{
|
||||||
|
if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false;
|
||||||
|
var memberships = App.Services.GetRequiredService<IGroupMembershipRepository>();
|
||||||
|
var membership = memberships.GetByStudentAndGroup(student.Id, vm.Group.Id);
|
||||||
|
if (membership is null) return false;
|
||||||
|
|
||||||
|
var dialogVm = new WithdrawStudentDialogViewModel(
|
||||||
|
memberships, membership, student.FullName, vm.Group.Name);
|
||||||
|
var dialog = new WithdrawStudentDialog { DataContext = dialogVm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||||
|
}
|
||||||
|
|
||||||
private Task<bool> ShowAddExamDialog(Guid groupId) =>
|
private Task<bool> ShowAddExamDialog(Guid groupId) =>
|
||||||
ShowExamDialog(groupId, editingExam: null, duplicateSource: null);
|
ShowExamDialog(groupId, editingExam: null, duplicateSource: null);
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ public partial class ParticipationTabView : UserControl
|
|||||||
App.Services.GetRequiredService<IParticipationAspectRepository>(),
|
App.Services.GetRequiredService<IParticipationAspectRepository>(),
|
||||||
App.Services.GetRequiredService<IParticipationSectionRepository>(),
|
App.Services.GetRequiredService<IParticipationSectionRepository>(),
|
||||||
App.Services.GetRequiredService<IStudentRepository>(),
|
App.Services.GetRequiredService<IStudentRepository>(),
|
||||||
|
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||||
App.Services.GetRequiredService<IExamRepository>(),
|
App.Services.GetRequiredService<IExamRepository>(),
|
||||||
App.Services.GetRequiredService<IExamResultRepository>(),
|
App.Services.GetRequiredService<IExamResultRepository>(),
|
||||||
App.Services.GetRequiredService<IGradeRepository>(),
|
App.Services.GetRequiredService<IGradeRepository>(),
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.WithdrawStudentDialog"
|
||||||
|
x:DataType="vm:WithdrawStudentDialogViewModel"
|
||||||
|
Title="Schüler austragen"
|
||||||
|
Width="440" Height="330"
|
||||||
|
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="14">
|
||||||
|
<TextBlock Text="Schüler aus Lerngruppe austragen" Classes="dialogtitle"/>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="6" Padding="12">
|
||||||
|
<Grid ColumnDefinitions="Auto,12,*" RowDefinitions="Auto,Auto">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Schüler" Opacity="0.65"/>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding StudentName}" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Lerngruppe" Opacity="0.65"/>
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding GroupName}" FontWeight="SemiBold"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Spacing="5">
|
||||||
|
<TextBlock Text="Austrittsdatum" FontSize="12" Opacity="0.7"/>
|
||||||
|
<CalendarDatePicker SelectedDate="{Binding SelectedDate}"
|
||||||
|
DisplayDateStart="{Binding EarliestDate}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
<TextBlock Text="{Binding DateError}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding DateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="Die bisherige Gruppenzugehörigkeit und alle zugehörigen Leistungen bleiben erhalten."
|
||||||
|
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,18,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Austragen" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class WithdrawStudentDialog : Window
|
||||||
|
{
|
||||||
|
public WithdrawStudentDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnSave(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not WithdrawStudentDialogViewModel vm) return;
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.WasSaved) Close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Tests;
|
||||||
|
|
||||||
|
public class GroupMembershipServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsActiveOn_BeruecksichtigtEinUndAustrittAuchBeiGanzemJahr()
|
||||||
|
{
|
||||||
|
var membership = new GroupMembership
|
||||||
|
{
|
||||||
|
Period = MembershipPeriod.FullYear,
|
||||||
|
JoinedAt = new DateOnly(2025, 9, 1),
|
||||||
|
LeftAt = new DateOnly(2025, 10, 15),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(GroupMembershipService.IsActiveOn(membership, new DateOnly(2025, 8, 31)));
|
||||||
|
Assert.True(GroupMembershipService.IsActiveOn(membership, new DateOnly(2025, 9, 1)));
|
||||||
|
Assert.True(GroupMembershipService.IsActiveOn(membership, new DateOnly(2025, 10, 15)));
|
||||||
|
Assert.False(GroupMembershipService.IsActiveOn(membership, new DateOnly(2025, 10, 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2025, 12, 1, true)]
|
||||||
|
[InlineData(2026, 1, 31, true)]
|
||||||
|
[InlineData(2026, 2, 1, false)]
|
||||||
|
public void IsActiveOn_BeruecksichtigtHalbjahresmodus(int year, int month, int day, bool expected)
|
||||||
|
{
|
||||||
|
var membership = new GroupMembership { Period = MembershipPeriod.H1Only };
|
||||||
|
|
||||||
|
Assert.Equal(expected,
|
||||||
|
GroupMembershipService.IsActiveOn(membership, new DateOnly(year, month, day)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Overlaps_FindetTeilweiseZugehoerigkeitImHalbjahr()
|
||||||
|
{
|
||||||
|
var membership = new GroupMembership
|
||||||
|
{
|
||||||
|
Period = MembershipPeriod.Custom,
|
||||||
|
JoinedAt = new DateOnly(2025, 12, 15),
|
||||||
|
LeftAt = new DateOnly(2026, 3, 10),
|
||||||
|
};
|
||||||
|
var h1 = GroupMembershipService.SchoolYearPeriod("2025/26", SchoolYearPeriodKind.H1);
|
||||||
|
var h2 = GroupMembershipService.SchoolYearPeriod("2025/26", SchoolYearPeriodKind.H2);
|
||||||
|
|
||||||
|
Assert.True(GroupMembershipService.Overlaps(membership, h1.From, h1.To));
|
||||||
|
Assert.True(GroupMembershipService.Overlaps(membership, h2.From, h2.To));
|
||||||
|
Assert.False(GroupMembershipService.Overlaps(membership,
|
||||||
|
new DateOnly(2026, 4, 1), new DateOnly(2026, 7, 31)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -862,9 +862,17 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
|||||||
Nachweise erhalten; ihr `GroupId` wird auf `null` gesetzt.
|
Nachweise erhalten; ihr `GroupId` wird auf `null` gesetzt.
|
||||||
- [ ] **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 `GroupMembership`-Einträge.
|
neues `SchoolYear`, neue `GroupMembership`-Einträge.
|
||||||
- [ ] **7.2.3** Schüler aus einer Gruppe entfernen (`GroupMembership.LeftAt` setzen statt löschen).
|
- [x] **7.2.3** Schüler aus einer Gruppe austragen, ohne die Mitgliedschaft zu löschen — eigener
|
||||||
- [ ] **7.2.4** Umgang mit `MembershipPeriod.Custom` in allen Auswertungen prüfen
|
Dialog mit `CalendarDatePicker`, aktuellem Datum als Vorbelegung und frei wählbarem
|
||||||
(Schüler zählt nur im belegten Zeitraum).
|
Austrittsdatum. Setzt `GroupMembership.LeftAt`; Eintrittsdatum, Niveau und historische
|
||||||
|
Leistungen bleiben erhalten. Ehemalige Schüler können in der Gruppenansicht eingeblendet
|
||||||
|
und ihre Austragung zurückgenommen werden. Ein bereits gesetztes Austrittsdatum lässt sich
|
||||||
|
über denselben Dialog ändern.
|
||||||
|
- [x] **7.2.4** `JoinedAt`/`LeftAt` und `MembershipPeriod` werden über den gemeinsamen
|
||||||
|
`GroupMembershipService` in allen gruppenbezogenen Auswertungen berücksichtigt: Mitarbeit,
|
||||||
|
Mitarbeits-Assistent/-Aggregation, Klausur-Punkteeingabe, Sammelnoten, Notenübersicht und
|
||||||
|
Zeugnisnoten. Ein Schüler zählt nur an Tagen bzw. in Zeiträumen, in denen die Mitgliedschaft
|
||||||
|
tatsächlich aktiv war; der Austrittstag selbst zählt noch als zugehörig.
|
||||||
- [ ] **7.2.5** Archivansicht abgeschlossener Schuljahre (schreibgeschützt).
|
- [ ] **7.2.5** Archivansicht abgeschlossener Schuljahre (schreibgeschützt).
|
||||||
|
|
||||||
### 7.3 Import
|
### 7.3 Import
|
||||||
|
|||||||
Reference in New Issue
Block a user