feat: Fehlzeiten Tabelle für Templates

This commit is contained in:
2026-09-15 10:35:55 +02:00
parent 1671796844
commit 09cbbf1b77
11 changed files with 178 additions and 18 deletions
+3 -1
View File
@@ -20,7 +20,9 @@ public static class LetterDialogs
App.Services.GetRequiredService<ITemplateRenderer>(),
App.Services.GetRequiredService<IGroupMembershipRepository>(),
App.Services.GetRequiredService<IGroupRepository>(),
options => App.Services.GetRequiredService<StudentAttendanceCalendarService>().Build(student, options));
options => App.Services.GetRequiredService<StudentAttendanceCalendarService>().Build(student, options),
options => App.Services.GetRequiredService<StudentAttendanceCalendarService>()
.BuildAbsenceDayList(student, options));
var dialog = new CreateLetterDialog { DataContext = vm };
var path = await dialog.ShowDialog<string?>(owner);
if (!string.IsNullOrEmpty(path) && File.Exists(path))
@@ -15,7 +15,8 @@ public static class LetterPlaceholderBuilder
{
public static Dictionary<string, PlaceholderValue> BuildStandardValues(
Student student, Contact? contact, LearningGroup? group, DateOnly date,
string letterText, string teacherName, DrawingValue? attendanceCalendar = null)
string letterText, string teacherName, DrawingValue? attendanceCalendar = null,
DrawingValue? absenceDays = null)
{
var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City }.Where(x => !string.IsNullOrWhiteSpace(x)));
var address = string.Join(Environment.NewLine, new[] { contact?.Name, contact?.Street, cityLine }.Where(x => !string.IsNullOrWhiteSpace(x)));
@@ -30,6 +31,7 @@ public static class LetterPlaceholderBuilder
["Contact.City"] = new TextValue(contact?.City ?? ""), ["Letter.Salutation"] = new TextValue(contact?.LetterSalutation ?? ""),
["Group.Name"] = new TextValue(group?.Name ?? ""), ["SchoolYear"] = new TextValue(group?.SchoolYear ?? ""),
[StudentAttendanceCalendarDrawingBuilder.PlaceholderName] = attendanceCalendar ?? new DrawingValue([], 0),
[StudentAbsenceDayListDrawingBuilder.PlaceholderName] = absenceDays ?? new DrawingValue([], 0),
};
}
@@ -75,7 +75,8 @@ public class LetterTemplateTools(
var date = letterDate ?? DateOnly.FromDateTime(DateTime.Today);
var values = LetterPlaceholderBuilder.BuildStandardValues(student, contact, group, date, letterText,
teacherName, attendanceCalendars?.Build(student, date));
teacherName, attendanceCalendars?.Build(student, date),
attendanceCalendars?.BuildAbsenceDayList(student, new AttendanceCalendarOptions(date, 1)));
foreach (var (name, raw) in extraValues ?? [])
{
var definition = loaded.Manifest.Placeholders.FirstOrDefault(p => p.Name == name);
@@ -91,6 +91,87 @@ public static class StudentAttendanceCalendarDrawingBuilder
}
}
/// <summary>Chronologische, portable Fehlzeitenliste für Elternbriefvorlagen.</summary>
public static class StudentAbsenceDayListDrawingBuilder
{
public const string PlaceholderName = "Student.AbsenceDays";
public static DrawingValue Build(string studentName, AttendanceCalendarOptions options,
IReadOnlyList<ClassAbsenceDaySummaryRow> absences)
{
const float width = 170;
var scale = options.Size switch
{
AttendanceCalendarSize.Small => .75f,
AttendanceCalendarSize.Large => 1f,
_ => .88f,
};
var contentWidth = width * scale;
var rowHeight = 9 * scale;
var start = options.NormalizedStartMonth;
var end = start.AddMonths(options.NormalizedMonthCount).AddDays(-1);
var rows = absences
.Where(a => a.Date >= start && a.Date <= end &&
UntisNameMatching.NamesMatch(a.StudentName, studentName))
.OrderBy(a => a.Date)
.ToList();
var commands = new List<DrawingCommand>
{
new DrawStringEx(0, 0, 7 * scale, contentWidth, "Fehltage",
DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true),
new DrawStringEx(0, 7 * scale, 6 * scale, contentWidth, studentName,
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"),
};
var y = 16 * scale;
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6"));
commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - 2 * scale, 31 * scale, "Datum",
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - 2 * scale, 75 * scale, "Umfang",
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - 2 * scale, 55 * scale, "Status",
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
y += rowHeight;
if (rows.Count == 0)
{
commands.Add(new DrawStringEx(2 * scale, y + 2 * scale, rowHeight, contentWidth - 4 * scale,
"Keine Fehltage im gewählten Zeitraum", DrawingTextAlignment.AlignLeft, 8 * scale,
Color: "#6B7280", Italic: true));
y += rowHeight + 3 * scale;
}
else
{
for (var index = 0; index < rows.Count; index++)
{
var row = rows[index];
var fill = index % 2 == 0 ? "#FFFFFF" : "#F9FAFB";
var extent = row.CountsAsFullDay
? "Ganzer Fehltag"
: row.TotalAbsentPeriods > 0
? $"Fehlzeit · {row.TotalAbsentPeriods} Std."
: $"Fehlzeit · {row.TotalAbsentMinutes} Min.";
var status = row.IsUnexcused
? "Unentschuldigt"
: row.FriendlyStatusLabel.Contains("Entschuldigt", StringComparison.OrdinalIgnoreCase)
? "Entschuldigt"
: row.FriendlyStatusLabel;
var statusColor = row.IsUnexcused ? "#C62828" : "#2E7D32";
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#E5E7EB", .3f, fill));
commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - 2 * scale, 31 * scale,
row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale,
Color: "#374151"));
commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - 2 * scale, 75 * scale,
extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - 2 * scale, 55 * scale,
status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor,
Bold: row.IsUnexcused));
y += rowHeight;
}
}
return new DrawingValue(commands, y);
}
}
/// <summary>Liest ausschließlich den lokalen WebUntis-Cache. Das Öffnen oder Rendern eines
/// Elternbriefs löst dadurch keinen unerwarteten Netzwerkabruf aus.</summary>
public sealed class StudentAttendanceCalendarService(
@@ -126,4 +207,24 @@ public sealed class StudentAttendanceCalendarService(
return StudentAttendanceCalendarDrawingBuilder.Build(rosterName, options,
ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences), register);
}
public DrawingValue BuildAbsenceDayList(Student student, AttendanceCalendarOptions options)
{
var className = settings.HomeroomClassName;
if (string.IsNullOrWhiteSpace(className)) return new DrawingValue([], 0);
var first = options.NormalizedStartMonth;
var last = first.AddMonths(options.NormalizedMonthCount).AddDays(-1);
var rosterName = rosterCache.GetByClass(className)
.FirstOrDefault(r => UntisNameMatching.NamesMatch(r.DisplayName, student.FullName))?.DisplayName
?? student.FullName;
var start = first.Year * 10000 + first.Month * 100 + first.Day;
var end = last.Year * 10000 + last.Month * 100 + last.Day;
var absences = absenceCache.GetByClassAndRange(className, start, end)
.Select(e => new UntisClassAbsenceEntryDto(e.StudentName, e.ExternKey, e.ClassName, e.Date,
e.AbsentPeriods, e.AbsentMinutes, e.TeacherUsernames, e.Subject, e.AbsenceReason, e.Note,
e.EntryId, e.HandledOn, e.Counts, e.ExcuseNote, e.PeriodNumber, e.Status, e.CountsAsFullDay));
return StudentAbsenceDayListDrawingBuilder.Build(rosterName, options,
ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences));
}
}