diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs index 6bc830f..c2afd80 100644 --- a/LehrerApp.Core/Models/LearningGroup.cs +++ b/LehrerApp.Core/Models/LearningGroup.cs @@ -14,13 +14,6 @@ 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 index 4b46583..b3505bd 100644 --- a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs +++ b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs @@ -1,5 +1,5 @@ using LehrerApp.Desktop.Services; -using LehrerApp.Desktop.ViewModels.Groups; +using LehrerApp.Desktop.ViewModels.ClassTeacher; using Xunit; namespace LehrerApp.Desktop.Tests; @@ -62,4 +62,48 @@ public sealed class ClassTeacherViewModelsTests Assert.Equal(new DateOnly(2026, 8, 25), rows[0].Date); Assert.Equal(new DateOnly(2026, 8, 24), rows[1].Date); } + + [Fact] + public void RosterBuild_OrdnetFehlzeitPerExternKeyUndKlassenbuchPerNameZu() + { + var students = new[] + { + Student(1001, "Ada Müller"), + Student(1002, "Ben Schmidt"), + Student(1003, "Cem Yilmaz"), + }; + var todayAbsences = new[] + { + new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 25), "Ada Müller", 1001, 2, 90, + ["Che"], [3, 4], ["nicht entsch."], ["Absent"], null, null, false), + }; + var recentClassRegister = new[] + { + new UntisForeignClassRegisterEventDto("6a", 20260824, "Deu", "Ben Schmidt", "mueller", + "Vergessen", "Organisation", "Buch vergessen"), + }; + + var roster = ClassTeacherRosterRow.Build(students, todayAbsences, recentClassRegister); + + Assert.Equal(3, roster.Count); + var ada = Assert.Single(roster, r => r.StudentName == "Ada Müller"); + Assert.True(ada.HasAbsenceToday); + Assert.Contains("nicht entsch.", ada.AbsenceTooltip); + Assert.False(ada.HasRecentClassRegisterEntry); + + var ben = Assert.Single(roster, r => r.StudentName == "Ben Schmidt"); + Assert.False(ben.HasAbsenceToday); + Assert.True(ben.HasRecentClassRegisterEntry); + + var cem = Assert.Single(roster, r => r.StudentName == "Cem Yilmaz"); + Assert.False(cem.HasAbsenceToday); + Assert.False(cem.HasRecentClassRegisterEntry); + } + + private static UntisStudentDto Student(int externKey, string displayName) => new( + UntisId: externKey, ExternKey: externKey, ClassName: "6a", Name: displayName, LongName: null, + ForeName: null, DisplayName: displayName, Gender: null, BirthDate: null, BirthDateRaw: null, + EntryDate: null, EntryDateRaw: null, ExitDate: null, ExitDateRaw: null, Text: null, + MedicalReportDuty: null, Schulpflicht: null, Majority: null, + Address: new UntisStudentAddressDto(null, null, null, null, null, null), AttributeIL: null); } diff --git a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs index 8637a6e..6847b14 100644 --- a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs @@ -1,6 +1,5 @@ using LehrerApp.Core.Models; using LehrerApp.Core.Services; -using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Groups; using Xunit; @@ -30,9 +29,7 @@ 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 ClassTeacherTabViewModel( - new WebUntisIntegrationService(new HttpClient(), TestSupport.BuildWebUntisSettingsService()), groups)); + new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups)); vm.LoadGroup(group.Id); vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id); diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index d9db3a1..91b181f 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -5,6 +5,7 @@ using LehrerApp.Data; using LehrerApp.Data.Repositories; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels; +using LehrerApp.Desktop.ViewModels.ClassTeacher; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Desktop.ViewModels.Settings; @@ -297,6 +298,8 @@ public static class AppBootstrapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Transient: neue Instanz pro Navigation (für Detailseiten) services.AddTransient(); @@ -308,7 +311,6 @@ public static class AppBootstrapper services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/LehrerApp.Desktop/Services/WebUntisSettingsService.cs b/LehrerApp.Desktop/Services/WebUntisSettingsService.cs index 1af427e..b9eebe5 100644 --- a/LehrerApp.Desktop/Services/WebUntisSettingsService.cs +++ b/LehrerApp.Desktop/Services/WebUntisSettingsService.cs @@ -11,6 +11,8 @@ internal class WebUntisSettingsConfig public string LastSyncStatus { get; set; } = ""; public string? EncryptedApiCredentials { get; set; } public int? TeacherUntisId { get; set; } + public int? HomeroomClassUntisId { get; set; } + public string? HomeroomClassName { get; set; } } public sealed record WebUntisCredentials(string School, string Host, string Username, string Password); @@ -36,6 +38,8 @@ public class WebUntisSettingsService public bool IsConfigured => _config.EncryptedIcalUrl is not null; public bool ApiIsConfigured => _config.EncryptedApiCredentials is not null; public int? TeacherUntisId => _config.TeacherUntisId; + public int? HomeroomClassUntisId => _config.HomeroomClassUntisId; + public string? HomeroomClassName => _config.HomeroomClassName; public DateTime? LastSyncAt => _config.LastSyncAt; public string LastSyncStatus => _config.LastSyncStatus; @@ -83,6 +87,8 @@ public class WebUntisSettingsService { _config.EncryptedApiCredentials = null; _config.TeacherUntisId = null; + _config.HomeroomClassUntisId = null; + _config.HomeroomClassName = null; Save(); } @@ -92,6 +98,16 @@ public class WebUntisSettingsService Save(); } + // Die Klasse, deren Klassenlehrer/-in man ist - unabhängig von jeder LearningGroup (siehe + // TODO.md "Klassenlehrer"-Feature): man ist es für die ganze Klasse, nicht für einen einzelnen + // Unterricht/Kurs. HomeroomClassName ist wie TeacherUntisId unverschlüsselt, da kein Geheimnis. + public void SetHomeroomClass(int? untisId, string? name) + { + _config.HomeroomClassUntisId = untisId; + _config.HomeroomClassName = name; + Save(); + } + public void SetLastSync(DateTime at, string status) { _config.LastSyncAt = at; diff --git a/LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs similarity index 76% rename from LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs rename to LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs index 0434592..aff4767 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs @@ -1,10 +1,9 @@ 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; +namespace LehrerApp.Desktop.ViewModels.ClassTeacher; /// 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 @@ -57,19 +56,19 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName } /// -/// 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 +/// Details-Ansicht des Klassenlehrer-Bereichs: Klassenbucheinträge, die andere Lehrkräfte zu +/// Schülern der 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). +/// Beides rein zur Ansicht, kein Übernehmen in lokale Daten. Bewusst unabhängig von jeder +/// — Klassenlehrer ist man für die ganze Klasse, nicht für +/// einen einzelnen Unterricht; die Klasse kommt aus +/// (letztlich aus den WebUntis-Einstellungen), nicht aus einer Gruppen-ID. /// -public partial class ClassTeacherTabViewModel : ObservableObject +public partial class ClassTeacherDetailsViewModel : ObservableObject { private readonly WebUntisIntegrationService _untis; - private readonly IGroupRepository _groups; private string _className = ""; public ObservableCollection Entries { get; } = []; @@ -79,15 +78,17 @@ public partial class ClassTeacherTabViewModel : ObservableObject [ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now; [ObservableProperty] private string _status = "Zeitraum wählen und laden."; [ObservableProperty] private bool _busy; + /// Von der Übersicht gesetzt (Klick auf eine Roster-Zeile) - leer zeigt alle Schüler*innen. + [ObservableProperty] private string _studentFilter = ""; - public ClassTeacherTabViewModel(WebUntisIntegrationService untis, IGroupRepository groups) + public ClassTeacherDetailsViewModel(WebUntisIntegrationService untis) { - _untis = untis; _groups = groups; + _untis = untis; } - public void Initialize(Guid groupId) + public void Initialize(string className) { - _className = _groups.GetById(groupId)?.Name ?? ""; + _className = className; Entries.Clear(); AbsenceEntries.Clear(); Status = "Zeitraum wählen und laden."; @@ -99,7 +100,7 @@ public partial class ClassTeacherTabViewModel : ObservableObject 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; } + if (string.IsNullOrWhiteSpace(_className)) { Status = "Keine Klasse ausgewählt."; return; } Busy = true; Entries.Clear(); AbsenceEntries.Clear(); try @@ -109,6 +110,7 @@ public partial class ClassTeacherTabViewModel : ObservableObject await Task.WhenAll(classRegisterTask, absencesTask); var ordered = classRegisterTask.Result + .Where(MatchesStudentFilter) .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); @@ -118,7 +120,8 @@ public partial class ClassTeacherTabViewModel : ObservableObject entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text)); } - foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result)) + var absences = absencesTask.Result.Where(MatchesStudentFilter); + foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences)) AbsenceEntries.Add(row); Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " + @@ -128,6 +131,14 @@ public partial class ClassTeacherTabViewModel : ObservableObject finally { Busy = false; } } + private bool MatchesStudentFilter(UntisForeignClassRegisterEventDto entry) => + string.IsNullOrWhiteSpace(StudentFilter) || + string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase); + + private bool MatchesStudentFilter(UntisClassAbsenceEntryDto entry) => + string.IsNullOrWhiteSpace(StudentFilter) || + string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase); + private static bool TryDate(int value, out DateOnly date) => DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date); } diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs new file mode 100644 index 0000000..048f1d8 --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs @@ -0,0 +1,135 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LehrerApp.Desktop.Services; + +namespace LehrerApp.Desktop.ViewModels.ClassTeacher; + +/// Eine Zeile der Klassenlehrer-Übersicht ("auf einen Blick", Nutzer-Feedback) - pro +/// Schüler*in ein Ampel-Symbol für "heute laut WebUntis keine gemeldete Fehlzeit" und ein Badge, +/// falls in den letzten Tagen ein Klassenbucheintrag einer anderen Lehrkraft angelegt wurde. +/// Bewusst kein Bezug zu eigenem Unterricht/eigener Anwesenheitserfassung - genau der Fall, den es +/// abzudecken gilt, ist der Tag ohne eigenen Unterricht mit der Klasse. +public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday, + string? AbsenceTooltip, bool HasRecentClassRegisterEntry) +{ + public string StatusIcon => HasAbsenceToday ? "⚠" : "✓"; + + /// Führt die WebUntis-Schülerliste der Klasse mit den heutigen Fehlzeiten und den + /// jüngsten Klassenbucheinträgen zusammen. Zuordnung primär über ExternKey (beide + /// stammen aus WebUntis, gleiche Kennung), Fallback auf Namensgleichheit für den + /// Klassenbuch-Bericht, der keinen ExternKey liefert (siehe UntisForeignClassRegisterEventDto). + public static IReadOnlyList Build( + IReadOnlyList students, + IReadOnlyList todayAbsences, + IReadOnlyList recentClassRegisterEntries) + { + var absenceByKey = todayAbsences + .Where(a => a.ExternKey is not null) + .GroupBy(a => a.ExternKey!.Value) + .ToDictionary(g => g.Key, g => g.First()); + var absenceByName = todayAbsences + .GroupBy(a => a.StudentName, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); + var recentNames = recentClassRegisterEntries + .Select(e => e.StudentName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + return students + .Select(s => + { + var absence = absenceByKey.GetValueOrDefault(s.ExternKey) + ?? absenceByName.GetValueOrDefault(s.DisplayName); + return new ClassTeacherRosterRow(s.DisplayName, s.ExternKey, absence is not null, + absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}", + recentNames.Contains(s.DisplayName)); + }) + .OrderBy(r => r.StudentName) + .ToList(); + } +} + +/// +/// Top-Level-Bereich "Klassenlehrer" (eigener Sidebar-Eintrag, siehe MainWindowViewModel) - bewusst +/// nicht an eine LearningGroup gehängt, da man Klassenlehrer für eine ganze Klasse ist, unabhängig +/// vom eigenen Unterricht (Nutzer-Feedback zur ursprünglichen Umsetzung als Gruppen-Tab). Die +/// Klasse selbst kommt aus den WebUntis-Einstellungen (). +/// Zwei Ebenen: Übersicht (dieses ViewModel, kompaktes Roster) und Details +/// (, die bisherigen Rohdaten-Listen) - ein Klick auf +/// eine Roster-Zeile springt gefiltert in die Details. +/// +public partial class ClassTeacherOverviewViewModel : ObservableObject +{ + private readonly WebUntisSettingsService _settings; + private readonly WebUntisIntegrationService _untis; + + public ClassTeacherDetailsViewModel DetailsTab { get; } + public ObservableCollection Roster { get; } = []; + + [ObservableProperty] private string? _homeroomClassName; + [ObservableProperty] private int _activeTabIndex; + [ObservableProperty] private string _status = ""; + [ObservableProperty] private bool _busy; + + public bool HomeroomClassConfigured => !string.IsNullOrWhiteSpace(HomeroomClassName); + + /// Vom Code-Behind/MainWindowViewModel gesetzt: springt in die WebUntis-Einstellungen, wenn noch + /// keine Klasse gewählt ist. + public Func? OnNavigateToSettings { get; set; } + + public ClassTeacherOverviewViewModel(WebUntisSettingsService settings, WebUntisIntegrationService untis, + ClassTeacherDetailsViewModel detailsTab) + { + _settings = settings; _untis = untis; DetailsTab = detailsTab; + } + + partial void OnHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(HomeroomClassConfigured)); + + [RelayCommand] + private async Task Load() + { + HomeroomClassName = _settings.HomeroomClassName; + ActiveTabIndex = 0; + Roster.Clear(); + if (!HomeroomClassConfigured) + { + Status = "Noch keine Klasse ausgewählt."; + return; + } + + var className = HomeroomClassName!; + DetailsTab.Initialize(className); + Busy = true; + try + { + var today = DateOnly.FromDateTime(DateTime.Today); + var studentsTask = _untis.GetStudentsAsync(className); + var absencesTask = _untis.GetClassAbsencesAsync(className, today, today); + var classRegisterTask = _untis.GetForeignClassRegisterEventsAsync(className, today.AddDays(-7), today); + await Task.WhenAll(studentsTask, absencesTask, classRegisterTask); + + var todayAbsences = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result); + foreach (var row in ClassTeacherRosterRow.Build(studentsTask.Result.Students, todayAbsences, + classRegisterTask.Result)) + Roster.Add(row); + Status = $"{Roster.Count} Schüler*innen · Stand heute."; + } + catch (WebUntisIntegrationException ex) { Status = ex.Message; } + finally { Busy = false; } + } + + [RelayCommand] + private void ShowDetailsForStudent(ClassTeacherRosterRow? row) + { + if (row is null) return; + DetailsTab.StudentFilter = row.StudentName; + ActiveTabIndex = 1; + DetailsTab.LoadCommand.Execute(null); + } + + [RelayCommand] + private async Task GoToSettings() + { + if (OnNavigateToSettings is not null) await OnNavigateToSettings(); + } +} diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index 3b2c599..265dad6 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -251,7 +251,6 @@ 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; } = ""; @@ -259,7 +258,6 @@ public partial class GroupDetailViewModel : ObservableObject partial void OnGroupChanged(LearningGroup? value) { OnPropertyChanged(nameof(IsDifferentiated)); - OnPropertyChanged(nameof(IsClassTeacher)); OnPropertyChanged(nameof(IsReadOnly)); OnPropertyChanged(nameof(IsEditable)); NotifyWriteCommands(); @@ -281,7 +279,6 @@ 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; } @@ -298,8 +295,7 @@ public partial class GroupDetailViewModel : ObservableObject GroupOverviewViewModel overviewTab, ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab, PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab, - SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab, - ClassTeacherTabViewModel classTeacherTab) + SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab) { _groups = groups; _students = students; _memberships = memberships; _subjects = subjects; _exams = exams; _grades = grades; _tasks = tasks; @@ -310,7 +306,6 @@ public partial class GroupDetailViewModel : ObservableObject CompetencyOverviewTab = competencyOverviewTab; SeatingPlanTab = seatingPlanTab; GroupDocumentationTab = groupDocumentationTab; - ClassTeacherTab = classTeacherTab; SeatingPlanTab.OnAssessmentChanged = () => { ParticipationTab.LoadSessions(); @@ -340,7 +335,6 @@ public partial class GroupDetailViewModel : ObservableObject CompetencyOverviewTab.Initialize(Group); SeatingPlanTab.Initialize(Group.Id, IsReadOnly); GroupDocumentationTab.Initialize(Group.Id); - ClassTeacherTab.Initialize(Group.Id); } private void ReloadExams() @@ -883,7 +877,6 @@ 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 = ""; @@ -924,7 +917,6 @@ 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)); @@ -972,7 +964,6 @@ 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/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index 3007bfa..2b57169 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Services; using LehrerApp.Desktop.Services; +using LehrerApp.Desktop.ViewModels.ClassTeacher; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Desktop.ViewModels.Settings; @@ -31,6 +32,7 @@ public partial class MainWindowViewModel : ObservableObject public bool IsExamsActive => ActiveNavItem == NavItem.Exams; public bool IsPlannerActive => ActiveNavItem == NavItem.Planner; public bool IsWorkloadActive => ActiveNavItem == NavItem.Workload; + public bool IsClassTeacherActive => ActiveNavItem == NavItem.ClassTeacher; public bool IsSettingsActive => ActiveNavItem == NavItem.Settings; public MainWindowViewModel(IServiceProvider services, @@ -71,6 +73,7 @@ public partial class MainWindowViewModel : ObservableObject case TimetableViewModel vm: vm.Load(); break; case WorkloadViewModel vm: vm.Tasks.Load(); vm.TimeTracking.Load(); vm.Evaluation.Load(); break; + case ClassTeacherOverviewViewModel vm: vm.LoadCommand.Execute(null); break; case GroupDetailViewModel { Group: { } group } vm: vm.LoadGroup(group.Id); break; // Inline-Bearbeitung (IsEditing) nicht überschreiben - anders als die Gruppenansicht // laufen Namens-/Geschlechtsänderungen hier nicht über einen Dialog. @@ -87,6 +90,7 @@ public partial class MainWindowViewModel : ObservableObject OnPropertyChanged(nameof(IsExamsActive)); OnPropertyChanged(nameof(IsPlannerActive)); OnPropertyChanged(nameof(IsWorkloadActive)); + OnPropertyChanged(nameof(IsClassTeacherActive)); OnPropertyChanged(nameof(IsSettingsActive)); } @@ -102,6 +106,7 @@ public partial class MainWindowViewModel : ObservableObject NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" }, NavItem.Planner => GetTimetable(), NavItem.Workload => GetWorkload(), + NavItem.ClassTeacher => GetClassTeacherOverview(), NavItem.Settings => _services.GetRequiredService(), _ => CurrentPage, }; @@ -141,6 +146,14 @@ public partial class MainWindowViewModel : ObservableObject return workload; } + private ClassTeacherOverviewViewModel GetClassTeacherOverview() + { + var vm = _services.GetRequiredService(); + vm.OnNavigateToSettings = () => { NavigateToSettings(SettingsTab.WebUntis); return Task.CompletedTask; }; + vm.LoadCommand.Execute(null); + return vm; + } + public void NavigateToGroupDetail(Guid groupId, int initialTab = 0) { ActiveNavItem = NavItem.Groups; @@ -175,7 +188,7 @@ public partial class MainWindowViewModel : ObservableObject public void NavigateToWorkload() => NavigateTo(NavItem.Workload); } -public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings } +public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, ClassTeacher, Settings } public partial class PlaceholderViewModel : ObservableObject { diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs index fb23578..8d77b88 100644 --- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs @@ -217,7 +217,14 @@ public partial class SettingsViewModel : ObservableObject [ObservableProperty] private string _untisPassword = ""; [ObservableProperty] private string _untisApiStatus = ""; [ObservableProperty] private bool _untisApiBusy; + [ObservableProperty] private string? _untisHomeroomClassName; + public string UntisHomeroomClassDisplay => UntisHomeroomClassName is { Length: > 0 } name + ? $"Ausgewählt: {name}" + : "Keine Klasse ausgewählt."; + partial void OnUntisHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(UntisHomeroomClassDisplay)); public Func? OnReviewUntisMapping { get; set; } + /// Vom Code-Behind gesetzt: öffnet den WebUntis-Klassenauswahldialog, liefert null bei Abbruch. + public Func>? OnPickHomeroomClass { get; set; } // ── Schulweiter Jahresplan (ClassyPlan-iCal) ───────────────────────────── @@ -559,6 +566,7 @@ public partial class SettingsViewModel : ObservableObject UntisUsername = credentials.Username; UntisApiStatus = $"API-Zugang für {credentials.Username} ist lokal verschlüsselt gespeichert."; } + UntisHomeroomClassName = _untisSettings.HomeroomClassName; } [RelayCommand] @@ -647,6 +655,23 @@ public partial class SettingsViewModel : ObservableObject if (OnReviewUntisMapping is not null) await OnReviewUntisMapping(); } + [RelayCommand] + private async Task UntisPickHomeroomClass() + { + if (OnPickHomeroomClass is null) return; + var result = await OnPickHomeroomClass(); + if (result is null) return; + _untisSettings.SetHomeroomClass(result.Value.UntisId, result.Value.Name); + LoadUntisSettings(); + } + + [RelayCommand] + private void UntisClearHomeroomClass() + { + _untisSettings.SetHomeroomClass(null, null); + LoadUntisSettings(); + } + // ── Schulweiter Jahresplan: Laden / Speichern / Entfernen / Jetzt abrufen ─ private void LoadAnnualPlanSettings() diff --git a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml similarity index 82% rename from LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml rename to LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml index 10245ab..b992d2d 100644 --- a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml +++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml @@ -1,10 +1,10 @@ + xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher" + x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherDetailsView" + x:DataType="vm:ClassTeacherDetailsViewModel"> - + @@ -15,8 +15,14 @@ + + + + + + + + + + + + + + diff --git a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml.cs b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml.cs new file mode 100644 index 0000000..4de2158 --- /dev/null +++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace LehrerApp.Desktop.Views.ClassTeacher; + +public partial class ClassTeacherOverviewView : UserControl +{ + public ClassTeacherOverviewView() => InitializeComponent(); +} diff --git a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml index d4070c7..fd63de9 100644 --- a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml @@ -71,9 +71,6 @@ - - diff --git a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml.cs b/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml.cs deleted file mode 100644 index dfbf55c..0000000 --- a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Avalonia.Controls; - -namespace LehrerApp.Desktop.Views.Groups; - -public partial class ClassTeacherTabView : UserControl -{ - public ClassTeacherTabView() => InitializeComponent(); -} diff --git a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml index dd9b41c..085241a 100644 --- a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml +++ b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml @@ -205,11 +205,6 @@ - - - - - diff --git a/LehrerApp.Desktop/Views/MainWindow.axaml b/LehrerApp.Desktop/Views/MainWindow.axaml index 88d42fe..e1fc74d 100644 --- a/LehrerApp.Desktop/Views/MainWindow.axaml +++ b/LehrerApp.Desktop/Views/MainWindow.axaml @@ -13,6 +13,8 @@ xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning" xmlns:vw="clr-namespace:LehrerApp.Desktop.Views.Workload" xmlns:vmw="clr-namespace:LehrerApp.Desktop.ViewModels.Workload" + xmlns:vct="clr-namespace:LehrerApp.Desktop.Views.ClassTeacher" + xmlns:vmct="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher" xmlns:svc="clr-namespace:LehrerApp.Desktop.Services" x:Class="LehrerApp.Desktop.Views.MainWindow" x:DataType="vm:MainWindowViewModel" @@ -58,6 +60,9 @@ + + + @@ -209,6 +214,16 @@ +