feat: add student master data import
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Importing;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Data;
|
||||
using LehrerApp.Data.Repositories;
|
||||
@@ -111,6 +112,8 @@ public static class AppBootstrapper
|
||||
services.AddSingleton(_ => new PrivacySettingsService(appData));
|
||||
services.AddSingleton<AttendanceBalanceService>();
|
||||
services.AddSingleton<PersonalDataExportService>();
|
||||
services.AddSingleton<IImportHandler<ImportedStudent>, StudentMasterDataCsvImportHandler>();
|
||||
services.AddSingleton<StudentImportService>();
|
||||
|
||||
// ── Datenbank ─────────────────────────────────────────────────────────
|
||||
services.AddSingleton(_ => new LiteDbContext(DbPath, DbPassword));
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using LehrerApp.Core.Importing;
|
||||
using LehrerApp.Core.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Students;
|
||||
|
||||
public partial class StudentImportDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly StudentImportService _service;
|
||||
private readonly StudentImportPreview _preview;
|
||||
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private string _error = "";
|
||||
|
||||
public string FileName { get; }
|
||||
public string FormatName => _preview.FormatName;
|
||||
public int TotalStudents => _preview.Entries.Count;
|
||||
public int NewStudents => _preview.Entries.Count(entry =>
|
||||
entry.AutomaticResolution?.Kind == StudentImportResolutionKind.CreateNew);
|
||||
public int ExistingStudents => _preview.Entries.Count(entry =>
|
||||
entry.AutomaticResolution?.Kind == StudentImportResolutionKind.UseExisting);
|
||||
public int StudentsToSupplement => _preview.Entries.Count(entry => entry.FieldsToSupplement.Count > 0);
|
||||
public string Summary =>
|
||||
$"{TotalStudents} erkannt · {NewStudents} neu · {ExistingStudents} eindeutig vorhanden · "
|
||||
+ $"{StudentsToSupplement} mit Ergänzungen";
|
||||
|
||||
public string GroupSummary { get; }
|
||||
public ObservableCollection<StudentImportMessageItem> Messages { get; } = [];
|
||||
public ObservableCollection<StudentImportConflictItem> Conflicts { get; } = [];
|
||||
public bool HasMessages => Messages.Count > 0;
|
||||
public bool HasConflicts => Conflicts.Count > 0;
|
||||
public bool IsReadyWithoutConflicts => !HasConflicts && _preview.CanApply;
|
||||
public bool HasBlockingErrors => !_preview.CanApply;
|
||||
public bool CanApply => _preview.CanApply && !IsBusy;
|
||||
public StudentImportApplyResult? Result { get; private set; }
|
||||
|
||||
public StudentImportDialogViewModel(
|
||||
StudentImportService service,
|
||||
StudentImportPreview preview,
|
||||
string fileName)
|
||||
{
|
||||
_service = service;
|
||||
_preview = preview;
|
||||
FileName = fileName;
|
||||
|
||||
foreach (var message in preview.Messages)
|
||||
Messages.Add(new StudentImportMessageItem(message));
|
||||
foreach (var conflict in preview.Conflicts)
|
||||
Conflicts.Add(new StudentImportConflictItem(conflict));
|
||||
|
||||
var automaticGroups = preview.GroupAssignments.Count(group => group.AutomaticGroupId is not null);
|
||||
var unresolvedGroups = preview.GroupAssignments.Count(group => group.ConflictId is not null);
|
||||
var unassignedGroups = preview.GroupAssignments.Count - automaticGroups - unresolvedGroups;
|
||||
GroupSummary = preview.GroupAssignments.Count == 0
|
||||
? "Die Datei enthält keine Klassenangabe."
|
||||
: $"Lerngruppen: {automaticGroups} automatisch · {unresolvedGroups} zu entscheiden"
|
||||
+ (unassignedGroups > 0 ? $" · {unassignedGroups} nicht zugeordnet" : "");
|
||||
}
|
||||
|
||||
partial void OnIsBusyChanged(bool value) => OnPropertyChanged(nameof(CanApply));
|
||||
|
||||
public IReadOnlyList<ImportDecision> BuildDecisions() => Conflicts
|
||||
.Select(conflict => new ImportDecision(conflict.Id, conflict.SelectedOption.Id))
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public async Task<bool> TryApplyAsync()
|
||||
{
|
||||
Error = "";
|
||||
if (!_preview.CanApply)
|
||||
{
|
||||
Error = "Der Import enthält Fehler und kann nicht angewendet werden.";
|
||||
return false;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var decisions = BuildDecisions();
|
||||
Result = await Task.Run(async () =>
|
||||
await _service.ApplyAsync(_preview, decisions).ConfigureAwait(false));
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StudentImportMessageItem
|
||||
{
|
||||
public string Message { get; }
|
||||
public string SourceReference { get; }
|
||||
public string Display => SourceReference.Length == 0 ? Message : $"{SourceReference}: {Message}";
|
||||
public string Foreground { get; }
|
||||
|
||||
public StudentImportMessageItem(ImportMessage message)
|
||||
{
|
||||
Message = message.Message;
|
||||
SourceReference = message.SourceReference ?? "";
|
||||
Foreground = message.Severity switch
|
||||
{
|
||||
ImportMessageSeverity.Error => "#C62828",
|
||||
ImportMessageSeverity.Warning => "#B06A00",
|
||||
_ => "#2563EB",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partial class StudentImportConflictItem : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private StudentImportConflictOptionItem _selectedOption;
|
||||
|
||||
public string Id { get; }
|
||||
public string Title { get; }
|
||||
public string Description { get; }
|
||||
public string ImportedValue { get; }
|
||||
public string ExistingValue { get; }
|
||||
public string SourceReference { get; }
|
||||
public IReadOnlyList<StudentImportConflictOptionItem> Options { get; }
|
||||
|
||||
public StudentImportConflictItem(ImportConflict conflict)
|
||||
{
|
||||
Id = conflict.Id;
|
||||
Title = conflict.Title;
|
||||
Description = conflict.Description;
|
||||
ImportedValue = conflict.ImportedValue;
|
||||
ExistingValue = conflict.ExistingValue ?? "";
|
||||
SourceReference = conflict.SourceReference ?? "";
|
||||
Options = conflict.Options
|
||||
.Select(option => new StudentImportConflictOptionItem(
|
||||
option.Id, option.Label, option.Description ?? ""))
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
_selectedOption = Options.First(option => option.Id == conflict.DefaultOptionId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record StudentImportConflictOptionItem(
|
||||
string Id,
|
||||
string Label,
|
||||
string Description);
|
||||
@@ -572,14 +572,16 @@ public class ContactItem
|
||||
public string Relation { get; }
|
||||
public string LetterSalutation { get; }
|
||||
public string? Phone { get; }
|
||||
public string? MobilePhone { get; }
|
||||
public string? Email { get; }
|
||||
public string Address { get; }
|
||||
public bool HasPhone => !string.IsNullOrEmpty(Phone);
|
||||
public bool HasPhone => !string.IsNullOrEmpty(Phone) || !string.IsNullOrEmpty(MobilePhone);
|
||||
public bool HasEmail => !string.IsNullOrEmpty(Email);
|
||||
public bool HasAddress => !string.IsNullOrEmpty(Address);
|
||||
public bool IsInvalid => Model.InvalidSince.HasValue;
|
||||
public bool IsValid => !IsInvalid;
|
||||
public string PhoneDisplay => Phone ?? "";
|
||||
public string PhoneDisplay => string.Join(" · ", new[] { MobilePhone, Phone }
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||
public string EmailDisplay => Email ?? "";
|
||||
public string StatusText => IsInvalid
|
||||
? $"Ungültig seit {Model.InvalidSince:dd.MM.yyyy} · {InvalidReasonText(Model.InvalidReason)}"
|
||||
@@ -595,9 +597,10 @@ public class ContactItem
|
||||
Relation = c.Relation;
|
||||
LetterSalutation = c.LetterSalutation ?? "";
|
||||
Phone = c.Phone;
|
||||
MobilePhone = c.MobilePhone;
|
||||
Email = c.Email;
|
||||
Address = FormatAddress(c);
|
||||
CallCommand = new RelayCommand(() => OpenUri($"tel:{Phone}"), () => HasPhone);
|
||||
CallCommand = new RelayCommand(() => OpenUri($"tel:{MobilePhone ?? Phone}"), () => HasPhone);
|
||||
MailCommand = new RelayCommand(() => OpenUri($"mailto:{Email}"), () => HasEmail);
|
||||
}
|
||||
|
||||
@@ -696,6 +699,7 @@ public partial class ContactEntryViewModel : ObservableObject
|
||||
[ObservableProperty] private string _relation = "";
|
||||
[ObservableProperty] private string _letterSalutation = "";
|
||||
[ObservableProperty] private string _phone = "";
|
||||
[ObservableProperty] private string _mobilePhone = "";
|
||||
[ObservableProperty] private string _email = "";
|
||||
[ObservableProperty] private string _street = "";
|
||||
[ObservableProperty] private string _postalCode = "";
|
||||
@@ -711,6 +715,7 @@ public partial class ContactEntryViewModel : ObservableObject
|
||||
Relation = Relation.Trim(),
|
||||
LetterSalutation = string.IsNullOrWhiteSpace(LetterSalutation) ? null : LetterSalutation.Trim(),
|
||||
Phone = string.IsNullOrWhiteSpace(Phone) ? null : Phone.Trim(),
|
||||
MobilePhone = string.IsNullOrWhiteSpace(MobilePhone) ? null : MobilePhone.Trim(),
|
||||
Email = string.IsNullOrWhiteSpace(Email) ? null : Email.Trim(),
|
||||
Street = string.IsNullOrWhiteSpace(Street) ? null : Street.Trim(),
|
||||
PostalCode = string.IsNullOrWhiteSpace(PostalCode) ? null : PostalCode.Trim(),
|
||||
@@ -726,6 +731,7 @@ public partial class ContactEditDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private string _relation = "Elternteil";
|
||||
[ObservableProperty] private string _letterSalutation = "";
|
||||
[ObservableProperty] private string _phone = "";
|
||||
[ObservableProperty] private string _mobilePhone = "";
|
||||
[ObservableProperty] private string _email = "";
|
||||
[ObservableProperty] private string _street = "";
|
||||
[ObservableProperty] private string _postalCode = "";
|
||||
@@ -753,6 +759,7 @@ public partial class ContactEditDialogViewModel : ObservableObject
|
||||
Relation = source.Relation;
|
||||
LetterSalutation = source.LetterSalutation ?? "";
|
||||
Phone = source.Phone ?? "";
|
||||
MobilePhone = source.MobilePhone ?? "";
|
||||
Email = source.Email ?? "";
|
||||
Street = source.Street ?? "";
|
||||
PostalCode = source.PostalCode ?? "";
|
||||
@@ -804,6 +811,7 @@ public partial class ContactEditDialogViewModel : ObservableObject
|
||||
Relation = Relation.Trim(),
|
||||
LetterSalutation = NullIfEmpty(LetterSalutation),
|
||||
Phone = NullIfEmpty(Phone),
|
||||
MobilePhone = NullIfEmpty(MobilePhone),
|
||||
Email = NullIfEmpty(Email),
|
||||
Street = NullIfEmpty(Street),
|
||||
PostalCode = NullIfEmpty(PostalCode),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user