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 Messages { get; } = []; 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; 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 BuildDecisions() => Conflicts .Select(conflict => new ImportDecision(conflict.Id, conflict.SelectedOption.Id)) .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 = ""; 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 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) { 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);