Files
LehrerApp/LehrerApp.Desktop/Services/StudentWarningEvidenceService.cs
adminandClaude Opus 5.5 aa326cf8b6
CI / build-and-test (push) Canceled after 0s
Add warning detail popup and quick blocks for Vorgänge
Dashboard warnings (attendance, homework, lateness) get a "Details" flyout
with key figures, a list of dates and the compact month calendar from the
class teacher overview, now extracted as a shared control. The calendar
now aligns days under their weekday (Mo-So header, blank leading cells).

Creating Vorgänge and documentation is supported by quick blocks with the
current figures, dated standard phrases (e.g. "Klassenleitung informiert")
and documentation templates (status snapshot, conversation, informing the
class/subject teachers, parent call, parent letter). The class teacher
Vorgänge tab uses WebUntis figures (absences across all subjects, class
register) for the same blocks and templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 11:16:13 +02:00

123 lines
6.3 KiB
C#

using System.Globalization;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Services;
public enum StudentEvidenceKind { Absence, Lateness, Homework }
/// <summary>Ein einzelner Beleg hinter einer klassenübergreifenden Warnung (ein Fehltermin, eine
/// Verspätung, eine offene Hausaufgabe) — Code/Farbe bewusst identisch mit der Klassenlehrer-
/// Monatsansicht (ClassTeacherDetailsViewModel.BuildDayEvents: U/E/A/V/H), damit dasselbe Kürzel in
/// beiden Widgets dasselbe bedeutet.</summary>
public sealed record StudentEvidenceEvent(DateOnly Date, string GroupName, string Label, string Code,
string ColorHex, StudentEvidenceKind Kind)
{
private static readonly CultureInfo De = new("de-DE");
public string DateLabel => Date.ToString("ddd dd.MM.", De);
public string ListLine => $"{Date:dd.MM.yyyy} · {GroupName}: {Label}";
}
/// <summary>Momentaufnahme aller Belege einer/eines Schüler*in im laufenden Schuljahr. Grundlage
/// sowohl für das Detail-Popup der Dashboard-Warnungen als auch für die Schnellbausteine/Vorlagen
/// beim Anlegen von Vorgängen und Dokumentation — beides soll exakt dieselben Zahlen zeigen, die
/// die Warnung ausgelöst haben.</summary>
public sealed record StudentWarningEvidence(
Guid StudentId, string StudentName, string StudentFirstName, string StudentNaturalName, string SchoolYear, DateOnly From, DateOnly To, DateOnly AsOf,
AttendanceBalance Balance,
IReadOnlyList<StudentEvidenceEvent> Absences,
IReadOnlyList<StudentEvidenceEvent> Lateness,
IReadOnlyList<StudentEvidenceEvent> MissingHomework)
{
public int SignificantlyLateCount => Lateness.Count(e => e.Code == "V!");
}
/// <summary>
/// Sammelt die Belege hinter den drei klassenübergreifenden Dashboard-Warnungen (Fehlzeiten,
/// Hausaufgaben-Rückstand, Verspätungen) für eine/n einzelne/n Schüler*in. Zählt bewusst nach
/// denselben Regeln wie DashboardViewModel.LoadAttendanceWarnings/LoadMissingHomeworkWarnings/
/// LoadLatenessWarnings (Schuljahreszeitraum, AttendanceBalanceService, HomeworkDisplay.CountsAsMissing,
/// Late/SignificantlyLate) — eine abweichende Zahl im Popup als in der Warnzeile wäre verwirrend.
/// Keine eigene Persistenz, rein lesend.
/// </summary>
public class StudentWarningEvidenceService
{
private readonly IStudentRepository _students;
private readonly IParticipationSessionRepository _sessions;
private readonly IParticipationRepository _entries;
private readonly IGroupRepository _groups;
private readonly SchoolYearService _sy;
private readonly AttendanceBalanceService _attendanceBalance;
private readonly Func<DateTime> _now;
public StudentWarningEvidenceService(IStudentRepository students, IParticipationSessionRepository sessions,
IParticipationRepository entries, IGroupRepository groups, SchoolYearService sy,
AttendanceBalanceService attendanceBalance, Func<DateTime>? now = null)
{
_students = students;
_sessions = sessions;
_entries = entries;
_groups = groups;
_sy = sy;
_attendanceBalance = attendanceBalance;
_now = now ?? (() => DateTime.Now);
}
public StudentWarningEvidence Build(Guid studentId)
{
var schoolYear = _sy.CurrentSchoolYear();
var from = _sy.SchoolYearStart(schoolYear);
var to = _sy.SchoolYearEnd(schoolYear);
var sessions = _sessions.GetAll().ToDictionary(s => s.Id);
var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name);
var dated = _entries.GetByStudent(studentId)
.Select(e => (Entry: e, Session: sessions.GetValueOrDefault(e.SessionId)))
.Where(x => x.Session is not null && x.Session.Date >= from && x.Session.Date <= to)
.Select(x => (x.Entry, x.Session!.Date, Group: groupNames.GetValueOrDefault(x.Session.GroupId, "")))
.OrderByDescending(x => x.Date).ThenBy(x => x.Group)
.ToList();
var balance = _attendanceBalance.Calculate(dated.Select(x => (x.Date, x.Entry.Attendance)), from, to);
var absences = new List<StudentEvidenceEvent>();
var lateness = new List<StudentEvidenceEvent>();
var homework = new List<StudentEvidenceEvent>();
foreach (var (entry, date, group) in dated)
{
if (AbsenceEvent(entry.Attendance, date, group) is { } absence) absences.Add(absence);
if (entry.Attendance is AttendanceStatus.Late or AttendanceStatus.SignificantlyLate)
lateness.Add(new StudentEvidenceEvent(date, group, AttendanceDisplay.Label(entry.Attendance),
entry.Attendance == AttendanceStatus.SignificantlyLate ? "V!" : "V", "#EF6C00",
StudentEvidenceKind.Lateness));
var hw = HomeworkDisplay.Effective(entry);
if (HomeworkDisplay.CountsAsMissing(hw))
homework.Add(new StudentEvidenceEvent(date, group, HomeworkDisplay.Label(hw), "H", "#6A1B9A",
StudentEvidenceKind.Homework));
}
var student = _students.GetById(studentId);
return new StudentWarningEvidence(studentId, student?.FullName ?? "", student?.FirstName ?? "",
student is null ? "" : $"{student.FirstName} {student.LastName}".Trim(), schoolYear, from, to, DateOnly.FromDateTime(_now()),
balance, absences, lateness, homework);
}
/// Nur die Status, die AttendanceBalanceService in die Fehlquote einrechnet (entschuldigt,
/// unentschuldigt, Entschuldigung offen) — schulisch veranlasste Abwesenheit zählt dort nicht als
/// Fehlzeit und taucht deshalb auch hier nicht als Beleg auf.
private static StudentEvidenceEvent? AbsenceEvent(AttendanceStatus? status, DateOnly date, string group)
{
var (code, color) = status switch
{
AttendanceStatus.Unexcused or AttendanceStatus.Truant or AttendanceStatus.LeftDuringClass => ("U", "#C62828"),
AttendanceStatus.ExcusePending => ("A", "#1565C0"),
AttendanceStatus.Excused => ("E", "#2E7D32"),
_ => ("", ""),
};
return code == "" ? null
: new StudentEvidenceEvent(date, group, AttendanceDisplay.Label(status), code, color, StudentEvidenceKind.Absence);
}
}