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? 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(); 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(); 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, bool value) { var entry = _entries.GetBySessionAndStudent(sessionId, studentId) ?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId }; entry.HomeworkMissing = 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] 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 bool _homeworkMissing; [ObservableProperty] private AttendanceStatus? _attendance; public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance); public string AttendanceTooltip => AttendanceDisplay.Label(Attendance); 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; _homeworkMissing = entry.HomeworkMissing; _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() { HomeworkMissing = !HomeworkMissing; HomeworkChangedCallback?.Invoke(StudentId, HomeworkMissing); } [RelayCommand] private void CycleAttendance() { Attendance = Attendance switch { null => AttendanceStatus.ExcusePending, AttendanceStatus.ExcusePending => AttendanceStatus.Excused, AttendanceStatus.Excused => AttendanceStatus.Unexcused, AttendanceStatus.Unexcused => 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); } } // ── Anwesenheits-Anzeige ────────────────────────────────────────────────────── public static class AttendanceDisplay { public static string Label(AttendanceStatus? s) => s switch { null => "Anwesend", AttendanceStatus.ExcusePending => "Krank (Entschuldigung offen)", AttendanceStatus.Excused => "Krank, entschuldigt", AttendanceStatus.Unexcused => "Krank, unentschuldigt", _ => "Anwesend", }; public static string ShortLabel(AttendanceStatus? s) => s switch { null => "", AttendanceStatus.ExcusePending => "K ?", AttendanceStatus.Excused => "K ✓", AttendanceStatus.Unexcused => "K ✗", _ => "", }; } // ── 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 _validationMessage = ""; 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)) { ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; return; } 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 => "−−", _ => "·", }; }