feat: add student master data import

This commit is contained in:
2026-08-17 13:12:03 +02:00
parent 9286bfa3b5
commit 37f4fee574
18 changed files with 2359 additions and 13 deletions
@@ -75,13 +75,14 @@
<TextBox Text="{Binding LetterSalutation}"
PlaceholderText="Briefanrede, z.B. Sehr geehrte Frau Mustermann,"
FontSize="12"/>
<!-- Zeile 2: Telefon + E-Mail -->
<!-- Zeile 2: Telefon + Mobiltelefon -->
<Grid ColumnDefinitions="*,10,*">
<TextBox Grid.Column="0" Text="{Binding Phone}"
PlaceholderText="Telefon / Handy" FontSize="12"/>
<TextBox Grid.Column="2" Text="{Binding Email}"
PlaceholderText="E-Mail" FontSize="12"/>
PlaceholderText="Festnetz" FontSize="12"/>
<TextBox Grid.Column="2" Text="{Binding MobilePhone}"
PlaceholderText="Mobiltelefon" FontSize="12"/>
</Grid>
<TextBox Text="{Binding Email}" PlaceholderText="E-Mail" FontSize="12"/>
<!-- Zeile 3: Adresse -->
<TextBox Text="{Binding Street}" PlaceholderText="Straße und Hausnummer" FontSize="12"/>
<Grid ColumnDefinitions="100,10,*">
@@ -39,14 +39,19 @@
<Grid ColumnDefinitions="*,10,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Telefon" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Phone}" PlaceholderText="Telefon / Handy"/>
<TextBox Text="{Binding Phone}" PlaceholderText="Festnetz"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="E-Mail" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Email}" PlaceholderText="E-Mail"/>
<TextBlock Text="Mobiltelefon" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding MobilePhone}" PlaceholderText="Mobiltelefon"/>
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="E-Mail" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Email}" PlaceholderText="E-Mail"/>
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Adresse" FontSize="14" FontWeight="SemiBold"/>
<TextBox Text="{Binding Street}" PlaceholderText="Straße und Hausnummer"/>
@@ -0,0 +1,103 @@
<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.StudentImportDialog"
x:DataType="vm:StudentImportDialogViewModel"
Title="Schüler importieren"
Width="820" Height="760" MinWidth="660" MinHeight="580"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
<StackPanel Grid.Row="0" Spacing="9">
<TextBlock Text="Schülerimport prüfen" Classes="dialogtitle"/>
<TextBlock Text="{Binding FileName}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding FormatName}" FontSize="12" Opacity="0.65"/>
<Border Background="#143B82F6" CornerRadius="6" Padding="12,10">
<StackPanel Spacing="4">
<TextBlock Text="{Binding Summary}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding GroupSummary}" FontSize="12" TextWrapping="Wrap"/>
<TextBlock Text="Vorhandene Werte bleiben erhalten; nur leere Stammdaten werden ergänzt."
FontSize="12" Opacity="0.75" TextWrapping="Wrap"/>
</StackPanel>
</Border>
<ScrollViewer MaxHeight="130" IsVisible="{Binding HasMessages}">
<ItemsControl ItemsSource="{Binding Messages}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:StudentImportMessageItem">
<TextBlock Text="{Binding Display}" Foreground="{Binding Foreground}"
TextWrapping="Wrap" FontSize="12" Margin="0,1"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
<Grid Grid.Row="1" RowDefinitions="Auto,*" Margin="0,16,0,0">
<TextBlock Grid.Row="0" Text="Entscheidungen" FontSize="15" FontWeight="SemiBold"
Margin="0,0,0,8" IsVisible="{Binding HasConflicts}"/>
<Border Grid.Row="1" IsVisible="{Binding IsReadyWithoutConflicts}"
Background="#1422A06B" CornerRadius="6" Padding="14"
VerticalAlignment="Top">
<TextBlock Text="Keine Konflikte der Import kann angewendet werden."
Foreground="#16845B" FontWeight="SemiBold"/>
</Border>
<Border Grid.Row="1" IsVisible="{Binding HasBlockingErrors}"
Background="#14C62828" CornerRadius="6" Padding="14"
VerticalAlignment="Top">
<TextBlock Text="Die Datei enthält Fehler. Bitte korrigiere sie und starte den Import erneut."
Foreground="#C62828" FontWeight="SemiBold" TextWrapping="Wrap"/>
</Border>
<ScrollViewer Grid.Row="1" IsVisible="{Binding HasConflicts}">
<ItemsControl ItemsSource="{Binding Conflicts}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:StudentImportConflictItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="1" CornerRadius="6" Padding="12" Margin="0,0,0,10">
<StackPanel Spacing="7">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="{Binding Title}" FontWeight="SemiBold"/>
<TextBlock Grid.Column="1" Text="{Binding SourceReference}" FontSize="11" Opacity="0.55"/>
</Grid>
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="12" Opacity="0.75"/>
<Grid ColumnDefinitions="100,*" RowDefinitions="Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Importiert:" FontSize="12" Opacity="0.6"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ImportedValue}" TextWrapping="Wrap" FontSize="12"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Vorhanden:" FontSize="12" Opacity="0.6"
IsVisible="{Binding ExistingValue, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ExistingValue}" TextWrapping="Wrap" FontSize="12"/>
</Grid>
<ComboBox ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:StudentImportConflictOptionItem">
<StackPanel Spacing="2">
<TextBlock Text="{Binding Label}"/>
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.6"
TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<StackPanel Grid.Row="2" Spacing="10" Margin="0,14,0,0">
<TextBlock Text="{Binding Error}" Foreground="#C62828" TextWrapping="Wrap"
IsVisible="{Binding Error, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Grid ColumnDefinitions="*,10,*">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Import anwenden" HorizontalAlignment="Stretch"
IsEnabled="{Binding CanApply}" Click="OnApply"/>
</Grid>
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Students;
namespace LehrerApp.Desktop.Views.Students;
public partial class StudentImportDialog : Window
{
public StudentImportDialog() => InitializeComponent();
private async void OnApply(object? sender, RoutedEventArgs e)
{
if (DataContext is StudentImportDialogViewModel vm && await vm.TryApplyAsync())
Close(true);
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
}
@@ -8,12 +8,14 @@
<Border Grid.Row="0" Padding="20,16"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto,Auto">
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<shared:PageHeader Grid.Column="0" Title="Schüler" Subtitle="{Binding CountSummary}"/>
<CheckBox Grid.Column="1" Content="Inaktive anzeigen"
IsChecked="{Binding ShowInactive}"
VerticalAlignment="Center" Margin="0,0,12,0"/>
<Button Grid.Column="2" Content=" Neuer Schüler"
<Button Grid.Column="2" Content="⇩ Importieren…" Click="OnImportClick"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<Button Grid.Column="3" Content=" Neuer Schüler"
Command="{Binding AddStudentCommand}" VerticalAlignment="Center"/>
</Grid>
</Border>
@@ -1,3 +1,71 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using LehrerApp.Core.Importing;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Students;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Students;
public partial class StudentListView : UserControl { public StudentListView() => InitializeComponent(); }
public partial class StudentListView : UserControl
{
private const int MaximumImportFileSize = 20 * 1024 * 1024;
public StudentListView() => InitializeComponent();
private async void OnImportClick(object? sender, RoutedEventArgs e)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null || DataContext is not StudentListViewModel list) return;
var service = App.Services.GetRequiredService<StudentImportService>();
var patterns = service.SupportedExtensions.Select(extension => $"*{extension}").ToArray();
var files = await owner.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Schüler-Stammdaten importieren",
AllowMultiple = false,
FileTypeFilter =
[
new FilePickerFileType("Unterstützte Schülerlisten")
{
Patterns = patterns.Length > 0 ? patterns : ["*.csv"],
},
],
});
if (files.Count == 0) return;
try
{
await using var source = await files[0].OpenReadAsync();
if (source.CanSeek && source.Length > MaximumImportFileSize)
throw new InvalidDataException("Die Importdatei ist größer als 20 MB.");
using var buffer = new MemoryStream();
await source.CopyToAsync(buffer);
if (buffer.Length > MaximumImportFileSize)
throw new InvalidDataException("Die Importdatei ist größer als 20 MB.");
var importFile = new ImportFile(files[0].Name, buffer.ToArray());
var preview = await Task.Run(async () =>
await service.AnalyzeAsync(importFile).ConfigureAwait(false));
var dialogVm = new StudentImportDialogViewModel(service, preview, files[0].Name);
var dialog = new StudentImportDialog { DataContext = dialogVm };
if (!await dialog.ShowDialog<bool>(owner)) return;
list.LoadStudents();
var result = dialogVm.Result!;
App.Services.GetRequiredService<NotificationService>().ShowSuccess(
$"Import abgeschlossen: {result.CreatedStudents} neu, {result.UpdatedStudents} ergänzt, "
+ $"{result.CreatedMemberships} Gruppenzuordnungen.");
}
catch (Exception ex) when (ex is ImportFormatException
or InvalidDataException
or IOException
or UnauthorizedAccessException)
{
App.Services.GetRequiredService<NotificationService>().ShowError(ex.Message);
}
}
}