Bisher war die Einheit einer Stunde nach dem Anlegen fix - bei falscher Auswahl im Erstellen-Dialog blieb nur Löschen+Neuanlegen. Neuer Button öffnet einen Dialog zur Auswahl der Ziel-Einheit; die Ansicht springt danach automatisch dorthin mit der Stunde vorausgewählt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
334 lines
14 KiB
C#
334 lines
14 KiB
C#
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.OnPickUnitChange = ShowChangeLessonUnitDialog;
|
|
vm.OnShowLesson = ShowLessonViewerDialog;
|
|
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
|
|
vm.OnAiAssist = ShowAiAssistDialog;
|
|
vm.OnNotify = Notifications.ShowSuccess;
|
|
vm.OnError = Notifications.ShowError;
|
|
}
|
|
}
|
|
|
|
private async Task<bool> ShowUnitDialog(Guid groupId, Unit? editingUnit)
|
|
{
|
|
if (DataContext is not PlanningTabViewModel vm) return false;
|
|
|
|
var dialogVm = new UnitDialogViewModel(
|
|
App.Services.GetRequiredService<IUnitRepository>(),
|
|
App.Services.GetRequiredService<ICompetencyDomainRepository>(),
|
|
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<bool>(owner);
|
|
return ok && dialogVm.Result is not null;
|
|
}
|
|
|
|
private async Task<bool> 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<bool>(owner);
|
|
}
|
|
|
|
private async Task<CopyUnitTarget?> ShowCopyUnitDialog(Unit unit)
|
|
{
|
|
var dialogVm = new CopyUnitDialogViewModel(
|
|
App.Services.GetRequiredService<IGroupRepository>(), 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<bool>(owner);
|
|
return ok ? dialogVm.Result : null;
|
|
}
|
|
|
|
private async Task<bool> ShowLessonDialog(Guid unitId, Guid groupId,
|
|
List<string> materialSuggestions, List<string> shorthandHistorySuggestions, Lesson? editingLesson)
|
|
{
|
|
if (DataContext is not PlanningTabViewModel vm) return false;
|
|
|
|
var dialogVm = new LessonDialogViewModel(
|
|
App.Services.GetRequiredService<ILessonRepository>(),
|
|
App.Services.GetRequiredService<IShorthandCodeRepository>(),
|
|
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
|
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
|
App.Services.GetRequiredService<PeriodScheduleService>(),
|
|
App.Services.GetRequiredService<IAttachmentStorage>(),
|
|
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<bool>(owner);
|
|
return ok && dialogVm.Result is not null;
|
|
}
|
|
|
|
private async Task<bool> 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<bool>(owner);
|
|
}
|
|
|
|
private async Task<MoveLessonTarget?> ShowMoveLessonDialog(Lesson lesson)
|
|
{
|
|
var dialogVm = new MoveLessonDialogViewModel(lesson.Date, lesson.LessonNumber);
|
|
var dialog = new MoveLessonDialog { DataContext = dialogVm };
|
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
|
if (owner is null) return null;
|
|
|
|
var ok = await dialog.ShowDialog<bool>(owner);
|
|
return ok ? dialogVm.Result : null;
|
|
}
|
|
|
|
private async Task<Unit?> ShowChangeLessonUnitDialog(Lesson lesson)
|
|
{
|
|
var dialogVm = new ChangeLessonUnitDialogViewModel(
|
|
App.Services.GetRequiredService<IUnitRepository>(),
|
|
lesson.GroupId, lesson.UnitId,
|
|
(DataContext as PlanningTabViewModel)?.SelectedUnit?.Title ?? "");
|
|
var dialog = new ChangeLessonUnitDialog { DataContext = dialogVm };
|
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
|
if (owner is null) return null;
|
|
|
|
var ok = await dialog.ShowDialog<bool>(owner);
|
|
return ok ? dialogVm.Result : null;
|
|
}
|
|
|
|
private async Task ShowLessonViewerDialog(Lesson lesson)
|
|
{
|
|
var viewerVm = new LessonViewerViewModel(lesson,
|
|
App.Services.GetRequiredService<IAlternativeLessonPathRepository>());
|
|
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<LessonSeriesResult?> ShowGenerateLessonSeriesDialog(Unit unit)
|
|
{
|
|
var dialogVm = new GenerateLessonSeriesDialogViewModel(
|
|
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
|
App.Services.GetRequiredService<ILessonRepository>(),
|
|
App.Services.GetRequiredService<ISchoolHolidayRepository>(),
|
|
App.Services.GetRequiredService<PublicHolidayService>(),
|
|
App.Services.GetRequiredService<SchoolCalendarSettingsService>(),
|
|
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<bool>(owner);
|
|
if (ok && dialogVm.Result is { } result)
|
|
App.Services.GetRequiredService<NotificationService>().ShowSuccess(result.Summary);
|
|
return ok ? dialogVm.Result : null;
|
|
}
|
|
|
|
private async Task<bool> ShowAiAssistDialog(Unit unit)
|
|
{
|
|
var dialogVm = new AiAssistDialogViewModel(
|
|
App.Services.GetRequiredService<AiPlanningService>(),
|
|
App.Services.GetRequiredService<AiSettingsService>(),
|
|
App.Services.GetRequiredService<ILessonRepository>(),
|
|
unit);
|
|
|
|
var dialog = new AiAssistDialog { DataContext = dialogVm };
|
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
|
if (owner is null) return false;
|
|
|
|
await dialog.ShowDialog<bool>(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<IStorageFile?> 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<string> 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<Task> 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<PlanningExchangeService>();
|
|
|
|
private static NotificationService Notifications =>
|
|
App.Services.GetRequiredService<NotificationService>();
|
|
}
|