From 65c2211dab601ce7f086462ef8c904dbe249f7fe Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Tue, 18 Aug 2026 00:52:05 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Importkonflikte=20gesammelt=20=C3=BCber?= =?UTF-8?q?nehmen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudentImportDialogViewModelTests.cs | 29 +++++++++++++++++++ .../Students/StudentImportDialogViewModel.cs | 26 +++++++++++++++++ .../Views/Students/StudentImportDialog.axaml | 10 +++++-- .../Students/StudentImportDialog.axaml.cs | 6 ++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/LehrerApp.Desktop.Tests/StudentImportDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/StudentImportDialogViewModelTests.cs index d3e01f5..dddc24f 100644 --- a/LehrerApp.Desktop.Tests/StudentImportDialogViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/StudentImportDialogViewModelTests.cs @@ -56,6 +56,35 @@ public sealed class StudentImportDialogViewModelTests Assert.Single(stored); } + [Fact] + public async Task UebernehmenFuerAlle_WaehltBeiJedemEindeutigenKonfliktDenVorhandenenSchueler() + { + var stored = new List + { + new() { FirstName = "Max", LastName = "Beispiel" }, + new() { FirstName = "Erika", LastName = "Muster" }, + }; + var service = Service(stored, + [ + new ImportedStudent { FirstName = "Max", LastName = "Beispiel" }, + new ImportedStudent { FirstName = "Erika", LastName = "Muster" }, + ]); + var preview = await service.AnalyzeAsync(File()); + var vm = new StudentImportDialogViewModel(service, preview, "students.csv"); + + Assert.True(vm.CanUseExistingForAll); + Assert.All(vm.Conflicts, conflict => Assert.Equal("skip", conflict.SelectedOption.Id)); + + vm.UseExistingForAll(); + + Assert.All(vm.Conflicts, + conflict => Assert.StartsWith("existing:", conflict.SelectedOption.Id)); + Assert.True(await vm.TryApplyAsync()); + Assert.Equal(2, vm.Result?.MatchedExistingStudents); + Assert.Equal(0, vm.Result?.SkippedStudents); + Assert.Equal(2, stored.Count); + } + private static StudentImportService Service( List stored, IReadOnlyList imported) => new( diff --git a/LehrerApp.Desktop/ViewModels/Students/StudentImportDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/StudentImportDialogViewModel.cs index 44eb6ca..2162e48 100644 --- a/LehrerApp.Desktop/ViewModels/Students/StudentImportDialogViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Students/StudentImportDialogViewModel.cs @@ -30,6 +30,7 @@ public partial class StudentImportDialogViewModel : ObservableObject public ObservableCollection Conflicts { get; } = []; public bool HasMessages => Messages.Count > 0; public bool HasConflicts => Conflicts.Count > 0; + public bool CanUseExistingForAll => Conflicts.Any(conflict => conflict.HasSingleExistingStudentOption); public bool IsReadyWithoutConflicts => !HasConflicts && _preview.CanApply; public bool HasBlockingErrors => !_preview.CanApply; public bool CanApply => _preview.CanApply && !IsBusy; @@ -65,6 +66,16 @@ public partial class StudentImportDialogViewModel : ObservableObject .ToList() .AsReadOnly(); + /// + /// Wählt für alle eindeutigen Schüler-Konflikte den jeweils vorhandenen Datensatz. + /// Mehrdeutige Treffer und Lerngruppen-Konflikte bleiben unverändert. + /// + public void UseExistingForAll() + { + foreach (var conflict in Conflicts) + conflict.TrySelectSingleExistingStudent(); + } + public async Task TryApplyAsync() { Error = ""; @@ -116,6 +127,8 @@ public sealed class StudentImportMessageItem public partial class StudentImportConflictItem : ObservableObject { + private const string ExistingStudentOptionPrefix = "existing:"; + [ObservableProperty] private StudentImportConflictOptionItem _selectedOption; public string Id { get; } @@ -125,6 +138,11 @@ public partial class StudentImportConflictItem : ObservableObject public string ExistingValue { get; } public string SourceReference { get; } public IReadOnlyList Options { get; } + public bool HasSingleExistingStudentOption => ExistingStudentOptions.Count == 1; + + private IReadOnlyList ExistingStudentOptions => Options + .Where(option => option.Id.StartsWith(ExistingStudentOptionPrefix, StringComparison.Ordinal)) + .ToList(); public StudentImportConflictItem(ImportConflict conflict) { @@ -141,6 +159,14 @@ public partial class StudentImportConflictItem : ObservableObject .AsReadOnly(); _selectedOption = Options.First(option => option.Id == conflict.DefaultOptionId); } + + public bool TrySelectSingleExistingStudent() + { + var options = ExistingStudentOptions; + if (options.Count != 1) return false; + SelectedOption = options[0]; + return true; + } } public sealed record StudentImportConflictOptionItem( diff --git a/LehrerApp.Desktop/Views/Students/StudentImportDialog.axaml b/LehrerApp.Desktop/Views/Students/StudentImportDialog.axaml index 467acf3..d952165 100644 --- a/LehrerApp.Desktop/Views/Students/StudentImportDialog.axaml +++ b/LehrerApp.Desktop/Views/Students/StudentImportDialog.axaml @@ -34,8 +34,14 @@ - + + +