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",
};
}