Kurs-Übersicht gefüllt: nächste Termine, Mitarbeitshinweis, offene Kontrollen/Dokumentationen, auffällige Fehlzeiten
Der bislang leere "Übersicht"-Tab je Kurs zeigt jetzt einen kompakten "Was steht an"-Überblick aus bereits vorhandenen Daten mit Klick-Durchsprung in den jeweiligen Tab: nächste Stunde/Klausur, Hinweis auf die letzte Mitarbeitssitzung, offene Hausaufgaben-Kontrolle, offene Entschuldigungen, offene Dokumentations-Entwürfe und auffällige Fehlzeiten. Die Fehlzeiten-Karte ist bewusst kein 1:1-Abbild der Dashboard-Warnung: erst ab einer Mindeststichprobe von 8 erfassten Terminen im Schuljahr gemeldet, damit ein einzelner Fehltag zu Schuljahresbeginn nicht sofort als auffällig gilt. Das Hauptdashboard hat dasselbe Problem noch, wird dort separat nachgezogen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ public sealed class GroupDetailViewModelTests
|
|||||||
var grades = new FakeGrades();
|
var grades = new FakeGrades();
|
||||||
|
|
||||||
var vm = new GroupDetailViewModel(groups, students, memberships, subjects, exams, grades, tasks,
|
var vm = new GroupDetailViewModel(groups, students, memberships, subjects, exams, grades, tasks,
|
||||||
|
new GroupOverviewViewModel(new FakeLessons(), exams, new FakeSessions([]), new FakeEntries(), students,
|
||||||
|
new FakeDocumentation(), new AttendanceBalanceService(), new SchoolYearService()),
|
||||||
new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(),
|
new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(),
|
||||||
students, memberships, groups, new FakeCompetencyDomains()),
|
students, memberships, groups, new FakeCompetencyDomains()),
|
||||||
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
/// Tests für die Kurs-Übersicht: sie liest bewusst keine eigenen Daten, sondern zieht nur
|
||||||
|
/// zusammen, was Planung/Klausuren/Mitarbeit/Dokumentation ohnehin schon verwalten.
|
||||||
|
public sealed class GroupOverviewViewModelTests
|
||||||
|
{
|
||||||
|
private static GroupOverviewViewModel NewVm(FakeLessons? lessons = null, FakeExams? exams = null,
|
||||||
|
FakeSessions? sessions = null, FakeEntries? entries = null, FakeStudents? students = null,
|
||||||
|
FakeDocumentation? documentation = null) =>
|
||||||
|
new(lessons ?? new FakeLessons(), exams ?? new FakeExams([]), sessions ?? new FakeSessions([]),
|
||||||
|
entries ?? new FakeEntries(), students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
||||||
|
new AttendanceBalanceService(), new SchoolYearService());
|
||||||
|
|
||||||
|
private static (GroupOverviewViewModel Vm, FakeLessons Lessons, FakeExams Exams,
|
||||||
|
FakeSessions Sessions, FakeEntries Entries, FakeStudents Students, Guid GroupId) BuildScenario(
|
||||||
|
List<Lesson>? lessons = null, List<Exam>? exams = null)
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var lessonsRepo = new FakeLessons();
|
||||||
|
foreach (var l in lessons ?? []) lessonsRepo.Add(l);
|
||||||
|
var examsRepo = new FakeExams(exams ?? []);
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
var students = new FakeStudents([]);
|
||||||
|
|
||||||
|
var vm = NewVm(lessonsRepo, examsRepo, sessions, entries, students);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
return (vm, lessonsRepo, examsRepo, sessions, entries, students, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NaechsteStunde_FindetDieNaechsteGeplanteStundeInnerhalbDesZeitfensters()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(10), Topic = "Später", Status = LessonStatus.Planned });
|
||||||
|
lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(3), Topic = "Brechung", Status = LessonStatus.Planned });
|
||||||
|
lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(1), Topic = "Schon vorbei", Status = LessonStatus.Conducted });
|
||||||
|
|
||||||
|
var vm = NewVm(lessons: lessons);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.True(vm.HasNextLesson);
|
||||||
|
Assert.Contains("Brechung", vm.NextLessonLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NaechsteStunde_OhneGeplanteStundeIstNichtVorhanden()
|
||||||
|
{
|
||||||
|
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||||
|
Assert.False(vm.HasNextLesson);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NaechsteKlausur_FindetFruehesteKuenftigeKlausur()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var exams = new FakeExams([
|
||||||
|
new Exam { GroupId = groupId, Date = today.AddDays(20), Title = "Später" },
|
||||||
|
new Exam { GroupId = groupId, Date = today.AddDays(5), Title = "Klassenarbeit 2" },
|
||||||
|
new Exam { GroupId = groupId, Date = today.AddDays(-3), Title = "Vergangen" },
|
||||||
|
]);
|
||||||
|
var vm = NewVm(exams: exams);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.True(vm.HasNextExam);
|
||||||
|
Assert.Contains("Klassenarbeit 2", vm.NextExamLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Mitarbeit_OhneSitzungenZeigtHinweisOhneWarnung()
|
||||||
|
{
|
||||||
|
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||||
|
Assert.Contains("Noch keine Mitarbeitssitzung", vm.ParticipationHintLabel);
|
||||||
|
Assert.False(vm.ParticipationHintIsStale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Mitarbeit_LetzteSitzungVorMehrAls14TagenGiltAlsVeraltet()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var sessions = new FakeSessions([new ParticipationSession { GroupId = groupId, Date = today.AddDays(-20) }]);
|
||||||
|
var vm = NewVm(sessions: sessions);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.True(vm.ParticipationHintIsStale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Mitarbeit_LetzteSitzungKuerzlichGiltNichtAlsVeraltet()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var sessions = new FakeSessions([new ParticipationSession { GroupId = groupId, Date = today.AddDays(-2) }]);
|
||||||
|
var vm = NewVm(sessions: sessions);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.False(vm.ParticipationHintIsStale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OffeneHausaufgabenKontrolle_ZeigtUnkontrollierteHausaufgabeDerLetztenStunde()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson
|
||||||
|
{
|
||||||
|
GroupId = groupId, Date = today.AddDays(-2), Homework = "S. 42 Nr. 3",
|
||||||
|
HomeworkChecked = false, HomeworkCheckDismissed = false,
|
||||||
|
});
|
||||||
|
var vm = NewVm(lessons: lessons);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.True(vm.HasOpenHomeworkCheck);
|
||||||
|
Assert.Contains("S. 42 Nr. 3", vm.OpenHomeworkCheckLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OffeneHausaufgabenKontrolle_VerschwindetSobaldKontrolliert()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson
|
||||||
|
{
|
||||||
|
GroupId = groupId, Date = today.AddDays(-2), Homework = "S. 42 Nr. 3",
|
||||||
|
HomeworkChecked = true,
|
||||||
|
});
|
||||||
|
var vm = NewVm(lessons: lessons);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.False(vm.HasOpenHomeworkCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OffeneEntschuldigungen_ListetOffenePendenteEntschuldigungenUndErlaubtAufloesen()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var session = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-2) };
|
||||||
|
var sessions = new FakeSessions([session]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
var entry = new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = AttendanceStatus.ExcusePending };
|
||||||
|
entries.Add(entry);
|
||||||
|
var students = new FakeStudents([student]);
|
||||||
|
|
||||||
|
var vm = NewVm(sessions: sessions, entries: entries, students: students);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
var item = Assert.Single(vm.OpenExcuses);
|
||||||
|
Assert.Equal(student.FullName, item.StudentName);
|
||||||
|
|
||||||
|
item.MarkExcusedCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Empty(vm.OpenExcuses);
|
||||||
|
Assert.Equal(AttendanceStatus.Excused, entries.GetBySessionAndStudent(session.Id, student.Id)!.Attendance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OffeneDokumentationen_ZaehltNurEntwuerfeDieserGruppeOderOhneGruppe()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var otherGroupId = Guid.NewGuid();
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var students = new FakeStudents([student]);
|
||||||
|
var documentation = new FakeDocumentation();
|
||||||
|
documentation.Add(new Documentation { StudentId = student.Id, GroupId = groupId, IsDraft = true, Title = "Beobachtung" });
|
||||||
|
documentation.Add(new Documentation { StudentId = student.Id, GroupId = null, IsDraft = true, Title = "Ohne Gruppe" });
|
||||||
|
documentation.Add(new Documentation { StudentId = student.Id, GroupId = otherGroupId, IsDraft = true, Title = "Anderer Kurs" });
|
||||||
|
documentation.Add(new Documentation { StudentId = student.Id, GroupId = groupId, IsDraft = false, Title = "Fertig" });
|
||||||
|
|
||||||
|
var vm = NewVm(students: students, documentation: documentation);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.Equal(2, vm.DraftDocumentationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuffaelligeFehlzeiten_WirdErstAbMindeststichprobeGemeldet()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var students = new FakeStudents([student]);
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
|
||||||
|
// Nur 3 erfasste Termine, davon 3 unentschuldigt (100 %) — trotz hoher Quote noch keine
|
||||||
|
// Meldung, weil die Stichprobe zu klein ist (Schuljahresbeginn-Problem).
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var session = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-i - 1) };
|
||||||
|
sessions.Save(session);
|
||||||
|
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = AttendanceStatus.Unexcused });
|
||||||
|
}
|
||||||
|
|
||||||
|
var vm = NewVm(sessions: sessions, entries: entries, students: students);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.False(vm.HasAttendanceNotices);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuffaelligeFehlzeiten_MeldetHoheQuoteAbMindeststichprobe()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var students = new FakeStudents([student]);
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
|
||||||
|
// 10 erfasste Termine, 3 davon unentschuldigt (30 % > 20 % Schwelle) — jetzt genug
|
||||||
|
// Stichprobe für eine Meldung.
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var session = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-i - 1) };
|
||||||
|
sessions.Save(session);
|
||||||
|
var status = i < 3 ? AttendanceStatus.Unexcused : AttendanceStatus.Present;
|
||||||
|
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = status });
|
||||||
|
}
|
||||||
|
|
||||||
|
var vm = NewVm(sessions: sessions, entries: entries, students: students);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
var item = Assert.Single(vm.AttendanceNotices);
|
||||||
|
Assert.Equal(student.FullName, item.StudentName);
|
||||||
|
Assert.Equal(30.0, item.AbsenceRatePercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigationCommands_RufenDelegatMitPassendemTabIndexAuf()
|
||||||
|
{
|
||||||
|
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||||
|
var indices = new List<int>();
|
||||||
|
vm.OnNavigateToTab = idx => indices.Add(idx);
|
||||||
|
|
||||||
|
vm.NavigateToPlanningCommand.Execute(null);
|
||||||
|
vm.NavigateToExamsCommand.Execute(null);
|
||||||
|
vm.NavigateToParticipationCommand.Execute(null);
|
||||||
|
vm.NavigateToDocumentationCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal([6, 4, 3, 8], indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -249,6 +249,7 @@ public static class AppBootstrapper
|
|||||||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||||||
services.AddTransient<GroupDetailViewModel>();
|
services.AddTransient<GroupDetailViewModel>();
|
||||||
services.AddTransient<StudentDetailViewModel>();
|
services.AddTransient<StudentDetailViewModel>();
|
||||||
|
services.AddTransient<GroupOverviewViewModel>();
|
||||||
services.AddTransient<ParticipationTabViewModel>();
|
services.AddTransient<ParticipationTabViewModel>();
|
||||||
services.AddTransient<GradeOverviewTabViewModel>();
|
services.AddTransient<GradeOverviewTabViewModel>();
|
||||||
services.AddTransient<PlanningTabViewModel>();
|
services.AddTransient<PlanningTabViewModel>();
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
// ── Tab: Übersicht — kompakte "Was steht an"-Zusammenfassung eines Kurses ────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zieht keine eigenen neuen Daten — jede Karte liest nur, was die anderen Tabs (Planung,
|
||||||
|
/// Klausuren, Mitarbeit) ohnehin schon verwalten, und verweist per Klick dorthin. Bewusst kein
|
||||||
|
/// Ersatz für die Detail-Tabs, sondern ein schneller Überblick "was ist als nächstes dran /
|
||||||
|
/// was wurde vergessen".
|
||||||
|
/// </summary>
|
||||||
|
public partial class GroupOverviewViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private const int OpenExcuseMaxAgeDays = 21;
|
||||||
|
private const int HomeworkCheckLookbackDays = 120;
|
||||||
|
private const int NextLessonLookaheadDays = 90;
|
||||||
|
|
||||||
|
/// Unterhalb dieser Anzahl erfasster Anwesenheits-Einträge im laufenden Schuljahr wird eine
|
||||||
|
/// hohe Fehlquote bewusst NICHT gemeldet — wer zu Schuljahresbeginn zweimal fehlt, hat rein
|
||||||
|
/// rechnerisch schon 100 %, das ist noch kein auffälliges Muster, nur eine zu kleine
|
||||||
|
/// Stichprobe. Dasselbe Problem hat aktuell noch die Fehlzeiten-Warnung im Hauptdashboard
|
||||||
|
/// (AttendanceBalanceService/AttendanceWarningItem) — bewusst unangetastet, wird dort separat
|
||||||
|
/// nachgezogen.
|
||||||
|
private const int AttendanceMinSampleSize = 8;
|
||||||
|
|
||||||
|
private readonly ILessonRepository _lessons;
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly IParticipationSessionRepository _sessions;
|
||||||
|
private readonly IParticipationRepository _entries;
|
||||||
|
private readonly IStudentRepository _students;
|
||||||
|
private readonly IDocumentationRepository _documentation;
|
||||||
|
private readonly AttendanceBalanceService _attendanceBalance;
|
||||||
|
private readonly SchoolYearService _schoolYear;
|
||||||
|
|
||||||
|
private Guid _groupId;
|
||||||
|
private string _groupName = "";
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _hasNextLesson;
|
||||||
|
[ObservableProperty] private string _nextLessonLabel = "";
|
||||||
|
[ObservableProperty] private bool _hasNextExam;
|
||||||
|
[ObservableProperty] private string _nextExamLabel = "";
|
||||||
|
[ObservableProperty] private string _participationHintLabel = "";
|
||||||
|
[ObservableProperty] private bool _participationHintIsStale;
|
||||||
|
[ObservableProperty] private bool _hasOpenHomeworkCheck;
|
||||||
|
[ObservableProperty] private string _openHomeworkCheckLabel = "";
|
||||||
|
[ObservableProperty] private int _draftDocumentationCount;
|
||||||
|
|
||||||
|
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
||||||
|
public bool HasOpenExcuses => OpenExcuses.Count > 0;
|
||||||
|
|
||||||
|
/// Absichtlich zurückhaltender formuliert/gestylt als das Hauptdashboard (kein "Warnfall") —
|
||||||
|
/// siehe <see cref="AttendanceMinSampleSize"/>.
|
||||||
|
public ObservableCollection<AttendanceWarningItem> AttendanceNotices { get; } = [];
|
||||||
|
public bool HasAttendanceNotices => AttendanceNotices.Count > 0;
|
||||||
|
|
||||||
|
/// Ein Delegate statt einem pro Karte, mit dem Ziel-Tab-Index von GroupDetailView.axaml als
|
||||||
|
/// Parameter (0 Übersicht, 1 Schüler, 2 Sitzpläne, 3 Mitarbeit, 4 Klausuren, 5 Noten,
|
||||||
|
/// 6 Planung, 7 Kompetenzen, 8 Dokumentation).
|
||||||
|
public Action<int>? OnNavigateToTab { get; set; }
|
||||||
|
|
||||||
|
[RelayCommand] private void NavigateToPlanning() => OnNavigateToTab?.Invoke(6);
|
||||||
|
[RelayCommand] private void NavigateToExams() => OnNavigateToTab?.Invoke(4);
|
||||||
|
[RelayCommand] private void NavigateToParticipation() => OnNavigateToTab?.Invoke(3);
|
||||||
|
[RelayCommand] private void NavigateToDocumentation() => OnNavigateToTab?.Invoke(8);
|
||||||
|
|
||||||
|
public GroupOverviewViewModel(ILessonRepository lessons, IExamRepository exams,
|
||||||
|
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
||||||
|
IStudentRepository students, IDocumentationRepository documentation,
|
||||||
|
AttendanceBalanceService attendanceBalance, SchoolYearService schoolYear)
|
||||||
|
{
|
||||||
|
_lessons = lessons; _exams = exams; _sessions = sessions;
|
||||||
|
_entries = entries; _students = students; _documentation = documentation;
|
||||||
|
_attendanceBalance = attendanceBalance; _schoolYear = schoolYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(Guid groupId, string groupName)
|
||||||
|
{
|
||||||
|
_groupId = groupId;
|
||||||
|
_groupName = groupName;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
LoadNextLesson(today);
|
||||||
|
LoadNextExam(today);
|
||||||
|
LoadParticipationHint(today);
|
||||||
|
LoadOpenHomeworkCheck(today);
|
||||||
|
LoadOpenExcuses(today);
|
||||||
|
LoadDraftDocumentationCount();
|
||||||
|
LoadAttendanceNotices(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadNextLesson(DateOnly today)
|
||||||
|
{
|
||||||
|
var next = _lessons.GetByGroupAndRange(_groupId, today, today.AddDays(NextLessonLookaheadDays))
|
||||||
|
.Where(l => l.Status == LessonStatus.Planned)
|
||||||
|
.OrderBy(l => l.Date).ThenBy(l => l.LessonNumber ?? 0)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
HasNextLesson = next is not null;
|
||||||
|
NextLessonLabel = next is null ? ""
|
||||||
|
: string.IsNullOrWhiteSpace(next.Topic)
|
||||||
|
? next.Date.ToString("dd.MM.yyyy")
|
||||||
|
: $"{next.Date:dd.MM.yyyy} — {next.Topic}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadNextExam(DateOnly today)
|
||||||
|
{
|
||||||
|
var next = _exams.GetByGroup(_groupId).Where(e => e.Date >= today).MinBy(e => e.Date);
|
||||||
|
HasNextExam = next is not null;
|
||||||
|
NextExamLabel = next is null ? "" : $"{next.Date:dd.MM.yyyy} — {next.Title}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erinnert nicht an eine Note, sondern schlicht daran, überhaupt wieder eine Sitzung
|
||||||
|
/// anzulegen — genau das vergisst man in Kursen, die man seltener unterrichtet, zuerst.
|
||||||
|
private void LoadParticipationHint(DateOnly today)
|
||||||
|
{
|
||||||
|
var last = _sessions.GetByGroup(_groupId).OrderByDescending(s => s.Date).FirstOrDefault();
|
||||||
|
if (last is null)
|
||||||
|
{
|
||||||
|
ParticipationHintLabel = "Noch keine Mitarbeitssitzung angelegt.";
|
||||||
|
ParticipationHintIsStale = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var daysAgo = today.DayNumber - last.Date.DayNumber;
|
||||||
|
ParticipationHintLabel = daysAgo <= 0
|
||||||
|
? "Letzte Sitzung: heute."
|
||||||
|
: $"Letzte Sitzung: {last.Date:dd.MM.yyyy} (vor {daysAgo} Tag(en)).";
|
||||||
|
ParticipationHintIsStale = daysAgo > 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dieselbe Erkennung wie das Stundenplan-Badge "Hausaufgabe kontrollieren"
|
||||||
|
/// (TimetableViewModel.HasUnhandledHomework) — bewusst nur die unmittelbar letzte Lesson,
|
||||||
|
/// nicht die gesamte Historie.
|
||||||
|
private void LoadOpenHomeworkCheck(DateOnly today)
|
||||||
|
{
|
||||||
|
var previous = _lessons.GetByGroupAndRange(_groupId, today.AddDays(-HomeworkCheckLookbackDays), today.AddDays(-1))
|
||||||
|
.OrderByDescending(l => l.Date).ThenByDescending(l => l.LessonNumber ?? 0)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
var open = previous is not null && !string.IsNullOrWhiteSpace(previous.Homework)
|
||||||
|
&& !previous.HomeworkChecked && !previous.HomeworkCheckDismissed;
|
||||||
|
|
||||||
|
HasOpenHomeworkCheck = open;
|
||||||
|
OpenHomeworkCheckLabel = open
|
||||||
|
? $"Aus der Stunde vom {previous!.Date:dd.MM.yyyy}: {previous.Homework}"
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadOpenExcuses(DateOnly today)
|
||||||
|
{
|
||||||
|
OpenExcuses.Clear();
|
||||||
|
var cutoff = today.AddDays(-OpenExcuseMaxAgeDays);
|
||||||
|
|
||||||
|
var items = new List<OpenExcuseItem>();
|
||||||
|
foreach (var session in _sessions.GetByGroup(_groupId).Where(s => s.Date >= cutoff && s.Date <= today))
|
||||||
|
{
|
||||||
|
foreach (var entry in _entries.GetBySession(session.Id)
|
||||||
|
.Where(e => e.Attendance == AttendanceStatus.ExcusePending))
|
||||||
|
{
|
||||||
|
var student = _students.GetById(entry.StudentId);
|
||||||
|
if (student is null) continue;
|
||||||
|
var item = new OpenExcuseItem(session.Id, entry.StudentId, student.FullName, _groupName, session.Date);
|
||||||
|
item.OnResolve = ResolveExcuse;
|
||||||
|
items.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var item in items.OrderBy(i => i.Date))
|
||||||
|
OpenExcuses.Add(item);
|
||||||
|
OnPropertyChanged(nameof(HasOpenExcuses));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolveExcuse(OpenExcuseItem item, AttendanceStatus status)
|
||||||
|
{
|
||||||
|
var entry = _entries.GetBySessionAndStudent(item.SessionId, item.StudentId);
|
||||||
|
if (entry is null) return;
|
||||||
|
entry.Attendance = status;
|
||||||
|
_entries.Save(entry);
|
||||||
|
OpenExcuses.Remove(item);
|
||||||
|
OnPropertyChanged(nameof(HasOpenExcuses));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entwürfe (5.1.1: schnelle Notiz beim Sitzplan, noch nicht zu einem echten Dokumentations-
|
||||||
|
/// eintrag ausformuliert) dieser Gruppe — dieselbe Zählung wie das Badge in
|
||||||
|
/// GroupDocumentationTabViewModel.Load, hier nur ohne die vollständige Entwurfsliste.
|
||||||
|
private void LoadDraftDocumentationCount()
|
||||||
|
{
|
||||||
|
DraftDocumentationCount = _students.GetByGroup(_groupId)
|
||||||
|
.SelectMany(s => _documentation.GetByStudent(s.Id))
|
||||||
|
.Count(d => d.IsDraft && (d.GroupId is null || d.GroupId == _groupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bewusst kein 1:1-Abbild der Dashboard-Fehlzeiten-Warnung: dieselbe Quote
|
||||||
|
/// (AttendanceBalanceService.WarningThresholdPercent), aber erst ab
|
||||||
|
/// <see cref="AttendanceMinSampleSize"/> erfassten Terminen, damit ein Fehltag in der ersten
|
||||||
|
/// Schulwoche nicht sofort als auffällig gilt.
|
||||||
|
private void LoadAttendanceNotices(DateOnly today)
|
||||||
|
{
|
||||||
|
AttendanceNotices.Clear();
|
||||||
|
var schoolYear = _schoolYear.CurrentSchoolYear(today);
|
||||||
|
var from = _schoolYear.SchoolYearStart(schoolYear);
|
||||||
|
var to = _schoolYear.SchoolYearEnd(schoolYear);
|
||||||
|
|
||||||
|
var entriesByStudent = new Dictionary<Guid, List<(DateOnly Date, AttendanceStatus? Status)>>();
|
||||||
|
foreach (var session in _sessions.GetByGroup(_groupId).Where(s => s.Date >= from && s.Date <= to))
|
||||||
|
{
|
||||||
|
foreach (var entry in _entries.GetBySession(session.Id))
|
||||||
|
{
|
||||||
|
if (!entriesByStudent.TryGetValue(entry.StudentId, out var list))
|
||||||
|
entriesByStudent[entry.StudentId] = list = [];
|
||||||
|
list.Add((session.Date, entry.Attendance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<AttendanceWarningItem>();
|
||||||
|
foreach (var student in _students.GetByGroup(_groupId))
|
||||||
|
{
|
||||||
|
if (!entriesByStudent.TryGetValue(student.Id, out var entries) || entries.Count < AttendanceMinSampleSize)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var balance = _attendanceBalance.Calculate(entries, from, to);
|
||||||
|
if (balance.ExceedsThreshold)
|
||||||
|
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
|
||||||
|
}
|
||||||
|
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
|
||||||
|
AttendanceNotices.Add(item);
|
||||||
|
OnPropertyChanged(nameof(HasAttendanceNotices));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -208,12 +208,14 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
|
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
|
||||||
partial void OnActiveTabIndexChanged(int value)
|
partial void OnActiveTabIndexChanged(int value)
|
||||||
{
|
{
|
||||||
|
if (value == 0) OverviewTab.Refresh();
|
||||||
if (value == 7) CompetencyOverviewTab.Refresh();
|
if (value == 7) CompetencyOverviewTab.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||||
|
|
||||||
|
public GroupOverviewViewModel OverviewTab { get; }
|
||||||
public ParticipationTabViewModel ParticipationTab { get; }
|
public ParticipationTabViewModel ParticipationTab { get; }
|
||||||
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
||||||
public PlanningTabViewModel PlanningTab { get; }
|
public PlanningTabViewModel PlanningTab { get; }
|
||||||
@@ -233,12 +235,14 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||||
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||||
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
||||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
GroupOverviewViewModel overviewTab, ParticipationTabViewModel participationTab,
|
||||||
|
GradeOverviewTabViewModel gradeOverviewTab,
|
||||||
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
|
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
|
||||||
SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab)
|
SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab)
|
||||||
{
|
{
|
||||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||||
_exams = exams; _grades = grades; _tasks = tasks;
|
_exams = exams; _grades = grades; _tasks = tasks;
|
||||||
|
OverviewTab = overviewTab;
|
||||||
ParticipationTab = participationTab;
|
ParticipationTab = participationTab;
|
||||||
GradeOverviewTab = gradeOverviewTab;
|
GradeOverviewTab = gradeOverviewTab;
|
||||||
PlanningTab = planningTab;
|
PlanningTab = planningTab;
|
||||||
@@ -251,6 +255,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
ParticipationTab.RefreshCurrentGrid();
|
ParticipationTab.RefreshCurrentGrid();
|
||||||
};
|
};
|
||||||
SeatingPlanTab.OnDocumentationChanged = GroupDocumentationTab.Refresh;
|
SeatingPlanTab.OnDocumentationChanged = GroupDocumentationTab.Refresh;
|
||||||
|
OverviewTab.OnNavigateToTab = idx => ActiveTabIndex = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadGroup(Guid id)
|
public void LoadGroup(Guid id)
|
||||||
@@ -266,6 +271,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}";
|
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}";
|
||||||
LoadStudents();
|
LoadStudents();
|
||||||
ReloadExams();
|
ReloadExams();
|
||||||
|
OverviewTab.Initialize(Group.Id, GroupTitle);
|
||||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear, IsReadOnly);
|
ParticipationTab.Initialize(Group.Id, Group.SchoolYear, IsReadOnly);
|
||||||
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
|
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
|
||||||
PlanningTab.Initialize(Group.Id, IsReadOnly);
|
PlanningTab.Initialize(Group.Id, IsReadOnly);
|
||||||
|
|||||||
@@ -53,13 +53,7 @@
|
|||||||
|
|
||||||
<!-- Tab: Übersicht -->
|
<!-- Tab: Übersicht -->
|
||||||
<ContentPage Header="Übersicht">
|
<ContentPage Header="Übersicht">
|
||||||
<ScrollViewer Padding="20">
|
<views:GroupOverviewTabView DataContext="{Binding OverviewTab}"/>
|
||||||
<StackPanel Spacing="12">
|
|
||||||
<TextBlock Text="{Binding GroupSubtitle}" Opacity="0.7" FontSize="14"/>
|
|
||||||
<TextBlock Text="Hier erscheint später eine Zusammenfassung der Lerngruppe."
|
|
||||||
Opacity="0.4"/>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Schüler -->
|
<!-- Tab: Schüler -->
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
xmlns:vmRoot="clr-namespace:LehrerApp.Desktop.ViewModels"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.GroupOverviewTabView"
|
||||||
|
x:DataType="vm:GroupOverviewViewModel">
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Border.card">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltHighBrush}"/>
|
||||||
|
<Setter Property="CornerRadius" Value="8"/>
|
||||||
|
<Setter Property="Padding" Value="16"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||||
|
<Setter Property="MinWidth" Value="260"/>
|
||||||
|
<Setter Property="MaxWidth" Value="340"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.cardTitle">
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="Opacity" Value="0.5"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,10"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.cardBody.stale">
|
||||||
|
<Setter Property="Foreground" Value="#D97706"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.cardLink">
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Margin" Value="0,10,0,0"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<ScrollViewer Padding="20">
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<WrapPanel>
|
||||||
|
|
||||||
|
<!-- Nächste Stunde -->
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="NÄCHSTE STUNDE" Classes="cardTitle"/>
|
||||||
|
<TextBlock Text="{Binding NextLessonLabel}" TextWrapping="Wrap" Classes="cardBody"
|
||||||
|
IsVisible="{Binding HasNextLesson}"/>
|
||||||
|
<TextBlock Text="Keine geplante Stunde in Sicht." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !HasNextLesson}"/>
|
||||||
|
<Button Content="Zur Planung ›" Classes="cardLink" Command="{Binding NavigateToPlanningCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Nächste Klausur -->
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="NÄCHSTE KLAUSUR" Classes="cardTitle"/>
|
||||||
|
<TextBlock Text="{Binding NextExamLabel}" TextWrapping="Wrap" Classes="cardBody"
|
||||||
|
IsVisible="{Binding HasNextExam}"/>
|
||||||
|
<TextBlock Text="Keine Klausur geplant." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !HasNextExam}"/>
|
||||||
|
<Button Content="Zu den Klausuren ›" Classes="cardLink" Command="{Binding NavigateToExamsCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Mitarbeit -->
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="MITARBEIT" Classes="cardTitle"/>
|
||||||
|
<TextBlock Text="{Binding ParticipationHintLabel}" TextWrapping="Wrap"
|
||||||
|
Classes="cardBody" Classes.stale="{Binding ParticipationHintIsStale}"/>
|
||||||
|
<Button Content="Zur Mitarbeit ›" Classes="cardLink" Command="{Binding NavigateToParticipationCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Offene Hausaufgaben-Kontrolle -->
|
||||||
|
<Border Classes="card" IsVisible="{Binding HasOpenHomeworkCheck}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="OFFENE HAUSAUFGABEN-KONTROLLE" Classes="cardTitle"/>
|
||||||
|
<TextBlock Text="{Binding OpenHomeworkCheckLabel}" TextWrapping="Wrap" Classes="cardBody stale"/>
|
||||||
|
<Button Content="Zur Planung ›" Classes="cardLink" Command="{Binding NavigateToPlanningCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Offene Dokumentationen -->
|
||||||
|
<Border Classes="card" IsVisible="{Binding !!DraftDocumentationCount}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="OFFENE DOKUMENTATIONEN" Classes="cardTitle"/>
|
||||||
|
<TextBlock TextWrapping="Wrap" Classes="cardBody">
|
||||||
|
<Run Text="{Binding DraftDocumentationCount}"/>
|
||||||
|
<Run Text=" Entwurf/Entwürfe noch nicht ausformuliert."/>
|
||||||
|
</TextBlock>
|
||||||
|
<Button Content="Zur Dokumentation ›" Classes="cardLink" Command="{Binding NavigateToDocumentationCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
|
<WrapPanel>
|
||||||
|
<!-- Offene Entschuldigungen -->
|
||||||
|
<Border Classes="card" MinWidth="340" MaxWidth="420"
|
||||||
|
IsVisible="{Binding HasOpenExcuses}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="OFFENE ENTSCHULDIGUNGEN" Classes="cardTitle"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding OpenExcuses}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vmRoot:OpenExcuseItem">
|
||||||
|
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,4">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock Text="{Binding DateDisplay}" FontSize="11" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="Entschuldigt" FontSize="11" Padding="7,3"
|
||||||
|
Command="{Binding MarkExcusedCommand}" Margin="0,0,4,0"/>
|
||||||
|
<Button Grid.Column="2" Content="Unentschuldigt" FontSize="11" Padding="7,3"
|
||||||
|
Command="{Binding MarkUnexcusedCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Auffällige Fehlzeiten: bewusst zurückhaltender formuliert/gestylt als die
|
||||||
|
Fehlzeiten-Warnung im Hauptdashboard, siehe GroupOverviewViewModel.AttendanceMinSampleSize. -->
|
||||||
|
<Border Classes="card" MinWidth="340" MaxWidth="420"
|
||||||
|
IsVisible="{Binding HasAttendanceNotices}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="AUFFÄLLIGE FEHLZEITEN" Classes="cardTitle"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding AttendanceNotices}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vmRoot:AttendanceWarningItem">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding StudentName}" FontSize="13"
|
||||||
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="1" FontSize="12" Foreground="#D97706" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Run Text="{Binding AbsenceRatePercent, StringFormat={}{0:0.#}}"/>
|
||||||
|
<Run Text=" %"/>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class GroupOverviewTabView : UserControl
|
||||||
|
{
|
||||||
|
public GroupOverviewTabView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -2188,6 +2188,25 @@ von 4.2.2) der Tab "Kürzel-Katalog" für die Von/Nach-Kürzel des Stundenverlau
|
|||||||
Emoji-Darstellung einführen.
|
Emoji-Darstellung einführen.
|
||||||
- [x] **14.11** Aktiven Navigationspunkt in der Seitenleiste sichtbar hervorheben; Zustand wird
|
- [x] **14.11** Aktiven Navigationspunkt in der Seitenleiste sichtbar hervorheben; Zustand wird
|
||||||
über `MainWindowViewModel.ActiveNavItem` gesteuert.
|
über `MainWindowViewModel.ActiveNavItem` gesteuert.
|
||||||
|
- [x] **14.12** Tab "Übersicht" im Kurs (`GroupDetailView`, bislang nur Platzhaltertext) gefüllt —
|
||||||
|
neue [GroupOverviewTabView.axaml](LehrerApp.Desktop/Views/Groups/GroupOverviewTabView.axaml)
|
||||||
|
/ `GroupOverviewViewModel`. Bewusst kein Ersatz für die Detail-Tabs, sondern ein
|
||||||
|
"Was steht an"-Überblick aus bereits vorhandenen Daten mit Klick-Durchsprung in den
|
||||||
|
jeweiligen Tab: nächste geplante Stunde, nächste Klausur, Hinweis auf die letzte
|
||||||
|
Mitarbeitssitzung (warnfarben, wenn älter als 14 Tage — man vergisst Sitzungen in seltener
|
||||||
|
unterrichteten Kursen zuerst), offene Hausaufgaben-Kontrolle der letzten Stunde (dieselbe
|
||||||
|
Erkennung wie das Stundenplan-Badge, `TimetableViewModel.HasUnhandledHomework`) sowie offene
|
||||||
|
Entschuldigungen (`AttendanceStatus.ExcusePending`, direkt auflösbar) — letzteres dieselbe
|
||||||
|
Karte wie im globalen Dashboard (9), hier nur auf den einen Kurs eingeschränkt statt über
|
||||||
|
alle Gruppen aggregiert. **Nachtrag (Nutzer-Feedback):** zwei weitere Karten — offene
|
||||||
|
Dokumentationen (`Documentation.IsDraft`-Zählung, dieselbe wie das Badge in
|
||||||
|
`GroupDocumentationTabViewModel`) und auffällige Fehlzeiten. Letztere bewusst KEIN 1:1-Abbild
|
||||||
|
der Dashboard-Fehlzeiten-Warnung: dieselbe Quote (`AttendanceBalanceService.WarningThresholdPercent`,
|
||||||
|
20 %), aber erst ab `GroupOverviewViewModel.AttendanceMinSampleSize` (8) erfassten
|
||||||
|
Anwesenheits-Terminen im laufenden Schuljahr gemeldet — wer zu Schuljahresbeginn zweimal
|
||||||
|
fehlt, hat rechnerisch schon 100 % Fehlquote, das ist noch kein auffälliges Muster, nur eine
|
||||||
|
zu kleine Stichprobe. Dasselbe Problem hat die Fehlzeiten-Warnung im Hauptdashboard (5.2.3)
|
||||||
|
aktuell noch — dort bewusst unangetastet gelassen, wird separat nachgezogen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user