UX-Update 2.0, Neues Dashboard
This commit is contained in:
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user