375 lines
23 KiB
C#
375 lines
23 KiB
C#
using System.Globalization;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
|
using LehrerApp.Templating;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
public enum AttendanceCalendarSize { Small, Medium, Large }
|
|
|
|
public sealed record AttendanceCalendarOptions(DateOnly StartMonth, int MonthCount,
|
|
AttendanceCalendarSize Size = AttendanceCalendarSize.Medium)
|
|
{
|
|
public DateOnly NormalizedStartMonth => new(StartMonth.Year, StartMonth.Month, 1);
|
|
public int NormalizedMonthCount => Math.Clamp(MonthCount, 1, 3);
|
|
}
|
|
|
|
/// <summary>Erzeugt den portablen Advanced-Content-Platzhalter für Elternbriefe aus derselben
|
|
/// priorisierten Monatsansicht, die im Klassenlehrer-Sidebar-Widget verwendet wird.</summary>
|
|
public static class StudentAttendanceCalendarDrawingBuilder
|
|
{
|
|
public const string PlaceholderName = "Student.AttendanceCalendar";
|
|
|
|
public static DrawingValue Build(string studentName, DateOnly month,
|
|
IReadOnlyList<ClassAbsenceDaySummaryRow> absences,
|
|
IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries) =>
|
|
Build(studentName, new AttendanceCalendarOptions(month, 1), absences, registerEntries);
|
|
|
|
public static DrawingValue Build(string studentName, AttendanceCalendarOptions options,
|
|
IReadOnlyList<ClassAbsenceDaySummaryRow> absences,
|
|
IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries, float contentWidth = 170,
|
|
float millimeterScale = 1, float contentHeight = float.PositiveInfinity)
|
|
{
|
|
var requestedScale = options.Size switch
|
|
{
|
|
AttendanceCalendarSize.Small => .7f,
|
|
AttendanceCalendarSize.Large => 1f,
|
|
_ => .85f,
|
|
};
|
|
// contentWidth/contentHeight/millimeterScale kommen aus der tatsächlichen DRAWBOX/
|
|
// FLOWDRAWBOX-Deklaration der jeweiligen Vorlage (siehe
|
|
// LetterPlaceholderBuilder.FindDeclaredDrawingBox). Das gesamte Raster wird in einem festen
|
|
// Millimeter-Referenzentwurf (designWidth 170mm) gezeichnet und dann mit EINEM einzigen,
|
|
// gemeinsamen Faktor "k" in die Koordinaten-Einheit der Vorlage skaliert - k ist bewusst für
|
|
// X- und Y-Richtung identisch (kein separates a_x/a_y), sonst verzerren die Tageskacheln je
|
|
// nachdem, ob Breite oder Höhe der realen Box stärker vom Referenzentwurf abweicht.
|
|
var designWidth = 170f;
|
|
var designMonthGapX = 6f;
|
|
// Eigener (kleinerer) Y-Abstand vor der Legende, getrennt von designMonthGapX (das misst den
|
|
// horizontalen Abstand zwischen den Monatsblöcken und soll davon unberührt bleiben).
|
|
var designTrailingGapY = 2f;
|
|
var first = options.NormalizedStartMonth;
|
|
var monthCount = options.NormalizedMonthCount;
|
|
|
|
// Wochenanzahl je Monat vorab ermitteln, unabhängig von "Größe" - nötig, um VOR dem
|
|
// eigentlichen Zeichnen zu wissen, wie viel Höhe das Raster im Referenzentwurf braucht.
|
|
var maxWeeks = 0;
|
|
for (var monthIndex = 0; monthIndex < monthCount; monthIndex++)
|
|
{
|
|
var current = first.AddMonths(monthIndex);
|
|
var days = ClassTeacherOverviewViewModel.BuildCompactMonthDays(current, absences, registerEntries,
|
|
DateOnly.FromDateTime(DateTime.Today), studentName);
|
|
var offset = ((int)current.DayOfWeek + 6) % 7;
|
|
maxWeeks = Math.Max(maxWeeks, (int)Math.Ceiling((offset + days.Count) / 7d));
|
|
}
|
|
// Höhe des Referenzentwurfs bei k=1 (mm): Name+Abstand (5.5, siehe unten - keine eigene
|
|
// "Anwesenheit"-Überschrift mehr, die Monatsnamen darunter machen bereits klar, worum es
|
|
// geht; der Name bleibt klein/grau nur zur Kontrolle "ist das das richtige Kind") + Monats-/
|
|
// Wochentagskopf (20) + maxWeeks Wochenzeilen (10 je Woche) + Trailing-Gap (2) + Legende/
|
|
// Nachlauf (9). Muss mit der Höhenberechnung am Ende dieser Methode ("y") übereinstimmen. Je
|
|
// kleiner dieser "Overhead" ausfällt, desto größer kann k (und damit das ganze Raster inkl.
|
|
// der Tageskacheln) werden, wenn contentHeight der begrenzende Faktor ist.
|
|
var designHeight = 5.5f + 20f + 10f * maxWeeks + designTrailingGapY + 9f;
|
|
|
|
// a_x/a_y: wie viel größer/kleiner als der Referenzentwurf die reale Box in jeweils einer
|
|
// Richtung ist. Eine DRAWBOX bricht anders als FLOWDRAWBOX nicht automatisch auf Folgeseiten
|
|
// um (siehe DrawingElementRenderer.RenderFixed/Slice) - Inhalt, der contentHeight
|
|
// überschreitet, wird von QuestTemplateRenderer am unteren Rand stillschweigend
|
|
// abgeschnitten. a = min(a_x, a_y, "Größe") verkleinert nötigenfalls gleichmäßig, statt die
|
|
// Boxgrenzen zu verletzen oder die Kacheln durch getrenntes Skalieren zu verzerren.
|
|
var scaleForWidth = contentWidth / (designWidth * millimeterScale);
|
|
var scaleForHeight = float.IsFinite(contentHeight)
|
|
? contentHeight / (designHeight * millimeterScale)
|
|
: float.PositiveInfinity;
|
|
var finalScale = Math.Max(.35f, Math.Min(requestedScale, Math.Min(scaleForWidth, scaleForHeight)));
|
|
var k = finalScale * millimeterScale;
|
|
|
|
var cellHeight = 10 * k;
|
|
// War 10mm im Referenzentwurf - bei 3 Monaten nebeneinander größer als eine einzelne
|
|
// Tageskachel und zog damit spürbar Platz von den Kacheln ab, ohne selbst als Inhalt
|
|
// wahrgenommen zu werden.
|
|
var monthGapX = designMonthGapX * k;
|
|
var monthWidth = (designWidth * k - monthGapX * (monthCount - 1)) / monthCount;
|
|
var cellWidth = monthWidth / 7;
|
|
// Kachel-Zwischenraum (Trennung zu Nachbarzellen) deutlich knapper als vorher (war 1mm im
|
|
// Referenzentwurf - bei einer ~7mm breiten Zelle ein gutes Viertel reiner Leerraum). Die
|
|
// Kachel selbst füllt dadurch ihren Rasterplatz sichtbar besser aus.
|
|
var cellInset = .4f * k;
|
|
var cellCornerRadius = Math.Min(cellWidth, cellHeight) * .2f;
|
|
var commands = new List<DrawingCommand>();
|
|
var contentWidthDesign = designWidth * k;
|
|
var y = 0f;
|
|
// Keine eigene "Anwesenheit"-Überschrift mehr - die Monatsnamen im Raster machen bereits
|
|
// klar, worum es geht. Der Name bleibt klein und in hellem Grau: er dient nur der Kontrolle,
|
|
// dass der Kalender zum richtigen Kind gehört, nicht als gestalterischer Blickfang - der
|
|
// eingesparte Platz kommt stattdessen den Tageskacheln zugute (siehe designHeight oben).
|
|
commands.Add(new DrawStringEx(0, y, 5 * k, contentWidthDesign, studentName,
|
|
DrawingTextAlignment.AlignLeft, 6.5f * finalScale, Color: "#9CA3AF"));
|
|
y += 5 * k + .5f * k;
|
|
var weekdays = new[] { "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So" };
|
|
var gridStartY = y;
|
|
for (var monthIndex = 0; monthIndex < monthCount; monthIndex++)
|
|
{
|
|
var current = first.AddMonths(monthIndex);
|
|
var days = ClassTeacherOverviewViewModel.BuildCompactMonthDays(current, absences, registerEntries,
|
|
DateOnly.FromDateTime(DateTime.Today), studentName);
|
|
var offset = ((int)current.DayOfWeek + 6) % 7;
|
|
var xOffset = monthIndex * (monthWidth + monthGapX);
|
|
var monthY = gridStartY;
|
|
commands.Add(new DrawStringEx(xOffset, monthY, 11 * k, monthWidth,
|
|
current.ToString("MMMM yyyy", CultureInfo.GetCultureInfo("de-DE")),
|
|
DrawingTextAlignment.AlignLeft, 9 * finalScale, Color: "#374151", Bold: true));
|
|
monthY += 11 * k;
|
|
for (var column = 0; column < 7; column++)
|
|
commands.Add(new DrawStringEx(xOffset + column * cellWidth, monthY, 9 * k,
|
|
cellWidth, weekdays[column], DrawingTextAlignment.AlignCenter, 7 * finalScale, Color: "#6B7280",
|
|
Bold: true));
|
|
monthY += 9 * k;
|
|
|
|
foreach (var day in days)
|
|
{
|
|
var index = offset + day.Date.Day - 1;
|
|
var column = index % 7;
|
|
var row = index / 7;
|
|
var x = xOffset + column * cellWidth;
|
|
var cellY = monthY + row * cellHeight;
|
|
// Sa/So (Spalten 5/6, siehe weekdays-Array oben) ohne Eintrag dezent ausgrauen - das
|
|
// sind normalerweise keine Schultage, "keine Fehlzeit" dort ist keine Leistung und
|
|
// soll nicht optisch gleichwertig neben echten schulfreien-Tage-Einträgen stehen. Ein
|
|
// tatsächlicher Eintrag an einem Wochenendtag (day.HasSignal) bleibt unverändert
|
|
// farbig, da er echte Information trägt.
|
|
var isMutedWeekend = column is 5 or 6 && !day.HasSignal;
|
|
commands.Add(new DrawRoundedRectangle(x + cellInset, cellY, cellWidth - 2 * cellInset,
|
|
cellHeight - cellInset, cellCornerRadius, isMutedWeekend ? "#E5E7EB" : "#D1D5DB", .35f,
|
|
day.HasSignal ? day.SignalColorHex : isMutedWeekend ? "#F3F4F6" : "#FFFFFF"));
|
|
commands.Add(new DrawStringEx(x, cellY + k, cellHeight - 2 * k, cellWidth,
|
|
day.HasSignal ? day.SignalCode : day.DayNumber, DrawingTextAlignment.AlignCenter, 7 * finalScale,
|
|
Color: day.HasSignal ? "#FFFFFF" : isMutedWeekend ? "#B0B7C0" : "#374151", Bold: day.HasSignal));
|
|
}
|
|
}
|
|
y = gridStartY + 11 * k + 9 * k + maxWeeks * cellHeight + designTrailingGapY * k;
|
|
|
|
commands.Add(new DrawStringEx(0, y, 8 * k, contentWidthDesign,
|
|
"U/E/A ganztägig · T teilweise · G gegangen · V verspätet · ! Klassenbuch",
|
|
DrawingTextAlignment.AlignLeft, 6.5f * finalScale, Color: "#6B7280"));
|
|
return new DrawingValue(commands, y + 9 * k);
|
|
}
|
|
}
|
|
|
|
/// <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, float contentWidth = 170,
|
|
float millimeterScale = 1)
|
|
{
|
|
var scale = options.Size switch
|
|
{
|
|
AttendanceCalendarSize.Small => .75f,
|
|
AttendanceCalendarSize.Large => 1f,
|
|
_ => .88f,
|
|
};
|
|
// contentWidth ist die reale DRAWBOX/FLOWDRAWBOX-Breite (bereits in der Koordinaten-Einheit
|
|
// der Vorlage) und wird für die Spalten NICHT zusätzlich mit millimeterScale multipliziert -
|
|
// die Spaltenpositionen sind Anteile von contentWidth (proportional, unitless), nicht eigene
|
|
// mm-Maße. Nur echte mm-Maße wie Zeilenhöhe/Abstände (Y-Richtung) brauchen millimeterScale,
|
|
// um bei einer in "pt" deklarierten Vorlage die richtige physische Größe zu erreichen (siehe
|
|
// StudentAttendanceCalendarDrawingBuilder). Eine frühere Version multiplizierte auch die
|
|
// Spalten mit millimeterScale, wodurch sie bei "pt"-Vorlagen weit über contentWidth
|
|
// hinausliefen (z.B. Status-Spalte bei x≈323 in einer nur 170 Einheiten breiten Box).
|
|
var rowHeight = 12 * scale * millimeterScale;
|
|
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>();
|
|
var y = 0f;
|
|
// Kein eigener "Fehltage"-Titel mehr - analog zum Anwesenheitskalender dient der (klein und
|
|
// hellgrau gehaltene) Name nur der Kontrolle, dass die Liste zum richtigen Kind gehört.
|
|
commands.Add(new DrawStringEx(0, y, 5 * millimeterScale, contentWidth, studentName,
|
|
DrawingTextAlignment.AlignLeft, 6.5f * scale, Color: "#9CA3AF"));
|
|
y += 5 * millimeterScale + .5f * millimeterScale;
|
|
// Spaltenbreiten als Anteile von contentWidth (gegen eine 170-Einheiten-Referenz austariert),
|
|
// damit auch die Datenzeilen (nicht nur die kurzen Kopfzeilen-Labels) hineinpassen - "Datum"
|
|
// als Kopfzeile ist kürzer als "dd.MM.yyyy" und wurde bei 31 zu schmal bemessen, wodurch das
|
|
// Datum am rechten Rand abgeschnitten wurde.
|
|
const float referenceWidth = 170f;
|
|
var dateColumnX = contentWidth * (2f / referenceWidth);
|
|
var dateColumnWidth = contentWidth * (38f / referenceWidth);
|
|
var extentColumnX = contentWidth * (44f / referenceWidth);
|
|
var extentColumnWidth = contentWidth * (66f / referenceWidth);
|
|
var statusColumnX = contentWidth * (114f / referenceWidth);
|
|
var statusColumnWidth = contentWidth * (54f / referenceWidth);
|
|
|
|
(string Extent, string Status, string StatusColor) Format(ClassAbsenceDaySummaryRow row)
|
|
{
|
|
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;
|
|
return (extent, status, row.IsUnexcused ? "#C62828" : "#2E7D32");
|
|
}
|
|
|
|
void DrawRows(float columnX, float columnWidth, float dColumnX, float dColumnWidth, float eColumnX,
|
|
float eColumnWidth, float sColumnX, float sColumnWidth,
|
|
IReadOnlyList<ClassAbsenceDaySummaryRow> columnRows)
|
|
{
|
|
var rowY = y;
|
|
commands.Add(new DrawRectangle(columnX, rowY, columnWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6"));
|
|
commands.Add(new DrawStringEx(dColumnX, rowY + scale * millimeterScale, rowHeight - scale * millimeterScale,
|
|
dColumnWidth, "Datum", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
|
commands.Add(new DrawStringEx(eColumnX, rowY + scale * millimeterScale, rowHeight - scale * millimeterScale,
|
|
eColumnWidth, "Umfang", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
|
commands.Add(new DrawStringEx(sColumnX, rowY + scale * millimeterScale, rowHeight - scale * millimeterScale,
|
|
sColumnWidth, "Status", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
|
rowY += rowHeight;
|
|
for (var index = 0; index < columnRows.Count; index++)
|
|
{
|
|
var row = columnRows[index];
|
|
var (extent, status, statusColor) = Format(row);
|
|
var fill = index % 2 == 0 ? "#FFFFFF" : "#F9FAFB";
|
|
commands.Add(new DrawRectangle(columnX, rowY, columnWidth, rowHeight, "#E5E7EB", .3f, fill));
|
|
commands.Add(new DrawStringEx(dColumnX, rowY + scale * millimeterScale,
|
|
rowHeight - scale * millimeterScale, dColumnWidth,
|
|
row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
|
|
commands.Add(new DrawStringEx(eColumnX, rowY + scale * millimeterScale,
|
|
rowHeight - scale * millimeterScale, eColumnWidth,
|
|
extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
|
|
commands.Add(new DrawStringEx(sColumnX, rowY + scale * millimeterScale,
|
|
rowHeight - scale * millimeterScale, sColumnWidth,
|
|
status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor, Bold: row.IsUnexcused));
|
|
rowY += rowHeight;
|
|
}
|
|
}
|
|
|
|
if (rows.Count == 0)
|
|
{
|
|
DrawRows(0, contentWidth, dateColumnX, dateColumnWidth, extentColumnX, extentColumnWidth,
|
|
statusColumnX, statusColumnWidth, []);
|
|
y += rowHeight;
|
|
commands.Add(new DrawStringEx(dateColumnX, y + 2 * scale * millimeterScale, rowHeight,
|
|
contentWidth - 2 * dateColumnX, "Keine Fehltage im gewählten Zeitraum",
|
|
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280", Italic: true));
|
|
y += rowHeight + 3 * scale * millimeterScale;
|
|
}
|
|
else
|
|
{
|
|
// Ab genug Zeilen zweispaltig (chronologisch: erste Hälfte links, Rest rechts) - spart bei
|
|
// längeren Listen spürbar Höhe. Nur wenn die Box auch wirklich breit genug für zwei volle
|
|
// Spalten ist (>= 2x die Einspalten-Referenzbreite + Zwischenraum) - sonst blieben Datum/
|
|
// Umfang/Status in jeder Hälfte zu schmal und würden abgeschnitten, siehe die reale
|
|
// Vorlage dieses Nutzers, deren FLOWDRAWBOX für diesen Platzhalter aktuell nur 170 statt
|
|
// z.B. 400+ Einheiten breit ist (dort bleibt es also bewusst einspaltig).
|
|
const float columnGapDesign = 8f;
|
|
var minTwoColumnWidth = 2 * referenceWidth + columnGapDesign;
|
|
if (rows.Count >= 4 && contentWidth >= minTwoColumnWidth)
|
|
{
|
|
var columnGap = contentWidth * (columnGapDesign / referenceWidth);
|
|
var columnWidth = (contentWidth - columnGap) / 2;
|
|
var leftCount = (rows.Count + 1) / 2;
|
|
var left = rows.Take(leftCount).ToList();
|
|
var right = rows.Skip(leftCount).ToList();
|
|
// Spaltenanteile innerhalb EINER Hälfte, gegen eine 81-Einheiten-Referenz austariert:
|
|
// 81 + columnGapDesign(8) + 81 = 170 = referenceWidth, dieselbe Gesamtbreite wie das
|
|
// einspaltige Layout, nur eben in zwei Hälften aufgeteilt.
|
|
const float halfReferenceWidth = 81f;
|
|
(float X, float Width) Fraction(float leftEdge, float x, float width) =>
|
|
(leftEdge + columnWidth * (x / halfReferenceWidth), columnWidth * (width / halfReferenceWidth));
|
|
var (leftDateX, leftDateWidth) = Fraction(0, 1, 19);
|
|
var (leftExtentX, leftExtentWidth) = Fraction(0, 21, 32);
|
|
var (leftStatusX, leftStatusWidth) = Fraction(0, 54, 26);
|
|
var rightEdge = columnWidth + columnGap;
|
|
var (rightDateX, rightDateWidth) = Fraction(rightEdge, 1, 19);
|
|
var (rightExtentX, rightExtentWidth) = Fraction(rightEdge, 21, 32);
|
|
var (rightStatusX, rightStatusWidth) = Fraction(rightEdge, 54, 26);
|
|
DrawRows(0, columnWidth, leftDateX, leftDateWidth, leftExtentX, leftExtentWidth, leftStatusX,
|
|
leftStatusWidth, left);
|
|
DrawRows(rightEdge, columnWidth, rightDateX, rightDateWidth, rightExtentX, rightExtentWidth,
|
|
rightStatusX, rightStatusWidth, right);
|
|
y += rowHeight * (1 + Math.Max(left.Count, right.Count));
|
|
}
|
|
else
|
|
{
|
|
DrawRows(0, contentWidth, dateColumnX, dateColumnWidth, extentColumnX, extentColumnWidth,
|
|
statusColumnX, statusColumnWidth, rows);
|
|
y += rowHeight * (1 + rows.Count);
|
|
}
|
|
}
|
|
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(
|
|
WebUntisSettingsService settings,
|
|
IUntisAbsenceCacheRepository absenceCache,
|
|
IUntisClassRegisterCacheRepository registerCache,
|
|
IUntisStudentRosterCacheRepository rosterCache)
|
|
{
|
|
public DrawingValue Build(Student student, DateOnly month) =>
|
|
Build(student, new AttendanceCalendarOptions(month, 1));
|
|
|
|
public DrawingValue Build(Student student, AttendanceCalendarOptions options, float contentWidth = 170,
|
|
float millimeterScale = 1, float contentHeight = float.PositiveInfinity)
|
|
{
|
|
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 matchName = $"{student.FirstName} {student.LastName}";
|
|
var rosterName = rosterCache.GetByClass(className)
|
|
.FirstOrDefault(r => UntisNameMatching.NamesMatch(r.DisplayName, matchName))?.DisplayName
|
|
?? matchName;
|
|
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))
|
|
.ToList();
|
|
var register = registerCache.GetByClassAndRange(className, start, end)
|
|
.Select(e => new UntisForeignClassRegisterEventDto(e.ClassName, e.Date, e.Subject, e.StudentName,
|
|
e.TeacherUsername, e.CategoryName, e.CategoryGroup, e.Text))
|
|
.ToList();
|
|
return StudentAttendanceCalendarDrawingBuilder.Build(rosterName, options,
|
|
ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences), register, contentWidth, millimeterScale,
|
|
contentHeight);
|
|
}
|
|
|
|
public DrawingValue BuildAbsenceDayList(Student student, AttendanceCalendarOptions options, float contentWidth = 170,
|
|
float millimeterScale = 1)
|
|
{
|
|
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 matchName = $"{student.FirstName} {student.LastName}";
|
|
var rosterName = rosterCache.GetByClass(className)
|
|
.FirstOrDefault(r => UntisNameMatching.NamesMatch(r.DisplayName, matchName))?.DisplayName
|
|
?? matchName;
|
|
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), contentWidth, millimeterScale);
|
|
}
|
|
}
|