Review der Codebase
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -40,40 +40,74 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
|
|||||||
}
|
}
|
||||||
public void Delete(Guid id)
|
public void Delete(Guid id)
|
||||||
{
|
{
|
||||||
foreach (var membership in db.Memberships.Find(e => e.GroupId == id).ToList())
|
db.ExecuteInTransaction(() =>
|
||||||
db.Memberships.Delete(membership.Id);
|
|
||||||
|
|
||||||
foreach (var exam in db.Exams.Find(e => e.GroupId == id).ToList())
|
|
||||||
{
|
{
|
||||||
foreach (var result in db.ExamResults.Find(r => r.ExamId == exam.Id).ToList())
|
foreach (var membership in db.Memberships.Find(e => e.GroupId == id).ToList())
|
||||||
db.ExamResults.Delete(result.Id);
|
db.Memberships.Delete(membership.Id);
|
||||||
db.Exams.Delete(exam.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var grade in db.Grades.Find(g => g.GroupId == id).ToList())
|
foreach (var exam in db.Exams.Find(e => e.GroupId == id).ToList())
|
||||||
db.Grades.Delete(grade.Id);
|
{
|
||||||
|
foreach (var result in db.ExamResults.Find(r => r.ExamId == exam.Id).ToList())
|
||||||
|
db.ExamResults.Delete(result.Id);
|
||||||
|
db.Exams.Delete(exam.Id);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var unit in db.Units.Find(u => u.GroupId == id).ToList())
|
foreach (var grade in db.Grades.Find(g => g.GroupId == id).ToList())
|
||||||
{
|
db.Grades.Delete(grade.Id);
|
||||||
foreach (var lesson in db.Lessons.Find(l => l.UnitId == unit.Id).ToList())
|
|
||||||
|
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())
|
||||||
|
db.Lessons.Delete(lesson.Id);
|
||||||
|
db.Units.Delete(unit.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var lesson in db.Lessons.Find(l => l.GroupId == id).ToList())
|
||||||
db.Lessons.Delete(lesson.Id);
|
db.Lessons.Delete(lesson.Id);
|
||||||
db.Units.Delete(unit.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var lesson in db.Lessons.Find(l => l.GroupId == id).ToList())
|
foreach (var session in db.ParticipationSessions.Find(s => s.GroupId == id).ToList())
|
||||||
db.Lessons.Delete(lesson.Id);
|
{
|
||||||
|
foreach (var entry in db.ParticipationEntries.Find(e => e.SessionId == session.Id).ToList())
|
||||||
|
db.ParticipationEntries.Delete(entry.Id);
|
||||||
|
db.ParticipationSessions.Delete(session.Id);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var session in db.ParticipationSessions.Find(s => s.GroupId == id).ToList())
|
foreach (var aspect in db.ParticipationAspects.Find(a => a.GroupId == id).ToList())
|
||||||
{
|
db.ParticipationAspects.Delete(aspect.Id);
|
||||||
foreach (var entry in db.ParticipationEntries.Find(e => e.SessionId == session.Id).ToList())
|
|
||||||
db.ParticipationEntries.Delete(entry.Id);
|
|
||||||
db.ParticipationSessions.Delete(session.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var aspect in db.ParticipationAspects.Find(a => a.GroupId == id).ToList())
|
foreach (var section in db.ParticipationSections.Find(s => s.GroupId == id).ToList())
|
||||||
db.ParticipationAspects.Delete(aspect.Id);
|
db.ParticipationSections.Delete(section.Id);
|
||||||
|
|
||||||
db.Groups.Delete(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 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)
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user