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
@@ -127,6 +127,32 @@ public sealed class ClassTeacherViewModelsTests
Assert.True(large.ContentHeight > small.ContentHeight); Assert.True(large.ContentHeight > small.ContentHeight);
} }
[Fact]
public void ElternbriefFehltage_ListetDatumUmfangUndEntschuldigungsstatus()
{
var options = new AttendanceCalendarOptions(new DateOnly(2026, 9, 1), 1);
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 3), "Müller Ada", 1, 6, 270,
["Deu"], [1, 2, 3, 4, 5, 6], ["entsch."], ["Krank"], null, null, true),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 8), "Müller Ada", 1, 2, 90,
["Mathe"], [1, 2], ["nicht entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 9), "Schmidt Ben", 2, 4, 180,
["Eng"], [1, 2, 3, 4], ["entsch."], ["Krank"], null, null, true),
};
var drawing = StudentAbsenceDayListDrawingBuilder.Build("Ada Müller", options, absences);
var labels = drawing.Commands.OfType<DrawStringEx>().Select(c => c.Text).ToList();
Assert.Contains("03.09.2026", labels);
Assert.Contains("Ganzer Fehltag", labels);
Assert.Contains("08.09.2026", labels);
Assert.Contains("Fehlzeit · 2 Std.", labels);
Assert.Contains("Entschuldigt", labels);
Assert.Contains("Unentschuldigt", labels);
Assert.DoesNotContain("09.09.2026", labels);
}
[Fact] [Fact]
public void GroupByStudentAndDay_FasstFehlstundenProSchuelerUndTagZusammen() public void GroupByStudentAndDay_FasstFehlstundenProSchuelerUndTagZusammen()
{ {
@@ -76,6 +76,23 @@ 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 AdvancedContent_Fehlzeitenliste_AktiviertKonfigurationsschritt()
{
var store = StoreWithTemplate(new PlaceholderDefinition(
StudentAbsenceDayListDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true));
var drawing = StudentAbsenceDayListDrawingBuilder.Build("Lena Beispiel",
new AttendanceCalendarOptions(new DateOnly(2026, 9, 1), 1), []);
var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store,
new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]),
attendanceCalendarFactory: null, absenceDayListFactory: _ => drawing);
Assert.False(vm.UsesAttendanceCalendar);
Assert.True(vm.UsesAbsenceDayList);
Assert.True(vm.UsesAttendanceAdvancedContent);
Assert.True(vm.CanGenerate);
}
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([]));
+3 -1
View File
@@ -20,7 +20,9 @@ public static class LetterDialogs
App.Services.GetRequiredService<ITemplateRenderer>(), App.Services.GetRequiredService<ITemplateRenderer>(),
App.Services.GetRequiredService<IGroupMembershipRepository>(), App.Services.GetRequiredService<IGroupMembershipRepository>(),
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>()
.BuildAbsenceDayList(student, options));
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))
@@ -15,7 +15,8 @@ public static class LetterPlaceholderBuilder
{ {
public static Dictionary<string, PlaceholderValue> BuildStandardValues( public static Dictionary<string, PlaceholderValue> BuildStandardValues(
Student student, Contact? contact, LearningGroup? group, DateOnly date, 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 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))); 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 ?? ""), ["Contact.City"] = new TextValue(contact?.City ?? ""), ["Letter.Salutation"] = new TextValue(contact?.LetterSalutation ?? ""),
["Group.Name"] = new TextValue(group?.Name ?? ""), ["SchoolYear"] = new TextValue(group?.SchoolYear ?? ""), ["Group.Name"] = new TextValue(group?.Name ?? ""), ["SchoolYear"] = new TextValue(group?.SchoolYear ?? ""),
[StudentAttendanceCalendarDrawingBuilder.PlaceholderName] = attendanceCalendar ?? new DrawingValue([], 0), [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 date = letterDate ?? DateOnly.FromDateTime(DateTime.Today);
var values = LetterPlaceholderBuilder.BuildStandardValues(student, contact, group, date, letterText, 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 ?? []) foreach (var (name, raw) in extraValues ?? [])
{ {
var definition = loaded.Manifest.Placeholders.FirstOrDefault(p => p.Name == name); 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 /// <summary>Liest ausschließlich den lokalen WebUntis-Cache. Das Öffnen oder Rendern eines
/// Elternbriefs löst dadurch keinen unerwarteten Netzwerkabruf aus.</summary> /// Elternbriefs löst dadurch keinen unerwarteten Netzwerkabruf aus.</summary>
public sealed class StudentAttendanceCalendarService( public sealed class StudentAttendanceCalendarService(
@@ -126,4 +207,24 @@ public sealed class StudentAttendanceCalendarService(
return StudentAttendanceCalendarDrawingBuilder.Build(rosterName, options, return StudentAttendanceCalendarDrawingBuilder.Build(rosterName, options,
ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences), register); 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));
}
} }
@@ -13,6 +13,7 @@ public partial class CreateLetterDialogViewModel : ObservableObject
private readonly TemplateStore _templates; private readonly TemplateStore _templates;
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 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);
@@ -25,6 +26,7 @@ public partial class CreateLetterDialogViewModel : ObservableObject
[ObservableProperty] private string _generationError = ""; [ObservableProperty] private string _generationError = "";
[ObservableProperty] private bool _canGenerate; [ObservableProperty] private bool _canGenerate;
[ObservableProperty] private bool _usesAttendanceCalendar; [ObservableProperty] private bool _usesAttendanceCalendar;
[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";
@@ -36,15 +38,18 @@ public partial class CreateLetterDialogViewModel : ObservableObject
public bool HasIssues => Issues.Count > 0; public bool HasIssues => Issues.Count > 0;
public bool HasNoTemplates => Templates.Count == 0; public bool HasNoTemplates => Templates.Count == 0;
public bool HasNoContacts => Contacts.Count == 0; public bool HasNoContacts => Contacts.Count == 0;
public bool UsesAttendanceAdvancedContent => UsesAttendanceCalendar || UsesAbsenceDayList;
public string SuggestedFileName => SanitizeFileName( public string SuggestedFileName => SanitizeFileName(
$"{SelectedTemplate?.Name ?? "Elternbrief"}_{_student.LastName}_{_student.FirstName}.pdf"); $"{SelectedTemplate?.Name ?? "Elternbrief"}_{_student.LastName}_{_student.FirstName}.pdf");
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)
{ {
_student = student; _templates = templates; _renderer = renderer; _student = student; _templates = templates; _renderer = renderer;
_attendanceCalendarFactory = attendanceCalendarFactory; _attendanceCalendarFactory = attendanceCalendarFactory;
_absenceDayListFactory = absenceDayListFactory;
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))
@@ -56,7 +61,11 @@ public partial class CreateLetterDialogViewModel : ObservableObject
partial void OnSelectedTemplateChanged(LetterTemplateChoice? value) partial void OnSelectedTemplateChanged(LetterTemplateChoice? value)
{ {
OnPropertyChanged(nameof(SuggestedFileName)); OnPropertyChanged(nameof(SuggestedFileName));
UsesAttendanceCalendar = TemplateUsesAttendanceCalendar(value); UsesAttendanceCalendar = TemplateUsesPlaceholder(value,
StudentAttendanceCalendarDrawingBuilder.PlaceholderName);
UsesAbsenceDayList = TemplateUsesPlaceholder(value,
StudentAbsenceDayListDrawingBuilder.PlaceholderName);
OnPropertyChanged(nameof(UsesAttendanceAdvancedContent));
AttendanceCalendarConfigured = false; AttendanceCalendarConfigured = false;
ResetAttendanceCalendarOptions(); ResetAttendanceCalendarOptions();
RefreshValidation(); RefreshValidation();
@@ -111,7 +120,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject
private IReadOnlyDictionary<string, PlaceholderValue> BuildValues() => LetterPlaceholderBuilder.BuildStandardValues( private IReadOnlyDictionary<string, PlaceholderValue> BuildValues() => LetterPlaceholderBuilder.BuildStandardValues(
_student, SelectedContact?.Model, SelectedGroup?.Model, _student, SelectedContact?.Model, SelectedGroup?.Model,
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);
public AttendanceCalendarOptions GetAttendanceCalendarOptions() => _attendanceCalendarOptions; public AttendanceCalendarOptions GetAttendanceCalendarOptions() => _attendanceCalendarOptions;
@@ -131,14 +141,14 @@ public partial class CreateLetterDialogViewModel : ObservableObject
AttendanceCalendarSummary = FormatAttendanceCalendarSummary(_attendanceCalendarOptions); AttendanceCalendarSummary = FormatAttendanceCalendarSummary(_attendanceCalendarOptions);
} }
private bool TemplateUsesAttendanceCalendar(LetterTemplateChoice? choice) private bool TemplateUsesPlaceholder(LetterTemplateChoice? choice, string placeholderName)
{ {
if (choice is null) return false; if (choice is null) return false;
try try
{ {
var loaded = _templates.Load(choice.Model); var loaded = _templates.Load(choice.Model);
return UsesPlaceholder(loaded.Layout) || return UsesPlaceholder(loaded.Layout, placeholderName) ||
(loaded.ContinuationLayout is not null && UsesPlaceholder(loaded.ContinuationLayout)); (loaded.ContinuationLayout is not null && UsesPlaceholder(loaded.ContinuationLayout, placeholderName));
} }
catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException) catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException)
{ {
@@ -146,11 +156,11 @@ public partial class CreateLetterDialogViewModel : ObservableObject
} }
} }
private static bool UsesPlaceholder(TemplateLayout layout) => private static bool UsesPlaceholder(TemplateLayout layout, string placeholderName) =>
layout.Elements.Concat(layout.PageTemplates.SelectMany(p => p.Elements)) layout.Elements.Concat(layout.PageTemplates.SelectMany(p => p.Elements))
.Concat(layout.ContentFlows.SelectMany(f => f.Elements)) .Concat(layout.ContentFlows.SelectMany(f => f.Elements))
.Any(e => e is DrawBoxElement { Placeholder: StudentAttendanceCalendarDrawingBuilder.PlaceholderName } .Any(e => e is DrawBoxElement draw && draw.Placeholder == placeholderName ||
or FlowDrawBoxElement { Placeholder: StudentAttendanceCalendarDrawingBuilder.PlaceholderName }); e is FlowDrawBoxElement flow && flow.Placeholder == placeholderName);
private static string FormatAttendanceCalendarSummary(AttendanceCalendarOptions options) private static string FormatAttendanceCalendarSummary(AttendanceCalendarOptions options)
{ {
@@ -4,12 +4,12 @@
xmlns:conv="clr-namespace:LehrerApp.Desktop.Converters" xmlns:conv="clr-namespace:LehrerApp.Desktop.Converters"
x:Class="LehrerApp.Desktop.Views.Students.AttendanceCalendarConfigurationDialog" x:Class="LehrerApp.Desktop.Views.Students.AttendanceCalendarConfigurationDialog"
x:DataType="vm:AttendanceCalendarConfigurationViewModel" x:DataType="vm:AttendanceCalendarConfigurationViewModel"
Title="Anwesenheitskalender konfigurieren" Width="440" Height="360" Title="Anwesenheitsdaten konfigurieren" Width="440" Height="360"
CanResize="False" WindowStartupLocation="CenterOwner"> CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24,20"> <Grid RowDefinitions="*,Auto" Margin="24,20">
<StackPanel Spacing="16"> <StackPanel Spacing="16">
<TextBlock Text="Anwesenheitskalender" Classes="dialogtitle"/> <TextBlock Text="Anwesenheitsdaten" Classes="dialogtitle"/>
<TextBlock Text="Diese Angaben steuern den dynamischen Kalender im Elternbrief. Der Starttag wird automatisch auf den ersten Tag des gewählten Monats gesetzt." <TextBlock Text="Diese Angaben steuern Kalender und Fehlzeitenliste im Elternbrief. Der Starttag wird automatisch auf den ersten Tag des gewählten Monats gesetzt."
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/> FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Erster Monat" FontSize="12" Opacity="0.7"/> <TextBlock Text="Erster Monat" FontSize="12" Opacity="0.7"/>
@@ -44,11 +44,11 @@
<CalendarDatePicker SelectedDate="{Binding LetterDate, Converter={x:Static conv:DateTimeOffsetToDateTimeConverter.Instance}}" HorizontalAlignment="Stretch"/> <CalendarDatePicker SelectedDate="{Binding LetterDate, Converter={x:Static conv:DateTimeOffsetToDateTimeConverter.Instance}}" HorizontalAlignment="Stretch"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Border IsVisible="{Binding UsesAttendanceCalendar}" 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"> <Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="3"> <StackPanel Spacing="3">
<TextBlock Text="Anwesenheitskalender im Brief" FontWeight="SemiBold"/> <TextBlock Text="Anwesenheitsdaten im Brief" FontWeight="SemiBold"/>
<TextBlock Text="{Binding AttendanceCalendarSummary}" FontSize="12" Opacity="0.65"/> <TextBlock Text="{Binding AttendanceCalendarSummary}" FontSize="12" Opacity="0.65"/>
</StackPanel> </StackPanel>
<Button Grid.Column="1" Content="Konfigurieren …" Click="OnConfigureAttendanceCalendar" <Button Grid.Column="1" Content="Konfigurieren …" Click="OnConfigureAttendanceCalendar"
@@ -12,7 +12,7 @@ public partial class CreateLetterDialog : Window
private async void OnGenerate(object? sender, RoutedEventArgs e) private async void OnGenerate(object? sender, RoutedEventArgs e)
{ {
if (DataContext is not CreateLetterDialogViewModel vm) return; if (DataContext is not CreateLetterDialogViewModel vm) return;
if (vm.UsesAttendanceCalendar && !vm.AttendanceCalendarConfigured && if (vm.UsesAttendanceAdvancedContent && !vm.AttendanceCalendarConfigured &&
!await ConfigureAttendanceCalendar(vm)) return; !await ConfigureAttendanceCalendar(vm)) return;
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{ {
@@ -62,6 +62,7 @@ public partial class DesignerViewModel : ObservableObject
new("Brieftext", PlaceholderType.Multiline, true, "hiermit informieren wir Sie über einen wichtigen Termin.\n\nMit freundlichen Grüßen"), new("Brieftext", PlaceholderType.Multiline, true, "hiermit informieren wir Sie über einen wichtigen Termin.\n\nMit freundlichen Grüßen"),
new("LehrerName", PlaceholderType.Text, true, "M. Mustermann"), new("LehrerName", PlaceholderType.Text, true, "M. Mustermann"),
new("Student.AttendanceCalendar", PlaceholderType.Drawing, false, ""), new("Student.AttendanceCalendar", PlaceholderType.Drawing, false, ""),
new("Student.AbsenceDays", PlaceholderType.Drawing, false, ""),
]; ];
public ObservableCollection<DesignerMetadata> MetadataItems { get; } = public ObservableCollection<DesignerMetadata> MetadataItems { get; } =
[ [