Compare commits
2
Commits
dade41ee8d
...
da4b88c93d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da4b88c93d | ||
|
|
538ad65a3a |
@@ -24,6 +24,14 @@ public class ParticipationEntry
|
|||||||
// Bleibt für bereits gespeicherte Daten erhalten. Neue Schreibvorgänge setzen beide Felder.
|
// Bleibt für bereits gespeicherte Daten erhalten. Neue Schreibvorgänge setzen beide Felder.
|
||||||
public bool HomeworkMissing { get; set; }
|
public bool HomeworkMissing { get; set; }
|
||||||
public AttendanceStatus? Attendance { get; set; }
|
public AttendanceStatus? Attendance { get; set; }
|
||||||
|
// Strichliste im Sitzplan (Nutzer-Feedback): RaisedHandCount zählt jede beobachtete Meldung,
|
||||||
|
// CalledOnCount die Teilmenge davon, bei der der Schüler auch drangekommen ist —
|
||||||
|
// "drangekommen" ist immer auch eine Meldung und erhöht daher immer beide Zähler zugleich,
|
||||||
|
// nie CalledOnCount allein. Ergibt nebenbei eine Aufrufgerechtigkeits-Quote
|
||||||
|
// (CalledOnCount / RaisedHandCount) und schlägt die Quantitätsbewertung vor, siehe
|
||||||
|
// ParticipationCountSuggestion.
|
||||||
|
public int RaisedHandCount { get; set; }
|
||||||
|
public int CalledOnCount { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,3 +168,27 @@ public static class ParticipationRatingScale
|
|||||||
_ => value,
|
_ => value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nutzer-Feedback: die Strichliste im Sitzplan (<see cref="ParticipationEntry.RaisedHandCount"/>)
|
||||||
|
/// schlägt direkt eine Quantitätsbewertung vor — wie oft sich jemand meldet, ist ja bereits die
|
||||||
|
/// Quantität. Nur ein Richtwert, kein fester Automatismus: die Lehrkraft übernimmt den Vorschlag
|
||||||
|
/// per Klick oder ignoriert ihn. Bewusst nur für <see cref="AspectValueType.Scale5"/> (der
|
||||||
|
/// Standardtyp des "quantity"-Aspekts) — für individuell umkonfigurierte Aspekttypen gäbe es keine
|
||||||
|
/// sinnvolle, unmissverständliche Abbildung.
|
||||||
|
/// </summary>
|
||||||
|
public static class ParticipationCountSuggestion
|
||||||
|
{
|
||||||
|
public static int? SuggestQuantity(AspectValueType type, int raisedHandCount)
|
||||||
|
{
|
||||||
|
if (type != AspectValueType.Scale5) return null;
|
||||||
|
return raisedHandCount switch
|
||||||
|
{
|
||||||
|
0 => -2,
|
||||||
|
1 => -1,
|
||||||
|
2 or 3 => 0,
|
||||||
|
4 or 5 => 1,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed class DashboardSettingsService
|
|||||||
public static readonly string[] DefaultCardOrder =
|
public static readonly string[] DefaultCardOrder =
|
||||||
[
|
[
|
||||||
"today", "tasks", "calendar", "excuses", "upcoming",
|
"today", "tasks", "calendar", "excuses", "upcoming",
|
||||||
"corrections", "unplanned", "alerts", "attendance", "support", "groups",
|
"corrections", "unplanned", "alerts", "attendance", "support", "groups", "examload",
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly string _configPath;
|
private readonly string _configPath;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
|
||||||
|
namespace LehrerApp.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zählt eigene Klausuren pro ISO-Kalenderwoche über alle Lerngruppen hinweg. Nutzer-Feedback:
|
||||||
|
/// die klassenbezogene Kollisionsprüfung (mehrere Klausuren einer einzelnen Klasse in einer
|
||||||
|
/// Woche) übernimmt bereits der schulische Klausurplaner (externes Klassenbuch) — was dort
|
||||||
|
/// fehlt, ist die persönliche Belastung der Lehrkraft: mehr eigene Klausuren in derselben Woche
|
||||||
|
/// bedeuten unabhängig von der Klasse mehr Erstellungsaufwand vorher und mehr Korrekturaufwand
|
||||||
|
/// danach. Da eine LiteDB-Datei genau einem Nutzer gehört (siehe CLAUDE.md), sind "alle
|
||||||
|
/// Klausuren in der Datenbank" bereits gleichbedeutend mit "meine Klausuren".
|
||||||
|
/// </summary>
|
||||||
|
public static class ExamWeekLoadService
|
||||||
|
{
|
||||||
|
/// Ab dieser Anzahl eigener Klausuren in derselben Woche gilt die Woche als spürbar belastet.
|
||||||
|
/// Nutzer-Feedback: zwei Klausuren aus zwei unterrichteten Fächern in derselben Woche sind
|
||||||
|
/// normal, eine dritte ist die eigentliche Häufung.
|
||||||
|
public const int WarningThreshold = 3;
|
||||||
|
|
||||||
|
/// Anzahl Klausuren (ohne <paramref name="excludeExamId"/>, z.B. die gerade im Dialog
|
||||||
|
/// bearbeitete), die in dieselbe ISO-Kalenderwoche wie <paramref name="date"/> fallen.
|
||||||
|
public static int CountInSameWeek(IEnumerable<Exam> allExams, DateOnly date, Guid? excludeExamId = null)
|
||||||
|
{
|
||||||
|
var (year, week) = IsoWeek(date);
|
||||||
|
return allExams.Count(e => e.Id != excludeExamId && IsoWeek(e.Date) == (year, week));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gruppiert die übergebenen Klausuren nach ISO-Kalenderwoche und liefert nur die Wochen mit
|
||||||
|
/// mindestens <see cref="WarningThreshold"/> Klausuren, chronologisch sortiert — für einen
|
||||||
|
/// vorausschauenden Überblick (z.B. Dashboard), nicht für die Einzelprüfung beim Anlegen.
|
||||||
|
public static List<ExamWeekLoad> FindOverloadedWeeks(IEnumerable<Exam> exams) =>
|
||||||
|
exams
|
||||||
|
.GroupBy(e => IsoWeek(e.Date))
|
||||||
|
.Where(g => g.Count() >= WarningThreshold)
|
||||||
|
.Select(g => new ExamWeekLoad(g.Key.Year, g.Key.Week, WeekStart(g.Key.Year, g.Key.Week), g.Count()))
|
||||||
|
.OrderBy(w => w.WeekStart)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static (int Year, int Week) IsoWeek(DateOnly date)
|
||||||
|
{
|
||||||
|
var dt = date.ToDateTime(TimeOnly.MinValue);
|
||||||
|
return (ISOWeek.GetYear(dt), ISOWeek.GetWeekOfYear(dt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateOnly WeekStart(int isoYear, int isoWeek) =>
|
||||||
|
DateOnly.FromDateTime(ISOWeek.ToDateTime(isoYear, isoWeek, DayOfWeek.Monday));
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct ExamWeekLoad(int IsoYear, int IsoWeek, DateOnly WeekStart, int ExamCount)
|
||||||
|
{
|
||||||
|
public DateOnly WeekEnd => WeekStart.AddDays(6);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using System.Globalization;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Tests;
|
namespace LehrerApp.Desktop.Tests;
|
||||||
@@ -75,6 +76,67 @@ public sealed class DashboardViewModelTests
|
|||||||
substitutions ?? new FakeSubstitutionEntries(), annualPlanEvents);
|
substitutions ?? new FakeSubstitutionEntries(), annualPlanEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
|
||||||
|
/// Klausurwochen-Vorausschau — unabhängig davon, welcher Wochentag "heute" gerade ist.
|
||||||
|
private static DateOnly NextIsoWeekMonday()
|
||||||
|
{
|
||||||
|
var reference = DateTime.Today.AddDays(7);
|
||||||
|
return DateOnly.FromDateTime(
|
||||||
|
ISOWeek.ToDateTime(ISOWeek.GetYear(reference), ISOWeek.GetWeekOfYear(reference), DayOfWeek.Monday));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExamWeekLoads_DreiGeplanteKlausurenInDerselbenWoche_ErzeugtEintrag()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var monday = NextIsoWeekMonday();
|
||||||
|
var exams = new FakeExams([
|
||||||
|
new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Planned },
|
||||||
|
new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Planned },
|
||||||
|
new Exam { GroupId = group.Id, Title = "c", Date = monday.AddDays(2), Status = ExamStatus.Planned },
|
||||||
|
]);
|
||||||
|
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
|
||||||
|
|
||||||
|
var entry = Assert.Single(vm.ExamWeekLoads);
|
||||||
|
Assert.Equal(3, entry.ExamCount);
|
||||||
|
Assert.True(vm.ExamLoadCard.EffectiveIsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExamWeekLoads_WenigeKlausurenProWoche_BleibtLeer()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var monday = NextIsoWeekMonday();
|
||||||
|
var exams = new FakeExams([
|
||||||
|
new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Planned },
|
||||||
|
new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Planned },
|
||||||
|
]);
|
||||||
|
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
|
||||||
|
|
||||||
|
Assert.Empty(vm.ExamWeekLoads);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExamWeekLoads_BereitsDurchgefuehrteKlausurenZaehlenNicht()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var monday = NextIsoWeekMonday();
|
||||||
|
var exams = new FakeExams([
|
||||||
|
new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Conducted },
|
||||||
|
new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Graded },
|
||||||
|
new Exam { GroupId = group.Id, Title = "c", Date = monday.AddDays(2), Status = ExamStatus.Planned },
|
||||||
|
]);
|
||||||
|
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
|
||||||
|
|
||||||
|
Assert.Empty(vm.ExamWeekLoads);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TodaysLessons_LoestRaumUeberPassendenStundenplanSlotAuf()
|
public void TodaysLessons_LoestRaumUeberPassendenStundenplanSlotAuf()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class ExamDialogViewModelTests
|
||||||
|
{
|
||||||
|
private static ExamDialogViewModel BuildViewModel(List<Exam> existingExams, string dateText,
|
||||||
|
Exam? editingExam = null)
|
||||||
|
{
|
||||||
|
var vm = new ExamDialogViewModel(new FakeExams(existingExams), new FakeCompetencyDomains(),
|
||||||
|
new FakeGradingKeyTemplates(), new GradingService(), Guid.NewGuid(), null, 9,
|
||||||
|
GradingSystem.Grades1To6, "Chemie", isDifferentiated: false, editingExam, null);
|
||||||
|
vm.DateText = dateText;
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exam MakeExam(DateOnly date) => new() { GroupId = Guid.NewGuid(), Title = "x", Date = date };
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WeekLoadHint_WenigeKlausurenInDerWoche_BleibtLeer()
|
||||||
|
{
|
||||||
|
var monday = new DateOnly(2026, 3, 16);
|
||||||
|
var existing = new List<Exam> { MakeExam(monday) };
|
||||||
|
|
||||||
|
var vm = BuildViewModel(existing, monday.AddDays(1).ToString("dd.MM.yyyy"));
|
||||||
|
|
||||||
|
Assert.Equal("", vm.WeekLoadHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WeekLoadHint_DritteKlausurInDerWoche_ZeigtHinweis()
|
||||||
|
{
|
||||||
|
var monday = new DateOnly(2026, 3, 16);
|
||||||
|
var existing = new List<Exam> { MakeExam(monday), MakeExam(monday.AddDays(1)) };
|
||||||
|
|
||||||
|
var vm = BuildViewModel(existing, monday.AddDays(2).ToString("dd.MM.yyyy"));
|
||||||
|
|
||||||
|
Assert.Contains("3 Klausuren", vm.WeekLoadHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WeekLoadHint_BearbeiteteKlausurZaehltNichtDoppelt()
|
||||||
|
{
|
||||||
|
var monday = new DateOnly(2026, 3, 16);
|
||||||
|
var editing = MakeExam(monday);
|
||||||
|
var existing = new List<Exam> { editing, MakeExam(monday.AddDays(1)) };
|
||||||
|
|
||||||
|
// Nur die eigene Klausur (editing) + eine weitere -> 2 insgesamt, kein Hinweis.
|
||||||
|
var vm = BuildViewModel(existing, monday.ToString("dd.MM.yyyy"), editing);
|
||||||
|
|
||||||
|
Assert.Equal("", vm.WeekLoadHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WeekLoadHint_UngueltigesDatum_BleibtLeer()
|
||||||
|
{
|
||||||
|
var vm = BuildViewModel([], "keindatum");
|
||||||
|
|
||||||
|
Assert.Equal("", vm.WeekLoadHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,26 @@ public sealed class QuickInputViewModelTests
|
|||||||
Assert.Equal(0.4, vm.CurrentStudentContentOpacity);
|
Assert.Equal(0.4, vm.CurrentStudentContentOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearRating_SetztVersehentlichGesetzteBewertungAufNichtBewertetZurueck()
|
||||||
|
{
|
||||||
|
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.SetRatingByNumber(4); // aus Versehen eine Note für Qualität vergeben
|
||||||
|
|
||||||
|
vm.ClearRating();
|
||||||
|
|
||||||
|
Assert.Null(vm.AspectRows[0].Value);
|
||||||
|
Assert.Null(rows[0].GetRating("quality"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -483,6 +483,35 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
Assert.True(entry.HomeworkMissing);
|
Assert.True(entry.HomeworkMissing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SitzplatzBewertung_SchlaegtQuantitaetAusStrichlisteVor()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var session = new ParticipationSession { GroupId = groupId, Date = today };
|
||||||
|
var sessions = new FakeSessions([session]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = studentId, RaisedHandCount = 3 });
|
||||||
|
|
||||||
|
var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
|
||||||
|
groupId, studentId, "Beispiel, Anna", canEdit: true);
|
||||||
|
|
||||||
|
var quantityRow = vm.AspectRows.Single(r => r.Key == "quantity");
|
||||||
|
Assert.True(quantityRow.HasSuggestion);
|
||||||
|
Assert.Equal(0, quantityRow.SuggestedValue); // 3 gemeldet -> "∼" laut ParticipationCountSuggestion
|
||||||
|
Assert.Contains("3× gemeldet", quantityRow.SuggestionLabel);
|
||||||
|
|
||||||
|
var qualityRow = vm.AspectRows.Single(r => r.Key == "quality");
|
||||||
|
Assert.False(qualityRow.HasSuggestion);
|
||||||
|
|
||||||
|
quantityRow.ApplySuggestionCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(0, quantityRow.Value);
|
||||||
|
var updatedEntry = entries.GetBySessionAndStudent(session.Id, studentId)!;
|
||||||
|
Assert.Equal(0, updatedEntry.Ratings.Single(r => r.Key == "quantity").Value);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SitzplatzBewertung_ArchiviertOhneSitzung_LegtKeineNeueSitzungAn()
|
public void SitzplatzBewertung_ArchiviertOhneSitzung_LegtKeineNeueSitzungAn()
|
||||||
{
|
{
|
||||||
@@ -517,6 +546,60 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
Assert.Null(vm.SelectedSession);
|
Assert.Null(vm.SelectedSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TallyRaisedHand_ErhoehtNurGemeldetZaehler()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "A" };
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
|
||||||
|
Assignments = [new SeatAssignment { StudentId = student.Id }],
|
||||||
|
};
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
|
||||||
|
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
|
||||||
|
sessions, entries, new FakeAspects());
|
||||||
|
vm.Initialize(groupId, isReadOnly: false);
|
||||||
|
var seat = vm.Seats.Single();
|
||||||
|
|
||||||
|
seat.TallyRaisedHandCommand.Execute(null);
|
||||||
|
seat.TallyRaisedHandCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(2, seat.RaisedHandCount);
|
||||||
|
Assert.Equal(0, seat.CalledOnCount);
|
||||||
|
var session = Assert.Single(sessions.GetByGroup(groupId));
|
||||||
|
var entry = entries.GetBySessionAndStudent(session.Id, student.Id)!;
|
||||||
|
Assert.Equal(2, entry.RaisedHandCount);
|
||||||
|
Assert.Equal(0, entry.CalledOnCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TallyCalledOn_ErhoehtBeideZaehler()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "A" };
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
|
||||||
|
Assignments = [new SeatAssignment { StudentId = student.Id }],
|
||||||
|
};
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
|
||||||
|
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
|
||||||
|
sessions, entries, new FakeAspects());
|
||||||
|
vm.Initialize(groupId, isReadOnly: false);
|
||||||
|
var seat = vm.Seats.Single();
|
||||||
|
|
||||||
|
seat.TallyRaisedHandCommand.Execute(null); // erste Meldung, nicht drangekommen
|
||||||
|
seat.TallyCalledOnCommand.Execute(null); // zweite Meldung, drangekommen
|
||||||
|
|
||||||
|
Assert.Equal(2, seat.RaisedHandCount);
|
||||||
|
Assert.Equal(1, seat.CalledOnCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AssessStudent_LegtErstBeiTatsaechlicherBewertungEineSitzungAn()
|
public async Task AssessStudent_LegtErstBeiTatsaechlicherBewertungEineSitzungAn()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
/// hat rein rechnerisch schon 100 %, das ist noch kein auffälliges Muster, nur eine zu kleine
|
/// hat rein rechnerisch schon 100 %, das ist noch kein auffälliges Muster, nur eine zu kleine
|
||||||
/// Stichprobe. Dieselbe Konstante wie GroupOverviewViewModel.AttendanceMinSampleSize.
|
/// Stichprobe. Dieselbe Konstante wie GroupOverviewViewModel.AttendanceMinSampleSize.
|
||||||
private const int AttendanceMinSampleSize = 8;
|
private const int AttendanceMinSampleSize = 8;
|
||||||
|
/// Vorausschau für die Klausurwochen-Karte (Nutzer-Feedback): weit genug, um eine sich
|
||||||
|
/// anbahnende Häufung noch rechtzeitig vor dem Anlegen weiterer Klausuren zu zeigen, aber
|
||||||
|
/// keine Vorschau auf das ganze Schuljahr.
|
||||||
|
private const int ExamLoadLookaheadDays = 60;
|
||||||
|
|
||||||
[ObservableProperty] private string _greeting = "";
|
[ObservableProperty] private string _greeting = "";
|
||||||
[ObservableProperty] private string _currentDate = "";
|
[ObservableProperty] private string _currentDate = "";
|
||||||
@@ -72,6 +76,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
|
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
|
||||||
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
||||||
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
|
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
|
||||||
|
public ObservableCollection<ExamWeekLoadItem> ExamWeekLoads { get; } = [];
|
||||||
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
|
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
|
||||||
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
|
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
|
||||||
public ObservableCollection<CorrectionProgressItem> OpenCorrections { get; } = [];
|
public ObservableCollection<CorrectionProgressItem> OpenCorrections { get; } = [];
|
||||||
@@ -106,6 +111,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public DashboardCardOption UnplannedCard => Card("unplanned");
|
public DashboardCardOption UnplannedCard => Card("unplanned");
|
||||||
public DashboardCardOption AlertsCard => Card("alerts");
|
public DashboardCardOption AlertsCard => Card("alerts");
|
||||||
public DashboardCardOption AttendanceCard => Card("attendance");
|
public DashboardCardOption AttendanceCard => Card("attendance");
|
||||||
|
public DashboardCardOption ExamLoadCard => Card("examload");
|
||||||
public DashboardCardOption SupportCard => Card("support");
|
public DashboardCardOption SupportCard => Card("support");
|
||||||
public DashboardCardOption GroupsCard => Card("groups");
|
public DashboardCardOption GroupsCard => Card("groups");
|
||||||
public int TodayLessonCount => TodaysLessons.Count;
|
public int TodayLessonCount => TodaysLessons.Count;
|
||||||
@@ -209,6 +215,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
LoadCalendar();
|
LoadCalendar();
|
||||||
LoadOpenExcuses(groups.Values.ToList(), today);
|
LoadOpenExcuses(groups.Values.ToList(), today);
|
||||||
LoadAttendanceWarnings(today);
|
LoadAttendanceWarnings(today);
|
||||||
|
LoadExamWeekLoads(groups, today);
|
||||||
LoadSupportPlanReviews(today);
|
LoadSupportPlanReviews(today);
|
||||||
LoadUpcomingDates(groups, today);
|
LoadUpcomingDates(groups, today);
|
||||||
LoadOpenCorrections(groups, today);
|
LoadOpenCorrections(groups, today);
|
||||||
@@ -228,6 +235,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
UnplannedCard.IsEmpty = UnplannedLessons.Count == 0;
|
UnplannedCard.IsEmpty = UnplannedLessons.Count == 0;
|
||||||
AlertsCard.IsEmpty = Alerts.Count == 0;
|
AlertsCard.IsEmpty = Alerts.Count == 0;
|
||||||
AttendanceCard.IsEmpty = AttendanceWarnings.Count == 0;
|
AttendanceCard.IsEmpty = AttendanceWarnings.Count == 0;
|
||||||
|
ExamLoadCard.IsEmpty = ExamWeekLoads.Count == 0;
|
||||||
SupportCard.IsEmpty = SupportPlanReviews.Count == 0;
|
SupportCard.IsEmpty = SupportPlanReviews.Count == 0;
|
||||||
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
|
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
|
||||||
|
|
||||||
@@ -322,6 +330,27 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
AttendanceWarnings.Add(item);
|
AttendanceWarnings.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Klausurwochen (Nutzer-Feedback) ───────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Persönliche Klausurlast über alle Kurse hinweg — anders als die klassenbezogene
|
||||||
|
// Kollisionsprüfung, die bereits der schulische Klausurplaner übernimmt (siehe
|
||||||
|
// ExamWeekLoadService). Nur geplante, noch bevorstehende Klausuren zählen: die
|
||||||
|
// Vorausschau soll helfen, eine sich anbahnende Woche zu erkennen, bevor man eine weitere
|
||||||
|
// Klausur genau dort einträgt — bereits durchgeführte/korrigierte Klausuren werden schon
|
||||||
|
// von der Karte "Offene Korrekturen" abgedeckt.
|
||||||
|
|
||||||
|
private void LoadExamWeekLoads(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||||
|
{
|
||||||
|
ExamWeekLoads.Clear();
|
||||||
|
var lastDay = today.AddDays(ExamLoadLookaheadDays);
|
||||||
|
var upcoming = groups.Keys
|
||||||
|
.SelectMany(gid => _exams.GetByGroup(gid))
|
||||||
|
.Where(e => e.Status == ExamStatus.Planned && e.Date >= today && e.Date <= lastDay);
|
||||||
|
|
||||||
|
foreach (var week in ExamWeekLoadService.FindOverloadedWeeks(upcoming))
|
||||||
|
ExamWeekLoads.Add(new ExamWeekLoadItem(week.IsoWeek, week.WeekStart, week.WeekEnd, week.ExamCount));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
|
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
|
||||||
|
|
||||||
private void LoadSupportPlanReviews(DateOnly today)
|
private void LoadSupportPlanReviews(DateOnly today)
|
||||||
@@ -547,7 +576,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
|
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
|
||||||
"corrections" => "Offene Korrekturen", "unplanned" => "Ungeplante Stunden", "alerts" => "Auffälligkeiten",
|
"corrections" => "Offene Korrekturen", "unplanned" => "Ungeplante Stunden", "alerts" => "Auffälligkeiten",
|
||||||
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
|
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
|
||||||
"groups" => "Meine Lerngruppen", _ => key,
|
"groups" => "Meine Lerngruppen", "examload" => "Klausurwochen", _ => key,
|
||||||
};
|
};
|
||||||
|
|
||||||
private void ApplyCardLayout()
|
private void ApplyCardLayout()
|
||||||
@@ -966,6 +995,13 @@ public class AttendanceWarningItem(Guid studentId, string studentName, double ab
|
|||||||
public double AbsenceRatePercent { get; } = absenceRatePercent;
|
public double AbsenceRatePercent { get; } = absenceRatePercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ExamWeekLoadItem(int isoWeek, DateOnly weekStart, DateOnly weekEnd, int examCount)
|
||||||
|
{
|
||||||
|
public int ExamCount { get; } = examCount;
|
||||||
|
public string Label => $"KW {isoWeek} ({weekStart:dd.MM.}–{weekEnd:dd.MM.})";
|
||||||
|
public string CountDisplay => ExamCount == 1 ? "1 Klausur" : $"{ExamCount} Klausuren";
|
||||||
|
}
|
||||||
|
|
||||||
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────────
|
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────────
|
||||||
|
|
||||||
public class SupportPlanDueItem
|
public class SupportPlanDueItem
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public partial class ExamDialogViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _titleError = "";
|
[ObservableProperty] private string _titleError = "";
|
||||||
[ObservableProperty] private string _dateTextError = "";
|
[ObservableProperty] private string _dateTextError = "";
|
||||||
[ObservableProperty] private string _returnedAtTextError = "";
|
[ObservableProperty] private string _returnedAtTextError = "";
|
||||||
|
[ObservableProperty] private string _weekLoadHint = "";
|
||||||
[ObservableProperty] private double _totalPoints;
|
[ObservableProperty] private double _totalPoints;
|
||||||
[ObservableProperty] private bool _hasCompetencyCatalog;
|
[ObservableProperty] private bool _hasCompetencyCatalog;
|
||||||
[ObservableProperty] private bool _useWeighting;
|
[ObservableProperty] private bool _useWeighting;
|
||||||
@@ -108,6 +109,7 @@ public partial class ExamDialogViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
|
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
|
||||||
RecomputeTotals();
|
RecomputeTotals();
|
||||||
|
UpdateWeekLoadHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnUseWeightingChanged(bool value)
|
partial void OnUseWeightingChanged(bool value)
|
||||||
@@ -115,6 +117,26 @@ public partial class ExamDialogViewModel : ObservableObject
|
|||||||
foreach (var t in Tasks) t.ShowWeight = value;
|
foreach (var t in Tasks) t.ShowWeight = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnDateTextChanged(string value) => UpdateWeekLoadHint();
|
||||||
|
|
||||||
|
/// Nutzer-Feedback: persönliche Klausurlast pro Woche — die klassenbezogene
|
||||||
|
/// Kollisionsprüfung übernimmt bereits der schulische Klausurplaner (siehe
|
||||||
|
/// ExamWeekLoadService), hier geht es um die eigene Häufung über alle Kurse hinweg.
|
||||||
|
private void UpdateWeekLoadHint()
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
||||||
|
{
|
||||||
|
WeekLoadHint = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = ExamWeekLoadService.CountInSameWeek(_exams.GetAll(), date, _editingExam?.Id);
|
||||||
|
var totalIfSaved = existing + 1;
|
||||||
|
WeekLoadHint = totalIfSaved >= ExamWeekLoadService.WarningThreshold
|
||||||
|
? $"Damit hättest du {totalIfSaved} Klausuren in dieser Woche."
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void AddTask()
|
private void AddTask()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ 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 · Esc schließen";
|
"Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Entf nicht bewertet · Esc schließen";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,6 +902,13 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
ApplyRating(steps[Math.Clamp(idx + 1, 0, steps.Count - 1)]);
|
ApplyRating(steps[Math.Clamp(idx + 1, 0, steps.Count - 1)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nutzer-Feedback: eine aus Versehen gesetzte Bewertung (z.B. Qualität, obwohl der Schüler
|
||||||
|
/// sich nie gemeldet hat) muss sich wieder auf "nicht bewertet" zurücksetzen lassen, ohne
|
||||||
|
/// einen inhaltlich falschen Wert stehen lassen zu müssen. Bewusst eine eigene Aktion statt
|
||||||
|
/// über 0/1 zu laufen — 0 ("--") ist bei Quantität ein echter, beobachteter Wert ("hat sich
|
||||||
|
/// nicht gemeldet"), kein Platzhalter für "nicht ermittelt".
|
||||||
|
public void ClearRating() => ApplyRating(null);
|
||||||
|
|
||||||
public void DecrementRating()
|
public void DecrementRating()
|
||||||
{
|
{
|
||||||
var cell = GetCurrentCell();
|
var cell = GetCurrentCell();
|
||||||
@@ -918,7 +925,7 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
ApplyRating(steps[Math.Clamp(idx - 1, 0, steps.Count - 1)]);
|
ApplyRating(steps[Math.Clamp(idx - 1, 0, steps.Count - 1)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyRating(int val)
|
private void ApplyRating(int? val)
|
||||||
{
|
{
|
||||||
if (_rows.Count == 0 || !_aspects.Any()) return;
|
if (_rows.Count == 0 || !_aspects.Any()) return;
|
||||||
var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key;
|
var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key;
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
?? StudentSeatOption.Empty;
|
?? StudentSeatOption.Empty;
|
||||||
var isHidden = hiddenSeats.Any(h => h.Row == row && h.Column == column);
|
var isHidden = hiddenSeats.Any(h => h.Row == row && h.Column == column);
|
||||||
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
|
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
|
||||||
CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden));
|
CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden, TallyParticipation));
|
||||||
}
|
}
|
||||||
UpdateAssignmentSummary();
|
UpdateAssignmentSummary();
|
||||||
RefreshSeatLessonData();
|
RefreshSeatLessonData();
|
||||||
@@ -266,6 +266,22 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strichliste im Sitzplan (Nutzer-Feedback): "drangekommen" ist immer auch eine Meldung und
|
||||||
|
/// erhöht daher beide Zähler — es gibt kein "drangekommen, ohne sich gemeldet zu haben".
|
||||||
|
private void TallyParticipation(SeatCellViewModel seat, bool calledOn)
|
||||||
|
{
|
||||||
|
if (!IsEditable || seat.SelectedOption.StudentId is not Guid studentId) return;
|
||||||
|
var session = EnsureTodaySession();
|
||||||
|
if (session is null) return;
|
||||||
|
|
||||||
|
var entry = _participation.GetBySessionAndStudent(session.Id, studentId)
|
||||||
|
?? new ParticipationEntry { SessionId = session.Id, StudentId = studentId };
|
||||||
|
entry.RaisedHandCount++;
|
||||||
|
if (calledOn) entry.CalledOnCount++;
|
||||||
|
_participation.Save(entry);
|
||||||
|
RefreshSeatLessonData();
|
||||||
|
}
|
||||||
|
|
||||||
private void ToggleSituationTag(SeatCellViewModel seat, string tag)
|
private void ToggleSituationTag(SeatCellViewModel seat, string tag)
|
||||||
{
|
{
|
||||||
if (!IsEditable || _documentation is null ||
|
if (!IsEditable || _documentation is null ||
|
||||||
@@ -531,6 +547,8 @@ 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 int _raisedHandCount;
|
||||||
|
[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;
|
||||||
@@ -555,10 +573,13 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
/// die Abblendung für ausgeblendete Plätze hier statt per CSS-Klasse erfolgen.</summary>
|
/// die Abblendung für ausgeblendete Plätze hier statt per CSS-Klasse erfolgen.</summary>
|
||||||
public double DisplayOpacity => IsHidden ? 0.4 : LessonOpacity;
|
public double DisplayOpacity => IsHidden ? 0.4 : LessonOpacity;
|
||||||
|
|
||||||
|
private readonly Action<SeatCellViewModel, bool> _tally;
|
||||||
|
|
||||||
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
||||||
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
|
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
|
||||||
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false,
|
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false,
|
||||||
bool isHidden = false, Action<SeatCellViewModel>? toggleHidden = null)
|
bool isHidden = false, Action<SeatCellViewModel>? toggleHidden = null,
|
||||||
|
Action<SeatCellViewModel, bool>? tally = null)
|
||||||
{
|
{
|
||||||
Row = row;
|
Row = row;
|
||||||
Column = column;
|
Column = column;
|
||||||
@@ -570,10 +591,18 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
_canRecordLesson = canRecordLesson;
|
_canRecordLesson = canRecordLesson;
|
||||||
_isHidden = isHidden;
|
_isHidden = isHidden;
|
||||||
_toggleHidden = toggleHidden ?? (_ => { });
|
_toggleHidden = toggleHidden ?? (_ => { });
|
||||||
|
_tally = tally ?? ((_, _) => { });
|
||||||
foreach (var tag in SituationTagChoice.DefaultTags)
|
foreach (var tag in SituationTagChoice.DefaultTags)
|
||||||
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
|
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strichliste im Sitzplan (Nutzer-Feedback): schnelles Mitzählen während des Unterrichts,
|
||||||
|
/// ohne dafür den vollen Sitzplatz-Bewertungsdialog öffnen zu müssen — bewusst nur ein
|
||||||
|
/// Hochzählen ohne Korrekturmöglichkeit hier direkt auf der Kachel; die Rohwerte bleiben über
|
||||||
|
/// den Bewertungsdialog einsehbar und fließen dort als Quantitäts-Vorschlag ein.
|
||||||
|
[RelayCommand] private void TallyRaisedHand() => _tally(this, false);
|
||||||
|
[RelayCommand] private void TallyCalledOn() => _tally(this, true);
|
||||||
|
|
||||||
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsOccupied));
|
OnPropertyChanged(nameof(IsOccupied));
|
||||||
@@ -615,6 +644,8 @@ 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));
|
||||||
|
RaisedHandCount = entry?.RaisedHandCount ?? 0;
|
||||||
|
CalledOnCount = entry?.CalledOnCount ?? 0;
|
||||||
LessonOpacity = attendance is not null and not AttendanceStatus.Present
|
LessonOpacity = attendance is not null and not AttendanceStatus.Present
|
||||||
and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate ? 0.42 : 1.0;
|
and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate ? 0.42 : 1.0;
|
||||||
var selected = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var selected = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -690,7 +721,10 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
foreach (var (aspect, index) in aspectDefinitions.Select((a, i) => (a, i)))
|
foreach (var (aspect, index) in aspectDefinitions.Select((a, i) => (a, i)))
|
||||||
{
|
{
|
||||||
var value = _entry?.Ratings.FirstOrDefault(r => r.Key == aspect.Key)?.Value;
|
var value = _entry?.Ratings.FirstOrDefault(r => r.Key == aspect.Key)?.Value;
|
||||||
AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating));
|
// Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback): nur für den
|
||||||
|
// eingebauten "quantity"-Aspekt, siehe ParticipationCountSuggestion.
|
||||||
|
var raisedHandCount = aspect.Key == "quantity" ? _entry?.RaisedHandCount : null;
|
||||||
|
AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating, raisedHandCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildAttendanceChoices();
|
BuildAttendanceChoices();
|
||||||
@@ -847,11 +881,22 @@ public partial class SeatAssessmentAspectRow : ObservableObject
|
|||||||
public string DisplayValue => ParticipationRatingScale.DisplayLabel(ValueType, Value);
|
public string DisplayValue => ParticipationRatingScale.DisplayLabel(ValueType, Value);
|
||||||
public ObservableCollection<SeatRatingChoice> Choices { get; } = [];
|
public ObservableCollection<SeatRatingChoice> Choices { get; } = [];
|
||||||
|
|
||||||
|
/// Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback), siehe
|
||||||
|
/// ParticipationCountSuggestion — null, wenn keine Strichliste zu diesem Aspekt gehört.
|
||||||
|
private readonly int? _raisedHandCount;
|
||||||
|
public int? SuggestedValue { get; }
|
||||||
|
public bool HasSuggestion => SuggestedValue.HasValue && SuggestedValue != Value;
|
||||||
|
public string SuggestionLabel =>
|
||||||
|
$"{_raisedHandCount}× gemeldet → Vorschlag: {ParticipationRatingScale.DisplayLabel(ValueType, SuggestedValue)}";
|
||||||
|
|
||||||
public SeatAssessmentAspectRow(int index, ParticipationAspect aspect, int? value,
|
public SeatAssessmentAspectRow(int index, ParticipationAspect aspect, int? value,
|
||||||
Action<string, int?> apply)
|
Action<string, int?> apply, int? raisedHandCount = null)
|
||||||
{
|
{
|
||||||
Index = index; Key = aspect.Key; Label = aspect.Label; ValueType = aspect.ValueType;
|
Index = index; Key = aspect.Key; Label = aspect.Label; ValueType = aspect.ValueType;
|
||||||
MaxPoints = aspect.MaxPoints; _value = value; _apply = apply;
|
MaxPoints = aspect.MaxPoints; _value = value; _apply = apply;
|
||||||
|
_raisedHandCount = raisedHandCount;
|
||||||
|
SuggestedValue = raisedHandCount is int rhc
|
||||||
|
? ParticipationCountSuggestion.SuggestQuantity(ValueType, rhc) : null;
|
||||||
var steps = ValueType == AspectValueType.Points
|
var steps = ValueType == AspectValueType.Points
|
||||||
? Enumerable.Range(0, Math.Min(MaxPoints, 9) + 1).Select(v => (v, v.ToString())).ToList()
|
? Enumerable.Range(0, Math.Min(MaxPoints, 9) + 1).Select(v => (v, v.ToString())).ToList()
|
||||||
: ParticipationRatingScale.Steps(ValueType).ToList();
|
: ParticipationRatingScale.Steps(ValueType).ToList();
|
||||||
@@ -864,10 +909,13 @@ public partial class SeatAssessmentAspectRow : ObservableObject
|
|||||||
{
|
{
|
||||||
Value = value;
|
Value = value;
|
||||||
OnPropertyChanged(nameof(DisplayValue));
|
OnPropertyChanged(nameof(DisplayValue));
|
||||||
|
OnPropertyChanged(nameof(HasSuggestion));
|
||||||
foreach (var choice in Choices) choice.IsSelected = choice.Value == value;
|
foreach (var choice in Choices) choice.IsSelected = choice.Value == value;
|
||||||
_apply(Key, value);
|
_apply(Key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand] private void ApplySuggestion() => ApplyValue(SuggestedValue);
|
||||||
|
|
||||||
public void Adjust(int delta)
|
public void Adjust(int delta)
|
||||||
{
|
{
|
||||||
if (ValueType == AspectValueType.Points)
|
if (ValueType == AspectValueType.Points)
|
||||||
|
|||||||
@@ -216,7 +216,7 @@
|
|||||||
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
|
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
|
||||||
IsVisible="{Binding CalendarCard.EffectiveIsVisible}" Margin="0,0,8,8"
|
IsVisible="{Binding CalendarCard.EffectiveIsVisible}" Margin="0,0,8,8"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16" HorizontalAlignment="Left" MaxWidth="320">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<StackPanel.Styles>
|
<StackPanel.Styles>
|
||||||
<!-- Basiswerte als Style (nicht lokal), damit die spezifischeren Selektoren
|
<!-- Basiswerte als Style (nicht lokal), damit die spezifischeren Selektoren
|
||||||
@@ -279,6 +279,7 @@
|
|||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:CalendarDayCell">
|
<DataTemplate x:DataType="vm:CalendarDayCell">
|
||||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="1"
|
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).SelectCalendarDayCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).SelectCalendarDayCommand}"
|
||||||
CommandParameter="{Binding}" ToolTip.Tip="{Binding Tooltip}">
|
CommandParameter="{Binding}" ToolTip.Tip="{Binding Tooltip}">
|
||||||
<Border Classes="daycell" Classes.ownclass="{Binding IsOwnClassDay}"
|
<Border Classes="daycell" Classes.ownclass="{Binding IsOwnClassDay}"
|
||||||
@@ -419,6 +420,30 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Klausurwochen (Nutzer-Feedback): eigene Klausurlast über alle Kurse hinweg -->
|
||||||
|
<Border Grid.Column="{Binding ExamLoadCard.Column}" Grid.Row="{Binding ExamLoadCard.Row}"
|
||||||
|
IsVisible="{Binding ExamLoadCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||||
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="8" Padding="16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="KLAUSURWOCHEN" FontSize="11" FontWeight="Bold"
|
||||||
|
Opacity="0.5" Margin="0,0,0,10"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding ExamWeekLoads}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="vm:ExamWeekLoadItem">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding Label}" FontSize="13"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding CountDisplay}" Foreground="#F59E0B"
|
||||||
|
FontSize="12" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine Woche mit auffälliger Klausurhäufung." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !ExamWeekLoads.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
||||||
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
|
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
|
||||||
IsVisible="{Binding SupportCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
IsVisible="{Binding SupportCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||||
|
|||||||
@@ -36,6 +36,8 @@
|
|||||||
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
|
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||||
<TextBlock Text="{Binding DateTextError}" Foreground="Red" FontSize="11"
|
<TextBlock Text="{Binding DateTextError}" Foreground="Red" FontSize="11"
|
||||||
IsVisible="{Binding DateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding DateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<TextBlock Text="{Binding WeekLoadHint}" Foreground="#F59E0B" FontSize="11"
|
||||||
|
IsVisible="{Binding WeekLoadHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="2" Spacing="4">
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
<TextBlock Text="Klausurnummer" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Klausurnummer" FontSize="12" Opacity="0.7"/>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public partial class ParticipationQuickInputDialog : Window
|
|||||||
|
|
||||||
case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break;
|
case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break;
|
||||||
case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break;
|
case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break;
|
||||||
|
case Key.Delete: vm.ClearRating(); e.Handled = true; break;
|
||||||
|
|
||||||
case Key.Escape: Close(); e.Handled = true; break;
|
case Key.Escape: Close(); e.Handled = true; break;
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,10 @@
|
|||||||
<Run Text="["/><Run Text="{Binding Shortcut}"/><Run Text="] "/><Run Text="{Binding Label}"/>
|
<Run Text="["/><Run Text="{Binding Shortcut}"/><Run Text="] "/><Run Text="{Binding Label}"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock Text="{Binding DisplayValue}" FontSize="11" Opacity="0.6"/>
|
<TextBlock Text="{Binding DisplayValue}" FontSize="11" Opacity="0.6"/>
|
||||||
|
<Button Content="{Binding SuggestionLabel}" Command="{Binding ApplySuggestionCommand}"
|
||||||
|
IsVisible="{Binding HasSuggestion}" FontSize="10" Padding="6,2" Margin="0,3,0,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ToolTip.Tip="Aus der Strichliste im Sitzplan übernehmen"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
|
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
|
||||||
<ItemsControl ItemsSource="{Binding Choices}">
|
<ItemsControl ItemsSource="{Binding Choices}">
|
||||||
|
|||||||
@@ -155,6 +155,21 @@
|
|||||||
ToolTip.Tip="Hausaufgabe"/>
|
ToolTip.Tip="Hausaufgabe"/>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<!-- Strichliste Meldungen (Nutzer-Feedback): schnelles Mitzählen ohne den
|
||||||
|
vollen Bewertungsdialog zu öffnen -->
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5"
|
||||||
|
IsVisible="{Binding ShowLessonOverview}">
|
||||||
|
<Button Padding="6,2" FontSize="10"
|
||||||
|
Command="{Binding TallyRaisedHandCommand}"
|
||||||
|
ToolTip.Tip="Meldung zählen">
|
||||||
|
<TextBlock Text="{Binding RaisedHandCount, StringFormat='✋ {0}'}"/>
|
||||||
|
</Button>
|
||||||
|
<Button Padding="6,2" FontSize="10"
|
||||||
|
Command="{Binding TallyCalledOnCommand}"
|
||||||
|
ToolTip.Tip="Meldung + drangekommen zählen">
|
||||||
|
<TextBlock Text="{Binding CalledOnCount, StringFormat='✋✓ {0}'}"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
<Expander Header="+ Situation" FontSize="10"
|
<Expander Header="+ Situation" FontSize="10"
|
||||||
IsVisible="{Binding CanRecordLesson}">
|
IsVisible="{Binding CanRecordLesson}">
|
||||||
<ItemsControl ItemsSource="{Binding SituationTags}" Margin="0,4,0,0">
|
<ItemsControl ItemsSource="{Binding SituationTags}" Margin="0,4,0,0">
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Tests;
|
||||||
|
|
||||||
|
public class ExamWeekLoadServiceTests
|
||||||
|
{
|
||||||
|
private static Exam MakeExam(DateOnly date, ExamStatus status = ExamStatus.Planned) => new()
|
||||||
|
{
|
||||||
|
GroupId = Guid.NewGuid(), Title = "Klausur", Status = status, Date = date,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CountInSameWeek_ZaehltNurKlausurenInDerselbenIsoWoche()
|
||||||
|
{
|
||||||
|
// KW 12/2026: Montag 16.03. bis Sonntag 22.03.
|
||||||
|
var monday = new DateOnly(2026, 3, 16);
|
||||||
|
var exams = new List<Exam>
|
||||||
|
{
|
||||||
|
MakeExam(monday),
|
||||||
|
MakeExam(monday.AddDays(4)), // Freitag derselben Woche
|
||||||
|
MakeExam(monday.AddDays(-1)), // Sonntag der Vorwoche
|
||||||
|
MakeExam(monday.AddDays(7)), // Montag der Folgewoche
|
||||||
|
};
|
||||||
|
|
||||||
|
var count = ExamWeekLoadService.CountInSameWeek(exams, monday.AddDays(2));
|
||||||
|
|
||||||
|
Assert.Equal(2, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CountInSameWeek_SchliesstAngegebeneKlausurAus()
|
||||||
|
{
|
||||||
|
var monday = new DateOnly(2026, 3, 16);
|
||||||
|
var editing = MakeExam(monday);
|
||||||
|
var exams = new List<Exam> { editing, MakeExam(monday.AddDays(1)) };
|
||||||
|
|
||||||
|
var count = ExamWeekLoadService.CountInSameWeek(exams, monday, editing.Id);
|
||||||
|
|
||||||
|
Assert.Equal(1, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindOverloadedWeeks_MeldetNurWochenAbDemSchwellenwert()
|
||||||
|
{
|
||||||
|
var monday = new DateOnly(2026, 3, 16);
|
||||||
|
var exams = new List<Exam>
|
||||||
|
{
|
||||||
|
MakeExam(monday), MakeExam(monday.AddDays(1)), // KW 12: 2 -> unauffällig
|
||||||
|
MakeExam(monday.AddDays(7)), MakeExam(monday.AddDays(8)), MakeExam(monday.AddDays(9)), // KW 13: 3 -> Meldung
|
||||||
|
};
|
||||||
|
|
||||||
|
var overloaded = ExamWeekLoadService.FindOverloadedWeeks(exams);
|
||||||
|
|
||||||
|
var week = Assert.Single(overloaded);
|
||||||
|
Assert.Equal(3, week.ExamCount);
|
||||||
|
Assert.Equal(monday.AddDays(7), week.WeekStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindOverloadedWeeks_OhneHaeufung_GibtLeereListeZurueck()
|
||||||
|
{
|
||||||
|
var monday = new DateOnly(2026, 3, 16);
|
||||||
|
var exams = new List<Exam> { MakeExam(monday), MakeExam(monday.AddDays(7)) };
|
||||||
|
|
||||||
|
Assert.Empty(ExamWeekLoadService.FindOverloadedWeeks(exams));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindOverloadedWeeks_SortiertChronologisch()
|
||||||
|
{
|
||||||
|
var laterWeek = new DateOnly(2026, 4, 20);
|
||||||
|
var earlierWeek = new DateOnly(2026, 3, 16);
|
||||||
|
var exams = new List<Exam>
|
||||||
|
{
|
||||||
|
MakeExam(laterWeek), MakeExam(laterWeek.AddDays(1)), MakeExam(laterWeek.AddDays(2)),
|
||||||
|
MakeExam(earlierWeek), MakeExam(earlierWeek.AddDays(1)), MakeExam(earlierWeek.AddDays(2)),
|
||||||
|
};
|
||||||
|
|
||||||
|
var overloaded = ExamWeekLoadService.FindOverloadedWeeks(exams);
|
||||||
|
|
||||||
|
Assert.Equal(2, overloaded.Count);
|
||||||
|
Assert.True(overloaded[0].WeekStart < overloaded[1].WeekStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Tests;
|
||||||
|
|
||||||
|
/// Tests für den Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback):
|
||||||
|
/// wie oft sich jemand meldet, schlägt direkt eine Quantitätsbewertung vor.
|
||||||
|
public sealed class ParticipationCountSuggestionTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, -2)]
|
||||||
|
[InlineData(1, -1)]
|
||||||
|
[InlineData(2, 0)]
|
||||||
|
[InlineData(3, 0)]
|
||||||
|
[InlineData(4, 1)]
|
||||||
|
[InlineData(5, 1)]
|
||||||
|
[InlineData(6, 2)]
|
||||||
|
[InlineData(20, 2)]
|
||||||
|
public void SuggestQuantity_Scale5_BildetMeldungszahlAufStufeAb(int raisedHandCount, int expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, ParticipationCountSuggestion.SuggestQuantity(AspectValueType.Scale5, raisedHandCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(AspectValueType.Scale3)]
|
||||||
|
[InlineData(AspectValueType.Binary)]
|
||||||
|
[InlineData(AspectValueType.Points)]
|
||||||
|
public void SuggestQuantity_AndereTypen_GibtKeinenVorschlag(AspectValueType type)
|
||||||
|
{
|
||||||
|
Assert.Null(ParticipationCountSuggestion.SuggestQuantity(type, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -144,6 +144,22 @@ unterrichten oft mehrere Kurse gleichzeitig und brauchen einen gruppenübergreif
|
|||||||
`GradingService`/`GradeBarItem`) — kein Kompetenz-Breakdown in dieser Iteration. Button
|
`GradingService`/`GradeBarItem`) — kein Kompetenz-Breakdown in dieser Iteration. Button
|
||||||
"Zum Kurs" springt in den Klausuren-Tab der jeweiligen Gruppe (dort wie gehabt Zugriff auf
|
"Zum Kurs" springt in den Klausuren-Tab der jeweiligen Gruppe (dort wie gehabt Zugriff auf
|
||||||
Kompetenzen-Tab, 8.2/8.3).
|
Kompetenzen-Tab, 8.2/8.3).
|
||||||
|
- [x] **1.6.7** Persönliche Klausurlast pro Woche (Nutzer-Feedback). Bewusst **keine**
|
||||||
|
klassenbezogene Kollisionsprüfung (mehrere Klausuren einer einzelnen Klasse in einer
|
||||||
|
Woche) — das übernimmt bereits der schulische Klausurplaner (externes Klassenbuch) mit
|
||||||
|
eigenem Alarm. Was dort fehlt: die eigene Belastung über alle Kurse hinweg, unabhängig von
|
||||||
|
der Klasse — mehr Klausuren in derselben Woche bedeuten unabhängig davon mehr
|
||||||
|
Erstellungsaufwand vorher und mehr Korrekturaufwand danach. Neuer
|
||||||
|
`ExamWeekLoadService` (`LehrerApp.Core/Services/ExamWeekLoadService.cs`) zählt eigene
|
||||||
|
Klausuren je ISO-Kalenderwoche; da eine LiteDB-Datei genau einem Nutzer gehört, sind "alle
|
||||||
|
Klausuren in der Datenbank" bereits gleichbedeutend mit "meine Klausuren". Ab drei
|
||||||
|
Klausuren in derselben Woche gilt sie als spürbar belastet (zwei aus zwei unterrichteten
|
||||||
|
Fächern sind normal, eine dritte die eigentliche Häufung). Zwei Einstiege: (a) im
|
||||||
|
`ExamDialog` direkt beim Ändern des Datums ein Hinweis, wenn die Klausur die Woche über
|
||||||
|
den Schwellenwert heben würde — greift am Entscheidungspunkt, bevor überhaupt gespeichert
|
||||||
|
wird; (b) neue Dashboard-Karte "Klausurwochen" (9), die bevorstehende Wochen (60 Tage
|
||||||
|
Vorausschau) mit auffälliger Häufung unter den noch **geplanten** Klausuren auflistet —
|
||||||
|
bereits durchgeführte/korrigierte Klausuren deckt schon die Karte "Offene Korrekturen" ab.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -281,6 +297,16 @@ Siehe [ParticipationViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/Participa
|
|||||||
Anwesenheitsstatus ("⚠ Abwesend — Krank, unentschuldigt" o.ä.) in Orange. Bewusst kein
|
Anwesenheitsstatus ("⚠ Abwesend — Krank, unentschuldigt" o.ä.) in Orange. Bewusst kein
|
||||||
Blockieren der Eingabe (manche Bewertungssysteme erwarten trotzdem einen expliziten
|
Blockieren der Eingabe (manche Bewertungssysteme erwarten trotzdem einen expliziten
|
||||||
Eintrag) — nur ein visueller Hinweis.
|
Eintrag) — nur ein visueller Hinweis.
|
||||||
|
- [x] **3.1.6** Bewertung wieder auf "nicht bewertet" zurücksetzen können (Nutzer-Feedback: eine
|
||||||
|
aus Versehen gesetzte Bewertung — z.B. Qualität, obwohl sich der Schüler nie gemeldet hat —
|
||||||
|
muss sich zurücknehmen lassen, ohne einen inhaltlich falschen Wert stehen lassen zu müssen).
|
||||||
|
Im Sitzplatz-Dialog (`SeatAssessmentDialog`) gab es das bereits über "Löschen"/Backspace je
|
||||||
|
Aspekt; in der session-weiten Schnelleingabe (`ParticipationQuickInputDialog`, Q/W/E/R/T +
|
||||||
|
Zifferntasten) fehlte ein entsprechender Weg — Backspace ist dort bereits für "vorheriger
|
||||||
|
Schüler" belegt. Neue Taste **Entf** ruft `QuickInputViewModel.ClearRating()` auf
|
||||||
|
(`ApplyRating` nimmt jetzt `int?` statt `int` an); bewusst eine eigene Aktion statt über 0
|
||||||
|
zu laufen, da 0 ("--") bei Quantität ein echter, beobachteter Wert ist, kein Platzhalter für
|
||||||
|
"nicht ermittelt".
|
||||||
|
|
||||||
### 3.2 Aggregation zur Mitarbeitsnote
|
### 3.2 Aggregation zur Mitarbeitsnote
|
||||||
- [x] **3.2.1** Gewichtung je Aspekt konfigurierbar (z.B. Qualität 50 %, Quantität 30 %, Experiment 20 %).
|
- [x] **3.2.1** Gewichtung je Aspekt konfigurierbar (z.B. Qualität 50 %, Quantität 30 %, Experiment 20 %).
|
||||||
@@ -330,6 +356,20 @@ Mitarbeit-Note für denselben Zeitraum statt sie zu duplizieren (erkannt über d
|
|||||||
und die Kalender-Aggregation (`DayAgg`/`CalendarDayCell`/`CalendarEventItem`) bereits, sodass
|
und die Kalender-Aggregation (`DayAgg`/`CalendarDayCell`/`CalendarEventItem`) bereits, sodass
|
||||||
sich der dritte Termintyp am Exam-Vorbild (In-Memory-Filterung nach `GetByGroup`, keine neue
|
sich der dritte Termintyp am Exam-Vorbild (In-Memory-Filterung nach `GetByGroup`, keine neue
|
||||||
Repository-Methode) ergänzen ließ.
|
Repository-Methode) ergänzen ließ.
|
||||||
|
- [x] **3.3.5** Strichliste "gemeldet"/"gemeldet und drangekommen" direkt im Sitzplan
|
||||||
|
(Nutzer-Feedback, als Ergänzung zur "ohne Mitarbeitsbewertung"-Zählung aus dem
|
||||||
|
Aufrufgerechtigkeits-Gedanken): zwei neue Felder `ParticipationEntry.RaisedHandCount`/
|
||||||
|
`.CalledOnCount` — "drangekommen" ist immer auch eine Meldung und erhöht daher immer
|
||||||
|
beide Zähler zugleich, nie `CalledOnCount` allein. Zwei kompakte Zähl-Buttons auf jeder
|
||||||
|
belegten Sitzplatz-Kachel im Anzeigemodus (`ShowLessonOverview`, gleiche Sichtbarkeit wie
|
||||||
|
die Anwesenheits-/Hausaufgaben-Badges) erlauben das Mitzählen während der Stunde, ohne den
|
||||||
|
vollen Sitzplatz-Dialog öffnen zu müssen — bewusst nur Hochzählen, keine Korrektur direkt
|
||||||
|
auf der Kachel (die Rohwerte bleiben über den Sitzplatz-Dialog einsehbar). Tüpfelchen auf
|
||||||
|
dem i: die Strichliste schlägt im Sitzplatz-Dialog direkt eine Quantitätsbewertung vor
|
||||||
|
(neuer `ParticipationCountSuggestion.SuggestQuantity`, nur für den eingebauten
|
||||||
|
"quantity"-Aspekt mit Skalentyp `Scale5`) — als sichtbarer Vorschlag mit einem Klick zum
|
||||||
|
Übernehmen, nie automatisch gesetzt, damit eine bereits vorhandene bewusste Bewertung nie
|
||||||
|
stillschweigend überschrieben wird.
|
||||||
|
|
||||||
### 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