UX-Update 2.0, Neues Dashboard
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public partial class GlobalSearchViewModel : ObservableObject
|
||||
{
|
||||
private const int MaxDataResults = 12;
|
||||
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
|
||||
[ObservableProperty] private string _query = "";
|
||||
[ObservableProperty] private GlobalSearchResult? _selectedResult;
|
||||
|
||||
public ObservableCollection<GlobalSearchResult> Results { get; } = [];
|
||||
public bool HasResults => Results.Count > 0;
|
||||
public bool ShowNoResults => !string.IsNullOrWhiteSpace(Query) && Results.Count == 0;
|
||||
|
||||
public Action<GlobalSearchResult>? OnNavigate { get; set; }
|
||||
public Func<bool, Task>? OnQuickAddTask { get; set; }
|
||||
public Func<Task>? OnQuickAddStudent { get; set; }
|
||||
public Action? OnClose { get; set; }
|
||||
|
||||
public GlobalSearchViewModel(IStudentRepository students, IGroupRepository groups,
|
||||
IExamRepository exams, IWorkTaskRepository tasks)
|
||||
{
|
||||
_students = students;
|
||||
_groups = groups;
|
||||
_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(includeInactive: true);
|
||||
var groupNames = groups.ToDictionary(g => g.Id, g => g.Name);
|
||||
var candidates = new List<GlobalSearchResult>();
|
||||
|
||||
candidates.AddRange(_students.GetAll(includeInactive: true)
|
||||
.Where(s => Matches(s.FullName, query))
|
||||
.Select(s => GlobalSearchResult.ForStudent(s)));
|
||||
|
||||
candidates.AddRange(groups
|
||||
.Where(g => Matches($"{g.Name} {g.SchoolYear} {g.GradeLevel}", query))
|
||||
.Select(GlobalSearchResult.ForGroup));
|
||||
|
||||
candidates.AddRange(_exams.GetAll()
|
||||
.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 => 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", "+"),
|
||||
};
|
||||
|
||||
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;
|
||||
default:
|
||||
OnNavigate?.Invoke(result);
|
||||
break;
|
||||
}
|
||||
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public enum GlobalSearchResultKind { Action, Student, Group, Exam, Task }
|
||||
public enum GlobalSearchAction { None, NewTask, NewReminder, NewStudent }
|
||||
|
||||
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) => new()
|
||||
{
|
||||
Kind = GlobalSearchResultKind.Group, EntityId = group.Id, GroupId = group.Id,
|
||||
Title = group.Name, Subtitle = $"{group.SchoolYear} · Stufe {group.GradeLevel}", 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",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user