176 lines
6.6 KiB
C#
176 lines
6.6 KiB
C#
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 CanUseExistingForAll => Conflicts.Any(conflict => conflict.HasSingleExistingStudentOption);
|
|
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();
|
|
|
|
/// <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()
|
|
{
|
|
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
|
|
{
|
|
private const string ExistingStudentOptionPrefix = "existing:";
|
|
|
|
[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 bool HasSingleExistingStudentOption => ExistingStudentOptions.Count == 1;
|
|
|
|
private IReadOnlyList<StudentImportConflictOptionItem> ExistingStudentOptions => Options
|
|
.Where(option => option.Id.StartsWith(ExistingStudentOptionPrefix, StringComparison.Ordinal))
|
|
.ToList();
|
|
|
|
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 bool TrySelectSingleExistingStudent()
|
|
{
|
|
var options = ExistingStudentOptions;
|
|
if (options.Count != 1) return false;
|
|
SelectedOption = options[0];
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public sealed record StudentImportConflictOptionItem(
|
|
string Id,
|
|
string Label,
|
|
string Description);
|