feat: Importkonflikte gesammelt übernehmen
This commit is contained in:
@@ -56,6 +56,35 @@ public sealed class StudentImportDialogViewModelTests
|
|||||||
Assert.Single(stored);
|
Assert.Single(stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UebernehmenFuerAlle_WaehltBeiJedemEindeutigenKonfliktDenVorhandenenSchueler()
|
||||||
|
{
|
||||||
|
var stored = new List<Student>
|
||||||
|
{
|
||||||
|
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(
|
private static StudentImportService Service(
|
||||||
List<Student> stored,
|
List<Student> stored,
|
||||||
IReadOnlyList<ImportedStudent> imported) => new(
|
IReadOnlyList<ImportedStudent> imported) => new(
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public partial class StudentImportDialogViewModel : ObservableObject
|
|||||||
public ObservableCollection<StudentImportConflictItem> Conflicts { get; } = [];
|
public ObservableCollection<StudentImportConflictItem> Conflicts { get; } = [];
|
||||||
public bool HasMessages => Messages.Count > 0;
|
public bool HasMessages => Messages.Count > 0;
|
||||||
public bool HasConflicts => Conflicts.Count > 0;
|
public bool HasConflicts => Conflicts.Count > 0;
|
||||||
|
public bool CanUseExistingForAll => Conflicts.Any(conflict => conflict.HasSingleExistingStudentOption);
|
||||||
public bool IsReadyWithoutConflicts => !HasConflicts && _preview.CanApply;
|
public bool IsReadyWithoutConflicts => !HasConflicts && _preview.CanApply;
|
||||||
public bool HasBlockingErrors => !_preview.CanApply;
|
public bool HasBlockingErrors => !_preview.CanApply;
|
||||||
public bool CanApply => _preview.CanApply && !IsBusy;
|
public bool CanApply => _preview.CanApply && !IsBusy;
|
||||||
@@ -65,6 +66,16 @@ public partial class StudentImportDialogViewModel : ObservableObject
|
|||||||
.ToList()
|
.ToList()
|
||||||
.AsReadOnly();
|
.AsReadOnly();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wählt für alle eindeutigen Schüler-Konflikte den jeweils vorhandenen Datensatz.
|
||||||
|
/// Mehrdeutige Treffer und Lerngruppen-Konflikte bleiben unverändert.
|
||||||
|
/// </summary>
|
||||||
|
public void UseExistingForAll()
|
||||||
|
{
|
||||||
|
foreach (var conflict in Conflicts)
|
||||||
|
conflict.TrySelectSingleExistingStudent();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> TryApplyAsync()
|
public async Task<bool> TryApplyAsync()
|
||||||
{
|
{
|
||||||
Error = "";
|
Error = "";
|
||||||
@@ -116,6 +127,8 @@ public sealed class StudentImportMessageItem
|
|||||||
|
|
||||||
public partial class StudentImportConflictItem : ObservableObject
|
public partial class StudentImportConflictItem : ObservableObject
|
||||||
{
|
{
|
||||||
|
private const string ExistingStudentOptionPrefix = "existing:";
|
||||||
|
|
||||||
[ObservableProperty] private StudentImportConflictOptionItem _selectedOption;
|
[ObservableProperty] private StudentImportConflictOptionItem _selectedOption;
|
||||||
|
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
@@ -125,6 +138,11 @@ public partial class StudentImportConflictItem : ObservableObject
|
|||||||
public string ExistingValue { get; }
|
public string ExistingValue { get; }
|
||||||
public string SourceReference { get; }
|
public string SourceReference { get; }
|
||||||
public IReadOnlyList<StudentImportConflictOptionItem> Options { get; }
|
public IReadOnlyList<StudentImportConflictOptionItem> Options { get; }
|
||||||
|
public bool HasSingleExistingStudentOption => ExistingStudentOptions.Count == 1;
|
||||||
|
|
||||||
|
private IReadOnlyList<StudentImportConflictOptionItem> ExistingStudentOptions => Options
|
||||||
|
.Where(option => option.Id.StartsWith(ExistingStudentOptionPrefix, StringComparison.Ordinal))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
public StudentImportConflictItem(ImportConflict conflict)
|
public StudentImportConflictItem(ImportConflict conflict)
|
||||||
{
|
{
|
||||||
@@ -141,6 +159,14 @@ public partial class StudentImportConflictItem : ObservableObject
|
|||||||
.AsReadOnly();
|
.AsReadOnly();
|
||||||
_selectedOption = Options.First(option => option.Id == conflict.DefaultOptionId);
|
_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(
|
public sealed record StudentImportConflictOptionItem(
|
||||||
|
|||||||
@@ -34,8 +34,14 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid Grid.Row="1" RowDefinitions="Auto,*" Margin="0,16,0,0">
|
<Grid Grid.Row="1" RowDefinitions="Auto,*" Margin="0,16,0,0">
|
||||||
<TextBlock Grid.Row="0" Text="Entscheidungen" FontSize="15" FontWeight="SemiBold"
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,8"
|
||||||
Margin="0,0,0,8" IsVisible="{Binding HasConflicts}"/>
|
IsVisible="{Binding HasConflicts}">
|
||||||
|
<TextBlock Grid.Column="0" Text="Entscheidungen" FontSize="15" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" Content="Übernehmen für alle" Click="OnUseExistingForAll"
|
||||||
|
IsVisible="{Binding CanUseExistingForAll}"
|
||||||
|
ToolTip.Tip="Wählt bei allen eindeutigen Treffern den jeweils vorhandenen Schüler aus."/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Border Grid.Row="1" IsVisible="{Binding IsReadyWithoutConflicts}"
|
<Border Grid.Row="1" IsVisible="{Binding IsReadyWithoutConflicts}"
|
||||||
Background="#1422A06B" CornerRadius="6" Padding="14"
|
Background="#1422A06B" CornerRadius="6" Padding="14"
|
||||||
|
|||||||
@@ -14,5 +14,11 @@ public partial class StudentImportDialog : Window
|
|||||||
Close(true);
|
Close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnUseExistingForAll(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is StudentImportDialogViewModel vm)
|
||||||
|
vm.UseExistingForAll();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user