UX-Update 2.0, Neues Dashboard
This commit is contained in:
@@ -107,6 +107,15 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public DashboardCardOption AttendanceCard => Card("attendance");
|
||||
public DashboardCardOption SupportCard => Card("support");
|
||||
public DashboardCardOption GroupsCard => Card("groups");
|
||||
public int TodayLessonCount => TodaysLessons.Count;
|
||||
public int OpenTaskCount => OpenTasks.Count;
|
||||
public int UpcomingCount => UpcomingDates.Count;
|
||||
public int AttentionCount => OpenExcuses.Count + AttendanceWarnings.Count + SupportPlanReviews.Count
|
||||
+ OpenCorrections.Count + UnplannedLessons.Count + Alerts.Count;
|
||||
public string TodayLessonSummary => TodayLessonCount == 1 ? "1 Stunde" : $"{TodayLessonCount} Stunden";
|
||||
public string OpenTaskSummary => OpenTaskCount == 1 ? "1 Aufgabe" : $"{OpenTaskCount} Aufgaben";
|
||||
public string AttentionSummary => AttentionCount == 1 ? "1 offener Punkt" : $"{AttentionCount} offene Punkte";
|
||||
public string UpcomingSummary => UpcomingCount == 1 ? "1 Termin" : $"{UpcomingCount} Termine";
|
||||
|
||||
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
||||
IExamRepository exams, IExamResultRepository examResults, IGradeRepository grades,
|
||||
@@ -204,6 +213,31 @@ public partial class DashboardViewModel : ObservableObject
|
||||
LoadOpenCorrections(groups, today);
|
||||
LoadUnplannedLessons(groups, today);
|
||||
LoadAlerts(groups, today);
|
||||
UpdateDashboardSummary();
|
||||
}
|
||||
|
||||
private void UpdateDashboardSummary()
|
||||
{
|
||||
TodayCard.IsEmpty = TodaysLessons.Count == 0;
|
||||
TasksCard.IsEmpty = OpenTasks.Count == 0;
|
||||
CalendarCard.IsEmpty = false;
|
||||
ExcusesCard.IsEmpty = OpenExcuses.Count == 0;
|
||||
UpcomingCard.IsEmpty = UpcomingDates.Count == 0;
|
||||
CorrectionsCard.IsEmpty = OpenCorrections.Count == 0;
|
||||
UnplannedCard.IsEmpty = UnplannedLessons.Count == 0;
|
||||
AlertsCard.IsEmpty = Alerts.Count == 0;
|
||||
AttendanceCard.IsEmpty = AttendanceWarnings.Count == 0;
|
||||
SupportCard.IsEmpty = SupportPlanReviews.Count == 0;
|
||||
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
|
||||
|
||||
OnPropertyChanged(nameof(TodayLessonCount));
|
||||
OnPropertyChanged(nameof(OpenTaskCount));
|
||||
OnPropertyChanged(nameof(UpcomingCount));
|
||||
OnPropertyChanged(nameof(AttentionCount));
|
||||
OnPropertyChanged(nameof(TodayLessonSummary));
|
||||
OnPropertyChanged(nameof(OpenTaskSummary));
|
||||
OnPropertyChanged(nameof(AttentionSummary));
|
||||
OnPropertyChanged(nameof(UpcomingSummary));
|
||||
}
|
||||
|
||||
private async Task LoadWeatherAsync()
|
||||
@@ -592,6 +626,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
entry.Attendance = status;
|
||||
_participationEntries.Save(entry);
|
||||
OpenExcuses.Remove(item);
|
||||
UpdateDashboardSummary();
|
||||
}
|
||||
|
||||
private void LoadCalendar()
|
||||
@@ -1034,18 +1069,29 @@ public sealed class DashboardAlertItem(Guid studentId, Guid? groupId, string stu
|
||||
public partial class DashboardCardOption : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private bool _isVisible;
|
||||
[ObservableProperty] private bool _isEmpty;
|
||||
[ObservableProperty] private int _row;
|
||||
[ObservableProperty] private int _column;
|
||||
public string Key { get; }
|
||||
public string Title { get; }
|
||||
public bool HideWhenEmpty { get; }
|
||||
public bool EffectiveIsVisible => IsVisible && (!HideWhenEmpty || !IsEmpty);
|
||||
public Action? OnVisibilityChanged { get; set; }
|
||||
|
||||
public DashboardCardOption(string key, string title, bool isVisible)
|
||||
{
|
||||
Key = key;
|
||||
Title = title;
|
||||
HideWhenEmpty = key is "excuses" or "upcoming" or "corrections" or "unplanned"
|
||||
or "alerts" or "attendance" or "support";
|
||||
_isVisible = isVisible;
|
||||
}
|
||||
|
||||
partial void OnIsVisibleChanged(bool value) => OnVisibilityChanged?.Invoke();
|
||||
partial void OnIsVisibleChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(EffectiveIsVisible));
|
||||
OnVisibilityChanged?.Invoke();
|
||||
}
|
||||
|
||||
partial void OnIsEmptyChanged(bool value) => OnPropertyChanged(nameof(EffectiveIsVisible));
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -13,13 +13,8 @@ namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
public partial class GroupListViewModel : ObservableObject
|
||||
{
|
||||
private const int QuickTasksMaxCount = 3;
|
||||
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
|
||||
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
||||
public Func<Task>? OnAddGroup { get; set; }
|
||||
@@ -29,11 +24,8 @@ public partial class GroupListViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private string _selectedSchoolYear = "";
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private GroupListItem? _selectedGroup;
|
||||
[ObservableProperty] private bool _showArchived;
|
||||
|
||||
public string SelectedGroupDisplayName => SelectedGroup?.DisplayName ?? "";
|
||||
public string SelectedGroupSubtitle => SelectedGroup?.Subtitle ?? "";
|
||||
public string ListSummary => ShowArchived
|
||||
? $"{Groups.Count} archivierte Gruppen · {SelectedSchoolYear}"
|
||||
: $"{Groups.Count} aktive Gruppen · {SelectedSchoolYear}";
|
||||
@@ -45,22 +37,10 @@ public partial class GroupListViewModel : ObservableObject
|
||||
public ObservableCollection<string> SchoolYears { get; } = [];
|
||||
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
||||
|
||||
// ── Schnellüberblick im Auswahl-Panel (Nutzer-Feedback: "oberhalb der Buttonliste ein paar
|
||||
// Daten auswerfen. Nächste Stunde, nächste Arbeit, wichtige Todos") — bewusst dieselben
|
||||
// kompakten Kennzahlen wie die obersten Karten des Kurs-Dashboards (GroupOverviewViewModel),
|
||||
// hier nur ohne eigenen Tab-Wechsel, da man ohnehin schon auf der Gruppenliste steht.
|
||||
[ObservableProperty] private bool _quickHasNextLesson;
|
||||
[ObservableProperty] private string _quickNextLessonLabel = "";
|
||||
[ObservableProperty] private bool _quickHasNextExam;
|
||||
[ObservableProperty] private string _quickNextExamLabel = "";
|
||||
public ObservableCollection<GroupTaskItem> QuickTasks { get; } = [];
|
||||
public bool QuickHasTasks => QuickTasks.Count > 0;
|
||||
public bool QuickHasAnything => QuickHasNextLesson || QuickHasNextExam || QuickHasTasks;
|
||||
|
||||
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy,
|
||||
ILessonRepository lessons, IExamRepository exams, IWorkTaskRepository tasks)
|
||||
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy)
|
||||
{
|
||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||
_groups = groups;
|
||||
_subjects = subjects;
|
||||
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
|
||||
SelectedSchoolYear = sy.CurrentSchoolYear();
|
||||
}
|
||||
@@ -68,58 +48,8 @@ public partial class GroupListViewModel : ObservableObject
|
||||
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
|
||||
partial void OnSearchTextChanged(string value) => LoadGroups();
|
||||
partial void OnShowArchivedChanged(bool value) => LoadGroups();
|
||||
partial void OnSelectedGroupChanged(GroupListItem? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(SelectedGroupDisplayName));
|
||||
OnPropertyChanged(nameof(SelectedGroupSubtitle));
|
||||
NavigateToSectionCommand.NotifyCanExecuteChanged();
|
||||
EditGroupCommand.NotifyCanExecuteChanged();
|
||||
RollOverGroupCommand.NotifyCanExecuteChanged();
|
||||
ToggleArchiveCommand.NotifyCanExecuteChanged();
|
||||
DeleteGroupCommand.NotifyCanExecuteChanged();
|
||||
LoadQuickInfo();
|
||||
}
|
||||
|
||||
private void LoadQuickInfo()
|
||||
{
|
||||
QuickTasks.Clear();
|
||||
if (SelectedGroup is null)
|
||||
{
|
||||
QuickHasNextLesson = false; QuickNextLessonLabel = "";
|
||||
QuickHasNextExam = false; QuickNextExamLabel = "";
|
||||
OnPropertyChanged(nameof(QuickHasTasks));
|
||||
OnPropertyChanged(nameof(QuickHasAnything));
|
||||
return;
|
||||
}
|
||||
|
||||
var groupId = SelectedGroup.Id;
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
|
||||
var nextLesson = _lessons.GetByGroupAndRange(groupId, today, today.AddDays(90))
|
||||
.Where(l => l.Status == LessonStatus.Planned)
|
||||
.OrderBy(l => l.Date).ThenBy(l => l.LessonNumber ?? 0).FirstOrDefault();
|
||||
QuickHasNextLesson = nextLesson is not null;
|
||||
QuickNextLessonLabel = nextLesson is null ? ""
|
||||
: string.IsNullOrWhiteSpace(nextLesson.Topic)
|
||||
? nextLesson.Date.ToString("dd.MM.yyyy")
|
||||
: $"{nextLesson.Date:dd.MM.yyyy} — {nextLesson.Topic}";
|
||||
|
||||
var nextExam = _exams.GetByGroup(groupId).Where(e => e.Date >= today).MinBy(e => e.Date);
|
||||
QuickHasNextExam = nextExam is not null;
|
||||
QuickNextExamLabel = nextExam is null ? "" : $"{nextExam.Date:dd.MM.yyyy} — {nextExam.Title}";
|
||||
|
||||
foreach (var t in _tasks.GetByGroup(groupId).Where(t => t.Status != WorkTaskStatus.Done)
|
||||
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(QuickTasksMaxCount))
|
||||
QuickTasks.Add(new GroupTaskItem(t.Title, t.Kind == TaskKind.Reminder,
|
||||
t.DueDate?.ToString("dd.MM.yyyy") ?? "", t.DueDate is { } d && d < today,
|
||||
TaskPriorityDisplay.ColorHex(t.Priority), t.Priority == TaskPriority.High));
|
||||
OnPropertyChanged(nameof(QuickHasTasks));
|
||||
OnPropertyChanged(nameof(QuickHasAnything));
|
||||
}
|
||||
|
||||
public void LoadGroups()
|
||||
{
|
||||
var selectedId = SelectedGroup?.Id;
|
||||
Groups.Clear();
|
||||
var all = _groups.GetBySchoolYear(SelectedSchoolYear, includeInactive: ShowArchived)
|
||||
.Where(g => g.IsActive != ShowArchived);
|
||||
@@ -129,8 +59,18 @@ public partial class GroupListViewModel : ObservableObject
|
||||
|| (g.SubjectId is Guid id && subjectNames.GetValueOrDefault(id, "")
|
||||
.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
|
||||
foreach (var g in filtered.OrderBy(g => g.Name))
|
||||
Groups.Add(new GroupListItem(g, g.SubjectId is Guid id ? subjectNames.GetValueOrDefault(id, "") : ""));
|
||||
SelectedGroup = Groups.FirstOrDefault(g => g.Id == selectedId);
|
||||
{
|
||||
var item = new GroupListItem(g,
|
||||
g.SubjectId is Guid id ? subjectNames.GetValueOrDefault(id, "") : "")
|
||||
{
|
||||
OnOpen = OpenGroup,
|
||||
OnEdit = EditGroup,
|
||||
OnRollOver = RollOverGroup,
|
||||
OnToggleArchive = ToggleArchive,
|
||||
OnDelete = DeleteGroup,
|
||||
};
|
||||
Groups.Add(item);
|
||||
}
|
||||
OnPropertyChanged(nameof(ListSummary));
|
||||
OnPropertyChanged(nameof(HasNoGroups));
|
||||
OnPropertyChanged(nameof(EmptyListMessage));
|
||||
@@ -145,63 +85,57 @@ public partial class GroupListViewModel : ObservableObject
|
||||
}
|
||||
[RelayCommand] private void Refresh() => LoadGroups();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanEditSelectedGroup))]
|
||||
private async Task EditGroup()
|
||||
[RelayCommand]
|
||||
private void OpenGroup(GroupListItem? group)
|
||||
{
|
||||
if (SelectedGroup is null || OnEditGroup is null) return;
|
||||
var id = SelectedGroup.Id;
|
||||
await OnEditGroup(id);
|
||||
LoadGroups();
|
||||
SelectedGroup = Groups.FirstOrDefault(g => g.Id == id);
|
||||
if (group is not null) OnNavigateToDetail?.Invoke(group.Id, 0);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
|
||||
private async Task RollOverGroup()
|
||||
[RelayCommand]
|
||||
private async Task EditGroup(GroupListItem? group)
|
||||
{
|
||||
if (SelectedGroup is null || OnRollOverGroup is null) return;
|
||||
var targetId = await OnRollOverGroup(SelectedGroup.Id);
|
||||
if (group?.IsActive != true || OnEditGroup is null) return;
|
||||
var id = group.Id;
|
||||
await OnEditGroup(id);
|
||||
LoadGroups();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RollOverGroup(GroupListItem? group)
|
||||
{
|
||||
if (group is null || OnRollOverGroup is null) return;
|
||||
var targetId = await OnRollOverGroup(group.Id);
|
||||
if (targetId is null) return;
|
||||
var target = _groups.GetById(targetId.Value);
|
||||
if (target is null) return;
|
||||
if (!SchoolYears.Contains(target.SchoolYear)) SchoolYears.Insert(0, target.SchoolYear);
|
||||
SelectedSchoolYear = target.SchoolYear;
|
||||
LoadGroups();
|
||||
SelectedGroup = Groups.FirstOrDefault(g => g.Id == target.Id);
|
||||
OnNavigateToDetail?.Invoke(target.Id, 0);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
|
||||
private void ToggleArchive()
|
||||
[RelayCommand]
|
||||
private void ToggleArchive(GroupListItem? selected)
|
||||
{
|
||||
if (SelectedGroup is null) return;
|
||||
var group = _groups.GetById(SelectedGroup.Id);
|
||||
if (selected is null) return;
|
||||
var group = _groups.GetById(selected.Id);
|
||||
if (group is null) return;
|
||||
group.IsActive = !group.IsActive;
|
||||
_groups.Save(group);
|
||||
LoadGroups();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanEditSelectedGroup))]
|
||||
private async Task DeleteGroup()
|
||||
[RelayCommand]
|
||||
private async Task DeleteGroup(GroupListItem? group)
|
||||
{
|
||||
if (SelectedGroup is null || OnConfirmDelete is null) return;
|
||||
var selected = SelectedGroup;
|
||||
if (!await OnConfirmDelete(selected)) return;
|
||||
_groups.Delete(selected.Id);
|
||||
if (group?.IsActive != true || OnConfirmDelete is null) return;
|
||||
if (!await OnConfirmDelete(group)) return;
|
||||
_groups.Delete(group.Id);
|
||||
LoadGroups();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
|
||||
private void NavigateToSection(string? tabIndex)
|
||||
{
|
||||
if (SelectedGroup is null || !int.TryParse(tabIndex, out var tab)) return;
|
||||
OnNavigateToDetail?.Invoke(SelectedGroup.Id, tab);
|
||||
}
|
||||
|
||||
private bool HasSelectedGroup() => SelectedGroup is not null;
|
||||
private bool CanEditSelectedGroup() => SelectedGroup?.IsActive == true;
|
||||
}
|
||||
|
||||
public class GroupListItem
|
||||
public partial class GroupListItem : ObservableObject
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public string Name { get; }
|
||||
@@ -212,6 +146,13 @@ public class GroupListItem
|
||||
public string Subtitle { get; }
|
||||
public bool IsActive { get; }
|
||||
public string ArchiveActionLabel => IsActive ? "Archivieren" : "Wieder aktivieren";
|
||||
public string OpenAutomationName => $"Lerngruppe {DisplayName} öffnen";
|
||||
public string ManageAutomationName => $"Lerngruppe {DisplayName} verwalten";
|
||||
public Action<GroupListItem>? OnOpen { get; init; }
|
||||
public Func<GroupListItem, Task>? OnEdit { get; init; }
|
||||
public Func<GroupListItem, Task>? OnRollOver { get; init; }
|
||||
public Action<GroupListItem>? OnToggleArchive { get; init; }
|
||||
public Func<GroupListItem, Task>? OnDelete { get; init; }
|
||||
|
||||
public GroupListItem(LearningGroup g, string subjectName)
|
||||
{
|
||||
@@ -224,6 +165,12 @@ public class GroupListItem
|
||||
DisplayName = string.IsNullOrEmpty(subjectName) ? g.Name : $"{g.Name} · {subjectName}";
|
||||
Subtitle = $"{TypeLabel} · Stufe {g.GradeLevel} · Noten {GradingLabel} · {g.SchoolYear}";
|
||||
}
|
||||
|
||||
[RelayCommand] private void Open() => OnOpen?.Invoke(this);
|
||||
[RelayCommand] private Task Edit() => OnEdit?.Invoke(this) ?? Task.CompletedTask;
|
||||
[RelayCommand] private Task RollOver() => OnRollOver?.Invoke(this) ?? Task.CompletedTask;
|
||||
[RelayCommand] private void ToggleArchive() => OnToggleArchive?.Invoke(this);
|
||||
[RelayCommand] private Task Delete() => OnDelete?.Invoke(this) ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── Gruppendetail ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -21,10 +21,12 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty] private ObservableObject? _currentPage;
|
||||
[ObservableProperty] private NavItem _activeNavItem = NavItem.Dashboard;
|
||||
[ObservableProperty] private string _currentSchoolYear = "";
|
||||
[ObservableProperty] private bool _isCommandPaletteOpen;
|
||||
|
||||
public SyncStatusViewModel SyncStatus { get; }
|
||||
public ObservableCollection<ToastItem> Toasts { get; }
|
||||
public AppLockViewModel AppLock { get; }
|
||||
public GlobalSearchViewModel CommandPalette { get; }
|
||||
|
||||
public bool IsDashboardActive => ActiveNavItem == NavItem.Dashboard;
|
||||
public bool IsGroupsActive => ActiveNavItem == NavItem.Groups;
|
||||
@@ -37,18 +39,32 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
public MainWindowViewModel(IServiceProvider services,
|
||||
DashboardViewModel dashboard, SchoolYearService sy,
|
||||
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock)
|
||||
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock,
|
||||
GlobalSearchViewModel commandPalette)
|
||||
{
|
||||
_services = services;
|
||||
SyncStatus = syncStatus;
|
||||
Toasts = notifications.Toasts;
|
||||
AppLock = appLock;
|
||||
CommandPalette = commandPalette;
|
||||
CommandPalette.OnClose = CloseCommandPalette;
|
||||
CurrentSchoolYear = sy.CurrentSchoolYear();
|
||||
CurrentPage = dashboard;
|
||||
AppLock.ApplyConfig();
|
||||
SyncStatus.DataChanged += OnSyncDataChanged;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenCommandPalette()
|
||||
{
|
||||
if (AppLock.IsLocked) return;
|
||||
CommandPalette.Reset();
|
||||
IsCommandPaletteOpen = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseCommandPalette() => IsCommandPaletteOpen = false;
|
||||
|
||||
// EventApplier schreibt bei eingehenden Sync-Ereignissen absichtlich direkt auf die rohe
|
||||
// LiteDB-Collection, an jedem ViewModel vorbei (Ping-Pong-Vermeidung, siehe EventApplier-
|
||||
// Klassenkommentar) - ohne diesen Hook blieb die gerade sichtbare Seite bis zum nächsten
|
||||
|
||||
Reference in New Issue
Block a user