Files
LehrerApp/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs
T

881 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
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<string> ActiveCompetencyCodes { get; private set; } = [];
public ObservableCollection<ParticipationSessionItem> Sessions { get; } = [];
public ObservableCollection<ParticipationStudentRow> StudentRows { get; } = [];
public ObservableCollection<AspectColumnDef> Aspects { get; } = [];
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
public Func<Task<ParticipationSession?>>? OnAddSession { get; set; }
public Func<ParticipationTabViewModel, Task>? OnQuickInput { get; set; }
public Func<ParticipationTabViewModel, Task>? OnStatusQuickInput { get; set; }
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
public Func<ParticipationTabViewModel, Task>? 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 && !GroupMembershipService.IsActiveOn(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 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<AspectColumnDef> _aspectDefs;
public ObservableCollection<RatingCell> Cells { get; } = [];
public ObservableCollection<RatingCell> 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<Guid, string, int?>? OnRatingChanged { get; set; }
public Action<Guid, string, int?>? OnCompetencyRatingChanged { get; set; }
public Action<Guid, HomeworkStatus?>? HomeworkChangedCallback { get; set; }
public Action<Guid, AttendanceStatus?>? AttendanceChangedCallback { get; set; }
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry,
List<AspectColumnDef> aspects, List<string> 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<Guid, string, int?>? 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<CompetencyTag> 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<string, bool>? 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<ParticipationStudentRow> _rows;
private readonly List<AspectColumnDef> _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<QuickAspectRow> AspectRows { get; } = [];
public QuickInputViewModel(List<ParticipationStudentRow> rows, List<AspectColumnDef> 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<ParticipationStudentRow> Rows { get; }
public string SessionLabel { get; }
[ObservableProperty] private ParticipationStudentRow? _selectedRow;
[ObservableProperty] private bool _advanceAfterInput = true;
public AttendanceHomeworkQuickInputViewModel(
IEnumerable<ParticipationStudentRow> 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);
}
}