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
@@ -0,0 +1,88 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using Xunit;
namespace LehrerApp.Tests;
public sealed class AttendanceBalanceServiceTests
{
private readonly AttendanceBalanceService _service = new();
private static readonly DateOnly From = new(2025, 8, 1);
private static readonly DateOnly To = new(2026, 7, 31);
[Fact]
public void Calculate_ZaehltJedenStatusKorrekt()
{
var entries = new List<(DateOnly, AttendanceStatus?)>
{
(new DateOnly(2025, 9, 1), AttendanceStatus.Present),
(new DateOnly(2025, 9, 2), AttendanceStatus.Present),
(new DateOnly(2025, 9, 3), AttendanceStatus.Excused),
(new DateOnly(2025, 9, 4), AttendanceStatus.Unexcused),
(new DateOnly(2025, 9, 5), AttendanceStatus.Truant),
(new DateOnly(2025, 9, 6), AttendanceStatus.OtherSchoolEvent),
(new DateOnly(2025, 9, 7), AttendanceStatus.ExcusePending),
};
var balance = _service.Calculate(entries, From, To);
Assert.Equal(7, balance.TotalChecked);
Assert.Equal(2, balance.Present);
Assert.Equal(1, balance.Excused);
Assert.Equal(2, balance.Unexcused); // Unexcused + Truant
Assert.Equal(1, balance.SchoolEvent);
Assert.Equal(1, balance.ExcusePending);
}
[Fact]
public void Calculate_IgnoriertEintraegeOhneStatusUndAusserhalbDesZeitraums()
{
var entries = new List<(DateOnly, AttendanceStatus?)>
{
(new DateOnly(2025, 9, 1), null),
(new DateOnly(2024, 9, 1), AttendanceStatus.Unexcused), // vor dem Zeitraum
(new DateOnly(2025, 9, 1), AttendanceStatus.Present),
};
var balance = _service.Calculate(entries, From, To);
Assert.Equal(1, balance.TotalChecked);
Assert.Equal(1, balance.Present);
}
[Fact]
public void Calculate_SchulischVeranlassteAbwesenheitZaehltNichtAlsFehlzeit()
{
var entries = new List<(DateOnly, AttendanceStatus?)>
{
(new DateOnly(2025, 9, 1), AttendanceStatus.OtherSchoolEvent),
(new DateOnly(2025, 9, 2), AttendanceStatus.Present),
};
var balance = _service.Calculate(entries, From, To);
Assert.Equal(0.0, balance.AbsenceRatePercent);
}
[Fact]
public void ExceedsThreshold_UeberZwanzigProzentFehlquote_IstTrue()
{
var entries = Enumerable.Range(0, 10)
.Select(i => (From.AddDays(i), (AttendanceStatus?)(i < 3 ? AttendanceStatus.Unexcused : AttendanceStatus.Present)))
.ToList();
var balance = _service.Calculate(entries, From, To);
Assert.Equal(30.0, balance.AbsenceRatePercent);
Assert.True(balance.ExceedsThreshold);
}
[Fact]
public void ExceedsThreshold_KeineKontrolliertenStunden_IstFalseUndRateIstNull()
{
var balance = _service.Calculate([], From, To);
Assert.Equal(0.0, balance.AbsenceRatePercent);
Assert.False(balance.ExceedsThreshold);
}
}
@@ -0,0 +1,60 @@
using LehrerApp.Core.Services;
using Xunit;
namespace LehrerApp.Tests;
public sealed class PrivacySettingsServiceTests
{
[Fact]
public void NeueKonfiguration_HatDreiJahreAlsStandard()
{
using var temp = new TempAppData();
var service = new PrivacySettingsService(temp.Path);
Assert.Equal(3, service.RetentionYears);
}
[Fact]
public void SetRetentionYears_WirdUeberNeueInstanzHinwegPersistiert()
{
using var temp = new TempAppData();
new PrivacySettingsService(temp.Path).SetRetentionYears(5);
var second = new PrivacySettingsService(temp.Path);
Assert.Equal(5, second.RetentionYears);
}
[Fact]
public void SetRetentionYears_WertUnterEinsWirdAufEinsBegrenzt()
{
using var temp = new TempAppData();
var service = new PrivacySettingsService(temp.Path);
service.SetRetentionYears(0);
Assert.Equal(1, service.RetentionYears);
}
[Fact]
public void RetentionCutoff_LiegtRetentionYearsVorDemStichtag()
{
using var temp = new TempAppData();
var service = new PrivacySettingsService(temp.Path);
service.SetRetentionYears(3);
var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var cutoff = service.RetentionCutoff(now);
Assert.Equal(new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc), cutoff);
}
private sealed class TempAppData : IDisposable
{
public string Path { get; } = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-privacy-tests-{Guid.NewGuid():N}");
public TempAppData() => Directory.CreateDirectory(Path);
public void Dispose() { if (Directory.Exists(Path)) Directory.Delete(Path, recursive: true); }
}
}