CI / build-and-test (push) Canceled after 0s
Neue Menü-Kategorie "Formulare" in der Menüleiste: "Elternbrief erzeugen..." öffnet jetzt einen Schüler-Picker statt nur aus der Schülerdetailansicht erreichbar zu sein, "Arbeitsblatt personalisieren..." ist aktiv, sobald eine einzelne Lerngruppe geöffnet ist, und erzeugt aus einer in TemplateDesigner gebauten .lavorlage-Vorlage ein PDF je aktivem Gruppenmitglied - über eine von den Elternbrief-Vorlagen getrennte WorksheetTemplateStore-Bibliothek. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
257 lines
11 KiB
C#
257 lines
11 KiB
C#
using Avalonia.Threading;
|
|
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.Exams;
|
|
using LehrerApp.Desktop.ViewModels.Groups;
|
|
using LehrerApp.Desktop.ViewModels.Planning;
|
|
using LehrerApp.Desktop.ViewModels.Settings;
|
|
using LehrerApp.Desktop.ViewModels.Students;
|
|
using LehrerApp.Desktop.ViewModels.Workload;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels;
|
|
|
|
public partial class MainWindowViewModel : ObservableObject
|
|
{
|
|
private readonly IServiceProvider _services;
|
|
|
|
[ObservableProperty] private ObservableObject? _currentPage;
|
|
[ObservableProperty] private NavItem _activeNavItem = NavItem.Dashboard;
|
|
[ObservableProperty] private string _currentSchoolYear = "";
|
|
[ObservableProperty] private bool _isCommandPaletteOpen;
|
|
|
|
public SyncStatusViewModel SyncStatus { get; }
|
|
public ObservableCollection<ToastItem> Toasts { get; }
|
|
public AppLockViewModel AppLock { get; }
|
|
public GlobalSearchViewModel CommandPalette { get; }
|
|
|
|
public bool IsDashboardActive => ActiveNavItem == NavItem.Dashboard;
|
|
public bool IsGroupsActive => ActiveNavItem == NavItem.Groups;
|
|
public bool IsStudentsActive => ActiveNavItem == NavItem.Students;
|
|
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 GroupDetailViewModel? CurrentGroupDetail => CurrentPage as GroupDetailViewModel;
|
|
public bool CanPersonalizeWorksheet => CurrentGroupDetail?.Group is not null;
|
|
|
|
public MainWindowViewModel(IServiceProvider services,
|
|
DashboardViewModel dashboard, SchoolYearService sy,
|
|
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock,
|
|
GlobalSearchViewModel commandPalette)
|
|
{
|
|
_services = services;
|
|
SyncStatus = syncStatus;
|
|
Toasts = notifications.Toasts;
|
|
AppLock = appLock;
|
|
CommandPalette = commandPalette;
|
|
CommandPalette.OnClose = CloseCommandPalette;
|
|
CurrentSchoolYear = sy.CurrentSchoolYear();
|
|
CurrentPage = dashboard;
|
|
AppLock.ApplyConfig();
|
|
SyncStatus.DataChanged += OnSyncDataChanged;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenCommandPalette()
|
|
{
|
|
if (AppLock.IsLocked) return;
|
|
CommandPalette.Reset();
|
|
IsCommandPaletteOpen = true;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void CloseCommandPalette() => IsCommandPaletteOpen = false;
|
|
|
|
// EventApplier schreibt bei eingehenden Sync-Ereignissen absichtlich direkt auf die rohe
|
|
// LiteDB-Collection, an jedem ViewModel vorbei (Ping-Pong-Vermeidung, siehe EventApplier-
|
|
// Klassenkommentar) - ohne diesen Hook blieb die gerade sichtbare Seite bis zum nächsten
|
|
// manuellen Neuladen (z.B. Tab-Wechsel) auf dem alten Stand (TODO 10.1.11). Lädt bewusst nur
|
|
// die aktuell sichtbare Seite über ihren eigenen, längst vorhandenen Lade-Einstieg neu - keine
|
|
// neue Lade-Logik, keine Navigation weg von einer offenen Detailansicht. SyncEngine.DataChanged
|
|
// feuert aus dem Sync-Timer-Hintergrundthread - daher der Dispatcher-Sprung zurück auf den
|
|
// UI-Thread (gleiches Muster wie NotificationService.Show).
|
|
private void OnSyncDataChanged()
|
|
{
|
|
if (Dispatcher.UIThread.CheckAccess()) RefreshCurrentPage();
|
|
else Dispatcher.UIThread.Post(RefreshCurrentPage);
|
|
}
|
|
|
|
private void RefreshCurrentPage()
|
|
{
|
|
switch (CurrentPage)
|
|
{
|
|
case DashboardViewModel vm: vm.RefreshCommand.Execute(null); break;
|
|
case GroupListViewModel vm: vm.RefreshCommand.Execute(null); break;
|
|
case StudentListViewModel vm: vm.LoadStudents(); break;
|
|
case TimetableViewModel vm: vm.Load(); break;
|
|
case WorkloadViewModel vm:
|
|
vm.Tasks.Load(); vm.TimeTracking.Load(); vm.Evaluation.Load(); break;
|
|
// RefreshFromLocalDataOnly() statt LoadCommand: baut die Ansicht aus den zuletzt
|
|
// geholten WebUntis-Daten + aktuellen lokalen Daten neu auf, löst aber selbst nie einen
|
|
// WebUntis-Request aus - den holt bewusst ausschließlich der "Aktualisieren"-Button.
|
|
case ClassTeacherOverviewViewModel vm: vm.RefreshFromLocalDataOnly(); break;
|
|
case ExamsOverviewViewModel 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.
|
|
case StudentDetailViewModel { Student: { } student, IsEditing: false } vm:
|
|
vm.LoadStudent(student.Id); break;
|
|
}
|
|
}
|
|
|
|
// GroupDetailViewModel.Group wird erst nach dem Wechsel von CurrentPage per LoadGroup gesetzt
|
|
// (siehe Kommentar in NavigateToGroupDetail) - ohne dieses Abonnement bliebe
|
|
// CanPersonalizeWorksheet bis zum nächsten Seitenwechsel auf dem alten Stand.
|
|
private GroupDetailViewModel? _observedGroupDetail;
|
|
|
|
partial void OnCurrentPageChanged(ObservableObject? value)
|
|
{
|
|
if (_observedGroupDetail is not null) _observedGroupDetail.PropertyChanged -= OnGroupDetailPropertyChanged;
|
|
_observedGroupDetail = value as GroupDetailViewModel;
|
|
if (_observedGroupDetail is not null) _observedGroupDetail.PropertyChanged += OnGroupDetailPropertyChanged;
|
|
OnPropertyChanged(nameof(CurrentGroupDetail));
|
|
OnPropertyChanged(nameof(CanPersonalizeWorksheet));
|
|
}
|
|
|
|
private void OnGroupDetailPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(GroupDetailViewModel.Group)) OnPropertyChanged(nameof(CanPersonalizeWorksheet));
|
|
}
|
|
|
|
partial void OnActiveNavItemChanged(NavItem value)
|
|
{
|
|
OnPropertyChanged(nameof(IsDashboardActive));
|
|
OnPropertyChanged(nameof(IsGroupsActive));
|
|
OnPropertyChanged(nameof(IsStudentsActive));
|
|
OnPropertyChanged(nameof(IsExamsActive));
|
|
OnPropertyChanged(nameof(IsPlannerActive));
|
|
OnPropertyChanged(nameof(IsWorkloadActive));
|
|
OnPropertyChanged(nameof(IsClassTeacherActive));
|
|
OnPropertyChanged(nameof(IsSettingsActive));
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void NavigateTo(NavItem item)
|
|
{
|
|
ActiveNavItem = item;
|
|
CurrentPage = item switch
|
|
{
|
|
NavItem.Dashboard => GetDashboard(),
|
|
NavItem.Groups => _services.GetRequiredService<GroupListViewModel>(),
|
|
NavItem.Students => GetStudents(),
|
|
NavItem.Exams => GetExams(),
|
|
NavItem.Planner => GetTimetable(),
|
|
NavItem.Workload => GetWorkload(),
|
|
NavItem.ClassTeacher => GetClassTeacherOverview(),
|
|
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
|
_ => CurrentPage,
|
|
};
|
|
}
|
|
|
|
private DashboardViewModel GetDashboard()
|
|
{
|
|
var dashboard = _services.GetRequiredService<DashboardViewModel>();
|
|
dashboard.RefreshCommand.Execute(null);
|
|
return dashboard;
|
|
}
|
|
|
|
private StudentListViewModel GetStudents()
|
|
{
|
|
var students = _services.GetRequiredService<StudentListViewModel>();
|
|
students.SelectedStudent = null;
|
|
students.LoadStudents();
|
|
return students;
|
|
}
|
|
|
|
private TimetableViewModel GetTimetable()
|
|
{
|
|
// WeekOffset bewusst nicht zurücksetzen: TimetableViewModel ist ein DI-Singleton, die
|
|
// zuletzt angezeigte Woche bleibt also automatisch über Tab-Wechsel hinweg erhalten
|
|
// (Nutzer-Feedback: beim Planen der nächsten Woche nervt es, nach jedem kurzen Abstecher
|
|
// in einen anderen Bereich wieder manuell dorthin zurückblättern zu müssen).
|
|
var timetable = _services.GetRequiredService<TimetableViewModel>();
|
|
timetable.Load();
|
|
timetable.ActiveTabIndex = 0;
|
|
return timetable;
|
|
}
|
|
|
|
private WorkloadViewModel GetWorkload()
|
|
{
|
|
var workload = _services.GetRequiredService<WorkloadViewModel>();
|
|
workload.Tasks.Load();
|
|
workload.TimeTracking.Load();
|
|
workload.Evaluation.Load();
|
|
workload.ActiveTabIndex = 0;
|
|
return workload;
|
|
}
|
|
|
|
private ExamsOverviewViewModel GetExams()
|
|
{
|
|
var exams = _services.GetRequiredService<ExamsOverviewViewModel>();
|
|
exams.LoadCommand.Execute(null);
|
|
return exams;
|
|
}
|
|
|
|
private ClassTeacherOverviewViewModel GetClassTeacherOverview()
|
|
{
|
|
var vm = _services.GetRequiredService<ClassTeacherOverviewViewModel>();
|
|
vm.OnNavigateToSettings = () => { NavigateToSettings(SettingsTab.WebUntis); return Task.CompletedTask; };
|
|
vm.OnNavigateToWorkload = () => { NavigateToWorkload(); return Task.CompletedTask; };
|
|
// Kein automatischer WebUntis-Abruf beim Navigieren: PrepareForDisplay() baut nur den
|
|
// lokalen Grundzustand auf, die eigentlichen Report-Daten holt die Nutzerin bewusst über
|
|
// den "Aktualisieren"-Button (LoadCommand) - vermeidet WebUntis-Login+Report-Latenz beim
|
|
// bloßen Öffnen der Seite.
|
|
vm.PrepareForDisplay();
|
|
return vm;
|
|
}
|
|
|
|
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
|
{
|
|
ActiveNavItem = NavItem.Groups;
|
|
var vm = _services.GetRequiredService<GroupDetailViewModel>();
|
|
vm.ActiveTabIndex = initialTab;
|
|
CurrentPage = vm; // View bindet zuerst → Callbacks werden registriert
|
|
vm.LoadGroup(groupId); // dann Daten laden
|
|
}
|
|
|
|
public void NavigateToSettings(SettingsTab initialTab = SettingsTab.Subjects)
|
|
{
|
|
ActiveNavItem = NavItem.Settings;
|
|
var vm = _services.GetRequiredService<SettingsViewModel>();
|
|
vm.ActiveTabIndex = (int)initialTab;
|
|
CurrentPage = vm;
|
|
}
|
|
|
|
public void NavigateToStudent(Guid studentId)
|
|
{
|
|
ActiveNavItem = NavItem.Students;
|
|
var vm = _services.GetRequiredService<StudentDetailViewModel>();
|
|
vm.OnReturnToStudentList = NavigateToStudents;
|
|
vm.LoadStudent(studentId);
|
|
CurrentPage = vm;
|
|
}
|
|
|
|
public void NavigateToStudents() => NavigateTo(NavItem.Students);
|
|
|
|
/// Sprungziel aus dem Kurs-Dashboard ("Anstehende Aufgaben für diese Klasse") in den
|
|
/// Aufgaben-Bereich — anders als NavigateToGroupDetail/NavigateToSettings kein Tab innerhalb
|
|
/// einer Detailansicht, sondern ein eigener Top-Level-Bereich (NavItem.Workload).
|
|
public void NavigateToWorkload() => NavigateTo(NavItem.Workload);
|
|
}
|
|
|
|
public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, ClassTeacher, Settings }
|
|
|
|
public partial class PlaceholderViewModel : ObservableObject
|
|
{
|
|
[ObservableProperty] private string _title = "";
|
|
[ObservableProperty] private string _icon = "";
|
|
}
|