This commit is contained in:
@@ -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<LetterContactChoice>
|
||||
{
|
||||
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([]));
|
||||
|
||||
|
||||
@@ -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<string, PlaceholderValue>(StringComparer.Ordinal)
|
||||
{
|
||||
["Datum"] = new DateValue(date), ["CurrentDate"] = new DateValue(date),
|
||||
@@ -36,6 +35,14 @@ public static class LetterPlaceholderBuilder
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Mehrzeilige Anschrift (Name/Straße/PLZ Ort) für Adressvorschau im Dialog und
|
||||
/// den <c>Contact.Address</c>-Platzhalter - eine Formatierung für beide Verwendungen.</summary>
|
||||
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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<LetterContactChoice> Contacts { get; }
|
||||
public string AddressPreview => LetterPlaceholderBuilder.FormatAddress(SelectedContact?.Model);
|
||||
public bool HasAddressPreview => !string.IsNullOrWhiteSpace(AddressPreview);
|
||||
|
||||
public SelectContactDialogViewModel(IReadOnlyList<LetterContactChoice> contacts, LetterContactChoice? current)
|
||||
{
|
||||
Contacts = contacts;
|
||||
SelectedContact = current ?? contacts.FirstOrDefault();
|
||||
}
|
||||
|
||||
partial void OnSelectedContactChanged(LetterContactChoice? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(AddressPreview));
|
||||
OnPropertyChanged(nameof(HasAddressPreview));
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,17 @@
|
||||
Classes="emptyhint" IsVisible="{Binding HasNoTemplates}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Kontakt *" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding Contacts}" SelectedItem="{Binding SelectedContact}"
|
||||
DisplayMemberBinding="{Binding Display}" HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Text="Kein aktueller Kontakt vorhanden." Classes="emptyhint"
|
||||
IsVisible="{Binding HasNoContacts}"/>
|
||||
<TextBlock Text="Anschrift *" FontSize="12" Opacity="0.7"/>
|
||||
<Border BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="10">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding AddressPreview}" TextWrapping="Wrap"
|
||||
VerticalAlignment="Center" IsVisible="{Binding HasAddressPreview}"/>
|
||||
<TextBlock Grid.Column="0" Text="Kein aktueller Kontakt vorhanden." Classes="emptyhint"
|
||||
VerticalAlignment="Center" IsVisible="{Binding HasNoContacts}"/>
|
||||
<Button Grid.Column="1" Content="Kontakt wechseln …" Click="OnSelectContact"
|
||||
IsEnabled="{Binding !HasNoContacts}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Anrede *" FontSize="12" Opacity="0.7"/>
|
||||
|
||||
@@ -30,6 +30,14 @@ public partial class CreateLetterDialog : Window
|
||||
if (DataContext is CreateLetterDialogViewModel vm) await ConfigureAttendanceCalendar(vm);
|
||||
}
|
||||
|
||||
private async void OnSelectContact(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not CreateLetterDialogViewModel vm) return;
|
||||
var dialogVm = new SelectContactDialogViewModel(vm.Contacts, vm.SelectedContact);
|
||||
var dialog = new SelectContactDialog { DataContext = dialogVm };
|
||||
if (await dialog.ShowDialog<bool>(this)) vm.SelectedContact = dialogVm.SelectedContact;
|
||||
}
|
||||
|
||||
private async Task<bool> ConfigureAttendanceCalendar(CreateLetterDialogViewModel vm)
|
||||
{
|
||||
var configVm = new AttendanceCalendarConfigurationViewModel(vm.GetAttendanceCalendarOptions());
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||
x:Class="LehrerApp.Desktop.Views.Students.SelectContactDialog"
|
||||
x:DataType="vm:SelectContactDialogViewModel"
|
||||
Title="Kontakt wählen" Width="380" Height="440"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto,Auto" Margin="24">
|
||||
|
||||
<TextBlock Grid.Row="0" Text="Kontakt für Anrede und Anschrift" Classes="dialogtitle" Margin="0,0,0,12"/>
|
||||
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding Contacts}" SelectedItem="{Binding SelectedContact}"
|
||||
BorderThickness="1">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:LetterContactChoice">
|
||||
<TextBlock Text="{Binding Display}" Padding="4,2"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1" CornerRadius="6"
|
||||
Padding="10" Margin="0,12,0,0" IsVisible="{Binding HasAddressPreview}">
|
||||
<TextBlock Text="{Binding AddressPreview}" TextWrapping="Wrap" FontSize="12"/>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="3" ColumnDefinitions="*,10,*" Margin="0,16,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 SelectContactDialog : Window
|
||||
{
|
||||
public SelectContactDialog() => InitializeComponent();
|
||||
|
||||
private void OnApply(object? sender, RoutedEventArgs e) => Close(true);
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
Reference in New Issue
Block a user