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.Globalization; namespace LehrerApp.Desktop.ViewModels.Groups; // ── Notenübersicht der Gruppe (2.1) ────────────────────────────────────────── public partial class GradeOverviewTabViewModel : ObservableObject { private readonly IGradeRepository _grades; private readonly IExamRepository _exams; private readonly IExamResultRepository _results; private readonly IStudentRepository _students; private readonly IGroupMembershipRepository _memberships; private readonly GradingService _grading; private Guid _groupId; private GradingSystem _gradingSystem; private GroupType _groupType; private string _groupLabel = ""; private string _schoolYear = ""; private bool _sortByTotal; private bool _sortDescending; public Guid GroupId => _groupId; 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; [ObservableProperty] private GradeOverviewRow? _selectedRow; [ObservableProperty] private int _rebuildColumnsSignal; public bool CanTogglePointsView => _gradingSystem == GradingSystem.Points0To15; public List PeriodOptions { get; } = [ new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"), new(ParticipationPeriod.H1, "1. Halbjahr"), new(ParticipationPeriod.H2, "2. Halbjahr"), ]; public ObservableCollection Columns { get; } = []; public ObservableCollection Rows { get; } = []; public Func? OnManageStudentGrades { get; set; } public Func? OnCollectiveGrade { get; set; } public Func? OnReportGrades { get; set; } public GradeOverviewTabViewModel(IGradeRepository grades, IExamRepository exams, IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships, GradingService grading) { _grades = grades; _exams = exams; _results = results; _students = students; _memberships = memberships; _grading = grading; _selectedPeriod = PeriodOptions[0]; } 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(); } public void Refresh() => Recompute(); partial void OnSelectedPeriodChanged(ParticipationPeriodOption value) => Recompute(); partial void OnShowAsPointsChanged(bool value) => Recompute(); [RelayCommand] private void SortByName() { if (!_sortByTotal) _sortDescending = !_sortDescending; else { _sortByTotal = false; _sortDescending = false; } ApplySort(); } [RelayCommand] private void SortByTotal() { if (_sortByTotal) _sortDescending = !_sortDescending; else { _sortByTotal = true; _sortDescending = false; } ApplySort(); } [RelayCommand] private async Task ManageStudentGrades() { if (SelectedRow is null || OnManageStudentGrades is null) return; await OnManageStudentGrades(SelectedRow); Recompute(); } [RelayCommand] private async Task CollectiveGrade() { if (OnCollectiveGrade is null) return; await OnCollectiveGrade(); Recompute(); } [RelayCommand] private async Task ReportGrades() { if (OnReportGrades is null) return; await OnReportGrades(); } 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 => 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 => g.Date >= periodFrom && g.Date <= periodTo) .ToList(); var gradeColumnKeys = otherGrades .Select(g => (g.Category, Note: g.Note ?? "", g.Date)) .Distinct() .OrderBy(k => k.Date) .ToList(); Columns.Clear(); foreach (var exam in exams) Columns.Add(new GradeOverviewColumnDef($"{exam.Date:dd.MM.} {exam.Title}")); foreach (var key in gradeColumnKeys) { var header = string.IsNullOrWhiteSpace(key.Note) ? $"{GradeCategoryDisplay.Label(key.Category)} {key.Date:dd.MM.}" : key.Note; Columns.Add(new GradeOverviewColumnDef(header)); } Rows.Clear(); foreach (var student in students) { membershipsByStudent.TryGetValue(student.Id, out var membership); if (membership is not null && !GroupMembershipService.Overlaps(membership, periodFrom, periodTo)) continue; var cells = new List(); 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; } if (result.Absent) { cells.Add("abwesend"); continue; } var display = FormatValue(result.Grade); cells.Add(display); if (result.Grade is not null) numeric.Add((result.Grade, 1.0)); } 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); if (grade is null) { cells.Add(""); continue; } cells.Add(FormatValue(grade.Value)); numeric.Add((grade.Value, grade.Weight)); } var total = numeric.Count == 0 ? (double?)null : _grading.WeightedAverage(numeric); var totalDisplay = total is null ? "–" : total.Value.ToString("0.00", CultureInfo.InvariantCulture); Rows.Add(new GradeOverviewRow(student.Id, student.FullName, cells, totalDisplay, total)); } ApplySort(); RebuildColumnsSignal++; } private string FormatValue(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return ""; if (!ShowAsPoints && _gradingSystem == GradingSystem.Points0To15 && int.TryParse(raw, out var points)) return PointsNoteMapping.PointsToNote(points); return raw; } private void ApplySort() { var sorted = _sortByTotal ? Rows.OrderBy(r => r.TotalSortValue is null).ThenBy(r => r.TotalSortValue).ToList() : Rows.OrderBy(r => r.Name).ToList(); if (_sortDescending) sorted.Reverse(); for (var i = 0; i < sorted.Count; i++) Rows.Move(Rows.IndexOf(sorted[i]), i); } } public class GradeOverviewColumnDef(string header) { public string Header { get; } = header; } public class GradeOverviewRow(Guid studentId, string name, List cells, string totalDisplay, double? totalSortValue) { public Guid StudentId { get; } = studentId; public string Name { get; } = name; public List Cells { get; } = cells; public string TotalDisplay { get; } = totalDisplay; public double? TotalSortValue { get; } = totalSortValue; } // ── Kategorie-Anzeige & Punkte/Noten-Umrechnung ────────────────────────────── public static class GradeCategoryDisplay { public static string Label(GradeCategory c) => c switch { GradeCategory.Oral => "Mündlich", GradeCategory.Homework => "Hausaufgaben", GradeCategory.Participation => "Mitarbeit", GradeCategory.Project => "Projekt", GradeCategory.Other => "Sonstiges", _ => c.ToString(), }; public static string[] Options { get; } = Enum.GetValues().Select(Label).ToArray(); public static GradeCategory FromLabel(string? label) => Enum.GetValues().FirstOrDefault(c => Label(c) == label, GradeCategory.Other); } public static class PointsNoteMapping { // Grobe, standardübliche Punkte-Noten-Umrechnung (Oberstufe), nur für die Anzeige. public static string PointsToNote(int points) => points switch { >= 13 => "1", >= 10 => "2", >= 7 => "3", >= 4 => "4", >= 1 => "5", _ => "6", }; } // ── Note hinzufügen/bearbeiten für einen Schüler (2.2.1, 2.2.2) ────────────── public partial class StudentGradesDialogViewModel : ObservableObject { private readonly IGradeRepository _grades; private readonly Guid _studentId; private readonly Guid _groupId; public string StudentName { get; } public ObservableCollection Entries { get; } = []; public StudentGradesDialogViewModel(IGradeRepository grades, Guid studentId, Guid groupId, string studentName) { _grades = grades; _studentId = studentId; _groupId = groupId; StudentName = studentName; Load(); } private void Load() { Entries.Clear(); foreach (var g in _grades.GetByStudentAndGroup(_studentId, _groupId).OrderByDescending(g => g.Date)) Entries.Add(new GradeEditItem(g) { OnSave = Save, OnDelete = Delete }); } [RelayCommand] private void AddEntry() { var item = new GradeEditItem(new Grade { StudentId = _studentId, GroupId = _groupId, Category = GradeCategory.Other, Date = DateOnly.FromDateTime(DateTime.Today), }) { OnSave = Save, OnDelete = Delete, IsNew = true }; Entries.Insert(0, item); } private void Save(GradeEditItem item) { item.ValueError = ""; item.DateTextError = ""; var valid = true; if (string.IsNullOrWhiteSpace(item.Value)) { item.ValueError = "Wert darf nicht leer sein."; valid = false; } if (!DateOnly.TryParseExact(item.DateText, "dd.MM.yyyy", null, System.Globalization.DateTimeStyles.None, out _)) { item.DateTextError = "Format TT.MM.JJJJ."; valid = false; } if (!valid) return; var grade = item.ToModel(); _grades.Save(grade); item.IsNew = false; item.MarkSaved(grade.CreatedAt); } private void Delete(GradeEditItem item) { if (!item.IsNew) _grades.Delete(item.Id); Entries.Remove(item); } } public partial class GradeEditItem : ObservableObject { public Guid Id { get; } private readonly Guid _studentId; private readonly Guid _groupId; public bool IsNew { get; set; } [ObservableProperty] private GradeCategory _category; [ObservableProperty] private string _value; [ObservableProperty] private string _dateText; [ObservableProperty] private double _weight; [ObservableProperty] private string? _note; [ObservableProperty] private string _valueError = ""; [ObservableProperty] private string _dateTextError = ""; // Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens. public string CategoryName { get => GradeCategoryDisplay.Label(Category); set => Category = GradeCategoryDisplay.FromLabel(value); } public string CreatedAtDisplay { get; private set; } public Action? OnSave { get; set; } public Action? OnDelete { get; set; } public GradeEditItem(Grade g) { Id = g.Id; _studentId = g.StudentId; _groupId = g.GroupId; _category = g.Category; _value = g.Value; _dateText = g.Date.ToString("dd.MM.yyyy"); _weight = g.Weight; _note = g.Note; CreatedAtDisplay = $"erfasst am {g.CreatedAt.ToLocalTime():dd.MM.yyyy HH:mm}"; } public void MarkSaved(DateTime createdAt) => CreatedAtDisplay = $"erfasst am {createdAt.ToLocalTime():dd.MM.yyyy HH:mm}"; public Grade ToModel() => new() { Id = Id, StudentId = _studentId, GroupId = _groupId, Category = Category, Value = Value.Trim(), Date = DateOnly.ParseExact(DateText, "dd.MM.yyyy"), Weight = Weight, Note = string.IsNullOrWhiteSpace(Note) ? null : Note.Trim(), }; [RelayCommand] private void Save() => OnSave?.Invoke(this); [RelayCommand] private void Delete() => OnDelete?.Invoke(this); } // ── Sammelerfassung für die ganze Gruppe (2.2.3) ───────────────────────────── public partial class CollectiveGradeDialogViewModel : ObservableObject { private readonly IGradeRepository _grades; private readonly IGroupMembershipRepository _memberships; private readonly List _students; private readonly Guid _groupId; [ObservableProperty] private GradeCategory _category = GradeCategory.Other; [ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy"); [ObservableProperty] private double _weight = 1.0; [ObservableProperty] private string? _note; [ObservableProperty] private string _statusMessage = ""; [ObservableProperty] private string _dateTextError = ""; // Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens. public string CategoryName { get => GradeCategoryDisplay.Label(Category); set => Category = GradeCategoryDisplay.FromLabel(value); } public ObservableCollection Rows { get; } = []; public CollectiveGradeDialogViewModel(IGradeRepository grades, IStudentRepository students, IGroupMembershipRepository memberships, Guid groupId) { _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] private void SaveAll() { if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, System.Globalization.DateTimeStyles.None, out var date)) { DateTextError = "Format TT.MM.JJJJ."; return; } DateTextError = ""; var count = 0; foreach (var row in Rows) { if (string.IsNullOrWhiteSpace(row.Value)) continue; _grades.Save(new Grade { StudentId = row.StudentId, GroupId = _groupId, Category = Category, Value = row.Value.Trim(), Date = date, Weight = Weight, Note = string.IsNullOrWhiteSpace(Note) ? null : Note.Trim(), }); count++; } StatusMessage = count == 0 ? "Keine Werte eingegeben." : $"{count} Note(n) gespeichert."; } } public partial class CollectiveGradeStudentRow(Guid studentId, string name) : ObservableObject { public Guid StudentId { get; } = studentId; public string Name { get; } = name; [ObservableProperty] private string _value = ""; }