Schülerdokumentation: Einträge, Fehlzeitenbilanz, Datenschutz (Kapitel 5)

Dokumentationseinträge mit Typwahl (Gespräch, Vorkommnis, Förderplan,
Fehlzeit, Elternanruf, Elternbrief), vertrauliche Einträge nur nach
Bestätigung sichtbar, weiche Löschung mit Nachvollziehbarkeit. Fehlzeiten
als Auswertung des bestehenden Anwesenheits-Trackings statt zweiter
Erfassung, mit Schwellenwert-Warnung im Schülerdetail und Dashboard.
Förderplan-Wiedervorlage als Dashboard-Karte. Datenschutz: Löschfristen
mit manueller Bereinigung und DSGVO-Art.-15-Datenauskunft als Export.

Auf Nutzer-Feedback hin ergänzt: Elternanruf mit begleitendem
Gesprächsprotokoll-Dialog (Punkte abhaken, Eindrücke festhalten),
Elternbrief mit Versand-/Rückmeldungs-Tracking, Datei-Anhänge über
LiteDBs Dateispeicher, frei vergebbare Labels zur Nachverfolgung mit
Dringlichkeits-Farbcodierung, sowie eine sichtbare Farblegende für das
bestehende Notenentwicklungs-Diagramm. Dabei einen Absturz behoben:
leere Textfelder lieferten über das Binding null statt "", was beim
Speichern eine NullReferenceException auslöste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 17:49:03 +02:00
co-authored by Claude Sonnet 5
parent 629b8d1bf0
commit d987b93315
36 changed files with 2395 additions and 62 deletions
@@ -378,6 +378,58 @@
</ScrollViewer>
</ContentPage>
<!-- Tab: Datenschutz (5.4.2) -->
<ContentPage Header="Datenschutz">
<ScrollViewer>
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="640">
<TextBlock Text="Löschfristen für Dokumentationseinträge" FontSize="16" FontWeight="SemiBold"/>
<TextBlock Text="Einträge werden nie automatisch gelöscht — hier abgelaufene Einträge nur zur manuellen Prüfung anzeigen."
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
<Grid ColumnDefinitions="140,Auto">
<NumericUpDown Grid.Column="0" Value="{Binding RetentionYears}" Minimum="1" Maximum="20" FormatString="0"/>
<Button Grid.Column="1" Content="Speichern" Margin="8,0,0,0"
Command="{Binding SaveRetentionYearsCommand}"/>
</Grid>
<TextBlock Text="{Binding RetentionStatus}" Foreground="Green" FontSize="12"
IsVisible="{Binding RetentionStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Separator Margin="0,4"/>
<TextBlock Text="Abgelaufene Einträge" FontSize="14" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding ExpiredDocuments}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ExpiredDocumentItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1" Padding="0,7">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock FontSize="13">
<Run Text="{Binding StudentName}"/>
<Run Text=" — "/>
<Run Text="{Binding Title}"/>
</TextBlock>
<TextBlock FontSize="11" Opacity="0.5">
<Run Text="erfasst am "/>
<Run Text="{Binding CreatedAtDisplay}"/>
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Content="Endgültig löschen" FontSize="12" Padding="10,4"
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).HardDeleteDocumentCommand}"
CommandParameter="{Binding}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine abgelaufenen Einträge." Classes="emptyhint"
IsVisible="{Binding !ExpiredDocuments.Count}"/>
</StackPanel>
</ScrollViewer>
</ContentPage>
</TabbedPage>
</Grid>
</UserControl>
@@ -19,6 +19,7 @@ public partial class SettingsView : UserControl
{
vm.OnConfirmRestore = ShowRestoreConfirmDialog;
vm.OnAppLockChanged = () => App.Services.GetRequiredService<AppLockViewModel>().ApplyConfig();
vm.OnConfirmHardDelete = ShowHardDeleteConfirmDialog;
}
}
@@ -36,6 +37,19 @@ public partial class SettingsView : UserControl
return owner is not null && await dialog.ShowDialog<bool>(owner);
}
private async Task<bool> ShowHardDeleteConfirmDialog(ExpiredDocumentItem item)
{
var info = new ConfirmDialogInfo
{
Title = "Eintrag endgültig löschen?",
Message = $"\"{item.Title}\" ({item.StudentName}) wird unwiderruflich gelöscht, nicht nur ausgeblendet.",
ConfirmText = "Endgültig löschen",
};
var dialog = new ConfirmDialog { DataContext = info };
var owner = TopLevel.GetTopLevel(this) as Window;
return owner is not null && await dialog.ShowDialog<bool>(owner);
}
private async void OnImportClick(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);