diff --git a/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs index 117c8ac..243e269 100644 --- a/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs @@ -191,6 +191,49 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable Assert.True(vm.Generate(output)); } + [Fact] + public void AddressPreview_FolgtDemGewaehltenKontaktUndAktualisiertSichBeimWechsel() + { + var student = new Student + { + FirstName = "Lena", LastName = "Beispiel", + Contacts = + [ + new Contact { Name = "Frau Beispiel", Relation = "Mutter", Street = "Erste Str. 1", PostalCode = "11111", City = "Erststadt" }, + new Contact { Name = "Herr Beispiel", Relation = "Vater", Street = "Zweite Str. 2", PostalCode = "22222", City = "Zweitstadt" }, + ], + }; + var vm = Build(student, StoreWithTemplate(new PlaceholderDefinition("Anrede", PlaceholderType.Text, true))); + + Assert.Equal(2, vm.Contacts.Count); + Assert.Contains("Frau Beispiel", vm.AddressPreview); + Assert.Contains("Erste Str. 1", vm.AddressPreview); + + vm.SelectedContact = vm.Contacts.Single(c => c.Model.Name == "Herr Beispiel"); + + Assert.Contains("Herr Beispiel", vm.AddressPreview); + Assert.Contains("Zweite Str. 2", vm.AddressPreview); + Assert.DoesNotContain("Frau Beispiel", vm.AddressPreview); + } + + [Fact] + public void SelectContactDialog_StartetBeimAktuellenKontaktUndAktualisiertVorschau() + { + var contacts = new List + { + new(new Contact { Name = "Frau Beispiel", Street = "Erste Str. 1", PostalCode = "11111", City = "Erststadt" }), + new(new Contact { Name = "Herr Beispiel", Street = "Zweite Str. 2", PostalCode = "22222", City = "Zweitstadt" }), + }; + var dialogVm = new SelectContactDialogViewModel(contacts, contacts[1]); + + Assert.Same(contacts[1], dialogVm.SelectedContact); + Assert.Contains("Zweite Str. 2", dialogVm.AddressPreview); + + dialogVm.SelectedContact = contacts[0]; + + Assert.Contains("Erste Str. 1", dialogVm.AddressPreview); + } + private CreateLetterDialogViewModel Build(Student student, TemplateStore store) => new(student, store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([])); diff --git a/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs b/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs index 9f98394..aea0bb0 100644 --- a/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs +++ b/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs @@ -18,8 +18,7 @@ public static class LetterPlaceholderBuilder 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))); + var address = FormatAddress(contact); return new Dictionary(StringComparer.Ordinal) { ["Datum"] = new DateValue(date), ["CurrentDate"] = new DateValue(date), @@ -36,6 +35,14 @@ public static class LetterPlaceholderBuilder }; } + /// Mehrzeilige Anschrift (Name/Straße/PLZ Ort) für Adressvorschau im Dialog und + /// den Contact.Address-Platzhalter - eine Formatierung für beide Verwendungen. + public static string FormatAddress(Contact? contact) + { + var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City }.Where(x => !string.IsNullOrWhiteSpace(x))); + return string.Join(Environment.NewLine, new[] { contact?.Name, contact?.Street, cityLine }.Where(x => !string.IsNullOrWhiteSpace(x))); + } + public static bool IsEmpty(PlaceholderValue value) => value switch { TextValue x => string.IsNullOrWhiteSpace(x.Value), diff --git a/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs b/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs index 575d983..b61552f 100644 --- a/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs +++ b/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs @@ -37,9 +37,14 @@ public static class StudentAttendanceCalendarDrawingBuilder AttendanceCalendarSize.Large => 1f, _ => .85f, }; - var contentWidth = width * scale; + // Die Breite richtet sich nach der DRAWBOX-Deklaration im Layout-Skript (fest, unabhängig + // von der Größenwahl) - nur Schrift/Zellenhöhe skalieren mit "Größe". Würde die Breite mit + // skalieren, würde "Groß" (scale=1) exakt die Skript-Box ausfüllen, "Klein"/"Normal" aber + // nur einen Teil davon - und eine größere Skalierung als 1 liefe über die Box hinaus und + // würde am rechten Rand abgeschnitten (SVG overflow="hidden"). + var contentWidth = width; var cellHeight = 10 * scale; - var monthGapX = 6 * scale; + var monthGapX = 10f; var first = options.NormalizedStartMonth; var monthCount = options.NormalizedMonthCount; var monthWidth = (contentWidth - monthGapX * (monthCount - 1)) / monthCount; @@ -112,8 +117,10 @@ public static class StudentAbsenceDayListDrawingBuilder AttendanceCalendarSize.Large => 1f, _ => .88f, }; - var contentWidth = width * scale; - var rowHeight = 9 * scale; + // Breite bleibt an die DRAWBOX-Deklaration im Layout-Skript gebunden (siehe + // StudentAttendanceCalendarDrawingBuilder) - nur Zeilenhöhe/Schrift skalieren mit "Größe". + var contentWidth = width; + var rowHeight = 12 * scale; var start = options.NormalizedStartMonth; var end = start.AddMonths(options.NormalizedMonthCount).AddDays(-1); var rows = absences @@ -130,17 +137,17 @@ public static class StudentAbsenceDayListDrawingBuilder DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280")); y += 10 * scale + 2 * scale; commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6")); - commands.Add(new DrawStringEx(2 * scale, y + scale, rowHeight - scale, 31 * scale, "Datum", + commands.Add(new DrawStringEx(2, y + scale, rowHeight - scale, 31, "Datum", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); - commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - scale, 75 * scale, "Umfang", + commands.Add(new DrawStringEx(36, y + scale, rowHeight - scale, 75, "Umfang", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); - commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - scale, 55 * scale, "Status", + commands.Add(new DrawStringEx(113, y + scale, rowHeight - scale, 55, "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, + commands.Add(new DrawStringEx(2, y + 2 * scale, rowHeight, contentWidth - 4, "Keine Fehltage im gewählten Zeitraum", DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280", Italic: true)); y += rowHeight + 3 * scale; @@ -163,12 +170,12 @@ public static class StudentAbsenceDayListDrawingBuilder : 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 - scale, 31 * scale, + commands.Add(new DrawStringEx(2, y + scale, rowHeight - scale, 31, row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151")); - commands.Add(new DrawStringEx(36 * scale, y + scale, rowHeight - scale, 75 * scale, + commands.Add(new DrawStringEx(36, y + scale, rowHeight - scale, 75, extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151")); - commands.Add(new DrawStringEx(113 * scale, y + scale, rowHeight - scale, 55 * scale, + commands.Add(new DrawStringEx(113, y + scale, rowHeight - scale, 55, status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor, Bold: row.IsUnexcused)); y += rowHeight; diff --git a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs index b3885f3..2b928e1 100644 --- a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs @@ -44,6 +44,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject public bool HasCustomPlaceholders => CustomPlaceholders.Count > 0; public bool HasNoTemplates => Templates.Count == 0; public bool HasNoContacts => Contacts.Count == 0; + public string AddressPreview => LetterPlaceholderBuilder.FormatAddress(SelectedContact?.Model); + public bool HasAddressPreview => !string.IsNullOrWhiteSpace(AddressPreview); public bool UsesAttendanceAdvancedContent => UsesAttendanceCalendar || UsesAbsenceDayList; public string SuggestedFileName => SanitizeFileName( $"{SelectedTemplate?.Name ?? "Elternbrief"}_{_student.LastName}_{_student.FirstName}.pdf"); @@ -82,6 +84,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject partial void OnSelectedContactChanged(LetterContactChoice? value) { Anrede = value?.Model.LetterSalutation ?? ""; + OnPropertyChanged(nameof(AddressPreview)); + OnPropertyChanged(nameof(HasAddressPreview)); RefreshValidation(); } partial void OnAnredeChanged(string value) => RefreshValidation(); diff --git a/LehrerApp.Desktop/ViewModels/Students/SelectContactDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/SelectContactDialogViewModel.cs new file mode 100644 index 0000000..c5fe913 --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Students/SelectContactDialogViewModel.cs @@ -0,0 +1,25 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using LehrerApp.Desktop.Services; + +namespace LehrerApp.Desktop.ViewModels.Students; + +public partial class SelectContactDialogViewModel : ObservableObject +{ + [ObservableProperty] private LetterContactChoice? _selectedContact; + + public IReadOnlyList Contacts { get; } + public string AddressPreview => LetterPlaceholderBuilder.FormatAddress(SelectedContact?.Model); + public bool HasAddressPreview => !string.IsNullOrWhiteSpace(AddressPreview); + + public SelectContactDialogViewModel(IReadOnlyList contacts, LetterContactChoice? current) + { + Contacts = contacts; + SelectedContact = current ?? contacts.FirstOrDefault(); + } + + partial void OnSelectedContactChanged(LetterContactChoice? value) + { + OnPropertyChanged(nameof(AddressPreview)); + OnPropertyChanged(nameof(HasAddressPreview)); + } +} diff --git a/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml b/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml index c6986aa..238ed20 100644 --- a/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml +++ b/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml @@ -26,11 +26,17 @@ Classes="emptyhint" IsVisible="{Binding HasNoTemplates}"/> - - - + + + + + +