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
@@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using System.Collections.ObjectModel;
using System.Globalization;
@@ -79,6 +80,11 @@ public partial class StudentDetailViewModel : ObservableObject
private readonly IExamRepository _exams;
private readonly IExamResultRepository _examResults;
private readonly IGradeRepository _grades;
private readonly IParticipationRepository _participation;
private readonly IParticipationSessionRepository _participationSessions;
private readonly AttendanceBalanceService _attendanceBalance;
private readonly PersonalDataExportService _export;
private readonly SchoolYearService _schoolYear;
[ObservableProperty] private Student? _student;
[ObservableProperty] private string _studentTitle = "";
@@ -86,23 +92,33 @@ public partial class StudentDetailViewModel : ObservableObject
[ObservableProperty] private string _editFirstName = "";
[ObservableProperty] private string _editLastName = "";
[ObservableProperty] private ContactItem? _selectedContact;
[ObservableProperty] private AttendanceBalance? _attendance;
[ObservableProperty] private string _exportStatus = "";
public ObservableCollection<GroupMembershipEntry> GroupMemberships { get; } = [];
public ObservableCollection<DocEntry> Documentation { get; } = [];
public ObservableCollection<DocumentationItem> Documentation { get; } = [];
public ObservableCollection<ContactItem> Contacts { get; } = [];
public ObservableCollection<StudentGradeHistoryGroup> GradeHistory { get; } = [];
public bool HasNoContacts => Contacts.Count == 0;
public Func<Contact?, Task<Contact?>>? OnEditContact { get; set; }
public Action<ContactItem>? OnViewAddress { get; set; }
public Func<Guid, Documentation?, Task<Documentation?>>? OnEditDocumentation { get; set; }
public Func<DocumentationItem, Task<bool>>? OnConfirmDeleteDocumentation { get; set; }
public Func<string, Task>? OnSaveExportFile { get; set; }
public Func<Documentation, string, Task<Documentation?>>? OnConductParentCall { get; set; }
public StudentDetailViewModel(IStudentRepository students,
IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects,
IDocumentationRepository docs, IExamRepository exams, IExamResultRepository examResults,
IGradeRepository grades)
IGradeRepository grades, IParticipationRepository participation,
IParticipationSessionRepository participationSessions, AttendanceBalanceService attendanceBalance,
PersonalDataExportService export, SchoolYearService schoolYear)
{
_students = students; _memberships = memberships;
_groups = groups; _subjects = subjects; _docs = docs;
_exams = exams; _examResults = examResults; _grades = grades;
_participation = participation; _participationSessions = participationSessions;
_attendanceBalance = attendanceBalance; _export = export; _schoolYear = schoolYear;
}
public void LoadStudent(Guid id)
@@ -127,19 +143,86 @@ public partial class StudentDetailViewModel : ObservableObject
}
LoadContacts();
LoadDocumentation();
LoadAttendanceBalance();
}
// ── Dokumentation (5.1) ───────────────────────────────────────────────────
private void LoadDocumentation()
{
if (Student is null) return;
Documentation.Clear();
foreach (var d in _docs.GetByStudent(Student.Id))
Documentation.Add(new() { Date = d.Date.ToString("dd.MM.yyyy"), Title = d.Title,
TypeLabel = d.Type switch
{
DocumentationType.Conversation => "Gespräch",
DocumentationType.Incident => "Vorkommnis",
DocumentationType.SupportPlan => "Förderplan",
DocumentationType.Absence => "Fehlzeit",
_ => "",
},
IsConfidential = d.IsConfidential });
Documentation.Add(new DocumentationItem(d));
}
[RelayCommand]
private async Task AddDocumentation()
{
if (Student is null || OnEditDocumentation is null) return;
var result = await OnEditDocumentation(Student.Id, null);
if (result is null) return;
_docs.Save(result);
LoadDocumentation();
}
[RelayCommand]
private async Task EditDocumentation(DocumentationItem? item)
{
if (Student is null || item is null || OnEditDocumentation is null) return;
var result = await OnEditDocumentation(Student.Id, item.Model);
if (result is null) return;
_docs.Save(result);
LoadDocumentation();
}
[RelayCommand]
private async Task DeleteDocumentation(DocumentationItem? item)
{
if (item is null) return;
if (OnConfirmDeleteDocumentation is not null && !await OnConfirmDeleteDocumentation(item)) return;
_docs.Delete(item.Model.Id);
LoadDocumentation();
}
[RelayCommand]
private async Task ConductParentCall(DocumentationItem? item)
{
if (Student is null || item is null || OnConductParentCall is null) return;
var result = await OnConductParentCall(item.Model, Student.FullName);
if (result is null) return;
_docs.Save(result);
LoadDocumentation();
}
// ── Fehlzeitenbilanz (5.2.2/5.2.3) ────────────────────────────────────────
private void LoadAttendanceBalance()
{
if (Student is null) return;
var schoolYear = _schoolYear.CurrentSchoolYear();
var from = _schoolYear.SchoolYearStart(schoolYear);
var to = _schoolYear.SchoolYearEnd(schoolYear);
var entries = _participation.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));
Attendance = _attendanceBalance.Calculate(entries, from, to);
}
// ── Datenauskunft (5.4.3) ─────────────────────────────────────────────────
[RelayCommand]
private async Task ExportPersonalData()
{
if (Student is null || OnSaveExportFile is null) return;
var json = _export.ExportAsJson(Student.Id);
await OnSaveExportFile(json);
ExportStatus = "Datenauskunft exportiert.";
}
// ── Notenentwicklung (2.5) ────────────────────────────────────────────────
@@ -255,7 +338,6 @@ public partial class StudentDetailViewModel : ObservableObject
}
public class GroupMembershipEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
public class DocEntry { public string Date { get; set; } = ""; public string Title { get; set; } = ""; public string TypeLabel { get; set; } = ""; public bool IsConfidential { get; set; } }
// ── Notenentwicklung (2.5) ──────────────────────────────────────────────────