This commit is contained in:
@@ -191,6 +191,49 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable
|
|||||||
Assert.True(vm.Generate(output));
|
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) =>
|
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([]));
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ public static class LetterPlaceholderBuilder
|
|||||||
string letterText, string teacherName, DrawingValue? attendanceCalendar = null,
|
string letterText, string teacherName, DrawingValue? attendanceCalendar = null,
|
||||||
DrawingValue? absenceDays = null)
|
DrawingValue? absenceDays = null)
|
||||||
{
|
{
|
||||||
var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City }.Where(x => !string.IsNullOrWhiteSpace(x)));
|
var address = FormatAddress(contact);
|
||||||
var address = string.Join(Environment.NewLine, new[] { contact?.Name, contact?.Street, cityLine }.Where(x => !string.IsNullOrWhiteSpace(x)));
|
|
||||||
return new Dictionary<string, PlaceholderValue>(StringComparer.Ordinal)
|
return new Dictionary<string, PlaceholderValue>(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
["Datum"] = new DateValue(date), ["CurrentDate"] = new DateValue(date),
|
["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
|
public static bool IsEmpty(PlaceholderValue value) => value switch
|
||||||
{
|
{
|
||||||
TextValue x => string.IsNullOrWhiteSpace(x.Value),
|
TextValue x => string.IsNullOrWhiteSpace(x.Value),
|
||||||
|
|||||||
@@ -37,9 +37,14 @@ public static class StudentAttendanceCalendarDrawingBuilder
|
|||||||
AttendanceCalendarSize.Large => 1f,
|
AttendanceCalendarSize.Large => 1f,
|
||||||
_ => .85f,
|
_ => .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 cellHeight = 10 * scale;
|
||||||
var monthGapX = 6 * scale;
|
var monthGapX = 10f;
|
||||||
var first = options.NormalizedStartMonth;
|
var first = options.NormalizedStartMonth;
|
||||||
var monthCount = options.NormalizedMonthCount;
|
var monthCount = options.NormalizedMonthCount;
|
||||||
var monthWidth = (contentWidth - monthGapX * (monthCount - 1)) / monthCount;
|
var monthWidth = (contentWidth - monthGapX * (monthCount - 1)) / monthCount;
|
||||||
@@ -112,8 +117,10 @@ public static class StudentAbsenceDayListDrawingBuilder
|
|||||||
AttendanceCalendarSize.Large => 1f,
|
AttendanceCalendarSize.Large => 1f,
|
||||||
_ => .88f,
|
_ => .88f,
|
||||||
};
|
};
|
||||||
var contentWidth = width * scale;
|
// Breite bleibt an die DRAWBOX-Deklaration im Layout-Skript gebunden (siehe
|
||||||
var rowHeight = 9 * scale;
|
// StudentAttendanceCalendarDrawingBuilder) - nur Zeilenhöhe/Schrift skalieren mit "Größe".
|
||||||
|
var contentWidth = width;
|
||||||
|
var rowHeight = 12 * scale;
|
||||||
var start = options.NormalizedStartMonth;
|
var start = options.NormalizedStartMonth;
|
||||||
var end = start.AddMonths(options.NormalizedMonthCount).AddDays(-1);
|
var end = start.AddMonths(options.NormalizedMonthCount).AddDays(-1);
|
||||||
var rows = absences
|
var rows = absences
|
||||||
@@ -130,17 +137,17 @@ public static class StudentAbsenceDayListDrawingBuilder
|
|||||||
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"));
|
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280"));
|
||||||
y += 10 * scale + 2 * scale;
|
y += 10 * scale + 2 * scale;
|
||||||
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6"));
|
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));
|
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));
|
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));
|
DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
||||||
y += rowHeight;
|
y += rowHeight;
|
||||||
|
|
||||||
if (rows.Count == 0)
|
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,
|
"Keine Fehltage im gewählten Zeitraum", DrawingTextAlignment.AlignLeft, 8 * scale,
|
||||||
Color: "#6B7280", Italic: true));
|
Color: "#6B7280", Italic: true));
|
||||||
y += rowHeight + 3 * scale;
|
y += rowHeight + 3 * scale;
|
||||||
@@ -163,12 +170,12 @@ public static class StudentAbsenceDayListDrawingBuilder
|
|||||||
: row.FriendlyStatusLabel;
|
: row.FriendlyStatusLabel;
|
||||||
var statusColor = row.IsUnexcused ? "#C62828" : "#2E7D32";
|
var statusColor = row.IsUnexcused ? "#C62828" : "#2E7D32";
|
||||||
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#E5E7EB", .3f, fill));
|
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,
|
row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale,
|
||||||
Color: "#374151"));
|
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"));
|
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,
|
status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor,
|
||||||
Bold: row.IsUnexcused));
|
Bold: row.IsUnexcused));
|
||||||
y += rowHeight;
|
y += rowHeight;
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject
|
|||||||
public bool HasCustomPlaceholders => CustomPlaceholders.Count > 0;
|
public bool HasCustomPlaceholders => CustomPlaceholders.Count > 0;
|
||||||
public bool HasNoTemplates => Templates.Count == 0;
|
public bool HasNoTemplates => Templates.Count == 0;
|
||||||
public bool HasNoContacts => Contacts.Count == 0;
|
public bool HasNoContacts => Contacts.Count == 0;
|
||||||
|
public string AddressPreview => LetterPlaceholderBuilder.FormatAddress(SelectedContact?.Model);
|
||||||
|
public bool HasAddressPreview => !string.IsNullOrWhiteSpace(AddressPreview);
|
||||||
public bool UsesAttendanceAdvancedContent => UsesAttendanceCalendar || UsesAbsenceDayList;
|
public bool UsesAttendanceAdvancedContent => UsesAttendanceCalendar || UsesAbsenceDayList;
|
||||||
public string SuggestedFileName => SanitizeFileName(
|
public string SuggestedFileName => SanitizeFileName(
|
||||||
$"{SelectedTemplate?.Name ?? "Elternbrief"}_{_student.LastName}_{_student.FirstName}.pdf");
|
$"{SelectedTemplate?.Name ?? "Elternbrief"}_{_student.LastName}_{_student.FirstName}.pdf");
|
||||||
@@ -82,6 +84,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject
|
|||||||
partial void OnSelectedContactChanged(LetterContactChoice? value)
|
partial void OnSelectedContactChanged(LetterContactChoice? value)
|
||||||
{
|
{
|
||||||
Anrede = value?.Model.LetterSalutation ?? "";
|
Anrede = value?.Model.LetterSalutation ?? "";
|
||||||
|
OnPropertyChanged(nameof(AddressPreview));
|
||||||
|
OnPropertyChanged(nameof(HasAddressPreview));
|
||||||
RefreshValidation();
|
RefreshValidation();
|
||||||
}
|
}
|
||||||
partial void OnAnredeChanged(string value) => 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}"/>
|
Classes="emptyhint" IsVisible="{Binding HasNoTemplates}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Kontakt *" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Anschrift *" FontSize="12" Opacity="0.7"/>
|
||||||
<ComboBox ItemsSource="{Binding Contacts}" SelectedItem="{Binding SelectedContact}"
|
<Border BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="10">
|
||||||
DisplayMemberBinding="{Binding Display}" HorizontalAlignment="Stretch"/>
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock Text="Kein aktueller Kontakt vorhanden." Classes="emptyhint"
|
<TextBlock Grid.Column="0" Text="{Binding AddressPreview}" TextWrapping="Wrap"
|
||||||
IsVisible="{Binding HasNoContacts}"/>
|
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>
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Anrede *" FontSize="12" Opacity="0.7"/>
|
<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);
|
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)
|
private async Task<bool> ConfigureAttendanceCalendar(CreateLetterDialogViewModel vm)
|
||||||
{
|
{
|
||||||
var configVm = new AttendanceCalendarConfigurationViewModel(vm.GetAttendanceCalendarOptions());
|
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