Neue Funktion Schüler austragen statt aus Kurs löschen.
This commit is contained in:
@@ -37,7 +37,7 @@ public partial class ExamGradingDialogViewModel : ObservableObject
|
||||
foreach (var s in enrolled.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||
{
|
||||
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-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 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 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -23,6 +23,7 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
||||
private GradingSystem _gradingSystem;
|
||||
private GroupType _groupType;
|
||||
private string _groupLabel = "";
|
||||
private string _schoolYear = "";
|
||||
private bool _sortByTotal;
|
||||
private bool _sortDescending;
|
||||
|
||||
@@ -30,6 +31,7 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
||||
public GradingSystem GradingSystem => _gradingSystem;
|
||||
public GroupType GroupType => _groupType;
|
||||
public string GroupLabel => _groupLabel;
|
||||
public string SchoolYear => _schoolYear;
|
||||
|
||||
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
||||
[ObservableProperty] private bool _showAsPoints = true;
|
||||
@@ -61,12 +63,14 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
||||
_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;
|
||||
_gradingSystem = gradingSystem;
|
||||
_groupType = groupType;
|
||||
_groupLabel = groupLabel;
|
||||
_schoolYear = schoolYear;
|
||||
ShowAsPoints = gradingSystem == GradingSystem.Points0To15;
|
||||
OnPropertyChanged(nameof(CanTogglePointsView));
|
||||
Recompute();
|
||||
@@ -119,18 +123,24 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
||||
private void Recompute()
|
||||
{
|
||||
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 membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
||||
|
||||
var exams = _exams.GetByGroup(_groupId)
|
||||
.Where(e => InPeriod(e.Date, period))
|
||||
.Where(e => e.Date >= periodFrom && e.Date <= periodTo)
|
||||
.OrderBy(e => e.Date)
|
||||
.ToList();
|
||||
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
||||
|
||||
var otherGrades = _grades.GetByGroup(_groupId)
|
||||
.Where(g => InPeriod(g.Date, period))
|
||||
.Where(g => g.Date >= periodFrom && g.Date <= periodTo)
|
||||
.ToList();
|
||||
var gradeColumnKeys = otherGrades
|
||||
.Select(g => (g.Category, Note: g.Note ?? "", g.Date))
|
||||
@@ -153,13 +163,15 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
||||
foreach (var student in students)
|
||||
{
|
||||
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 numeric = new List<(string Grade, double Weight)>();
|
||||
|
||||
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; }
|
||||
resultsByExam[exam.Id].TryGetValue(student.Id, out var result);
|
||||
if (result is null) { cells.Add(""); continue; }
|
||||
@@ -171,6 +183,8 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
||||
|
||||
foreach (var key in gradeColumnKeys)
|
||||
{
|
||||
if (membership is not null && !GroupMembershipService.IsActiveOn(membership, key.Date))
|
||||
{ cells.Add(""); continue; }
|
||||
var grade = otherGrades.FirstOrDefault(g =>
|
||||
g.StudentId == student.Id && g.Category == key.Category &&
|
||||
(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);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -404,6 +401,8 @@ public partial class GradeEditItem : ObservableObject
|
||||
public partial class CollectiveGradeDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly IGroupMembershipRepository _memberships;
|
||||
private readonly List<Student> _students;
|
||||
private readonly Guid _groupId;
|
||||
|
||||
[ObservableProperty] private GradeCategory _category = GradeCategory.Other;
|
||||
@@ -422,11 +421,36 @@ public partial class CollectiveGradeDialogViewModel : ObservableObject
|
||||
|
||||
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;
|
||||
foreach (var s in students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||
Rows.Add(new CollectiveGradeStudentRow(s.Id, s.FullName));
|
||||
_grades = grades; _memberships = memberships; _groupId = groupId;
|
||||
_students = students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToList();
|
||||
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]
|
||||
|
||||
@@ -169,6 +169,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
[ObservableProperty] private int _activeTabIndex = 0;
|
||||
[ObservableProperty] private StudentSummary? _selectedStudent;
|
||||
[ObservableProperty] private ExamSummary? _selectedExam;
|
||||
[ObservableProperty] private bool _showFormerStudents;
|
||||
|
||||
// Eigene Property statt "Group.IsDifferentiated" im Binding-Pfad: die View bindet bereits,
|
||||
// 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; } = "";
|
||||
|
||||
partial void OnGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(IsDifferentiated));
|
||||
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
|
||||
|
||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||
@@ -185,6 +187,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
||||
public PlanningTabViewModel PlanningTab { get; }
|
||||
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<Exam, Task<bool>>? OnEditExam { get; set; }
|
||||
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
|
||||
@@ -219,7 +222,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
LoadStudents();
|
||||
ReloadExams();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -238,11 +241,14 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
Students.Clear();
|
||||
var enrolled = _students.GetByGroup(Group.Id);
|
||||
var memberships = _memberships.GetByGroup(Group.Id).ToDictionary(e => e.StudentId);
|
||||
StudentCount = enrolled.Count;
|
||||
foreach (var s in enrolled)
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -269,21 +275,36 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedStudent))]
|
||||
private void RemoveStudent()
|
||||
private async Task WithdrawStudent()
|
||||
{
|
||||
if (Group is null || SelectedStudent is null) return;
|
||||
var membership = _memberships.GetByStudentAndGroup(SelectedStudent.Id, Group.Id);
|
||||
if (membership is null) return;
|
||||
_memberships.Delete(membership.Id);
|
||||
if (SelectedStudent is null || OnWithdrawStudent is null) return;
|
||||
if (!await OnWithdrawStudent(SelectedStudent)) return;
|
||||
LoadStudents();
|
||||
SelectedStudent = null;
|
||||
ParticipationTab.RefreshCurrentGrid();
|
||||
}
|
||||
|
||||
partial void OnSelectedStudentChanged(StudentSummary? value) =>
|
||||
RemoveStudentCommand.NotifyCanExecuteChanged();
|
||||
[RelayCommand(CanExecute = nameof(CanReinstateSelectedStudent))]
|
||||
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 CanReinstateSelectedStudent() => SelectedStudent?.HasExitDate == true;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddExam()
|
||||
@@ -423,6 +444,10 @@ public partial class StudentSummary : ObservableObject
|
||||
public Guid Id { get; }
|
||||
public string FullName { 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;
|
||||
|
||||
@@ -434,17 +459,17 @@ public partial class StudentSummary : ObservableObject
|
||||
|
||||
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;
|
||||
FullName = s.FullName;
|
||||
PeriodLabel = membership?.Period switch
|
||||
{
|
||||
MembershipPeriod.H1Only => "H1",
|
||||
MembershipPeriod.H2Only => "H2",
|
||||
MembershipPeriod.Custom => BuildCustomLabel(membership),
|
||||
_ => "",
|
||||
};
|
||||
PeriodLabel = membership is null ? "" : BuildPeriodLabel(membership);
|
||||
HasExitDate = membership?.LeftAt.HasValue == true;
|
||||
IsFormer = membership?.LeftAt is { } leftAt
|
||||
&& leftAt < (today ?? DateOnly.FromDateTime(DateTime.Today));
|
||||
MembershipStatus = membership?.LeftAt is { } exitDate
|
||||
? IsFormer ? $"Ausgetreten am {exitDate:dd.MM.yyyy}" : $"Austritt am {exitDate:dd.MM.yyyy}"
|
||||
: "Aktiv";
|
||||
_niveau = membership?.Niveau;
|
||||
}
|
||||
|
||||
@@ -454,12 +479,20 @@ public partial class StudentSummary : ObservableObject
|
||||
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.}";
|
||||
if (membership.JoinedAt.HasValue) return $"ab {membership.JoinedAt:dd.MM.}";
|
||||
if (membership.LeftAt.HasValue) return $"bis {membership.LeftAt:dd.MM.}";
|
||||
return "Datum";
|
||||
var period = membership.Period switch
|
||||
{
|
||||
MembershipPeriod.H1Only => "H1",
|
||||
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; }
|
||||
}
|
||||
|
||||
// ── 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 ──────────────────────────────────────────
|
||||
|
||||
public partial class AddGroupDialogViewModel : ObservableObject
|
||||
|
||||
@@ -90,7 +90,7 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
||||
{
|
||||
var membership = membershipList.FirstOrDefault(e => e.StudentId == student.Id);
|
||||
var relevantSessions = sessions
|
||||
.Where(s => membership is null || IsMemberAtDate(membership, s.Date))
|
||||
.Where(s => membership is null || GroupMembershipService.IsActiveOn(membership, s.Date))
|
||||
.ToList();
|
||||
|
||||
var points = new List<(DateOnly Date, double Rating)>();
|
||||
@@ -152,14 +152,6 @@ public partial class ParticipationGradeDialogViewModel : ObservableObject
|
||||
_ => 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) ─────────────────────────────────────────
|
||||
|
||||
@@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -140,7 +141,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
foreach (var s in students)
|
||||
{
|
||||
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;
|
||||
|
||||
var entry = entries.FirstOrDefault(e => e.StudentId == s.Id)
|
||||
@@ -157,15 +158,6 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
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)
|
||||
{
|
||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||
|
||||
@@ -20,6 +20,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IExamResultRepository _results;
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly Dictionary<Guid, GroupMembership> _membershipsByStudent;
|
||||
private readonly GradingService _grading;
|
||||
private readonly Guid _groupId;
|
||||
private readonly string _schoolYear;
|
||||
@@ -69,7 +70,8 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
public ParticipationWizardDialogViewModel(
|
||||
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
||||
IParticipationAspectRepository aspects, IParticipationSectionRepository sectionRepo,
|
||||
IStudentRepository students, IExamRepository exams, IExamResultRepository results,
|
||||
IStudentRepository students, IGroupMembershipRepository memberships,
|
||||
IExamRepository exams, IExamResultRepository results,
|
||||
IGradeRepository grades, GradingService grading,
|
||||
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;
|
||||
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
||||
GroupLabel = groupLabel;
|
||||
_membershipsByStudent = memberships.GetByGroup(groupId).ToDictionary(m => m.StudentId);
|
||||
|
||||
_aspectWeights = aspects.GetDefaults()
|
||||
.Concat(aspects.GetByGroup(groupId))
|
||||
.GroupBy(a => a.Key)
|
||||
.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();
|
||||
_sectionList = sectionRepo.GetByGroup(groupId).OrderBy(s => s.StartDate).ToList();
|
||||
|
||||
@@ -123,17 +130,20 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
|
||||
foreach (var session in _allSessions)
|
||||
{
|
||||
if (!IsStudentActiveOn(studentId, session.Date)) continue;
|
||||
var entry = _entries.GetBySessionAndStudent(session.Id, studentId);
|
||||
points.Add((session.Date, BuildSessionPoint(session, entry, studentId)));
|
||||
}
|
||||
foreach (var exam in _exams.GetByGroup(_groupId).OrderBy(e => e.Date))
|
||||
{
|
||||
if (!IsStudentActiveOn(studentId, exam.Date)) continue;
|
||||
var result = _results.GetByExamAndStudent(exam.Id, studentId);
|
||||
if (result is null) continue;
|
||||
points.Add((exam.Date, BuildExamPoint(exam, result)));
|
||||
}
|
||||
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)));
|
||||
}
|
||||
@@ -269,6 +279,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
|
||||
foreach (var section in _sectionList)
|
||||
{
|
||||
if (!StudentOverlaps(studentId, section.StartDate, section.EndDate)) continue;
|
||||
var grade = studentGrades.FirstOrDefault(g => g.Note == AbschnittPrefix + section.Label);
|
||||
var row = new WizardSectionRow(section.Label, section.StartDate, section.EndDate,
|
||||
grade?.Value ?? "", isOpen: false);
|
||||
@@ -285,7 +296,8 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
private string? ComputeSuggestion(Guid studentId, DateOnly start, DateOnly end)
|
||||
{
|
||||
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);
|
||||
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)
|
||||
.Where(g => g.Category == GradeCategory.Participation && g.Note is not null && g.Note.StartsWith(AbschnittPrefix))
|
||||
.Where(g => InPeriod(g.Date, RollupPeriod.Period))
|
||||
.Where(g => IsStudentActiveOn(student.Id, g.Date))
|
||||
.Select(g => (g.Value, g.Weight))
|
||||
.ToList();
|
||||
if (sectionGrades.Count == 0) continue;
|
||||
@@ -401,6 +414,14 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
||||
_ => 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 ────────────────────────────────────────────────────
|
||||
|
||||
@@ -25,6 +25,7 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
||||
private readonly GroupType _groupType;
|
||||
private readonly GradingSystem _gradingSystem;
|
||||
private readonly string _groupLabel;
|
||||
private readonly string _schoolYear;
|
||||
|
||||
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
||||
[ObservableProperty] private RoundingRule _roundingRule = RoundingRule.Commercial;
|
||||
@@ -51,11 +52,12 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
||||
public ReportGradeDialogViewModel(IGradeRepository grades, IExamRepository exams,
|
||||
IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships,
|
||||
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;
|
||||
_memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading;
|
||||
_groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel;
|
||||
_schoolYear = schoolYear;
|
||||
|
||||
_selectedPeriod = PeriodOptions[0];
|
||||
Recompute();
|
||||
@@ -77,18 +79,24 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
||||
|
||||
var period = SelectedPeriod.Period;
|
||||
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 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 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();
|
||||
foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -101,6 +109,7 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
||||
var examGrades = new List<(string Grade, double Weight)>();
|
||||
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 (!resultsByExam[exam.Id].TryGetValue(student.Id, out var result)) continue;
|
||||
if (result.Absent || result.Grade is null) continue;
|
||||
@@ -108,10 +117,12 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
|
||||
var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades,
|
||||
@@ -168,23 +179,6 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user