Tagesflagge im Sitzplan, Leistungsüberblick mit Zielnoten-Rechner
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
- Mitarbeit: Tagesflagge (👑 Spitzentag / 😴 Schlaftag / ⚡ Schlechter Tag) je Schüler und Sitzung. Primär im Sitzplatz-Dialog (⇧1/2/3, ⇧X) mit Badge auf der Sitzplatz-Kachel; Fallback in der Schnelleingabe für Lerngruppen ohne Sitzplan. - Noten: neuer Dialog "Überblick" in der Notenübersicht zeigt Klausuren, Mitarbeit- und sonstige Noten eines Schülers samt berechneter Zeugnisnote. Zielnoten-Rechner (ReportGradeTargetCalculator) beantwortet "was brauche ich noch für Note X", ein Was-wäre-wenn-Rechner simuliert eine zusätzliche Klausurnote. Bewerter-/Schülermodus per Umschalter im selben Fenster — Bewertermodus zeigt zusätzlich Kursdurchschnitt je Klausur und erlaubt das Bearbeiten von Mitarbeit-/Sonstige-Noten. Klausurverlauf als Balken-Sparkline wie die bestehende Notenentwicklung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,9 +32,18 @@ public class ParticipationEntry
|
|||||||
// ParticipationCountSuggestion.
|
// ParticipationCountSuggestion.
|
||||||
public int RaisedHandCount { get; set; }
|
public int RaisedHandCount { get; set; }
|
||||||
public int CalledOnCount { get; set; }
|
public int CalledOnCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Tagesflagge (Nutzer-Feedback): markiert eine session-bezogene Bewertung als besonders
|
||||||
|
/// herausragend oder besonders schwach, unabhängig von der Richtung ("egal in welche
|
||||||
|
/// Richtung, herausragend festhalten"). Rein deskriptiv — fließt nirgends in eine Berechnung
|
||||||
|
/// (Mitarbeitsnote 3.2, Aufrufgerechtigkeit) ein, nur zur Erinnerung/Dokumentation.
|
||||||
|
/// </summary>
|
||||||
|
public DayHighlightKind? DayHighlight { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum DayHighlightKind { Standout, Sleepy, Rough }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hausaufgabenstatus einer Sitzung; null bedeutet, dass für diesen Termin keine
|
/// Hausaufgabenstatus einer Sitzung; null bedeutet, dass für diesen Termin keine
|
||||||
/// Hausaufgabe erfasst wurde. MissingOpen kann später in MissingOverdue oder SubmittedLate
|
/// Hausaufgabe erfasst wurde. MissingOpen kann später in MissingOverdue oder SubmittedLate
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
|
||||||
|
namespace LehrerApp.Core.Services;
|
||||||
|
|
||||||
|
/// Welcher der drei Leistungsbereiche (siehe GradingScheme) bei der Zielnoten-Frage der
|
||||||
|
/// gesuchte/unbekannte ist.
|
||||||
|
public enum GradeBucketKind { Exams, Participation, Other }
|
||||||
|
|
||||||
|
public readonly record struct TargetGradeResult(double RequiredAverage, bool IsAchievable);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Beantwortet die Zielnoten-Frage "Was brauche ich noch, um Zeugnisnote X zu erreichen?"
|
||||||
|
/// (Nutzer-Feedback) — kehrt <see cref="GradingService.CalculateReportGrade"/> um: löst nach
|
||||||
|
/// dem Durchschnitt eines gewählten Bereichs auf, der zusammen mit den bekannten Durchschnitten
|
||||||
|
/// der übrigen Bereiche (gewichtet nach <see cref="GradingScheme"/>) genau die Zielnote ergibt.
|
||||||
|
/// Bereiche ohne bekannten Durchschnitt (z.B. noch keine Mitarbeitsnote im Halbjahr) fließen
|
||||||
|
/// nicht in die bekannte Seite der Gleichung ein — exakt dieselbe Normierung wie
|
||||||
|
/// <see cref="GradingService.CalculateReportGrade"/> bei fehlenden Bereichen.
|
||||||
|
/// </summary>
|
||||||
|
public static class ReportGradeTargetCalculator
|
||||||
|
{
|
||||||
|
public static TargetGradeResult? SolveRequiredAverage(double target, GradeBucketKind solveFor,
|
||||||
|
double? examsAverage, double? participationAverage, double? otherAverage,
|
||||||
|
GradingScheme scheme, GradingSystem system)
|
||||||
|
{
|
||||||
|
var solvePercent = solveFor switch
|
||||||
|
{
|
||||||
|
GradeBucketKind.Exams => scheme.ExamsPercent,
|
||||||
|
GradeBucketKind.Participation => scheme.ParticipationPercent,
|
||||||
|
_ => scheme.OtherPercent,
|
||||||
|
};
|
||||||
|
// Dieser Bereich fließt laut Gewichtungsschema gar nicht in die Zeugnisnote ein —
|
||||||
|
// keine Zielnoten-Frage für ihn beantwortbar.
|
||||||
|
if (solvePercent <= 0) return null;
|
||||||
|
|
||||||
|
var known = new List<(double Avg, double Percent)>();
|
||||||
|
if (solveFor != GradeBucketKind.Exams && examsAverage is { } e) known.Add((e, scheme.ExamsPercent));
|
||||||
|
if (solveFor != GradeBucketKind.Participation && participationAverage is { } p) known.Add((p, scheme.ParticipationPercent));
|
||||||
|
if (solveFor != GradeBucketKind.Other && otherAverage is { } o) known.Add((o, scheme.OtherPercent));
|
||||||
|
|
||||||
|
var totalPercent = known.Sum(k => k.Percent) + solvePercent;
|
||||||
|
var knownContribution = known.Sum(k => k.Avg * k.Percent);
|
||||||
|
var required = (target * totalPercent - knownContribution) / solvePercent;
|
||||||
|
|
||||||
|
var (min, max) = system == GradingSystem.Grades1To6 ? (1.0, 6.0) : (0.0, 15.0);
|
||||||
|
return new TargetGradeResult(required, required >= min && required <= max);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,47 @@ public sealed class QuickInputViewModelTests
|
|||||||
Assert.Null(rows[0].GetRating("quality"));
|
Assert.Null(rows[0].GetRating("quality"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetDayHighlight_SpeichertAufDerZeileUndAktualisiertDieAnzeige()
|
||||||
|
{
|
||||||
|
var aspects = new List<AspectColumnDef>
|
||||||
|
{
|
||||||
|
new(new ParticipationAspect { Key = "quality", Label = "Qualität" }),
|
||||||
|
};
|
||||||
|
var rows = new List<ParticipationStudentRow>
|
||||||
|
{
|
||||||
|
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
|
||||||
|
};
|
||||||
|
var vm = new QuickInputViewModel(rows, aspects);
|
||||||
|
|
||||||
|
vm.SetDayHighlight(DayHighlightKind.Standout);
|
||||||
|
|
||||||
|
Assert.Equal(DayHighlightKind.Standout, rows[0].DayHighlight);
|
||||||
|
Assert.Equal("👑", vm.CurrentStudentDayHighlightSymbol);
|
||||||
|
Assert.Equal("Spitzentag", vm.CurrentStudentDayHighlightLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZurueckZuVorherigemSchueler_ZeigtDessenTagesflaggeWiederAn()
|
||||||
|
{
|
||||||
|
var aspects = new List<AspectColumnDef>
|
||||||
|
{
|
||||||
|
new(new ParticipationAspect { Key = "quality", Label = "Qualität" }),
|
||||||
|
};
|
||||||
|
var rows = new List<ParticipationStudentRow>
|
||||||
|
{
|
||||||
|
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
|
||||||
|
new(Guid.NewGuid(), "Ben", new ParticipationEntry(), aspects, []),
|
||||||
|
};
|
||||||
|
var vm = new QuickInputViewModel(rows, aspects);
|
||||||
|
|
||||||
|
vm.SetDayHighlight(DayHighlightKind.Sleepy); // Anna
|
||||||
|
vm.NextStudent(); // zu Ben, keine Flagge
|
||||||
|
vm.PreviousStudent(); // zurück zu Anna
|
||||||
|
|
||||||
|
Assert.Equal(DayHighlightKind.Sleepy, vm.CurrentStudentDayHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -472,6 +472,7 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
vm.SetRatingByNumber(5);
|
vm.SetRatingByNumber(5);
|
||||||
vm.ApplyAttendanceShortcut(1, clear: false);
|
vm.ApplyAttendanceShortcut(1, clear: false);
|
||||||
vm.ApplyHomeworkShortcut(7, clear: false);
|
vm.ApplyHomeworkShortcut(7, clear: false);
|
||||||
|
vm.ApplyDayHighlightShortcut(1, clear: false);
|
||||||
|
|
||||||
var session = Assert.Single(sessions.GetByGroup(groupId));
|
var session = Assert.Single(sessions.GetByGroup(groupId));
|
||||||
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), session.Date);
|
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), session.Date);
|
||||||
@@ -481,6 +482,27 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
Assert.Equal(AttendanceStatus.Present, entry.Attendance);
|
Assert.Equal(AttendanceStatus.Present, entry.Attendance);
|
||||||
Assert.Equal(HomeworkStatus.MissingOpen, entry.Homework);
|
Assert.Equal(HomeworkStatus.MissingOpen, entry.Homework);
|
||||||
Assert.True(entry.HomeworkMissing);
|
Assert.True(entry.HomeworkMissing);
|
||||||
|
Assert.Equal(DayHighlightKind.Standout, entry.DayHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SitzplatzBewertung_TagesflaggeKannWiederGeloeschtWerden()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
|
||||||
|
groupId, studentId, "Beispiel, Anna", canEdit: true);
|
||||||
|
|
||||||
|
vm.ApplyDayHighlightShortcut(3, clear: false); // aus Versehen "Schlechter Tag" gesetzt
|
||||||
|
vm.ApplyDayHighlightShortcut(null, clear: true);
|
||||||
|
|
||||||
|
var session = Assert.Single(sessions.GetByGroup(groupId));
|
||||||
|
var entry = entries.GetBySessionAndStudent(session.Id, studentId)!;
|
||||||
|
Assert.Null(entry.DayHighlight);
|
||||||
|
Assert.Equal("Keine Markierung", vm.DayHighlightLabel);
|
||||||
|
Assert.All(vm.DayHighlightChoices, c => Assert.Equal(c.Kind is null, c.IsSelected));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
/// Tests für den Schüler-Leistungsüberblick (Nutzer-Feedback): Klausuren/Mitarbeit/Sonstige
|
||||||
|
/// im Überblick, berechnete Zeugnisnote, Zielnoten- und Was-wäre-wenn-Rechner sowie der
|
||||||
|
/// Bewerter-/Schülermodus-Unterschied (Kursdurchschnitt, Bearbeitbarkeit).
|
||||||
|
public sealed class StudentPerformanceOverviewViewModelTests
|
||||||
|
{
|
||||||
|
private static readonly Guid GroupId = Guid.NewGuid();
|
||||||
|
private static readonly Guid StudentId = Guid.NewGuid();
|
||||||
|
private static readonly Guid OtherStudentId = Guid.NewGuid();
|
||||||
|
|
||||||
|
private static StudentPerformanceOverviewViewModel BuildViewModel(
|
||||||
|
FakeExams? exams = null, FakeResults? results = null, FakeGrades? grades = null,
|
||||||
|
FakeSchemes? schemes = null, GradingSystem system = GradingSystem.Grades1To6)
|
||||||
|
{
|
||||||
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
||||||
|
var schemesRepo = schemes ?? new FakeSchemes();
|
||||||
|
if (schemes is null) schemesRepo.SetForGroup(GroupId, scheme);
|
||||||
|
|
||||||
|
return new StudentPerformanceOverviewViewModel(
|
||||||
|
grades ?? new FakeGrades(), exams ?? new FakeExams([]), results ?? new FakeResults(),
|
||||||
|
new FakeMemberships([]), schemesRepo, new GradingService(),
|
||||||
|
GroupId, StudentId, GroupType.Class, system, "Beispiel, Anna", "Testkurs", "2025/26");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExamRows_ZeigtEigeneNoteUndKursdurchschnitt()
|
||||||
|
{
|
||||||
|
var exam = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
|
||||||
|
var exams = new FakeExams([exam]);
|
||||||
|
var results = new FakeResults();
|
||||||
|
results.Add(new ExamResult { ExamId = exam.Id, StudentId = StudentId, Grade = "2" });
|
||||||
|
results.Add(new ExamResult { ExamId = exam.Id, StudentId = OtherStudentId, Grade = "4" });
|
||||||
|
|
||||||
|
var vm = BuildViewModel(exams, results);
|
||||||
|
|
||||||
|
var row = Assert.Single(vm.ExamRows);
|
||||||
|
Assert.Equal("2", row.OwnGradeDisplay);
|
||||||
|
Assert.Equal("3.0", row.ClassAverageDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExamHistory_ZeigtEigenenVerlaufUndMarkiertNotenabfall()
|
||||||
|
{
|
||||||
|
var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
|
||||||
|
var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K2", Date = new DateOnly(2025, 9, 24) };
|
||||||
|
var exams = new FakeExams([exam1, exam2]);
|
||||||
|
var results = new FakeResults();
|
||||||
|
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = StudentId, Grade = "2" });
|
||||||
|
results.Add(new ExamResult { ExamId = exam2.Id, StudentId = StudentId, Grade = "5" }); // Abfall + mangelhaft
|
||||||
|
|
||||||
|
var vm = BuildViewModel(exams, results);
|
||||||
|
|
||||||
|
Assert.Equal(2, vm.ExamHistory.Points.Count);
|
||||||
|
Assert.False(vm.ExamHistory.Points[0].IsWarning);
|
||||||
|
Assert.True(vm.ExamHistory.Points[1].IsWarning);
|
||||||
|
Assert.Contains("Abfall", vm.ExamHistory.Points[1].WarningText);
|
||||||
|
Assert.Contains("Versetzungsgefährdung", vm.ExamHistory.Points[1].WarningText);
|
||||||
|
Assert.True(vm.ExamHistory.Points[1].BarHeight < vm.ExamHistory.Points[0].BarHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AktuelleZeugnisnote_KombiniertAlleDreiBereiche()
|
||||||
|
{
|
||||||
|
var exam = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
|
||||||
|
var exams = new FakeExams([exam]);
|
||||||
|
var results = new FakeResults();
|
||||||
|
results.Add(new ExamResult { ExamId = exam.Id, StudentId = StudentId, Grade = "2" });
|
||||||
|
var grades = new FakeGrades();
|
||||||
|
grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
|
||||||
|
Date = new DateOnly(2025, 9, 5), Value = "3" });
|
||||||
|
grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Other,
|
||||||
|
Date = new DateOnly(2025, 9, 5), Value = "1" });
|
||||||
|
|
||||||
|
var vm = BuildViewModel(exams, results, grades);
|
||||||
|
|
||||||
|
// (2*50 + 3*40 + 1*10) / 100 = 2.3 -> kaufmännisch 2
|
||||||
|
Assert.Equal("Note 2", vm.CurrentReportGradeDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Zielnote_BerechnetNoetigenKlausurschnitt()
|
||||||
|
{
|
||||||
|
var grades = new FakeGrades();
|
||||||
|
grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
|
||||||
|
Date = new DateOnly(2025, 9, 5), Value = "3" });
|
||||||
|
grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Other,
|
||||||
|
Date = new DateOnly(2025, 9, 5), Value = "2" });
|
||||||
|
var vm = BuildViewModel(grades: grades);
|
||||||
|
|
||||||
|
vm.TargetGradeText = "2";
|
||||||
|
vm.TargetBucketName = "Klausuren";
|
||||||
|
|
||||||
|
// Ziel 2,0 = (x*50 + 3*40 + 2*10)/100 => 200 = 50x+120+20 => x = 1,2
|
||||||
|
Assert.Contains("1.2", vm.TargetResultDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WasWaereWenn_ZeigtZeugnisnoteMitZusaetzlicherKlausurAn()
|
||||||
|
{
|
||||||
|
var exam = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
|
||||||
|
var exams = new FakeExams([exam]);
|
||||||
|
var results = new FakeResults();
|
||||||
|
results.Add(new ExamResult { ExamId = exam.Id, StudentId = StudentId, Grade = "2" });
|
||||||
|
var grades = new FakeGrades();
|
||||||
|
grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
|
||||||
|
Date = new DateOnly(2025, 9, 5), Value = "2" });
|
||||||
|
|
||||||
|
var vm = BuildViewModel(exams, results, grades);
|
||||||
|
|
||||||
|
vm.WhatIfExamGradeText = "6";
|
||||||
|
|
||||||
|
// Klausuren neu: (2+6)/2=4 (50%), Mitarbeit 2 (40%, Sonstige fehlt) -> (4*50+2*40)/90 = 3,11 -> kaufm. 3
|
||||||
|
Assert.Contains("3", vm.WhatIfResultDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SaveCommand_AufMitarbeitszeile_SpeichertUndAktualisiertDurchschnitt()
|
||||||
|
{
|
||||||
|
var grades = new FakeGrades();
|
||||||
|
var grade = new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
|
||||||
|
Date = new DateOnly(2025, 9, 5), Value = "3" };
|
||||||
|
grades.Add(grade);
|
||||||
|
var vm = BuildViewModel(grades: grades);
|
||||||
|
|
||||||
|
var row = Assert.Single(vm.ParticipationRows);
|
||||||
|
row.Value = "1";
|
||||||
|
row.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal("1.0", vm.ParticipationAverageDisplay);
|
||||||
|
Assert.Equal("1", grades.GetByStudentAndGroup(StudentId, GroupId).Single().Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OhneWerteImZeitraum_ZeigtHinweisStattZeugnisnote()
|
||||||
|
{
|
||||||
|
var vm = BuildViewModel();
|
||||||
|
|
||||||
|
Assert.Contains("Noch nicht berechenbar", vm.CurrentReportGradeDisplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
public Func<GradeOverviewRow, Task>? OnManageStudentGrades { get; set; }
|
public Func<GradeOverviewRow, Task>? OnManageStudentGrades { get; set; }
|
||||||
public Func<Task>? OnCollectiveGrade { get; set; }
|
public Func<Task>? OnCollectiveGrade { get; set; }
|
||||||
public Func<Task>? OnReportGrades { get; set; }
|
public Func<Task>? OnReportGrades { get; set; }
|
||||||
|
public Func<GradeOverviewRow, Task>? OnShowPerformanceOverview { get; set; }
|
||||||
|
|
||||||
public GradeOverviewTabViewModel(IGradeRepository grades, IExamRepository exams,
|
public GradeOverviewTabViewModel(IGradeRepository grades, IExamRepository exams,
|
||||||
IExamResultRepository results, IStudentRepository students,
|
IExamResultRepository results, IStudentRepository students,
|
||||||
@@ -107,6 +108,14 @@ public partial class GradeOverviewTabViewModel : ObservableObject
|
|||||||
Recompute();
|
Recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ShowPerformanceOverview()
|
||||||
|
{
|
||||||
|
if (SelectedRow is null || OnShowPerformanceOverview is null) return;
|
||||||
|
await OnShowPerformanceOverview(SelectedRow);
|
||||||
|
Recompute();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task CollectiveGrade()
|
private async Task CollectiveGrade()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val);
|
row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val);
|
||||||
row.HomeworkChangedCallback = (sid, val) => SaveHomework(sessionId, sid, val);
|
row.HomeworkChangedCallback = (sid, val) => SaveHomework(sessionId, sid, val);
|
||||||
row.AttendanceChangedCallback = (sid, val) => SaveAttendance(sessionId, sid, val);
|
row.AttendanceChangedCallback = (sid, val) => SaveAttendance(sessionId, sid, val);
|
||||||
|
row.DayHighlightChangedCallback = (sid, val) => SaveDayHighlight(sessionId, sid, val);
|
||||||
StudentRows.Add(row);
|
StudentRows.Add(row);
|
||||||
}
|
}
|
||||||
QuickInputCommand.NotifyCanExecuteChanged();
|
QuickInputCommand.NotifyCanExecuteChanged();
|
||||||
@@ -212,6 +213,15 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
_entries.Save(entry);
|
_entries.Save(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SaveDayHighlight(Guid sessionId, Guid studentId, DayHighlightKind? value)
|
||||||
|
{
|
||||||
|
if (IsReadOnly) return;
|
||||||
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||||
|
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
||||||
|
entry.DayHighlight = value;
|
||||||
|
_entries.Save(entry);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible;
|
private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible;
|
||||||
|
|
||||||
@@ -389,6 +399,7 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
|
|
||||||
[ObservableProperty] private HomeworkStatus? _homework;
|
[ObservableProperty] private HomeworkStatus? _homework;
|
||||||
[ObservableProperty] private AttendanceStatus? _attendance;
|
[ObservableProperty] private AttendanceStatus? _attendance;
|
||||||
|
[ObservableProperty] private DayHighlightKind? _dayHighlight;
|
||||||
|
|
||||||
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
|
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
|
||||||
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
||||||
@@ -405,6 +416,7 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
public Action<Guid, string, int?>? OnCompetencyRatingChanged { get; set; }
|
public Action<Guid, string, int?>? OnCompetencyRatingChanged { get; set; }
|
||||||
public Action<Guid, HomeworkStatus?>? HomeworkChangedCallback { get; set; }
|
public Action<Guid, HomeworkStatus?>? HomeworkChangedCallback { get; set; }
|
||||||
public Action<Guid, AttendanceStatus?>? AttendanceChangedCallback { get; set; }
|
public Action<Guid, AttendanceStatus?>? AttendanceChangedCallback { get; set; }
|
||||||
|
public Action<Guid, DayHighlightKind?>? DayHighlightChangedCallback { get; set; }
|
||||||
|
|
||||||
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry,
|
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry,
|
||||||
List<AspectColumnDef> aspects, List<string> competencyCodes)
|
List<AspectColumnDef> aspects, List<string> competencyCodes)
|
||||||
@@ -414,6 +426,7 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
_aspectDefs = aspects;
|
_aspectDefs = aspects;
|
||||||
_homework = HomeworkDisplay.Effective(entry);
|
_homework = HomeworkDisplay.Effective(entry);
|
||||||
_attendance = entry.Attendance;
|
_attendance = entry.Attendance;
|
||||||
|
_dayHighlight = entry.DayHighlight;
|
||||||
|
|
||||||
foreach (var a in aspects)
|
foreach (var a in aspects)
|
||||||
{
|
{
|
||||||
@@ -463,6 +476,12 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
HomeworkChangedCallback?.Invoke(StudentId, value);
|
HomeworkChangedCallback?.Invoke(StudentId, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetDayHighlight(DayHighlightKind? value)
|
||||||
|
{
|
||||||
|
DayHighlight = value;
|
||||||
|
DayHighlightChangedCallback?.Invoke(StudentId, value);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void CycleAttendance()
|
private void CycleAttendance()
|
||||||
{
|
{
|
||||||
@@ -614,6 +633,27 @@ public static class AttendanceDisplay
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tagesflagge (Nutzer-Feedback) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static class DayHighlightDisplay
|
||||||
|
{
|
||||||
|
public static string Symbol(DayHighlightKind? kind) => kind switch
|
||||||
|
{
|
||||||
|
DayHighlightKind.Standout => "👑",
|
||||||
|
DayHighlightKind.Sleepy => "😴",
|
||||||
|
DayHighlightKind.Rough => "⚡",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Label(DayHighlightKind? kind) => kind switch
|
||||||
|
{
|
||||||
|
DayHighlightKind.Standout => "Spitzentag",
|
||||||
|
DayHighlightKind.Sleepy => "Schlaftag",
|
||||||
|
DayHighlightKind.Rough => "Schlechter Tag",
|
||||||
|
_ => "Keine Markierung",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Eine Bewertungszelle ──────────────────────────────────────────────────────
|
// ── Eine Bewertungszelle ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
public partial class RatingCell : ObservableObject
|
public partial class RatingCell : ObservableObject
|
||||||
@@ -800,12 +840,31 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _progressText = "";
|
[ObservableProperty] private string _progressText = "";
|
||||||
[ObservableProperty] private bool _currentStudentIsAbsent;
|
[ObservableProperty] private bool _currentStudentIsAbsent;
|
||||||
[ObservableProperty] private string _currentStudentAttendanceLabel = "";
|
[ObservableProperty] private string _currentStudentAttendanceLabel = "";
|
||||||
|
[ObservableProperty] private DayHighlightKind? _currentStudentDayHighlight;
|
||||||
|
|
||||||
/// Dimmt Name/Aspektliste, wenn der aktuelle Schüler abwesend ist — kein Blockieren der
|
/// Dimmt Name/Aspektliste, wenn der aktuelle Schüler abwesend ist — kein Blockieren der
|
||||||
/// Eingabe (manche Bewertungssysteme wollen trotzdem einen Eintrag, z.B. "0 Punkte"), nur ein
|
/// Eingabe (manche Bewertungssysteme wollen trotzdem einen Eintrag, z.B. "0 Punkte"), nur ein
|
||||||
/// visueller Hinweis, dass eine Bewertung hier normalerweise keinen Sinn ergibt.
|
/// visueller Hinweis, dass eine Bewertung hier normalerweise keinen Sinn ergibt.
|
||||||
public double CurrentStudentContentOpacity => CurrentStudentIsAbsent ? 0.4 : 1.0;
|
public double CurrentStudentContentOpacity => CurrentStudentIsAbsent ? 0.4 : 1.0;
|
||||||
|
|
||||||
|
/// Tagesflagge (Nutzer-Feedback): Fallback für Kurse ohne Sitzplan — im Sitzplatz-Dialog gibt
|
||||||
|
/// es dieselbe Auswahl bereits über SeatAssessmentViewModel.DayHighlightChoices.
|
||||||
|
public string CurrentStudentDayHighlightSymbol => DayHighlightDisplay.Symbol(CurrentStudentDayHighlight);
|
||||||
|
public string CurrentStudentDayHighlightLabel => DayHighlightDisplay.Label(CurrentStudentDayHighlight);
|
||||||
|
|
||||||
|
public void SetDayHighlight(DayHighlightKind? value)
|
||||||
|
{
|
||||||
|
if (_rows.Count == 0) return;
|
||||||
|
_rows[StudentIndex].SetDayHighlight(value);
|
||||||
|
CurrentStudentDayHighlight = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnCurrentStudentDayHighlightChanged(DayHighlightKind? value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CurrentStudentDayHighlightSymbol));
|
||||||
|
OnPropertyChanged(nameof(CurrentStudentDayHighlightLabel));
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<QuickAspectRow> AspectRows { get; } = [];
|
public ObservableCollection<QuickAspectRow> AspectRows { get; } = [];
|
||||||
|
|
||||||
/// Wechselt je nach Typ des aktuell gewählten Aspekts (3.1.3) — Scale3/Binary haben andere
|
/// Wechselt je nach Typ des aktuell gewählten Aspekts (3.1.3) — Scale3/Binary haben andere
|
||||||
@@ -824,7 +883,8 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
_ => "1–5 bewerten",
|
_ => "1–5 bewerten",
|
||||||
};
|
};
|
||||||
return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste/↓ nächster Aspekt · ↑ vorheriger Aspekt · " +
|
return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste/↓ nächster Aspekt · ↑ vorheriger Aspekt · " +
|
||||||
"Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Entf nicht bewertet · Esc schließen";
|
"Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Entf nicht bewertet · " +
|
||||||
|
"⇧1/2/3 Tagesflagge, ⇧0 löschen · Esc schließen";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,6 +912,7 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
ProgressText = $"{index + 1} / {_rows.Count}";
|
ProgressText = $"{index + 1} / {_rows.Count}";
|
||||||
CurrentStudentIsAbsent = row.IsAbsent;
|
CurrentStudentIsAbsent = row.IsAbsent;
|
||||||
CurrentStudentAttendanceLabel = row.AttendanceTooltip;
|
CurrentStudentAttendanceLabel = row.AttendanceTooltip;
|
||||||
|
CurrentStudentDayHighlight = row.DayHighlight;
|
||||||
|
|
||||||
AspectRows.Clear();
|
AspectRows.Clear();
|
||||||
foreach (var (a, i) in _aspects.Select((a, i) => (a, i)))
|
foreach (var (a, i) in _aspects.Select((a, i) => (a, i)))
|
||||||
|
|||||||
@@ -547,11 +547,13 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
[ObservableProperty] private double _lessonOpacity = 1.0;
|
[ObservableProperty] private double _lessonOpacity = 1.0;
|
||||||
[ObservableProperty] private string _attendanceBadge = "";
|
[ObservableProperty] private string _attendanceBadge = "";
|
||||||
[ObservableProperty] private string _homeworkBadge = "";
|
[ObservableProperty] private string _homeworkBadge = "";
|
||||||
|
[ObservableProperty] private string _dayHighlightBadge = "";
|
||||||
[ObservableProperty] private int _raisedHandCount;
|
[ObservableProperty] private int _raisedHandCount;
|
||||||
[ObservableProperty] private int _calledOnCount;
|
[ObservableProperty] private int _calledOnCount;
|
||||||
public ObservableCollection<SituationTagChoice> SituationTags { get; } = [];
|
public ObservableCollection<SituationTagChoice> SituationTags { get; } = [];
|
||||||
public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
|
public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
|
||||||
public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
|
public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
|
||||||
|
public bool HasDayHighlightBadge => DayHighlightBadge.Length > 0;
|
||||||
public bool ShowLessonOverview => IsOccupied && !CanEdit;
|
public bool ShowLessonOverview => IsOccupied && !CanEdit;
|
||||||
[ObservableProperty] private bool _canRecordLesson;
|
[ObservableProperty] private bool _canRecordLesson;
|
||||||
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
||||||
@@ -644,6 +646,7 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
var attendance = entry?.Attendance;
|
var attendance = entry?.Attendance;
|
||||||
AttendanceBadge = attendance is null ? "" : AttendanceDisplay.ShortLabel(attendance);
|
AttendanceBadge = attendance is null ? "" : AttendanceDisplay.ShortLabel(attendance);
|
||||||
HomeworkBadge = entry is null ? "" : HomeworkDisplay.Symbol(HomeworkDisplay.Effective(entry));
|
HomeworkBadge = entry is null ? "" : HomeworkDisplay.Symbol(HomeworkDisplay.Effective(entry));
|
||||||
|
DayHighlightBadge = DayHighlightDisplay.Symbol(entry?.DayHighlight);
|
||||||
RaisedHandCount = entry?.RaisedHandCount ?? 0;
|
RaisedHandCount = entry?.RaisedHandCount ?? 0;
|
||||||
CalledOnCount = entry?.CalledOnCount ?? 0;
|
CalledOnCount = entry?.CalledOnCount ?? 0;
|
||||||
LessonOpacity = attendance is not null and not AttendanceStatus.Present
|
LessonOpacity = attendance is not null and not AttendanceStatus.Present
|
||||||
@@ -652,6 +655,7 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
foreach (var choice in SituationTags) choice.IsSelected = selected.Contains(choice.Text);
|
foreach (var choice in SituationTags) choice.IsSelected = selected.Contains(choice.Text);
|
||||||
OnPropertyChanged(nameof(HasAttendanceBadge));
|
OnPropertyChanged(nameof(HasAttendanceBadge));
|
||||||
OnPropertyChanged(nameof(HasHomeworkBadge));
|
OnPropertyChanged(nameof(HasHomeworkBadge));
|
||||||
|
OnPropertyChanged(nameof(HasDayHighlightBadge));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,6 +681,7 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
[ObservableProperty] private int _selectedAspectIndex;
|
[ObservableProperty] private int _selectedAspectIndex;
|
||||||
[ObservableProperty] private string _attendanceLabel = "Noch nicht kontrolliert";
|
[ObservableProperty] private string _attendanceLabel = "Noch nicht kontrolliert";
|
||||||
[ObservableProperty] private string _homeworkLabel = "Keine Hausaufgabe aufgegeben";
|
[ObservableProperty] private string _homeworkLabel = "Keine Hausaufgabe aufgegeben";
|
||||||
|
[ObservableProperty] private string _dayHighlightLabel = "Keine Markierung";
|
||||||
|
|
||||||
public string StudentName { get; }
|
public string StudentName { get; }
|
||||||
public string SessionDisplay { get; }
|
public string SessionDisplay { get; }
|
||||||
@@ -687,6 +692,7 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
public ObservableCollection<SeatAssessmentAspectRow> AspectRows { get; } = [];
|
public ObservableCollection<SeatAssessmentAspectRow> AspectRows { get; } = [];
|
||||||
public ObservableCollection<SeatAttendanceChoice> AttendanceChoices { get; } = [];
|
public ObservableCollection<SeatAttendanceChoice> AttendanceChoices { get; } = [];
|
||||||
public ObservableCollection<SeatHomeworkChoice> HomeworkChoices { get; } = [];
|
public ObservableCollection<SeatHomeworkChoice> HomeworkChoices { get; } = [];
|
||||||
|
public ObservableCollection<SeatDayHighlightChoice> DayHighlightChoices { get; } = [];
|
||||||
|
|
||||||
public SeatAssessmentViewModel(IParticipationSessionRepository sessions,
|
public SeatAssessmentViewModel(IParticipationSessionRepository sessions,
|
||||||
IParticipationRepository entries, IParticipationAspectRepository aspects,
|
IParticipationRepository entries, IParticipationAspectRepository aspects,
|
||||||
@@ -729,6 +735,7 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
|
|
||||||
BuildAttendanceChoices();
|
BuildAttendanceChoices();
|
||||||
BuildHomeworkChoices();
|
BuildHomeworkChoices();
|
||||||
|
BuildDayHighlightChoices();
|
||||||
RefreshStatusChoices();
|
RefreshStatusChoices();
|
||||||
SelectAspect(0);
|
SelectAspect(0);
|
||||||
}
|
}
|
||||||
@@ -761,6 +768,17 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
HomeworkChoices.Add(new("·", "Keine aufgegeben", "⌥X", null, SetHomework));
|
HomeworkChoices.Add(new("·", "Keine aufgegeben", "⌥X", null, SetHomework));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildDayHighlightChoices()
|
||||||
|
{
|
||||||
|
DayHighlightChoices.Add(new(DayHighlightDisplay.Symbol(DayHighlightKind.Standout),
|
||||||
|
DayHighlightDisplay.Label(DayHighlightKind.Standout), "⇧1", DayHighlightKind.Standout, SetDayHighlight));
|
||||||
|
DayHighlightChoices.Add(new(DayHighlightDisplay.Symbol(DayHighlightKind.Sleepy),
|
||||||
|
DayHighlightDisplay.Label(DayHighlightKind.Sleepy), "⇧2", DayHighlightKind.Sleepy, SetDayHighlight));
|
||||||
|
DayHighlightChoices.Add(new(DayHighlightDisplay.Symbol(DayHighlightKind.Rough),
|
||||||
|
DayHighlightDisplay.Label(DayHighlightKind.Rough), "⇧3", DayHighlightKind.Rough, SetDayHighlight));
|
||||||
|
DayHighlightChoices.Add(new("·", "Keine Markierung", "⇧X", null, SetDayHighlight));
|
||||||
|
}
|
||||||
|
|
||||||
public void SelectAspect(int index)
|
public void SelectAspect(int index)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= AspectRows.Count) return;
|
if (index < 0 || index >= AspectRows.Count) return;
|
||||||
@@ -827,6 +845,17 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
if (clear || digit is 0 or 1 or 3 or 4 or 5 or 7 or 8) SetHomework(status);
|
if (clear || digit is 0 or 1 or 3 or 4 or 5 or 7 or 8) SetHomework(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ApplyDayHighlightShortcut(int? digit, bool clear)
|
||||||
|
{
|
||||||
|
if (!CanEdit) return;
|
||||||
|
var kind = clear ? null : digit switch
|
||||||
|
{
|
||||||
|
1 => DayHighlightKind.Standout, 2 => DayHighlightKind.Sleepy, 3 => DayHighlightKind.Rough,
|
||||||
|
_ => (DayHighlightKind?)null,
|
||||||
|
};
|
||||||
|
if (clear || digit is 1 or 2 or 3) SetDayHighlight(kind);
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyRating(string key, int? value)
|
private void ApplyRating(string key, int? value)
|
||||||
{
|
{
|
||||||
if (!CanEdit || _entry is null) return;
|
if (!CanEdit || _entry is null) return;
|
||||||
@@ -857,13 +886,23 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
RefreshStatusChoices();
|
RefreshStatusChoices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetDayHighlight(DayHighlightKind? kind)
|
||||||
|
{
|
||||||
|
if (!CanEdit || _entry is null) return;
|
||||||
|
_entry.DayHighlight = kind;
|
||||||
|
_entries.Save(_entry);
|
||||||
|
RefreshStatusChoices();
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshStatusChoices()
|
private void RefreshStatusChoices()
|
||||||
{
|
{
|
||||||
AttendanceLabel = AttendanceDisplay.Label(_entry?.Attendance);
|
AttendanceLabel = AttendanceDisplay.Label(_entry?.Attendance);
|
||||||
HomeworkLabel = HomeworkDisplay.Label(_entry is null ? null : HomeworkDisplay.Effective(_entry));
|
HomeworkLabel = HomeworkDisplay.Label(_entry is null ? null : HomeworkDisplay.Effective(_entry));
|
||||||
|
DayHighlightLabel = DayHighlightDisplay.Label(_entry?.DayHighlight);
|
||||||
foreach (var choice in AttendanceChoices) choice.IsSelected = choice.Status == _entry?.Attendance;
|
foreach (var choice in AttendanceChoices) choice.IsSelected = choice.Status == _entry?.Attendance;
|
||||||
var homework = _entry is null ? null : HomeworkDisplay.Effective(_entry);
|
var homework = _entry is null ? null : HomeworkDisplay.Effective(_entry);
|
||||||
foreach (var choice in HomeworkChoices) choice.IsSelected = choice.Status == homework;
|
foreach (var choice in HomeworkChoices) choice.IsSelected = choice.Status == homework;
|
||||||
|
foreach (var choice in DayHighlightChoices) choice.IsSelected = choice.Kind == _entry?.DayHighlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,6 +1005,17 @@ public partial class SeatHomeworkChoice(string symbol, string label, string shor
|
|||||||
[RelayCommand] private void Apply() => apply(Status);
|
[RelayCommand] private void Apply() => apply(Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class SeatDayHighlightChoice(string symbol, string label, string shortcut,
|
||||||
|
DayHighlightKind? kind, Action<DayHighlightKind?> apply) : ObservableObject
|
||||||
|
{
|
||||||
|
public string Symbol { get; } = symbol;
|
||||||
|
public string Label { get; } = label;
|
||||||
|
public string Shortcut { get; } = shortcut;
|
||||||
|
public DayHighlightKind? Kind { get; } = kind;
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
[RelayCommand] private void Apply() => apply(Kind);
|
||||||
|
}
|
||||||
|
|
||||||
public partial class SeatingPlanDialogViewModel : ObservableObject
|
public partial class SeatingPlanDialogViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly ISeatingPlanRepository _plans;
|
private readonly ISeatingPlanRepository _plans;
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
// ── Schüler-Leistungsüberblick + Zielnoten-Rechner (Nutzer-Feedback) ─────────────
|
||||||
|
//
|
||||||
|
// Zeigt die Leistungen eines einzelnen Schülers in einem Kurs im Überblick — Klausuren,
|
||||||
|
// Mitarbeit- und sonstige Noten, die daraus berechnete Zeugnisnote sowie zwei Rechner
|
||||||
|
// ("Zielnote": was brauche ich noch? / "Was-wäre-wenn": wie wirkt sich eine zusätzliche
|
||||||
|
// Klausurnote aus?). Bewusst pro Kurs statt fächerübergreifend, da Gewichtungsschema und
|
||||||
|
// Notensystem am Kurs hängen. Zwei Modi über IsTeacherMode: im Bewertermodus zusätzlich
|
||||||
|
// Kursdurchschnitt je Klausur und Bearbeitbarkeit der Mitarbeit-/Sonstige-Noten; im
|
||||||
|
// Schülermodus ausschließlich die eigenen Werte, rein lesend — gedacht, um den Bildschirm im
|
||||||
|
// Gespräch umzudrehen. Exam-Noten bleiben bewusst nur lesbar: eine Korrektur läuft weiterhin
|
||||||
|
// über den bestehenden ExamGradingDialog, der die Punkte-Struktur konsistent hält — direktes
|
||||||
|
// Überschreiben von Exam.Grade hier könnte mit ExamResult.Points/TotalPoints auseinanderlaufen.
|
||||||
|
public partial class StudentPerformanceOverviewViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IGradeRepository _grades;
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly IExamResultRepository _results;
|
||||||
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
|
private readonly IGradingSchemeRepository _schemes;
|
||||||
|
private readonly GradingService _grading;
|
||||||
|
|
||||||
|
private readonly Guid _groupId;
|
||||||
|
private readonly Guid _studentId;
|
||||||
|
private readonly GroupType _groupType;
|
||||||
|
private readonly GradingSystem _gradingSystem;
|
||||||
|
private readonly string _schoolYear;
|
||||||
|
|
||||||
|
private GradingScheme _scheme = new();
|
||||||
|
private double? _examsAverage;
|
||||||
|
private double? _participationAverage;
|
||||||
|
private double? _otherAverage;
|
||||||
|
private List<(string Grade, double Weight)> _examGradesRaw = [];
|
||||||
|
private List<(string Grade, double Weight)> _participationGradesRaw = [];
|
||||||
|
private List<(string Grade, double Weight)> _otherGradesRaw = [];
|
||||||
|
|
||||||
|
public string StudentName { get; }
|
||||||
|
public string GroupLabel { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
||||||
|
[ObservableProperty] private bool _isTeacherMode = true;
|
||||||
|
[ObservableProperty] private string _schemeSummary = "";
|
||||||
|
[ObservableProperty] private string? _examsAverageDisplay;
|
||||||
|
[ObservableProperty] private string? _participationAverageDisplay;
|
||||||
|
[ObservableProperty] private string? _otherAverageDisplay;
|
||||||
|
[ObservableProperty] private string _currentReportGradeDisplay = "";
|
||||||
|
|
||||||
|
[ObservableProperty] private string _targetGradeText = "";
|
||||||
|
[ObservableProperty] private GradeBucketKind _targetBucket = GradeBucketKind.Exams;
|
||||||
|
[ObservableProperty] private string _targetResultDisplay = "";
|
||||||
|
|
||||||
|
[ObservableProperty] private string _whatIfExamGradeText = "";
|
||||||
|
[ObservableProperty] private string _whatIfResultDisplay = "";
|
||||||
|
|
||||||
|
public List<ParticipationPeriodOption> PeriodOptions { get; } =
|
||||||
|
[
|
||||||
|
new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"),
|
||||||
|
new(ParticipationPeriod.H1, "1. Halbjahr"),
|
||||||
|
new(ParticipationPeriod.H2, "2. Halbjahr"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public string[] TargetBucketOptions { get; } = ["Klausuren", "Mitarbeit", "Sonstige"];
|
||||||
|
|
||||||
|
public string TargetBucketName
|
||||||
|
{
|
||||||
|
get => TargetBucket switch
|
||||||
|
{
|
||||||
|
GradeBucketKind.Participation => "Mitarbeit",
|
||||||
|
GradeBucketKind.Other => "Sonstige",
|
||||||
|
_ => "Klausuren",
|
||||||
|
};
|
||||||
|
set => TargetBucket = value switch
|
||||||
|
{
|
||||||
|
"Mitarbeit" => GradeBucketKind.Participation,
|
||||||
|
"Sonstige" => GradeBucketKind.Other,
|
||||||
|
_ => GradeBucketKind.Exams,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<StudentExamRow> ExamRows { get; } = [];
|
||||||
|
/// Eigener Verlauf (Nutzer-Feedback): dieselbe Balken-Sparkline wie die bestehende
|
||||||
|
/// Notenentwicklung im Schülerdetail (2.5), hier nur auf die Klausuren dieses Kurses
|
||||||
|
/// beschränkt statt aller Lerngruppen — passt zum Kursdurchschnitt-Vergleich je Klausur.
|
||||||
|
public StudentGradeHistoryGroup ExamHistory { get; } = new("Klausurverlauf");
|
||||||
|
public ObservableCollection<StudentSimpleGradeRow> ParticipationRows { get; } = [];
|
||||||
|
public ObservableCollection<StudentSimpleGradeRow> OtherRows { get; } = [];
|
||||||
|
|
||||||
|
public StudentPerformanceOverviewViewModel(IGradeRepository grades, IExamRepository exams,
|
||||||
|
IExamResultRepository results, IGroupMembershipRepository memberships,
|
||||||
|
IGradingSchemeRepository schemes, GradingService grading,
|
||||||
|
Guid groupId, Guid studentId, GroupType groupType, GradingSystem gradingSystem,
|
||||||
|
string studentName, string groupLabel, string schoolYear)
|
||||||
|
{
|
||||||
|
_grades = grades; _exams = exams; _results = results; _memberships = memberships;
|
||||||
|
_schemes = schemes; _grading = grading;
|
||||||
|
_groupId = groupId; _studentId = studentId; _groupType = groupType;
|
||||||
|
_gradingSystem = gradingSystem; _schoolYear = schoolYear;
|
||||||
|
StudentName = studentName; GroupLabel = groupLabel;
|
||||||
|
|
||||||
|
_selectedPeriod = PeriodOptions[0];
|
||||||
|
Recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedPeriodChanged(ParticipationPeriodOption value) => Recompute();
|
||||||
|
|
||||||
|
public string ModeButtonLabel => IsTeacherMode ? "Modus: Bewerter" : "Modus: Schüler";
|
||||||
|
|
||||||
|
partial void OnIsTeacherModeChanged(bool value) => OnPropertyChanged(nameof(ModeButtonLabel));
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleMode() => IsTeacherMode = !IsTeacherMode;
|
||||||
|
|
||||||
|
private GradingScheme ResolveScheme() =>
|
||||||
|
_schemes.GetByGroup(_groupId)
|
||||||
|
?? _schemes.GetDefaultForType(_groupType)
|
||||||
|
?? new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
||||||
|
|
||||||
|
private void Recompute()
|
||||||
|
{
|
||||||
|
_scheme = ResolveScheme();
|
||||||
|
SchemeSummary = $"Klausuren {_scheme.ExamsPercent:0.#} % · Mitarbeit {_scheme.ParticipationPercent:0.#} % · " +
|
||||||
|
$"Sonstige {_scheme.OtherPercent:0.#} %";
|
||||||
|
|
||||||
|
var (periodFrom, periodTo) = GroupMembershipService.SchoolYearPeriod(_schoolYear, SelectedPeriod.Period switch
|
||||||
|
{
|
||||||
|
ParticipationPeriod.H1 => SchoolYearPeriodKind.H1,
|
||||||
|
ParticipationPeriod.H2 => SchoolYearPeriodKind.H2,
|
||||||
|
_ => SchoolYearPeriodKind.FullYear,
|
||||||
|
});
|
||||||
|
var membership = _memberships.GetByGroup(_groupId).FirstOrDefault(m => m.StudentId == _studentId);
|
||||||
|
|
||||||
|
// ── Klausuren ─────────────────────────────────────────────────────
|
||||||
|
ExamRows.Clear();
|
||||||
|
ExamHistory.Points.Clear();
|
||||||
|
_examGradesRaw = [];
|
||||||
|
int? previousNoteEquivalent = null;
|
||||||
|
foreach (var exam in _exams.GetByGroup(_groupId)
|
||||||
|
.Where(e => e.Date >= periodFrom && e.Date <= periodTo)
|
||||||
|
.Where(e => membership is null || GroupMembershipService.IsActiveOn(membership, e.Date))
|
||||||
|
.Where(e => !e.Niveau.HasValue || membership?.Niveau == e.Niveau)
|
||||||
|
.OrderBy(e => e.Date))
|
||||||
|
{
|
||||||
|
var allResults = _results.GetByExam(exam.Id);
|
||||||
|
var own = allResults.FirstOrDefault(r => r.StudentId == _studentId);
|
||||||
|
string? ownGrade = own is { Absent: false, Grade: not null } ? own.Grade : null;
|
||||||
|
if (ownGrade is not null) _examGradesRaw.Add((ownGrade, 1.0));
|
||||||
|
|
||||||
|
var classGrades = allResults.Where(r => !r.Absent && r.Grade is not null)
|
||||||
|
.Select(r => (r.Grade!, 1.0)).ToList();
|
||||||
|
var classAverage = classGrades.Count > 0 ? _grading.WeightedAverage(classGrades) : (double?)null;
|
||||||
|
|
||||||
|
ExamRows.Add(new StudentExamRow(exam.Title, exam.Date, ownGrade,
|
||||||
|
classAverage.HasValue ? FormatGrade(classAverage.Value) : "–"));
|
||||||
|
|
||||||
|
// Gleiche Balken-Sparkline wie die bestehende Notenentwicklung (2.5): Rohwert auf die
|
||||||
|
// 1..6-Notenäquivalent-Achse abbilden, damit Grades1To6 und Points0To15 dieselbe
|
||||||
|
// Balkenhöhen-Formel nutzen können.
|
||||||
|
int? noteEquivalent = ownGrade is not null && int.TryParse(ownGrade, out var raw)
|
||||||
|
? (_gradingSystem == GradingSystem.Grades1To6 ? raw : int.Parse(PointsNoteMapping.PointsToNote(raw)))
|
||||||
|
: null;
|
||||||
|
var isFailing = noteEquivalent is >= 5;
|
||||||
|
var isDrop = noteEquivalent.HasValue && previousNoteEquivalent.HasValue
|
||||||
|
&& noteEquivalent.Value - previousNoteEquivalent.Value >= 1;
|
||||||
|
var warnings = new List<string>();
|
||||||
|
if (isDrop) warnings.Add("Abfall um ≥ 1 Note");
|
||||||
|
if (isFailing) warnings.Add("Versetzungsgefährdung");
|
||||||
|
ExamHistory.Points.Add(new GradeHistoryPoint(exam.Date, exam.Title, ownGrade ?? "–",
|
||||||
|
noteEquivalent, warnings.Count > 0, string.Join(" · ", warnings)));
|
||||||
|
if (noteEquivalent.HasValue) previousNoteEquivalent = noteEquivalent;
|
||||||
|
}
|
||||||
|
_examsAverage = _examGradesRaw.Count > 0 ? _grading.WeightedAverage(_examGradesRaw) : null;
|
||||||
|
|
||||||
|
// ── Mitarbeit & Sonstige ──────────────────────────────────────────
|
||||||
|
var allGrades = _grades.GetByStudentAndGroup(_studentId, _groupId)
|
||||||
|
.Where(g => g.Date >= periodFrom && g.Date <= periodTo)
|
||||||
|
.Where(g => membership is null || GroupMembershipService.IsActiveOn(membership, g.Date))
|
||||||
|
.OrderBy(g => g.Date).ToList();
|
||||||
|
|
||||||
|
ParticipationRows.Clear();
|
||||||
|
_participationGradesRaw = [];
|
||||||
|
foreach (var g in allGrades.Where(g => g.Category == GradeCategory.Participation))
|
||||||
|
{
|
||||||
|
_participationGradesRaw.Add((g.Value, g.Weight));
|
||||||
|
ParticipationRows.Add(new StudentSimpleGradeRow(g, SaveGrade));
|
||||||
|
}
|
||||||
|
_participationAverage = _participationGradesRaw.Count > 0 ? _grading.WeightedAverage(_participationGradesRaw) : null;
|
||||||
|
|
||||||
|
OtherRows.Clear();
|
||||||
|
_otherGradesRaw = [];
|
||||||
|
foreach (var g in allGrades.Where(g => g.Category != GradeCategory.Participation))
|
||||||
|
{
|
||||||
|
_otherGradesRaw.Add((g.Value, g.Weight));
|
||||||
|
OtherRows.Add(new StudentSimpleGradeRow(g, SaveGrade));
|
||||||
|
}
|
||||||
|
_otherAverage = _otherGradesRaw.Count > 0 ? _grading.WeightedAverage(_otherGradesRaw) : null;
|
||||||
|
|
||||||
|
ExamsAverageDisplay = _examsAverage.HasValue ? FormatGrade(_examsAverage.Value) : null;
|
||||||
|
ParticipationAverageDisplay = _participationAverage.HasValue ? FormatGrade(_participationAverage.Value) : null;
|
||||||
|
OtherAverageDisplay = _otherAverage.HasValue ? FormatGrade(_otherAverage.Value) : null;
|
||||||
|
|
||||||
|
var reportGrade = _grading.CalculateReportGrade(_examGradesRaw, _participationGradesRaw, _otherGradesRaw,
|
||||||
|
_scheme, _gradingSystem, RoundingRule.Commercial);
|
||||||
|
CurrentReportGradeDisplay = reportGrade is null
|
||||||
|
? "Noch nicht berechenbar — keine Werte im gewählten Zeitraum."
|
||||||
|
: $"Note {reportGrade}";
|
||||||
|
|
||||||
|
RecomputeTarget();
|
||||||
|
RecomputeWhatIf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveGrade(Grade grade)
|
||||||
|
{
|
||||||
|
_grades.Save(grade);
|
||||||
|
Recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnTargetGradeTextChanged(string value) => RecomputeTarget();
|
||||||
|
partial void OnTargetBucketChanged(GradeBucketKind value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(TargetBucketName));
|
||||||
|
RecomputeTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeTarget()
|
||||||
|
{
|
||||||
|
if (!TryParseGrade(TargetGradeText, out var target))
|
||||||
|
{
|
||||||
|
TargetResultDisplay = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ReportGradeTargetCalculator.SolveRequiredAverage(target, TargetBucket,
|
||||||
|
_examsAverage, _participationAverage, _otherAverage, _scheme, _gradingSystem);
|
||||||
|
|
||||||
|
TargetResultDisplay = result switch
|
||||||
|
{
|
||||||
|
null => $"{TargetBucketName} fließt laut Gewichtungsschema nicht in die Zeugnisnote ein.",
|
||||||
|
{ IsAchievable: true } r => $"Nötiger Schnitt in {TargetBucketName}: {FormatGrade(r.RequiredAverage)}",
|
||||||
|
{ IsAchievable: false } r =>
|
||||||
|
$"Mit den übrigen Bereichen rechnerisch nicht mehr erreichbar (bräuchte {FormatGrade(r.RequiredAverage)}).",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnWhatIfExamGradeTextChanged(string value) => RecomputeWhatIf();
|
||||||
|
|
||||||
|
private void RecomputeWhatIf()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(WhatIfExamGradeText))
|
||||||
|
{
|
||||||
|
WhatIfResultDisplay = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hypothetical = new List<(string Grade, double Weight)>(_examGradesRaw) { (WhatIfExamGradeText.Trim(), 1.0) };
|
||||||
|
var result = _grading.CalculateReportGrade(hypothetical, _participationGradesRaw, _otherGradesRaw,
|
||||||
|
_scheme, _gradingSystem, RoundingRule.Commercial);
|
||||||
|
WhatIfResultDisplay = result is null
|
||||||
|
? "Ungültige Note."
|
||||||
|
: $"Zeugnisnote mit dieser zusätzlichen Klausurnote: {result}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseGrade(string text, out double value) =>
|
||||||
|
double.TryParse(text.Trim().Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
||||||
|
|
||||||
|
private static string FormatGrade(double value) => value.ToString("0.0", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StudentExamRow(string title, DateOnly date, string? ownGrade, string classAverageDisplay)
|
||||||
|
{
|
||||||
|
public string Title { get; } = title;
|
||||||
|
public string DateDisplay { get; } = date.ToString("dd.MM.yyyy");
|
||||||
|
public string OwnGradeDisplay { get; } = ownGrade ?? "–";
|
||||||
|
public string ClassAverageDisplay { get; } = classAverageDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class StudentSimpleGradeRow : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly Grade _grade;
|
||||||
|
private readonly Action<Grade> _save;
|
||||||
|
|
||||||
|
public string CategoryLabel { get; }
|
||||||
|
public string DateDisplay { get; }
|
||||||
|
[ObservableProperty] private string _value;
|
||||||
|
|
||||||
|
public StudentSimpleGradeRow(Grade grade, Action<Grade> save)
|
||||||
|
{
|
||||||
|
_grade = grade;
|
||||||
|
_save = save;
|
||||||
|
CategoryLabel = GradeCategoryDisplay.Label(grade.Category);
|
||||||
|
DateDisplay = grade.Date.ToString("dd.MM.yyyy");
|
||||||
|
_value = grade.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
_grade.Value = Value.Trim();
|
||||||
|
_save(_grade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,9 @@
|
|||||||
<Button Content="Noten verwalten" Command="{Binding ManageStudentGradesCommand}" Margin="12,0,0,0"
|
<Button Content="Noten verwalten" Command="{Binding ManageStudentGradesCommand}" Margin="12,0,0,0"
|
||||||
IsVisible="{Binding SelectedRow, Converter={x:Static ObjectConverters.IsNotNull}}"
|
IsVisible="{Binding SelectedRow, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||||
IsEnabled="{Binding !IsReadOnly}"/>
|
IsEnabled="{Binding !IsReadOnly}"/>
|
||||||
|
<Button Content="Überblick" Command="{Binding ShowPerformanceOverviewCommand}" Margin="6,0,0,0"
|
||||||
|
IsVisible="{Binding SelectedRow, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||||
|
ToolTip.Tip="Leistungsüberblick mit Zielnoten-Rechner für diesen Schüler"/>
|
||||||
<Button Content="+ Sammelnote" Command="{Binding CollectiveGradeCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
<Button Content="+ Sammelnote" Command="{Binding CollectiveGradeCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||||
<Button Content="Zeugnisnoten" Command="{Binding ReportGradesCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
<Button Content="Zeugnisnoten" Command="{Binding ReportGradesCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||||
<Button Content="Als PDF" Click="OnExportPdfClick" Margin="12,0,0,0"
|
<Button Content="Als PDF" Click="OnExportPdfClick" Margin="12,0,0,0"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public partial class GradeOverviewTabView : UserControl
|
|||||||
vm.OnManageStudentGrades = ShowStudentGradesDialog;
|
vm.OnManageStudentGrades = ShowStudentGradesDialog;
|
||||||
vm.OnCollectiveGrade = ShowCollectiveGradeDialog;
|
vm.OnCollectiveGrade = ShowCollectiveGradeDialog;
|
||||||
vm.OnReportGrades = ShowReportGradesDialog;
|
vm.OnReportGrades = ShowReportGradesDialog;
|
||||||
|
vm.OnShowPerformanceOverview = ShowPerformanceOverviewDialog;
|
||||||
vm.PropertyChanged += (_, pe) =>
|
vm.PropertyChanged += (_, pe) =>
|
||||||
{
|
{
|
||||||
if (pe.PropertyName == nameof(GradeOverviewTabViewModel.RebuildColumnsSignal))
|
if (pe.PropertyName == nameof(GradeOverviewTabViewModel.RebuildColumnsSignal))
|
||||||
@@ -79,6 +80,22 @@ public partial class GradeOverviewTabView : UserControl
|
|||||||
if (owner is not null) await dialog.ShowDialog(owner);
|
if (owner is not null) await dialog.ShowDialog(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ShowPerformanceOverviewDialog(GradeOverviewRow row)
|
||||||
|
{
|
||||||
|
var dialogVm = new StudentPerformanceOverviewViewModel(
|
||||||
|
App.Services.GetRequiredService<IGradeRepository>(),
|
||||||
|
App.Services.GetRequiredService<IExamRepository>(),
|
||||||
|
App.Services.GetRequiredService<IExamResultRepository>(),
|
||||||
|
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||||
|
App.Services.GetRequiredService<IGradingSchemeRepository>(),
|
||||||
|
App.Services.GetRequiredService<GradingService>(),
|
||||||
|
_vm!.GroupId, row.StudentId, _vm.GroupType, _vm.GradingSystem, row.Name, _vm.GroupLabel, _vm.SchoolYear);
|
||||||
|
|
||||||
|
var dialog = new StudentPerformanceOverviewDialog { DataContext = dialogVm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is not null) await dialog.ShowDialog(owner);
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnExportPdfClick(object? sender, RoutedEventArgs e)
|
private async void OnExportPdfClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
|||||||
@@ -12,8 +12,12 @@
|
|||||||
<!-- Schülername + Fortschritt -->
|
<!-- Schülername + Fortschritt -->
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
||||||
<StackPanel Grid.Column="0">
|
<StackPanel Grid.Column="0">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"
|
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"
|
||||||
Opacity="{Binding CurrentStudentContentOpacity}"/>
|
Opacity="{Binding CurrentStudentContentOpacity}"/>
|
||||||
|
<TextBlock Text="{Binding CurrentStudentDayHighlightSymbol}" FontSize="20"
|
||||||
|
VerticalAlignment="Center" ToolTip.Tip="{Binding CurrentStudentDayHighlightLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
<TextBlock Text="{Binding CurrentAspectLabel}" FontSize="13" Opacity="0.5"
|
<TextBlock Text="{Binding CurrentAspectLabel}" FontSize="13" Opacity="0.5"
|
||||||
IsVisible="{Binding !CurrentStudentIsAbsent}"/>
|
IsVisible="{Binding !CurrentStudentIsAbsent}"/>
|
||||||
<TextBlock Text="{Binding CurrentStudentAttendanceLabel, StringFormat='⚠ Abwesend — {0}'}"
|
<TextBlock Text="{Binding CurrentStudentAttendanceLabel, StringFormat='⚠ Abwesend — {0}'}"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Groups;
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
@@ -19,6 +20,18 @@ public partial class ParticipationQuickInputDialog : Window
|
|||||||
{
|
{
|
||||||
if (DataContext is not QuickInputViewModel vm) { base.OnKeyDown(e); return; }
|
if (DataContext is not QuickInputViewModel vm) { base.OnKeyDown(e); return; }
|
||||||
|
|
||||||
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||||
|
{
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.D1 or Key.NumPad1: vm.SetDayHighlight(DayHighlightKind.Standout); e.Handled = true; break;
|
||||||
|
case Key.D2 or Key.NumPad2: vm.SetDayHighlight(DayHighlightKind.Sleepy); e.Handled = true; break;
|
||||||
|
case Key.D3 or Key.NumPad3: vm.SetDayHighlight(DayHighlightKind.Rough); e.Handled = true; break;
|
||||||
|
case Key.D0 or Key.NumPad0: vm.SetDayHighlight(null); e.Handled = true; break;
|
||||||
|
}
|
||||||
|
if (e.Handled) return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (e.Key)
|
switch (e.Key)
|
||||||
{
|
{
|
||||||
case Key.D0 or Key.NumPad0: vm.SetRatingByNumber(0); e.Handled = true; break;
|
case Key.D0 or Key.NumPad0: vm.SetRatingByNumber(0); e.Handled = true; break;
|
||||||
|
|||||||
@@ -128,10 +128,30 @@
|
|||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="7">
|
||||||
|
<TextBlock Text="TAGESFLAGGE" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding DayHighlightLabel}" FontSize="12"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding DayHighlightChoices}">
|
||||||
|
<ItemsControl.ItemsPanel><ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SeatDayHighlightChoice">
|
||||||
|
<Button Classes="choice" Classes.selected="{Binding IsSelected}"
|
||||||
|
Command="{Binding ApplyCommand}" Margin="2">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<TextBlock Text="{Binding Symbol}" FontWeight="Bold"/>
|
||||||
|
<TextBlock Text="{Binding Label}"/>
|
||||||
|
<TextBlock Text="{Binding Shortcut}" FontSize="9" Opacity="0.5" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Margin="0,12,0,0" FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
<TextBlock Grid.Row="2" Margin="0,12,0,0" FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
||||||
Text="Mitarbeit: Q/W/E/R/T Aspekt · 1–5 Bewertung · +/− anpassen · Backspace löschen · ←/→ Aspekt | Anwesenheit: Strg+1/2/5/7/9/0, Strg+X | Hausaufgaben: ⌥+1/3/4/5/7/8/0, ⌥+X | Esc schließen"/>
|
Text="Mitarbeit: Q/W/E/R/T Aspekt · 1–5 Bewertung · +/− anpassen · Backspace löschen · ←/→ Aspekt | Anwesenheit: Strg+1/2/5/7/9/0, Strg+X | Hausaufgaben: ⌥+1/3/4/5/7/8/0, ⌥+X | Tagesflagge: ⇧1/2/3, ⇧X | Esc schließen"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ public partial class SeatAssessmentDialog : Window
|
|||||||
e.Handled = clear || digit is not null;
|
e.Handled = clear || digit is not null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||||
|
{
|
||||||
|
if (clear || digit is not null) vm.ApplyDayHighlightShortcut(digit, clear);
|
||||||
|
e.Handled = clear || digit is not null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (digit is not null)
|
if (digit is not null)
|
||||||
{
|
{
|
||||||
vm.SetRatingByNumber(digit.Value);
|
vm.SetRatingByNumber(digit.Value);
|
||||||
|
|||||||
@@ -154,6 +154,9 @@
|
|||||||
<TextBlock Text="{Binding HomeworkBadge}" Foreground="White" FontSize="10"
|
<TextBlock Text="{Binding HomeworkBadge}" Foreground="White" FontSize="10"
|
||||||
ToolTip.Tip="Hausaufgabe"/>
|
ToolTip.Tip="Hausaufgabe"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
<TextBlock Text="{Binding DayHighlightBadge}" FontSize="14"
|
||||||
|
IsVisible="{Binding HasDayHighlightBadge}"
|
||||||
|
ToolTip.Tip="Tagesflagge"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<!-- Strichliste Meldungen (Nutzer-Feedback): schnelles Mitzählen ohne den
|
<!-- Strichliste Meldungen (Nutzer-Feedback): schnelles Mitzählen ohne den
|
||||||
vollen Bewertungsdialog zu öffnen -->
|
vollen Bewertungsdialog zu öffnen -->
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
xmlns:students="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.StudentPerformanceOverviewDialog"
|
||||||
|
x:DataType="vm:StudentPerformanceOverviewViewModel"
|
||||||
|
Title="Leistungsüberblick"
|
||||||
|
Width="920" Height="720" MinWidth="700" MinHeight="520"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="24">
|
||||||
|
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="2">
|
||||||
|
<TextBlock Text="{Binding StudentName}" Classes="dialogtitle"/>
|
||||||
|
<TextBlock Text="{Binding GroupLabel}" FontSize="12" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="{Binding ModeButtonLabel}"
|
||||||
|
Command="{Binding ToggleModeCommand}" VerticalAlignment="Top"
|
||||||
|
ToolTip.Tip="Zwischen Bewerter- und Schülermodus umschalten"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="12" Margin="0,12,0,10">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Text="Zeitraum:" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox ItemsSource="{Binding PeriodOptions}" SelectedItem="{Binding SelectedPeriod}" MinWidth="170"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="{Binding SchemeSummary}" FontSize="12" Opacity="0.6" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding IsTeacherMode}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="2">
|
||||||
|
<Grid ColumnDefinitions="*,300">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="18" Margin="0,0,16,0">
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="KLAUSUREN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding ExamsAverageDisplay, StringFormat='Ø {0}'}"
|
||||||
|
FontSize="12" Opacity="0.7" IsVisible="{Binding ExamsAverageDisplay, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</Grid>
|
||||||
|
<ItemsControl ItemsSource="{Binding ExamRows}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:StudentExamRow">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,80,80" Margin="0,4">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="12" Opacity="0.6" Width="80"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Title}" FontSize="13"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding OwnGradeDisplay}" FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Right"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding ClassAverageDisplay}" Opacity="0.55"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
IsVisible="{Binding $parent[ItemsControl].((vm:StudentPerformanceOverviewViewModel)DataContext).IsTeacherMode}"
|
||||||
|
ToolTip.Tip="Kursdurchschnitt dieser Klausur"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine Klausuren im gewählten Zeitraum." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !ExamRows.Count}"/>
|
||||||
|
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||||
|
IsVisible="{Binding !!ExamHistory.Points.Count}">
|
||||||
|
<ItemsControl ItemsSource="{Binding ExamHistory.Points}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="10"/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="students:GradeHistoryPoint">
|
||||||
|
<StackPanel Width="46" VerticalAlignment="Bottom" ToolTip.Tip="{Binding TooltipText}">
|
||||||
|
<Border Classes="historybar" Height="{Binding BarHeight}" Width="16" CornerRadius="2"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||||
|
Classes.warning="{Binding IsWarning}"/>
|
||||||
|
<TextBlock Text="{Binding Value}" FontSize="11" FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center" Margin="0,3,0,0"/>
|
||||||
|
<TextBlock Text="{Binding DateDisplay}" FontSize="9" Opacity="0.5"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
<ItemsControl.Styles>
|
||||||
|
<Style Selector="Border.historybar">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.historybar.warning">
|
||||||
|
<Setter Property="Background" Value="#E53935"/>
|
||||||
|
</Style>
|
||||||
|
</ItemsControl.Styles>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="MITARBEIT" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding ParticipationAverageDisplay, StringFormat='Ø {0}'}"
|
||||||
|
FontSize="12" Opacity="0.7" IsVisible="{Binding ParticipationAverageDisplay, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</Grid>
|
||||||
|
<ItemsControl ItemsSource="{Binding ParticipationRows}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:StudentSimpleGradeRow">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,70,Auto" Margin="0,4">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="12" Opacity="0.6" Width="80"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding CategoryLabel}" FontSize="13"/>
|
||||||
|
<TextBox Grid.Column="2" Text="{Binding Value}"
|
||||||
|
IsVisible="{Binding $parent[ItemsControl].((vm:StudentPerformanceOverviewViewModel)DataContext).IsTeacherMode}"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding Value}" FontWeight="SemiBold" HorizontalAlignment="Right"
|
||||||
|
IsVisible="{Binding !$parent[ItemsControl].((vm:StudentPerformanceOverviewViewModel)DataContext).IsTeacherMode}"/>
|
||||||
|
<Button Grid.Column="3" Content="Speichern" Command="{Binding SaveCommand}" FontSize="11" Padding="8,3" Margin="6,0,0,0"
|
||||||
|
IsVisible="{Binding $parent[ItemsControl].((vm:StudentPerformanceOverviewViewModel)DataContext).IsTeacherMode}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine Mitarbeitsnote im gewählten Zeitraum." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !ParticipationRows.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="SONSTIGE" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding OtherAverageDisplay, StringFormat='Ø {0}'}"
|
||||||
|
FontSize="12" Opacity="0.7" IsVisible="{Binding OtherAverageDisplay, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</Grid>
|
||||||
|
<ItemsControl ItemsSource="{Binding OtherRows}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:StudentSimpleGradeRow">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,70,Auto" Margin="0,4">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="12" Opacity="0.6" Width="80"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding CategoryLabel}" FontSize="13"/>
|
||||||
|
<TextBox Grid.Column="2" Text="{Binding Value}"
|
||||||
|
IsVisible="{Binding $parent[ItemsControl].((vm:StudentPerformanceOverviewViewModel)DataContext).IsTeacherMode}"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding Value}" FontWeight="SemiBold" HorizontalAlignment="Right"
|
||||||
|
IsVisible="{Binding !$parent[ItemsControl].((vm:StudentPerformanceOverviewViewModel)DataContext).IsTeacherMode}"/>
|
||||||
|
<Button Grid.Column="3" Content="Speichern" Command="{Binding SaveCommand}" FontSize="11" Padding="8,3" Margin="6,0,0,0"
|
||||||
|
IsVisible="{Binding $parent[ItemsControl].((vm:StudentPerformanceOverviewViewModel)DataContext).IsTeacherMode}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine sonstigen Noten im gewählten Zeitraum." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !OtherRows.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="14">
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock Text="ZEUGNISNOTE (BERECHNET)" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding CurrentReportGradeDisplay}" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Spacing="16">
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="14">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="ZIELNOTE: WAS BRAUCHE ICH NOCH?" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Text="Ziel:" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Text="{Binding TargetGradeText}" Width="70" PlaceholderText="z.B. 2"/>
|
||||||
|
<TextBlock Text="in" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox ItemsSource="{Binding TargetBucketOptions}" SelectedItem="{Binding TargetBucketName}" MinWidth="110"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="{Binding TargetResultDisplay}" TextWrapping="Wrap" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="14">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="WAS-WÄRE-WENN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Text="Zusätzliche Klausurnote:" VerticalAlignment="Center" TextWrapping="Wrap"/>
|
||||||
|
<TextBox Text="{Binding WhatIfExamGradeText}" Width="70" PlaceholderText="z.B. 3"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="{Binding WhatIfResultDisplay}" TextWrapping="Wrap" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="0,16,0,0">
|
||||||
|
<Button Grid.Column="1" Content="Schließen" Click="OnClose"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class StudentPerformanceOverviewDialog : Window
|
||||||
|
{
|
||||||
|
public StudentPerformanceOverviewDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Tests;
|
||||||
|
|
||||||
|
/// Tests für die Zielnoten-Frage (Nutzer-Feedback): "Was brauche ich noch, um Note X zu
|
||||||
|
/// erreichen?" — Umkehrung von GradingService.CalculateReportGrade.
|
||||||
|
public sealed class ReportGradeTargetCalculatorTests
|
||||||
|
{
|
||||||
|
private static readonly GradingScheme Scheme = new()
|
||||||
|
{
|
||||||
|
ExamsPercent = 50, ParticipationPercent = 30, OtherPercent = 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SolveRequiredAverage_LoestNachDemGesuchtenBereichAuf()
|
||||||
|
{
|
||||||
|
// Ziel 2,0; Mitarbeit 3,0 (30%), Sonstige 2,0 (20%) bekannt -> Klausuren (50%) gesucht.
|
||||||
|
// 2,0 * 100 = x*50 + 3,0*30 + 2,0*20 => 200 = 50x + 90 + 40 => x = 1,4
|
||||||
|
var result = ReportGradeTargetCalculator.SolveRequiredAverage(
|
||||||
|
2.0, GradeBucketKind.Exams, examsAverage: null, participationAverage: 3.0, otherAverage: 2.0,
|
||||||
|
Scheme, GradingSystem.Grades1To6);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(1.4, result!.Value.RequiredAverage, precision: 4);
|
||||||
|
Assert.True(result.Value.IsAchievable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SolveRequiredAverage_IgnoriertBereicheOhneBekanntenDurchschnitt()
|
||||||
|
{
|
||||||
|
// Nur Klausuren (50%) bekannt, Mitarbeit gesucht (30%), Sonstige ohne Daten (fällt raus).
|
||||||
|
// Ziel 2,0 nur aus Klausuren(50%, Ø 2,0) + Mitarbeit(30%, gesucht) => Gesamtprozent 80.
|
||||||
|
// 2,0*80 = 2,0*50 + x*30 => 160 = 100 + 30x => x = 2,0
|
||||||
|
var result = ReportGradeTargetCalculator.SolveRequiredAverage(
|
||||||
|
2.0, GradeBucketKind.Participation, examsAverage: 2.0, participationAverage: null, otherAverage: null,
|
||||||
|
Scheme, GradingSystem.Grades1To6);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(2.0, result!.Value.RequiredAverage, precision: 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SolveRequiredAverage_AusserhalbDesGueltigenBereichs_IstNichtErreichbar()
|
||||||
|
{
|
||||||
|
// Mitarbeit bereits sehr schlecht (5,5) -> selbst eine Traumnote in Klausuren reicht nicht für 1,5.
|
||||||
|
var result = ReportGradeTargetCalculator.SolveRequiredAverage(
|
||||||
|
1.5, GradeBucketKind.Exams, examsAverage: null, participationAverage: 5.5, otherAverage: null,
|
||||||
|
Scheme, GradingSystem.Grades1To6);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.False(result!.Value.IsAchievable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SolveRequiredAverage_BereichOhneGewichtungsanteil_GibtNullZurueck()
|
||||||
|
{
|
||||||
|
var scheme = new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 };
|
||||||
|
|
||||||
|
var result = ReportGradeTargetCalculator.SolveRequiredAverage(
|
||||||
|
2.0, GradeBucketKind.Participation, examsAverage: 2.0, participationAverage: null, otherAverage: null,
|
||||||
|
scheme, GradingSystem.Grades1To6);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SolveRequiredAverage_FunktioniertAufDerPunkteskala()
|
||||||
|
{
|
||||||
|
// Punktesystem: höher ist besser. Ziel 12 Punkte, Mitarbeit 10 (30%), Sonstige 8 (20%)
|
||||||
|
// bekannt -> Klausuren (50%) gesucht. 12*100 = x*50+10*30+8*20 => 1200=50x+300+160 => x=14,8
|
||||||
|
var result = ReportGradeTargetCalculator.SolveRequiredAverage(
|
||||||
|
12.0, GradeBucketKind.Exams, examsAverage: null, participationAverage: 10.0, otherAverage: 8.0,
|
||||||
|
Scheme, GradingSystem.Points0To15);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(14.8, result!.Value.RequiredAverage, precision: 4);
|
||||||
|
Assert.True(result.Value.IsAchievable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -221,6 +221,38 @@ Tooltip und nicht sichtbar auf der Kachel — das führte zu Verwirrung, welche
|
|||||||
und wurde ergänzt (`GradeHistoryPoint.Label` jetzt auch unter dem Balken sichtbar, nicht nur im
|
und wurde ergänzt (`GradeHistoryPoint.Label` jetzt auch unter dem Balken sichtbar, nicht nur im
|
||||||
Tooltip), siehe [StudentDetailView.axaml](LehrerApp.Desktop/Views/Students/StudentDetailView.axaml).
|
Tooltip), siehe [StudentDetailView.axaml](LehrerApp.Desktop/Views/Students/StudentDetailView.axaml).
|
||||||
|
|
||||||
|
### 2.6 Leistungsüberblick & Zielnoten-Rechner (Nutzer-Feedback)
|
||||||
|
|
||||||
|
Die Zielnoten-Frage ("was brauche ich noch für Note X?") kommt wiederkehrend auf, meist im
|
||||||
|
Zusammenhang mit einer frisch geschriebenen Klausur und Blick auf die Zeugnisnote — bisher ohne
|
||||||
|
eigenes Werkzeug, nur im Kopf überschlagen.
|
||||||
|
|
||||||
|
- [x] **2.6.1** Neuer Dialog `StudentPerformanceOverviewDialog`, erreichbar über den Button
|
||||||
|
"Überblick" in der Notenübersicht der Gruppe (2.1) bei ausgewähltem Schüler — bewusst pro
|
||||||
|
Kurs statt fächerübergreifend, da Gewichtungsschema und Notensystem am Kurs hängen. Zeigt
|
||||||
|
Klausuren (mit Datum, eigener Note), Mitarbeit- und sonstige Noten sowie die daraus mit
|
||||||
|
`GradingService.CalculateReportGrade` berechnete aktuelle Zeugnisnote, Zeitraum wählbar
|
||||||
|
(Gesamtjahr/H1/H2, gleiches Muster wie 2.4).
|
||||||
|
- [x] **2.6.2** Zielnoten-Rechner: neuer `ReportGradeTargetCalculator` (Core) kehrt die
|
||||||
|
Zeugnisnoten-Formel um — löst nach dem Durchschnitt auf, den ein gewählter Bereich
|
||||||
|
(Klausuren/Mitarbeit/Sonstige) noch erreichen muss, damit die gewichtete Gesamtnote ein
|
||||||
|
eingegebenes Ziel trifft; meldet ausdrücklich, wenn das Ziel rechnerisch nicht mehr
|
||||||
|
erreichbar ist (nötiger Durchschnitt außerhalb der gültigen Notenskala).
|
||||||
|
- [x] **2.6.3** Was-wäre-wenn-Rechner: eine hypothetische zusätzliche Klausurnote eintragen und
|
||||||
|
sofort die sich daraus ergebende Zeugnisnote sehen, ohne zu speichern — deckt den
|
||||||
|
"ich habe gerade eine Arbeit zurückbekommen, was bedeutet das"-Fall ab.
|
||||||
|
- [x] **2.6.4** Bewerter-/Schülermodus: ein Umschalt-Button in einem einzigen Fenster (kein
|
||||||
|
getrennter Einstieg) statt zweier Fenster, gedacht zum Bildschirm-Umdrehen im Gespräch.
|
||||||
|
Im Bewertermodus zusätzlich der Kursdurchschnitt je Klausur (Ausreißer nach oben/unten
|
||||||
|
erkennbar) und Bearbeitbarkeit der Mitarbeit-/Sonstige-Noten inkl. Speichern; im
|
||||||
|
Schülermodus ausschließlich die eigenen Werte, rein lesend. **Bewusst nur Mitarbeit-/
|
||||||
|
Sonstige-Noten direkt editierbar, keine Klausurnoten:** eine Korrektur dort bliebe
|
||||||
|
inkonsistent zu `ExamResult.Points`/`TotalPoints` — dafür weiterhin der bestehende
|
||||||
|
`ExamGradingDialog` im Klausuren-Tab.
|
||||||
|
**Bewusst zurückgestellt:** eine Übersicht über alle Fächer eines Schülers gleichzeitig
|
||||||
|
(Gesamt-Zeugnisvorschau) — unterschiedliche Notenschemata/Gewichtungen pro Kurs wären ein
|
||||||
|
eigener, größerer Umbau.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Mündliche Mitarbeit — offene Punkte
|
## 3. Mündliche Mitarbeit — offene Punkte
|
||||||
@@ -370,6 +402,19 @@ Mitarbeit-Note für denselben Zeitraum statt sie zu duplizieren (erkannt über d
|
|||||||
"quantity"-Aspekt mit Skalentyp `Scale5`) — als sichtbarer Vorschlag mit einem Klick zum
|
"quantity"-Aspekt mit Skalentyp `Scale5`) — als sichtbarer Vorschlag mit einem Klick zum
|
||||||
Übernehmen, nie automatisch gesetzt, damit eine bereits vorhandene bewusste Bewertung nie
|
Übernehmen, nie automatisch gesetzt, damit eine bereits vorhandene bewusste Bewertung nie
|
||||||
stillschweigend überschrieben wird.
|
stillschweigend überschrieben wird.
|
||||||
|
- [x] **3.3.6** Tagesflagge je Schüler/Sitzung (Nutzer-Feedback): "Spitzentag" (👑),
|
||||||
|
"Schlaftag" (😴) und "Schlechter Tag" (⚡) — herausragende Leistungen egal in welche
|
||||||
|
Richtung eindrücklich festhalten, unabhängig von der eigentlichen Aspektbewertung. Neues
|
||||||
|
Feld `ParticipationEntry.DayHighlight` (nullable `DayHighlightKind`, genau eine Flagge pro
|
||||||
|
Eintrag statt kombinierbarer Kennzeichnungen). Primär im Sitzplatz-Dialog
|
||||||
|
(`SeatAssessmentDialog`, ⇧1/2/3 zum Setzen, ⇧X zum Löschen, gleiches Muster wie
|
||||||
|
Anwesenheit/Hausaufgabe) sowie als Badge direkt auf der Sitzplatz-Kachel; zusätzlich als
|
||||||
|
Fallback in der session-weiten Schnelleingabe (`ParticipationQuickInputDialog`, ⇧1/2/3/0)
|
||||||
|
für Lerngruppen ohne Sitzplan. Rein deskriptiv — fließt nirgends in eine Berechnung
|
||||||
|
(Mitarbeitsnote 3.2, Aufrufgerechtigkeits-Quote) ein. **Bewusst noch ohne Diagramm-Anzeige:**
|
||||||
|
die bestehende Notenentwicklung (2.5) zeigt ausschließlich Klausur-/Einzelnoten-Einträge,
|
||||||
|
keine Mitarbeit-Sitzungen — eine visuelle Markierung "im Diagramm" ist erst mit der
|
||||||
|
geplanten Schüler-Überblicksansicht sinnvoll umsetzbar.
|
||||||
|
|
||||||
### Abschnittsnoten & Mitarbeits-Assistent (nicht aus dieser Liste, eigener Workflow-Bedarf)
|
### Abschnittsnoten & Mitarbeits-Assistent (nicht aus dieser Liste, eigener Workflow-Bedarf)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user