feat: Formulare-Menü mit Elternbrief-Einstieg + Arbeitsblatt-Personalisierung (Nutzer-Feedback)
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
Neue Menü-Kategorie "Formulare" in der Menüleiste: "Elternbrief erzeugen..." öffnet jetzt einen Schüler-Picker statt nur aus der Schülerdetailansicht erreichbar zu sein, "Arbeitsblatt personalisieren..." ist aktiv, sobald eine einzelne Lerngruppe geöffnet ist, und erzeugt aus einer in TemplateDesigner gebauten .lavorlage-Vorlage ein PDF je aktivem Gruppenmitglied - über eine von den Elternbrief-Vorlagen getrennte WorksheetTemplateStore-Bibliothek. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.PersonalizeWorksheetDialog"
|
||||
x:DataType="vm:PersonalizeWorksheetDialogViewModel"
|
||||
Title="Arbeitsblatt personalisieren"
|
||||
Width="460" Height="620"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" Margin="24">
|
||||
|
||||
<StackPanel Grid.Row="0" Spacing="4" Margin="0,0,0,14">
|
||||
<TextBlock Text="Arbeitsblatt personalisieren" Classes="dialogtitle"/>
|
||||
<TextBlock Text="{Binding GroupName}" FontSize="12" Opacity="0.65"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Spacing="10" Margin="0,0,0,14">
|
||||
<TextBlock Text="Vorlage" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding Templates}" SelectedItem="{Binding SelectedTemplate}"
|
||||
HorizontalAlignment="Stretch" DisplayMemberBinding="{Binding Name}"/>
|
||||
<TextBlock Text="Noch keine Arbeitsblatt-Vorlage importiert — in den Einstellungen unter „Briefvorlagen“ eine .lavorlage-Datei aus dem Vorlagen-Designer hinzufügen."
|
||||
Classes="emptyhint" TextWrapping="Wrap"
|
||||
IsVisible="{Binding HasNoTemplates}"/>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Für diese Schüler erzeugen" FontSize="12" FontWeight="SemiBold" Opacity="0.7" Margin="0,0,0,4"/>
|
||||
<ItemsControl ItemsSource="{Binding Students}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WorksheetStudentChoice">
|
||||
<CheckBox Content="{Binding FullName}" IsChecked="{Binding IsIncluded}" Margin="0,3"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Results}" Margin="0,10,0,0" IsVisible="{Binding HasResults}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WorksheetGenerationResult">
|
||||
<Grid ColumnDefinitions="20,*" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Icon}" Foreground="{Binding Color}"/>
|
||||
<TextBlock Grid.Column="1" FontSize="12">
|
||||
<Run Text="{Binding StudentName}"/><Run Text=" "/><Run Text="{Binding ErrorMessage}" Foreground="#D97706"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock Grid.Row="3" Text="{Binding StatusMessage}" FontSize="12" Margin="0,10,0,0" TextWrapping="Wrap"
|
||||
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
|
||||
<Grid Grid.Row="4" ColumnDefinitions="*,8,*" Margin="0,16,0,0">
|
||||
<Button Grid.Column="0" Content="Schließen" HorizontalAlignment="Stretch" Click="OnClose"/>
|
||||
<Button Grid.Column="2" Content="Erzeugen…" HorizontalAlignment="Stretch" Click="OnGenerate"
|
||||
IsEnabled="{Binding !HasNoTemplates}"/>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class PersonalizeWorksheetDialog : Window
|
||||
{
|
||||
public PersonalizeWorksheetDialog() => InitializeComponent();
|
||||
|
||||
private async void OnGenerate(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not PersonalizeWorksheetDialogViewModel vm) return;
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Zielordner für die personalisierten Arbeitsblätter wählen",
|
||||
AllowMultiple = false,
|
||||
});
|
||||
if (folders.Count == 0) return;
|
||||
vm.Generate(folders[0].Path.LocalPath);
|
||||
}
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -40,6 +40,12 @@
|
||||
<MenuItem Header="Klassenbuchabgleich…" Click="OnCompareUntisKlassenbuch"/>
|
||||
<MenuItem Header="Hausaufgabenabgleich…" Click="OnCompareUntisHausaufgaben"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Formulare">
|
||||
<MenuItem Header="Elternbrief erzeugen…" Click="OnCreateParentLetter"/>
|
||||
<MenuItem Header="Arbeitsblatt personalisieren…" Click="OnPersonalizeWorksheet"
|
||||
IsEnabled="{Binding CanPersonalizeWorksheet}"
|
||||
ToolTip.Tip="Nur verfügbar, während eine einzelne Lerngruppe geöffnet ist"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<DrawerPage x:Name="RootDrawer"
|
||||
DrawerLength="220"
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Threading;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Desktop.Views.Groups;
|
||||
using LehrerApp.Desktop.Views.Students;
|
||||
using LehrerApp.Desktop.Views.UntisHub;
|
||||
using LehrerApp.Sync;
|
||||
using LehrerApp.Templating;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views;
|
||||
@@ -45,6 +52,25 @@ public partial class MainWindow : Window
|
||||
await UntisHubActions.RunHausaufgabenAsync(this, App.Services.GetRequiredService<UntisHubService>());
|
||||
}
|
||||
|
||||
private async void OnCreateParentLetter(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
var picker = new StudentPickerDialog
|
||||
{ DataContext = new StudentPickerDialogViewModel(App.Services.GetRequiredService<IStudentRepository>()) };
|
||||
if (await picker.ShowDialog<Student?>(this) is { } student)
|
||||
await LetterDialogs.ShowCreateLetterDialogAsync(this, student);
|
||||
}
|
||||
|
||||
private async void OnPersonalizeWorksheet(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel { CurrentGroupDetail.Group: { } group } vm) return;
|
||||
var studentIds = vm.CurrentGroupDetail!.Students.Select(s => s.Id).ToList();
|
||||
var dialogVm = new PersonalizeWorksheetDialogViewModel(group, studentIds,
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<WorksheetTemplateStore>(),
|
||||
App.Services.GetRequiredService<ITemplateRenderer>());
|
||||
await new PersonalizeWorksheetDialog { DataContext = dialogVm }.ShowDialog(this);
|
||||
}
|
||||
|
||||
private void OnWindowKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel vm) return;
|
||||
|
||||
@@ -393,6 +393,66 @@
|
||||
<Separator/>
|
||||
<TextBlock Text="Vorlagen werden mit dem separaten LehrerApp Vorlagen-Designer erstellt. Designer und Hauptapp verwenden exakt dieselbe QuestPDF-Renderingbibliothek."
|
||||
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
|
||||
|
||||
<Separator Margin="0,8"/>
|
||||
<TextBlock Text="Arbeitsblatt-Vorlagen" FontSize="15" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Eigene Vorlagenbibliothek für „Arbeitsblatt personalisieren…“ im Formulare-Menü — getrennt von den Elternbrief-Vorlagen oben, damit sich beide Listen nicht vermischen."
|
||||
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
|
||||
<Button Grid.Column="0" Content="+ .lavorlage importieren" Click="OnImportWorksheetTemplateClick"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding WorksheetTemplateStatus}" FontSize="12"
|
||||
VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
IsVisible="{Binding WorksheetTemplateStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Noch keine Arbeitsblatt-Vorlage importiert." Classes="emptyhint"
|
||||
IsVisible="{Binding !WorksheetTemplateList.Count}"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding WorksheetTemplateList}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:LetterTemplateListItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="7" Padding="14,12" Margin="0,0,0,9">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="14"/>
|
||||
<TextBlock FontSize="11" Opacity="0.55">
|
||||
<Run Text="{Binding PackageFileName}"/><Run Text=" · "/>
|
||||
<Run Text="{Binding ValidationSummary}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Öffnen" FontSize="12" Padding="10,4"
|
||||
Margin="8,0,0,0" Tag="{Binding}" Click="OnOpenWorksheetTemplateClick"/>
|
||||
<Button Grid.Column="2" Content="Neu prüfen" FontSize="12" Padding="10,4"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).ValidateWorksheetTemplateCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Button Grid.Column="3" Content="Löschen" FontSize="12" Padding="10,4"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteWorksheetTemplateCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Issues}" IsVisible="{Binding HasIssues}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:LetterTemplateIssueItem">
|
||||
<Grid ColumnDefinitions="24,*" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Icon}" Foreground="{Binding Color}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="{Binding Color}"
|
||||
FontSize="12" TextWrapping="Wrap"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="✓ Vorlage ohne Auffälligkeiten" Foreground="SeaGreen" FontSize="12"
|
||||
IsVisible="{Binding HasNoIssues}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
@@ -277,4 +277,26 @@ public partial class SettingsView : UserControl
|
||||
if (!File.Exists(path)) return;
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||
}
|
||||
|
||||
private async void OnImportWorksheetTemplateClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null || DataContext is not SettingsViewModel vm) return;
|
||||
|
||||
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "LehrerApp-Arbeitsblattvorlage importieren",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = [new FilePickerFileType("LehrerApp-Vorlagen") { Patterns = ["*.lavorlage"] }],
|
||||
});
|
||||
if (files.Count > 0) vm.ImportWorksheetTemplate(files[0].Path.LocalPath);
|
||||
}
|
||||
|
||||
private void OnOpenWorksheetTemplateClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { Tag: LetterTemplateListItem item }) return;
|
||||
var path = item.Model.PackagePath;
|
||||
if (!File.Exists(path)) return;
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Desktop.Views.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using LehrerApp.Templating;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Students;
|
||||
|
||||
@@ -56,16 +55,7 @@ public partial class StudentDetailView : UserControl
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null || DataContext is not StudentDetailViewModel { Student: { } student }) return;
|
||||
|
||||
var vm = new CreateLetterDialogViewModel(student,
|
||||
App.Services.GetRequiredService<TemplateStore>(),
|
||||
App.Services.GetRequiredService<ITemplateRenderer>(),
|
||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>());
|
||||
var dialog = new CreateLetterDialog { DataContext = vm };
|
||||
var path = await dialog.ShowDialog<string?>(owner);
|
||||
if (!string.IsNullOrEmpty(path) && File.Exists(path))
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||
await LetterDialogs.ShowCreateLetterDialogAsync(owner, student);
|
||||
}
|
||||
|
||||
private void ShowAddressViewer(ContactItem contact)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||
xmlns:vmg="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Students.StudentPickerDialog"
|
||||
x:DataType="vm:StudentPickerDialogViewModel"
|
||||
Title="Schüler wählen"
|
||||
Width="380" Height="480"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
|
||||
|
||||
<StackPanel Grid.Row="0" Spacing="12" Margin="0,0,0,12">
|
||||
<TextBlock Text="Schüler wählen" Classes="dialogtitle"/>
|
||||
<TextBox Text="{Binding SearchText}"
|
||||
PlaceholderText="Schüler suchen …"
|
||||
x:Name="SearchBox"/>
|
||||
</StackPanel>
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
ItemsSource="{Binding Students}"
|
||||
SelectedItem="{Binding SelectedStudent}"
|
||||
BorderThickness="1">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vmg:StudentPickerItem">
|
||||
<TextBlock Text="{Binding FullName}" Padding="4,2"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="2" Spacing="12" Margin="0,16,0,0">
|
||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||
<Button Grid.Column="2" Content="Weiter" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Students;
|
||||
|
||||
public partial class StudentPickerDialog : Window
|
||||
{
|
||||
public StudentPickerDialog() => InitializeComponent();
|
||||
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
this.FindControl<TextBox>("SearchBox")?.Focus();
|
||||
}
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not StudentPickerDialogViewModel vm) return;
|
||||
vm.SelectCommand.Execute(null);
|
||||
if (vm.Result is not null) Close(vm.Result);
|
||||
}
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||
}
|
||||
Reference in New Issue
Block a user