using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using System.Collections.ObjectModel; namespace LehrerApp.Desktop.ViewModels; /// /// Globale Suche und Schnellerfassung (14.2). Die Projektion bleibt bewusst klein und lokal: /// durchsucht werden die wichtigsten täglichen Ziele, ohne dafür einen zusätzlichen Suchindex /// oder eine Netzwerkabhängigkeit einzuführen. /// public partial class GlobalSearchViewModel : ObservableObject { private const int MaxDataResults = 12; private readonly IStudentRepository _students; private readonly IGroupRepository _groups; private readonly ISubjectRepository _subjects; private readonly IExamRepository _exams; private readonly IWorkTaskRepository _tasks; [ObservableProperty] private string _query = ""; [ObservableProperty] private GlobalSearchResult? _selectedResult; public ObservableCollection Results { get; } = []; public bool HasResults => Results.Count > 0; public bool ShowNoResults => !string.IsNullOrWhiteSpace(Query) && Results.Count == 0; public Action? OnNavigate { get; set; } public Func? OnQuickAddTask { get; set; } public Func? OnQuickAddStudent { get; set; } public Func? OnQuickAddGroupDocumentation { get; set; } public Action? OnClose { get; set; } public GlobalSearchViewModel(IStudentRepository students, IGroupRepository groups, ISubjectRepository subjects, IExamRepository exams, IWorkTaskRepository tasks) { _students = students; _groups = groups; _subjects = subjects; _exams = exams; _tasks = tasks; RefreshResults(); } partial void OnQueryChanged(string value) => RefreshResults(); public void Reset() { Query = ""; RefreshResults(); } private void RefreshResults() { Results.Clear(); var query = Query.Trim(); AddQuickActions(query); if (query.Length > 0) { var groups = _groups.GetAll().Where(g => g.IsActive).ToList(); var groupNames = groups.ToDictionary(g => g.Id, g => g.Name); var subjects = _subjects.GetAll().ToDictionary(s => s.Id); Subject? GroupSubject(LearningGroup group) => group.SubjectId is { } subjectId ? subjects.GetValueOrDefault(subjectId) : null; static string SubjectLabel(Subject? subject) => subject is null ? "" : string.IsNullOrWhiteSpace(subject.ShortName) || subject.Name.Equals(subject.ShortName, StringComparison.CurrentCultureIgnoreCase) ? subject.Name : $"{subject.ShortName} – {subject.Name}"; var candidates = new List(); candidates.AddRange(_students.GetAll().Where(s => s.IsActive) .Where(s => Matches(s.FullName, query)) .Select(s => GlobalSearchResult.ForStudent(s))); candidates.AddRange(groups .Where(g => Matches($"{g.Name} {GroupSubject(g)?.Name} {GroupSubject(g)?.ShortName} {g.SchoolYear} {g.GradeLevel}", query)) .Select(g => GlobalSearchResult.ForGroup(g, SubjectLabel(GroupSubject(g))))); candidates.AddRange(_exams.GetAll() .Where(e => groupNames.ContainsKey(e.GroupId)) .Where(e => Matches($"{e.Title} {groupNames.GetValueOrDefault(e.GroupId)}", query)) .Select(e => GlobalSearchResult.ForExam(e, groupNames.GetValueOrDefault(e.GroupId) ?? ""))); candidates.AddRange(_tasks.GetAll() .Where(t => t.GroupId is null || groupNames.ContainsKey(t.GroupId.Value)) .Where(t => Matches($"{t.Title} {t.Notes} {groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty)}", query)) .Select(t => GlobalSearchResult.ForTask(t, groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty) ?? ""))); foreach (var item in candidates .OrderByDescending(x => x.Title.StartsWith(query, StringComparison.CurrentCultureIgnoreCase)) .ThenBy(x => x.KindSortOrder) .ThenBy(x => x.Title) .Take(MaxDataResults)) Results.Add(item); } SelectedResult = Results.FirstOrDefault(); OnPropertyChanged(nameof(HasResults)); OnPropertyChanged(nameof(ShowNoResults)); } private void AddQuickActions(string query) { var actions = new[] { GlobalSearchResult.ForAction(GlobalSearchAction.NewTask, "Aufgabe anlegen", "Mit Fälligkeit, Gruppe und Priorität", "+"), GlobalSearchResult.ForAction(GlobalSearchAction.NewReminder, "Erinnerung anlegen", "Kurze Notiz ohne Zeiterfassung", "◷"), GlobalSearchResult.ForAction(GlobalSearchAction.NewStudent, "Schüler anlegen", "Neue Stammdaten erfassen", "+"), GlobalSearchResult.ForAction(GlobalSearchAction.NewGroupDocumentation, "Lerngruppen-Eintrag", "Planung, Klausur oder Erinnerung dokumentieren", "N"), }; foreach (var action in actions.Where(a => query.Length == 0 || Matches(a.Title, query))) Results.Add(action); } private static bool Matches(string? value, string query) => value?.Contains(query, StringComparison.CurrentCultureIgnoreCase) == true; [RelayCommand] private async Task Execute(GlobalSearchResult? result) { if (result is null) return; switch (result.Action) { case GlobalSearchAction.NewTask: if (OnQuickAddTask is not null) await OnQuickAddTask(false); break; case GlobalSearchAction.NewReminder: if (OnQuickAddTask is not null) await OnQuickAddTask(true); break; case GlobalSearchAction.NewStudent: if (OnQuickAddStudent is not null) await OnQuickAddStudent(); break; case GlobalSearchAction.NewGroupDocumentation: if (OnQuickAddGroupDocumentation is not null) await OnQuickAddGroupDocumentation(); break; default: OnNavigate?.Invoke(result); break; } OnClose?.Invoke(); } } public enum GlobalSearchResultKind { Action, Student, Group, Exam, Task } public enum GlobalSearchAction { None, NewTask, NewReminder, NewStudent, NewGroupDocumentation } public sealed class GlobalSearchResult { public GlobalSearchResultKind Kind { get; private init; } public GlobalSearchAction Action { get; private init; } public Guid? EntityId { get; private init; } public Guid? GroupId { get; private init; } public string Title { get; private init; } = ""; public string Subtitle { get; private init; } = ""; public string Icon { get; private init; } = ""; public int KindSortOrder => Kind switch { GlobalSearchResultKind.Student => 0, GlobalSearchResultKind.Group => 1, GlobalSearchResultKind.Exam => 2, GlobalSearchResultKind.Task => 3, _ => -1, }; public string KindLabel => Kind switch { GlobalSearchResultKind.Student => "Schüler", GlobalSearchResultKind.Group => "Lerngruppe", GlobalSearchResultKind.Exam => "Klausur", GlobalSearchResultKind.Task => "Aufgabe", _ => "Schnellaktion", }; public static GlobalSearchResult ForAction(GlobalSearchAction action, string title, string subtitle, string icon) => new() { Kind = GlobalSearchResultKind.Action, Action = action, Title = title, Subtitle = subtitle, Icon = icon }; public static GlobalSearchResult ForStudent(Student student) => new() { Kind = GlobalSearchResultKind.Student, EntityId = student.Id, Title = student.FullName, Subtitle = student.IsActive ? "Aktiv" : "Inaktiv", Icon = "P", }; public static GlobalSearchResult ForGroup(LearningGroup group, string subjectName) => new() { Kind = GlobalSearchResultKind.Group, EntityId = group.Id, GroupId = group.Id, Title = group.Name, Subtitle = string.Join(" · ", new[] { subjectName, group.SchoolYear, $"Stufe {group.GradeLevel}" } .Where(x => !string.IsNullOrWhiteSpace(x))), Icon = "G", }; public static GlobalSearchResult ForExam(Exam exam, string groupName) => new() { Kind = GlobalSearchResultKind.Exam, EntityId = exam.Id, GroupId = exam.GroupId, Title = exam.Title, Subtitle = $"{groupName} · {exam.Date:dd.MM.yyyy}", Icon = "K", }; public static GlobalSearchResult ForTask(WorkTask task, string groupName) => new() { Kind = GlobalSearchResultKind.Task, EntityId = task.Id, GroupId = task.GroupId, Title = task.Title, Subtitle = string.Join(" · ", new[] { groupName, task.DueDate?.ToString("dd.MM.yyyy") ?? "" } .Where(x => !string.IsNullOrWhiteSpace(x))), Icon = task.Kind == TaskKind.Reminder ? "E" : "A", }; }