feat: Fehlzeiten Calender

This commit is contained in:
2026-09-15 10:02:33 +02:00
parent 70dc78904c
commit 1671796844
16 changed files with 477 additions and 16 deletions
@@ -2,6 +2,7 @@ using LehrerApp.Core.Models;
using LehrerApp.Core.Services; using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services; using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.ClassTeacher; using LehrerApp.Desktop.ViewModels.ClassTeacher;
using LehrerApp.Templating;
using Xunit; using Xunit;
namespace LehrerApp.Desktop.Tests; namespace LehrerApp.Desktop.Tests;
@@ -71,6 +72,61 @@ public sealed class ClassTeacherViewModelsTests
Assert.False(days[4].HasSignal); // Hausaufgaben bleiben im Widget bewusst ausgeblendet Assert.False(days[4].HasSignal); // Hausaufgaben bleiben im Widget bewusst ausgeblendet
} }
[Fact]
public void Kompaktkalender_KannAufEinzelnenSchuelerEingeschraenktWerden()
{
var month = new DateOnly(2026, 9, 1);
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 3), "Müller Ada", 1, 2, 90,
["Deu"], [1, 2], ["nicht entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 4), "Schmidt Ben", 2, 2, 90,
["Mathe"], [1, 2], ["entsch."], ["Absent"], null, null, false),
};
var days = ClassTeacherOverviewViewModel.BuildCompactMonthDays(month, absences, [], month,
"Ada Müller");
Assert.Equal("U", days[2].SignalCode);
Assert.False(days[3].HasSignal);
}
[Fact]
public void ElternbriefKalender_LiefertPortablenDrawingPlatzhalterFuerSchueler()
{
var month = new DateOnly(2026, 9, 1);
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 3), "Müller Ada", 1, 2, 90,
["Deu"], [1, 2], ["nicht entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 4), "Schmidt Ben", 2, 2, 90,
["Mathe"], [1, 2], ["entsch."], ["Absent"], null, null, false),
};
var drawing = StudentAttendanceCalendarDrawingBuilder.Build("Ada Müller", month, absences, []);
Assert.True(drawing.ContentHeight > 0);
Assert.Contains(drawing.Commands.OfType<DrawStringEx>(), c => c.Text == "Ada Müller");
Assert.Contains(drawing.Commands.OfType<DrawStringEx>(), c => c.Text == "U");
Assert.DoesNotContain(drawing.Commands.OfType<DrawStringEx>(), c => c.Text == "E");
}
[Fact]
public void ElternbriefKalender_UnterstuetztEinBisDreiMonateUndGroessen()
{
var start = new DateOnly(2026, 9, 18);
var small = StudentAttendanceCalendarDrawingBuilder.Build("Ada Müller",
new AttendanceCalendarOptions(start, 3, AttendanceCalendarSize.Small), [], []);
var large = StudentAttendanceCalendarDrawingBuilder.Build("Ada Müller",
new AttendanceCalendarOptions(start, 3, AttendanceCalendarSize.Large), [], []);
var labels = large.Commands.OfType<DrawStringEx>().Select(c => c.Text).ToList();
Assert.Contains("September 2026", labels);
Assert.Contains("Oktober 2026", labels);
Assert.Contains("November 2026", labels);
Assert.True(large.ContentHeight > small.ContentHeight);
}
[Fact] [Fact]
public void GroupByStudentAndDay_FasstFehlstundenProSchuelerUndTagZusammen() public void GroupByStudentAndDay_FasstFehlstundenProSchuelerUndTagZusammen()
{ {
@@ -1,4 +1,5 @@
using LehrerApp.Core.Models; using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Templating; using LehrerApp.Templating;
using Xunit; using Xunit;
@@ -54,6 +55,27 @@ 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_Anwesenheitskalender_WirdAlsDrawingGerendert()
{
var store = StoreWithTemplate(new PlaceholderDefinition(
StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true));
var drawing = StudentAttendanceCalendarDrawingBuilder.Build("Lena Beispiel", new DateOnly(2026, 9, 1), [], []);
var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store,
new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), _ => drawing);
var output = Path.Combine(_directory, "Kalender.pdf");
Assert.True(vm.UsesAttendanceCalendar);
vm.SetAttendanceCalendarOptions(new AttendanceCalendarOptions(
new DateOnly(2026, 8, 19), 3, AttendanceCalendarSize.Large));
Assert.True(vm.AttendanceCalendarConfigured);
Assert.Contains("August 2026", vm.AttendanceCalendarSummary);
Assert.Contains("3 Monate", vm.AttendanceCalendarSummary);
Assert.Contains("Groß", vm.AttendanceCalendarSummary);
Assert.True(vm.Generate(output));
Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(File.ReadAllBytes(output), 0, 4));
}
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([]));
@@ -65,8 +87,18 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
var y = 20; var y = 20;
foreach (var definition in definitions) foreach (var definition in definitions)
{ {
var element = definition.Type == PlaceholderType.Multiline ? "TEXTBOX" : "TEXT"; var element = definition.Type switch
lines.Add(element == "TEXTBOX" ? $"TEXTBOX 20 {y} 170 80 ${definition.Name}" : $"TEXT 20 {y} ${definition.Name}"); {
PlaceholderType.Multiline => "TEXTBOX",
PlaceholderType.Drawing => "DRAWBOX",
_ => "TEXT",
};
lines.Add(element switch
{
"TEXTBOX" => $"TEXTBOX 20 {y} 170 80 ${definition.Name}",
"DRAWBOX" => $"DRAWBOX 20 {y} 170 80 ${definition.Name}",
_ => $"TEXT 20 {y} ${definition.Name}",
});
y += 20; y += 20;
} }
TemplatePackage.Create(source, manifest, string.Join('\n', lines), new Dictionary<string, byte[]>()); TemplatePackage.Create(source, manifest, string.Join('\n', lines), new Dictionary<string, byte[]>());
+1
View File
@@ -229,6 +229,7 @@ public static class AppBootstrapper
services.AddSingleton<LessonPlanTools>(); services.AddSingleton<LessonPlanTools>();
services.AddSingleton<GroupMembershipTools>(); services.AddSingleton<GroupMembershipTools>();
services.AddSingleton<LetterTemplateTools>(); services.AddSingleton<LetterTemplateTools>();
services.AddSingleton<StudentAttendanceCalendarService>();
services.AddSingleton<CompetencyTools>(); services.AddSingleton<CompetencyTools>();
services.AddSingleton<GroupTools>(); services.AddSingleton<GroupTools>();
services.AddSingleton<UntisComparisonTools>(); services.AddSingleton<UntisComparisonTools>();
+2 -1
View File
@@ -19,7 +19,8 @@ public static class LetterDialogs
App.Services.GetRequiredService<TemplateStore>(), App.Services.GetRequiredService<TemplateStore>(),
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));
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,7 @@ 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) string letterText, string teacherName, DrawingValue? attendanceCalendar = 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)));
@@ -29,6 +29,7 @@ public static class LetterPlaceholderBuilder
["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 ?? ""),
["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),
}; };
} }
@@ -36,6 +37,7 @@ public static class LetterPlaceholderBuilder
{ {
TextValue x => string.IsNullOrWhiteSpace(x.Value), TextValue x => string.IsNullOrWhiteSpace(x.Value),
MultilineValue x => string.IsNullOrWhiteSpace(x.Value), MultilineValue x => string.IsNullOrWhiteSpace(x.Value),
DrawingValue x => x.ContentHeight <= 0 || x.Commands.Count == 0,
_ => false, _ => false,
}; };
} }
@@ -19,7 +19,8 @@ namespace LehrerApp.Desktop.Services.Mcp.Tools;
/// tatsächlich nützlichen Fall ab: eine bestehende, vom Nutzer gestaltete Vorlage mit Werten füllen.</summary> /// tatsächlich nützlichen Fall ab: eine bestehende, vom Nutzer gestaltete Vorlage mit Werten füllen.</summary>
public class LetterTemplateTools( public class LetterTemplateTools(
TemplateStore templates, ITemplateLoader loader, ITemplateRenderer renderer, TemplateStore templates, ITemplateLoader loader, ITemplateRenderer renderer,
IStudentRepository students, IGroupRepository groups) IStudentRepository students, IGroupRepository groups,
StudentAttendanceCalendarService? attendanceCalendars = null)
{ {
[Description("Listet importierte Elternbrief-Vorlagen mit ihren deklarierten Platzhaltern (Name, Typ, Pflichtfeld, konstant).")] [Description("Listet importierte Elternbrief-Vorlagen mit ihren deklarierten Platzhaltern (Name, Typ, Pflichtfeld, konstant).")]
public List<LetterTemplateDto> ListLetterTemplates() public List<LetterTemplateDto> ListLetterTemplates()
@@ -73,7 +74,8 @@ public class LetterTemplateTools(
var group = groupId is { } gid ? groups.GetById(gid) : null; var group = groupId is { } gid ? groups.GetById(gid) : null;
var date = letterDate ?? DateOnly.FromDateTime(DateTime.Today); var date = letterDate ?? DateOnly.FromDateTime(DateTime.Today);
var values = LetterPlaceholderBuilder.BuildStandardValues(student, contact, group, date, letterText, teacherName); var values = LetterPlaceholderBuilder.BuildStandardValues(student, contact, group, date, letterText,
teacherName, attendanceCalendars?.Build(student, date));
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);
@@ -0,0 +1,129 @@
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)
{
const float width = 170;
var scale = options.Size switch
{
AttendanceCalendarSize.Small => .7f,
AttendanceCalendarSize.Large => 1f,
_ => .85f,
};
var contentWidth = width * scale;
var cellWidth = contentWidth / 7;
var cellHeight = 10 * scale;
var monthGap = 6 * scale;
var first = options.NormalizedStartMonth;
var monthCount = options.NormalizedMonthCount;
var commands = new List<DrawingCommand>
{
new DrawStringEx(0, 0, 7 * scale, contentWidth, "Anwesenheit",
DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true),
new DrawStringEx(0, 7 * scale, 6 * scale, contentWidth, studentName,
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"),
};
var weekdays = new[] { "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So" };
var y = 15 * scale;
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 weeks = (int)Math.Ceiling((offset + days.Count) / 7d);
commands.Add(new DrawStringEx(0, y, 7 * scale, contentWidth,
current.ToString("MMMM yyyy", CultureInfo.GetCultureInfo("de-DE")),
DrawingTextAlignment.AlignLeft, 9 * scale, Color: "#374151", Bold: true));
y += 7 * scale;
for (var column = 0; column < 7; column++)
commands.Add(new DrawStringEx(column * cellWidth, y, 6 * scale, cellWidth, weekdays[column],
DrawingTextAlignment.AlignCenter, 7 * scale, Color: "#6B7280", Bold: true));
y += 7 * scale;
foreach (var day in days)
{
var index = offset + day.Date.Day - 1;
var column = index % 7;
var row = index / 7;
var x = column * cellWidth;
var cellY = y + row * cellHeight;
commands.Add(new DrawRectangle(x + scale, cellY, cellWidth - 2 * scale, cellHeight - scale,
"#D1D5DB", .35f, day.HasSignal ? day.SignalColorHex : "#FFFFFF"));
commands.Add(new DrawStringEx(x, cellY + scale, cellHeight - 2 * scale, cellWidth,
day.HasSignal ? day.SignalCode : day.DayNumber, DrawingTextAlignment.AlignCenter, 7 * scale,
Color: day.HasSignal ? "#FFFFFF" : "#374151", Bold: day.HasSignal));
}
y += weeks * cellHeight + monthGap;
}
commands.Add(new DrawStringEx(0, y, 8 * scale, contentWidth,
"U unentschuldigt · A abwesend · V verspätet · E entschuldigt · ! Klassenbuch",
DrawingTextAlignment.AlignLeft, 6.5f * scale, Color: "#6B7280"));
return new DrawingValue(commands, y + 9 * scale);
}
}
/// <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)
{
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))
.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);
}
}
@@ -18,6 +18,10 @@ public enum ClassTeacherStatusKind { Ok, Info, Warning, Danger }
/// Schlagwort gezielt dämpfen kann, siehe <see cref="ClassTeacherRosterRow.MatchDomains"/>.</summary> /// Schlagwort gezielt dämpfen kann, siehe <see cref="ClassTeacherRosterRow.MatchDomains"/>.</summary>
public enum VorgangScoreDomain { Attendance, Lateness, Classbook } public enum VorgangScoreDomain { Attendance, Lateness, Classbook }
/// <summary>Auswahlbereich des kompakten Monatskalenders. Ein leerer Schülername steht für die
/// bisherige Klassen-Gesamtansicht.</summary>
public sealed record ClassTeacherCalendarScope(string? StudentName, string DisplayName);
public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday, public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday,
string? AbsenceTooltip, bool HasRecentClassRegisterEntry) string? AbsenceTooltip, bool HasRecentClassRegisterEntry)
{ {
@@ -424,6 +428,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
public ObservableCollection<ClassTeacherPatternNotice> PatternNotices { get; } = []; public ObservableCollection<ClassTeacherPatternNotice> PatternNotices { get; } = [];
public ObservableCollection<ClassTeacherOpenExcuseRow> OpenExcuses { get; } = []; public ObservableCollection<ClassTeacherOpenExcuseRow> OpenExcuses { get; } = [];
public ObservableCollection<ClassTeacherCompactCalendarDay> CompactMonthDays { get; } = []; public ObservableCollection<ClassTeacherCompactCalendarDay> CompactMonthDays { get; } = [];
public ObservableCollection<ClassTeacherCalendarScope> CalendarScopes { get; } = [];
[ObservableProperty] private string? _homeroomClassName; [ObservableProperty] private string? _homeroomClassName;
[ObservableProperty] private int _activeTabIndex; [ObservableProperty] private int _activeTabIndex;
@@ -453,6 +458,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
/// den "Klassenbuch öffnen"-Button gehängt statt in einer eigenen Kennzahlkarte. /// den "Klassenbuch öffnen"-Button gehängt statt in einer eigenen Kennzahlkarte.
[ObservableProperty] private int _ownDocumentationFollowUpCount; [ObservableProperty] private int _ownDocumentationFollowUpCount;
[ObservableProperty] private int _ownDocumentationCriticalCount; [ObservableProperty] private int _ownDocumentationCriticalCount;
[ObservableProperty] private ClassTeacherCalendarScope? _selectedCalendarScope;
// Zuletzt per Load() geholte WebUntis-Rohdaten, für RefreshFromLocalDataOnly() - damit ein // Zuletzt per Load() geholte WebUntis-Rohdaten, für RefreshFromLocalDataOnly() - damit ein
// eingehendes Sync-Ereignis die Ansicht neu aufbauen kann, ohne selbst WebUntis anzufragen. // eingehendes Sync-Ereignis die Ansicht neu aufbauen kann, ohne selbst WebUntis anzufragen.
@@ -501,8 +507,17 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
public double UnexcusedAbsenceFraction => StudentCount == 0 ? 0 : (double)UnexcusedAbsenceCount / StudentCount; public double UnexcusedAbsenceFraction => StudentCount == 0 ? 0 : (double)UnexcusedAbsenceCount / StudentCount;
public string DayOverviewTooltip => $"{PresentCount} anwesend · {LateCount} verspätet · " + public string DayOverviewTooltip => $"{PresentCount} anwesend · {LateCount} verspätet · " +
$"{ExcusedAbsenceCount} entschuldigt · {UnexcusedAbsenceCount} unentschuldigt"; $"{ExcusedAbsenceCount} entschuldigt · {UnexcusedAbsenceCount} unentschuldigt";
public string CompactMonthLabel => DateOnly.FromDateTime(DateTime.Today).ToString("MMMM yyyy", public string CompactMonthLabel
{
get
{
var month = DateOnly.FromDateTime(DateTime.Today).ToString("MMMM yyyy",
System.Globalization.CultureInfo.GetCultureInfo("de-DE")); System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
return SelectedCalendarScope?.StudentName is { Length: > 0 }
? $"{month} · {SelectedCalendarScope.DisplayName}"
: $"{month} · gesamte Klasse";
}
}
public Func<Task>? OnNavigateToSettings { get; set; } public Func<Task>? OnNavigateToSettings { get; set; }
public Func<Task>? OnNavigateToWorkload { get; set; } public Func<Task>? OnNavigateToWorkload { get; set; }
@@ -534,6 +549,11 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
partial void OnOpenExcuseOverflowCountChanged(int value) => OnPropertyChanged(nameof(HasOpenExcuseOverflow)); partial void OnOpenExcuseOverflowCountChanged(int value) => OnPropertyChanged(nameof(HasOpenExcuseOverflow));
partial void OnOwnDocumentationFollowUpCountChanged(int value) => OnPropertyChanged(nameof(HasOwnDocumentationAlerts)); partial void OnOwnDocumentationFollowUpCountChanged(int value) => OnPropertyChanged(nameof(HasOwnDocumentationAlerts));
partial void OnOwnDocumentationCriticalCountChanged(int value) => OnPropertyChanged(nameof(HasOwnDocumentationAlerts)); partial void OnOwnDocumentationCriticalCountChanged(int value) => OnPropertyChanged(nameof(HasOwnDocumentationAlerts));
partial void OnSelectedCalendarScopeChanged(ClassTeacherCalendarScope? value)
{
OnPropertyChanged(nameof(CompactMonthLabel));
if (CalendarScopes.Count > 0) RebuildCompactMonthFromCachedData();
}
partial void OnSearchTextChanged(string value) => ApplyRosterFilter(); partial void OnSearchTextChanged(string value) => ApplyRosterFilter();
partial void OnSelectedRosterFilterChanged(int value) partial void OnSelectedRosterFilterChanged(int value)
{ {
@@ -548,7 +568,8 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
{ {
ActiveTabIndex = 0; ActiveTabIndex = 0;
Roster.Clear(); PrimaryRoster.Clear(); SecondaryRoster.Clear(); TrendDays.Clear(); PatternNotices.Clear(); Roster.Clear(); PrimaryRoster.Clear(); SecondaryRoster.Clear(); TrendDays.Clear(); PatternNotices.Clear();
OpenExcuses.Clear(); CompactMonthDays.Clear(); OpenExcuseOverflowCount = 0; OpenExcuses.Clear(); CompactMonthDays.Clear(); CalendarScopes.Clear(); SelectedCalendarScope = null;
OpenExcuseOverflowCount = 0;
} }
// Wird beim Navigieren auf diese Seite aufgerufen statt LoadCommand: baut nur den lokalen // Wird beim Navigieren auf diese Seite aufgerufen statt LoadCommand: baut nur den lokalen
@@ -678,6 +699,12 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
holidayWeekdaysExcluded, termStart, classRegisterEvents, holidayWeekdaysExcluded, termStart, classRegisterEvents,
_patternScoreSettings.Load(), closedVorgaengeByNameKey)) Roster.Add(row); _patternScoreSettings.Load(), closedVorgaengeByNameKey)) Roster.Add(row);
CalendarScopes.Add(new ClassTeacherCalendarScope(null, "Gesamte Klasse"));
foreach (var student in students.OrderBy(s => s.DisplayName))
CalendarScopes.Add(new ClassTeacherCalendarScope(student.DisplayName,
ClassTeacherOverviewViewModel.DisplayStudentName(student.DisplayName)));
SelectedCalendarScope = CalendarScopes[0];
StudentCount = Roster.Count; StudentCount = Roster.Count;
TodayAlertCount = Roster.Count(r => r.HasAbsenceToday); TodayAlertCount = Roster.Count(r => r.HasAbsenceToday);
TodayUnexcusedCount = Roster.Count(r => r.IsUnexcused); TodayUnexcusedCount = Roster.Count(r => r.IsUnexcused);
@@ -724,7 +751,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
private void OpenMonthlyCalendar() private void OpenMonthlyCalendar()
{ {
var today = DateOnly.FromDateTime(DateTime.Today); var today = DateOnly.FromDateTime(DateTime.Today);
DetailsTab.StudentFilter = ""; DetailsTab.StudentFilter = SelectedCalendarScope?.StudentName ?? "";
DetailsTab.CalendarMonth = new DateOnly(today.Year, today.Month, 1); DetailsTab.CalendarMonth = new DateOnly(today.Year, today.Month, 1);
ActiveTabIndex = 2; ActiveTabIndex = 2;
DetailsTab.ShowCalendarCommand.Execute(null); DetailsTab.ShowCalendarCommand.Execute(null);
@@ -735,15 +762,29 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
{ {
CompactMonthDays.Clear(); CompactMonthDays.Clear();
foreach (var day in BuildCompactMonthDays(new DateOnly(today.Year, today.Month, 1), foreach (var day in BuildCompactMonthDays(new DateOnly(today.Year, today.Month, 1),
absences, registerEntries, today)) absences, registerEntries, today, SelectedCalendarScope?.StudentName))
CompactMonthDays.Add(day); CompactMonthDays.Add(day);
} }
private void RebuildCompactMonthFromCachedData()
{
if (_lastAbsences is null || _lastClassRegisterEvents is null) return;
var today = DateOnly.FromDateTime(DateTime.Today);
BuildCompactMonth(ClassAbsenceDaySummaryRow.GroupByStudentAndDay(_lastAbsences),
_lastClassRegisterEvents, today);
}
public static IReadOnlyList<ClassTeacherCompactCalendarDay> BuildCompactMonthDays(DateOnly month, public static IReadOnlyList<ClassTeacherCompactCalendarDay> BuildCompactMonthDays(DateOnly month,
IReadOnlyList<ClassAbsenceDaySummaryRow> absences, IReadOnlyList<ClassAbsenceDaySummaryRow> absences,
IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries, DateOnly today) IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries, DateOnly today,
string? studentName = null)
{ {
var first = new DateOnly(month.Year, month.Month, 1); var first = new DateOnly(month.Year, month.Month, 1);
if (!string.IsNullOrWhiteSpace(studentName))
{
absences = absences.Where(a => UntisNameMatching.NamesMatch(a.StudentName, studentName)).ToList();
registerEntries = registerEntries.Where(e => UntisNameMatching.NamesMatch(e.StudentName, studentName)).ToList();
}
var registerRows = registerEntries var registerRows = registerEntries
.Select(e => (Entry: e, Valid: TryDate(e.Date, out var date), Date: date)) .Select(e => (Entry: e, Valid: TryDate(e.Date, out var date), Date: date))
.Where(x => x.Valid && x.Date.Year == first.Year && x.Date.Month == first.Month) .Where(x => x.Valid && x.Date.Year == first.Year && x.Date.Month == first.Month)
@@ -768,6 +809,12 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
return result; return result;
} }
internal static string DisplayStudentName(string value)
{
var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return parts.Length < 2 ? value : string.Join(" ", parts.Skip(1).Append(parts[0]));
}
private static int SignalPriority(ClassTeacherCalendarEventKind kind) => kind switch private static int SignalPriority(ClassTeacherCalendarEventKind kind) => kind switch
{ {
ClassTeacherCalendarEventKind.Unexcused => 0, ClassTeacherCalendarEventKind.Unexcused => 0,
@@ -0,0 +1,35 @@
using CommunityToolkit.Mvvm.ComponentModel;
using LehrerApp.Desktop.Services;
namespace LehrerApp.Desktop.ViewModels.Students;
public sealed record AttendanceCalendarSizeChoice(AttendanceCalendarSize Value, string DisplayName);
public partial class AttendanceCalendarConfigurationViewModel : ObservableObject
{
[ObservableProperty] private DateTimeOffset? _startMonth;
[ObservableProperty] private int _monthCount;
[ObservableProperty] private AttendanceCalendarSizeChoice _selectedSize;
public IReadOnlyList<int> MonthCounts { get; } = [1, 2, 3];
public IReadOnlyList<AttendanceCalendarSizeChoice> Sizes { get; } =
[
new(AttendanceCalendarSize.Small, "Klein"),
new(AttendanceCalendarSize.Medium, "Standard"),
new(AttendanceCalendarSize.Large, "Groß"),
];
public AttendanceCalendarConfigurationViewModel(AttendanceCalendarOptions options)
{
StartMonth = new DateTimeOffset(options.NormalizedStartMonth.ToDateTime(TimeOnly.MinValue));
MonthCount = options.NormalizedMonthCount;
SelectedSize = Sizes.First(s => s.Value == options.Size);
}
public AttendanceCalendarOptions BuildResult()
{
var date = DateOnly.FromDateTime((StartMonth ?? DateTimeOffset.Now).LocalDateTime);
return new AttendanceCalendarOptions(new DateOnly(date.Year, date.Month, 1), MonthCount,
SelectedSize.Value);
}
}
@@ -12,6 +12,9 @@ public partial class CreateLetterDialogViewModel : ObservableObject
private readonly Student _student; private readonly Student _student;
private readonly TemplateStore _templates; private readonly TemplateStore _templates;
private readonly ITemplateRenderer _renderer; private readonly ITemplateRenderer _renderer;
private readonly Func<AttendanceCalendarOptions, DrawingValue>? _attendanceCalendarFactory;
private AttendanceCalendarOptions _attendanceCalendarOptions = new(
new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1), 1);
[ObservableProperty] private LetterTemplateChoice? _selectedTemplate; [ObservableProperty] private LetterTemplateChoice? _selectedTemplate;
[ObservableProperty] private LetterContactChoice? _selectedContact; [ObservableProperty] private LetterContactChoice? _selectedContact;
@@ -21,6 +24,9 @@ public partial class CreateLetterDialogViewModel : ObservableObject
[ObservableProperty] private string _teacherName = ""; [ObservableProperty] private string _teacherName = "";
[ObservableProperty] private string _generationError = ""; [ObservableProperty] private string _generationError = "";
[ObservableProperty] private bool _canGenerate; [ObservableProperty] private bool _canGenerate;
[ObservableProperty] private bool _usesAttendanceCalendar;
[ObservableProperty] private bool _attendanceCalendarConfigured;
[ObservableProperty] private string _attendanceCalendarSummary = "1 Monat · Standardgröße";
public string StudentName => _student.FullName; public string StudentName => _student.FullName;
public ObservableCollection<LetterTemplateChoice> Templates { get; } = []; public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
@@ -34,9 +40,11 @@ public partial class CreateLetterDialogViewModel : ObservableObject
$"{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)
{ {
_student = student; _templates = templates; _renderer = renderer; _student = student; _templates = templates; _renderer = renderer;
_attendanceCalendarFactory = attendanceCalendarFactory;
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))
@@ -45,10 +53,21 @@ public partial class CreateLetterDialogViewModel : ObservableObject
RefreshValidation(); RefreshValidation();
} }
partial void OnSelectedTemplateChanged(LetterTemplateChoice? value) { OnPropertyChanged(nameof(SuggestedFileName)); RefreshValidation(); } partial void OnSelectedTemplateChanged(LetterTemplateChoice? value)
{
OnPropertyChanged(nameof(SuggestedFileName));
UsesAttendanceCalendar = TemplateUsesAttendanceCalendar(value);
AttendanceCalendarConfigured = false;
ResetAttendanceCalendarOptions();
RefreshValidation();
}
partial void OnSelectedContactChanged(LetterContactChoice? value) => RefreshValidation(); partial void OnSelectedContactChanged(LetterContactChoice? value) => RefreshValidation();
partial void OnSelectedGroupChanged(LetterGroupChoice? value) => RefreshValidation(); partial void OnSelectedGroupChanged(LetterGroupChoice? value) => RefreshValidation();
partial void OnLetterDateChanged(DateTimeOffset? value) => RefreshValidation(); partial void OnLetterDateChanged(DateTimeOffset? value)
{
if (!AttendanceCalendarConfigured) ResetAttendanceCalendarOptions();
RefreshValidation();
}
partial void OnLetterTextChanged(string value) => RefreshValidation(); partial void OnLetterTextChanged(string value) => RefreshValidation();
partial void OnTeacherNameChanged(string value) => RefreshValidation(); partial void OnTeacherNameChanged(string value) => RefreshValidation();
@@ -91,7 +110,60 @@ 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);
public AttendanceCalendarOptions GetAttendanceCalendarOptions() => _attendanceCalendarOptions;
public void SetAttendanceCalendarOptions(AttendanceCalendarOptions options)
{
_attendanceCalendarOptions = options with { StartMonth = options.NormalizedStartMonth,
MonthCount = options.NormalizedMonthCount };
AttendanceCalendarConfigured = true;
AttendanceCalendarSummary = FormatAttendanceCalendarSummary(_attendanceCalendarOptions);
RefreshValidation();
}
private void ResetAttendanceCalendarOptions()
{
var date = DateOnly.FromDateTime((LetterDate ?? DateTimeOffset.Now).LocalDateTime);
_attendanceCalendarOptions = new AttendanceCalendarOptions(new(date.Year, date.Month, 1), 1);
AttendanceCalendarSummary = FormatAttendanceCalendarSummary(_attendanceCalendarOptions);
}
private bool TemplateUsesAttendanceCalendar(LetterTemplateChoice? choice)
{
if (choice is null) return false;
try
{
var loaded = _templates.Load(choice.Model);
return UsesPlaceholder(loaded.Layout) ||
(loaded.ContinuationLayout is not null && UsesPlaceholder(loaded.ContinuationLayout));
}
catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException)
{
return false;
}
}
private static bool UsesPlaceholder(TemplateLayout layout) =>
layout.Elements.Concat(layout.PageTemplates.SelectMany(p => p.Elements))
.Concat(layout.ContentFlows.SelectMany(f => f.Elements))
.Any(e => e is DrawBoxElement { Placeholder: StudentAttendanceCalendarDrawingBuilder.PlaceholderName }
or FlowDrawBoxElement { Placeholder: StudentAttendanceCalendarDrawingBuilder.PlaceholderName });
private static string FormatAttendanceCalendarSummary(AttendanceCalendarOptions options)
{
var month = options.NormalizedStartMonth.ToString("MMMM yyyy",
System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
var size = options.Size switch
{
AttendanceCalendarSize.Small => "Klein",
AttendanceCalendarSize.Large => "Groß",
_ => "Standard",
};
return $"Ab {month} · {options.NormalizedMonthCount} Monat{(options.NormalizedMonthCount == 1 ? "" : "e")} · {size}";
}
private static bool IsEmpty(PlaceholderValue value) => LetterPlaceholderBuilder.IsEmpty(value); private static bool IsEmpty(PlaceholderValue value) => LetterPlaceholderBuilder.IsEmpty(value);
private static string SanitizeFileName(string value) private static string SanitizeFileName(string value)
@@ -436,6 +436,11 @@
Background="Transparent" BorderThickness="0" Padding="6,2" Background="Transparent" BorderThickness="0" Padding="6,2"
Foreground="{DynamicResource AppAccentTextBrush}"/> Foreground="{DynamicResource AppAccentTextBrush}"/>
</Grid> </Grid>
<ComboBox ItemsSource="{Binding CalendarScopes}"
SelectedItem="{Binding SelectedCalendarScope, Mode=TwoWay}"
DisplayMemberBinding="{Binding DisplayName}"
HorizontalAlignment="Stretch"
AutomationProperties.Name="Schüler für Monatskalender auswählen"/>
<ItemsControl ItemsSource="{Binding CompactMonthDays}" HorizontalAlignment="Center"> <ItemsControl ItemsSource="{Binding CompactMonthDays}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate><UniformGrid Columns="7"/></ItemsPanelTemplate> <ItemsPanelTemplate><UniformGrid Columns="7"/></ItemsPanelTemplate>
@@ -0,0 +1,39 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
xmlns:conv="clr-namespace:LehrerApp.Desktop.Converters"
x:Class="LehrerApp.Desktop.Views.Students.AttendanceCalendarConfigurationDialog"
x:DataType="vm:AttendanceCalendarConfigurationViewModel"
Title="Anwesenheitskalender konfigurieren" Width="440" Height="360"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24,20">
<StackPanel Spacing="16">
<TextBlock Text="Anwesenheitskalender" 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."
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
<StackPanel Spacing="4">
<TextBlock Text="Erster Monat" FontSize="12" Opacity="0.7"/>
<CalendarDatePicker SelectedDate="{Binding StartMonth, Converter={x:Static conv:DateTimeOffsetToDateTimeConverter.Instance}}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<Grid ColumnDefinitions="*,12,*">
<StackPanel Spacing="4">
<TextBlock Text="Anzahl Monate" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding MonthCounts}" SelectedItem="{Binding MonthCount, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Darstellungsgröße" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Sizes}" SelectedItem="{Binding SelectedSize, Mode=TwoWay}"
DisplayMemberBinding="{Binding DisplayName}" HorizontalAlignment="Stretch"/>
</StackPanel>
</Grid>
<TextBlock Text="Bei mehreren Monaten wächst der Zeicheninhalt nach unten. Für drei große Monate sollte die Vorlage eine ausreichend hohe DRAWBOX oder eine FLOWDRAWBOX verwenden."
FontSize="11" Opacity="0.55" TextWrapping="Wrap"/>
</StackPanel>
<Grid Grid.Row="1" ColumnDefinitions="*,10,*" Margin="0,18,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Übernehmen" HorizontalAlignment="Stretch" Click="OnApply"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,12 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LehrerApp.Desktop.Views.Students;
public partial class AttendanceCalendarConfigurationDialog : Window
{
public AttendanceCalendarConfigurationDialog() => InitializeComponent();
private void OnApply(object? sender, RoutedEventArgs e) => Close(true);
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
}
@@ -44,6 +44,17 @@
<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}"
BorderThickness="1" CornerRadius="6" Padding="12">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="3">
<TextBlock Text="Anwesenheitskalender im Brief" FontWeight="SemiBold"/>
<TextBlock Text="{Binding AttendanceCalendarSummary}" FontSize="12" Opacity="0.65"/>
</StackPanel>
<Button Grid.Column="1" Content="Konfigurieren …" Click="OnConfigureAttendanceCalendar"
VerticalAlignment="Center"/>
</Grid>
</Border>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Brieftext" FontSize="12" Opacity="0.7"/> <TextBlock Text="Brieftext" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding LetterText}" AcceptsReturn="True" TextWrapping="Wrap" MinHeight="110" <TextBox Text="{Binding LetterText}" AcceptsReturn="True" TextWrapping="Wrap" MinHeight="110"
@@ -12,6 +12,8 @@ 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 &&
!await ConfigureAttendanceCalendar(vm)) return;
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{ {
Title = "PDF-Brief speichern", Title = "PDF-Brief speichern",
@@ -23,5 +25,19 @@ public partial class CreateLetterDialog : Window
Close(file.Path.LocalPath); Close(file.Path.LocalPath);
} }
private async void OnConfigureAttendanceCalendar(object? sender, RoutedEventArgs e)
{
if (DataContext is CreateLetterDialogViewModel vm) await ConfigureAttendanceCalendar(vm);
}
private async Task<bool> ConfigureAttendanceCalendar(CreateLetterDialogViewModel vm)
{
var configVm = new AttendanceCalendarConfigurationViewModel(vm.GetAttendanceCalendarOptions());
var dialog = new AttendanceCalendarConfigurationDialog { DataContext = configVm };
if (!await dialog.ShowDialog<bool>(this)) return false;
vm.SetAttendanceCalendarOptions(configVm.BuildResult());
return true;
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null); private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
} }
@@ -61,6 +61,7 @@ public partial class DesignerViewModel : ObservableObject
new("Anrede", PlaceholderType.Text, true, "Frau Beispiel"), new("Anrede", PlaceholderType.Text, true, "Frau Beispiel"),
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, ""),
]; ];
public ObservableCollection<DesignerMetadata> MetadataItems { get; } = public ObservableCollection<DesignerMetadata> MetadataItems { get; } =
[ [