CI / build-and-test (push) Canceled after 0s
Neuer Dialog ExamRevealDialog: zeigt zunächst nur den Namen eines Schülers, erst nach „Aufdecken“ Note, Punktzahl und Punkte je Aufgabe. Blättern und gezielte Auswahl oben; jeder Schülerwechsel deckt wieder zu. Erreichbar über „Einzelansicht“ im Klausuren-Tab und auf der Klausuren-Hauptseite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
5.4 KiB
C#
126 lines
5.4 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using System.Globalization;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
|
|
|
// ── Datenschutzkonforme Einzelansicht (1.7) ──────────────────────────────────
|
|
|
|
/// Zeigt die Leistung genau eines Schülers — zunächst nur den Namen, das Ergebnis erst nach
|
|
/// "Aufdecken". So kann ein einzelner Schüler nach vorne geholt werden, ohne dass er (oder die
|
|
/// Klasse über den Beamer) Ergebnisse anderer sieht. Rein lesend.
|
|
public partial class ExamRevealDialogViewModel : ObservableObject
|
|
{
|
|
private readonly Exam _exam;
|
|
|
|
public string ExamTitle => _exam.Title;
|
|
public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy");
|
|
|
|
public List<ExamRevealStudent> Students { get; } = [];
|
|
public bool HasStudents => Students.Count > 0;
|
|
|
|
[ObservableProperty] private ExamRevealStudent? _selectedStudent;
|
|
[ObservableProperty] private bool _isRevealed;
|
|
|
|
public string PositionLabel => SelectedStudent is null
|
|
? ""
|
|
: $"{Students.IndexOf(SelectedStudent) + 1} / {Students.Count}";
|
|
|
|
public ExamRevealDialogViewModel(IExamResultRepository results, IStudentRepository students,
|
|
GradingService grading, Exam exam)
|
|
{
|
|
_exam = exam;
|
|
var tasks = exam.Tasks.OrderBy(t => t.Nr).ToList();
|
|
var labels = ExamScoringService.TaskLabels(tasks);
|
|
var enrolled = students.GetByGroup(exam.GroupId).ToDictionary(s => s.Id);
|
|
|
|
// Grundlage sind die eingetragenen Ergebnisse: wer keins hat (anderes Niveau, nie
|
|
// angefasste Zeile), hat hier auch nichts zu sehen. Ausgetretene Schüler mit Ergebnis
|
|
// bleiben über GetById auffindbar.
|
|
Students = results.GetByExam(exam.Id)
|
|
.Select(r => (Result: r, Student: enrolled.GetValueOrDefault(r.StudentId) ?? students.GetById(r.StudentId)))
|
|
.Where(x => x.Student is not null)
|
|
.OrderBy(x => x.Student!.LastName).ThenBy(x => x.Student!.FirstName)
|
|
.Select(x => new ExamRevealStudent(x.Student!, x.Result, tasks, labels, exam.GradingKey, grading))
|
|
.ToList();
|
|
_selectedStudent = Students.FirstOrDefault();
|
|
}
|
|
|
|
/// Beim Wechsel wird immer wieder zugedeckt — sonst stünde beim Durchblättern sofort das
|
|
/// Ergebnis des nächsten Schülers offen auf dem Bildschirm.
|
|
partial void OnSelectedStudentChanged(ExamRevealStudent? value)
|
|
{
|
|
IsRevealed = false;
|
|
OnPropertyChanged(nameof(PositionLabel));
|
|
PreviousCommand.NotifyCanExecuteChanged();
|
|
NextCommand.NotifyCanExecuteChanged();
|
|
RevealCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
private int SelectedIndex => SelectedStudent is null ? -1 : Students.IndexOf(SelectedStudent);
|
|
private bool CanGoPrevious() => SelectedIndex > 0;
|
|
private bool CanGoNext() => SelectedIndex >= 0 && SelectedIndex < Students.Count - 1;
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGoPrevious))]
|
|
private void Previous() => SelectedStudent = Students[SelectedIndex - 1];
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGoNext))]
|
|
private void Next() => SelectedStudent = Students[SelectedIndex + 1];
|
|
|
|
[RelayCommand(CanExecute = nameof(CanReveal))]
|
|
private void Reveal() => IsRevealed = true;
|
|
private bool CanReveal() => SelectedStudent is not null;
|
|
|
|
[RelayCommand]
|
|
private void Hide() => IsRevealed = false;
|
|
}
|
|
|
|
/// Ergebnis eines Schülers, fertig aufbereitet für die Einzelansicht.
|
|
public class ExamRevealStudent
|
|
{
|
|
private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
|
|
|
|
/// "Nachname, Vorname" — für die sortierte Auswahlliste.
|
|
public string ListName { get; }
|
|
/// "Vorname Nachname" — für die große Anzeige.
|
|
public string DisplayName { get; }
|
|
public bool Absent { get; }
|
|
public bool HasGrade => !Absent;
|
|
public string GradeDisplay { get; }
|
|
public string PointsDisplay { get; }
|
|
public List<ExamRevealTaskRow> Tasks { get; } = [];
|
|
|
|
public ExamRevealStudent(Student student, ExamResult result, IReadOnlyList<ExamTask> orderedTasks,
|
|
IReadOnlyList<string> taskLabels, List<GradingKeyEntry> gradingKey, GradingService grading)
|
|
{
|
|
ListName = student.FullName;
|
|
DisplayName = $"{student.FirstName} {student.LastName}".Trim();
|
|
Absent = result.Absent;
|
|
if (Absent)
|
|
{
|
|
GradeDisplay = "abwesend";
|
|
PointsDisplay = "";
|
|
return;
|
|
}
|
|
|
|
var maximum = ExamScoringService.Maximum(orderedTasks, result.Variant);
|
|
GradeDisplay = result.Grade ?? grading.CalculateGrade(result.TotalPoints, maximum, gradingKey);
|
|
PointsDisplay = $"{Format(result.TotalPoints)} von {Format(maximum)} Punkten";
|
|
|
|
// Nur die Aufgaben der geschriebenen Fassung — die der anderen gab es für den Schüler nicht.
|
|
for (var i = 0; i < orderedTasks.Count; i++)
|
|
{
|
|
if (!ExamScoringService.AppliesTo(orderedTasks[i], result.Variant)) continue;
|
|
var points = i < result.Points.Count ? result.Points[i] : 0;
|
|
Tasks.Add(new ExamRevealTaskRow(taskLabels[i], Format(points), Format(orderedTasks[i].MaxPoints)));
|
|
}
|
|
}
|
|
|
|
private static string Format(double value) => value.ToString("0.##", German);
|
|
}
|
|
|
|
public record ExamRevealTaskRow(string Label, string Points, string MaxPoints);
|