feat: Abwesenheits-Hinweise, Fehlquote bei Zeugnisnoten, Gruppen-Dokumentation
- Schnellbewerten-Dialog: abwesende Schüler werden gedimmt und mit ihrem Anwesenheitsstatus statt der Aspektbeschriftung angezeigt, damit keine Mitarbeitsnote für nicht anwesende Schüler vergeben wird. - Zeugnisnoten-Dialog: zeigt je Schüler die Fehlquote im gewählten Zeitraum, ab 50 % hervorgehoben (informativ, keine automatische Notenänderung). - Gruppen-Tab "Dokumentation" (bisher Platzhalter) implementiert: listet alle Dokumentationseinträge der Gruppen-Schüler, mit Schüler-Filter und optionalem "Nur dieser Unterricht"-Schalter. Einträge aus anderen Lerngruppen werden standardmäßig mitangezeigt, aber gedimmt. Der Dokumentationsdialog bekommt dafür einen optionalen Schüler-Picker. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,60 @@ public sealed class DocumentationDialogViewModelTests
|
|||||||
Assert.NotEqual("", vm.AttachmentError);
|
Assert.NotEqual("", vm.AttachmentError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OhneStudentOptions_CanPickStudentIstFalse()
|
||||||
|
{
|
||||||
|
var vm = new DocumentationDialogViewModel(Guid.NewGuid(), null, new FakeAttachmentStorage());
|
||||||
|
|
||||||
|
Assert.False(vm.CanPickStudent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MitStudentOptions_OhneAuswahl_SaveSetztFehlerUndSpeichertNicht()
|
||||||
|
{
|
||||||
|
var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
|
||||||
|
var vm = new DocumentationDialogViewModel(Guid.Empty, null, new FakeAttachmentStorage(), [anna])
|
||||||
|
{
|
||||||
|
Title = "Vorfall", TypeName = "Vorkommnis",
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Null(vm.Result);
|
||||||
|
Assert.NotEqual("", vm.StudentError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MitStudentOptions_AuswahlGetroffen_SaveUebernimmtSchuelerUndGruppe()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
|
||||||
|
var ben = new StudentOption(Guid.NewGuid(), "Ben Muster");
|
||||||
|
var vm = new DocumentationDialogViewModel(Guid.Empty, null, new FakeAttachmentStorage(),
|
||||||
|
[anna, ben], groupId)
|
||||||
|
{
|
||||||
|
Title = "Vorfall", TypeName = "Vorkommnis", SelectedStudent = ben,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.NotNull(vm.Result);
|
||||||
|
Assert.Equal(ben.Id, vm.Result!.StudentId);
|
||||||
|
Assert.Equal(groupId, vm.Result.GroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MitStudentOptions_Bearbeiten_SchuelerIstVorausgewaehlt()
|
||||||
|
{
|
||||||
|
var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
|
||||||
|
var ben = new StudentOption(Guid.NewGuid(), "Ben Muster");
|
||||||
|
var existing = new Documentation { StudentId = ben.Id, Title = "Alt" };
|
||||||
|
|
||||||
|
var vm = new DocumentationDialogViewModel(ben.Id, existing, new FakeAttachmentStorage(), [anna, ben]);
|
||||||
|
|
||||||
|
Assert.Equal(ben, vm.SelectedStudent);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DiscardUnsavedAttachments_EntferntNurNieGespeicherteAnhaenge()
|
public void DiscardUnsavedAttachments_EntferntNurNieGespeicherteAnhaenge()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ public sealed class GroupDetailViewModelTests
|
|||||||
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
||||||
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
||||||
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
||||||
new FakeSessions([]), new FakeEntries(), new FakeAspects()));
|
new FakeSessions([]), new FakeEntries(), new FakeAspects()),
|
||||||
|
new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups));
|
||||||
|
|
||||||
vm.LoadGroup(group.Id);
|
vm.LoadGroup(group.Id);
|
||||||
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
/// Tests für den bisher als Platzhalter existierenden Gruppen-Tab "Dokumentation" (Nutzer-Feedback):
|
||||||
|
/// listet Dokumentationseinträge aller Gruppen-Schüler, zeigt Einträge aus anderen Lerngruppen
|
||||||
|
/// standardmäßig mit an (optisch abgesetzt statt versteckt), mit optionalem Filter darauf.
|
||||||
|
public sealed class GroupDocumentationTabViewModelTests
|
||||||
|
{
|
||||||
|
private static GroupDocumentationTabViewModel BuildVm(
|
||||||
|
List<Student> students, List<LearningGroup> groups, FakeDocumentation? docs = null)
|
||||||
|
{
|
||||||
|
var vm = new GroupDocumentationTabViewModel(docs ?? new FakeDocumentation(),
|
||||||
|
new FakeStudents(students), new FakeGroups(groups));
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Initialize_ZeigtEintraegeAllerGruppenschuelerSortiertNachDatumAbsteigend()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var ben = new Student { FirstName = "Ben", LastName = "Muster" };
|
||||||
|
var docs = new FakeDocumentation();
|
||||||
|
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Alt", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
docs.Add(new Documentation { StudentId = ben.Id, GroupId = group.Id, Title = "Neu", Date = new DateOnly(2025, 9, 10) });
|
||||||
|
|
||||||
|
var vm = BuildVm([anna, ben], [group], docs);
|
||||||
|
vm.Initialize(group.Id);
|
||||||
|
|
||||||
|
Assert.Equal(2, vm.Entries.Count);
|
||||||
|
Assert.Equal("Neu", vm.Entries[0].Model.Title);
|
||||||
|
Assert.Equal("Alt", vm.Entries[1].Model.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Initialize_EintragAusAndererGruppe_WirdMitangezeigtAberAlsFremdMarkiert()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var otherGroup = new LearningGroup { Name = "Mathematik 9b" };
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var docs = new FakeDocumentation();
|
||||||
|
docs.Add(new Documentation { StudentId = anna.Id, GroupId = otherGroup.Id, Title = "Aus anderem Kurs", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
|
||||||
|
var vm = BuildVm([anna], [group, otherGroup], docs);
|
||||||
|
vm.Initialize(group.Id);
|
||||||
|
|
||||||
|
var entry = Assert.Single(vm.Entries);
|
||||||
|
Assert.False(entry.IsOwnGroup);
|
||||||
|
Assert.Equal("Mathematik 9b", entry.OtherGroupLabel);
|
||||||
|
Assert.True(entry.ContentOpacity < 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Initialize_EintragOhneGruppe_GiltAlsEigen()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var docs = new FakeDocumentation();
|
||||||
|
docs.Add(new Documentation { StudentId = anna.Id, GroupId = null, Title = "Allgemein", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
|
||||||
|
var vm = BuildVm([anna], [group], docs);
|
||||||
|
vm.Initialize(group.Id);
|
||||||
|
|
||||||
|
var entry = Assert.Single(vm.Entries);
|
||||||
|
Assert.True(entry.IsOwnGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnlyThisGroup_Aktiviert_BlendetEintraegeAusAnderenGruppenAus()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var otherGroup = new LearningGroup { Name = "Mathematik 9b" };
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var docs = new FakeDocumentation();
|
||||||
|
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Eigen", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
docs.Add(new Documentation { StudentId = anna.Id, GroupId = otherGroup.Id, Title = "Fremd", Date = new DateOnly(2025, 9, 2) });
|
||||||
|
|
||||||
|
var vm = BuildVm([anna], [group, otherGroup], docs);
|
||||||
|
vm.Initialize(group.Id);
|
||||||
|
vm.OnlyThisGroup = true;
|
||||||
|
|
||||||
|
var entry = Assert.Single(vm.Entries);
|
||||||
|
Assert.Equal("Eigen", entry.Model.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedStudentFilter_AufEinzelnenSchueler_FiltertListe()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var ben = new Student { FirstName = "Ben", LastName = "Muster" };
|
||||||
|
var docs = new FakeDocumentation();
|
||||||
|
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Anna-Eintrag", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
docs.Add(new Documentation { StudentId = ben.Id, GroupId = group.Id, Title = "Ben-Eintrag", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
|
||||||
|
var vm = BuildVm([anna, ben], [group], docs);
|
||||||
|
vm.Initialize(group.Id);
|
||||||
|
vm.SelectedStudentFilter = vm.StudentFilterOptions.Single(s => s.Id == anna.Id);
|
||||||
|
|
||||||
|
var entry = Assert.Single(vm.Entries);
|
||||||
|
Assert.Equal("Anna-Eintrag", entry.Model.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddDocumentation_SpeichertErgebnisUndLaedtNeu()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var docs = new FakeDocumentation();
|
||||||
|
var vm = BuildVm([anna], [group], docs);
|
||||||
|
vm.Initialize(group.Id);
|
||||||
|
|
||||||
|
vm.OnEditDocumentation = (groupId, options, editing) =>
|
||||||
|
{
|
||||||
|
Assert.Equal(group.Id, groupId);
|
||||||
|
Assert.Contains(options, o => o.Id == anna.Id);
|
||||||
|
Assert.Null(editing);
|
||||||
|
return Task.FromResult<Documentation?>(new Documentation
|
||||||
|
{ StudentId = anna.Id, GroupId = groupId, Title = "Neu", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.AddDocumentationCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
var entry = Assert.Single(vm.Entries);
|
||||||
|
Assert.Equal("Neu", entry.Model.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteDocumentation_NachBestaetigung_EntferntEintragAusListe()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||||
|
var docs = new FakeDocumentation();
|
||||||
|
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Weg", Date = new DateOnly(2025, 9, 1) });
|
||||||
|
var vm = BuildVm([anna], [group], docs);
|
||||||
|
vm.Initialize(group.Id);
|
||||||
|
vm.OnConfirmDeleteDocumentation = _ => Task.FromResult(true);
|
||||||
|
|
||||||
|
await vm.DeleteDocumentationCommand.ExecuteAsync(vm.Entries[0]);
|
||||||
|
|
||||||
|
Assert.Empty(vm.Entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,44 @@ public sealed class QuickInputViewModelTests
|
|||||||
Assert.Equal(1, vm.AspectRows[0].Value);
|
Assert.Equal(1, vm.AspectRows[0].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnwesenderSchueler_CurrentStudentIsAbsent_IstFalse()
|
||||||
|
{
|
||||||
|
var aspects = new List<AspectColumnDef>
|
||||||
|
{
|
||||||
|
new(new ParticipationAspect { Key = "a", Label = "A" }),
|
||||||
|
};
|
||||||
|
var rows = new List<ParticipationStudentRow>
|
||||||
|
{
|
||||||
|
new(Guid.NewGuid(), "Anna", new ParticipationEntry { Attendance = AttendanceStatus.Present }, aspects, []),
|
||||||
|
};
|
||||||
|
var vm = new QuickInputViewModel(rows, aspects);
|
||||||
|
|
||||||
|
Assert.False(vm.CurrentStudentIsAbsent);
|
||||||
|
Assert.Equal(1.0, vm.CurrentStudentContentOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbwesenderSchueler_CurrentStudentIsAbsent_IstTrueUndGedimmt()
|
||||||
|
{
|
||||||
|
var aspects = new List<AspectColumnDef>
|
||||||
|
{
|
||||||
|
new(new ParticipationAspect { Key = "a", Label = "A" }),
|
||||||
|
};
|
||||||
|
var rows = new List<ParticipationStudentRow>
|
||||||
|
{
|
||||||
|
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
|
||||||
|
new(Guid.NewGuid(), "Ben", new ParticipationEntry { Attendance = AttendanceStatus.Unexcused }, aspects, []),
|
||||||
|
};
|
||||||
|
var vm = new QuickInputViewModel(rows, aspects);
|
||||||
|
|
||||||
|
vm.NextStudent(); // zu Ben, unentschuldigt abwesend
|
||||||
|
|
||||||
|
Assert.True(vm.CurrentStudentIsAbsent);
|
||||||
|
Assert.Equal("Krank, unentschuldigt", vm.CurrentStudentAttendanceLabel);
|
||||||
|
Assert.Equal(0.4, vm.CurrentStudentContentOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,14 +15,15 @@ public class ReportGradeCalculationTests
|
|||||||
|
|
||||||
private static ReportGradeDialogViewModel BuildViewModel(
|
private static ReportGradeDialogViewModel BuildViewModel(
|
||||||
FakeGrades grades, FakeSchemes schemes, FakeReportGrades reportGrades,
|
FakeGrades grades, FakeSchemes schemes, FakeReportGrades reportGrades,
|
||||||
FakeExams exams, FakeResults results)
|
FakeExams exams, FakeResults results, FakeSessions? sessions = null, FakeEntries? entries = null)
|
||||||
{
|
{
|
||||||
var students = new FakeStudents([Anna, Ben]);
|
var students = new FakeStudents([Anna, Ben]);
|
||||||
var memberships = new FakeMemberships([]);
|
var memberships = new FakeMemberships([]);
|
||||||
var grading = new GradingService();
|
var grading = new GradingService();
|
||||||
|
|
||||||
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
|
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
|
||||||
schemes, reportGrades, grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
|
schemes, reportGrades, sessions ?? new FakeSessions([]), entries ?? new FakeEntries(),
|
||||||
|
new AttendanceBalanceService(), grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
|
||||||
"Testgruppe", "2025/26");
|
"Testgruppe", "2025/26");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +69,28 @@ public class ReportGradeCalculationTests
|
|||||||
Assert.Equal("–", ben.CalculatedDisplay);
|
Assert.Equal("–", ben.CalculatedDisplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbsenceRatePercent_BerechnetFehlquoteAusSitzungenDieserGruppeImZeitraum()
|
||||||
|
{
|
||||||
|
var (_, _, exams, results) = BuildExams();
|
||||||
|
var session1 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 5) };
|
||||||
|
var session2 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 12) };
|
||||||
|
var sessions = new FakeSessions([session1, session2]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
entries.Add(new ParticipationEntry { SessionId = session1.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Present });
|
||||||
|
entries.Add(new ParticipationEntry { SessionId = session2.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Unexcused });
|
||||||
|
|
||||||
|
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results,
|
||||||
|
sessions, entries);
|
||||||
|
|
||||||
|
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||||||
|
Assert.Equal(50.0, anna.AbsenceRatePercent);
|
||||||
|
Assert.True(anna.HasHighAbsenceRate);
|
||||||
|
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
|
||||||
|
Assert.Equal(0.0, ben.AbsenceRatePercent);
|
||||||
|
Assert.False(ben.HasHighAbsenceRate);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveScheme_BevorzugtGruppenspezifischesSchemaVorVoreinstellung()
|
public void ResolveScheme_BevorzugtGruppenspezifischesSchemaVorVoreinstellung()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ public static class AppBootstrapper
|
|||||||
services.AddTransient<PlanningTabViewModel>();
|
services.AddTransient<PlanningTabViewModel>();
|
||||||
services.AddTransient<CompetencyOverviewTabViewModel>();
|
services.AddTransient<CompetencyOverviewTabViewModel>();
|
||||||
services.AddTransient<SeatingPlanTabViewModel>();
|
services.AddTransient<SeatingPlanTabViewModel>();
|
||||||
|
services.AddTransient<GroupDocumentationTabViewModel>();
|
||||||
services.AddTransient<AddGroupDialogViewModel>();
|
services.AddTransient<AddGroupDialogViewModel>();
|
||||||
services.AddTransient<SettingsViewModel>();
|
services.AddTransient<SettingsViewModel>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gruppen-Tab "Dokumentation" (bisher Platzhalter): zeigt die Dokumentationseinträge aller
|
||||||
|
/// aktuellen/ehemaligen Schüler dieser Gruppe an einem Ort, statt sie einzeln im Schüler-Tab
|
||||||
|
/// aufsuchen zu müssen. Nutzer-Feedback: die Liste soll standardmäßig auch Einträge aus anderen
|
||||||
|
/// Lerngruppen desselben Schülers mit anzeigen (optisch abgesetzt statt ausgeblendet), damit
|
||||||
|
/// Muster aus anderen Fächern/Kursen nicht verborgen bleiben — ein Schalter blendet sie bei
|
||||||
|
/// Bedarf ganz aus.
|
||||||
|
/// </summary>
|
||||||
|
public partial class GroupDocumentationTabViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IDocumentationRepository _docs;
|
||||||
|
private readonly IStudentRepository _students;
|
||||||
|
private readonly IGroupRepository _groups;
|
||||||
|
|
||||||
|
private Guid _groupId;
|
||||||
|
private List<StudentOption> _groupStudents = [];
|
||||||
|
|
||||||
|
public static readonly StudentOption AllStudentsOption = new(Guid.Empty, "Alle Schüler");
|
||||||
|
|
||||||
|
public ObservableCollection<DocumentationItem> Entries { get; } = [];
|
||||||
|
public ObservableCollection<StudentOption> StudentFilterOptions { get; } = [AllStudentsOption];
|
||||||
|
|
||||||
|
[ObservableProperty] private StudentOption _selectedStudentFilter = AllStudentsOption;
|
||||||
|
[ObservableProperty] private bool _onlyThisGroup;
|
||||||
|
|
||||||
|
public Func<Guid, List<StudentOption>, Documentation?, Task<Documentation?>>? OnEditDocumentation { get; set; }
|
||||||
|
public Func<DocumentationItem, Task<bool>>? OnConfirmDeleteDocumentation { get; set; }
|
||||||
|
public Func<Documentation, string, Task<Documentation?>>? OnConductParentCall { get; set; }
|
||||||
|
|
||||||
|
public GroupDocumentationTabViewModel(IDocumentationRepository docs, IStudentRepository students,
|
||||||
|
IGroupRepository groups)
|
||||||
|
{
|
||||||
|
_docs = docs; _students = students; _groups = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedStudentFilterChanged(StudentOption value) => Load();
|
||||||
|
partial void OnOnlyThisGroupChanged(bool value) => Load();
|
||||||
|
|
||||||
|
public void Initialize(Guid groupId)
|
||||||
|
{
|
||||||
|
_groupId = groupId;
|
||||||
|
_groupStudents = _students.GetByGroup(groupId)
|
||||||
|
.Select(s => new StudentOption(s.Id, s.FullName)).ToList();
|
||||||
|
|
||||||
|
StudentFilterOptions.Clear();
|
||||||
|
StudentFilterOptions.Add(AllStudentsOption);
|
||||||
|
foreach (var s in _groupStudents) StudentFilterOptions.Add(s);
|
||||||
|
SelectedStudentFilter = AllStudentsOption;
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Load()
|
||||||
|
{
|
||||||
|
Entries.Clear();
|
||||||
|
if (_groupStudents.Count == 0) return;
|
||||||
|
|
||||||
|
var studentNameById = _groupStudents.ToDictionary(s => s.Id, s => s.Name);
|
||||||
|
var groupNameCache = new Dictionary<Guid, string>();
|
||||||
|
string GroupLabel(Guid id)
|
||||||
|
{
|
||||||
|
if (groupNameCache.TryGetValue(id, out var cached)) return cached;
|
||||||
|
var label = _groups.GetById(id)?.Name ?? "";
|
||||||
|
groupNameCache[id] = label;
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relevantStudentIds = SelectedStudentFilter.Id == Guid.Empty
|
||||||
|
? _groupStudents.Select(s => s.Id)
|
||||||
|
: [SelectedStudentFilter.Id];
|
||||||
|
|
||||||
|
var all = relevantStudentIds
|
||||||
|
.SelectMany(id => _docs.GetByStudent(id))
|
||||||
|
.Where(d => !OnlyThisGroup || d.GroupId == _groupId)
|
||||||
|
.OrderByDescending(d => d.Date);
|
||||||
|
|
||||||
|
foreach (var d in all)
|
||||||
|
{
|
||||||
|
var isOwnGroup = d.GroupId is null || d.GroupId == _groupId;
|
||||||
|
var otherGroupLabel = isOwnGroup ? "" : GroupLabel(d.GroupId!.Value);
|
||||||
|
Entries.Add(new DocumentationItem(d, studentNameById.GetValueOrDefault(d.StudentId, ""),
|
||||||
|
isOwnGroup, otherGroupLabel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task AddDocumentation()
|
||||||
|
{
|
||||||
|
if (OnEditDocumentation is null || _groupStudents.Count == 0) return;
|
||||||
|
var result = await OnEditDocumentation(_groupId, _groupStudents, null);
|
||||||
|
if (result is null) return;
|
||||||
|
_docs.Save(result);
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task EditDocumentation(DocumentationItem? item)
|
||||||
|
{
|
||||||
|
if (item is null || OnEditDocumentation is null) return;
|
||||||
|
var result = await OnEditDocumentation(_groupId, _groupStudents, item.Model);
|
||||||
|
if (result is null) return;
|
||||||
|
_docs.Save(result);
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task DeleteDocumentation(DocumentationItem? item)
|
||||||
|
{
|
||||||
|
if (item is null) return;
|
||||||
|
if (OnConfirmDeleteDocumentation is not null && !await OnConfirmDeleteDocumentation(item)) return;
|
||||||
|
_docs.Delete(item.Model.Id);
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ConductParentCall(DocumentationItem? item)
|
||||||
|
{
|
||||||
|
if (item is null || OnConductParentCall is null) return;
|
||||||
|
var result = await OnConductParentCall(item.Model, item.StudentName);
|
||||||
|
if (result is null) return;
|
||||||
|
_docs.Save(result);
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -219,6 +219,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public PlanningTabViewModel PlanningTab { get; }
|
public PlanningTabViewModel PlanningTab { get; }
|
||||||
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
|
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
|
||||||
public SeatingPlanTabViewModel SeatingPlanTab { get; }
|
public SeatingPlanTabViewModel SeatingPlanTab { get; }
|
||||||
|
public GroupDocumentationTabViewModel GroupDocumentationTab { get; }
|
||||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||||
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
||||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||||
@@ -234,7 +235,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
|
||||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
||||||
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
|
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
|
||||||
SeatingPlanTabViewModel seatingPlanTab)
|
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;
|
||||||
@@ -243,6 +244,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
PlanningTab = planningTab;
|
PlanningTab = planningTab;
|
||||||
CompetencyOverviewTab = competencyOverviewTab;
|
CompetencyOverviewTab = competencyOverviewTab;
|
||||||
SeatingPlanTab = seatingPlanTab;
|
SeatingPlanTab = seatingPlanTab;
|
||||||
|
GroupDocumentationTab = groupDocumentationTab;
|
||||||
SeatingPlanTab.OnAssessmentChanged = () =>
|
SeatingPlanTab.OnAssessmentChanged = () =>
|
||||||
{
|
{
|
||||||
ParticipationTab.LoadSessions();
|
ParticipationTab.LoadSessions();
|
||||||
@@ -268,6 +270,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
PlanningTab.Initialize(Group.Id, IsReadOnly);
|
PlanningTab.Initialize(Group.Id, IsReadOnly);
|
||||||
CompetencyOverviewTab.Initialize(Group);
|
CompetencyOverviewTab.Initialize(Group);
|
||||||
SeatingPlanTab.Initialize(Group.Id, IsReadOnly);
|
SeatingPlanTab.Initialize(Group.Id, IsReadOnly);
|
||||||
|
GroupDocumentationTab.Initialize(Group.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadExams()
|
private void ReloadExams()
|
||||||
|
|||||||
@@ -361,6 +361,9 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
|
|
||||||
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
|
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
|
||||||
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
||||||
|
/// Abwesend im Sinne der Mitarbeitsbewertung: eine Bewertung ergibt für diese Stunde keinen
|
||||||
|
/// Sinn, unabhängig davon, ob die Abwesenheit entschuldigt ist oder noch geklärt werden muss.
|
||||||
|
public bool IsAbsent => Attendance is not null and not AttendanceStatus.Present;
|
||||||
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
|
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
|
||||||
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
|
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
|
||||||
|
|
||||||
@@ -442,6 +445,7 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
};
|
};
|
||||||
OnPropertyChanged(nameof(AttendanceLabel));
|
OnPropertyChanged(nameof(AttendanceLabel));
|
||||||
OnPropertyChanged(nameof(AttendanceTooltip));
|
OnPropertyChanged(nameof(AttendanceTooltip));
|
||||||
|
OnPropertyChanged(nameof(IsAbsent));
|
||||||
AttendanceChangedCallback?.Invoke(StudentId, Attendance);
|
AttendanceChangedCallback?.Invoke(StudentId, Attendance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,6 +455,7 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
Attendance = value;
|
Attendance = value;
|
||||||
OnPropertyChanged(nameof(AttendanceLabel));
|
OnPropertyChanged(nameof(AttendanceLabel));
|
||||||
OnPropertyChanged(nameof(AttendanceTooltip));
|
OnPropertyChanged(nameof(AttendanceTooltip));
|
||||||
|
OnPropertyChanged(nameof(IsAbsent));
|
||||||
AttendanceChangedCallback?.Invoke(StudentId, value);
|
AttendanceChangedCallback?.Invoke(StudentId, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,6 +732,13 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _currentAspectLabel = "";
|
[ObservableProperty] private string _currentAspectLabel = "";
|
||||||
[ObservableProperty] private string _currentValueLabel = "";
|
[ObservableProperty] private string _currentValueLabel = "";
|
||||||
[ObservableProperty] private string _progressText = "";
|
[ObservableProperty] private string _progressText = "";
|
||||||
|
[ObservableProperty] private bool _currentStudentIsAbsent;
|
||||||
|
[ObservableProperty] private string _currentStudentAttendanceLabel = "";
|
||||||
|
|
||||||
|
/// Dimmt Name/Aspektliste, wenn der aktuelle Schüler abwesend ist — kein Blockieren der
|
||||||
|
/// Eingabe (manche Bewertungssysteme wollen trotzdem einen Eintrag, z.B. "0 Punkte"), nur ein
|
||||||
|
/// visueller Hinweis, dass eine Bewertung hier normalerweise keinen Sinn ergibt.
|
||||||
|
public double CurrentStudentContentOpacity => CurrentStudentIsAbsent ? 0.4 : 1.0;
|
||||||
|
|
||||||
public ObservableCollection<QuickAspectRow> AspectRows { get; } = [];
|
public ObservableCollection<QuickAspectRow> AspectRows { get; } = [];
|
||||||
|
|
||||||
@@ -758,6 +770,7 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
partial void OnAspectIndexChanged(int value) => OnPropertyChanged(nameof(HotkeyLegend));
|
partial void OnAspectIndexChanged(int value) => OnPropertyChanged(nameof(HotkeyLegend));
|
||||||
|
partial void OnCurrentStudentIsAbsentChanged(bool value) => OnPropertyChanged(nameof(CurrentStudentContentOpacity));
|
||||||
|
|
||||||
private AspectValueType CurrentAspectType() =>
|
private AspectValueType CurrentAspectType() =>
|
||||||
_aspects.Count == 0 ? AspectValueType.Scale5 : _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].ValueType;
|
_aspects.Count == 0 ? AspectValueType.Scale5 : _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].ValueType;
|
||||||
@@ -771,6 +784,8 @@ public partial class QuickInputViewModel : ObservableObject
|
|||||||
var row = _rows[index];
|
var row = _rows[index];
|
||||||
StudentName = row.Name;
|
StudentName = row.Name;
|
||||||
ProgressText = $"{index + 1} / {_rows.Count}";
|
ProgressText = $"{index + 1} / {_rows.Count}";
|
||||||
|
CurrentStudentIsAbsent = row.IsAbsent;
|
||||||
|
CurrentStudentAttendanceLabel = row.AttendanceTooltip;
|
||||||
|
|
||||||
AspectRows.Clear();
|
AspectRows.Clear();
|
||||||
foreach (var (a, i) in _aspects.Select((a, i) => (a, i)))
|
foreach (var (a, i) in _aspects.Select((a, i) => (a, i)))
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
private readonly IGroupMembershipRepository _memberships;
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
private readonly IGradingSchemeRepository _schemes;
|
private readonly IGradingSchemeRepository _schemes;
|
||||||
private readonly IReportGradeRepository _reportGrades;
|
private readonly IReportGradeRepository _reportGrades;
|
||||||
|
private readonly IParticipationSessionRepository _participationSessions;
|
||||||
|
private readonly IParticipationRepository _participation;
|
||||||
|
private readonly AttendanceBalanceService _attendanceBalance;
|
||||||
private readonly GradingService _grading;
|
private readonly GradingService _grading;
|
||||||
private readonly Guid _groupId;
|
private readonly Guid _groupId;
|
||||||
private readonly GroupType _groupType;
|
private readonly GroupType _groupType;
|
||||||
@@ -51,11 +54,15 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
public ReportGradeDialogViewModel(IGradeRepository grades, IExamRepository exams,
|
public ReportGradeDialogViewModel(IGradeRepository grades, IExamRepository exams,
|
||||||
IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships,
|
IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships,
|
||||||
IGradingSchemeRepository schemes, IReportGradeRepository reportGrades, GradingService grading,
|
IGradingSchemeRepository schemes, IReportGradeRepository reportGrades,
|
||||||
|
IParticipationSessionRepository participationSessions, IParticipationRepository participation,
|
||||||
|
AttendanceBalanceService attendanceBalance, GradingService grading,
|
||||||
Guid groupId, GroupType groupType, GradingSystem gradingSystem, string groupLabel, string schoolYear)
|
Guid groupId, GroupType groupType, GradingSystem gradingSystem, string groupLabel, string schoolYear)
|
||||||
{
|
{
|
||||||
_grades = grades; _exams = exams; _results = results; _students = students;
|
_grades = grades; _exams = exams; _results = results; _students = students;
|
||||||
_memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading;
|
_memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading;
|
||||||
|
_participationSessions = participationSessions; _participation = participation;
|
||||||
|
_attendanceBalance = attendanceBalance;
|
||||||
_groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel;
|
_groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel;
|
||||||
_schoolYear = schoolYear;
|
_schoolYear = schoolYear;
|
||||||
|
|
||||||
@@ -92,17 +99,36 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
||||||
var allGrades = _grades.GetByGroup(_groupId).Where(g => g.Date >= periodFrom && g.Date <= periodTo).ToList();
|
var allGrades = _grades.GetByGroup(_groupId).Where(g => g.Date >= periodFrom && g.Date <= periodTo).ToList();
|
||||||
|
|
||||||
|
// Fehlquote (Nutzer-Feedback): je Schüler die Anwesenheits-Bilanz im gewählten Zeitraum,
|
||||||
|
// nur aus Sitzungen dieser Gruppe (anders als StudentDetailViewModel.LoadAttendanceBalance,
|
||||||
|
// das gruppenübergreifend über den ganzen Schüler rechnet) — hier zählt nur, was für diese
|
||||||
|
// Zeugnisnote relevant ist.
|
||||||
|
var sessionsInPeriod = _participationSessions.GetByGroup(_groupId)
|
||||||
|
.Where(s => s.Date >= periodFrom && s.Date <= periodTo).ToList();
|
||||||
|
var attendanceByStudent = new Dictionary<Guid, List<(DateOnly Date, AttendanceStatus? Status)>>();
|
||||||
|
foreach (var session in sessionsInPeriod)
|
||||||
|
foreach (var entry in _participation.GetBySession(session.Id))
|
||||||
|
{
|
||||||
|
if (!attendanceByStudent.TryGetValue(entry.StudentId, out var list))
|
||||||
|
attendanceByStudent[entry.StudentId] = list = [];
|
||||||
|
list.Add((session.Date, entry.Attendance));
|
||||||
|
}
|
||||||
|
|
||||||
Rows.Clear();
|
Rows.Clear();
|
||||||
foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||||
{
|
{
|
||||||
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
||||||
if (membership is not null && !GroupMembershipService.Overlaps(membership, periodFrom, periodTo)) continue;
|
if (membership is not null && !GroupMembershipService.Overlaps(membership, periodFrom, periodTo)) continue;
|
||||||
|
|
||||||
|
var absenceRate = _attendanceBalance.Calculate(
|
||||||
|
attendanceByStudent.TryGetValue(student.Id, out var entries) ? entries : [],
|
||||||
|
periodFrom, periodTo).AbsenceRatePercent;
|
||||||
|
|
||||||
var existing = _reportGrades.GetByStudentGroupPeriod(student.Id, _groupId, periodTag);
|
var existing = _reportGrades.GetByStudentGroupPeriod(student.Id, _groupId, periodTag);
|
||||||
|
|
||||||
if (existing is { IsLocked: true })
|
if (existing is { IsLocked: true })
|
||||||
{
|
{
|
||||||
Rows.Add(ReportGradeRow.FromLocked(student.Id, student.FullName, existing, Save, ToggleLock));
|
Rows.Add(ReportGradeRow.FromLocked(student.Id, student.FullName, existing, absenceRate, Save, ToggleLock));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +154,7 @@ public partial class ReportGradeDialogViewModel : ObservableObject
|
|||||||
var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades,
|
var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades,
|
||||||
scheme, _gradingSystem, RoundingRule);
|
scheme, _gradingSystem, RoundingRule);
|
||||||
|
|
||||||
Rows.Add(ReportGradeRow.FromCalculated(student.Id, student.FullName, calculated, existing, Save, ToggleLock));
|
Rows.Add(ReportGradeRow.FromCalculated(student.Id, student.FullName, calculated, existing, absenceRate, Save, ToggleLock));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,10 +228,16 @@ public static class RoundingRuleDisplay
|
|||||||
|
|
||||||
public partial class ReportGradeRow : ObservableObject
|
public partial class ReportGradeRow : ObservableObject
|
||||||
{
|
{
|
||||||
|
/// Nutzer-Vorgabe: ab dieser Fehlquote darf unabhängig von der fachlichen Leistung eine 5
|
||||||
|
/// vergeben werden — reine Information/Hervorhebung, kein automatisches Übersteuern der
|
||||||
|
/// berechneten Note.
|
||||||
|
public const double HighAbsenceThresholdPercent = 50.0;
|
||||||
|
|
||||||
public Guid StudentId { get; }
|
public Guid StudentId { get; }
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public string? CalculatedValue { get; private set; }
|
public string? CalculatedValue { get; private set; }
|
||||||
public bool IsLocked { get; private set; }
|
public bool IsLocked { get; private set; }
|
||||||
|
public double AbsenceRatePercent { get; }
|
||||||
|
|
||||||
[ObservableProperty] private string? _overrideValue;
|
[ObservableProperty] private string? _overrideValue;
|
||||||
[ObservableProperty] private string? _overrideReason;
|
[ObservableProperty] private string? _overrideReason;
|
||||||
@@ -214,17 +246,22 @@ public partial class ReportGradeRow : ObservableObject
|
|||||||
public string CalculatedDisplay => CalculatedValue ?? "–";
|
public string CalculatedDisplay => CalculatedValue ?? "–";
|
||||||
public string FinalDisplay => !string.IsNullOrWhiteSpace(OverrideValue) ? OverrideValue! : CalculatedDisplay;
|
public string FinalDisplay => !string.IsNullOrWhiteSpace(OverrideValue) ? OverrideValue! : CalculatedDisplay;
|
||||||
public string LockLabel => IsLocked ? "Entsperren" : "Festschreiben";
|
public string LockLabel => IsLocked ? "Entsperren" : "Festschreiben";
|
||||||
|
public string AbsenceRateDisplay => $"{AbsenceRatePercent:0.#} % gefehlt";
|
||||||
|
public bool HasHighAbsenceRate => AbsenceRatePercent >= HighAbsenceThresholdPercent;
|
||||||
|
// Rot wie AttendanceDisplay.Color(Truant) — dieselbe Warnfarbe wie im Mitarbeit-Feature.
|
||||||
|
public string AbsenceRateColorHex => HasHighAbsenceRate ? "#D64545" : "#8A8A8A";
|
||||||
|
|
||||||
public IRelayCommand SaveCommand { get; }
|
public IRelayCommand SaveCommand { get; }
|
||||||
public IRelayCommand ToggleLockCommand { get; }
|
public IRelayCommand ToggleLockCommand { get; }
|
||||||
|
|
||||||
private ReportGradeRow(Guid studentId, string name, string? calculated, ReportGrade? existing,
|
private ReportGradeRow(Guid studentId, string name, string? calculated, ReportGrade? existing,
|
||||||
bool locked, Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock)
|
double absenceRatePercent, bool locked, Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock)
|
||||||
{
|
{
|
||||||
StudentId = studentId;
|
StudentId = studentId;
|
||||||
Name = name;
|
Name = name;
|
||||||
CalculatedValue = calculated;
|
CalculatedValue = calculated;
|
||||||
IsLocked = locked;
|
IsLocked = locked;
|
||||||
|
AbsenceRatePercent = absenceRatePercent;
|
||||||
_overrideValue = existing?.OverrideValue;
|
_overrideValue = existing?.OverrideValue;
|
||||||
_overrideReason = existing?.OverrideReason;
|
_overrideReason = existing?.OverrideReason;
|
||||||
SaveCommand = new RelayCommand(() => onSave(this));
|
SaveCommand = new RelayCommand(() => onSave(this));
|
||||||
@@ -232,12 +269,13 @@ public partial class ReportGradeRow : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static ReportGradeRow FromCalculated(Guid studentId, string name, string? calculated,
|
public static ReportGradeRow FromCalculated(Guid studentId, string name, string? calculated,
|
||||||
ReportGrade? existing, Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock) =>
|
ReportGrade? existing, double absenceRatePercent,
|
||||||
new(studentId, name, calculated, existing, existing?.IsLocked ?? false, onSave, onToggleLock);
|
Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock) =>
|
||||||
|
new(studentId, name, calculated, existing, absenceRatePercent, existing?.IsLocked ?? false, onSave, onToggleLock);
|
||||||
|
|
||||||
public static ReportGradeRow FromLocked(Guid studentId, string name, ReportGrade locked,
|
public static ReportGradeRow FromLocked(Guid studentId, string name, ReportGrade locked,
|
||||||
Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock) =>
|
double absenceRatePercent, Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock) =>
|
||||||
new(studentId, name, locked.CalculatedValue, locked, true, onSave, onToggleLock);
|
new(studentId, name, locked.CalculatedValue, locked, absenceRatePercent, true, onSave, onToggleLock);
|
||||||
|
|
||||||
public void MarkSaved()
|
public void MarkSaved()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ using LehrerApp.Core.Interfaces;
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.ViewModels.Students;
|
namespace LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
|
||||||
|
/// Für die Schüler-Auswahl im Dokumentationsdialog, wenn er ohne festen Schüler geöffnet wird
|
||||||
|
/// (Gruppen-Tab, siehe GroupDocumentationTabViewModel).
|
||||||
|
public record StudentOption(Guid Id, string Name);
|
||||||
|
|
||||||
// ── Dokumentation: deutsche Anzeige für Typ/Status (5.1) ─────────────────────
|
// ── Dokumentation: deutsche Anzeige für Typ/Status (5.1) ─────────────────────
|
||||||
|
|
||||||
public static class DocumentationTypeDisplay
|
public static class DocumentationTypeDisplay
|
||||||
@@ -86,8 +91,17 @@ public partial class DocumentationDialogViewModel : ObservableObject
|
|||||||
private readonly IAttachmentStorage _attachmentStorage;
|
private readonly IAttachmentStorage _attachmentStorage;
|
||||||
private readonly Documentation? _editing;
|
private readonly Documentation? _editing;
|
||||||
private readonly Guid _studentId;
|
private readonly Guid _studentId;
|
||||||
|
private readonly Guid? _contextGroupId;
|
||||||
private readonly List<string> _newlyUploadedStorageIds = [];
|
private readonly List<string> _newlyUploadedStorageIds = [];
|
||||||
|
|
||||||
|
// Nur gesetzt, wenn der Dialog aus einem Gruppen-Kontext (5.1, Dokumentation-Tab der
|
||||||
|
// Lerngruppe) ohne festen Schüler geöffnet wird — vom Schüler-Tab aus (fester _studentId)
|
||||||
|
// bleibt die Liste leer und die Auswahl unsichtbar.
|
||||||
|
public List<StudentOption> StudentOptions { get; }
|
||||||
|
public bool CanPickStudent => StudentOptions.Count > 0;
|
||||||
|
[ObservableProperty] private StudentOption? _selectedStudent;
|
||||||
|
[ObservableProperty] private string _studentError = "";
|
||||||
|
|
||||||
[ObservableProperty] private string _typeName = DocumentationTypeDisplay.Options[0];
|
[ObservableProperty] private string _typeName = DocumentationTypeDisplay.Options[0];
|
||||||
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||||
[ObservableProperty] private string _title = "";
|
[ObservableProperty] private string _title = "";
|
||||||
@@ -145,11 +159,16 @@ public partial class DocumentationDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
public Documentation? Result { get; private set; }
|
public Documentation? Result { get; private set; }
|
||||||
|
|
||||||
public DocumentationDialogViewModel(Guid studentId, Documentation? editing, IAttachmentStorage attachmentStorage)
|
public DocumentationDialogViewModel(Guid studentId, Documentation? editing, IAttachmentStorage attachmentStorage,
|
||||||
|
List<StudentOption>? studentOptions = null, Guid? contextGroupId = null)
|
||||||
{
|
{
|
||||||
_studentId = studentId;
|
_studentId = studentId;
|
||||||
_editing = editing;
|
_editing = editing;
|
||||||
_attachmentStorage = attachmentStorage;
|
_attachmentStorage = attachmentStorage;
|
||||||
|
_contextGroupId = contextGroupId;
|
||||||
|
StudentOptions = studentOptions ?? [];
|
||||||
|
if (CanPickStudent)
|
||||||
|
SelectedStudent = StudentOptions.FirstOrDefault(s => s.Id == studentId);
|
||||||
if (editing is null) return;
|
if (editing is null) return;
|
||||||
|
|
||||||
TypeName = DocumentationTypeDisplay.Label(editing.Type);
|
TypeName = DocumentationTypeDisplay.Label(editing.Type);
|
||||||
@@ -276,10 +295,12 @@ public partial class DocumentationDialogViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Save()
|
private void Save()
|
||||||
{
|
{
|
||||||
TitleError = ""; DateTextError = ""; ReviewDateTextError = "";
|
TitleError = ""; DateTextError = ""; ReviewDateTextError = ""; StudentError = "";
|
||||||
LetterSentDateError = ""; LetterResponseDateError = "";
|
LetterSentDateError = ""; LetterResponseDateError = "";
|
||||||
var valid = true;
|
var valid = true;
|
||||||
|
|
||||||
|
if (CanPickStudent && SelectedStudent is null) { StudentError = "Schüler auswählen."; valid = false; }
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
|
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
|
||||||
|
|
||||||
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
||||||
@@ -312,7 +333,8 @@ public partial class DocumentationDialogViewModel : ObservableObject
|
|||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
|
|
||||||
var type = DocumentationTypeDisplay.FromLabel(TypeName);
|
var type = DocumentationTypeDisplay.FromLabel(TypeName);
|
||||||
Result = _editing ?? new Documentation { StudentId = _studentId };
|
var effectiveStudentId = CanPickStudent ? SelectedStudent!.Id : _studentId;
|
||||||
|
Result = _editing ?? new Documentation { StudentId = effectiveStudentId, GroupId = _contextGroupId };
|
||||||
Result.Type = type;
|
Result.Type = type;
|
||||||
Result.Date = date;
|
Result.Date = date;
|
||||||
Result.Title = Title.Trim();
|
Result.Title = Title.Trim();
|
||||||
@@ -401,10 +423,21 @@ public partial class DocumentationItem : ObservableObject
|
|||||||
public bool HasAttachments { get; }
|
public bool HasAttachments { get; }
|
||||||
public string StatusLabel { get; }
|
public string StatusLabel { get; }
|
||||||
public List<TagChip> TagChips { get; }
|
public List<TagChip> TagChips { get; }
|
||||||
|
/// Nur im Gruppen-Tab (5.1, GroupDocumentationTabViewModel) gefüllt — die Schüler-Detailansicht
|
||||||
|
/// zeigt ohnehin nur Einträge eines einzelnen Schülers und braucht den Namen nicht.
|
||||||
|
public string StudentName { get; }
|
||||||
|
/// True, wenn der Eintrag keiner Gruppe zugeordnet ist oder der aktuell angezeigten Gruppe
|
||||||
|
/// entspricht — false für Einträge aus einem anderen Unterricht desselben Schülers (werden im
|
||||||
|
/// Gruppen-Tab optisch abgesetzt statt komplett ausgeblendet, siehe Nutzer-Feedback).
|
||||||
|
public bool IsOwnGroup { get; }
|
||||||
|
/// Name der Gruppe, aus der ein nicht-eigener Eintrag stammt (nur gesetzt, wenn !IsOwnGroup).
|
||||||
|
public string OtherGroupLabel { get; }
|
||||||
|
/// Dimmt Einträge aus anderen Lerngruppen im Gruppen-Tab, ohne sie zu verstecken.
|
||||||
|
public double ContentOpacity => IsOwnGroup ? 1.0 : 0.55;
|
||||||
|
|
||||||
[ObservableProperty] private bool _isRevealed;
|
[ObservableProperty] private bool _isRevealed;
|
||||||
|
|
||||||
public DocumentationItem(Documentation d)
|
public DocumentationItem(Documentation d, string studentName = "", bool isOwnGroup = true, string otherGroupLabel = "")
|
||||||
{
|
{
|
||||||
Model = d;
|
Model = d;
|
||||||
DateDisplay = d.Date.ToString("dd.MM.yyyy");
|
DateDisplay = d.Date.ToString("dd.MM.yyyy");
|
||||||
@@ -415,6 +448,9 @@ public partial class DocumentationItem : ObservableObject
|
|||||||
HasAttachments = d.Attachments.Count > 0;
|
HasAttachments = d.Attachments.Count > 0;
|
||||||
StatusLabel = BuildStatusLabel(d);
|
StatusLabel = BuildStatusLabel(d);
|
||||||
TagChips = d.Tags.Select(t => new TagChip(t)).ToList();
|
TagChips = d.Tags.Select(t => new TagChip(t)).ToList();
|
||||||
|
StudentName = studentName;
|
||||||
|
IsOwnGroup = isOwnGroup;
|
||||||
|
OtherGroupLabel = otherGroupLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildStatusLabel(Documentation d) => d.Type switch
|
private static string BuildStatusLabel(Documentation d) => d.Type switch
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ public partial class GradeOverviewTabView : UserControl
|
|||||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||||
App.Services.GetRequiredService<IGradingSchemeRepository>(),
|
App.Services.GetRequiredService<IGradingSchemeRepository>(),
|
||||||
App.Services.GetRequiredService<IReportGradeRepository>(),
|
App.Services.GetRequiredService<IReportGradeRepository>(),
|
||||||
|
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||||
|
App.Services.GetRequiredService<IParticipationRepository>(),
|
||||||
|
App.Services.GetRequiredService<AttendanceBalanceService>(),
|
||||||
App.Services.GetRequiredService<GradingService>(),
|
App.Services.GetRequiredService<GradingService>(),
|
||||||
_vm!.GroupId, _vm.GroupType, _vm.GradingSystem, _vm.GroupLabel, _vm.SchoolYear);
|
_vm!.GroupId, _vm.GroupType, _vm.GradingSystem, _vm.GroupLabel, _vm.SchoolYear);
|
||||||
|
|
||||||
|
|||||||
@@ -205,12 +205,7 @@
|
|||||||
|
|
||||||
<!-- Tab: Dokumentation -->
|
<!-- Tab: Dokumentation -->
|
||||||
<ContentPage Header="Dokumentation">
|
<ContentPage Header="Dokumentation">
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<views:GroupDocumentationTabView DataContext="{Binding GroupDocumentationTab}"/>
|
||||||
<TextBlock Text="Schülerdokumentation" FontSize="16" Opacity="0.4"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Wird implementiert." FontSize="12" Opacity="0.3"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
xmlns:svm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.GroupDocumentationTabView"
|
||||||
|
x:DataType="vm:GroupDocumentationTabViewModel">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*" Margin="16">
|
||||||
|
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto" Margin="0,0,0,12">
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="Schüler:" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox ItemsSource="{Binding StudentFilterOptions}" SelectedItem="{Binding SelectedStudentFilter}"
|
||||||
|
DisplayMemberBinding="{Binding Name}" MinWidth="170"/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Grid.Column="1" Content="Nur dieser Unterricht" IsChecked="{Binding OnlyThisGroup}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="Standardmäßig werden auch Einträge aus anderen Lerngruppen desselben Schülers angezeigt (optisch abgesetzt) — damit Muster über mehrere Fächer/Kurse hinweg sichtbar bleiben."/>
|
||||||
|
<Button Grid.Column="3" Content="+ Eintrag" Command="{Binding AddDocumentationCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<StackPanel>
|
||||||
|
<ItemsControl ItemsSource="{Binding Entries}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="svm:DocumentationItem">
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="6" Padding="12,10" Margin="0,0,0,8"
|
||||||
|
Opacity="{Binding ContentOpacity}">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<Grid ColumnDefinitions="80,*,Auto,Auto,Auto,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" Opacity="0.5" FontSize="12"/>
|
||||||
|
<StackPanel Grid.Column="1" Margin="8,0">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"/>
|
||||||
|
<TextBlock Text="{Binding Model.Title}" FontSize="13" Opacity="0.8"
|
||||||
|
IsVisible="{Binding IsRevealed}"/>
|
||||||
|
<TextBlock Text="Vertraulich" FontSize="13" Opacity="0.6"
|
||||||
|
IsVisible="{Binding !IsRevealed}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Text="{Binding TypeLabel}" FontSize="11" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding OtherGroupLabel, StringFormat='aus: {0}'}" FontSize="11"
|
||||||
|
Opacity="0.5" FontStyle="Italic" IsVisible="{Binding !IsOwnGroup}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="🔒" FontSize="14" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding IsConfidential}" ToolTip.Tip="Vertraulich"/>
|
||||||
|
<Button Grid.Column="3" Content="Anzeigen" FontSize="11" Padding="8,3" Margin="6,0,0,0"
|
||||||
|
Command="{Binding RevealCommand}" IsVisible="{Binding !IsRevealed}"/>
|
||||||
|
<StackPanel Grid.Column="4" Orientation="Horizontal" Spacing="4" Margin="6,0,0,0"
|
||||||
|
IsVisible="{Binding IsRevealed}">
|
||||||
|
<Button Content="Gespräch begleiten" FontSize="11" Padding="8,3"
|
||||||
|
IsVisible="{Binding IsParentCall}"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:GroupDocumentationTabViewModel)DataContext).ConductParentCallCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="Bearbeiten" FontSize="11" Padding="8,3"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:GroupDocumentationTabViewModel)DataContext).EditDocumentationCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="Löschen" FontSize="11" Padding="8,3"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:GroupDocumentationTabViewModel)DataContext).DeleteDocumentationCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding Model.Content}" FontSize="12" TextWrapping="Wrap" Opacity="0.8"
|
||||||
|
IsVisible="{Binding IsRevealed}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" IsVisible="{Binding IsRevealed}">
|
||||||
|
<TextBlock Text="{Binding StatusLabel}" FontSize="11" Opacity="0.55"
|
||||||
|
IsVisible="{Binding StatusLabel, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<TextBlock Text="📎 Anhang" FontSize="11" Opacity="0.55" IsVisible="{Binding HasAttachments}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<ItemsControl ItemsSource="{Binding TagChips}" IsVisible="{Binding IsRevealed}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><WrapPanel ItemSpacing="6" LineSpacing="4"/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="svm:TagChip">
|
||||||
|
<Border Background="{Binding ColorHex}" CornerRadius="10" Padding="8,2">
|
||||||
|
<TextBlock Text="{Binding Text}" FontSize="10" Foreground="White"/>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine Dokumentation vorhanden." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !Entries.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using LehrerApp.Desktop.Views.Shared;
|
||||||
|
using LehrerApp.Desktop.Views.Students;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class GroupDocumentationTabView : UserControl
|
||||||
|
{
|
||||||
|
public GroupDocumentationTabView() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is not GroupDocumentationTabViewModel vm) return;
|
||||||
|
vm.OnEditDocumentation = ShowDocumentationDialog;
|
||||||
|
vm.OnConfirmDeleteDocumentation = ShowDeleteDocumentationDialog;
|
||||||
|
vm.OnConductParentCall = ShowParentCallSessionDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Documentation?> ShowDocumentationDialog(
|
||||||
|
Guid groupId, List<StudentOption> studentOptions, Documentation? editing)
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
|
||||||
|
var vm = new DocumentationDialogViewModel(editing?.StudentId ?? Guid.Empty, editing,
|
||||||
|
App.Services.GetRequiredService<IAttachmentStorage>(), studentOptions, groupId);
|
||||||
|
var dialog = new DocumentationDialog { DataContext = vm };
|
||||||
|
var saved = await dialog.ShowDialog<bool>(owner);
|
||||||
|
if (!saved) vm.DiscardUnsavedAttachments();
|
||||||
|
return saved ? vm.Result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Documentation?> ShowParentCallSessionDialog(Documentation documentation, string studentTitle)
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
|
||||||
|
var vm = new ParentCallSessionViewModel(documentation, studentTitle);
|
||||||
|
var dialog = new ParentCallSessionDialog { DataContext = vm };
|
||||||
|
var saved = await dialog.ShowDialog<bool>(owner);
|
||||||
|
return saved ? vm.Result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShowDeleteDocumentationDialog(DocumentationItem item)
|
||||||
|
{
|
||||||
|
var info = new ConfirmDialogInfo
|
||||||
|
{
|
||||||
|
Title = "Eintrag löschen?",
|
||||||
|
Message = $"\"{item.Model.Title}\" ({item.StudentName}) wird als gelöscht markiert und nicht mehr angezeigt.",
|
||||||
|
ConfirmText = "Löschen",
|
||||||
|
};
|
||||||
|
var dialog = new ConfirmDialog { DataContext = info };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,15 +12,21 @@
|
|||||||
<!-- Schülername + Fortschritt -->
|
<!-- Schülername + Fortschritt -->
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
||||||
<StackPanel Grid.Column="0">
|
<StackPanel Grid.Column="0">
|
||||||
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"/>
|
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"
|
||||||
<TextBlock Text="{Binding CurrentAspectLabel}" FontSize="13" Opacity="0.5"/>
|
Opacity="{Binding CurrentStudentContentOpacity}"/>
|
||||||
|
<TextBlock Text="{Binding CurrentAspectLabel}" FontSize="13" Opacity="0.5"
|
||||||
|
IsVisible="{Binding !CurrentStudentIsAbsent}"/>
|
||||||
|
<TextBlock Text="{Binding CurrentStudentAttendanceLabel, StringFormat='⚠ Abwesend — {0}'}"
|
||||||
|
FontSize="13" Foreground="#D96C00" FontWeight="SemiBold"
|
||||||
|
IsVisible="{Binding CurrentStudentIsAbsent}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding ProgressText}"
|
<TextBlock Grid.Column="1" Text="{Binding ProgressText}"
|
||||||
VerticalAlignment="Top" FontSize="13" Opacity="0.4"/>
|
VerticalAlignment="Top" FontSize="13" Opacity="0.4"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Aspektliste -->
|
<!-- Aspektliste -->
|
||||||
<ItemsControl Grid.Row="1" ItemsSource="{Binding AspectRows}">
|
<ItemsControl Grid.Row="1" ItemsSource="{Binding AspectRows}"
|
||||||
|
Opacity="{Binding CurrentStudentContentOpacity}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:QuickAspectRow">
|
<DataTemplate x:DataType="vm:QuickAspectRow">
|
||||||
<Grid ColumnDefinitions="4,8,*,44" Margin="0,3">
|
<Grid ColumnDefinitions="4,8,*,44" Margin="0,3">
|
||||||
|
|||||||
@@ -33,7 +33,12 @@
|
|||||||
BorderThickness="0,0,0,1" Padding="0,8">
|
BorderThickness="0,0,0,1" Padding="0,8">
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
<Grid ColumnDefinitions="180,90,*,120,Auto,Auto">
|
<Grid ColumnDefinitions="180,90,*,120,Auto,Auto">
|
||||||
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center" FontSize="13"/>
|
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="13"/>
|
||||||
|
<TextBlock Text="{Binding AbsenceRateDisplay}" FontSize="11"
|
||||||
|
Foreground="{Binding AbsenceRateColorHex}"
|
||||||
|
ToolTip.Tip="Fehlquote im gewählten Zeitraum — ab 50 % darf laut Schulrecht unabhängig von der fachlichen Leistung eine 5 vergeben werden."/>
|
||||||
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding CalculatedDisplay}" VerticalAlignment="Center"
|
<TextBlock Grid.Column="1" Text="{Binding CalculatedDisplay}" VerticalAlignment="Center"
|
||||||
FontSize="13" Opacity="0.7" ToolTip.Tip="Berechnet"/>
|
FontSize="13" Opacity="0.7" ToolTip.Tip="Berechnet"/>
|
||||||
<TextBox Grid.Column="2" Text="{Binding OverrideValue}" PlaceholderText="Übersteuern (optional)"
|
<TextBox Grid.Column="2" Text="{Binding OverrideValue}" PlaceholderText="Übersteuern (optional)"
|
||||||
|
|||||||
@@ -12,6 +12,15 @@
|
|||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4" IsVisible="{Binding CanPickStudent}">
|
||||||
|
<TextBlock Text="Schüler *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding StudentOptions}" SelectedItem="{Binding SelectedStudent}"
|
||||||
|
DisplayMemberBinding="{Binding Name}" HorizontalAlignment="Stretch"
|
||||||
|
PlaceholderText="Schüler wählen"/>
|
||||||
|
<TextBlock Text="{Binding StudentError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding StudentError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,12,140">
|
<Grid ColumnDefinitions="*,12,140">
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ vollständig umgesetzt (siehe unten), ebenso der gleichnamige Tab im Schülerdet
|
|||||||
- [x] **2.4.2** Manuelles Übersteuern mit Pflicht-Begründung (pädagogischer Spielraum).
|
- [x] **2.4.2** Manuelles Übersteuern mit Pflicht-Begründung (pädagogischer Spielraum).
|
||||||
- [x] **2.4.3** Zeugnisnoten-Ansicht mit Sperren/Festschreiben zum Konferenztermin.
|
- [x] **2.4.3** Zeugnisnoten-Ansicht mit Sperren/Festschreiben zum Konferenztermin.
|
||||||
- [x] **2.4.4** Export der Zeugnisnotenliste (siehe 11.2).
|
- [x] **2.4.4** Export der Zeugnisnotenliste (siehe 11.2).
|
||||||
|
- [x] **2.4.5** Fehlquote je Schüler im Zeugnisnoten-Dialog anzeigen (Nutzer-Feedback: "ab 50 %
|
||||||
|
darf eine 5 gegeben werden"). `ReportGradeDialogViewModel` berechnet je Schüler über
|
||||||
|
`AttendanceBalanceService.Calculate` die Fehlquote aus den `ParticipationSession`s dieser
|
||||||
|
Gruppe im gewählten Zeitraum (Halbjahr/Schuljahr) — bewusst nur diese Gruppe, anders als
|
||||||
|
`StudentDetailViewModel.LoadAttendanceBalance`, das gruppenübergreifend über den ganzen
|
||||||
|
Schüler rechnet. Anzeige als kleine Zeile unter dem Schülernamen
|
||||||
|
(`ReportGradeRow.AbsenceRateDisplay`), ab `HighAbsenceThresholdPercent = 50` rot
|
||||||
|
hervorgehoben (`AbsenceRateColorHex`) mit Tooltip-Hinweis auf die Regel — reine Information,
|
||||||
|
kein automatisches Übersteuern der berechneten Note.
|
||||||
|
|
||||||
### 2.5 Notenentwicklung
|
### 2.5 Notenentwicklung
|
||||||
- [x] **2.5.1** Verlaufsdiagramm pro Schüler über das Schuljahr (im Schülerdetail).
|
- [x] **2.5.1** Verlaufsdiagramm pro Schüler über das Schuljahr (im Schülerdetail).
|
||||||
@@ -216,6 +225,14 @@ Siehe [ParticipationViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/Participa
|
|||||||
Bewertungen), aber "Deaktivieren" ist der empfohlene Weg — ein echter Verwendungs-Check vor
|
Bewertungen), aber "Deaktivieren" ist der empfohlene Weg — ein echter Verwendungs-Check vor
|
||||||
dem Löschen (durchsucht alle `ParticipationEntry.Ratings` nach dem Key) ist nicht umgesetzt,
|
dem Löschen (durchsucht alle `ParticipationEntry.Ratings` nach dem Key) ist nicht umgesetzt,
|
||||||
da `AspectRating` nicht nach Aspekt-Key indiziert ist.
|
da `AspectRating` nicht nach Aspekt-Key indiziert ist.
|
||||||
|
- [x] **3.1.5** Abwesende Schüler im Schnellbewerten-Dialog kenntlich machen (Nutzer-Feedback:
|
||||||
|
"es macht ja keinen Sinn Schülern eine Mitarbeitsnote zu erteilen, die gar nicht da waren").
|
||||||
|
`ParticipationStudentRow.IsAbsent` (gesetzte `Attendance`, aber nicht `Present`) treibt
|
||||||
|
`QuickInputViewModel.CurrentStudentIsAbsent`/`CurrentStudentContentOpacity`: die
|
||||||
|
Aspektliste wird gedimmt, der Schülername zeigt stattdessen den konkreten
|
||||||
|
Anwesenheitsstatus ("⚠ Abwesend — Krank, unentschuldigt" o.ä.) in Orange. Bewusst kein
|
||||||
|
Blockieren der Eingabe (manche Bewertungssysteme erwarten trotzdem einen expliziten
|
||||||
|
Eintrag) — nur ein visueller Hinweis.
|
||||||
|
|
||||||
### 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 %).
|
||||||
@@ -1019,6 +1036,26 @@ dadurch faktisch schon (Quick-Input-Dialog bzw. "Offene Entschuldigungen" im Das
|
|||||||
im UI (nur im Tooltip) — wirkte dadurch wie zufällige/abwechselnde Farbgebung statt wie das
|
im UI (nur im Tooltip) — wirkte dadurch wie zufällige/abwechselnde Farbgebung statt wie das
|
||||||
eigentliche Signal "Auffälligkeit". Jetzt als kleine Legende über dem Diagramm sichtbar,
|
eigentliche Signal "Auffälligkeit". Jetzt als kleine Legende über dem Diagramm sichtbar,
|
||||||
siehe [StudentDetailView.axaml](LehrerApp.Desktop/Views/Students/StudentDetailView.axaml).
|
siehe [StudentDetailView.axaml](LehrerApp.Desktop/Views/Students/StudentDetailView.axaml).
|
||||||
|
- [x] **5.1.7** Dokumentation je Lerngruppe (Nutzer-Feedback: der "Dokumentation"-Tab der
|
||||||
|
Lerngruppe war seit seiner Anlage nur ein Platzhalter — "Wird implementiert."). Neuer
|
||||||
|
[GroupDocumentationTabView](LehrerApp.Desktop/Views/Groups/GroupDocumentationTabView.axaml)/
|
||||||
|
[GroupDocumentationTabViewModel](LehrerApp.Desktop/ViewModels/Groups/GroupDocumentationViewModels.cs)
|
||||||
|
listet die Dokumentationseinträge aller (auch ehemaligen) Schüler der Gruppe an einem Ort,
|
||||||
|
mit Schüler-Filter (Dropdown) und optionalem "Nur dieser Unterricht"-Schalter. Design-
|
||||||
|
Entscheidung nach Rückfrage: Einträge aus **anderen** Lerngruppen desselben Schülers werden
|
||||||
|
standardmäßig **mit angezeigt statt versteckt** — nur optisch gedimmt
|
||||||
|
(`DocumentationItem.ContentOpacity`) und mit `"aus: <Gruppenname>"` beschriftet — damit
|
||||||
|
Muster über mehrere Fächer/Kurse hinweg nicht verborgen bleiben; der Schalter blendet sie
|
||||||
|
bei Bedarf ganz aus. Einträge ohne Gruppenbezug (`Documentation.GroupId == null`, z.B. vom
|
||||||
|
Schüler-Tab aus angelegt) gelten als "eigen" und werden nie gedimmt.
|
||||||
|
`DocumentationDialogViewModel` bekam dafür einen optionalen Schüler-Picker
|
||||||
|
(`StudentOptions`/`SelectedStudent`, neuer `StudentOption`-Record): beim Anlegen aus dem
|
||||||
|
Gruppen-Tab (ohne festen Schüler) ist die Auswahl Pflicht (`StudentError`), neue Einträge
|
||||||
|
bekommen automatisch die `GroupId` der aktuellen Gruppe gesetzt; der bestehende Aufruf vom
|
||||||
|
Schüler-Tab (fester `studentId`, keine Optionsliste) bleibt unverändert und zeigt die
|
||||||
|
Auswahl gar nicht erst an. "Gespräch begleiten" (Elternanruf) und Anhänge funktionieren im
|
||||||
|
Gruppen-Tab identisch zum Schüler-Tab, da beide dieselbe `DocumentationItem`/
|
||||||
|
`DocumentationDialog`-Infrastruktur verwenden.
|
||||||
|
|
||||||
### 5.2 Fehlzeiten (als Auswertung des bestehenden Anwesenheits-Trackings, siehe oben)
|
### 5.2 Fehlzeiten (als Auswertung des bestehenden Anwesenheits-Trackings, siehe oben)
|
||||||
- [x] **5.2.1** Schnelle Abwesenheitserfassung je Stunde — bereits vorhanden über
|
- [x] **5.2.1** Schnelle Abwesenheitserfassung je Stunde — bereits vorhanden über
|
||||||
|
|||||||
Reference in New Issue
Block a user