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
@@ -20,9 +20,12 @@ public partial class DashboardViewModel : ObservableObject
private readonly IParticipationSessionRepository _participationSessions;
private readonly IParticipationRepository _participationEntries;
private readonly IStudentRepository _students;
private readonly IDocumentationRepository _documentation;
private readonly AttendanceBalanceService _attendanceBalance;
private readonly SchoolYearService _sy;
private const int OpenExcuseMaxAgeDays = 21;
private const int SupportPlanDueWithinDays = 14;
[ObservableProperty] private string _greeting = "";
[ObservableProperty] private string _currentDate = "";
@@ -36,18 +39,22 @@ public partial class DashboardViewModel : ObservableObject
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
// Navigation-Callback wird von App.axaml.cs verdrahtet
public Action<Guid>? OnNavigateToGroup { get; set; }
public Action<Guid>? OnNavigateToStudent { get; set; }
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
IParticipationRepository participationEntries, IStudentRepository students, SchoolYearService sy)
IParticipationRepository participationEntries, IStudentRepository students,
IDocumentationRepository documentation, AttendanceBalanceService attendanceBalance, SchoolYearService sy)
{
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
_participationSessions = participationSessions; _participationEntries = participationEntries;
_students = students; _sy = sy;
_students = students; _documentation = documentation; _attendanceBalance = attendanceBalance; _sy = sy;
Load();
}
@@ -90,8 +97,64 @@ public partial class DashboardViewModel : ObservableObject
CalendarMonth = FirstOfMonth(now);
LoadCalendar();
LoadOpenExcuses(groups.Values.ToList(), today);
LoadAttendanceWarnings(today);
LoadSupportPlanReviews(today);
}
// ── Fehlzeiten-Warnung (5.2.3) ────────────────────────────────────────────
private void LoadAttendanceWarnings(DateOnly today)
{
AttendanceWarnings.Clear();
var schoolYear = _sy.CurrentSchoolYear();
var from = _sy.SchoolYearStart(schoolYear);
var to = _sy.SchoolYearEnd(schoolYear);
var items = new List<AttendanceWarningItem>();
foreach (var student in _students.GetAll())
{
var entries = _participationEntries.GetByStudent(student.Id)
.Select(e => _participationSessions.GetById(e.SessionId) is { } session
? ((DateOnly?)session.Date, e.Attendance) : (null, e.Attendance))
.Where(t => t.Item1.HasValue)
.Select(t => (t.Item1!.Value, t.Attendance));
var balance = _attendanceBalance.Calculate(entries, from, to);
if (balance.ExceedsThreshold)
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
}
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
AttendanceWarnings.Add(item);
}
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
private void LoadSupportPlanReviews(DateOnly today)
{
SupportPlanReviews.Clear();
var dueBy = today.AddDays(SupportPlanDueWithinDays);
var due = _documentation.GetAll()
.Where(d => d.Type == DocumentationType.SupportPlan
&& d.SupportData is { Status: SupportStatus.Active, ReviewDate: not null }
&& d.SupportData.ReviewDate!.Value <= dueBy)
.OrderBy(d => d.SupportData!.ReviewDate);
foreach (var d in due)
{
var student = _students.GetById(d.StudentId);
if (student is null) continue;
SupportPlanReviews.Add(new SupportPlanDueItem(
d.StudentId, student.FullName, d.Title, d.SupportData!.ReviewDate!.Value, today));
}
}
[RelayCommand] private void OpenStudentAttendance(AttendanceWarningItem? item)
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
[RelayCommand] private void OpenStudentSupportPlan(SupportPlanDueItem? item)
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
private void LoadOpenExcuses(List<LearningGroup> groups, DateOnly today)
{
OpenExcuses.Clear();
@@ -243,6 +306,35 @@ public partial class OpenExcuseItem : ObservableObject
[RelayCommand] private void MarkUnexcused() => OnResolve?.Invoke(this, AttendanceStatus.Unexcused);
}
// ── Fehlzeiten-Warnung (5.2.3) ────────────────────────────────────────────────
public class AttendanceWarningItem(Guid studentId, string studentName, double absenceRatePercent)
{
public Guid StudentId { get; } = studentId;
public string StudentName { get; } = studentName;
public double AbsenceRatePercent { get; } = absenceRatePercent;
}
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────────
public class SupportPlanDueItem
{
public Guid StudentId { get; }
public string StudentName { get; }
public string Title { get; }
public string ReviewDateDisplay { get; }
public bool IsOverdue { get; }
public SupportPlanDueItem(Guid studentId, string studentName, string title, DateOnly reviewDate, DateOnly today)
{
StudentId = studentId;
StudentName = studentName;
Title = title;
ReviewDateDisplay = reviewDate.ToString("dd.MM.yyyy");
IsOverdue = reviewDate < today;
}
}
public class CalendarDayCell
{
public int DayNumber { get; }