using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using System.Collections.ObjectModel; using System.Collections.Generic; namespace LehrerApp.Desktop.ViewModels.Groups; // ── Tab-ViewModel ───────────────────────────────────────────────────────────── public partial class ParticipationTabViewModel : ObservableObject { private readonly IParticipationSessionRepository _sessions; private readonly IParticipationRepository _entries; private readonly IParticipationAspectRepository _aspects; private readonly IStudentRepository _students; private readonly IGroupMembershipRepository _memberships; private readonly IGroupRepository _groups; private readonly ICompetencyDomainRepository _competencyDomains; private Guid _groupId; private string _schoolYear = ""; private Guid? _subjectId; private int _gradeLevel; private GradingSystem _gradingSystem; public Guid GroupId => _groupId; public string SchoolYear => _schoolYear; public GradingSystem GradingSystem => _gradingSystem; public string GroupLabel { get; private set; } = ""; [ObservableProperty] private ParticipationSessionItem? _selectedSession; [ObservableProperty] private string _noDataText = "Keine Bewertungssitzung ausgewählt."; [ObservableProperty] private bool _competencyTagsVisible; [ObservableProperty] private bool _hasCompetencyCatalog; [ObservableProperty] private bool _studentCompetencyRatingsVisible; [ObservableProperty] private int _rebuildColumnsSignal; public string SelectedSessionDisplay => SelectedSession?.Display ?? ""; public List ActiveCompetencyCodes { get; private set; } = []; public ObservableCollection Sessions { get; } = []; public ObservableCollection StudentRows { get; } = []; public ObservableCollection Aspects { get; } = []; public ObservableCollection CompetencyTagGroups { get; } = []; public Func>? OnAddSession { get; set; } public Func? OnQuickInput { get; set; } public Func? OnStatusQuickInput { get; set; } public Func? OnComputeGrade { get; set; } public Func? OnOpenWizard { get; set; } public ParticipationTabViewModel( IParticipationSessionRepository sessions, IParticipationRepository entries, IParticipationAspectRepository aspects, IStudentRepository students, IGroupMembershipRepository memberships, IGroupRepository groups, ICompetencyDomainRepository competencyDomains) { _sessions = sessions; _entries = entries; _aspects = aspects; _students = students; _memberships = memberships; _groups = groups; _competencyDomains = competencyDomains; } public void Initialize(Guid groupId, string schoolYear) { _groupId = groupId; _schoolYear = schoolYear; var group = _groups.GetById(groupId); _subjectId = group?.SubjectId; _gradeLevel = group?.GradeLevel ?? 0; _gradingSystem = group?.GradingSystem ?? GradingSystem.Grades1To6; GroupLabel = group?.Name ?? ""; HasCompetencyCatalog = _subjectId.HasValue && _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel).Count > 0; LoadAspects(); LoadSessions(); } private void LoadAspects() { Aspects.Clear(); var defaults = _aspects.GetDefaults(); var specific = _aspects.GetByGroup(_groupId); var all = defaults.Concat(specific).ToList(); if (!all.Any()) { foreach (var a in DefaultParticipationAspects.All) Aspects.Add(new AspectColumnDef(a)); } else { foreach (var a in all) Aspects.Add(new AspectColumnDef(a)); } } public void LoadSessions() { Sessions.Clear(); foreach (var s in _sessions.GetByGroup(_groupId)) Sessions.Add(new ParticipationSessionItem(s)); if (SelectedSession is null && Sessions.Any()) SelectedSession = Sessions[0]; } partial void OnSelectedSessionChanged(ParticipationSessionItem? value) { OnPropertyChanged(nameof(SelectedSessionDisplay)); if (value is null) { StudentRows.Clear(); CompetencyTagGroups.Clear(); ActiveCompetencyCodes = []; QuickInputCommand.NotifyCanExecuteChanged(); StatusQuickInputCommand.NotifyCanExecuteChanged(); RebuildColumnsSignal++; return; } LoadCompetencyTags(value.Id); // sets ActiveCompetencyCodes first LoadGrid(value.Id); // uses ActiveCompetencyCodes, fires RebuildColumnsSignal++ } private void LoadGrid(Guid sessionId) { StudentRows.Clear(); var session = _sessions.GetById(sessionId); var sessionDate = session?.Date ?? DateOnly.FromDateTime(DateTime.Today); var students = _students.GetByGroup(_groupId); var memberships = _memberships.GetByGroup(_groupId); var entries = _entries.GetBySession(sessionId); foreach (var s in students) { var membership = memberships.FirstOrDefault(e => e.StudentId == s.Id); if (membership is not null && !IsMemberAtDate(membership, sessionDate)) continue; var entry = entries.FirstOrDefault(e => e.StudentId == s.Id) ?? new ParticipationEntry { SessionId = sessionId, StudentId = s.Id }; var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList(), ActiveCompetencyCodes); row.OnRatingChanged = (sid, key, val) => SaveRating(sessionId, sid, key, val); row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val); row.HomeworkChangedCallback = (sid, val) => SaveHomework(sessionId, sid, val); row.AttendanceChangedCallback = (sid, val) => SaveAttendance(sessionId, sid, val); StudentRows.Add(row); } QuickInputCommand.NotifyCanExecuteChanged(); StatusQuickInputCommand.NotifyCanExecuteChanged(); 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) ?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId, }; var existing = entry.Ratings.FirstOrDefault(r => r.Key == key); if (value is null) { if (existing is not null) entry.Ratings.Remove(existing); } else { if (existing is null) entry.Ratings.Add(new AspectRating { Key = key, Value = value.Value }); else existing.Value = value.Value; } _entries.Save(entry); } public void RefreshCurrentGrid() { if (SelectedSession is not null) LoadGrid(SelectedSession.Id); } private void SaveHomework(Guid sessionId, Guid studentId, HomeworkStatus? value) { var entry = _entries.GetBySessionAndStudent(sessionId, studentId) ?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId }; entry.Homework = value; entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(value); _entries.Save(entry); } private void SaveAttendance(Guid sessionId, Guid studentId, AttendanceStatus? value) { var entry = _entries.GetBySessionAndStudent(sessionId, studentId) ?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId }; entry.Attendance = value; _entries.Save(entry); } [RelayCommand] private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible; partial void OnStudentCompetencyRatingsVisibleChanged(bool value) => RebuildColumnsSignal++; private void LoadCompetencyTags(Guid sessionId) { CompetencyTagGroups.Clear(); var session = _sessions.GetById(sessionId); ActiveCompetencyCodes = session?.CompetencyCodes?.ToList() ?? []; if (!_subjectId.HasValue) return; var active = ActiveCompetencyCodes.ToHashSet(); foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel)) { var group = new CompetencyTagGroup(domain.Name, domain.Code); foreach (var item in domain.Items.OrderBy(i => i.SortOrder)) { var tag = new CompetencyTag(item.Code, item.Description, active.Contains(item.Code)); tag.OnChanged = (code, sel) => OnTagToggled(code, sel); group.Items.Add(tag); } if (group.Items.Count > 0) CompetencyTagGroups.Add(group); } } private void OnTagToggled(string code, bool selected) { if (SelectedSession is null) return; var session = _sessions.GetById(SelectedSession.Id); if (session is null) return; if (selected) { if (!session.CompetencyCodes.Contains(code)) session.CompetencyCodes.Add(code); } else { session.CompetencyCodes.Remove(code); } _sessions.Save(session); ActiveCompetencyCodes = session.CompetencyCodes.ToList(); if (StudentCompetencyRatingsVisible) LoadGrid(SelectedSession.Id); // reloads rows with updated cells, fires RebuildColumnsSignal++ } [RelayCommand] private async Task AddSession() { if (OnAddSession is null) return; var session = await OnAddSession(); if (session is null) return; session.GroupId = _groupId; _sessions.Save(session); Sessions.Clear(); foreach (var s in _sessions.GetByGroup(_groupId)) Sessions.Add(new ParticipationSessionItem(s)); SelectedSession = Sessions.FirstOrDefault(s => s.Id == session.Id); } [RelayCommand(CanExecute = nameof(CanQuickInput))] private async Task QuickInput() { if (OnQuickInput is null) return; await OnQuickInput(this); if (SelectedSession is not null) LoadGrid(SelectedSession.Id); } private bool CanQuickInput() => SelectedSession is not null && StudentRows.Count > 0; [RelayCommand(CanExecute = nameof(CanQuickInput))] private async Task StatusQuickInput() { if (OnStatusQuickInput is null) return; await OnStatusQuickInput(this); if (SelectedSession is not null) LoadGrid(SelectedSession.Id); } [RelayCommand] private async Task ComputeGrade() { if (OnComputeGrade is null) return; await OnComputeGrade(this); } [RelayCommand] private async Task OpenWizard() { if (OnOpenWizard is null) return; await OnOpenWizard(this); } [RelayCommand] private void DeleteSession() { if (SelectedSession is null) return; _sessions.Delete(SelectedSession.Id); LoadSessions(); } public void SaveNote(Guid sessionId, Guid studentId, string note) { var entry = _entries.GetBySessionAndStudent(sessionId, studentId); if (entry is null) return; entry.Note = note; _entries.Save(entry); } private void SaveCompetencyRating(Guid sessionId, Guid studentId, string code, int? value) { var entry = _entries.GetBySessionAndStudent(sessionId, studentId) ?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId, }; var existing = entry.CompetencyRatings.FirstOrDefault(r => r.Code == code); if (value is null) { if (existing is not null) entry.CompetencyRatings.Remove(existing); } else { if (existing is null) entry.CompetencyRatings.Add(new CompetencyRating { Code = code, Value = value.Value }); else existing.Value = value.Value; } _entries.Save(entry); } } // ── Zeilendaten für das Bewertungsraster ───────────────────────────────────── public partial class ParticipationStudentRow : ObservableObject { public Guid StudentId { get; } public string Name { get; } private readonly ParticipationEntry _entry; private readonly IReadOnlyList _aspectDefs; public ObservableCollection Cells { get; } = []; public ObservableCollection CompetencyCells { get; } = []; [ObservableProperty] private HomeworkStatus? _homework; [ObservableProperty] private AttendanceStatus? _attendance; public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance); public string AttendanceTooltip => AttendanceDisplay.Label(Attendance); public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework); public string HomeworkTooltip => HomeworkDisplay.Label(Homework); public Action? OnRatingChanged { get; set; } public Action? OnCompetencyRatingChanged { get; set; } public Action? HomeworkChangedCallback { get; set; } public Action? AttendanceChangedCallback { get; set; } public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry, List aspects, List competencyCodes) { StudentId = id; Name = name; _entry = entry; _aspectDefs = aspects; _homework = HomeworkDisplay.Effective(entry); _attendance = entry.Attendance; foreach (var a in aspects) { var existing = entry.Ratings.FirstOrDefault(r => r.Key == a.Key); var cell = new RatingCell(id, a.Key, existing?.Value); cell.OnChanged = (sid, key, val) => OnRatingChanged?.Invoke(sid, key, val); Cells.Add(cell); } foreach (var code in competencyCodes) { var existing = entry.CompetencyRatings.FirstOrDefault(r => r.Code == code); var cell = new RatingCell(id, code, existing?.Value); cell.OnChanged = (sid, key, val) => OnCompetencyRatingChanged?.Invoke(sid, key, val); CompetencyCells.Add(cell); } } public int? GetRating(string key) => _entry.Ratings.FirstOrDefault(r => r.Key == key)?.Value; public void SetRating(string key, int? value) { var cell = Cells.FirstOrDefault(c => c.AspectKey == key); cell?.SetValue(value); OnRatingChanged?.Invoke(StudentId, key, value); } [RelayCommand] private void ToggleHomework() { Homework = HomeworkDisplay.Next(Homework); OnPropertyChanged(nameof(HomeworkSymbol)); OnPropertyChanged(nameof(HomeworkTooltip)); HomeworkChangedCallback?.Invoke(StudentId, Homework); } public void SetHomework(HomeworkStatus? value) { Homework = value; OnPropertyChanged(nameof(HomeworkSymbol)); OnPropertyChanged(nameof(HomeworkTooltip)); HomeworkChangedCallback?.Invoke(StudentId, value); } [RelayCommand] private void CycleAttendance() { Attendance = Attendance switch { null => AttendanceStatus.Present, AttendanceStatus.Present => AttendanceStatus.ExcusePending, AttendanceStatus.ExcusePending => AttendanceStatus.Excused, AttendanceStatus.Excused => AttendanceStatus.Unexcused, AttendanceStatus.Unexcused => AttendanceStatus.Truant, AttendanceStatus.Truant => AttendanceStatus.OtherSchoolEvent, AttendanceStatus.OtherSchoolEvent => null, _ => null, }; OnPropertyChanged(nameof(AttendanceLabel)); OnPropertyChanged(nameof(AttendanceTooltip)); AttendanceChangedCallback?.Invoke(StudentId, Attendance); } // Direktes Setzen (z.B. aus dem Grading-Wizard heraus), ohne den Zyklus zu durchlaufen. public void SetAttendance(AttendanceStatus? value) { Attendance = value; OnPropertyChanged(nameof(AttendanceLabel)); OnPropertyChanged(nameof(AttendanceTooltip)); AttendanceChangedCallback?.Invoke(StudentId, value); } } // ── Hausaufgaben-Anzeige ───────────────────────────────────────────────────── public static class HomeworkDisplay { public static HomeworkStatus? Effective(ParticipationEntry entry) => entry.Homework ?? (entry.HomeworkMissing ? HomeworkStatus.MissingOpen : null); public static HomeworkStatus? Next(HomeworkStatus? status) => status switch { null => HomeworkStatus.Completed, HomeworkStatus.Completed => HomeworkStatus.PartiallyCompleted, HomeworkStatus.PartiallyCompleted => HomeworkStatus.PartialSubmittedLate, HomeworkStatus.PartialSubmittedLate => HomeworkStatus.PartialMissingOverdue, HomeworkStatus.PartialMissingOverdue => HomeworkStatus.MissingOpen, HomeworkStatus.MissingOpen => HomeworkStatus.MissingOverdue, HomeworkStatus.MissingOverdue => HomeworkStatus.SubmittedLate, HomeworkStatus.SubmittedLate => null, _ => null, }; public static string Label(HomeworkStatus? status) => status switch { null => "Keine Hausaufgabe aufgegeben", HomeworkStatus.Completed => "Hausaufgabe gemacht", HomeworkStatus.PartiallyCompleted => "Teilweise angefertigt – Rest offen", HomeworkStatus.PartialSubmittedLate => "Teilweise angefertigt – Rest nachgereicht", HomeworkStatus.PartialMissingOverdue => "Teilweise angefertigt – Rest nicht nachgereicht", HomeworkStatus.MissingOpen => "Nicht gemacht – Nachreichen offen", HomeworkStatus.MissingOverdue => "Nicht gemacht – nicht mehr nachgereicht", HomeworkStatus.SubmittedLate => "Hausaufgabe nachgereicht", _ => "Keine Hausaufgabe aufgegeben", }; public static string Symbol(HomeworkStatus? status) => status switch { null => "·", HomeworkStatus.Completed => "✓", HomeworkStatus.PartiallyCompleted => "◐", HomeworkStatus.PartialSubmittedLate => "◕", HomeworkStatus.PartialMissingOverdue => "◒", HomeworkStatus.MissingOpen => "!", HomeworkStatus.MissingOverdue => "✕", HomeworkStatus.SubmittedLate => "↺", _ => "·", }; public static string Color(HomeworkStatus? status) => status switch { HomeworkStatus.Completed => "#2E9D57", HomeworkStatus.PartiallyCompleted => "#D98200", HomeworkStatus.PartialSubmittedLate => "#7F77DD", HomeworkStatus.PartialMissingOverdue => "#D64545", HomeworkStatus.MissingOpen => "#D98200", HomeworkStatus.MissingOverdue => "#D64545", HomeworkStatus.SubmittedLate => "#7F77DD", _ => "", }; public static bool CountsAsMissing(HomeworkStatus? status) => status is HomeworkStatus.MissingOpen or HomeworkStatus.MissingOverdue or HomeworkStatus.PartiallyCompleted or HomeworkStatus.PartialMissingOverdue; } // ── Anwesenheits-Anzeige ────────────────────────────────────────────────────── public static class AttendanceDisplay { public static string Label(AttendanceStatus? s) => s switch { null => "Noch nicht kontrolliert", AttendanceStatus.Present => "Anwesend", AttendanceStatus.ExcusePending => "Krank (Entschuldigung offen)", AttendanceStatus.Excused => "Krank, entschuldigt", AttendanceStatus.Unexcused => "Krank, unentschuldigt", AttendanceStatus.Truant => "Geschwänzt", AttendanceStatus.OtherSchoolEvent => "Andere Schulveranstaltung", _ => "Anwesend", }; public static string ShortLabel(AttendanceStatus? s) => s switch { null => "", AttendanceStatus.Present => "✓", AttendanceStatus.ExcusePending => "?", AttendanceStatus.Excused => "⊘", AttendanceStatus.Unexcused => "!", AttendanceStatus.Truant => "✕", AttendanceStatus.OtherSchoolEvent => "◇", _ => "", }; public static string Color(AttendanceStatus? s) => s switch { AttendanceStatus.Present => "#2E9D57", AttendanceStatus.ExcusePending => "#D98200", AttendanceStatus.Excused => "#4C86A8", AttendanceStatus.Unexcused => "#D96C00", AttendanceStatus.Truant => "#D64545", AttendanceStatus.OtherSchoolEvent => "#5277C3", _ => "", }; } // ── Eine Bewertungszelle ────────────────────────────────────────────────────── public partial class RatingCell : ObservableObject { public Guid StudentId { get; } public string AspectKey { get; } [ObservableProperty] private int? _value; [ObservableProperty] private string _displayLabel = ""; public Action? OnChanged { get; set; } public RatingCell(Guid studentId, string key, int? value) { StudentId = studentId; AspectKey = key; _value = value; UpdateLabel(); } public void SetValue(int? value) { Value = value; UpdateLabel(); OnChanged?.Invoke(StudentId, AspectKey, value); } [RelayCommand] private void CycleUp() { var next = Value is null ? -2 : Math.Min(2, Value.Value + 1); SetValue(next); } [RelayCommand] private void CycleDown() { var next = Value is null ? 2 : Math.Max(-2, Value.Value - 1); SetValue(next); } [RelayCommand] private void Clear() => SetValue(null); private void UpdateLabel() => DisplayLabel = Value switch { 2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "", }; } // ── Kompetenz-Tags ──────────────────────────────────────────────────────────── public class CompetencyTagGroup(string name, string code) { public string Name { get; } = name; public string Code { get; } = code; public string DisplayName { get; } = string.IsNullOrEmpty(code) ? name : $"{name} ({code})"; public List Items { get; } = []; } public partial class CompetencyTag : ObservableObject { public string Code { get; } public string Description { get; } public string Display { get; } [ObservableProperty] private bool _isSelected; public Action? OnChanged { get; set; } public CompetencyTag(string code, string description, bool isSelected) { Code = code; Description = description; Display = string.IsNullOrEmpty(code) ? description : $"[{code}] {description}"; _isSelected = isSelected; } partial void OnIsSelectedChanged(bool value) => OnChanged?.Invoke(Code, value); } // ── Hilfsklassen ────────────────────────────────────────────────────────────── public class AspectColumnDef { public string Key { get; } public string Label { get; } public AspectColumnDef(ParticipationAspect a) { Key = a.Key; Label = a.Label; } } public class ParticipationSessionItem { public Guid Id { get; } public string Display { get; } public string Comment { get; } public DateOnly Date { get; } public ParticipationSessionItem(ParticipationSession s) { Id = s.Id; Date = s.Date; Comment = s.Comment ?? ""; Display = s.Comment is { Length: > 0 } ? $"{s.Date:dd.MM.yyyy} – {s.Comment}" : s.Date.ToString("dd.MM.yyyy"); } } // ── Dialog: Sitzung anlegen ─────────────────────────────────────────────────── public partial class AddSessionDialogViewModel : ObservableObject { [ObservableProperty] private DateOnly _date = DateOnly.FromDateTime(DateTime.Today); [ObservableProperty] private string _comment = ""; [ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy"); [ObservableProperty] private string _dateTextError = ""; public ParticipationSession? Result { get; private set; } [RelayCommand] private void Save() { if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, System.Globalization.DateTimeStyles.None, out var date)) { DateTextError = "Format TT.MM.JJJJ."; return; } DateTextError = ""; Result = new ParticipationSession { Date = date, Comment = Comment.Trim() }; } } // ── Dialog: Schnelleingabe ──────────────────────────────────────────────────── public partial class QuickInputViewModel : ObservableObject { private readonly List _rows; private readonly List _aspects; [ObservableProperty] private int _studentIndex; [ObservableProperty] private int _aspectIndex; [ObservableProperty] private string _studentName = ""; [ObservableProperty] private string _currentAspectLabel = ""; [ObservableProperty] private string _currentValueLabel = ""; [ObservableProperty] private string _progressText = ""; public ObservableCollection AspectRows { get; } = []; public QuickInputViewModel(List rows, List aspects) { _rows = rows; _aspects = aspects; if (rows.Any()) ShowStudent(0); } private void ShowStudent(int index) { if (index < 0 || index >= _rows.Count) return; StudentIndex = index; var row = _rows[index]; StudentName = row.Name; ProgressText = $"{index + 1} / {_rows.Count}"; AspectRows.Clear(); foreach (var (a, i) in _aspects.Select((a, i) => (a, i))) { var val = row.GetRating(a.Key); AspectRows.Add(new QuickAspectRow(i, a.Label, val, i == AspectIndex)); } UpdateCurrentAspect(); } private void UpdateCurrentAspect() { if (!_aspects.Any()) return; var safeIdx = Math.Clamp(AspectIndex, 0, _aspects.Count - 1); CurrentAspectLabel = _aspects[safeIdx].Label; var row = AspectRows.ElementAtOrDefault(safeIdx); CurrentValueLabel = row?.DisplayLabel ?? ""; foreach (var r in AspectRows) r.IsActive = r.Index == safeIdx; } public void SetRatingByNumber(int num) { // 1=−−, 2=−, 3=~, 4=+, 5=++ var val = num switch { 1 => -2, 2 => -1, 3 => 0, 4 => 1, 5 => 2, _ => (int?)null }; if (val is null) return; ApplyRating(val.Value); } public void IncrementRating() { var cell = GetCurrentCell(); if (cell is null) return; var next = cell.Value is null ? -2 : Math.Min(2, cell.Value.Value + 1); ApplyRating(next); } public void DecrementRating() { var cell = GetCurrentCell(); if (cell is null) return; var next = cell.Value is null ? 2 : Math.Max(-2, cell.Value.Value - 1); ApplyRating(next); } private void ApplyRating(int val) { if (_rows.Count == 0 || !_aspects.Any()) return; var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key; _rows[StudentIndex].SetRating(key, val); var row = AspectRows.ElementAtOrDefault(AspectIndex); if (row is not null) { row.Value = val; row.UpdateLabel(); } CurrentValueLabel = RatingLabel(val); } public void SelectAspect(int index) { if (index < 0 || index >= _aspects.Count) return; AspectIndex = index; UpdateCurrentAspect(); } public void NextAspect() { AspectIndex = (AspectIndex + 1) % _aspects.Count; UpdateCurrentAspect(); } public void NextStudent() { if (StudentIndex >= _rows.Count - 1) return; AspectIndex = 0; ShowStudent(StudentIndex + 1); } public void PreviousStudent() { if (StudentIndex <= 0) return; AspectIndex = 0; ShowStudent(StudentIndex - 1); } public bool IsLastStudent => StudentIndex >= _rows.Count - 1; private RatingCell? GetCurrentCell() { if (_rows.Count == 0 || !_aspects.Any()) return null; var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key; return _rows[StudentIndex].Cells.FirstOrDefault(c => c.AspectKey == key); } private static string RatingLabel(int? v) => v switch { 2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "", }; } public partial class QuickAspectRow : ObservableObject { public int Index { get; } public string Label { get; } [ObservableProperty] private bool _isActive; [ObservableProperty] private string _displayLabel = ""; public int? Value { get; set; } public QuickAspectRow(int index, string label, int? value, bool isActive) { Index = index; Label = label; Value = value; IsActive = isActive; UpdateLabel(); } public void UpdateLabel() => DisplayLabel = Value switch { 2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "·", }; } // ── Schnelleingabe Anwesenheit / Hausaufgaben ──────────────────────────────── public partial class AttendanceHomeworkQuickInputViewModel : ObservableObject { public List Rows { get; } public string SessionLabel { get; } [ObservableProperty] private ParticipationStudentRow? _selectedRow; [ObservableProperty] private bool _advanceAfterInput = true; public AttendanceHomeworkQuickInputViewModel( IEnumerable rows, string sessionLabel) { Rows = rows.ToList(); SessionLabel = sessionLabel; SelectedRow = Rows.FirstOrDefault(); } public void ApplyAttendance(ParticipationStudentRow row, AttendanceStatus? status) { SelectedRow = row; row.SetAttendance(status); AdvanceIfRequested(); } public void ApplyHomework(ParticipationStudentRow row, HomeworkStatus? status) { SelectedRow = row; row.SetHomework(status); AdvanceIfRequested(); } public void MoveSelection(int delta) { if (Rows.Count == 0) return; var current = SelectedRow is null ? 0 : Rows.IndexOf(SelectedRow); SelectedRow = Rows[Math.Clamp(current + delta, 0, Rows.Count - 1)]; } private void AdvanceIfRequested() { if (AdvanceAfterInput) MoveSelection(1); } }