UX-Update 2.0, Neues Dashboard

This commit is contained in:
2026-08-29 00:45:19 +02:00
parent 229ef79e75
commit 9b04d7eb98
14 changed files with 774 additions and 355 deletions
@@ -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 ─────────────────────────────────────────────────────────────