Compare commits
3
Commits
70dc78904c
...
0b5cfc5522
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b5cfc5522 | ||
|
|
09cbbf1b77 | ||
|
|
1671796844 |
@@ -2,6 +2,7 @@ using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||
using LehrerApp.Templating;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
@@ -71,6 +72,87 @@ public sealed class ClassTeacherViewModelsTests
|
||||
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]
|
||||
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]
|
||||
public void GroupByStudentAndDay_FasstFehlstundenProSchuelerUndTagZusammen()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Templating;
|
||||
using Xunit;
|
||||
@@ -54,6 +55,44 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
|
||||
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));
|
||||
}
|
||||
|
||||
[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) =>
|
||||
new(student, store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]));
|
||||
|
||||
@@ -65,8 +104,18 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
|
||||
var y = 20;
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
var element = definition.Type == PlaceholderType.Multiline ? "TEXTBOX" : "TEXT";
|
||||
lines.Add(element == "TEXTBOX" ? $"TEXTBOX 20 {y} 170 80 ${definition.Name}" : $"TEXT 20 {y} ${definition.Name}");
|
||||
var element = definition.Type switch
|
||||
{
|
||||
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;
|
||||
}
|
||||
TemplatePackage.Create(source, manifest, string.Join('\n', lines), new Dictionary<string, byte[]>());
|
||||
|
||||
@@ -229,6 +229,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<LessonPlanTools>();
|
||||
services.AddSingleton<GroupMembershipTools>();
|
||||
services.AddSingleton<LetterTemplateTools>();
|
||||
services.AddSingleton<StudentAttendanceCalendarService>();
|
||||
services.AddSingleton<CompetencyTools>();
|
||||
services.AddSingleton<GroupTools>();
|
||||
services.AddSingleton<UntisComparisonTools>();
|
||||
|
||||
@@ -19,7 +19,10 @@ public static class LetterDialogs
|
||||
App.Services.GetRequiredService<TemplateStore>(),
|
||||
App.Services.GetRequiredService<ITemplateRenderer>(),
|
||||
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>()
|
||||
.BuildAbsenceDayList(student, options));
|
||||
var dialog = new CreateLetterDialog { DataContext = vm };
|
||||
var path = await dialog.ShowDialog<string?>(owner);
|
||||
if (!string.IsNullOrEmpty(path) && File.Exists(path))
|
||||
|
||||
@@ -15,7 +15,8 @@ public static class LetterPlaceholderBuilder
|
||||
{
|
||||
public static Dictionary<string, PlaceholderValue> BuildStandardValues(
|
||||
Student student, Contact? contact, LearningGroup? group, DateOnly date,
|
||||
string letterText, string teacherName)
|
||||
string letterText, string teacherName, DrawingValue? attendanceCalendar = null,
|
||||
DrawingValue? absenceDays = null)
|
||||
{
|
||||
var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City }.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
var address = string.Join(Environment.NewLine, new[] { contact?.Name, contact?.Street, cityLine }.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
@@ -29,6 +30,8 @@ public static class LetterPlaceholderBuilder
|
||||
["Contact.Street"] = new TextValue(contact?.Street ?? ""), ["Contact.PostalCode"] = new TextValue(contact?.PostalCode ?? ""),
|
||||
["Contact.City"] = new TextValue(contact?.City ?? ""), ["Letter.Salutation"] = new TextValue(contact?.LetterSalutation ?? ""),
|
||||
["Group.Name"] = new TextValue(group?.Name ?? ""), ["SchoolYear"] = new TextValue(group?.SchoolYear ?? ""),
|
||||
[StudentAttendanceCalendarDrawingBuilder.PlaceholderName] = attendanceCalendar ?? new DrawingValue([], 0),
|
||||
[StudentAbsenceDayListDrawingBuilder.PlaceholderName] = absenceDays ?? new DrawingValue([], 0),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,6 +39,7 @@ public static class LetterPlaceholderBuilder
|
||||
{
|
||||
TextValue x => string.IsNullOrWhiteSpace(x.Value),
|
||||
MultilineValue x => string.IsNullOrWhiteSpace(x.Value),
|
||||
DrawingValue x => x.ContentHeight <= 0 || x.Commands.Count == 0,
|
||||
_ => 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>
|
||||
public class LetterTemplateTools(
|
||||
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).")]
|
||||
public List<LetterTemplateDto> ListLetterTemplates()
|
||||
@@ -73,7 +74,9 @@ public class LetterTemplateTools(
|
||||
var group = groupId is { } gid ? groups.GetById(gid) : null;
|
||||
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),
|
||||
attendanceCalendars?.BuildAbsenceDayList(student, new AttendanceCalendarOptions(date, 1)));
|
||||
foreach (var (name, raw) in extraValues ?? [])
|
||||
{
|
||||
var definition = loaded.Manifest.Placeholders.FirstOrDefault(p => p.Name == name);
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
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>Chronologische, portable Fehlzeitenliste für Elternbriefvorlagen.</summary>
|
||||
public static class StudentAbsenceDayListDrawingBuilder
|
||||
{
|
||||
public const string PlaceholderName = "Student.AbsenceDays";
|
||||
|
||||
public static DrawingValue Build(string studentName, AttendanceCalendarOptions options,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> absences)
|
||||
{
|
||||
const float width = 170;
|
||||
var scale = options.Size switch
|
||||
{
|
||||
AttendanceCalendarSize.Small => .75f,
|
||||
AttendanceCalendarSize.Large => 1f,
|
||||
_ => .88f,
|
||||
};
|
||||
var contentWidth = width * scale;
|
||||
var rowHeight = 9 * scale;
|
||||
var start = options.NormalizedStartMonth;
|
||||
var end = start.AddMonths(options.NormalizedMonthCount).AddDays(-1);
|
||||
var rows = absences
|
||||
.Where(a => a.Date >= start && a.Date <= end &&
|
||||
UntisNameMatching.NamesMatch(a.StudentName, studentName))
|
||||
.OrderBy(a => a.Date)
|
||||
.ToList();
|
||||
var commands = new List<DrawingCommand>
|
||||
{
|
||||
new DrawStringEx(0, 0, 7 * scale, contentWidth, "Fehltage",
|
||||
DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true),
|
||||
new DrawStringEx(0, 7 * scale, 6 * scale, contentWidth, studentName,
|
||||
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"),
|
||||
};
|
||||
var y = 16 * scale;
|
||||
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6"));
|
||||
commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - 2 * scale, 31 * scale, "Datum",
|
||||
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
||||
commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - 2 * scale, 75 * scale, "Umfang",
|
||||
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
||||
commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - 2 * scale, 55 * scale, "Status",
|
||||
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
||||
y += rowHeight;
|
||||
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
commands.Add(new DrawStringEx(2 * scale, y + 2 * scale, rowHeight, contentWidth - 4 * scale,
|
||||
"Keine Fehltage im gewählten Zeitraum", DrawingTextAlignment.AlignLeft, 8 * scale,
|
||||
Color: "#6B7280", Italic: true));
|
||||
y += rowHeight + 3 * scale;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var index = 0; index < rows.Count; index++)
|
||||
{
|
||||
var row = rows[index];
|
||||
var fill = index % 2 == 0 ? "#FFFFFF" : "#F9FAFB";
|
||||
var extent = row.CountsAsFullDay
|
||||
? "Ganzer Fehltag"
|
||||
: row.TotalAbsentPeriods > 0
|
||||
? $"Fehlzeit · {row.TotalAbsentPeriods} Std."
|
||||
: $"Fehlzeit · {row.TotalAbsentMinutes} Min.";
|
||||
var status = row.IsUnexcused
|
||||
? "Unentschuldigt"
|
||||
: row.FriendlyStatusLabel.Contains("Entschuldigt", StringComparison.OrdinalIgnoreCase)
|
||||
? "Entschuldigt"
|
||||
: row.FriendlyStatusLabel;
|
||||
var statusColor = row.IsUnexcused ? "#C62828" : "#2E7D32";
|
||||
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#E5E7EB", .3f, fill));
|
||||
commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - 2 * scale, 31 * scale,
|
||||
row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale,
|
||||
Color: "#374151"));
|
||||
commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - 2 * scale, 75 * scale,
|
||||
extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
|
||||
commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - 2 * scale, 55 * scale,
|
||||
status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor,
|
||||
Bold: row.IsUnexcused));
|
||||
y += rowHeight;
|
||||
}
|
||||
}
|
||||
return new DrawingValue(commands, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Liest ausschließlich den lokalen WebUntis-Cache. Das Öffnen oder Rendern eines
|
||||
/// Elternbriefs löst dadurch keinen unerwarteten Netzwerkabruf aus.</summary>
|
||||
public sealed class StudentAttendanceCalendarService(
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ public enum ClassTeacherStatusKind { Ok, Info, Warning, Danger }
|
||||
/// Schlagwort gezielt dämpfen kann, siehe <see cref="ClassTeacherRosterRow.MatchDomains"/>.</summary>
|
||||
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,
|
||||
string? AbsenceTooltip, bool HasRecentClassRegisterEntry)
|
||||
{
|
||||
@@ -424,6 +428,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
public ObservableCollection<ClassTeacherPatternNotice> PatternNotices { get; } = [];
|
||||
public ObservableCollection<ClassTeacherOpenExcuseRow> OpenExcuses { get; } = [];
|
||||
public ObservableCollection<ClassTeacherCompactCalendarDay> CompactMonthDays { get; } = [];
|
||||
public ObservableCollection<ClassTeacherCalendarScope> CalendarScopes { get; } = [];
|
||||
|
||||
[ObservableProperty] private string? _homeroomClassName;
|
||||
[ObservableProperty] private int _activeTabIndex;
|
||||
@@ -453,6 +458,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
/// den "Klassenbuch öffnen"-Button gehängt statt in einer eigenen Kennzahlkarte.
|
||||
[ObservableProperty] private int _ownDocumentationFollowUpCount;
|
||||
[ObservableProperty] private int _ownDocumentationCriticalCount;
|
||||
[ObservableProperty] private ClassTeacherCalendarScope? _selectedCalendarScope;
|
||||
|
||||
// Zuletzt per Load() geholte WebUntis-Rohdaten, für RefreshFromLocalDataOnly() - damit ein
|
||||
// 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 string DayOverviewTooltip => $"{PresentCount} anwesend · {LateCount} verspätet · " +
|
||||
$"{ExcusedAbsenceCount} entschuldigt · {UnexcusedAbsenceCount} unentschuldigt";
|
||||
public string CompactMonthLabel => DateOnly.FromDateTime(DateTime.Today).ToString("MMMM yyyy",
|
||||
System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
|
||||
public string CompactMonthLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
var month = DateOnly.FromDateTime(DateTime.Today).ToString("MMMM yyyy",
|
||||
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>? OnNavigateToWorkload { get; set; }
|
||||
@@ -534,6 +549,11 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
partial void OnOpenExcuseOverflowCountChanged(int value) => OnPropertyChanged(nameof(HasOpenExcuseOverflow));
|
||||
partial void OnOwnDocumentationFollowUpCountChanged(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 OnSelectedRosterFilterChanged(int value)
|
||||
{
|
||||
@@ -548,7 +568,8 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
ActiveTabIndex = 0;
|
||||
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
|
||||
@@ -678,6 +699,12 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
holidayWeekdaysExcluded, termStart, classRegisterEvents,
|
||||
_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;
|
||||
TodayAlertCount = Roster.Count(r => r.HasAbsenceToday);
|
||||
TodayUnexcusedCount = Roster.Count(r => r.IsUnexcused);
|
||||
@@ -724,7 +751,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
private void OpenMonthlyCalendar()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
DetailsTab.StudentFilter = "";
|
||||
DetailsTab.StudentFilter = SelectedCalendarScope?.StudentName ?? "";
|
||||
DetailsTab.CalendarMonth = new DateOnly(today.Year, today.Month, 1);
|
||||
ActiveTabIndex = 2;
|
||||
DetailsTab.ShowCalendarCommand.Execute(null);
|
||||
@@ -735,15 +762,29 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
CompactMonthDays.Clear();
|
||||
foreach (var day in BuildCompactMonthDays(new DateOnly(today.Year, today.Month, 1),
|
||||
absences, registerEntries, today))
|
||||
absences, registerEntries, today, SelectedCalendarScope?.StudentName))
|
||||
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,
|
||||
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);
|
||||
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
|
||||
.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)
|
||||
@@ -768,6 +809,12 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
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
|
||||
{
|
||||
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,10 @@ public partial class CreateLetterDialogViewModel : ObservableObject
|
||||
private readonly Student _student;
|
||||
private readonly TemplateStore _templates;
|
||||
private readonly ITemplateRenderer _renderer;
|
||||
private readonly Func<AttendanceCalendarOptions, DrawingValue>? _attendanceCalendarFactory;
|
||||
private readonly Func<AttendanceCalendarOptions, DrawingValue>? _absenceDayListFactory;
|
||||
private AttendanceCalendarOptions _attendanceCalendarOptions = new(
|
||||
new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1), 1);
|
||||
|
||||
[ObservableProperty] private LetterTemplateChoice? _selectedTemplate;
|
||||
[ObservableProperty] private LetterContactChoice? _selectedContact;
|
||||
@@ -21,6 +25,10 @@ public partial class CreateLetterDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private string _teacherName = "";
|
||||
[ObservableProperty] private string _generationError = "";
|
||||
[ObservableProperty] private bool _canGenerate;
|
||||
[ObservableProperty] private bool _usesAttendanceCalendar;
|
||||
[ObservableProperty] private bool _usesAbsenceDayList;
|
||||
[ObservableProperty] private bool _attendanceCalendarConfigured;
|
||||
[ObservableProperty] private string _attendanceCalendarSummary = "1 Monat · Standardgröße";
|
||||
|
||||
public string StudentName => _student.FullName;
|
||||
public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
|
||||
@@ -30,13 +38,18 @@ public partial class CreateLetterDialogViewModel : ObservableObject
|
||||
public bool HasIssues => Issues.Count > 0;
|
||||
public bool HasNoTemplates => Templates.Count == 0;
|
||||
public bool HasNoContacts => Contacts.Count == 0;
|
||||
public bool UsesAttendanceAdvancedContent => UsesAttendanceCalendar || UsesAbsenceDayList;
|
||||
public string SuggestedFileName => SanitizeFileName(
|
||||
$"{SelectedTemplate?.Name ?? "Elternbrief"}_{_student.LastName}_{_student.FirstName}.pdf");
|
||||
|
||||
public CreateLetterDialogViewModel(Student student, TemplateStore templates, ITemplateRenderer renderer,
|
||||
IGroupMembershipRepository memberships, IGroupRepository groups)
|
||||
IGroupMembershipRepository memberships, IGroupRepository groups,
|
||||
Func<AttendanceCalendarOptions, DrawingValue>? attendanceCalendarFactory = null,
|
||||
Func<AttendanceCalendarOptions, DrawingValue>? absenceDayListFactory = null)
|
||||
{
|
||||
_student = student; _templates = templates; _renderer = renderer;
|
||||
_attendanceCalendarFactory = attendanceCalendarFactory;
|
||||
_absenceDayListFactory = absenceDayListFactory;
|
||||
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 membership in memberships.GetByStudent(student.Id))
|
||||
@@ -45,10 +58,25 @@ public partial class CreateLetterDialogViewModel : ObservableObject
|
||||
RefreshValidation();
|
||||
}
|
||||
|
||||
partial void OnSelectedTemplateChanged(LetterTemplateChoice? value) { OnPropertyChanged(nameof(SuggestedFileName)); RefreshValidation(); }
|
||||
partial void OnSelectedTemplateChanged(LetterTemplateChoice? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(SuggestedFileName));
|
||||
UsesAttendanceCalendar = TemplateUsesPlaceholder(value,
|
||||
StudentAttendanceCalendarDrawingBuilder.PlaceholderName);
|
||||
UsesAbsenceDayList = TemplateUsesPlaceholder(value,
|
||||
StudentAbsenceDayListDrawingBuilder.PlaceholderName);
|
||||
OnPropertyChanged(nameof(UsesAttendanceAdvancedContent));
|
||||
AttendanceCalendarConfigured = false;
|
||||
ResetAttendanceCalendarOptions();
|
||||
RefreshValidation();
|
||||
}
|
||||
partial void OnSelectedContactChanged(LetterContactChoice? 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 OnTeacherNameChanged(string value) => RefreshValidation();
|
||||
|
||||
@@ -91,7 +119,61 @@ public partial class CreateLetterDialogViewModel : ObservableObject
|
||||
|
||||
private IReadOnlyDictionary<string, PlaceholderValue> BuildValues() => LetterPlaceholderBuilder.BuildStandardValues(
|
||||
_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,
|
||||
UsesAbsenceDayList ? _absenceDayListFactory?.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 TemplateUsesPlaceholder(LetterTemplateChoice? choice, string placeholderName)
|
||||
{
|
||||
if (choice is null) return false;
|
||||
try
|
||||
{
|
||||
var loaded = _templates.Load(choice.Model);
|
||||
return UsesPlaceholder(loaded.Layout, placeholderName) ||
|
||||
(loaded.ContinuationLayout is not null && UsesPlaceholder(loaded.ContinuationLayout, placeholderName));
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool UsesPlaceholder(TemplateLayout layout, string placeholderName) =>
|
||||
layout.Elements.Concat(layout.PageTemplates.SelectMany(p => p.Elements))
|
||||
.Concat(layout.ContentFlows.SelectMany(f => f.Elements))
|
||||
.Any(e => e is DrawBoxElement draw && draw.Placeholder == placeholderName ||
|
||||
e is FlowDrawBoxElement flow && flow.Placeholder == 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 string SanitizeFileName(string value)
|
||||
|
||||
@@ -436,6 +436,11 @@
|
||||
Background="Transparent" BorderThickness="0" Padding="6,2"
|
||||
Foreground="{DynamicResource AppAccentTextBrush}"/>
|
||||
</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.ItemsPanel>
|
||||
<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="Anwesenheitsdaten konfigurieren" Width="440" Height="360"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="*,Auto" Margin="24,20">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Anwesenheitsdaten" Classes="dialogtitle"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border IsVisible="{Binding UsesAttendanceAdvancedContent}" BorderBrush="{DynamicResource AppCardBorderBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Spacing="3">
|
||||
<TextBlock Text="Anwesenheitsdaten 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">
|
||||
<TextBlock Text="Brieftext" FontSize="12" Opacity="0.7"/>
|
||||
<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)
|
||||
{
|
||||
if (DataContext is not CreateLetterDialogViewModel vm) return;
|
||||
if (vm.UsesAttendanceAdvancedContent && !vm.AttendanceCalendarConfigured &&
|
||||
!await ConfigureAttendanceCalendar(vm)) return;
|
||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "PDF-Brief speichern",
|
||||
@@ -23,5 +25,19 @@ public partial class CreateLetterDialog : Window
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,40 @@ public sealed class ProjectLifecycleTests
|
||||
Assert.True(viewModel.HasNoSelectedPlaceholder);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Anwesenheitskalender", "Student.AttendanceCalendar", "DRAWBOX", "90")]
|
||||
[InlineData("Fehltage nach Datum", "Student.AbsenceDays", "FLOWDRAWBOX", "140")]
|
||||
public void Sonderinhalt_AusKatalog_BereitetPlatzhalterUndElementVor(
|
||||
string displayName, string placeholderName, string elementType, string height)
|
||||
{
|
||||
var viewModel = new DesignerViewModel();
|
||||
viewModel.Placeholders.Clear();
|
||||
viewModel.SelectedSpecialContent = viewModel.SpecialContents.Single(x => x.DisplayName == displayName);
|
||||
|
||||
viewModel.PrepareSelectedSpecialContent();
|
||||
|
||||
var placeholder = Assert.Single(viewModel.Placeholders);
|
||||
Assert.Equal(placeholderName, placeholder.Name);
|
||||
Assert.Equal(LehrerApp.Templating.PlaceholderType.Drawing, placeholder.Type);
|
||||
Assert.Same(placeholder, viewModel.SelectedPlaceholder);
|
||||
Assert.Equal(elementType, viewModel.NewElementType);
|
||||
Assert.Equal("$" + placeholderName, viewModel.NewContent);
|
||||
Assert.Equal("170", viewModel.NewWidth);
|
||||
Assert.Equal(height, viewModel.NewHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sonderinhalt_WirdNichtDoppeltDeklariert()
|
||||
{
|
||||
var viewModel = new DesignerViewModel();
|
||||
viewModel.SelectedSpecialContent = viewModel.SpecialContents.Single(x =>
|
||||
x.PlaceholderName == "Student.AttendanceCalendar");
|
||||
|
||||
viewModel.PrepareSelectedSpecialContent();
|
||||
|
||||
Assert.Single(viewModel.Placeholders, x => x.Name == "Student.AttendanceCalendar");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoppeltePlatzhalternamen_WerdenVerstaendlichAbgelehnt()
|
||||
{
|
||||
|
||||
@@ -48,12 +48,14 @@ public partial class DesignerViewModel : ObservableObject
|
||||
[ObservableProperty] private string _overlayPageUnit = "mm";
|
||||
[ObservableProperty] private string _selectedPageTemplate = "first";
|
||||
[ObservableProperty] private DesignerPreviewPage? _selectedPreviewPage;
|
||||
[ObservableProperty] private SpecialContentItem? _selectedSpecialContent;
|
||||
|
||||
public IReadOnlyList<string> Units { get; } = ["mm", "cm", "pt", "in"];
|
||||
public IReadOnlyList<PlaceholderType> PlaceholderTypes { get; } = Enum.GetValues<PlaceholderType>();
|
||||
public IReadOnlyList<string> ElementTypes { get; } =
|
||||
["TEXT", "TEXTBOX", "FLOWBOX", "DRAWBOX", "FLOWDRAWBOX", "IMG", "TABLE", "CHART"];
|
||||
public IReadOnlyList<string> ElementScopes { get; } = ["Seitenvorlage (fest)", "Content-Flow"];
|
||||
public IReadOnlyList<SpecialContentItem> SpecialContents { get; } = SpecialContentCatalog.Items;
|
||||
public ObservableCollection<DesignerPlaceholder> Placeholders { get; } =
|
||||
[
|
||||
new("Datum", PlaceholderType.Date, true, DateTime.Today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
|
||||
@@ -61,6 +63,8 @@ public partial class DesignerViewModel : ObservableObject
|
||||
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("LehrerName", PlaceholderType.Text, true, "M. Mustermann"),
|
||||
new("Student.AttendanceCalendar", PlaceholderType.Drawing, false, ""),
|
||||
new("Student.AbsenceDays", PlaceholderType.Drawing, false, ""),
|
||||
];
|
||||
public ObservableCollection<DesignerMetadata> MetadataItems { get; } =
|
||||
[
|
||||
@@ -76,7 +80,11 @@ public partial class DesignerViewModel : ObservableObject
|
||||
public bool HasSelectedPlaceholder => SelectedPlaceholder is not null;
|
||||
public bool HasNoSelectedPlaceholder => SelectedPlaceholder is null;
|
||||
|
||||
public DesignerViewModel() => SelectedPlaceholder = Placeholders.FirstOrDefault();
|
||||
public DesignerViewModel()
|
||||
{
|
||||
SelectedPlaceholder = Placeholders.FirstOrDefault();
|
||||
SelectedSpecialContent = SpecialContents.FirstOrDefault();
|
||||
}
|
||||
|
||||
partial void OnSelectedPlaceholderChanged(DesignerPlaceholder? value)
|
||||
{
|
||||
@@ -196,6 +204,32 @@ public partial class DesignerViewModel : ObservableObject
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
/// <summary>Übernimmt einen lesbar benannten LehrerApp-Sonderinhalt in das normale
|
||||
/// Elementformular und legt seine Drawing-Deklaration an, falls sie noch fehlt.</summary>
|
||||
public void PrepareSelectedSpecialContent()
|
||||
{
|
||||
if (SelectedSpecialContent is not { } special)
|
||||
throw new InvalidOperationException("Bitte zuerst einen Sonderinhalt auswählen.");
|
||||
var placeholder = Placeholders.FirstOrDefault(p => p.Name == special.PlaceholderName);
|
||||
if (placeholder is not null && placeholder.Type != PlaceholderType.Drawing)
|
||||
throw new InvalidDataException(
|
||||
$"Der vorhandene Platzhalter „{special.PlaceholderName}“ hat nicht den Typ Drawing.");
|
||||
if (placeholder is null)
|
||||
{
|
||||
placeholder = new DesignerPlaceholder(special.PlaceholderName, PlaceholderType.Drawing,
|
||||
false, special.Description);
|
||||
Placeholders.Add(placeholder);
|
||||
}
|
||||
SelectedPlaceholder = placeholder;
|
||||
NewElementType = special.RecommendedElementType;
|
||||
NewContent = "$" + special.PlaceholderName;
|
||||
NewWidth = special.RecommendedWidth;
|
||||
NewHeight = special.RecommendedHeight;
|
||||
NewAttributes = "";
|
||||
CanExport = false;
|
||||
SetStatus($"„{special.DisplayName}“ vorbereitet. Position und Größe prüfen, dann ins Layout übernehmen.", false);
|
||||
}
|
||||
|
||||
public void RemoveSelectedPlaceholder()
|
||||
{
|
||||
if (SelectedPlaceholder is not { } selected) return;
|
||||
@@ -701,6 +735,48 @@ public sealed record DesignerPreviewPage(int Number, Bitmap Image)
|
||||
public string Display => $"Dokumentseite {Number}";
|
||||
}
|
||||
|
||||
public sealed record SpecialContentItem(string DisplayName, string PlaceholderName, string Description,
|
||||
string RecommendedElementType, string RecommendedWidth, string RecommendedHeight);
|
||||
|
||||
public static class SpecialContentCatalog
|
||||
{
|
||||
public static IReadOnlyList<SpecialContentItem> Items { get; } =
|
||||
[
|
||||
new("Anwesenheitskalender", "Student.AttendanceCalendar",
|
||||
"Monatskalender mit farbigen Anwesenheitsmarkern; Zeitraum und Größe werden beim Erstellen des Briefs gewählt.",
|
||||
"DRAWBOX", "170", "90"),
|
||||
new("Fehltage nach Datum", "Student.AbsenceDays",
|
||||
"Chronologische Liste mit Datum, Fehlzeit oder ganzem Fehltag sowie Entschuldigungsstatus.",
|
||||
"FLOWDRAWBOX", "170", "140"),
|
||||
];
|
||||
|
||||
public static DrawingValue SampleDrawing(string placeholderName) => placeholderName switch
|
||||
{
|
||||
"Student.AttendanceCalendar" => new DrawingValue(
|
||||
[
|
||||
new DrawString(2, 2, "Anwesenheit · Erika Beispiel", 10, "#1F2937", Bold: true),
|
||||
new DrawRectangle(2, 14, 22, 12, "#D1D5DB", .4f, "#FFFFFF"),
|
||||
new DrawString(9, 16, "12", 7, "#374151"),
|
||||
new DrawRectangle(26, 14, 22, 12, "#C62828", .4f, "#C62828"),
|
||||
new DrawString(34, 16, "U", 7, "#FFFFFF", Bold: true),
|
||||
new DrawRectangle(50, 14, 22, 12, "#2E7D32", .4f, "#2E7D32"),
|
||||
new DrawString(58, 16, "E", 7, "#FFFFFF", Bold: true),
|
||||
], 28),
|
||||
"Student.AbsenceDays" => new DrawingValue(
|
||||
[
|
||||
new DrawString(2, 2, "Fehltage · Erika Beispiel", 10, "#1F2937", Bold: true),
|
||||
new DrawRectangle(2, 14, 166, 11, "#CBD5E1", .4f, "#F3F4F6"),
|
||||
new DrawString(4, 15, "Datum", 7, "#374151", Bold: true),
|
||||
new DrawString(42, 15, "Umfang", 7, "#374151", Bold: true),
|
||||
new DrawString(116, 15, "Status", 7, "#374151", Bold: true),
|
||||
new DrawString(4, 27, "03.09.2026", 7, "#374151"),
|
||||
new DrawString(42, 27, "Ganzer Fehltag", 7, "#374151"),
|
||||
new DrawString(116, 27, "Entschuldigt", 7, "#2E7D32"),
|
||||
], 38),
|
||||
_ => DesignerPlaceholder.GenericSampleDrawing(),
|
||||
};
|
||||
}
|
||||
|
||||
public partial class DesignerAsset(string name, int pixelWidth, int pixelHeight, long byteCount) : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private int _usageCount;
|
||||
@@ -755,7 +831,7 @@ public partial class DesignerPlaceholder : ObservableObject
|
||||
PlaceholderType.Image => new ImageValue([], "image/png"),
|
||||
PlaceholderType.Table => ParseTable(Sample),
|
||||
PlaceholderType.Chart => ParseChart(Sample),
|
||||
PlaceholderType.Drawing => SampleDrawing(),
|
||||
PlaceholderType.Drawing => SpecialContentCatalog.SampleDrawing(Name),
|
||||
_ => new TextValue(Sample),
|
||||
};
|
||||
public static string SampleFor(PlaceholderType type) => type switch
|
||||
@@ -770,7 +846,7 @@ public partial class DesignerPlaceholder : ObservableObject
|
||||
}
|
||||
private static ChartValue ParseChart(string value) => new([new("Werte", value.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select((x, i) => { var parts = x.Split(':', 2); return new ChartPoint(parts[0], parts.Length == 2 && decimal.TryParse(parts[1], CultureInfo.InvariantCulture, out var y) ? y : i + 1); }).ToList())]);
|
||||
private static DrawingValue SampleDrawing() => new DrawingValue(
|
||||
internal static DrawingValue GenericSampleDrawing() => new DrawingValue(
|
||||
[new DrawRectangle(0, 0, 80, 24, "#2563EB", 0.8f, "#EFF6FF"),
|
||||
new DrawString(4, 4, "Dynamischer Inhalt", 10, "#1E3A8A", Bold: true),
|
||||
new MoveTo(4, 19), new LineTo(76, 19, "#93C5FD", 0.6f)], 24);
|
||||
|
||||
@@ -213,6 +213,21 @@
|
||||
<ScrollViewer Padding="8">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Element hinzufügen" Classes="section"/>
|
||||
<Border BorderBrush="#93C5FD" BorderThickness="1" Background="#EFF6FF"
|
||||
CornerRadius="6" Padding="12">
|
||||
<StackPanel Spacing="7">
|
||||
<TextBlock Text="LehrerApp-Sonderinhalte" FontWeight="SemiBold" Foreground="#1E3A8A"/>
|
||||
<TextBlock Text="Kein technischer Platzhaltername nötig: Inhalt auswählen und übernehmen."
|
||||
FontSize="11" Foreground="#475569" TextWrapping="Wrap"/>
|
||||
<ComboBox ItemsSource="{Binding SpecialContents}"
|
||||
SelectedItem="{Binding SelectedSpecialContent, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding DisplayName}"/>
|
||||
<TextBlock Text="{Binding SelectedSpecialContent.Description}" FontSize="11"
|
||||
Foreground="#475569" TextWrapping="Wrap"/>
|
||||
<Button Content="Für Layout vorbereiten" HorizontalAlignment="Left"
|
||||
Click="OnPrepareSpecialContent"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel><TextBlock Text="Einfügen in" Classes="label"/>
|
||||
<ComboBox ItemsSource="{Binding ElementScopes}" SelectedItem="{Binding NewElementScope}"/></StackPanel>
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
|
||||
@@ -58,6 +58,8 @@ public partial class MainWindow : Window
|
||||
}
|
||||
private void OnAddElement(object? sender, RoutedEventArgs e)
|
||||
{ try { _viewModel.AddElement(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
|
||||
private void OnPrepareSpecialContent(object? sender, RoutedEventArgs e)
|
||||
{ try { _viewModel.PrepareSelectedSpecialContent(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
|
||||
private void OnAddPageTemplate(object? sender, RoutedEventArgs e)
|
||||
{ try { _viewModel.AddPageTemplate(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
|
||||
private void OnAddContentFlow(object? sender, RoutedEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user