using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Platform.Storage; using LehrerApp.Core.Importing; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.Views.Shared; using LehrerApp.Desktop.Views.Students; using LehrerApp.Desktop.Views.Workload; using Microsoft.Extensions.DependencyInjection; using System.Text; namespace LehrerApp.Desktop.Views.Groups; public partial class GroupDetailView : UserControl { private const int MaximumImportFileSize = 20 * 1024 * 1024; public GroupDetailView() => InitializeComponent(); protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is GroupDetailViewModel vm) { vm.OnAddStudent = ShowAddStudentDialog; vm.OnWithdrawStudent = ShowWithdrawStudentDialog; vm.OnAddExam = ShowAddExamDialog; vm.OnEditExam = ShowEditExamDialog; vm.OnDuplicateExam = ShowDuplicateExamDialog; vm.OnConfirmDeleteExam = ShowDeleteExamDialog; vm.OnGradeExam = ShowGradeExamDialog; vm.OnEvaluateExam = ShowEvaluateExamDialog; vm.OnConfirmReactivate = ShowReactivateConfirmDialog; vm.OverviewTab.OnNavigateToWorkload = () => App.Services.GetRequiredService().NavigateToWorkload(); vm.OverviewTab.OnAddGroupTask = ShowAddGroupTaskDialog; } } private async Task ShowAddGroupTaskDialog(Guid groupId) { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return null; return await WorkTaskDialogHelper.ShowDialog(owner, preselectedGroupId: groupId); } private async Task ShowReactivateConfirmDialog() { var dialog = new ConfirmDialog { DataContext = new ConfirmDialogInfo { Title = "Archivierte Gruppe wieder aktivieren?", Message = "Nach der Wiederaktivierung können historische Daten dieser Gruppe wieder verändert werden.", ConfirmText = "Wieder aktivieren", }, }; var owner = TopLevel.GetTopLevel(this) as Window; return owner is not null && await dialog.ShowDialog(owner); } private async Task ShowAddStudentDialog() { if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false; var dialogVm = new AddStudentToGroupDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), vm.Group.Id); var dialog = new AddStudentToGroupDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return false; return await dialog.ShowDialog(owner); } private async void OnImportParticipantsClick(object? sender, RoutedEventArgs e) { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null || DataContext is not GroupDetailViewModel vm || vm.Group is null) return; var service = App.Services.GetRequiredService(); var patterns = service.SupportedExtensions.Select(extension => $"*{extension}").ToArray(); var files = await owner.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = $"Teilnehmer für {vm.Group.Name} 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()); await ShowStudentImportPreview(owner, vm, importFile, files[0].Name); } catch (Exception ex) when (ex is ImportFormatException or InvalidDataException or IOException or UnauthorizedAccessException) { App.Services.GetRequiredService().ShowError(ex.Message); } } private async void OnImportParticipantsFromWebUntisClick(object? sender, RoutedEventArgs e) { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null || DataContext is not GroupDetailViewModel vm || vm.Group is null) return; try { var untis = App.Services.GetRequiredService(); var selectionVm = new WebUntisClassSelectionViewModel(untis); var selection = new WebUntisClassSelectionDialog { DataContext = selectionVm }; await selectionVm.InitializeAsync(); if (!await selection.ShowDialog(owner) || selectionVm.SelectedClass is null) return; var report = await untis.GetStudentsAsync(selectionVm.SelectedClass.Name); var importFile = BuildWebUntisStudentImport(report); await ShowStudentImportPreview(owner, vm, importFile, $"WebUntis · {selectionVm.SelectedClass.Name}"); } catch (WebUntisIntegrationException ex) { App.Services.GetRequiredService().ShowError(ex.Message); } catch (Exception ex) when (ex is ImportFormatException or InvalidDataException) { App.Services.GetRequiredService().ShowError(ex.Message); } } private async void OnCompareWebUntisAbsencesClick(object? sender, RoutedEventArgs e) { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null || DataContext is not GroupDetailViewModel vm || vm.Group is null) return; var dialogVm = new WebUntisAbsenceComparisonViewModel(vm.Group, App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService()); await new WebUntisAbsenceComparisonDialog { DataContext = dialogVm }.ShowDialog(owner); vm.ParticipationTab.RefreshCurrentGrid(); } private static ImportFile BuildWebUntisStudentImport(UntisStudentReportDto report) { var builder = new StringBuilder(); builder.AppendLine("longName\tforeName\tgender\tbirthDate\tklasse.name\texternKey\taddress.email\taddress.mobile\taddress.phone\taddress.city\taddress.postCode\taddress.street"); foreach (var student in report.Students) { var values = new[] { student.LongName ?? student.Name, student.ForeName, student.Gender, student.BirthDate?.ToString() ?? student.BirthDateRaw, student.ClassName, student.ExternKey.ToString(), student.Address.Email, student.Address.Mobile, student.Address.Phone, student.Address.City, student.Address.PostCode, student.Address.Street, }; builder.AppendLine(string.Join('\t', values.Select(SafeTsv))); } return new ImportFile("webuntis-students.csv", Encoding.UTF8.GetBytes(builder.ToString())); } private static string SafeTsv(string? value) => (value ?? "").Replace('\t', ' ') .Replace('\r', ' ').Replace('\n', ' '); private static async Task ShowStudentImportPreview(Window owner, GroupDetailViewModel vm, ImportFile importFile, string sourceName) { var service = App.Services.GetRequiredService(); var preview = await Task.Run(async () => await service.AnalyzeAsync(importFile, vm.Group!.Id).ConfigureAwait(false)); var dialogVm = new StudentImportDialogViewModel(service, preview, sourceName); var dialog = new StudentImportDialog { DataContext = dialogVm }; if (!await dialog.ShowDialog(owner)) return; vm.LoadStudents(); vm.ParticipationTab.RefreshCurrentGrid(); var result = dialogVm.Result!; App.Services.GetRequiredService().ShowSuccess( $"Teilnehmerimport abgeschlossen: {result.CreatedStudents} neu, " + $"{result.UpdatedStudents} ergänzt, {result.CreatedMemberships} zugeordnet."); } private async Task ShowWithdrawStudentDialog(StudentSummary student) { if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false; var memberships = App.Services.GetRequiredService(); var membership = memberships.GetByStudentAndGroup(student.Id, vm.Group.Id); if (membership is null) return false; var dialogVm = new WithdrawStudentDialogViewModel( memberships, membership, student.FullName, vm.Group.Name); var dialog = new WithdrawStudentDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; return owner is not null && await dialog.ShowDialog(owner); } private Task ShowAddExamDialog(Guid groupId) => ShowExamDialog(groupId, editingExam: null, duplicateSource: null); private Task ShowEditExamDialog(Exam exam) => ShowExamDialog(exam.GroupId, editingExam: exam, duplicateSource: null); private Task ShowDuplicateExamDialog(Exam exam) => ShowExamDialog(exam.GroupId, editingExam: null, duplicateSource: exam); private async Task ShowExamDialog(Guid groupId, Exam? editingExam, Exam? duplicateSource) { if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false; var dialogVm = new ExamDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem, vm.SubjectName, vm.Group.IsDifferentiated, editingExam, duplicateSource); var dialog = new ExamDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return false; return await dialog.ShowDialog(owner); } private async Task ShowDeleteExamDialog(ExamSummary exam) { var dialog = new DeleteExamDialog { DataContext = exam.Title }; var owner = TopLevel.GetTopLevel(this) as Window; return owner is not null && await dialog.ShowDialog(owner); } private async Task ShowGradeExamDialog(Exam exam) { if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return; var dialogVm = new ExamGradingDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), exam, vm.Group.Id); var dialog = new ExamGradingDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is not null) await dialog.ShowDialog(owner); } private async Task ShowEvaluateExamDialog(Exam exam) { if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return; var dialogVm = new ExamEvaluationDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), exam, vm.Group.GradingSystem); var dialog = new ExamEvaluationDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is not null) await dialog.ShowDialog(owner); } }