201 lines
7.8 KiB
C#
201 lines
7.8 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using System.Collections.ObjectModel;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
|
|
|
public enum StudentDrawMode { FullRound, Intelligent }
|
|
|
|
public sealed record StudentDrawGroup(Guid Id, string Name);
|
|
|
|
/// <summary>
|
|
/// Steuert das eigenstaendige Ziehfenster. Die Ziehhistorie ist bewusst nur fuer die Laufzeit
|
|
/// des Fensters gueltig; es entstehen dadurch keine zusaetzlichen personenbezogenen Datensaetze.
|
|
/// </summary>
|
|
public partial class RandomStudentPickerViewModel : ObservableObject
|
|
{
|
|
public const string FullRoundLabel = "Alle einmal, dann neu";
|
|
public const string IntelligentLabel = "Intelligent ausgleichen";
|
|
|
|
private readonly IStudentRepository _students;
|
|
private readonly IGroupMembershipRepository _memberships;
|
|
private readonly IParticipationSessionRepository _sessions;
|
|
private readonly IParticipationRepository _participation;
|
|
private readonly Random _random;
|
|
private readonly List<Student> _candidates = [];
|
|
private readonly List<Guid> _roundBag = [];
|
|
private readonly Dictionary<Guid, int> _drawCounts = [];
|
|
private readonly Dictionary<Guid, ParticipationSignal> _signals = [];
|
|
private Guid? _lastStudentId;
|
|
|
|
[ObservableProperty] private StudentDrawGroup? _selectedGroup;
|
|
[ObservableProperty] private string _selectionModeName = FullRoundLabel;
|
|
[ObservableProperty] private bool _considerParticipation = true;
|
|
[ObservableProperty] private string _selectedStudentName = "Bereit?";
|
|
[ObservableProperty] private string _statusText = "Wähle in den Optionen eine Lerngruppe.";
|
|
[ObservableProperty] private bool _hasDrawnStudent;
|
|
|
|
public ObservableCollection<StudentDrawGroup> Groups { get; } = [];
|
|
public string[] SelectionModeOptions { get; } = [FullRoundLabel, IntelligentLabel];
|
|
public bool IsIntelligent => SelectionModeName == IntelligentLabel;
|
|
public bool CanDraw => SelectedGroup is not null && _candidates.Count > 0;
|
|
|
|
public RandomStudentPickerViewModel(
|
|
IStudentRepository students,
|
|
IGroupRepository groups,
|
|
IGroupMembershipRepository memberships,
|
|
IParticipationSessionRepository sessions,
|
|
IParticipationRepository participation,
|
|
Guid? initialGroupId = null,
|
|
Random? random = null)
|
|
{
|
|
_students = students;
|
|
_memberships = memberships;
|
|
_sessions = sessions;
|
|
_participation = participation;
|
|
_random = random ?? Random.Shared;
|
|
|
|
foreach (var group in groups.GetAll().OrderBy(g => g.Name, StringComparer.CurrentCultureIgnoreCase))
|
|
Groups.Add(new StudentDrawGroup(group.Id, group.Name));
|
|
|
|
SelectedGroup = Groups.FirstOrDefault(g => g.Id == initialGroupId) ?? Groups.FirstOrDefault();
|
|
}
|
|
|
|
partial void OnSelectedGroupChanged(StudentDrawGroup? value) => ReloadGroup();
|
|
|
|
partial void OnSelectionModeNameChanged(string value)
|
|
{
|
|
OnPropertyChanged(nameof(IsIntelligent));
|
|
ResetDrawState();
|
|
}
|
|
|
|
partial void OnConsiderParticipationChanged(bool value) => ResetDrawState();
|
|
|
|
[RelayCommand(CanExecute = nameof(CanDraw))]
|
|
private void Draw()
|
|
{
|
|
var student = IsIntelligent ? DrawIntelligently() : DrawFromFullRound();
|
|
_drawCounts[student.Id] = _drawCounts.GetValueOrDefault(student.Id) + 1;
|
|
_lastStudentId = student.Id;
|
|
SelectedStudentName = student.FullName;
|
|
HasDrawnStudent = true;
|
|
UpdateStatus();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Reset() => ResetDrawState();
|
|
|
|
private void ReloadGroup()
|
|
{
|
|
_candidates.Clear();
|
|
_signals.Clear();
|
|
|
|
if (SelectedGroup is not null)
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var activeIds = _memberships.GetByGroup(SelectedGroup.Id)
|
|
.Where(m => GroupMembershipService.IsActiveOn(m, today))
|
|
.Select(m => m.StudentId)
|
|
.ToHashSet();
|
|
_candidates.AddRange(_students.GetByGroup(SelectedGroup.Id)
|
|
.Where(s => s.IsActive && activeIds.Contains(s.Id))
|
|
.OrderBy(s => s.LastName, StringComparer.CurrentCultureIgnoreCase)
|
|
.ThenBy(s => s.FirstName, StringComparer.CurrentCultureIgnoreCase));
|
|
LoadParticipationSignals(SelectedGroup.Id);
|
|
}
|
|
|
|
ResetDrawState();
|
|
OnPropertyChanged(nameof(CanDraw));
|
|
DrawCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
private void LoadParticipationSignals(Guid groupId)
|
|
{
|
|
var sessionIds = _sessions.GetByGroup(groupId).Select(s => s.Id).ToList();
|
|
var entries = sessionIds.SelectMany(_participation.GetBySession).ToList();
|
|
|
|
foreach (var student in _candidates)
|
|
{
|
|
var own = entries.Where(e => e.StudentId == student.Id).ToList();
|
|
var activity = own.Sum(e => e.RaisedHandCount + e.CalledOnCount);
|
|
var values = own.SelectMany(e => e.Ratings).Select(r => Math.Clamp(r.Value, -2, 2)).ToList();
|
|
_signals[student.Id] = new ParticipationSignal(
|
|
activity,
|
|
values.Count == 0 ? null : values.Average());
|
|
}
|
|
}
|
|
|
|
private Student DrawFromFullRound()
|
|
{
|
|
if (_roundBag.Count == 0)
|
|
{
|
|
_roundBag.AddRange(_candidates.Select(s => s.Id));
|
|
for (var i = _roundBag.Count - 1; i > 0; i--)
|
|
{
|
|
var swap = _random.Next(i + 1);
|
|
(_roundBag[i], _roundBag[swap]) = (_roundBag[swap], _roundBag[i]);
|
|
}
|
|
}
|
|
|
|
var id = _roundBag[^1];
|
|
_roundBag.RemoveAt(_roundBag.Count - 1);
|
|
return _candidates.Single(s => s.Id == id);
|
|
}
|
|
|
|
private Student DrawIntelligently()
|
|
{
|
|
var maxActivity = Math.Max(1, _signals.Values.Select(s => s.Activity).DefaultIfEmpty().Max());
|
|
var eligible = _candidates.Count > 1
|
|
? _candidates.Where(student => student.Id != _lastStudentId)
|
|
: _candidates;
|
|
var weighted = eligible.Select(student =>
|
|
{
|
|
var weight = 1.0 / (1.0 + 1.5 * _drawCounts.GetValueOrDefault(student.Id));
|
|
if (ConsiderParticipation && _signals.TryGetValue(student.Id, out var signal))
|
|
{
|
|
var activityRatio = signal.Activity / (double)maxActivity;
|
|
weight *= 1.65 - activityRatio; // wenig Aktive werden wahrscheinlicher gezogen
|
|
if (signal.AverageRating is { } rating)
|
|
weight *= 1.0 - Math.Clamp(rating, -2, 2) * 0.175; // starke spaeter, schwache frueher
|
|
}
|
|
return (Student: student, Weight: Math.Max(0.01, weight));
|
|
}).ToList();
|
|
|
|
var point = _random.NextDouble() * weighted.Sum(x => x.Weight);
|
|
foreach (var item in weighted)
|
|
{
|
|
point -= item.Weight;
|
|
if (point <= 0) return item.Student;
|
|
}
|
|
return weighted[^1].Student;
|
|
}
|
|
|
|
private void ResetDrawState()
|
|
{
|
|
_roundBag.Clear();
|
|
_drawCounts.Clear();
|
|
_lastStudentId = null;
|
|
HasDrawnStudent = false;
|
|
SelectedStudentName = "Bereit?";
|
|
UpdateStatus();
|
|
}
|
|
|
|
private void UpdateStatus()
|
|
{
|
|
StatusText = _candidates.Count switch
|
|
{
|
|
0 when SelectedGroup is null => "Wähle in den Optionen eine Lerngruppe.",
|
|
0 => "In dieser Lerngruppe sind heute keine aktiven Schüler.",
|
|
_ when !HasDrawnStudent => $"{_candidates.Count} Schüler stehen zur Auswahl.",
|
|
_ when !IsIntelligent && _roundBag.Count == 0 => "Alle waren dran. Der nächste Zug startet eine neue Runde.",
|
|
_ when !IsIntelligent => $"Noch {_roundBag.Count} bis zur nächsten Runde.",
|
|
_ => "Nächste Ziehung bereit.",
|
|
};
|
|
}
|
|
|
|
private sealed record ParticipationSignal(int Activity, double? AverageRating);
|
|
}
|