using Avalonia.Controls; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Platform; using Avalonia.Platform.Storage; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.Views.Shared; using Microsoft.Extensions.DependencyInjection; namespace LehrerApp.Desktop.Views.Groups; public partial class PlanningTabView : UserControl { private static readonly FilePickerFileType JsonFileType = new("JSON-Dateien") { Patterns = ["*.json"], MimeTypes = ["application/json"], }; public PlanningTabView() => InitializeComponent(); protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is PlanningTabViewModel vm) { vm.OnAddUnit = groupId => ShowUnitDialog(groupId, editingUnit: null); vm.OnEditUnit = unit => ShowUnitDialog(unit.GroupId, editingUnit: unit); vm.OnConfirmDeleteUnit = ShowDeleteUnitDialog; vm.OnPickCopyTarget = ShowCopyUnitDialog; vm.OnAddLesson = (unitId, groupId, materials, shorthands) => ShowLessonDialog(unitId, groupId, materials, shorthands, editingLesson: null); vm.OnEditLesson = (lesson, materials, shorthands) => ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson); vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog; vm.OnPickMoveTarget = ShowMoveLessonDialog; vm.OnShowLesson = ShowLessonViewerDialog; vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog; vm.OnAiAssist = ShowAiAssistDialog; } } private async Task ShowUnitDialog(Guid groupId, Unit? editingUnit) { if (DataContext is not PlanningTabViewModel vm) return false; var dialogVm = new UnitDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), groupId, vm.SubjectId, vm.GradeLevel, vm.GroupLabel, vm.SubjectName, editingUnit); var dialog = new UnitDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return false; var ok = await dialog.ShowDialog(owner); return ok && dialogVm.Result is not null; } private async Task ShowDeleteUnitDialog(UnitSummary unit) { var info = new ConfirmDialogInfo { Title = "Einheit löschen?", Message = $"\"{unit.Title}\" wird inkl. aller zugehörigen Stunden endgültig gelöscht.", ConfirmText = "Löschen", }; var dialog = new ConfirmDialog { DataContext = info }; var owner = TopLevel.GetTopLevel(this) as Window; return owner is not null && await dialog.ShowDialog(owner); } private async Task ShowCopyUnitDialog(Unit unit) { var dialogVm = new CopyUnitDialogViewModel( App.Services.GetRequiredService(), unit.GroupId); var dialog = new CopyUnitDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return null; var ok = await dialog.ShowDialog(owner); return ok ? dialogVm.Result : null; } private async Task ShowLessonDialog(Guid unitId, Guid groupId, List materialSuggestions, List shorthandHistorySuggestions, Lesson? editingLesson) { if (DataContext is not PlanningTabViewModel vm) return false; var dialogVm = new LessonDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), unitId, groupId, vm.GroupLabel, vm.SubjectName, materialSuggestions, shorthandHistorySuggestions, editingLesson); var dialog = new LessonDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return false; var ok = await dialog.ShowDialog(owner); return ok && dialogVm.Result is not null; } private async Task ShowDeleteLessonDialog(LessonSummary lesson) { var info = new ConfirmDialogInfo { Title = "Stunde löschen?", Message = $"Die Stunde vom {lesson.DateDisplay} wird endgültig gelöscht.", ConfirmText = "Löschen", }; var dialog = new ConfirmDialog { DataContext = info }; var owner = TopLevel.GetTopLevel(this) as Window; return owner is not null && await dialog.ShowDialog(owner); } private async Task ShowMoveLessonDialog(Lesson lesson) { var dialogVm = new MoveLessonDialogViewModel(lesson.Date); var dialog = new MoveLessonDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return null; var ok = await dialog.ShowDialog(owner); return ok ? dialogVm.Result : null; } private async Task ShowLessonViewerDialog(Lesson lesson) { var viewerVm = new LessonViewerViewModel(lesson, App.Services.GetRequiredService()); var dialog = new LessonViewerDialog { DataContext = viewerVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is not null) await dialog.ShowDialog(owner); } private async Task ShowGenerateLessonSeriesDialog(Unit unit) { var dialogVm = new GenerateLessonSeriesDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), unit.Id, unit.GroupId, unit.StartDate, unit.EndDate); var dialog = new GenerateLessonSeriesDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return null; var ok = await dialog.ShowDialog(owner); if (ok && dialogVm.Result is { } result) App.Services.GetRequiredService().ShowSuccess(result.Summary); return ok ? dialogVm.Result : null; } private async Task ShowAiAssistDialog(Unit unit) { var dialogVm = new AiAssistDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), unit); var dialog = new AiAssistDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return false; await dialog.ShowDialog(owner); return dialogVm.Result; } private async void OnExportUnit(object? sender, RoutedEventArgs e) => await RunExchange(async () => { if (DataContext is not PlanningTabViewModel { SelectedUnit: { } selected } vm) return; var topLevel = TopLevel.GetTopLevel(this); if (topLevel is null) return; var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Unterrichtseinheit exportieren", SuggestedFileName = $"Einheit_{SafeFileName(selected.Title)}.json", FileTypeChoices = [JsonFileType], }); if (file is null) return; var json = Exchange.ExportUnit(selected.Model, CreateExchangeContext(vm, selected.Title)); await WriteTextAsync(file, json); Notifications.ShowSuccess("Unterrichtseinheit als JSON exportiert."); }); private async void OnImportUnit(object? sender, RoutedEventArgs e) => await RunExchange(async () => { if (DataContext is not PlanningTabViewModel { CanImportUnitPlanning: true } vm) return; var file = await PickJsonFile("Neue Unterrichtseinheit importieren"); if (file is null) return; var result = Exchange.ImportUnit(await ReadTextAsync(file), vm.GroupId); vm.RefreshPlanning(result.Unit.Id, result.Lessons.FirstOrDefault()?.Id); Notifications.ShowSuccess($"Einheit „{result.Unit.Title}“ mit {result.Lessons.Count} Stunde(n) importiert."); }); private async void OnExportLesson(object? sender, RoutedEventArgs e) => await RunExchange(async () => { if (DataContext is not PlanningTabViewModel { SelectedLesson: { } lesson } vm) return; var topLevel = TopLevel.GetTopLevel(this); if (topLevel is null) return; var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Stundenplanung exportieren", SuggestedFileName = $"Stunde_{lesson.Model.Date:yyyy-MM-dd}_{SafeFileName(lesson.Topic)}.json", FileTypeChoices = [JsonFileType], }); if (file is null) return; var json = Exchange.ExportLesson(lesson.Model, CreateExchangeContext(vm, vm.SelectedUnit?.Title)); await WriteTextAsync(file, json); Notifications.ShowSuccess("Stundenplanung als JSON exportiert."); }); private async void OnImportLesson(object? sender, RoutedEventArgs e) => await RunExchange(async () => { if (DataContext is not PlanningTabViewModel { CanImportLessonPlanning: true, SelectedUnit: { } unit } vm) return; var file = await PickJsonFile("Neue Stundenplanung importieren"); if (file is null) return; var lesson = Exchange.ImportLesson(await ReadTextAsync(file), unit.Id, vm.GroupId); vm.RefreshPlanning(unit.Id, lesson.Id); Notifications.ShowSuccess($"Stunde „{lesson.Topic}“ importiert."); }); private async void OnCopyUnitFormat(object? sender, RoutedEventArgs e) => await CopyFormatDescription("Einheitenplanung-Format.md", "Formatbeschreibung für Einheiten kopiert."); private async void OnCopyLessonFormat(object? sender, RoutedEventArgs e) => await CopyFormatDescription("Stundenplanung-Format.md", "Formatbeschreibung für Stunden kopiert."); private async Task CopyFormatDescription(string fileName, string successMessage) => await RunExchange(async () => { var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; if (clipboard is null) throw new InvalidOperationException("Die Zwischenablage ist nicht verfügbar."); var uri = new Uri($"avares://LehrerApp.Desktop/Assets/PlanningFormats/{fileName}"); await using var stream = AssetLoader.Open(uri); using var reader = new StreamReader(stream); await clipboard.SetTextAsync(await reader.ReadToEndAsync()); Notifications.ShowSuccess(successMessage); }); private async Task PickJsonFile(string title) { var topLevel = TopLevel.GetTopLevel(this); if (topLevel is null) return null; var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = title, AllowMultiple = false, FileTypeFilter = [JsonFileType], }); return files.FirstOrDefault(); } private static async Task ReadTextAsync(IStorageFile file) { await using var stream = await file.OpenReadAsync(); using var reader = new StreamReader(stream); return await reader.ReadToEndAsync(); } private static async Task WriteTextAsync(IStorageFile file, string content) { await using var stream = await file.OpenWriteAsync(); stream.SetLength(0); await using var writer = new StreamWriter(stream); await writer.WriteAsync(content); } private static PlanningExchangeContext CreateExchangeContext(PlanningTabViewModel vm, string? unitTitle) => new() { Group = vm.GroupLabel, Subject = vm.SubjectName, GradeLevel = vm.GradeLevel, UnitTitle = unitTitle, }; private static string SafeFileName(string value) { var invalid = Path.GetInvalidFileNameChars().ToHashSet(); var safe = new string(value.Select(c => invalid.Contains(c) ? '_' : c).ToArray()).Trim(); return string.IsNullOrWhiteSpace(safe) ? "Planung" : safe; } private async Task RunExchange(Func action) { try { await action(); } catch (Exception ex) when (ex is PlanningExchangeException or IOException or UnauthorizedAccessException or InvalidOperationException) { Notifications.ShowError(ex.Message); } } private static PlanningExchangeService Exchange => App.Services.GetRequiredService(); private static NotificationService Notifications => App.Services.GetRequiredService(); }