Review der Codebase

This commit is contained in:
2026-08-13 19:57:48 +02:00
parent d987b93315
commit 27359794f7
9 changed files with 269 additions and 80 deletions
+24
View File
@@ -102,6 +102,24 @@ public sealed class RepositoryTests
var aspectId = Guid.NewGuid(); var aspectId = Guid.NewGuid();
db.ParticipationAspects.Insert(new ParticipationAspect { Id = aspectId, GroupId = groupId, Key = "quality" }); db.ParticipationAspects.Insert(new ParticipationAspect { Id = aspectId, GroupId = groupId, Key = "quality" });
var reportGradeId = Guid.NewGuid();
db.ReportGrades.Insert(new ReportGrade { Id = reportGradeId, GroupId = groupId, StudentId = studentId });
var gradingSchemeId = Guid.NewGuid();
db.GradingSchemes.Insert(new GradingScheme { Id = gradingSchemeId, GroupId = groupId });
var sectionId = Guid.NewGuid();
db.ParticipationSections.Insert(new ParticipationSection { Id = sectionId, GroupId = groupId });
var documentationId = Guid.NewGuid();
db.Documentation.Insert(new Documentation { Id = documentationId, GroupId = groupId, StudentId = studentId });
var taskId = Guid.NewGuid();
db.Tasks.Insert(new WorkTask { Id = taskId, GroupId = groupId });
var timeEntryId = Guid.NewGuid();
db.TimeEntries.Insert(new TimeEntry { Id = timeEntryId, GroupId = groupId, TaskId = taskId });
groupRepo.Delete(groupId); groupRepo.Delete(groupId);
Assert.Null(db.Groups.FindById(groupId)); Assert.Null(db.Groups.FindById(groupId));
@@ -115,6 +133,12 @@ public sealed class RepositoryTests
Assert.Null(db.ParticipationSessions.FindById(sessionId)); Assert.Null(db.ParticipationSessions.FindById(sessionId));
Assert.Null(db.ParticipationEntries.FindById(entryId)); Assert.Null(db.ParticipationEntries.FindById(entryId));
Assert.Null(db.ParticipationAspects.FindById(aspectId)); Assert.Null(db.ParticipationAspects.FindById(aspectId));
Assert.Null(db.ReportGrades.FindById(reportGradeId));
Assert.Null(db.GradingSchemes.FindById(gradingSchemeId));
Assert.Null(db.ParticipationSections.FindById(sectionId));
Assert.Null(db.Documentation.FindById(documentationId)?.GroupId);
Assert.Null(db.Tasks.FindById(taskId)?.GroupId);
Assert.Null(db.TimeEntries.FindById(timeEntryId)?.GroupId);
} }
[Fact] [Fact]
+15
View File
@@ -60,6 +60,21 @@ public class LiteDbContext : IDisposable
public int SchemaVersion => ReadSchemaVersion(); public int SchemaVersion => ReadSchemaVersion();
internal void ExecuteInTransaction(Action action)
{
_db.BeginTrans();
try
{
action();
_db.Commit();
}
catch
{
_db.Rollback();
throw;
}
}
private void RunVersionedMigrations() private void RunVersionedMigrations()
{ {
var version = ReadSchemaVersion(); var version = ReadSchemaVersion();
@@ -39,6 +39,8 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
db.Groups.Upsert(g); db.Groups.Upsert(g);
} }
public void Delete(Guid id) public void Delete(Guid id)
{
db.ExecuteInTransaction(() =>
{ {
foreach (var membership in db.Memberships.Find(e => e.GroupId == id).ToList()) foreach (var membership in db.Memberships.Find(e => e.GroupId == id).ToList())
db.Memberships.Delete(membership.Id); db.Memberships.Delete(membership.Id);
@@ -53,6 +55,12 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
foreach (var grade in db.Grades.Find(g => g.GroupId == id).ToList()) foreach (var grade in db.Grades.Find(g => g.GroupId == id).ToList())
db.Grades.Delete(grade.Id); db.Grades.Delete(grade.Id);
foreach (var reportGrade in db.ReportGrades.Find(g => g.GroupId == id).ToList())
db.ReportGrades.Delete(reportGrade.Id);
foreach (var scheme in db.GradingSchemes.Find(s => s.GroupId == id).ToList())
db.GradingSchemes.Delete(scheme.Id);
foreach (var unit in db.Units.Find(u => u.GroupId == id).ToList()) foreach (var unit in db.Units.Find(u => u.GroupId == id).ToList())
{ {
foreach (var lesson in db.Lessons.Find(l => l.UnitId == unit.Id).ToList()) foreach (var lesson in db.Lessons.Find(l => l.UnitId == unit.Id).ToList())
@@ -73,7 +81,33 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
foreach (var aspect in db.ParticipationAspects.Find(a => a.GroupId == id).ToList()) foreach (var aspect in db.ParticipationAspects.Find(a => a.GroupId == id).ToList())
db.ParticipationAspects.Delete(aspect.Id); db.ParticipationAspects.Delete(aspect.Id);
foreach (var section in db.ParticipationSections.Find(s => s.GroupId == id).ToList())
db.ParticipationSections.Delete(section.Id);
// Dokumentation und Arbeitszeit sind historische Nachweise. Sie bleiben erhalten,
// werden aber von der nicht mehr existierenden Lerngruppe entkoppelt.
foreach (var documentation in db.Documentation.Find(d => d.GroupId == id).ToList())
{
documentation.GroupId = null;
documentation.UpdatedAt = DateTime.UtcNow;
db.Documentation.Update(documentation);
}
foreach (var task in db.Tasks.Find(t => t.GroupId == id).ToList())
{
task.GroupId = null;
task.UpdatedAt = DateTime.UtcNow;
db.Tasks.Update(task);
}
foreach (var timeEntry in db.TimeEntries.Find(t => t.GroupId == id).ToList())
{
timeEntry.GroupId = null;
db.TimeEntries.Update(timeEntry);
}
db.Groups.Delete(id); db.Groups.Delete(id);
});
} }
} }
+34 -5
View File
@@ -15,6 +15,8 @@ namespace LehrerApp.Desktop;
public class App : Application public class App : Application
{ {
public static IServiceProvider Services { get; private set; } = null!; public static IServiceProvider Services { get; private set; } = null!;
private static ServiceProvider? _serviceProvider;
private static bool _exitHandlerAttached;
public override void Initialize() => AvaloniaXamlLoader.Load(this); public override void Initialize() => AvaloniaXamlLoader.Load(this);
@@ -32,31 +34,58 @@ public class App : Application
promptVm.OnUnlocked = password => promptVm.OnUnlocked = password =>
{ {
AppBootstrapper.DbPassword = password; AppBootstrapper.DbPassword = password;
StartMainApp(desktop); StartMainApp(desktop, showImmediately: true);
promptWindow.Close(); promptWindow.Close();
}; };
desktop.MainWindow = promptWindow; desktop.MainWindow = promptWindow;
} }
else else
{ {
StartMainApp(desktop); StartMainApp(desktop, showImmediately: false);
} }
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
private static void StartMainApp(IClassicDesktopStyleApplicationLifetime desktop) private static void StartMainApp(
IClassicDesktopStyleApplicationLifetime desktop, bool showImmediately)
{ {
Services = AppBootstrapper.BuildServices(); _serviceProvider = AppBootstrapper.BuildServices();
Services = _serviceProvider;
Services.GetRequiredService<AppLogger>().Info("Anwendung gestartet."); Services.GetRequiredService<AppLogger>().Info("Anwendung gestartet.");
GlobalExceptionHandler.AttachNotifications(Services.GetRequiredService<NotificationService>()); GlobalExceptionHandler.AttachNotifications(Services.GetRequiredService<NotificationService>());
if (!_exitHandlerAttached)
{
desktop.Exit += (_, _) => DisposeServices();
_exitHandlerAttached = true;
}
var mainVm = Services.GetRequiredService<MainWindowViewModel>(); var mainVm = Services.GetRequiredService<MainWindowViewModel>();
WireCallbacks(mainVm); WireCallbacks(mainVm);
var main = new MainWindow { DataContext = mainVm }; var main = new MainWindow { DataContext = mainVm };
desktop.MainWindow = main; desktop.MainWindow = main;
main.Show(); if (showImmediately) main.Show();
}
private static void DisposeServices()
{
if (_serviceProvider is null) return;
try
{
_serviceProvider.GetService<LiteDbContext>()?.Checkpoint();
}
catch (Exception ex)
{
AppBootstrapper.Logger.Error("Datenbank-Checkpoint beim Beenden fehlgeschlagen.", ex);
}
finally
{
_serviceProvider.Dispose();
_serviceProvider = null;
}
} }
private static void WireCallbacks(MainWindowViewModel main) private static void WireCallbacks(MainWindowViewModel main)
+1 -1
View File
@@ -76,7 +76,7 @@ public static class AppBootstrapper
Environment.Exit(0); Environment.Exit(0);
} }
public static IServiceProvider BuildServices() public static ServiceProvider BuildServices()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
@@ -7,10 +7,10 @@ namespace LehrerApp.Desktop.Services;
/// Zentrale Exception-Behandlung (13.2.1): protokolliert jeden unerwarteten Fehler und zeigt /// Zentrale Exception-Behandlung (13.2.1): protokolliert jeden unerwarteten Fehler und zeigt
/// eine verständliche Meldung statt eines rohen Absturzes. /// eine verständliche Meldung statt eines rohen Absturzes.
/// ///
/// Reichweite ehrlich betrachtet: <see cref="Dispatcher"/>.UIThread.UnhandledException kann /// <see cref="Dispatcher"/>.UIThread.UnhandledException fängt Fehler aus Befehlen und
/// Ausnahmen aus Befehlen/Ereignis-Handlern auf dem UI-Thread abfangen und die App am Leben /// Ereignis-Handlern ab. Nur erwartbar wiederherstellbare I/O-, Netzwerk-, Timeout- und
/// erhalten (Handled = true) — das deckt den weit überwiegenden Teil realer Abstürze in einer /// Abbruchfehler werden behandelt; unbekannte Zustandsfehler dürfen die App beenden.
/// Desktop-App ab. AppDomain.UnhandledException und TaskScheduler.UnobservedTaskException sind /// AppDomain.UnhandledException und TaskScheduler.UnobservedTaskException sind
/// Sicherheitsnetze für Fehler außerhalb des UI-Threads; die App kann eine "IsTerminating"- /// Sicherheitsnetze für Fehler außerhalb des UI-Threads; die App kann eine "IsTerminating"-
/// Ausnahme dort nicht mehr verhindern, aber wenigstens vollständig protokollieren, bevor sie endet. /// Ausnahme dort nicht mehr verhindern, aber wenigstens vollständig protokollieren, bevor sie endet.
/// </summary> /// </summary>
@@ -36,10 +36,18 @@ public static class GlobalExceptionHandler
private static void OnDispatcherUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e) private static void OnDispatcherUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
{ {
_logger?.Error("Unbehandelter Fehler auf dem UI-Thread.", e.Exception); _logger?.Error("Unbehandelter Fehler auf dem UI-Thread.", e.Exception);
_notifications?.ShowError("Es ist ein unerwarteter Fehler aufgetreten. Details wurden protokolliert."); e.Handled = IsRecoverable(e.Exception);
e.Handled = true; // App am Leben halten statt abzustürzen. if (e.Handled)
_notifications?.ShowError("Der Vorgang ist fehlgeschlagen. Details wurden protokolliert.");
} }
private static bool IsRecoverable(Exception exception) => exception is
IOException or
UnauthorizedAccessException or
HttpRequestException or
TimeoutException or
OperationCanceledException;
private static void OnAppDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) private static void OnAppDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
_logger?.Error( _logger?.Error(
@@ -22,6 +22,14 @@ public partial class MainWindowViewModel : ObservableObject
public ObservableCollection<ToastItem> Toasts { get; } public ObservableCollection<ToastItem> Toasts { get; }
public AppLockViewModel AppLock { get; } public AppLockViewModel AppLock { 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 IsSettingsActive => ActiveNavItem == NavItem.Settings;
public MainWindowViewModel(IServiceProvider services, public MainWindowViewModel(IServiceProvider services,
DashboardViewModel dashboard, SchoolYearService sy, DashboardViewModel dashboard, SchoolYearService sy,
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock) SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock)
@@ -35,6 +43,17 @@ public partial class MainWindowViewModel : ObservableObject
AppLock.ApplyConfig(); AppLock.ApplyConfig();
} }
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(IsSettingsActive));
}
[RelayCommand] [RelayCommand]
private void NavigateTo(NavItem item) private void NavigateTo(NavItem item)
{ {
+57 -37
View File
@@ -60,6 +60,15 @@
überschreiben kann lokale Werte hätten immer Vorrang vor Style-Settern. --> überschreiben kann lokale Werte hätten immer Vorrang vor Style-Settern. -->
<Style Selector="TextBlock.navicon"> <Style Selector="TextBlock.navicon">
<Setter Property="FontSize" Value="15"/> <Setter Property="FontSize" Value="15"/>
<Setter Property="Width" Value="24"/>
</Style>
<Style Selector="Button.navitem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="10,8"/>
</Style>
<Style Selector="Button.navitem.active">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
<Setter Property="Foreground" Value="White"/>
</Style> </Style>
<!-- Minimierter Zustand: Icons größer & zentriert, Beschriftungen ausgeblendet --> <!-- Minimierter Zustand: Icons größer & zentriert, Beschriftungen ausgeblendet -->
<Style Selector="DockPanel.compact TextBlock.navlabel"> <Style Selector="DockPanel.compact TextBlock.navlabel">
@@ -67,9 +76,20 @@
</Style> </Style>
<Style Selector="DockPanel.compact TextBlock.navicon"> <Style Selector="DockPanel.compact TextBlock.navicon">
<Setter Property="FontSize" Value="20"/> <Setter Property="FontSize" Value="20"/>
<Setter Property="Width" Value="28"/>
</Style> </Style>
<Style Selector="DockPanel.compact Button.navitem"> <Style Selector="DockPanel.compact Button.navitem">
<Setter Property="HorizontalContentAlignment" Value="Center"/> <Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="2,8"/>
</Style>
<Style Selector="DockPanel.compact StackPanel.navitems">
<Setter Property="Margin" Value="2,12,2,0"/>
</Style>
<Style Selector="DockPanel.compact StackPanel.navcontent">
<Setter Property="Spacing" Value="0"/>
</Style>
<Style Selector="DockPanel.compact Border.drawerheader">
<Setter Property="Padding" Value="8,20,8,14"/>
</Style> </Style>
<Style Selector="DockPanel.compact TextBlock.navsection"> <Style Selector="DockPanel.compact TextBlock.navsection">
<Setter Property="IsVisible" Value="False"/> <Setter Property="IsVisible" Value="False"/>
@@ -82,7 +102,7 @@
</Style> </Style>
</DockPanel.Styles> </DockPanel.Styles>
<Border DockPanel.Dock="Top" Padding="16,20,16,14" <Border Classes="drawerheader" DockPanel.Dock="Top" Padding="16,20,16,14"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
@@ -106,14 +126,14 @@
</Border> </Border>
<ScrollViewer> <ScrollViewer>
<StackPanel Margin="8,12,8,0" Spacing="2"> <StackPanel Classes="navitems" Margin="8,12,8,0" Spacing="2">
<Button Classes="navitem" HorizontalAlignment="Stretch" <Button Classes="navitem" Classes.active="{Binding IsDashboardActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent" HorizontalContentAlignment="Left"
Padding="10,8" CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Dashboard}"> CommandParameter="{x:Static vm:NavItem.Dashboard}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📊" Width="20" TextAlignment="Center"/> <TextBlock Classes="navicon" Text="📊" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Dashboard"/> <TextBlock Classes="navlabel" Text="Dashboard"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -121,43 +141,43 @@
<TextBlock Classes="navsection" Text="UNTERRICHT" FontSize="10" FontWeight="Bold" <TextBlock Classes="navsection" Text="UNTERRICHT" FontSize="10" FontWeight="Bold"
Opacity="0.4" Margin="10,14,0,4"/> Opacity="0.4" Margin="10,14,0,4"/>
<Button Classes="navitem" HorizontalAlignment="Stretch" <Button Classes="navitem" Classes.active="{Binding IsGroupsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent" HorizontalContentAlignment="Left"
Padding="10,8" CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Groups}"> CommandParameter="{x:Static vm:NavItem.Groups}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="🏫" Width="20" TextAlignment="Center"/> <TextBlock Classes="navicon" Text="🏫" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Lerngruppen"/> <TextBlock Classes="navlabel" Text="Lerngruppen"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Classes="navitem" HorizontalAlignment="Stretch" <Button Classes="navitem" Classes.active="{Binding IsStudentsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent" HorizontalContentAlignment="Left"
Padding="10,8" CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Students}"> CommandParameter="{x:Static vm:NavItem.Students}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="👤" Width="20" TextAlignment="Center"/> <TextBlock Classes="navicon" Text="👤" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Schüler"/> <TextBlock Classes="navlabel" Text="Schüler"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Classes="navitem" HorizontalAlignment="Stretch" <Button Classes="navitem" Classes.active="{Binding IsExamsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent" HorizontalContentAlignment="Left"
Padding="10,8" CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Exams}"> CommandParameter="{x:Static vm:NavItem.Exams}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📝" Width="20" TextAlignment="Center"/> <TextBlock Classes="navicon" Text="📝" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Klausuren"/> <TextBlock Classes="navlabel" Text="Klausuren"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Classes="navitem" HorizontalAlignment="Stretch" <Button Classes="navitem" Classes.active="{Binding IsPlannerActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent" HorizontalContentAlignment="Left"
Padding="10,8" CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Planner}"> CommandParameter="{x:Static vm:NavItem.Planner}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📅" Width="20" TextAlignment="Center"/> <TextBlock Classes="navicon" Text="📅" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Planung"/> <TextBlock Classes="navlabel" Text="Planung"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -165,23 +185,23 @@
<TextBlock Classes="navsection" Text="VERWALTUNG" FontSize="10" FontWeight="Bold" <TextBlock Classes="navsection" Text="VERWALTUNG" FontSize="10" FontWeight="Bold"
Opacity="0.4" Margin="10,14,0,4"/> Opacity="0.4" Margin="10,14,0,4"/>
<Button Classes="navitem" HorizontalAlignment="Stretch" <Button Classes="navitem" Classes.active="{Binding IsWorkloadActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent" HorizontalContentAlignment="Left"
Padding="10,8" CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Workload}"> CommandParameter="{x:Static vm:NavItem.Workload}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⏱" Width="20" TextAlignment="Center"/> <TextBlock Classes="navicon" Text="⏱" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Arbeitszeit"/> <TextBlock Classes="navlabel" Text="Arbeitszeit"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Classes="navitem" HorizontalAlignment="Stretch" <Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent" HorizontalContentAlignment="Left"
Padding="10,8" CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Settings}"> CommandParameter="{x:Static vm:NavItem.Settings}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⚙️" Width="20" TextAlignment="Center"/> <TextBlock Classes="navicon" Text="⚙️" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Einstellungen"/> <TextBlock Classes="navlabel" Text="Einstellungen"/>
</StackPanel> </StackPanel>
</Button> </Button>
+45 -5
View File
@@ -415,7 +415,10 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
### 7.2 Gruppen ### 7.2 Gruppen
- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/ - [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/
`ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit, `ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit,
beim Review aber bestätigt. beim Review aber bestätigt. Die Löschung läuft transaktional und entfernt alle fachlich
abhängigen Datensätze (inkl. Zeugnisnoten, gruppenspezifischem Notenschema und
Mitarbeitsabschnitten). Dokumentation, Aufgaben und Zeiteinträge bleiben als historische
Nachweise erhalten; ihr `GroupId` wird auf `null` gesetzt.
- [ ] **7.2.2** Gruppe ins neue Schuljahr übernehmen: Kopie mit gleicher Schülerschaft, - [ ] **7.2.2** Gruppe ins neue Schuljahr übernehmen: Kopie mit gleicher Schülerschaft,
neues `SchoolYear`, neue `GroupMembership`-Einträge. neues `SchoolYear`, neue `GroupMembership`-Einträge.
- [ ] **7.2.3** Schüler aus einer Gruppe entfernen (`GroupMembership.LeftAt` setzen statt löschen). - [ ] **7.2.3** Schüler aus einer Gruppe entfernen (`GroupMembership.LeftAt` setzen statt löschen).
@@ -487,6 +490,8 @@ ist aber nur aktiv, wenn eine Server-URL konfiguriert ist.
- [ ] **10.1.3** Konfliktanzeige in der UI — was `ConflictResolver` entscheidet, muss sichtbar sein. - [ ] **10.1.3** Konfliktanzeige in der UI — was `ConflictResolver` entscheidet, muss sichtbar sein.
- [ ] **10.1.4** Manuelles Auslösen einer vollständigen Synchronisation. - [ ] **10.1.4** Manuelles Auslösen einer vollständigen Synchronisation.
- [ ] **10.1.5** Statusanzeige erweitern: letzter Sync, Anzahl wartender Events, Fehlerzustand. - [ ] **10.1.5** Statusanzeige erweitern: letzter Sync, Anzahl wartender Events, Fehlerzustand.
- [ ] **10.1.6** Lokale Schreibvorgänge atomar an die Outbox (`EventQueue`) anbinden — aktuell ist
das Sync-Grundgerüst registriert, die Repositories erzeugen aber noch keine Sync-Ereignisse.
### 10.2 Server ### 10.2 Server
- [ ] **10.2.1** Benutzerverwaltung/Registrierung prüfen und absichern - [ ] **10.2.1** Benutzerverwaltung/Registrierung prüfen und absichern
@@ -579,10 +584,11 @@ Fächer- und Kompetenzverwaltung existiert bereits in
### 13.2 Fehlerbehandlung & Logging ### 13.2 Fehlerbehandlung & Logging
- [x] **13.2.1** Zentrale Exception-Behandlung mit verständlicher Fehlermeldung statt Absturz — - [x] **13.2.1** Zentrale Exception-Behandlung mit verständlicher Fehlermeldung statt Absturz —
[GlobalExceptionHandler.cs](LehrerApp.Desktop/Services/GlobalExceptionHandler.cs). [GlobalExceptionHandler.cs](LehrerApp.Desktop/Services/GlobalExceptionHandler.cs).
`Dispatcher.UIThread.UnhandledException` fängt Fehler aus Befehlen/Ereignis-Handlern auf `Dispatcher.UIThread.UnhandledException` protokolliert Fehler aus Befehlen/Ereignis-Handlern.
dem UI-Thread ab, protokolliert sie und setzt `Handled = true` — die App stürzt dabei Nur als wiederherstellbar eingestufte I/O-, Netzwerk-, Timeout- und Abbruchfehler werden mit
nachweislich nicht ab (per Headless-Test verifiziert: Fehler in einem Dispatcher-Callback `Handled = true` behandelt und als Toast angezeigt. Unbekannte Programmier-/Zustandsfehler
wird geloggt + als Toast angezeigt, App läuft weiter). `AppDomain.UnhandledException` und dürfen die Anwendung kontrolliert beenden, statt mit möglicherweise beschädigtem Zustand
weiterzulaufen. `AppDomain.UnhandledException` und
`TaskScheduler.UnobservedTaskException` sind Sicherheitsnetze für Fehler außerhalb des `TaskScheduler.UnobservedTaskException` sind Sicherheitsnetze für Fehler außerhalb des
UI-Threads — dort kann ein bereits "IsTerminating"-Fehler nicht mehr verhindert werden, UI-Threads — dort kann ein bereits "IsTerminating"-Fehler nicht mehr verhindert werden,
wird aber vollständig protokolliert. wird aber vollständig protokolliert.
@@ -664,6 +670,10 @@ Fächer- und Kompetenzverwaltung existiert bereits in
zeigt `ProcessPath` auf den `dotnet`-Host statt auf die App, ein Neustart über die zeigt `ProcessPath` auf den `dotnet`-Host statt auf die App, ein Neustart über die
Einstellungen ist dort also nur in einer veröffentlichten Build (`dotnet publish`) Einstellungen ist dort also nur in einer veröffentlichten Build (`dotnet publish`)
sinnvoll zu testen. sinnvoll zu testen.
- [x] **13.3.6** Sauberer Desktop-Lifecycle — beim Beenden wird ein LiteDB-Checkpoint ausgeführt
und anschließend der DI-Container samt Singleton-Datenbankverbindung entsorgt. Das initiale
Hauptfenster wird vom Avalonia-Desktop-Lifetime angezeigt; ein explizites `Show()` erfolgt
nur beim späteren Wechsel vom Passwortfenster zur Hauptansicht.
### 13.4 Codepflege ### 13.4 Codepflege
- [x] **13.4.1** `CLAUDE.md` mit Projektkonventionen anlegen (`/init`) — [CLAUDE.md](CLAUDE.md). - [x] **13.4.1** `CLAUDE.md` mit Projektkonventionen anlegen (`/init`) — [CLAUDE.md](CLAUDE.md).
@@ -706,6 +716,16 @@ Fächer- und Kompetenzverwaltung existiert bereits in
- [ ] **14.5** Leere Zustände mit Handlungsaufforderung statt leerer Tabellen. - [ ] **14.5** Leere Zustände mit Handlungsaufforderung statt leerer Tabellen.
- [ ] **14.6** Fenstergröße und Spaltenbreiten über Sitzungen hinweg merken. - [ ] **14.6** Fenstergröße und Spaltenbreiten über Sitzungen hinweg merken.
- [ ] **14.7** Bedienung auf Touch-Geräten prüfen (Tablet im Unterricht). - [ ] **14.7** Bedienung auf Touch-Geräten prüfen (Tablet im Unterricht).
- [ ] **14.8** Responsive Layout und Windows-DPI prüfen (kleine Notebook-Auflösungen sowie
125/150/200 % Skalierung; starre Master-Detail-Spalten bei Bedarf stapeln). Der kompakte
Drawer berücksichtigt bereits die schmalere verfügbare Breite mit reduziertem Außen-/
Innenabstand und einer eigenen Iconfläche, damit Windows-Emoji nicht abgeschnitten werden.
- [ ] **14.9** Barrierefreiheit prüfen: Automation-Namen für Icon-Buttons, sichtbare Fokusrahmen,
Kontraste und Status nicht ausschließlich über Farbe/Emoji vermitteln.
- [ ] **14.10** Plattformübergreifend konsistentes SVG-/`PathIcon`-Set statt systemabhängiger
Emoji-Darstellung einführen.
- [x] **14.11** Aktiven Navigationspunkt in der Seitenleiste sichtbar hervorheben; Zustand wird
über `MainWindowViewModel.ActiveNavItem` gesteuert.
--- ---
@@ -719,6 +739,26 @@ Fächer- und Kompetenzverwaltung existiert bereits in
--- ---
## 16. Datenmodell- und Architekturqualität
- [ ] **16.1** Referenzielle Integrität für alle Modellbeziehungen dokumentieren und je Beziehung
explizit `Cascade`, `Restrict`, `SetNull` oder Archivierung festlegen; Löschpfade mit
Transaktions- und Vollständigkeitstests absichern.
- [ ] **16.2** Domain-Validierung aus den Dialog-ViewModels in gemeinsam nutzbare Regeln/Services
überführen, damit Import, Sync und API dieselben Regeln durchsetzen.
- [ ] **16.3** Prüfen, ob Punkte, Gewichtungen und Prozentgrenzen von `double` auf `decimal` oder
skalierte Ganzzahlen migriert werden sollen; Rundungs- und Migrationsstrategie festlegen.
- [ ] **16.4** Redundant gespeicherte Bewertungswerte (`ExamResult.TotalPoints`, berechnete Note)
entweder ableiten oder zusammen mit einer unveränderlichen Version des verwendeten
Notenschlüssels als historischen Snapshot speichern.
- [ ] **16.5** Kompetenzzuordnungen auf stabile `CompetencyItem.Id` umstellen; Code/Beschreibung bei
Bedarf zusätzlich als historischen Snapshot speichern, damit Umbenennungen keine alten
Klausur- oder Sitzungsreferenzen brechen.
- [ ] **16.6** Navigation und manuelle Callback-Verdrahtung in `App.axaml.cs` langfristig durch
einen testbaren Navigationsdienst oder Messenger ersetzen; statischen Servicezugriff abbauen.
---
## Empfohlene Reihenfolge ## Empfohlene Reihenfolge
Die Abschnitte sind thematisch, nicht chronologisch nummeriert. Sinnvolle Bearbeitungsreihenfolge: Die Abschnitte sind thematisch, nicht chronologisch nummeriert. Sinnvolle Bearbeitungsreihenfolge: