diff --git a/LehrerApp.Desktop.Tests/PersonalizeWorksheetDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/PersonalizeWorksheetDialogViewModelTests.cs new file mode 100644 index 0000000..8fff4cd --- /dev/null +++ b/LehrerApp.Desktop.Tests/PersonalizeWorksheetDialogViewModelTests.cs @@ -0,0 +1,76 @@ +using LehrerApp.Core.Models; +using LehrerApp.Desktop.ViewModels.Groups; +using LehrerApp.Templating; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +public sealed class PersonalizeWorksheetDialogViewModelTests : IDisposable +{ + private readonly string _directory = Path.Combine(Path.GetTempPath(), $"lehrerapp-worksheet-vm-tests-{Guid.NewGuid():N}"); + public PersonalizeWorksheetDialogViewModelTests() => Directory.CreateDirectory(_directory); + + [Fact] + public void Generate_ErzeugtEinePdfJeAusgewaehltemSchueler() + { + var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" }; + var anna = new Student { FirstName = "Anna", LastName = "Adler" }; + var ben = new Student { FirstName = "Ben", LastName = "Bauer" }; + var vm = Build(group, [anna, ben], StoreWithTemplate()); + var output = Path.Combine(_directory, "output"); + + vm.Generate(output); + + Assert.Equal(2, vm.Results.Count); + Assert.All(vm.Results, r => Assert.True(r.Success)); + Assert.Equal(2, Directory.GetFiles(output, "*.pdf").Length); + } + + [Fact] + public void Generate_AbgewaehlterSchueler_WirdUebersprungen() + { + var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" }; + var anna = new Student { FirstName = "Anna", LastName = "Adler" }; + var ben = new Student { FirstName = "Ben", LastName = "Bauer" }; + var vm = Build(group, [anna, ben], StoreWithTemplate()); + vm.Students.Single(s => s.Model.Id == ben.Id).IsIncluded = false; + var output = Path.Combine(_directory, "output"); + + vm.Generate(output); + + Assert.Single(vm.Results); + Assert.Equal(anna.FullName, vm.Results[0].StudentName); + } + + [Fact] + public void OhneVorlage_GenerateTutNichts() + { + var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" }; + var anna = new Student { FirstName = "Anna", LastName = "Adler" }; + var vm = Build(group, [anna], new WorksheetTemplateStore(new TemplateStore(Path.Combine(_directory, "empty-store")))); + var output = Path.Combine(_directory, "output"); + + vm.Generate(output); + + Assert.Empty(vm.Results); + } + + private static PersonalizeWorksheetDialogViewModel Build(LearningGroup group, List students, WorksheetTemplateStore store) => + new(group, [.. students.Select(s => s.Id)], new FakeStudents(students), store, new QuestTemplateRenderer()); + + private WorksheetTemplateStore StoreWithTemplate() + { + var source = Path.Combine(_directory, $"{Guid.NewGuid():N}.lavorlage"); + var manifest = new TemplateManifest + { + Id = $"arbeitsblatt-{Guid.NewGuid():N}", Name = "Arbeitsblatt", + Placeholders = [new PlaceholderDefinition("Student.FirstName", PlaceholderType.Text)], + }; + TemplatePackage.Create(source, manifest, "PAGE 210 297 mm\nTEXT 20 20 $Student.FirstName", new Dictionary()); + var store = new WorksheetTemplateStore(new TemplateStore(Path.Combine(_directory, $"store-{Guid.NewGuid():N}"))); + store.Store.Import(source); + return store; + } + + public void Dispose() { if (Directory.Exists(_directory)) Directory.Delete(_directory, true); } +} diff --git a/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs b/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs index d4b7fce..ebf77a5 100644 --- a/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs @@ -35,7 +35,7 @@ public sealed class SettingsViewModelTests new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(), - new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), + new TemplateStore(tempPath), new WorksheetTemplateStore(new TemplateStore(tempPath, subfolder: "worksheet-template-packages")), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildMcpSettingsService(), TestSupport.BuildMcpClientRegistrationService(), TestSupport.BuildWebUntisSettingsService(), @@ -342,7 +342,7 @@ public sealed class SettingsViewModelTests new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath), new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath), - new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), + new FakeSupervisionDuties(), new TemplateStore(tempPath), new WorksheetTemplateStore(new TemplateStore(tempPath, subfolder: "worksheet-template-packages")), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildMcpSettingsService(), TestSupport.BuildMcpClientRegistrationService(), TestSupport.BuildWebUntisSettingsService(), @@ -372,7 +372,7 @@ public sealed class SettingsViewModelTests new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath), new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule, - new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), + new FakeSupervisionDuties(), new TemplateStore(tempPath), new WorksheetTemplateStore(new TemplateStore(tempPath, subfolder: "worksheet-template-packages")), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildMcpSettingsService(), TestSupport.BuildMcpClientRegistrationService(), TestSupport.BuildWebUntisSettingsService(), @@ -406,7 +406,7 @@ public sealed class SettingsViewModelTests new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath), new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule, - new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), + new FakeSupervisionDuties(), new TemplateStore(tempPath), new WorksheetTemplateStore(new TemplateStore(tempPath, subfolder: "worksheet-template-packages")), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildMcpSettingsService(), TestSupport.BuildMcpClientRegistrationService(), TestSupport.BuildWebUntisSettingsService(), diff --git a/LehrerApp.Desktop.Tests/StudentPickerDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/StudentPickerDialogViewModelTests.cs new file mode 100644 index 0000000..b76e529 --- /dev/null +++ b/LehrerApp.Desktop.Tests/StudentPickerDialogViewModelTests.cs @@ -0,0 +1,49 @@ +using LehrerApp.Core.Models; +using LehrerApp.Desktop.ViewModels.Students; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +public sealed class StudentPickerDialogViewModelTests +{ + private static (StudentPickerDialogViewModel Vm, Student Anna, Student Ben) Build() + { + var anna = new Student { FirstName = "Anna", LastName = "Adler" }; + var ben = new Student { FirstName = "Ben", LastName = "Bauer" }; + var vm = new StudentPickerDialogViewModel(new FakeStudents([anna, ben])); + return (vm, anna, ben); + } + + [Fact] + public void SearchText_FiltertNachTeilstring() + { + var (vm, anna, _) = Build(); + + vm.SearchText = "ann"; + + Assert.Single(vm.Students); + Assert.Equal(anna.FullName, vm.Students[0].FullName); + } + + [Fact] + public void Select_OhneAuswahl_SetztValidationMessage() + { + var (vm, _, _) = Build(); + + vm.SelectCommand.Execute(null); + + Assert.Null(vm.Result); + Assert.NotEqual("", vm.ValidationMessage); + } + + [Fact] + public void Select_MitAuswahl_LiefertVollenSchueler() + { + var (vm, anna, _) = Build(); + vm.SelectedStudent = vm.Students.Single(s => s.Id == anna.Id); + + vm.SelectCommand.Execute(null); + + Assert.Equal(anna.Id, vm.Result?.Id); + } +} diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index e83aed6..2a844ac 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -123,6 +123,7 @@ public static class AppBootstrapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(_ => new TemplateStore(appData)); + services.AddSingleton(_ => new WorksheetTemplateStore(new TemplateStore(appData, subfolder: "worksheet-template-packages"))); // ── Datensicherheit (13.3) ─────────────────────────────────────────── // Backup läuft vor dem Öffnen der Datenbank, rein dateibasiert (unabhängig von diff --git a/LehrerApp.Desktop/Services/LetterDialogs.cs b/LehrerApp.Desktop/Services/LetterDialogs.cs new file mode 100644 index 0000000..755a8f9 --- /dev/null +++ b/LehrerApp.Desktop/Services/LetterDialogs.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using LehrerApp.Core.Interfaces; +using LehrerApp.Core.Models; +using LehrerApp.Desktop.ViewModels.Students; +using LehrerApp.Desktop.Views.Students; +using LehrerApp.Templating; +using Microsoft.Extensions.DependencyInjection; + +namespace LehrerApp.Desktop.Services; + +/// Öffnet den bestehenden Elternbrief-Dialog für einen Schüler, egal ob der Aufruf aus +/// der Schülerdetailansicht (Student schon im DataContext) oder aus dem Formulare-Menü (Student +/// erst per Picker gewählt) kommt. +public static class LetterDialogs +{ + public static async Task ShowCreateLetterDialogAsync(Window owner, Student student) + { + var vm = new CreateLetterDialogViewModel(student, + App.Services.GetRequiredService(), + App.Services.GetRequiredService(), + App.Services.GetRequiredService(), + App.Services.GetRequiredService()); + var dialog = new CreateLetterDialog { DataContext = vm }; + var path = await dialog.ShowDialog(owner); + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); + } +} diff --git a/LehrerApp.Desktop/ViewModels/Groups/PersonalizeWorksheetDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/PersonalizeWorksheetDialogViewModel.cs new file mode 100644 index 0000000..b275874 --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Groups/PersonalizeWorksheetDialogViewModel.cs @@ -0,0 +1,86 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using LehrerApp.Core.Interfaces; +using LehrerApp.Core.Models; +using LehrerApp.Desktop.Services; +using LehrerApp.Desktop.ViewModels.Students; +using LehrerApp.Templating; +using System.Collections.ObjectModel; + +namespace LehrerApp.Desktop.ViewModels.Groups; + +/// Erzeugt aus einer in TemplateDesigner gebauten Arbeitsblatt-.lavorlage-Vorlage ein +/// personalisiertes PDF je aktivem Mitglied der aktuell geöffneten Lerngruppe. Nutzt bewusst +/// dieselbe Platzhalter-/Rendering-Infrastruktur wie der Elternbrief-Dialog +/// (, ), aber eine getrennte +/// -Vorlagenbibliothek. +public partial class PersonalizeWorksheetDialogViewModel : ObservableObject +{ + private readonly LearningGroup _group; + private readonly WorksheetTemplateStore _templates; + private readonly ITemplateRenderer _renderer; + + [ObservableProperty] private LetterTemplateChoice? _selectedTemplate; + [ObservableProperty] private string _statusMessage = ""; + + public string GroupName => $"{_group.Name} · {_group.SchoolYear}"; + public ObservableCollection Templates { get; } = []; + public ObservableCollection Students { get; } = []; + public ObservableCollection Results { get; } = []; + public bool HasNoTemplates => Templates.Count == 0; + public bool HasResults => Results.Count > 0; + + public PersonalizeWorksheetDialogViewModel(LearningGroup group, IReadOnlyList studentIds, + IStudentRepository students, WorksheetTemplateStore templates, ITemplateRenderer renderer) + { + _group = group; _templates = templates; _renderer = renderer; + foreach (var template in templates.Store.GetTemplates()) Templates.Add(new(template)); + SelectedTemplate = Templates.FirstOrDefault(); + foreach (var id in studentIds) + if (students.GetById(id) is { } student) Students.Add(new(student)); + } + + public void Generate(string outputFolder) + { + Results.Clear(); + if (SelectedTemplate is null) return; + LoadedTemplate loaded; + try { loaded = _templates.Store.Load(SelectedTemplate.Model); } + catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException) + { StatusMessage = $"Vorlage ist ungültig: {ex.Message}"; return; } + + Directory.CreateDirectory(outputFolder); + foreach (var choice in Students.Where(x => x.IsIncluded)) + { + var student = choice.Model; + try + { + var values = LetterPlaceholderBuilder.BuildStandardValues(student, contact: null, _group, + DateOnly.FromDateTime(DateTime.Now), "", ""); + var pdf = _renderer.RenderToPdf(loaded, new LetterDataProvider(values)); + var fileName = SanitizeFileName($"{SelectedTemplate.Name}_{student.LastName}_{student.FirstName}.pdf"); + File.WriteAllBytes(Path.Combine(outputFolder, fileName), pdf); + Results.Add(new(student.FullName, true, "")); + } + catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException) + { Results.Add(new(student.FullName, false, ex.Message)); } + } + OnPropertyChanged(nameof(HasResults)); + StatusMessage = $"{Results.Count(x => x.Success)} von {Results.Count} Arbeitsblättern erzeugt."; + } + + private static string SanitizeFileName(string value) + { foreach (var character in Path.GetInvalidFileNameChars()) value = value.Replace(character, '_'); return value; } +} + +public partial class WorksheetStudentChoice(Student model) : ObservableObject +{ + public Student Model { get; } = model; + public string FullName => Model.FullName; + [ObservableProperty] private bool _isIncluded = true; +} + +public sealed record WorksheetGenerationResult(string StudentName, bool Success, string ErrorMessage) +{ + public string Icon => Success ? "✓" : "⚠"; + public string Color => Success ? "SeaGreen" : "#D97706"; +} diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index f328767..499a2be 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -12,6 +12,7 @@ using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Desktop.ViewModels.Workload; using Microsoft.Extensions.DependencyInjection; using System.Collections.ObjectModel; +using System.ComponentModel; namespace LehrerApp.Desktop.ViewModels; @@ -38,6 +39,9 @@ public partial class MainWindowViewModel : ObservableObject public bool IsClassTeacherActive => ActiveNavItem == NavItem.ClassTeacher; public bool IsSettingsActive => ActiveNavItem == NavItem.Settings; + public GroupDetailViewModel? CurrentGroupDetail => CurrentPage as GroupDetailViewModel; + public bool CanPersonalizeWorksheet => CurrentGroupDetail?.Group is not null; + public MainWindowViewModel(IServiceProvider services, DashboardViewModel dashboard, SchoolYearService sy, SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock, @@ -103,6 +107,25 @@ public partial class MainWindowViewModel : ObservableObject } } + // GroupDetailViewModel.Group wird erst nach dem Wechsel von CurrentPage per LoadGroup gesetzt + // (siehe Kommentar in NavigateToGroupDetail) - ohne dieses Abonnement bliebe + // CanPersonalizeWorksheet bis zum nächsten Seitenwechsel auf dem alten Stand. + private GroupDetailViewModel? _observedGroupDetail; + + partial void OnCurrentPageChanged(ObservableObject? value) + { + if (_observedGroupDetail is not null) _observedGroupDetail.PropertyChanged -= OnGroupDetailPropertyChanged; + _observedGroupDetail = value as GroupDetailViewModel; + if (_observedGroupDetail is not null) _observedGroupDetail.PropertyChanged += OnGroupDetailPropertyChanged; + OnPropertyChanged(nameof(CurrentGroupDetail)); + OnPropertyChanged(nameof(CanPersonalizeWorksheet)); + } + + private void OnGroupDetailPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(GroupDetailViewModel.Group)) OnPropertyChanged(nameof(CanPersonalizeWorksheet)); + } + partial void OnActiveNavItemChanged(NavItem value) { OnPropertyChanged(nameof(IsDashboardActive)); diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.WorksheetTemplates.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.WorksheetTemplates.cs new file mode 100644 index 0000000..9eccc36 --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.WorksheetTemplates.cs @@ -0,0 +1,60 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LehrerApp.Templating; +using System.Collections.ObjectModel; + +namespace LehrerApp.Desktop.ViewModels.Settings; + +public partial class SettingsViewModel +{ + [ObservableProperty] private string _worksheetTemplateStatus = ""; + public ObservableCollection WorksheetTemplateList { get; } = []; + + private void LoadWorksheetTemplates() + { + WorksheetTemplateList.Clear(); + foreach (var template in _worksheetTemplates.Store.GetTemplates()) WorksheetTemplateList.Add(CreateWorksheetItem(template)); + } + + public void ImportWorksheetTemplate(string path) + { + WorksheetTemplateStatus = ""; + try + { + var template = _worksheetTemplates.Store.Import(path); + LoadWorksheetTemplates(); + WorksheetTemplateStatus = $"„{template.Name}“ wurde geprüft und lokal importiert."; + } + catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException) + { WorksheetTemplateStatus = $"Import fehlgeschlagen: {ex.Message}"; } + } + + [RelayCommand] + private void ValidateWorksheetTemplate(LetterTemplateListItem? item) + { + if (item is null) return; + try + { + var refreshed = CreateWorksheetItem(item.Model); + var index = WorksheetTemplateList.IndexOf(item); + if (index >= 0) WorksheetTemplateList[index] = refreshed; + WorksheetTemplateStatus = $"„{item.Name}“ ist gültig (Schema {refreshed.SchemaVersion})."; + } + catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException) + { WorksheetTemplateStatus = $"„{item.Name}“ ist ungültig: {ex.Message}"; } + } + + [RelayCommand] + private void DeleteWorksheetTemplate(LetterTemplateListItem? item) + { + if (item is null) return; + _worksheetTemplates.Store.Delete(item.Id); WorksheetTemplateList.Remove(item); WorksheetTemplateStatus = "Vorlage gelöscht."; + } + + private LetterTemplateListItem CreateWorksheetItem(InstalledTemplate template) + { + var loaded = _worksheetTemplates.Store.Load(template); + return new(template, loaded.Manifest.SchemaVersion, loaded.Manifest.Placeholders.Count(x => !x.IsConstant), + loaded.Manifest.Placeholders.Count(x => !x.IsConstant && x.Required)); + } +} diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs index 73e666b..e0f1d23 100644 --- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs @@ -62,6 +62,7 @@ public partial class SettingsViewModel : ObservableObject private readonly IStudentRepository _students; private readonly IShorthandCodeRepository _shorthandCodes; private readonly TemplateStore _letterTemplates; + private readonly WorksheetTemplateStore _worksheetTemplates; [ObservableProperty] private int _activeTabIndex; @@ -102,6 +103,7 @@ public partial class SettingsViewModel : ObservableObject IShorthandCodeRepository shorthandCodes, ISchoolHolidayRepository schoolHolidays, SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule, ISupervisionDutyRepository supervisionDuties, TemplateStore letterTemplates, + WorksheetTemplateStore worksheetTemplates, AiSettingsService aiSettings, AiPlanningService aiPlanning, McpSettingsService mcpSettings, Services.Mcp.McpClientRegistrationService mcpRegistration, WebUntisSettingsService untisSettings, @@ -139,6 +141,7 @@ public partial class SettingsViewModel : ObservableObject _periodSchedule = periodSchedule; _supervisionDuties = supervisionDuties; _letterTemplates = letterTemplates; + _worksheetTemplates = worksheetTemplates; _aiSettings = aiSettings; _aiPlanning = aiPlanning; _mcpSettings = mcpSettings; @@ -169,6 +172,7 @@ public partial class SettingsViewModel : ObservableObject LoadPeriodTimes(); LoadSupervisionDuties(); LoadLetterTemplates(); + LoadWorksheetTemplates(); LoadAiSettings(); LoadMcpSettings(); LoadMcpRegistrationStatus(); diff --git a/LehrerApp.Desktop/ViewModels/Students/StudentPickerDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/StudentPickerDialogViewModel.cs new file mode 100644 index 0000000..a71da17 --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Students/StudentPickerDialogViewModel.cs @@ -0,0 +1,48 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LehrerApp.Core.Interfaces; +using LehrerApp.Core.Models; +using LehrerApp.Desktop.ViewModels.Groups; +using System.Collections.ObjectModel; + +namespace LehrerApp.Desktop.ViewModels.Students; + +/// Einfacher "einen Schüler wählen"-Dialog für Einstiegspunkte ohne bereits geöffneten +/// Schüler (z.B. das Formulare-Menü) - anders als +/// ohne Gruppenbezug/Mitgliedschaftszeitraum, einfach alle Schüler durchsuchbar. +public partial class StudentPickerDialogViewModel : ObservableObject +{ + private readonly IStudentRepository _students; + + [ObservableProperty] private string _searchText = ""; + [ObservableProperty] private StudentPickerItem? _selectedStudent; + [ObservableProperty] private string _validationMessage = ""; + + public ObservableCollection Students { get; } = []; + public Student? Result { get; private set; } + + public StudentPickerDialogViewModel(IStudentRepository students) + { + _students = students; + LoadStudents(); + } + + partial void OnSearchTextChanged(string value) => LoadStudents(); + + private void LoadStudents() + { + var matches = _students.GetAll() + .Where(s => string.IsNullOrWhiteSpace(SearchText) || + s.FullName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)) + .OrderBy(s => s.FullName, StringComparer.CurrentCultureIgnoreCase); + Students.Clear(); + foreach (var student in matches) Students.Add(new StudentPickerItem(student)); + } + + [RelayCommand] + private void Select() + { + if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; } + Result = _students.GetById(SelectedStudent.Id); + } +} diff --git a/LehrerApp.Desktop/Views/Groups/PersonalizeWorksheetDialog.axaml b/LehrerApp.Desktop/Views/Groups/PersonalizeWorksheetDialog.axaml new file mode 100644 index 0000000..34fac1f --- /dev/null +++ b/LehrerApp.Desktop/Views/Groups/PersonalizeWorksheetDialog.axaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +