fix: Fehlzeitenkalender in Templates
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-09-16 00:50:10 +02:00
parent 6e57bf407d
commit 6dccf96039
7 changed files with 166 additions and 42 deletions
@@ -56,7 +56,7 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
} }
[Fact] [Fact]
public void AdvancedContent_Anwesenheitskalender_WirdAlsDrawingGerendert() public async Task AdvancedContent_Anwesenheitskalender_WirdAlsDrawingGerendert()
{ {
var store = StoreWithTemplate(new PlaceholderDefinition( var store = StoreWithTemplate(new PlaceholderDefinition(
StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true)); StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true));
@@ -66,7 +66,7 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
var output = Path.Combine(_directory, "Kalender.pdf"); var output = Path.Combine(_directory, "Kalender.pdf");
Assert.True(vm.UsesAttendanceCalendar); Assert.True(vm.UsesAttendanceCalendar);
vm.SetAttendanceCalendarOptions(new AttendanceCalendarOptions( await vm.SetAttendanceCalendarOptionsAsync(new AttendanceCalendarOptions(
new DateOnly(2026, 8, 19), 3, AttendanceCalendarSize.Large)); new DateOnly(2026, 8, 19), 3, AttendanceCalendarSize.Large));
Assert.True(vm.AttendanceCalendarConfigured); Assert.True(vm.AttendanceCalendarConfigured);
Assert.Contains("August 2026", vm.AttendanceCalendarSummary); Assert.Contains("August 2026", vm.AttendanceCalendarSummary);
@@ -76,6 +76,45 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(File.ReadAllBytes(output), 0, 4)); Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(File.ReadAllBytes(output), 0, 4));
} }
[Fact]
public async Task AttendanceKalenderKonfigurieren_LoestGezieltenDatenAbrufFuerDenZeitraumAus()
{
var store = StoreWithTemplate(new PlaceholderDefinition(
StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true));
AttendanceCalendarOptions? requested = null;
var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store,
new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]),
_ => new DrawingValue([], 0), attendanceDataRefresher: (options, _) =>
{ requested = options; return Task.CompletedTask; });
await vm.SetAttendanceCalendarOptionsAsync(new AttendanceCalendarOptions(
new DateOnly(2026, 9, 1), 2, AttendanceCalendarSize.Medium));
Assert.NotNull(requested);
Assert.Equal(new DateOnly(2026, 9, 1), requested!.NormalizedStartMonth);
Assert.Equal(2, requested.NormalizedMonthCount);
Assert.False(vm.IsRefreshingAttendanceData);
Assert.Equal("", vm.AttendanceRefreshError);
}
[Fact]
public async Task AttendanceKalenderKonfigurieren_ZeigtFehlerBeiFehlgeschlagenemAbrufAnStattZuBlockieren()
{
var store = StoreWithTemplate(new PlaceholderDefinition(
StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true));
var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store,
new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]),
_ => new DrawingValue([], 0),
attendanceDataRefresher: (_, _) => throw new WebUntisIntegrationException("Keine Verbindung."));
await vm.SetAttendanceCalendarOptionsAsync(new AttendanceCalendarOptions(
new DateOnly(2026, 9, 1), 1, AttendanceCalendarSize.Medium));
Assert.True(vm.AttendanceCalendarConfigured);
Assert.Contains("Keine Verbindung.", vm.AttendanceRefreshError);
Assert.False(vm.IsRefreshingAttendanceData);
}
[Fact] [Fact]
public void AdvancedContent_Fehlzeitenliste_AktiviertKonfigurationsschritt() public void AdvancedContent_Fehlzeitenliste_AktiviertKonfigurationsschritt()
{ {
@@ -116,6 +155,42 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(File.ReadAllBytes(output), 0, 4)); Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(File.ReadAllBytes(output), 0, 4));
} }
[Fact]
public void StudentName_WirdAutomatischMitVollemNamenBefuellt()
{
var store = StoreWithTemplate(new PlaceholderDefinition("Anrede", PlaceholderType.Text, true),
new PlaceholderDefinition("Datum", PlaceholderType.Date, true),
new PlaceholderDefinition("Brieftext", PlaceholderType.Multiline, true),
new PlaceholderDefinition("Student.Name", PlaceholderType.Text, true));
var vm = Build(StudentWithContact("Sehr geehrte Frau Muster,"), store);
vm.LetterText = "Dies ist der Inhalt.";
Assert.Empty(vm.CustomPlaceholders);
Assert.True(vm.CanGenerate);
var output = Path.Combine(_directory, "StudentName.pdf");
Assert.True(vm.Generate(output));
}
[Fact]
public void Anrede_WirdAusKontaktVorbelegtUndBleibtEditierbar()
{
var store = StoreWithTemplate(new PlaceholderDefinition("Anrede", PlaceholderType.Text, true),
new PlaceholderDefinition("Datum", PlaceholderType.Date, true),
new PlaceholderDefinition("Brieftext", PlaceholderType.Multiline, true));
var vm = Build(StudentWithContact(null), store);
vm.LetterText = "Dies ist der Inhalt.";
Assert.Equal("", vm.Anrede);
Assert.False(vm.CanGenerate);
Assert.Contains(vm.Issues, i => i.Message.Contains("Anrede", StringComparison.OrdinalIgnoreCase));
vm.Anrede = "Sehr geehrte Familie Beispiel,";
var output = Path.Combine(_directory, "AnredeManuell.pdf");
Assert.True(vm.CanGenerate);
Assert.True(vm.Generate(output));
}
private CreateLetterDialogViewModel Build(Student student, TemplateStore store) => private CreateLetterDialogViewModel Build(Student student, TemplateStore store) =>
new(student, store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([])); new(student, store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]));
+18 -1
View File
@@ -22,10 +22,27 @@ public static class LetterDialogs
App.Services.GetRequiredService<IGroupRepository>(), 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>() options => App.Services.GetRequiredService<StudentAttendanceCalendarService>()
.BuildAbsenceDayList(student, options)); .BuildAbsenceDayList(student, options),
RefreshAttendanceDataAsync);
var dialog = new CreateLetterDialog { DataContext = vm }; var dialog = new CreateLetterDialog { DataContext = vm };
var path = await dialog.ShowDialog<string?>(owner); var path = await dialog.ShowDialog<string?>(owner);
if (!string.IsNullOrEmpty(path) && File.Exists(path)) if (!string.IsNullOrEmpty(path) && File.Exists(path))
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
} }
/// <summary>Holt genau den im Anwesenheitskalender-Dialog gewählten Zeitraum gezielt per
/// WebUntis nach (ausgelöst durch den expliziten "Konfigurieren…"-Klick, kein Hintergrundabruf
/// beim bloßen Öffnen des Briefdialogs). Damit sieht <see cref="StudentAttendanceCalendarService"/>
/// anschließend frische Daten im lokalen Cache, statt stillschweigend "keine Fehltage" zu
/// melden, nur weil die Klassenlehrer-Übersicht für diesen Zeitraum noch nie geöffnet wurde.</summary>
private static async Task RefreshAttendanceDataAsync(AttendanceCalendarOptions options, CancellationToken token)
{
var className = App.Services.GetRequiredService<WebUntisSettingsService>().HomeroomClassName;
if (string.IsNullOrWhiteSpace(className)) return;
var cache = App.Services.GetRequiredService<UntisReportCacheService>();
var start = options.NormalizedStartMonth;
var end = start.AddMonths(options.NormalizedMonthCount).AddDays(-1);
await cache.GetAbsencesAsync(className, start, end, token: token);
await cache.GetClassRegisterEventsAsync(className, start, end, token: token);
}
} }
@@ -26,6 +26,7 @@ public static class LetterPlaceholderBuilder
["Empfaenger"] = new TextValue(contact?.Name ?? ""), ["Anrede"] = new TextValue(contact?.LetterSalutation ?? ""), ["Empfaenger"] = new TextValue(contact?.Name ?? ""), ["Anrede"] = new TextValue(contact?.LetterSalutation ?? ""),
["Brieftext"] = new MultilineValue(letterText), ["LehrerName"] = new TextValue(teacherName), ["Brieftext"] = new MultilineValue(letterText), ["LehrerName"] = new TextValue(teacherName),
["Student.FirstName"] = new TextValue(student.FirstName), ["Student.LastName"] = new TextValue(student.LastName), ["Student.FirstName"] = new TextValue(student.FirstName), ["Student.LastName"] = new TextValue(student.LastName),
["Student.Name"] = new TextValue(student.FullName),
["Contact.Name"] = new TextValue(contact?.Name ?? ""), ["Contact.Address"] = new MultilineValue(address), ["Contact.Name"] = new TextValue(contact?.Name ?? ""), ["Contact.Address"] = new MultilineValue(address),
["Contact.Street"] = new TextValue(contact?.Street ?? ""), ["Contact.PostalCode"] = new TextValue(contact?.PostalCode ?? ""), ["Contact.Street"] = new TextValue(contact?.Street ?? ""), ["Contact.PostalCode"] = new TextValue(contact?.PostalCode ?? ""),
["Contact.City"] = new TextValue(contact?.City ?? ""), ["Letter.Salutation"] = new TextValue(contact?.LetterSalutation ?? ""), ["Contact.City"] = new TextValue(contact?.City ?? ""), ["Letter.Salutation"] = new TextValue(contact?.LetterSalutation ?? ""),
@@ -43,15 +43,15 @@ public static class StudentAttendanceCalendarDrawingBuilder
var monthGap = 6 * scale; var monthGap = 6 * scale;
var first = options.NormalizedStartMonth; var first = options.NormalizedStartMonth;
var monthCount = options.NormalizedMonthCount; var monthCount = options.NormalizedMonthCount;
var commands = new List<DrawingCommand> var commands = new List<DrawingCommand>();
{ var y = 0f;
new DrawStringEx(0, 0, 7 * scale, contentWidth, "Anwesenheit", commands.Add(new DrawStringEx(0, y, 14 * scale, contentWidth, "Anwesenheit",
DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true), DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true));
new DrawStringEx(0, 7 * scale, 6 * scale, contentWidth, studentName, y += 14 * scale;
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"), commands.Add(new DrawStringEx(0, y, 10 * scale, contentWidth, studentName,
}; DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"));
y += 10 * scale + 2 * scale;
var weekdays = new[] { "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So" }; var weekdays = new[] { "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So" };
var y = 15 * scale;
for (var monthIndex = 0; monthIndex < monthCount; monthIndex++) for (var monthIndex = 0; monthIndex < monthCount; monthIndex++)
{ {
var current = first.AddMonths(monthIndex); var current = first.AddMonths(monthIndex);
@@ -59,14 +59,14 @@ public static class StudentAttendanceCalendarDrawingBuilder
DateOnly.FromDateTime(DateTime.Today), studentName); DateOnly.FromDateTime(DateTime.Today), studentName);
var offset = ((int)current.DayOfWeek + 6) % 7; var offset = ((int)current.DayOfWeek + 6) % 7;
var weeks = (int)Math.Ceiling((offset + days.Count) / 7d); var weeks = (int)Math.Ceiling((offset + days.Count) / 7d);
commands.Add(new DrawStringEx(0, y, 7 * scale, contentWidth, commands.Add(new DrawStringEx(0, y, 11 * scale, contentWidth,
current.ToString("MMMM yyyy", CultureInfo.GetCultureInfo("de-DE")), current.ToString("MMMM yyyy", CultureInfo.GetCultureInfo("de-DE")),
DrawingTextAlignment.AlignLeft, 9 * scale, Color: "#374151", Bold: true)); DrawingTextAlignment.AlignLeft, 9 * scale, Color: "#374151", Bold: true));
y += 7 * scale; y += 11 * scale;
for (var column = 0; column < 7; column++) for (var column = 0; column < 7; column++)
commands.Add(new DrawStringEx(column * cellWidth, y, 6 * scale, cellWidth, weekdays[column], commands.Add(new DrawStringEx(column * cellWidth, y, 9 * scale, cellWidth, weekdays[column],
DrawingTextAlignment.AlignCenter, 7 * scale, Color: "#6B7280", Bold: true)); DrawingTextAlignment.AlignCenter, 7 * scale, Color: "#6B7280", Bold: true));
y += 7 * scale; y += 9 * scale;
foreach (var day in days) foreach (var day in days)
{ {
@@ -115,20 +115,20 @@ public static class StudentAbsenceDayListDrawingBuilder
UntisNameMatching.NamesMatch(a.StudentName, studentName)) UntisNameMatching.NamesMatch(a.StudentName, studentName))
.OrderBy(a => a.Date) .OrderBy(a => a.Date)
.ToList(); .ToList();
var commands = new List<DrawingCommand> var commands = new List<DrawingCommand>();
{ var y = 0f;
new DrawStringEx(0, 0, 7 * scale, contentWidth, "Fehltage", commands.Add(new DrawStringEx(0, y, 14 * scale, contentWidth, "Fehltage",
DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true), DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true));
new DrawStringEx(0, 7 * scale, 6 * scale, contentWidth, studentName, y += 14 * scale;
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"), commands.Add(new DrawStringEx(0, y, 10 * scale, contentWidth, studentName,
}; DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"));
var y = 16 * scale; y += 10 * scale + 2 * scale;
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6")); 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", commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - scale, 31 * scale, "Datum",
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - 2 * scale, 75 * scale, "Umfang", commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - scale, 75 * scale, "Umfang",
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - 2 * scale, 55 * scale, "Status", commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - scale, 55 * scale, "Status",
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
y += rowHeight; y += rowHeight;
@@ -157,12 +157,12 @@ public static class StudentAbsenceDayListDrawingBuilder
: row.FriendlyStatusLabel; : row.FriendlyStatusLabel;
var statusColor = row.IsUnexcused ? "#C62828" : "#2E7D32"; var statusColor = row.IsUnexcused ? "#C62828" : "#2E7D32";
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#E5E7EB", .3f, fill)); commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#E5E7EB", .3f, fill));
commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - 2 * scale, 31 * scale, commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - scale, 31 * scale,
row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale, row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale,
Color: "#374151")); Color: "#374151"));
commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - 2 * scale, 75 * scale, commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - scale, 75 * scale,
extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151")); extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - 2 * scale, 55 * scale, commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - scale, 55 * scale,
status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor, status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor,
Bold: row.IsUnexcused)); Bold: row.IsUnexcused));
y += rowHeight; y += rowHeight;
@@ -14,6 +14,7 @@ public partial class CreateLetterDialogViewModel : ObservableObject
private readonly ITemplateRenderer _renderer; private readonly ITemplateRenderer _renderer;
private readonly Func<AttendanceCalendarOptions, DrawingValue>? _attendanceCalendarFactory; private readonly Func<AttendanceCalendarOptions, DrawingValue>? _attendanceCalendarFactory;
private readonly Func<AttendanceCalendarOptions, DrawingValue>? _absenceDayListFactory; private readonly Func<AttendanceCalendarOptions, DrawingValue>? _absenceDayListFactory;
private readonly Func<AttendanceCalendarOptions, CancellationToken, Task>? _attendanceDataRefresher;
private AttendanceCalendarOptions _attendanceCalendarOptions = new( private AttendanceCalendarOptions _attendanceCalendarOptions = new(
new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1), 1); new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1), 1);
@@ -21,6 +22,7 @@ public partial class CreateLetterDialogViewModel : ObservableObject
[ObservableProperty] private LetterContactChoice? _selectedContact; [ObservableProperty] private LetterContactChoice? _selectedContact;
[ObservableProperty] private LetterGroupChoice? _selectedGroup; [ObservableProperty] private LetterGroupChoice? _selectedGroup;
[ObservableProperty] private DateTimeOffset? _letterDate = DateTimeOffset.Now; [ObservableProperty] private DateTimeOffset? _letterDate = DateTimeOffset.Now;
[ObservableProperty] private string _anrede = "";
[ObservableProperty] private string _letterText = ""; [ObservableProperty] private string _letterText = "";
[ObservableProperty] private string _teacherName = ""; [ObservableProperty] private string _teacherName = "";
[ObservableProperty] private string _generationError = ""; [ObservableProperty] private string _generationError = "";
@@ -29,6 +31,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject
[ObservableProperty] private bool _usesAbsenceDayList; [ObservableProperty] private bool _usesAbsenceDayList;
[ObservableProperty] private bool _attendanceCalendarConfigured; [ObservableProperty] private bool _attendanceCalendarConfigured;
[ObservableProperty] private string _attendanceCalendarSummary = "1 Monat · Standardgröße"; [ObservableProperty] private string _attendanceCalendarSummary = "1 Monat · Standardgröße";
[ObservableProperty] private bool _isRefreshingAttendanceData;
[ObservableProperty] private string _attendanceRefreshError = "";
public string StudentName => _student.FullName; public string StudentName => _student.FullName;
public ObservableCollection<LetterTemplateChoice> Templates { get; } = []; public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
@@ -47,11 +51,13 @@ public partial class CreateLetterDialogViewModel : ObservableObject
public CreateLetterDialogViewModel(Student student, TemplateStore templates, ITemplateRenderer renderer, public CreateLetterDialogViewModel(Student student, TemplateStore templates, ITemplateRenderer renderer,
IGroupMembershipRepository memberships, IGroupRepository groups, IGroupMembershipRepository memberships, IGroupRepository groups,
Func<AttendanceCalendarOptions, DrawingValue>? attendanceCalendarFactory = null, Func<AttendanceCalendarOptions, DrawingValue>? attendanceCalendarFactory = null,
Func<AttendanceCalendarOptions, DrawingValue>? absenceDayListFactory = null) Func<AttendanceCalendarOptions, DrawingValue>? absenceDayListFactory = null,
Func<AttendanceCalendarOptions, CancellationToken, Task>? attendanceDataRefresher = null)
{ {
_student = student; _templates = templates; _renderer = renderer; _student = student; _templates = templates; _renderer = renderer;
_attendanceCalendarFactory = attendanceCalendarFactory; _attendanceCalendarFactory = attendanceCalendarFactory;
_absenceDayListFactory = absenceDayListFactory; _absenceDayListFactory = absenceDayListFactory;
_attendanceDataRefresher = attendanceDataRefresher;
foreach (var template in templates.GetTemplates()) Templates.Add(new(template)); foreach (var template in templates.GetTemplates()) Templates.Add(new(template));
foreach (var contact in student.Contacts.Where(c => !c.InvalidSince.HasValue).OrderBy(c => c.Name)) Contacts.Add(new(contact)); foreach (var contact in student.Contacts.Where(c => !c.InvalidSince.HasValue).OrderBy(c => c.Name)) Contacts.Add(new(contact));
foreach (var membership in memberships.GetByStudent(student.Id)) foreach (var membership in memberships.GetByStudent(student.Id))
@@ -73,7 +79,12 @@ public partial class CreateLetterDialogViewModel : ObservableObject
RebuildCustomPlaceholders(value); RebuildCustomPlaceholders(value);
RefreshValidation(); RefreshValidation();
} }
partial void OnSelectedContactChanged(LetterContactChoice? value) => RefreshValidation(); partial void OnSelectedContactChanged(LetterContactChoice? value)
{
Anrede = value?.Model.LetterSalutation ?? "";
RefreshValidation();
}
partial void OnAnredeChanged(string value) => RefreshValidation();
partial void OnSelectedGroupChanged(LetterGroupChoice? value) => RefreshValidation(); partial void OnSelectedGroupChanged(LetterGroupChoice? value) => RefreshValidation();
partial void OnLetterDateChanged(DateTimeOffset? value) partial void OnLetterDateChanged(DateTimeOffset? value)
{ {
@@ -127,6 +138,7 @@ public partial class CreateLetterDialogViewModel : ObservableObject
DateOnly.FromDateTime((LetterDate ?? DateTimeOffset.Now).LocalDateTime), LetterText, TeacherName, DateOnly.FromDateTime((LetterDate ?? DateTimeOffset.Now).LocalDateTime), LetterText, TeacherName,
UsesAttendanceCalendar ? _attendanceCalendarFactory?.Invoke(_attendanceCalendarOptions) : null, UsesAttendanceCalendar ? _attendanceCalendarFactory?.Invoke(_attendanceCalendarOptions) : null,
UsesAbsenceDayList ? _absenceDayListFactory?.Invoke(_attendanceCalendarOptions) : null); UsesAbsenceDayList ? _absenceDayListFactory?.Invoke(_attendanceCalendarOptions) : null);
values["Anrede"] = new TextValue(Anrede); values["Letter.Salutation"] = new TextValue(Anrede);
foreach (var custom in CustomPlaceholders) values[custom.Name] = custom.ToPlaceholderValue(); foreach (var custom in CustomPlaceholders) values[custom.Name] = custom.ToPlaceholderValue();
return values; return values;
} }
@@ -159,19 +171,28 @@ public partial class CreateLetterDialogViewModel : ObservableObject
private static readonly HashSet<string> StandardPlaceholderNames = new(StringComparer.Ordinal) private static readonly HashSet<string> StandardPlaceholderNames = new(StringComparer.Ordinal)
{ {
"Datum", "CurrentDate", "Empfaenger", "Anrede", "Brieftext", "LehrerName", "Datum", "CurrentDate", "Empfaenger", "Anrede", "Brieftext", "LehrerName",
"Student.FirstName", "Student.LastName", "Contact.Name", "Contact.Address", "Contact.Street", "Student.FirstName", "Student.LastName", "Student.Name", "Contact.Name", "Contact.Address", "Contact.Street",
"Contact.PostalCode", "Contact.City", "Letter.Salutation", "Group.Name", "SchoolYear", "Contact.PostalCode", "Contact.City", "Letter.Salutation", "Group.Name", "SchoolYear",
StudentAttendanceCalendarDrawingBuilder.PlaceholderName, StudentAbsenceDayListDrawingBuilder.PlaceholderName, StudentAttendanceCalendarDrawingBuilder.PlaceholderName, StudentAbsenceDayListDrawingBuilder.PlaceholderName,
}; };
public AttendanceCalendarOptions GetAttendanceCalendarOptions() => _attendanceCalendarOptions; public AttendanceCalendarOptions GetAttendanceCalendarOptions() => _attendanceCalendarOptions;
public void SetAttendanceCalendarOptions(AttendanceCalendarOptions options) public async Task SetAttendanceCalendarOptionsAsync(AttendanceCalendarOptions options, CancellationToken token = default)
{ {
_attendanceCalendarOptions = options with { StartMonth = options.NormalizedStartMonth, _attendanceCalendarOptions = options with { StartMonth = options.NormalizedStartMonth,
MonthCount = options.NormalizedMonthCount }; MonthCount = options.NormalizedMonthCount };
AttendanceCalendarConfigured = true; AttendanceCalendarConfigured = true;
AttendanceCalendarSummary = FormatAttendanceCalendarSummary(_attendanceCalendarOptions); AttendanceCalendarSummary = FormatAttendanceCalendarSummary(_attendanceCalendarOptions);
AttendanceRefreshError = "";
if (_attendanceDataRefresher is not null)
{
IsRefreshingAttendanceData = true;
try { await _attendanceDataRefresher(_attendanceCalendarOptions, token); }
catch (WebUntisIntegrationException ex)
{ AttendanceRefreshError = $"WebUntis-Daten konnten nicht aktualisiert werden: {ex.Message}"; }
finally { IsRefreshingAttendanceData = false; }
}
RefreshValidation(); RefreshValidation();
} }
@@ -32,6 +32,10 @@
<TextBlock Text="Kein aktueller Kontakt vorhanden." Classes="emptyhint" <TextBlock Text="Kein aktueller Kontakt vorhanden." Classes="emptyhint"
IsVisible="{Binding HasNoContacts}"/> IsVisible="{Binding HasNoContacts}"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Anrede *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Anrede}" PlaceholderText="z.B. Sehr geehrte Frau Mustermann,"/>
</StackPanel>
<Grid ColumnDefinitions="*,12,*"> <Grid ColumnDefinitions="*,12,*">
<StackPanel Grid.Column="0" Spacing="4"> <StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Lerngruppe" FontSize="12" Opacity="0.7"/> <TextBlock Text="Lerngruppe" FontSize="12" Opacity="0.7"/>
@@ -46,14 +50,20 @@
</Grid> </Grid>
<Border IsVisible="{Binding UsesAttendanceAdvancedContent}" BorderBrush="{DynamicResource AppCardBorderBrush}" <Border IsVisible="{Binding UsesAttendanceAdvancedContent}" BorderBrush="{DynamicResource AppCardBorderBrush}"
BorderThickness="1" CornerRadius="6" Padding="12"> BorderThickness="1" CornerRadius="6" Padding="12">
<Grid ColumnDefinitions="*,Auto"> <StackPanel Spacing="6">
<StackPanel Spacing="3"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Anwesenheitsdaten im Brief" FontWeight="SemiBold"/> <StackPanel Spacing="3">
<TextBlock Text="{Binding AttendanceCalendarSummary}" FontSize="12" Opacity="0.65"/> <TextBlock Text="Anwesenheitsdaten im Brief" FontWeight="SemiBold"/>
</StackPanel> <TextBlock Text="{Binding AttendanceCalendarSummary}" FontSize="12" Opacity="0.65"/>
<Button Grid.Column="1" Content="Konfigurieren …" Click="OnConfigureAttendanceCalendar" </StackPanel>
VerticalAlignment="Center"/> <Button Grid.Column="1" Content="Konfigurieren …" Click="OnConfigureAttendanceCalendar"
</Grid> IsEnabled="{Binding !IsRefreshingAttendanceData}" VerticalAlignment="Center"/>
</Grid>
<TextBlock Text="Aktualisiere WebUntis-Daten für den gewählten Zeitraum …" FontSize="12"
Foreground="#6B7280" IsVisible="{Binding IsRefreshingAttendanceData}"/>
<TextBlock Text="{Binding AttendanceRefreshError}" FontSize="12" Foreground="#D97706" TextWrapping="Wrap"
IsVisible="{Binding AttendanceRefreshError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</Border> </Border>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Brieftext" FontSize="12" Opacity="0.7"/> <TextBlock Text="Brieftext" FontSize="12" Opacity="0.7"/>
@@ -35,7 +35,7 @@ public partial class CreateLetterDialog : Window
var configVm = new AttendanceCalendarConfigurationViewModel(vm.GetAttendanceCalendarOptions()); var configVm = new AttendanceCalendarConfigurationViewModel(vm.GetAttendanceCalendarOptions());
var dialog = new AttendanceCalendarConfigurationDialog { DataContext = configVm }; var dialog = new AttendanceCalendarConfigurationDialog { DataContext = configVm };
if (!await dialog.ShowDialog<bool>(this)) return false; if (!await dialog.ShowDialog<bool>(this)) return false;
vm.SetAttendanceCalendarOptions(configVm.BuildResult()); await vm.SetAttendanceCalendarOptionsAsync(configVm.BuildResult());
return true; return true;
} }