Review der Codebase
This commit is contained in:
@@ -102,6 +102,24 @@ public sealed class RepositoryTests
|
||||
var aspectId = Guid.NewGuid();
|
||||
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);
|
||||
|
||||
Assert.Null(db.Groups.FindById(groupId));
|
||||
@@ -115,6 +133,12 @@ public sealed class RepositoryTests
|
||||
Assert.Null(db.ParticipationSessions.FindById(sessionId));
|
||||
Assert.Null(db.ParticipationEntries.FindById(entryId));
|
||||
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]
|
||||
|
||||
@@ -60,6 +60,21 @@ public class LiteDbContext : IDisposable
|
||||
|
||||
public int SchemaVersion => ReadSchemaVersion();
|
||||
|
||||
internal void ExecuteInTransaction(Action action)
|
||||
{
|
||||
_db.BeginTrans();
|
||||
try
|
||||
{
|
||||
action();
|
||||
_db.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_db.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RunVersionedMigrations()
|
||||
{
|
||||
var version = ReadSchemaVersion();
|
||||
|
||||
@@ -39,6 +39,8 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
|
||||
db.Groups.Upsert(g);
|
||||
}
|
||||
public void Delete(Guid id)
|
||||
{
|
||||
db.ExecuteInTransaction(() =>
|
||||
{
|
||||
foreach (var membership in db.Memberships.Find(e => e.GroupId == id).ToList())
|
||||
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())
|
||||
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 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())
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace LehrerApp.Desktop;
|
||||
public class App : Application
|
||||
{
|
||||
public static IServiceProvider Services { get; private set; } = null!;
|
||||
private static ServiceProvider? _serviceProvider;
|
||||
private static bool _exitHandlerAttached;
|
||||
|
||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
@@ -32,31 +34,58 @@ public class App : Application
|
||||
promptVm.OnUnlocked = password =>
|
||||
{
|
||||
AppBootstrapper.DbPassword = password;
|
||||
StartMainApp(desktop);
|
||||
StartMainApp(desktop, showImmediately: true);
|
||||
promptWindow.Close();
|
||||
};
|
||||
desktop.MainWindow = promptWindow;
|
||||
}
|
||||
else
|
||||
{
|
||||
StartMainApp(desktop);
|
||||
StartMainApp(desktop, showImmediately: false);
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
GlobalExceptionHandler.AttachNotifications(Services.GetRequiredService<NotificationService>());
|
||||
|
||||
if (!_exitHandlerAttached)
|
||||
{
|
||||
desktop.Exit += (_, _) => DisposeServices();
|
||||
_exitHandlerAttached = true;
|
||||
}
|
||||
|
||||
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
|
||||
WireCallbacks(mainVm);
|
||||
var main = new MainWindow { DataContext = mainVm };
|
||||
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)
|
||||
|
||||
@@ -76,7 +76,7 @@ public static class AppBootstrapper
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
public static IServiceProvider BuildServices()
|
||||
public static ServiceProvider BuildServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ namespace LehrerApp.Desktop.Services;
|
||||
/// Zentrale Exception-Behandlung (13.2.1): protokolliert jeden unerwarteten Fehler und zeigt
|
||||
/// eine verständliche Meldung statt eines rohen Absturzes.
|
||||
///
|
||||
/// Reichweite ehrlich betrachtet: <see cref="Dispatcher"/>.UIThread.UnhandledException kann
|
||||
/// Ausnahmen aus Befehlen/Ereignis-Handlern auf dem UI-Thread abfangen und die App am Leben
|
||||
/// erhalten (Handled = true) — das deckt den weit überwiegenden Teil realer Abstürze in einer
|
||||
/// Desktop-App ab. AppDomain.UnhandledException und TaskScheduler.UnobservedTaskException sind
|
||||
/// <see cref="Dispatcher"/>.UIThread.UnhandledException fängt Fehler aus Befehlen und
|
||||
/// Ereignis-Handlern ab. Nur erwartbar wiederherstellbare I/O-, Netzwerk-, Timeout- und
|
||||
/// Abbruchfehler werden behandelt; unbekannte Zustandsfehler dürfen die App beenden.
|
||||
/// AppDomain.UnhandledException und TaskScheduler.UnobservedTaskException sind
|
||||
/// 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.
|
||||
/// </summary>
|
||||
@@ -36,10 +36,18 @@ public static class GlobalExceptionHandler
|
||||
private static void OnDispatcherUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
_logger?.Error("Unbehandelter Fehler auf dem UI-Thread.", e.Exception);
|
||||
_notifications?.ShowError("Es ist ein unerwarteter Fehler aufgetreten. Details wurden protokolliert.");
|
||||
e.Handled = true; // App am Leben halten statt abzustürzen.
|
||||
e.Handled = IsRecoverable(e.Exception);
|
||||
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)
|
||||
{
|
||||
_logger?.Error(
|
||||
|
||||
@@ -22,6 +22,14 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
public ObservableCollection<ToastItem> Toasts { 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,
|
||||
DashboardViewModel dashboard, SchoolYearService sy,
|
||||
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock)
|
||||
@@ -35,6 +43,17 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
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]
|
||||
private void NavigateTo(NavItem item)
|
||||
{
|
||||
|
||||
@@ -60,6 +60,15 @@
|
||||
überschreiben kann – lokale Werte hätten immer Vorrang vor Style-Settern. -->
|
||||
<Style Selector="TextBlock.navicon">
|
||||
<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>
|
||||
<!-- Minimierter Zustand: Icons größer & zentriert, Beschriftungen ausgeblendet -->
|
||||
<Style Selector="DockPanel.compact TextBlock.navlabel">
|
||||
@@ -67,9 +76,20 @@
|
||||
</Style>
|
||||
<Style Selector="DockPanel.compact TextBlock.navicon">
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
<Setter Property="Width" Value="28"/>
|
||||
</Style>
|
||||
<Style Selector="DockPanel.compact Button.navitem">
|
||||
<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 Selector="DockPanel.compact TextBlock.navsection">
|
||||
<Setter Property="IsVisible" Value="False"/>
|
||||
@@ -82,7 +102,7 @@
|
||||
</Style>
|
||||
</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}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
@@ -106,14 +126,14 @@
|
||||
</Border>
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8,12,8,0" Spacing="2">
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Background="Transparent"
|
||||
Padding="10,8" CornerRadius="6"
|
||||
<StackPanel Classes="navitems" Margin="8,12,8,0" Spacing="2">
|
||||
<Button Classes="navitem" Classes.active="{Binding IsDashboardActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
Command="{Binding NavigateToCommand}"
|
||||
CommandParameter="{x:Static vm:NavItem.Dashboard}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="📊" Width="20" TextAlignment="Center"/>
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="📊" TextAlignment="Center"/>
|
||||
<TextBlock Classes="navlabel" Text="Dashboard"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -121,43 +141,43 @@
|
||||
<TextBlock Classes="navsection" Text="UNTERRICHT" FontSize="10" FontWeight="Bold"
|
||||
Opacity="0.4" Margin="10,14,0,4"/>
|
||||
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Background="Transparent"
|
||||
Padding="10,8" CornerRadius="6"
|
||||
<Button Classes="navitem" Classes.active="{Binding IsGroupsActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
Command="{Binding NavigateToCommand}"
|
||||
CommandParameter="{x:Static vm:NavItem.Groups}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="🏫" Width="20" TextAlignment="Center"/>
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="🏫" TextAlignment="Center"/>
|
||||
<TextBlock Classes="navlabel" Text="Lerngruppen"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Background="Transparent"
|
||||
Padding="10,8" CornerRadius="6"
|
||||
<Button Classes="navitem" Classes.active="{Binding IsStudentsActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
Command="{Binding NavigateToCommand}"
|
||||
CommandParameter="{x:Static vm:NavItem.Students}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="👤" Width="20" TextAlignment="Center"/>
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="👤" TextAlignment="Center"/>
|
||||
<TextBlock Classes="navlabel" Text="Schüler"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Background="Transparent"
|
||||
Padding="10,8" CornerRadius="6"
|
||||
<Button Classes="navitem" Classes.active="{Binding IsExamsActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
Command="{Binding NavigateToCommand}"
|
||||
CommandParameter="{x:Static vm:NavItem.Exams}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="📝" Width="20" TextAlignment="Center"/>
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="📝" TextAlignment="Center"/>
|
||||
<TextBlock Classes="navlabel" Text="Klausuren"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Background="Transparent"
|
||||
Padding="10,8" CornerRadius="6"
|
||||
<Button Classes="navitem" Classes.active="{Binding IsPlannerActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
Command="{Binding NavigateToCommand}"
|
||||
CommandParameter="{x:Static vm:NavItem.Planner}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="📅" Width="20" TextAlignment="Center"/>
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="📅" TextAlignment="Center"/>
|
||||
<TextBlock Classes="navlabel" Text="Planung"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -165,23 +185,23 @@
|
||||
<TextBlock Classes="navsection" Text="VERWALTUNG" FontSize="10" FontWeight="Bold"
|
||||
Opacity="0.4" Margin="10,14,0,4"/>
|
||||
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Background="Transparent"
|
||||
Padding="10,8" CornerRadius="6"
|
||||
<Button Classes="navitem" Classes.active="{Binding IsWorkloadActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
Command="{Binding NavigateToCommand}"
|
||||
CommandParameter="{x:Static vm:NavItem.Workload}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="⏱" Width="20" TextAlignment="Center"/>
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="⏱" TextAlignment="Center"/>
|
||||
<TextBlock Classes="navlabel" Text="Arbeitszeit"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Background="Transparent"
|
||||
Padding="10,8" CornerRadius="6"
|
||||
<Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
Command="{Binding NavigateToCommand}"
|
||||
CommandParameter="{x:Static vm:NavItem.Settings}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="⚙️" Width="20" TextAlignment="Center"/>
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="⚙️" TextAlignment="Center"/>
|
||||
<TextBlock Classes="navlabel" Text="Einstellungen"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -415,7 +415,10 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
||||
### 7.2 Gruppen
|
||||
- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/
|
||||
`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,
|
||||
neues `SchoolYear`, neue `GroupMembership`-Einträge.
|
||||
- [ ] **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.4** Manuelles Auslösen einer vollständigen Synchronisation.
|
||||
- [ ] **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.1** Benutzerverwaltung/Registrierung prüfen und absichern
|
||||
@@ -579,10 +584,11 @@ Fächer- und Kompetenzverwaltung existiert bereits in
|
||||
### 13.2 Fehlerbehandlung & Logging
|
||||
- [x] **13.2.1** Zentrale Exception-Behandlung mit verständlicher Fehlermeldung statt Absturz —
|
||||
[GlobalExceptionHandler.cs](LehrerApp.Desktop/Services/GlobalExceptionHandler.cs).
|
||||
`Dispatcher.UIThread.UnhandledException` fängt Fehler aus Befehlen/Ereignis-Handlern auf
|
||||
dem UI-Thread ab, protokolliert sie und setzt `Handled = true` — die App stürzt dabei
|
||||
nachweislich nicht ab (per Headless-Test verifiziert: Fehler in einem Dispatcher-Callback
|
||||
wird geloggt + als Toast angezeigt, App läuft weiter). `AppDomain.UnhandledException` und
|
||||
`Dispatcher.UIThread.UnhandledException` protokolliert Fehler aus Befehlen/Ereignis-Handlern.
|
||||
Nur als wiederherstellbar eingestufte I/O-, Netzwerk-, Timeout- und Abbruchfehler werden mit
|
||||
`Handled = true` behandelt und als Toast angezeigt. Unbekannte Programmier-/Zustandsfehler
|
||||
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
|
||||
UI-Threads — dort kann ein bereits "IsTerminating"-Fehler nicht mehr verhindert werden,
|
||||
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
|
||||
Einstellungen ist dort also nur in einer veröffentlichten Build (`dotnet publish`)
|
||||
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
|
||||
- [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.6** Fenstergröße und Spaltenbreiten über Sitzungen hinweg merken.
|
||||
- [ ] **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
|
||||
|
||||
Die Abschnitte sind thematisch, nicht chronologisch nummeriert. Sinnvolle Bearbeitungsreihenfolge:
|
||||
|
||||
Reference in New Issue
Block a user