diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs index c2afd80..6bc830f 100644 --- a/LehrerApp.Core/Models/LearningGroup.cs +++ b/LehrerApp.Core/Models/LearningGroup.cs @@ -14,6 +14,13 @@ public class LearningGroup public bool IsOwnClass { get; set; } public bool IsDifferentiated { get; set; } /// + /// Ich bin Klassenlehrer/-in dieser Klasse. Anders als (rein + /// kalenderfarbliche Markierung, mehrfach setzbar für Klasse + zugehörige Kurse) schaltet + /// dieses Flag Zugriff auf Daten anderer Lehrkräfte zu dieser Klasse frei (Klassenlehrer-Tab) + /// und sollte deshalb nur an genau einer Klasse-Gruppe gesetzt werden. + /// + public bool IsClassTeacher { get; set; } + /// /// Default true (alte Datensätze ohne dieses Feld verhalten sich damit unverändert wie bisher /// - LiteDB liefert den C#-Default für fehlende Felder). Für Gruppen wie Klassenrat oder /// Willkommenskreis, die keinen inhaltlichen Verlaufsplan brauchen, in den Stammdaten diff --git a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs new file mode 100644 index 0000000..4b46583 --- /dev/null +++ b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs @@ -0,0 +1,65 @@ +using LehrerApp.Desktop.Services; +using LehrerApp.Desktop.ViewModels.Groups; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +public sealed class ClassTeacherViewModelsTests +{ + [Fact] + public void GroupByStudentAndDay_FasstFehlstundenProSchuelerUndTagZusammen() + { + var entries = new[] + { + new UntisClassAbsenceEntryDto("Muster Max", 1, "6a", 20260824, 1, 45, "HED", "Che", "Absent", + "Krank", 1, "24.08.26", true, null, 3, "nicht entsch.", false), + new UntisClassAbsenceEntryDto("Muster Max", 1, "6a", 20260824, 1, 45, "PAN", "Kunst", null, + "Krank", 2, "24.08.26", true, null, 5, "nicht entsch.", true), + new UntisClassAbsenceEntryDto("Andere Schuelerin", 2, "6a", 20260824, 1, 20, "BEN", "Deu", + "Verspätung", null, 3, "24.08.26", false, "Zug verpasst", 1, "entsch.", false), + }; + + var rows = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(entries); + + Assert.Equal(2, rows.Count); + + var max = rows[1]; + Assert.Equal("Muster Max", max.StudentName); + Assert.Equal(2, max.TotalAbsentPeriods); + Assert.Equal(90, max.TotalAbsentMinutes); + Assert.Equal(["Che", "Kunst"], max.Subjects); + Assert.Equal([3, 5], max.PeriodNumbers.Order()); + Assert.Equal(["nicht entsch."], max.Statuses); + Assert.Equal(["Absent"], max.AbsenceReasons); + Assert.Equal("Krank", max.Note); + Assert.Null(max.ExcuseNote); + Assert.True(max.CountsAsFullDay); + + var andere = rows[0]; + Assert.Equal("Andere Schuelerin", andere.StudentName); + Assert.Equal(1, andere.TotalAbsentPeriods); + Assert.Equal(20, andere.TotalAbsentMinutes); + Assert.Equal(["Deu"], andere.Subjects); + Assert.Null(andere.Note); + Assert.Equal("Zug verpasst", andere.ExcuseNote); + Assert.False(andere.CountsAsFullDay); + } + + [Fact] + public void GroupByStudentAndDay_TrenntGleichenNamenAnUnterschiedlichenTagen() + { + var entries = new[] + { + new UntisClassAbsenceEntryDto("Muster Max", 1, "6a", 20260824, 1, 45, "HED", "Che", null, + null, 1, "24.08.26", true, null, 3, "nicht entsch.", false), + new UntisClassAbsenceEntryDto("Muster Max", 1, "6a", 20260825, 1, 45, "HED", "Che", null, + null, 2, "25.08.26", true, null, 3, "nicht entsch.", false), + }; + + var rows = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(entries); + + Assert.Equal(2, rows.Count); + Assert.Equal(new DateOnly(2026, 8, 25), rows[0].Date); + Assert.Equal(new DateOnly(2026, 8, 24), rows[1].Date); + } +} diff --git a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs index 6847b14..8637a6e 100644 --- a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs @@ -1,5 +1,6 @@ using LehrerApp.Core.Models; using LehrerApp.Core.Services; +using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Groups; using Xunit; @@ -29,7 +30,9 @@ public sealed class GroupDetailViewModelTests new FakeCompetencyDomains(), students, new CompetencyAnalysisService()), new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects()), - new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups)); + new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups), + new ClassTeacherTabViewModel( + new WebUntisIntegrationService(new HttpClient(), TestSupport.BuildWebUntisSettingsService()), groups)); vm.LoadGroup(group.Id); vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id); diff --git a/LehrerApp.Desktop.Tests/WebUntisIntegrationServiceTests.cs b/LehrerApp.Desktop.Tests/WebUntisIntegrationServiceTests.cs index f8875a9..5b7b7ba 100644 --- a/LehrerApp.Desktop.Tests/WebUntisIntegrationServiceTests.cs +++ b/LehrerApp.Desktop.Tests/WebUntisIntegrationServiceTests.cs @@ -30,6 +30,41 @@ public sealed class WebUntisIntegrationServiceTests Assert.Single(handler.Requests, request => request.Body.Contains("\"method\":\"authenticate\"")); } + [Fact] + public async Task GetClassAbsencesAsync_LoestKlasseNamensbasiertAufUndAdressiertSieAlsKLPlusId() + { + var csv = Encoding.UTF8.GetBytes( + "Schüler*innen\tExterne Id\tText\tKlasse\tDatum\tWochentag\tFehlstd.\tFehlmin.\tLehrkraft\tFach\t" + + "Abwesenheitsgrund\tText\tENr\tErledigt\tAbwesenheit zählt\tEntschuldigungstext\tStundennr.\tStatus\tFehltage\r\n" + + "Muster Max\t12345\t\t10c\t24.08.26\tMo.\t1\t45\tHED\tChe\tAbsent\tKrank gemeldet\t555\t24.08.26\ttrue\t\t3\tnicht entsch.\t1\r\n"); + var handler = new QueueHandler( + Json("{\"result\":{\"sessionId\":\"desktop-session\"}}"), // authenticate (ConnectAsync) + Json("{\"result\":[{\"id\":7,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"), // getSchoolyears (ConnectAsync-Validierung) + Json("{\"result\":[{\"id\":7,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"), // getSchoolyears (ResolveClassIdAsync) + Json("{\"result\":[{\"id\":874,\"name\":\"10c\"}]}"), // getKlassen + Json("{\"data\":{\"finished\":true,\"error\":false," + + "\"reportParams\":\"get=rpt.tmp&name=AbsencePerStudent&format=csv\"}}"), + new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(csv) }, + Json("{\"result\":{}}")); // logout (Dispose) + var settings = TestSupport.BuildWebUntisSettingsService(); + await using var service = new WebUntisIntegrationService(new HttpClient(handler), settings); + + await service.ConnectAsync(new WebUntisCredentials( + "bk-ostvest", "arche.webuntis.com", "lehrkraft", "geheim")); + var entries = await service.GetClassAbsencesAsync("10c", + new DateOnly(2026, 8, 24), new DateOnly(2026, 8, 28)); + + var entry = Assert.Single(entries); + Assert.Equal("Muster Max", entry.StudentName); + Assert.Equal("nicht entsch.", entry.Status); + // Zweimal getSchoolyears: einmal als ConnectAsync-Validierung, einmal in ResolveClassIdAsync. + Assert.Equal(2, handler.Requests.Count(request => request.Body.Contains("\"method\":\"getSchoolyears\""))); + Assert.Single(handler.Requests, request => request.Body.Contains("\"method\":\"getKlassen\"")); + Assert.Single(handler.Requests, request => + request.Uri.Contains("reports.do?name=AbsencePerStudent") && + request.Uri.Contains("klasseOrStudentgroupId=KL874")); + } + private static HttpResponseMessage Json(string json) => new(HttpStatusCode.OK) { Content = new StringContent(json, Encoding.UTF8, "application/json"), diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index 7cda975..d9db3a1 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -308,6 +308,7 @@ public static class AppBootstrapper services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs b/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs index ae6b444..905b23c 100644 --- a/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs +++ b/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs @@ -32,6 +32,12 @@ public sealed record UntisLessonAbsenceDto(string StudentName, int Date, int Abs string? Reason, int? ExternKey, bool ExternKeyInParentheses, string? HandledOn, bool Counts); public sealed record UntisClassRegisterEventDto(string ClassName, int Date, string? Subject, string StudentName, string? CategoryName, string? CategoryGroup, string? Text); +public sealed record UntisForeignClassRegisterEventDto(string ClassName, int Date, string? Subject, + string StudentName, string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text); +public sealed record UntisClassAbsenceEntryDto(string StudentName, int? ExternKey, string ClassName, int Date, + int AbsentPeriods, int AbsentMinutes, string? TeacherUsernames, string? Subject, string? AbsenceReason, + string? Note, int? EntryId, string? HandledOn, bool Counts, string? ExcuseNote, int? PeriodNumber, + string? Status, bool CountsAsFullDay); /// Direkter WebUntis-Client des Desktops. Personenbezogene Antworten und der /// unverschlüsselte CSV-Report passieren zu keinem Zeitpunkt den LehrerApp-Server. @@ -135,6 +141,65 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings .ToList(); }, token); + // Spiegelbild von GetOwnClassRegisterEventsAsync für das "Klassenlehrer"-Feature (siehe TODO.md): + // dieselben "-alle-"-Rohdaten, aber Einträge anderer Lehrkräfte statt der eigenen, zusätzlich auf + // die übergebene Klasse eingeschränkt (der "-alle-"-Bericht liefert sonst auch andere Klassen mit, + // sobald eine Lehrkraft mehrere Klassen unterrichtet). + public Task> GetForeignClassRegisterEventsAsync( + string className, DateOnly start, DateOnly end, CancellationToken token = default) => ExecuteAsync(async client => + { + var ownUsername = settings.GetApiCredentials()?.Username; + var entries = await client.GetClassRegisterEventsReportAsync(Date(start), Date(end), token); + return (IReadOnlyList)entries + .Where(x => string.Equals(x.ClassName, className, StringComparison.OrdinalIgnoreCase) + // Nicht einfach "!=" gegen ownUsername: wäre ownUsername unbekannt (null), würde + // ein ebenfalls fehlender TeacherUsername sonst fälschlich als "eigen" erkannt + // (Equals(null,null)==true) und rausgefiltert statt als fremd angezeigt zu werden. + && !(ownUsername is not null + && string.Equals(x.TeacherUsername, ownUsername, StringComparison.OrdinalIgnoreCase))) + .Select(x => new UntisForeignClassRegisterEventDto(x.ClassName, x.Date, x.Subject, x.StudentName, + x.TeacherUsername, x.CategoryName, x.CategoryGroup, x.Text)) + .ToList(); + }, token); + + // Fehlzeiten der ganzen Klasse über alle Fächer, Ergänzung zu GetForeignClassRegisterEventsAsync + // für das "Klassenlehrer"-Feature. Der Bericht adressiert die Klasse über "KL"+getKlassen-ID statt + // eines Namens, siehe ResolveClassIdAsync. + public Task> GetClassAbsencesAsync(string className, + DateOnly start, DateOnly end, CancellationToken token = default) => ExecuteAsync(async client => + { + var classId = await ResolveClassIdAsync(client, className, token); + var entries = await client.GetClassAbsencesAsync(classId, Date(start), Date(end), token); + return (IReadOnlyList)entries.Select(x => new UntisClassAbsenceEntryDto( + x.StudentName, x.ExternKey, x.ClassName, x.Date, x.AbsentPeriods, x.AbsentMinutes, + x.TeacherUsernames, x.Subject, x.AbsenceReason, x.Note, x.EntryId, x.HandledOn, x.Counts, + x.ExcuseNote, x.PeriodNumber, x.Status, x.CountsAsFullDay)) + .ToList(); + }, token); + + // "KL" + getKlassen-ID, dasselbe Element-Kürzel-Schema wie kl/te/su/ro bei anderen Berichten + // (siehe Entities(...) unten). Unbestätigt (kein offizieller Report-Parameter dokumentiert), aber + // aus einem echten Browser-Request rückgeschlossen - schlägt die Zuordnung fehl, liefert der + // Bericht erkennbar leere/falsche Daten statt eines stillen Fehlers. + private static async Task ResolveClassIdAsync(WebUntisClient client, string className, + CancellationToken token) + { + var years = await client.GetSchoolYearsAsync(token); + var today = Date(DateOnly.FromDateTime(DateTime.Today)); + var year = years.FirstOrDefault(y => y.StartDate <= today && y.EndDate >= today) + ?? years.OrderByDescending(y => y.StartDate).FirstOrDefault() + ?? throw new WebUntisIntegrationException("WebUntis hat kein Schuljahr geliefert."); + var classes = await client.GetClassesAsync(year.UntisId, token); + var matches = classes.Where(c => string.Equals(c.Name, className, StringComparison.OrdinalIgnoreCase)).ToList(); + return matches.Count switch + { + 1 => $"KL{matches[0].UntisId}", + 0 => throw new WebUntisIntegrationException($"WebUntis kennt keine Klasse namens \"{className}\"."), + _ => throw new WebUntisIntegrationException( + $"Mehrere WebUntis-Klassen heißen \"{className}\" - bitte manuell prüfen."), + }; + } + private async Task ExecuteAsync(Func> operation, CancellationToken token) { try { return await operation(await GetClientAsync(token)); } diff --git a/LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs new file mode 100644 index 0000000..0434592 --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs @@ -0,0 +1,133 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LehrerApp.Core.Interfaces; +using LehrerApp.Desktop.Services; + +namespace LehrerApp.Desktop.ViewModels.Groups; + +/// Eine Zeile aus dem "-alle-"-Klassenbuchbericht, die eine andere Lehrkraft angelegt hat. +/// Rein zur Ansicht - anders als beim Dokumentations-Abgleich (eigene Einträge) gibt es hier kein +/// "Übernehmen", da es keine eigenen Einträge zum lokalen Synchronisieren sind. +public sealed record ClassTeacherClassRegisterRow(DateOnly Date, string? Subject, string StudentName, + string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text) +{ + public string DateLabel => Date.ToString("dd.MM.yyyy"); +} + +/// Fehlzeiten eines/einer Schüler*in an einem Tag, über alle Fächer zusammengefasst - +/// der WebUntis-Bericht liefert eine Zeile pro Fehlstunde, was bei einer ganzen Klasse über mehrere +/// Wochen zu unübersichtlich wäre (Nutzer-Feedback: kompakte "auf einen Blick"-Übersicht statt +/// Rohdaten). Die Rohzeilen () bleiben trotzdem erreichbar, +/// falls später ein Detail-Drill-down pro Schüler*in dazukommt (siehe TODO.md). +public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName, int? ExternKey, + int TotalAbsentPeriods, int TotalAbsentMinutes, IReadOnlyList Subjects, + IReadOnlyList PeriodNumbers, IReadOnlyList Statuses, IReadOnlyList AbsenceReasons, + string? Note, string? ExcuseNote, bool CountsAsFullDay) +{ + public string DateLabel => Date.ToString("dd.MM.yyyy"); + public string SubjectsLabel => string.Join(", ", Subjects); + public string PeriodsLabel => string.Join(", ", PeriodNumbers.Order()); + public string StatusLabel => string.Join(", ", Statuses); + public string ReasonLabel => string.Join("; ", AbsenceReasons); + + public static IReadOnlyList GroupByStudentAndDay( + IEnumerable entries) => entries + .Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null)) + .Where(x => x.Date is not null) + .GroupBy(x => (Date: x.Date!.Value, x.Entry.StudentName, x.Entry.ExternKey)) + .Select(g => new ClassAbsenceDaySummaryRow( + g.Key.Date, g.Key.StudentName, g.Key.ExternKey, + g.Sum(x => x.Entry.AbsentPeriods), g.Sum(x => x.Entry.AbsentMinutes), + Distinct(g.Select(x => x.Entry.Subject)), + g.Select(x => x.Entry.PeriodNumber).Where(p => p is not null).Select(p => p!.Value).Distinct().ToList(), + Distinct(g.Select(x => x.Entry.Status)), + Distinct(g.Select(x => x.Entry.AbsenceReason)), + g.Select(x => x.Entry.Note).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)), + g.Select(x => x.Entry.ExcuseNote).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)), + g.Any(x => x.Entry.CountsAsFullDay))) + .OrderByDescending(r => r.Date).ThenBy(r => r.StudentName) + .ToList(); + + private static List Distinct(IEnumerable values) => + values.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v!).Distinct().ToList(); + + private static bool TryDate(int value, out DateOnly date) => + DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date); +} + +/// +/// Klassenlehrer-Tab: zeigt Klassenbucheinträge, die andere Lehrkräfte zu Schülern dieser Klasse +/// angelegt haben (WebUntis "-alle-"-Bericht, gefiltert auf fremde statt der eigenen +/// Benutzerkennung wie beim bestehenden Dokumentations-Abgleich, siehe +/// ), sowie die +/// Fehlzeiten der Klasse über alle Fächer (). +/// Beides rein zur Ansicht, kein Übernehmen in lokale Daten. Nur für Gruppen mit +/// sichtbar (siehe +/// GroupDetailViewModel/GroupDetailView). +/// +public partial class ClassTeacherTabViewModel : ObservableObject +{ + private readonly WebUntisIntegrationService _untis; + private readonly IGroupRepository _groups; + private string _className = ""; + + public ObservableCollection Entries { get; } = []; + public ObservableCollection AbsenceEntries { get; } = []; + + [ObservableProperty] private DateTimeOffset _startDate = DateTimeOffset.Now.AddDays(-7); + [ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now; + [ObservableProperty] private string _status = "Zeitraum wählen und laden."; + [ObservableProperty] private bool _busy; + + public ClassTeacherTabViewModel(WebUntisIntegrationService untis, IGroupRepository groups) + { + _untis = untis; _groups = groups; + } + + public void Initialize(Guid groupId) + { + _className = _groups.GetById(groupId)?.Name ?? ""; + Entries.Clear(); + AbsenceEntries.Clear(); + Status = "Zeitraum wählen und laden."; + } + + [RelayCommand] + private async Task Load() + { + var start = DateOnly.FromDateTime(StartDate.LocalDateTime); + var end = DateOnly.FromDateTime(EndDate.LocalDateTime); + if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; } + if (string.IsNullOrWhiteSpace(_className)) { Status = "Dieser Gruppe fehlt ein Klassenname."; return; } + + Busy = true; Entries.Clear(); AbsenceEntries.Clear(); + try + { + var classRegisterTask = _untis.GetForeignClassRegisterEventsAsync(_className, start, end); + var absencesTask = _untis.GetClassAbsencesAsync(_className, start, end); + await Task.WhenAll(classRegisterTask, absencesTask); + + var ordered = classRegisterTask.Result + .Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null)) + .Where(x => x.Date is not null) + .OrderByDescending(x => x.Date).ThenBy(x => x.Entry.StudentName); + foreach (var (entry, date) in ordered) + { + Entries.Add(new ClassTeacherClassRegisterRow(date!.Value, entry.Subject, entry.StudentName, + entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text)); + } + + foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result)) + AbsenceEntries.Add(row); + + Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " + + $"{AbsenceEntries.Count} Fehlzeiten-Tage im Zeitraum."; + } + catch (WebUntisIntegrationException ex) { Status = ex.Message; } + finally { Busy = false; } + } + + private static bool TryDate(int value, out DateOnly date) => + DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date); +} diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index 265dad6..3b2c599 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -251,6 +251,7 @@ public partial class GroupDetailViewModel : ObservableObject // bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann // kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen. public bool IsDifferentiated => Group?.IsDifferentiated ?? false; + public bool IsClassTeacher => Group?.IsClassTeacher ?? false; public bool IsReadOnly => Group is { IsActive: false }; public bool IsEditable => Group is { IsActive: true }; public string SubjectName { get; private set; } = ""; @@ -258,6 +259,7 @@ public partial class GroupDetailViewModel : ObservableObject partial void OnGroupChanged(LearningGroup? value) { OnPropertyChanged(nameof(IsDifferentiated)); + OnPropertyChanged(nameof(IsClassTeacher)); OnPropertyChanged(nameof(IsReadOnly)); OnPropertyChanged(nameof(IsEditable)); NotifyWriteCommands(); @@ -279,6 +281,7 @@ public partial class GroupDetailViewModel : ObservableObject public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; } public SeatingPlanTabViewModel SeatingPlanTab { get; } public GroupDocumentationTabViewModel GroupDocumentationTab { get; } + public ClassTeacherTabViewModel ClassTeacherTab { get; } public Func>? OnAddStudent { get; set; } public Func>? OnWithdrawStudent { get; set; } public Func>? OnAddExam { get; set; } @@ -295,7 +298,8 @@ public partial class GroupDetailViewModel : ObservableObject GroupOverviewViewModel overviewTab, ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab, PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab, - SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab) + SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab, + ClassTeacherTabViewModel classTeacherTab) { _groups = groups; _students = students; _memberships = memberships; _subjects = subjects; _exams = exams; _grades = grades; _tasks = tasks; @@ -306,6 +310,7 @@ public partial class GroupDetailViewModel : ObservableObject CompetencyOverviewTab = competencyOverviewTab; SeatingPlanTab = seatingPlanTab; GroupDocumentationTab = groupDocumentationTab; + ClassTeacherTab = classTeacherTab; SeatingPlanTab.OnAssessmentChanged = () => { ParticipationTab.LoadSessions(); @@ -335,6 +340,7 @@ public partial class GroupDetailViewModel : ObservableObject CompetencyOverviewTab.Initialize(Group); SeatingPlanTab.Initialize(Group.Id, IsReadOnly); GroupDocumentationTab.Initialize(Group.Id); + ClassTeacherTab.Initialize(Group.Id); } private void ReloadExams() @@ -877,6 +883,7 @@ public partial class AddGroupDialogViewModel : ObservableObject [ObservableProperty] private int? _hoursPerWeek; [ObservableProperty] private bool _isOwnClass; [ObservableProperty] private bool _isDifferentiated; + [ObservableProperty] private bool _isClassTeacher; [ObservableProperty] private bool _requiresLessonPlanning = true; [ObservableProperty] private int? _webUntisLessonId; [ObservableProperty] private string _nameError = ""; @@ -917,6 +924,7 @@ public partial class AddGroupDialogViewModel : ObservableObject HoursPerWeek = group.HoursPerWeek; IsOwnClass = group.IsOwnClass; IsDifferentiated = group.IsDifferentiated; + IsClassTeacher = group.IsClassTeacher; RequiresLessonPlanning = group.RequiresLessonPlanning; WebUntisLessonId = group.WebUntisLessonId; OnPropertyChanged(nameof(DialogTitle)); @@ -964,6 +972,7 @@ public partial class AddGroupDialogViewModel : ObservableObject Result.HoursPerWeek = HoursPerWeek; Result.IsOwnClass = IsOwnClass; Result.IsDifferentiated = IsDifferentiated; + Result.IsClassTeacher = IsClassTeacher; Result.RequiresLessonPlanning = RequiresLessonPlanning; Result.WebUntisLessonId = WebUntisLessonId; _groups.Save(Result); diff --git a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml index fd63de9..d4070c7 100644 --- a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml @@ -71,6 +71,9 @@ + + diff --git a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml b/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml new file mode 100644 index 0000000..10245ab --- /dev/null +++ b/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml @@ -0,0 +1,84 @@ + + + + + + + + + +