UX-Update 2.0, Neues Dashboard
This commit is contained in:
@@ -11,6 +11,22 @@ namespace LehrerApp.Desktop.Tests;
|
|||||||
/// (offene Aufgaben, ...).
|
/// (offene Aufgaben, ...).
|
||||||
public sealed class DashboardViewModelTests
|
public sealed class DashboardViewModelTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void LeereHinweisbereiche_WerdenAusgeblendetUndZusammenfassungBleibtKompakt()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
||||||
|
|
||||||
|
Assert.False(vm.ExcusesCard.EffectiveIsVisible);
|
||||||
|
Assert.False(vm.CorrectionsCard.EffectiveIsVisible);
|
||||||
|
Assert.False(vm.AlertsCard.EffectiveIsVisible);
|
||||||
|
Assert.Equal("0 offene Punkte", vm.AttentionSummary);
|
||||||
|
Assert.True(vm.TodayCard.EffectiveIsVisible);
|
||||||
|
Assert.True(vm.CalendarCard.EffectiveIsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
private static PeriodScheduleService NewPeriodSchedule()
|
private static PeriodScheduleService NewPeriodSchedule()
|
||||||
{
|
{
|
||||||
var tempPath = System.IO.Path.Combine(
|
var tempPath = System.IO.Path.Combine(
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class GlobalSearchViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void LeereSuche_ZeigtSchnellaktionen()
|
||||||
|
{
|
||||||
|
var vm = BuildVm();
|
||||||
|
|
||||||
|
Assert.Collection(vm.Results,
|
||||||
|
item => Assert.Equal(GlobalSearchAction.NewTask, item.Action),
|
||||||
|
item => Assert.Equal(GlobalSearchAction.NewReminder, item.Action),
|
||||||
|
item => Assert.Equal(GlobalSearchAction.NewStudent, item.Action));
|
||||||
|
Assert.Same(vm.Results[0], vm.SelectedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Suche_FindetSchuelerGruppeKlausurUndAufgabe()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9 Chemie", SchoolYear = "2026/27", GradeLevel = 9 };
|
||||||
|
var student = new Student { FirstName = "Mia", LastName = "Chemie" };
|
||||||
|
var exam = new Exam { GroupId = group.Id, Title = "Chemie-Test", Date = new DateOnly(2026, 9, 1) };
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { GroupId = group.Id, Title = "Chemie-Klausur korrigieren" });
|
||||||
|
var vm = BuildVm([student], [group], [exam], tasks);
|
||||||
|
|
||||||
|
vm.Query = "Chemie";
|
||||||
|
|
||||||
|
Assert.Contains(vm.Results, x => x.Kind == GlobalSearchResultKind.Student && x.EntityId == student.Id);
|
||||||
|
Assert.Contains(vm.Results, x => x.Kind == GlobalSearchResultKind.Group && x.EntityId == group.Id);
|
||||||
|
Assert.Contains(vm.Results, x => x.Kind == GlobalSearchResultKind.Exam && x.EntityId == exam.Id);
|
||||||
|
Assert.Contains(vm.Results, x => x.Kind == GlobalSearchResultKind.Task && x.Title.Contains("korrigieren"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TrefferAusfuehren_NavigiertUndSchliesstPalette()
|
||||||
|
{
|
||||||
|
var student = new Student { FirstName = "Mia", LastName = "Muster" };
|
||||||
|
var vm = BuildVm([student]);
|
||||||
|
vm.Query = "Muster";
|
||||||
|
var result = Assert.Single(vm.Results, x => x.Kind == GlobalSearchResultKind.Student);
|
||||||
|
GlobalSearchResult? navigated = null;
|
||||||
|
var closed = false;
|
||||||
|
vm.OnNavigate = x => navigated = x;
|
||||||
|
vm.OnClose = () => closed = true;
|
||||||
|
|
||||||
|
await vm.ExecuteCommand.ExecuteAsync(result);
|
||||||
|
|
||||||
|
Assert.Same(result, navigated);
|
||||||
|
Assert.True(closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ErinnerungSchnellErfassen_StartetDialogAlsErinnerung()
|
||||||
|
{
|
||||||
|
var vm = BuildVm();
|
||||||
|
bool? reminder = null;
|
||||||
|
vm.OnQuickAddTask = value => { reminder = value; return Task.CompletedTask; };
|
||||||
|
var action = vm.Results.Single(x => x.Action == GlobalSearchAction.NewReminder);
|
||||||
|
|
||||||
|
await vm.ExecuteCommand.ExecuteAsync(action);
|
||||||
|
|
||||||
|
Assert.True(reminder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GlobalSearchViewModel BuildVm(List<Student>? students = null,
|
||||||
|
List<LearningGroup>? groups = null, List<Exam>? exams = null, FakeWorkTasks? tasks = null) =>
|
||||||
|
new(new FakeStudents(students ?? []), new FakeGroups(groups ?? []),
|
||||||
|
new FakeExams(exams ?? []), tasks ?? new FakeWorkTasks());
|
||||||
|
}
|
||||||
@@ -5,76 +5,69 @@ using Xunit;
|
|||||||
|
|
||||||
namespace LehrerApp.Desktop.Tests;
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
/// Tests für den Schnellüberblick im Auswahl-Panel der Gruppenliste (Nutzer-Feedback:
|
/// <summary>Tests für die direkte, einlagige Lerngruppen-Navigation.</summary>
|
||||||
/// "oberhalb der Buttonliste ein paar Daten auswerfen. Nächste Stunde, nächste Arbeit,
|
|
||||||
/// wichtige Todos").
|
|
||||||
public sealed class GroupListViewModelTests
|
public sealed class GroupListViewModelTests
|
||||||
{
|
{
|
||||||
private static GroupListViewModel BuildVm(LearningGroup group, FakeLessons? lessons = null,
|
private static GroupListViewModel BuildVm(params LearningGroup[] groups) =>
|
||||||
FakeExams? exams = null, FakeWorkTasks? tasks = null) =>
|
new(new FakeGroups([.. groups]), new FakeSubjects([]), new SchoolYearService());
|
||||||
new(new FakeGroups([group]), new FakeSubjects([]), new SchoolYearService(),
|
|
||||||
lessons ?? new FakeLessons(), exams ?? new FakeExams([]), tasks ?? new FakeWorkTasks());
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SelectedGroup_OhneDatenZeigtKeinenSchnellueberblick()
|
public void Gruppenkarte_OeffnetDirektDieUebersicht()
|
||||||
{
|
{
|
||||||
var group = new LearningGroup { Name = "9c" };
|
var group = CurrentGroup("9c");
|
||||||
var vm = BuildVm(group);
|
var vm = BuildVm(group);
|
||||||
|
Guid? openedId = null;
|
||||||
|
int? openedTab = null;
|
||||||
|
vm.OnNavigateToDetail = (id, tab) => { openedId = id; openedTab = tab; };
|
||||||
|
|
||||||
vm.SelectedGroup = vm.Groups.Count > 0 ? vm.Groups[0] : new GroupListItem(group, "");
|
Assert.Single(vm.Groups).OpenCommand.Execute(null);
|
||||||
|
|
||||||
Assert.False(vm.QuickHasAnything);
|
Assert.Equal(group.Id, openedId);
|
||||||
|
Assert.Equal(0, openedTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SelectedGroup_ZeigtNaechsteGeplanteStundeUndKlausur()
|
public async Task Verwaltungsmenue_BearbeitetDieGewaehlteKarteOhneZwischenauswahl()
|
||||||
{
|
{
|
||||||
var group = new LearningGroup { Name = "9c" };
|
var first = CurrentGroup("9a");
|
||||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
var second = CurrentGroup("9b");
|
||||||
var lessons = new FakeLessons();
|
var vm = BuildVm(first, second);
|
||||||
lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(3), Topic = "Redox", Status = LessonStatus.Planned });
|
Guid? editedId = null;
|
||||||
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur 1" }]);
|
vm.OnEditGroup = id => { editedId = id; return Task.CompletedTask; };
|
||||||
var vm = BuildVm(group, lessons: lessons, exams: exams);
|
var secondItem = vm.Groups.Single(x => x.Id == second.Id);
|
||||||
|
|
||||||
vm.SelectedGroup = new GroupListItem(group, "");
|
await secondItem.EditCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
Assert.True(vm.QuickHasNextLesson);
|
Assert.Equal(second.Id, editedId);
|
||||||
Assert.Contains("Redox", vm.QuickNextLessonLabel);
|
|
||||||
Assert.True(vm.QuickHasNextExam);
|
|
||||||
Assert.Contains("Klausur 1", vm.QuickNextExamLabel);
|
|
||||||
Assert.True(vm.QuickHasAnything);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SelectedGroup_ZeigtOffeneAufgabenDieserGruppeSortiertNachFaelligkeit()
|
public void Suche_FiltertWeiterhinNachGruppenname()
|
||||||
{
|
{
|
||||||
var group = new LearningGroup { Name = "9c" };
|
var vm = BuildVm(CurrentGroup("9 Chemie"), CurrentGroup("10 Mathematik"));
|
||||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
||||||
var tasks = new FakeWorkTasks();
|
|
||||||
tasks.Add(new WorkTask { GroupId = group.Id, Title = "Bald fällig", DueDate = today.AddDays(1), Status = WorkTaskStatus.Open });
|
|
||||||
tasks.Add(new WorkTask { GroupId = group.Id, Title = "Erledigt", DueDate = today, Status = WorkTaskStatus.Done });
|
|
||||||
tasks.Add(new WorkTask { GroupId = Guid.NewGuid(), Title = "Andere Gruppe", DueDate = today, Status = WorkTaskStatus.Open });
|
|
||||||
var vm = BuildVm(group, tasks: tasks);
|
|
||||||
|
|
||||||
vm.SelectedGroup = new GroupListItem(group, "");
|
vm.SearchText = "Chemie";
|
||||||
|
|
||||||
Assert.True(vm.QuickHasTasks);
|
Assert.Equal("9 Chemie", Assert.Single(vm.Groups).Name);
|
||||||
Assert.Equal("Bald fällig", Assert.Single(vm.QuickTasks).Title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SelectedGroup_Zuruecksetzen_LeertSchnellueberblick()
|
public void Archivieren_WirktAufDieKarteUndEntferntSieAusDerAktivenListe()
|
||||||
{
|
{
|
||||||
var group = new LearningGroup { Name = "9c" };
|
var group = CurrentGroup("9c");
|
||||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
var vm = BuildVm(group);
|
||||||
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur 1" }]);
|
var item = Assert.Single(vm.Groups);
|
||||||
var vm = BuildVm(group, exams: exams);
|
|
||||||
vm.SelectedGroup = new GroupListItem(group, "");
|
|
||||||
Assert.True(vm.QuickHasAnything);
|
|
||||||
|
|
||||||
vm.SelectedGroup = null;
|
item.ToggleArchiveCommand.Execute(null);
|
||||||
|
|
||||||
Assert.False(vm.QuickHasAnything);
|
Assert.False(group.IsActive);
|
||||||
Assert.Empty(vm.QuickTasks);
|
Assert.Empty(vm.Groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LearningGroup CurrentGroup(string name) => new()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
SchoolYear = new SchoolYearService().CurrentSchoolYear(),
|
||||||
|
IsActive = true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ using LehrerApp.Desktop.ViewModels;
|
|||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using LehrerApp.Desktop.ViewModels.Planning;
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
using LehrerApp.Desktop.Views;
|
using LehrerApp.Desktop.Views;
|
||||||
|
using LehrerApp.Desktop.Views.Workload;
|
||||||
using LehrerApp.Sync;
|
using LehrerApp.Sync;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -152,6 +154,23 @@ public class App : Application
|
|||||||
dash.OnNavigateToExam = id => main.NavigateToGroupDetail(id, 4); // Tab "Klausuren"
|
dash.OnNavigateToExam = id => main.NavigateToGroupDetail(id, 4); // Tab "Klausuren"
|
||||||
dash.OnNavigateToUnplannedLesson = id => main.NavigateToGroupDetail(id, 6); // Tab "Planung"
|
dash.OnNavigateToUnplannedLesson = id => main.NavigateToGroupDetail(id, 6); // Tab "Planung"
|
||||||
|
|
||||||
|
// Globale Suche/Schnellerfassung (14.2): Navigation bleibt im MainWindow-VM, die
|
||||||
|
// vorhandenen Dialog-Helfer übernehmen Eingabe und Validierung.
|
||||||
|
var search = Services.GetRequiredService<GlobalSearchViewModel>();
|
||||||
|
search.OnNavigate = result =>
|
||||||
|
{
|
||||||
|
if (result.Kind == GlobalSearchResultKind.Student && result.EntityId is { } studentId)
|
||||||
|
main.NavigateToStudent(studentId);
|
||||||
|
else if (result.Kind == GlobalSearchResultKind.Group && result.GroupId is { } groupId)
|
||||||
|
main.NavigateToGroupDetail(groupId);
|
||||||
|
else if (result.Kind == GlobalSearchResultKind.Exam && result.GroupId is { } examGroupId)
|
||||||
|
main.NavigateToGroupDetail(examGroupId, 4);
|
||||||
|
else if (result.Kind == GlobalSearchResultKind.Task)
|
||||||
|
main.NavigateToWorkload();
|
||||||
|
};
|
||||||
|
search.OnQuickAddTask = startAsReminder => ShowQuickTaskDialog(startAsReminder, dash);
|
||||||
|
search.OnQuickAddStudent = ShowAddStudentDialog;
|
||||||
|
|
||||||
// StudentList → StudentDetail + Anlegen
|
// StudentList → StudentDetail + Anlegen
|
||||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||||
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
|
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
|
||||||
@@ -171,4 +190,17 @@ public class App : Application
|
|||||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
||||||
await dialog.ShowDialog<bool>(owner);
|
await dialog.ShowDialog<bool>(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task ShowQuickTaskDialog(bool startAsReminder, DashboardViewModel dashboard)
|
||||||
|
{
|
||||||
|
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime
|
||||||
|
{ MainWindow: { } owner }) return;
|
||||||
|
|
||||||
|
var result = await WorkTaskDialogHelper.ShowDialog(owner, startAsReminder: startAsReminder);
|
||||||
|
if (result is null) return;
|
||||||
|
|
||||||
|
Services.GetRequiredService<IWorkTaskRepository>().Save(result);
|
||||||
|
dashboard.RefreshCommand.Execute(null);
|
||||||
|
Services.GetRequiredService<WorkTaskListViewModel>().Load();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ public static class AppBootstrapper
|
|||||||
// Singleton: einmal erstellt, überall dieselbe Instanz
|
// Singleton: einmal erstellt, überall dieselbe Instanz
|
||||||
services.AddSingleton<AppLockViewModel>();
|
services.AddSingleton<AppLockViewModel>();
|
||||||
services.AddSingleton<MainWindowViewModel>();
|
services.AddSingleton<MainWindowViewModel>();
|
||||||
|
services.AddSingleton<GlobalSearchViewModel>();
|
||||||
services.AddSingleton<DashboardViewModel>();
|
services.AddSingleton<DashboardViewModel>();
|
||||||
services.AddSingleton(sp =>
|
services.AddSingleton(sp =>
|
||||||
new SyncStatusViewModel(
|
new SyncStatusViewModel(
|
||||||
|
|||||||
@@ -107,6 +107,15 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public DashboardCardOption AttendanceCard => Card("attendance");
|
public DashboardCardOption AttendanceCard => Card("attendance");
|
||||||
public DashboardCardOption SupportCard => Card("support");
|
public DashboardCardOption SupportCard => Card("support");
|
||||||
public DashboardCardOption GroupsCard => Card("groups");
|
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,
|
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
||||||
IExamRepository exams, IExamResultRepository examResults, IGradeRepository grades,
|
IExamRepository exams, IExamResultRepository examResults, IGradeRepository grades,
|
||||||
@@ -204,6 +213,31 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
LoadOpenCorrections(groups, today);
|
LoadOpenCorrections(groups, today);
|
||||||
LoadUnplannedLessons(groups, today);
|
LoadUnplannedLessons(groups, today);
|
||||||
LoadAlerts(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()
|
private async Task LoadWeatherAsync()
|
||||||
@@ -592,6 +626,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
entry.Attendance = status;
|
entry.Attendance = status;
|
||||||
_participationEntries.Save(entry);
|
_participationEntries.Save(entry);
|
||||||
OpenExcuses.Remove(item);
|
OpenExcuses.Remove(item);
|
||||||
|
UpdateDashboardSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadCalendar()
|
private void LoadCalendar()
|
||||||
@@ -1034,18 +1069,29 @@ public sealed class DashboardAlertItem(Guid studentId, Guid? groupId, string stu
|
|||||||
public partial class DashboardCardOption : ObservableObject
|
public partial class DashboardCardOption : ObservableObject
|
||||||
{
|
{
|
||||||
[ObservableProperty] private bool _isVisible;
|
[ObservableProperty] private bool _isVisible;
|
||||||
|
[ObservableProperty] private bool _isEmpty;
|
||||||
[ObservableProperty] private int _row;
|
[ObservableProperty] private int _row;
|
||||||
[ObservableProperty] private int _column;
|
[ObservableProperty] private int _column;
|
||||||
public string Key { get; }
|
public string Key { get; }
|
||||||
public string Title { get; }
|
public string Title { get; }
|
||||||
|
public bool HideWhenEmpty { get; }
|
||||||
|
public bool EffectiveIsVisible => IsVisible && (!HideWhenEmpty || !IsEmpty);
|
||||||
public Action? OnVisibilityChanged { get; set; }
|
public Action? OnVisibilityChanged { get; set; }
|
||||||
|
|
||||||
public DashboardCardOption(string key, string title, bool isVisible)
|
public DashboardCardOption(string key, string title, bool isVisible)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
Title = title;
|
Title = title;
|
||||||
|
HideWhenEmpty = key is "excuses" or "upcoming" or "corrections" or "unplanned"
|
||||||
|
or "alerts" or "attendance" or "support";
|
||||||
_isVisible = isVisible;
|
_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
|
public partial class GroupListViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private const int QuickTasksMaxCount = 3;
|
|
||||||
|
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly ISubjectRepository _subjects;
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly ILessonRepository _lessons;
|
|
||||||
private readonly IExamRepository _exams;
|
|
||||||
private readonly IWorkTaskRepository _tasks;
|
|
||||||
|
|
||||||
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
||||||
public Func<Task>? OnAddGroup { get; set; }
|
public Func<Task>? OnAddGroup { get; set; }
|
||||||
@@ -29,11 +24,8 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
|
|
||||||
[ObservableProperty] private string _selectedSchoolYear = "";
|
[ObservableProperty] private string _selectedSchoolYear = "";
|
||||||
[ObservableProperty] private string _searchText = "";
|
[ObservableProperty] private string _searchText = "";
|
||||||
[ObservableProperty] private GroupListItem? _selectedGroup;
|
|
||||||
[ObservableProperty] private bool _showArchived;
|
[ObservableProperty] private bool _showArchived;
|
||||||
|
|
||||||
public string SelectedGroupDisplayName => SelectedGroup?.DisplayName ?? "";
|
|
||||||
public string SelectedGroupSubtitle => SelectedGroup?.Subtitle ?? "";
|
|
||||||
public string ListSummary => ShowArchived
|
public string ListSummary => ShowArchived
|
||||||
? $"{Groups.Count} archivierte Gruppen · {SelectedSchoolYear}"
|
? $"{Groups.Count} archivierte Gruppen · {SelectedSchoolYear}"
|
||||||
: $"{Groups.Count} aktive Gruppen · {SelectedSchoolYear}";
|
: $"{Groups.Count} aktive Gruppen · {SelectedSchoolYear}";
|
||||||
@@ -45,22 +37,10 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
public ObservableCollection<string> SchoolYears { get; } = [];
|
public ObservableCollection<string> SchoolYears { get; } = [];
|
||||||
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
||||||
|
|
||||||
// ── Schnellüberblick im Auswahl-Panel (Nutzer-Feedback: "oberhalb der Buttonliste ein paar
|
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
_groups = groups;
|
||||||
|
_subjects = subjects;
|
||||||
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
|
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
|
||||||
SelectedSchoolYear = sy.CurrentSchoolYear();
|
SelectedSchoolYear = sy.CurrentSchoolYear();
|
||||||
}
|
}
|
||||||
@@ -68,58 +48,8 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
|
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
|
||||||
partial void OnSearchTextChanged(string value) => LoadGroups();
|
partial void OnSearchTextChanged(string value) => LoadGroups();
|
||||||
partial void OnShowArchivedChanged(bool 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()
|
public void LoadGroups()
|
||||||
{
|
{
|
||||||
var selectedId = SelectedGroup?.Id;
|
|
||||||
Groups.Clear();
|
Groups.Clear();
|
||||||
var all = _groups.GetBySchoolYear(SelectedSchoolYear, includeInactive: ShowArchived)
|
var all = _groups.GetBySchoolYear(SelectedSchoolYear, includeInactive: ShowArchived)
|
||||||
.Where(g => g.IsActive != ShowArchived);
|
.Where(g => g.IsActive != ShowArchived);
|
||||||
@@ -129,8 +59,18 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
|| (g.SubjectId is Guid id && subjectNames.GetValueOrDefault(id, "")
|
|| (g.SubjectId is Guid id && subjectNames.GetValueOrDefault(id, "")
|
||||||
.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
|
.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
|
||||||
foreach (var g in filtered.OrderBy(g => g.Name))
|
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(ListSummary));
|
||||||
OnPropertyChanged(nameof(HasNoGroups));
|
OnPropertyChanged(nameof(HasNoGroups));
|
||||||
OnPropertyChanged(nameof(EmptyListMessage));
|
OnPropertyChanged(nameof(EmptyListMessage));
|
||||||
@@ -145,63 +85,57 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
[RelayCommand] private void Refresh() => LoadGroups();
|
[RelayCommand] private void Refresh() => LoadGroups();
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanEditSelectedGroup))]
|
[RelayCommand]
|
||||||
private async Task EditGroup()
|
private void OpenGroup(GroupListItem? group)
|
||||||
{
|
{
|
||||||
if (SelectedGroup is null || OnEditGroup is null) return;
|
if (group is not null) OnNavigateToDetail?.Invoke(group.Id, 0);
|
||||||
var id = SelectedGroup.Id;
|
|
||||||
await OnEditGroup(id);
|
|
||||||
LoadGroups();
|
|
||||||
SelectedGroup = Groups.FirstOrDefault(g => g.Id == id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
|
[RelayCommand]
|
||||||
private async Task RollOverGroup()
|
private async Task EditGroup(GroupListItem? group)
|
||||||
{
|
{
|
||||||
if (SelectedGroup is null || OnRollOverGroup is null) return;
|
if (group?.IsActive != true || OnEditGroup is null) return;
|
||||||
var targetId = await OnRollOverGroup(SelectedGroup.Id);
|
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;
|
if (targetId is null) return;
|
||||||
var target = _groups.GetById(targetId.Value);
|
var target = _groups.GetById(targetId.Value);
|
||||||
if (target is null) return;
|
if (target is null) return;
|
||||||
if (!SchoolYears.Contains(target.SchoolYear)) SchoolYears.Insert(0, target.SchoolYear);
|
if (!SchoolYears.Contains(target.SchoolYear)) SchoolYears.Insert(0, target.SchoolYear);
|
||||||
SelectedSchoolYear = target.SchoolYear;
|
SelectedSchoolYear = target.SchoolYear;
|
||||||
LoadGroups();
|
LoadGroups();
|
||||||
SelectedGroup = Groups.FirstOrDefault(g => g.Id == target.Id);
|
OnNavigateToDetail?.Invoke(target.Id, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
|
[RelayCommand]
|
||||||
private void ToggleArchive()
|
private void ToggleArchive(GroupListItem? selected)
|
||||||
{
|
{
|
||||||
if (SelectedGroup is null) return;
|
if (selected is null) return;
|
||||||
var group = _groups.GetById(SelectedGroup.Id);
|
var group = _groups.GetById(selected.Id);
|
||||||
if (group is null) return;
|
if (group is null) return;
|
||||||
group.IsActive = !group.IsActive;
|
group.IsActive = !group.IsActive;
|
||||||
_groups.Save(group);
|
_groups.Save(group);
|
||||||
LoadGroups();
|
LoadGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanEditSelectedGroup))]
|
[RelayCommand]
|
||||||
private async Task DeleteGroup()
|
private async Task DeleteGroup(GroupListItem? group)
|
||||||
{
|
{
|
||||||
if (SelectedGroup is null || OnConfirmDelete is null) return;
|
if (group?.IsActive != true || OnConfirmDelete is null) return;
|
||||||
var selected = SelectedGroup;
|
if (!await OnConfirmDelete(group)) return;
|
||||||
if (!await OnConfirmDelete(selected)) return;
|
_groups.Delete(group.Id);
|
||||||
_groups.Delete(selected.Id);
|
|
||||||
LoadGroups();
|
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;
|
public partial class GroupListItem : ObservableObject
|
||||||
private bool CanEditSelectedGroup() => SelectedGroup?.IsActive == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GroupListItem
|
|
||||||
{
|
{
|
||||||
public Guid Id { get; }
|
public Guid Id { get; }
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
@@ -212,6 +146,13 @@ public class GroupListItem
|
|||||||
public string Subtitle { get; }
|
public string Subtitle { get; }
|
||||||
public bool IsActive { get; }
|
public bool IsActive { get; }
|
||||||
public string ArchiveActionLabel => IsActive ? "Archivieren" : "Wieder aktivieren";
|
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)
|
public GroupListItem(LearningGroup g, string subjectName)
|
||||||
{
|
{
|
||||||
@@ -224,6 +165,12 @@ public class GroupListItem
|
|||||||
DisplayName = string.IsNullOrEmpty(subjectName) ? g.Name : $"{g.Name} · {subjectName}";
|
DisplayName = string.IsNullOrEmpty(subjectName) ? g.Name : $"{g.Name} · {subjectName}";
|
||||||
Subtitle = $"{TypeLabel} · Stufe {g.GradeLevel} · Noten {GradingLabel} · {g.SchoolYear}";
|
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 ─────────────────────────────────────────────────────────────
|
// ── Gruppendetail ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty] private ObservableObject? _currentPage;
|
[ObservableProperty] private ObservableObject? _currentPage;
|
||||||
[ObservableProperty] private NavItem _activeNavItem = NavItem.Dashboard;
|
[ObservableProperty] private NavItem _activeNavItem = NavItem.Dashboard;
|
||||||
[ObservableProperty] private string _currentSchoolYear = "";
|
[ObservableProperty] private string _currentSchoolYear = "";
|
||||||
|
[ObservableProperty] private bool _isCommandPaletteOpen;
|
||||||
|
|
||||||
public SyncStatusViewModel SyncStatus { get; }
|
public SyncStatusViewModel SyncStatus { get; }
|
||||||
public ObservableCollection<ToastItem> Toasts { get; }
|
public ObservableCollection<ToastItem> Toasts { get; }
|
||||||
public AppLockViewModel AppLock { get; }
|
public AppLockViewModel AppLock { get; }
|
||||||
|
public GlobalSearchViewModel CommandPalette { get; }
|
||||||
|
|
||||||
public bool IsDashboardActive => ActiveNavItem == NavItem.Dashboard;
|
public bool IsDashboardActive => ActiveNavItem == NavItem.Dashboard;
|
||||||
public bool IsGroupsActive => ActiveNavItem == NavItem.Groups;
|
public bool IsGroupsActive => ActiveNavItem == NavItem.Groups;
|
||||||
@@ -37,18 +39,32 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
|
|
||||||
public MainWindowViewModel(IServiceProvider services,
|
public MainWindowViewModel(IServiceProvider services,
|
||||||
DashboardViewModel dashboard, SchoolYearService sy,
|
DashboardViewModel dashboard, SchoolYearService sy,
|
||||||
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock)
|
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock,
|
||||||
|
GlobalSearchViewModel commandPalette)
|
||||||
{
|
{
|
||||||
_services = services;
|
_services = services;
|
||||||
SyncStatus = syncStatus;
|
SyncStatus = syncStatus;
|
||||||
Toasts = notifications.Toasts;
|
Toasts = notifications.Toasts;
|
||||||
AppLock = appLock;
|
AppLock = appLock;
|
||||||
|
CommandPalette = commandPalette;
|
||||||
|
CommandPalette.OnClose = CloseCommandPalette;
|
||||||
CurrentSchoolYear = sy.CurrentSchoolYear();
|
CurrentSchoolYear = sy.CurrentSchoolYear();
|
||||||
CurrentPage = dashboard;
|
CurrentPage = dashboard;
|
||||||
AppLock.ApplyConfig();
|
AppLock.ApplyConfig();
|
||||||
SyncStatus.DataChanged += OnSyncDataChanged;
|
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
|
// EventApplier schreibt bei eingehenden Sync-Ereignissen absichtlich direkt auf die rohe
|
||||||
// LiteDB-Collection, an jedem ViewModel vorbei (Ping-Pong-Vermeidung, siehe EventApplier-
|
// LiteDB-Collection, an jedem ViewModel vorbei (Ping-Pong-Vermeidung, siehe EventApplier-
|
||||||
// Klassenkommentar) - ohne diesen Hook blieb die gerade sichtbare Seite bis zum nächsten
|
// Klassenkommentar) - ohne diesen Hook blieb die gerade sichtbare Seite bis zum nächsten
|
||||||
|
|||||||
@@ -28,11 +28,37 @@
|
|||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||||
<Button Content="Klassenbuch abgleichen…" Click="OnCompareWebUntisDocumentationClick"
|
<Button Content="Klassenbuch abgleichen…" Click="OnCompareWebUntisDocumentationClick"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<Button Content="Dashboard anpassen" Command="{Binding ToggleDashboardSettingsCommand}"
|
<Button Content="Bereiche anpassen" Command="{Binding ToggleDashboardSettingsCommand}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Der Tagesfokus beantwortet zuerst die vier Fragen, die beim Öffnen der App zählen:
|
||||||
|
Was unterrichte ich, was ist zu tun, wo muss ich reagieren und was steht an? -->
|
||||||
|
<Border Background="{DynamicResource AppCardBackgroundBrush}"
|
||||||
|
BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1"
|
||||||
|
CornerRadius="10" Padding="16,14">
|
||||||
|
<Grid ColumnDefinitions="*,*,*,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="3">
|
||||||
|
<TextBlock Text="UNTERRICHT HEUTE" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding TodayLessonSummary}" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1" Spacing="3" Margin="18,0,0,0">
|
||||||
|
<TextBlock Text="AUFGABEN" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding OpenTaskSummary}" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="3" Margin="18,0,0,0">
|
||||||
|
<TextBlock Text="HANDLUNGSBEDARF" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding AttentionSummary}" FontSize="18" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AppStatusWarningBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="3" Spacing="3" Margin="18,0,0,0">
|
||||||
|
<TextBlock Text="NÄCHSTE 30 TAGE" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding UpcomingSummary}" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8"
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8"
|
||||||
Padding="12" IsVisible="{Binding IsDashboardSettingsOpen}">
|
Padding="12" IsVisible="{Binding IsDashboardSettingsOpen}">
|
||||||
<ItemsControl ItemsSource="{Binding DashboardCards}">
|
<ItemsControl ItemsSource="{Binding DashboardCards}">
|
||||||
@@ -59,14 +85,15 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Serverseitig gecachte DWD-Daten für den in den Einstellungen hinterlegten Schulstandort. -->
|
<!-- Serverseitig gecachte DWD-Daten für den in den Einstellungen hinterlegten Schulstandort. -->
|
||||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8"
|
<Expander Header="{Binding WeatherSummary}" IsExpanded="{Binding HasWeatherWarnings}"
|
||||||
Padding="16" IsVisible="{Binding IsWeatherPanelVisible}">
|
IsVisible="{Binding IsWeatherPanelVisible}"
|
||||||
<StackPanel Spacing="10">
|
Background="{DynamicResource AppCardBackgroundBrush}"
|
||||||
|
BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1"
|
||||||
|
CornerRadius="8" Padding="12,6">
|
||||||
|
<StackPanel Spacing="10" Margin="6,8,6,6">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="WETTER AM SCHULSTANDORT" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
<TextBlock Text="WETTER AM SCHULSTANDORT" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
<TextBlock Text="{Binding WeatherSummary}" FontSize="20" FontWeight="SemiBold" Margin="0,5,0,0"
|
|
||||||
IsVisible="{Binding WeatherSummary, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<TextBlock Text="{Binding WeatherDetails}" FontSize="11" Opacity="0.65" TextWrapping="Wrap"
|
<TextBlock Text="{Binding WeatherDetails}" FontSize="11" Opacity="0.65" TextWrapping="Wrap"
|
||||||
IsVisible="{Binding WeatherDetails, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding WeatherDetails, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -96,17 +123,20 @@
|
|||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
<TextBlock Text="Wetterdaten © Deutscher Wetterdienst" FontSize="10" Opacity="0.5"/>
|
<TextBlock Text="Wetterdaten © Deutscher Wetterdienst" FontSize="10" Opacity="0.5"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Expander>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
|
<TextBlock Text="HEUTE UND HANDLUNGSBEDARF" FontSize="11" FontWeight="Bold" Opacity="0.5"
|
||||||
|
Margin="2,2,0,-8"/>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="3*,2*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
|
||||||
|
|
||||||
<!-- Heutige Stunden -->
|
<!-- Heutige Stunden -->
|
||||||
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
|
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
|
||||||
IsVisible="{Binding TodayCard.IsVisible}" Margin="0,0,8,8"
|
IsVisible="{Binding TodayCard.EffectiveIsVisible}" Margin="0,0,8,8"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="HEUTE" FontSize="11" FontWeight="Bold"
|
<TextBlock Text="Heute" FontSize="14" FontWeight="SemiBold"
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
Opacity="0.5" Margin="0,0,0,10"/>
|
||||||
<ItemsControl ItemsSource="{Binding TodaysLessons}">
|
<ItemsControl ItemsSource="{Binding TodaysLessons}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
@@ -143,12 +173,12 @@
|
|||||||
|
|
||||||
<!-- Offene Aufgaben -->
|
<!-- Offene Aufgaben -->
|
||||||
<Border Grid.Column="{Binding TasksCard.Column}" Grid.Row="{Binding TasksCard.Row}"
|
<Border Grid.Column="{Binding TasksCard.Column}" Grid.Row="{Binding TasksCard.Row}"
|
||||||
IsVisible="{Binding TasksCard.IsVisible}" Margin="8,0,0,8"
|
IsVisible="{Binding TasksCard.EffectiveIsVisible}" Margin="8,0,0,8"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,10">
|
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,10">
|
||||||
<TextBlock Grid.Column="0" Text="OFFENE AUFGABEN" FontSize="11" FontWeight="Bold"
|
<TextBlock Grid.Column="0" Text="Offene Aufgaben" FontSize="14" FontWeight="SemiBold"
|
||||||
Opacity="0.5" VerticalAlignment="Center"/>
|
Opacity="0.5" VerticalAlignment="Center"/>
|
||||||
<Button Grid.Column="1" Content="🔔+" FontSize="12" Padding="7,2" Margin="0,0,4,0"
|
<Button Grid.Column="1" Content="🔔+" FontSize="12" Padding="7,2" Margin="0,0,4,0"
|
||||||
ToolTip.Tip="Erinnerung anlegen" Command="{Binding AddReminderCommand}"/>
|
ToolTip.Tip="Erinnerung anlegen" Command="{Binding AddReminderCommand}"/>
|
||||||
@@ -180,7 +210,7 @@
|
|||||||
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
|
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
|
||||||
Lerngruppen-Liste darunter ihn nicht nach unten verdrängt. -->
|
Lerngruppen-Liste darunter ihn nicht nach unten verdrängt. -->
|
||||||
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
|
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
|
||||||
IsVisible="{Binding CalendarCard.IsVisible}" Margin="0,0,8,8"
|
IsVisible="{Binding CalendarCard.EffectiveIsVisible}" Margin="0,0,8,8"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
@@ -321,7 +351,7 @@
|
|||||||
|
|
||||||
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
|
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
|
||||||
<Border Grid.Column="{Binding ExcusesCard.Column}" Grid.Row="{Binding ExcusesCard.Row}"
|
<Border Grid.Column="{Binding ExcusesCard.Column}" Grid.Row="{Binding ExcusesCard.Row}"
|
||||||
IsVisible="{Binding ExcusesCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
IsVisible="{Binding ExcusesCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -355,7 +385,7 @@
|
|||||||
|
|
||||||
<!-- Fehlzeiten-Warnung (5.2.3) -->
|
<!-- Fehlzeiten-Warnung (5.2.3) -->
|
||||||
<Border Grid.Column="{Binding AttendanceCard.Column}" Grid.Row="{Binding AttendanceCard.Row}"
|
<Border Grid.Column="{Binding AttendanceCard.Column}" Grid.Row="{Binding AttendanceCard.Row}"
|
||||||
IsVisible="{Binding AttendanceCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
IsVisible="{Binding AttendanceCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -385,7 +415,7 @@
|
|||||||
|
|
||||||
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
||||||
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
|
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
|
||||||
IsVisible="{Binding SupportCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
IsVisible="{Binding SupportCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -416,7 +446,7 @@
|
|||||||
|
|
||||||
<!-- Anstehende Termine (9.3) -->
|
<!-- Anstehende Termine (9.3) -->
|
||||||
<Border Grid.Column="{Binding UpcomingCard.Column}" Grid.Row="{Binding UpcomingCard.Row}"
|
<Border Grid.Column="{Binding UpcomingCard.Column}" Grid.Row="{Binding UpcomingCard.Row}"
|
||||||
IsVisible="{Binding UpcomingCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
IsVisible="{Binding UpcomingCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -453,7 +483,7 @@
|
|||||||
|
|
||||||
<!-- Offene Korrekturen (9.4) -->
|
<!-- Offene Korrekturen (9.4) -->
|
||||||
<Border Grid.Column="{Binding CorrectionsCard.Column}" Grid.Row="{Binding CorrectionsCard.Row}"
|
<Border Grid.Column="{Binding CorrectionsCard.Column}" Grid.Row="{Binding CorrectionsCard.Row}"
|
||||||
IsVisible="{Binding CorrectionsCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
IsVisible="{Binding CorrectionsCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -486,7 +516,7 @@
|
|||||||
|
|
||||||
<!-- Ungeplante Stunden -->
|
<!-- Ungeplante Stunden -->
|
||||||
<Border Grid.Column="{Binding UnplannedCard.Column}" Grid.Row="{Binding UnplannedCard.Row}"
|
<Border Grid.Column="{Binding UnplannedCard.Column}" Grid.Row="{Binding UnplannedCard.Row}"
|
||||||
IsVisible="{Binding UnplannedCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
IsVisible="{Binding UnplannedCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -516,7 +546,7 @@
|
|||||||
|
|
||||||
<!-- Auffälligkeiten (9.5) -->
|
<!-- Auffälligkeiten (9.5) -->
|
||||||
<Border Grid.Column="{Binding AlertsCard.Column}" Grid.Row="{Binding AlertsCard.Row}"
|
<Border Grid.Column="{Binding AlertsCard.Column}" Grid.Row="{Binding AlertsCard.Row}"
|
||||||
IsVisible="{Binding AlertsCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
IsVisible="{Binding AlertsCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -550,7 +580,7 @@
|
|||||||
|
|
||||||
<!-- Meine Lerngruppen -->
|
<!-- Meine Lerngruppen -->
|
||||||
<Border Grid.Column="{Binding GroupsCard.Column}" Grid.Row="{Binding GroupsCard.Row}"
|
<Border Grid.Column="{Binding GroupsCard.Column}" Grid.Row="{Binding GroupsCard.Row}"
|
||||||
IsVisible="{Binding GroupsCard.IsVisible}"
|
IsVisible="{Binding GroupsCard.EffectiveIsVisible}"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|||||||
@@ -5,13 +5,6 @@
|
|||||||
x:Class="LehrerApp.Desktop.Views.Groups.GroupListView"
|
x:Class="LehrerApp.Desktop.Views.Groups.GroupListView"
|
||||||
x:DataType="vm:GroupListViewModel">
|
x:DataType="vm:GroupListViewModel">
|
||||||
|
|
||||||
<UserControl.Styles>
|
|
||||||
<Style Selector="TextBlock.overdue">
|
|
||||||
<Setter Property="Foreground" Value="Red"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
</Style>
|
|
||||||
</UserControl.Styles>
|
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
|
||||||
<!-- Kopfzeile mit Schuljahr-Wähler und Neu-Button -->
|
<!-- Kopfzeile mit Schuljahr-Wähler und Neu-Button -->
|
||||||
@@ -28,180 +21,85 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Master-Detail: Liste links, Übersicht rechts -->
|
<!-- Eine Navigationsebene: Karten öffnen direkt das Gruppendetail. Die Verwaltung sitzt
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="260,*">
|
am jeweiligen Eintrag und benötigt keine vorgeschaltete Bereichsauswahl mehr. -->
|
||||||
|
<Grid Grid.Row="1" RowDefinitions="Auto,*">
|
||||||
<!-- Linke Spalte: Sucheingabe + Listenansicht -->
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="24,18,24,8">
|
||||||
<Border Grid.Column="0"
|
<TextBox Grid.Column="0" Text="{Binding SearchText}"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
PlaceholderText="Name oder Fach suchen …" MaxWidth="560"
|
||||||
BorderThickness="0,0,1,0">
|
HorizontalAlignment="Stretch"/>
|
||||||
<DockPanel>
|
<ToggleSwitch Grid.Column="1" Content="Archiv anzeigen" IsChecked="{Binding ShowArchived}"
|
||||||
<StackPanel DockPanel.Dock="Top" Margin="12,8" Spacing="8">
|
Margin="20,0,0,0" VerticalAlignment="Center"/>
|
||||||
<TextBox Text="{Binding SearchText}" PlaceholderText="Suchen…"/>
|
|
||||||
<ToggleSwitch Content="Archiv anzeigen" IsChecked="{Binding ShowArchived}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Text="{Binding EmptyListMessage}" TextWrapping="Wrap"
|
|
||||||
Margin="16,12" FontSize="12" Opacity="0.45"
|
|
||||||
IsVisible="{Binding HasNoGroups}"/>
|
|
||||||
<ListBox ItemsSource="{Binding Groups}"
|
|
||||||
SelectedItem="{Binding SelectedGroup}">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:GroupListItem">
|
|
||||||
<Grid ColumnDefinitions="4,*" Margin="2,4">
|
|
||||||
<Border Grid.Column="0" Width="4" CornerRadius="2"
|
|
||||||
Background="{DynamicResource SystemAccentColor}"
|
|
||||||
Margin="0,0,10,0"/>
|
|
||||||
<StackPanel Grid.Column="1">
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<TextBlock Grid.Column="0" Text="{Binding Name}"
|
|
||||||
FontWeight="SemiBold" FontSize="13"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding TypeLabel}"
|
|
||||||
FontSize="11" Opacity="0.5"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock Text="{Binding Subject}" FontSize="12" Opacity="0.65"
|
|
||||||
|
<StackPanel Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10"
|
||||||
|
IsVisible="{Binding HasNoGroups}">
|
||||||
|
<TextBlock Text="{Binding EmptyListMessage}" FontSize="15" Opacity="0.55"
|
||||||
|
TextWrapping="Wrap" TextAlignment="Center"/>
|
||||||
|
<Button Content="+ Erste Lerngruppe anlegen" Command="{Binding AddGroupCommand}"
|
||||||
|
IsVisible="{Binding !ShowArchived}" HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" IsVisible="{Binding !HasNoGroups}"
|
||||||
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<ItemsControl ItemsSource="{Binding Groups}" Margin="20,12,20,24">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:GroupListItem">
|
||||||
|
<Border Width="330" MinHeight="116" Margin="6" CornerRadius="9"
|
||||||
|
Background="{DynamicResource AppCardBackgroundBrush}"
|
||||||
|
BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1">
|
||||||
|
<Grid ColumnDefinitions="4,*,Auto">
|
||||||
|
<Border Grid.Column="0" Background="{DynamicResource SystemAccentColor}"
|
||||||
|
CornerRadius="9,0,0,9"/>
|
||||||
|
<Button Grid.Column="1" Background="Transparent" BorderThickness="0"
|
||||||
|
Padding="16,14" HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
|
||||||
|
Command="{Binding OpenCommand}"
|
||||||
|
AutomationProperties.Name="{Binding OpenAutomationName}">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="17" FontWeight="SemiBold"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock Text="{Binding Subject}" FontSize="13" Opacity="0.7"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
IsVisible="{Binding Subject, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding Subject, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
<TextBlock Text="{Binding GradingLabel}" FontSize="11" Opacity="0.4"/>
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
</StackPanel>
|
<Border Background="{DynamicResource AppChipBackgroundBrush}" CornerRadius="8" Padding="7,2">
|
||||||
</Grid>
|
<TextBlock Text="{Binding TypeLabel}" FontSize="10" Opacity="0.75"/>
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
</Border>
|
||||||
|
<TextBlock Text="{Binding GradingLabel}" FontSize="11" Opacity="0.5"
|
||||||
<!-- Rechte Spalte: Platzhalter wenn keine Auswahl -->
|
VerticalAlignment="Center"/>
|
||||||
<StackPanel Grid.Column="1" HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center" Spacing="8"
|
|
||||||
IsVisible="{Binding SelectedGroup, Converter={x:Static ObjectConverters.IsNull}}">
|
|
||||||
<TextBlock Text="Gruppe auswählen" FontSize="16" Opacity="0.4"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
<TextBlock Text="oder + Neue Gruppe anlegen" FontSize="12" Opacity="0.3"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Rechte Spalte: Gruppen-Übersicht wenn ausgewählt -->
|
|
||||||
<ScrollViewer Grid.Column="1"
|
|
||||||
IsVisible="{Binding SelectedGroup, Converter={x:Static ObjectConverters.IsNotNull}}">
|
|
||||||
<StackPanel Margin="28,24" Spacing="20">
|
|
||||||
|
|
||||||
<!-- Gruppenname und Kurzinfos -->
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
|
||||||
<TextBlock Text="{Binding SelectedGroupDisplayName}"
|
|
||||||
FontSize="24" FontWeight="SemiBold" TextWrapping="Wrap"/>
|
|
||||||
<TextBlock Text="{Binding SelectedGroupSubtitle}"
|
|
||||||
FontSize="12" Opacity="0.55"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="1" Content="⋯ Verwalten" VerticalAlignment="Top"
|
</Button>
|
||||||
Margin="16,0,0,0">
|
<Button Grid.Column="2" Content="⋯" Width="36" Height="32" Margin="0,10,10,0"
|
||||||
|
Padding="0" VerticalAlignment="Top"
|
||||||
|
ToolTip.Tip="Lerngruppe verwalten"
|
||||||
|
AutomationProperties.Name="{Binding ManageAutomationName}">
|
||||||
<Button.Flyout>
|
<Button.Flyout>
|
||||||
<MenuFlyout>
|
<MenuFlyout>
|
||||||
<MenuItem Header="Details bearbeiten"
|
<MenuItem Header="Details bearbeiten" IsEnabled="{Binding IsActive}"
|
||||||
Command="{Binding EditGroupCommand}"/>
|
Command="{Binding EditCommand}"/>
|
||||||
<MenuItem Header="Ins nächste Schuljahr übernehmen …"
|
<MenuItem Header="Ins nächste Schuljahr übernehmen …"
|
||||||
Command="{Binding RollOverGroupCommand}"/>
|
Command="{Binding RollOverCommand}"/>
|
||||||
<MenuItem Header="{Binding SelectedGroup.ArchiveActionLabel}"
|
<MenuItem Header="{Binding ArchiveActionLabel}"
|
||||||
Command="{Binding ToggleArchiveCommand}"/>
|
Command="{Binding ToggleArchiveCommand}"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem Header="Teilnehmer importieren (bald)" IsEnabled="False"
|
<MenuItem Header="Lerngruppe löschen" IsEnabled="{Binding IsActive}"
|
||||||
ToolTip.Tip="Import aus dem Teilnehmerexport der Lernplattform folgt."/>
|
Command="{Binding DeleteCommand}"/>
|
||||||
<Separator/>
|
|
||||||
<MenuItem Header="Lerngruppe löschen"
|
|
||||||
Command="{Binding DeleteGroupCommand}"/>
|
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Border>
|
||||||
<Separator/>
|
|
||||||
|
|
||||||
<!-- Schnellüberblick (Nutzer-Feedback) -->
|
|
||||||
<StackPanel Spacing="8" IsVisible="{Binding QuickHasAnything}">
|
|
||||||
<TextBlock Text="SCHNELLÜBERBLICK" FontSize="10" FontWeight="Bold" Opacity="0.4"/>
|
|
||||||
<StackPanel Spacing="1" IsVisible="{Binding QuickHasNextLesson}">
|
|
||||||
<TextBlock Text="Nächste Stunde" FontSize="11" Opacity="0.55"/>
|
|
||||||
<TextBlock Text="{Binding QuickNextLessonLabel}" FontSize="13" TextWrapping="Wrap"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Spacing="1" IsVisible="{Binding QuickHasNextExam}">
|
|
||||||
<TextBlock Text="Nächste Klausur" FontSize="11" Opacity="0.55"/>
|
|
||||||
<TextBlock Text="{Binding QuickNextExamLabel}" FontSize="13" TextWrapping="Wrap"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Spacing="4" IsVisible="{Binding QuickHasTasks}">
|
|
||||||
<TextBlock Text="Wichtige Aufgaben" FontSize="11" Opacity="0.55"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding QuickTasks}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:GroupTaskItem">
|
|
||||||
<Grid ColumnDefinitions="4,Auto,*,Auto" Margin="0,2">
|
|
||||||
<Border Grid.Column="0" Background="{Binding PriorityColorHex}" CornerRadius="2"
|
|
||||||
Margin="0,0,6,0" IsVisible="{Binding IsHighPriority}"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="🔔" FontSize="11" Margin="0,0,4,0"
|
|
||||||
IsVisible="{Binding IsReminder}" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Grid.Column="2" Text="{Binding Title}" FontSize="12"
|
|
||||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Grid.Column="3" Text="{Binding DueDateDisplay}" FontSize="11"
|
|
||||||
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
<Separator IsVisible="{Binding QuickHasAnything}"/>
|
|
||||||
|
|
||||||
<!-- Bereichs-Navigation -->
|
|
||||||
<TextBlock Text="BEREICHE" FontSize="10" FontWeight="Bold"
|
|
||||||
Opacity="0.4" Margin="0,0,0,2"/>
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<Button Content="📊 Übersicht"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="0"/>
|
|
||||||
<Button Content="👤 Schülerliste"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="1"/>
|
|
||||||
<Button Content="🪑 Sitzpläne"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="2"/>
|
|
||||||
<Button Content="✋ Mitarbeit"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="3"/>
|
|
||||||
<Button Content="📝 Klausuren"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="4"/>
|
|
||||||
<Button Content="🔢 Notenübersicht"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="5"/>
|
|
||||||
<Button Content="📅 Unterrichtsplanung"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="6"/>
|
|
||||||
<Button Content="🎯 Kompetenzübersicht"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="7"/>
|
|
||||||
<Button Content="📋 Dokumentation"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
||||||
Padding="12,9"
|
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
|
||||||
CommandParameter="8"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -146,6 +146,20 @@
|
|||||||
|
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Classes="navitems" Margin="8,12,8,0" Spacing="2">
|
<StackPanel Classes="navitems" Margin="8,12,8,0" Spacing="2">
|
||||||
|
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left" CornerRadius="6"
|
||||||
|
Click="OnOpenCommandPaletteClick"
|
||||||
|
ToolTip.Tip="Suchen und schnell erfassen (Strg/⌘+K)"
|
||||||
|
AutomationProperties.Name="Suchen und schnell erfassen">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Classes="navicon" Text="⌕" TextAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Suchen / Erfassen"/>
|
||||||
|
<TextBlock Grid.Column="2" Classes="navlabel" Text="⌘K" FontSize="10" Opacity="0.45"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
<Separator Margin="4,6"/>
|
||||||
|
|
||||||
<Button Classes="navitem" Classes.active="{Binding IsDashboardActive}" HorizontalAlignment="Stretch"
|
<Button Classes="navitem" Classes.active="{Binding IsDashboardActive}" HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Left"
|
HorizontalContentAlignment="Left"
|
||||||
CornerRadius="6"
|
CornerRadius="6"
|
||||||
@@ -241,6 +255,77 @@
|
|||||||
|
|
||||||
</DrawerPage>
|
</DrawerPage>
|
||||||
|
|
||||||
|
<!-- Globale Suche und Schnellerfassung (14.2). Bewusst als Overlay auf der aktuellen Seite:
|
||||||
|
Der Nutzer behält den Kontext und kann mit Escape ohne Navigation zurückkehren. -->
|
||||||
|
<Border Background="#A0000000" IsVisible="{Binding IsCommandPaletteOpen}"
|
||||||
|
AutomationProperties.Name="Globale Suche und Schnellerfassung">
|
||||||
|
<Grid>
|
||||||
|
<Button Background="Transparent" BorderThickness="0"
|
||||||
|
Command="{Binding CloseCommandPaletteCommand}"
|
||||||
|
AutomationProperties.Name="Suche schließen"/>
|
||||||
|
<Border Width="680" MaxHeight="570" Margin="24" Padding="0"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Top"
|
||||||
|
Background="{DynamicResource AppCardBackgroundBrush}"
|
||||||
|
BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1"
|
||||||
|
CornerRadius="12">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="18,16,18,10">
|
||||||
|
<TextBlock Text="⌕" FontSize="24" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
|
<TextBox x:Name="CommandPaletteSearchBox" Grid.Column="1"
|
||||||
|
Text="{Binding CommandPalette.Query, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
PlaceholderText="Schüler, Lerngruppe, Klausur oder Aufgabe suchen …"
|
||||||
|
FontSize="16" BorderThickness="0" Background="Transparent"
|
||||||
|
AutomationProperties.Name="Suchbegriff"/>
|
||||||
|
<Button Grid.Column="2" Content="Esc" FontSize="10" Padding="8,3"
|
||||||
|
Command="{Binding CloseCommandPaletteCommand}"
|
||||||
|
AutomationProperties.Name="Suche schließen"/>
|
||||||
|
</Grid>
|
||||||
|
<Separator Grid.Row="1"/>
|
||||||
|
<ListBox Grid.Row="2" ItemsSource="{Binding CommandPalette.Results}"
|
||||||
|
SelectedItem="{Binding CommandPalette.SelectedResult}"
|
||||||
|
Background="Transparent" BorderThickness="0" Margin="8"
|
||||||
|
MaxHeight="420">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:GlobalSearchResult">
|
||||||
|
<Button Background="Transparent" BorderThickness="0" Padding="10,8"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||||
|
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).CommandPalette.ExecuteCommand}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<Grid ColumnDefinitions="38,*,Auto">
|
||||||
|
<Border Width="30" Height="30" CornerRadius="7"
|
||||||
|
Background="{DynamicResource AppAccentSoftBackgroundBrush}"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Icon}" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AppAccentOnSoftBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Grid.Column="1" Margin="10,0">
|
||||||
|
<TextBlock Text="{Binding Title}" FontSize="14" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding Subtitle}" FontSize="11" Opacity="0.6"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border Grid.Column="2" Padding="7,3" CornerRadius="8"
|
||||||
|
Background="{DynamicResource AppChipBackgroundBrush}"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding KindLabel}" FontSize="10" Opacity="0.7"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
<TextBlock Grid.Row="2" Text="Keine passenden Ergebnisse."
|
||||||
|
Classes="emptyhint" HorizontalAlignment="Center" Margin="20"
|
||||||
|
IsVisible="{Binding CommandPalette.ShowNoResults}"/>
|
||||||
|
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="18,8,18,14">
|
||||||
|
<TextBlock Text="↑↓ auswählen · Enter öffnen · Esc schließen" FontSize="10" Opacity="0.5"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Strg/⌘ + K" FontSize="10" Opacity="0.5"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Dauerhafte Meldung bei einer echten Protokoll-Inkompatibilität. Als Overlay außerhalb
|
<!-- Dauerhafte Meldung bei einer echten Protokoll-Inkompatibilität. Als Overlay außerhalb
|
||||||
von DrawerPage.Content bleibt der DataContext das MainWindowViewModel; innerhalb des
|
von DrawerPage.Content bleibt der DataContext das MainWindowViewModel; innerhalb des
|
||||||
ContentPresenters würde eine fehlgeschlagene Bindung IsVisible auf true stehen lassen. -->
|
ContentPresenters würde eine fehlgeschlagene Bindung IsVisible auf true stehen lassen. -->
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Threading;
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
using LehrerApp.Sync;
|
using LehrerApp.Sync;
|
||||||
@@ -19,6 +20,62 @@ public partial class MainWindow : Window
|
|||||||
PointerMoved += (_, _) => NotifyActivity();
|
PointerMoved += (_, _) => NotifyActivity();
|
||||||
PointerPressed += (_, _) => NotifyActivity();
|
PointerPressed += (_, _) => NotifyActivity();
|
||||||
KeyDown += (_, _) => NotifyActivity();
|
KeyDown += (_, _) => NotifyActivity();
|
||||||
|
KeyDown += OnWindowKeyDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWindowKeyDown(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not MainWindowViewModel vm) return;
|
||||||
|
|
||||||
|
var commandModifier = (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Meta)) != 0;
|
||||||
|
if (commandModifier && e.Key == Key.K)
|
||||||
|
{
|
||||||
|
vm.OpenCommandPaletteCommand.Execute(null);
|
||||||
|
FocusCommandPalette();
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vm.IsCommandPaletteOpen) return;
|
||||||
|
if (e.Key == Key.Escape)
|
||||||
|
{
|
||||||
|
vm.CloseCommandPaletteCommand.Execute(null);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (e.Key == Key.Enter)
|
||||||
|
{
|
||||||
|
vm.CommandPalette.ExecuteCommand.Execute(vm.CommandPalette.SelectedResult);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (e.Key is Key.Down or Key.Up)
|
||||||
|
{
|
||||||
|
MoveCommandPaletteSelection(vm, e.Key == Key.Down ? 1 : -1);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOpenCommandPaletteClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is MainWindowViewModel vm)
|
||||||
|
vm.OpenCommandPaletteCommand.Execute(null);
|
||||||
|
FocusCommandPalette();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FocusCommandPalette() => Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (this.FindControl<TextBox>("CommandPaletteSearchBox") is { } search)
|
||||||
|
{
|
||||||
|
search.Focus();
|
||||||
|
search.SelectAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private static void MoveCommandPaletteSelection(MainWindowViewModel vm, int delta)
|
||||||
|
{
|
||||||
|
var results = vm.CommandPalette.Results;
|
||||||
|
if (results.Count == 0) return;
|
||||||
|
var current = vm.CommandPalette.SelectedResult is { } selected ? results.IndexOf(selected) : -1;
|
||||||
|
vm.CommandPalette.SelectedResult = results[Math.Clamp(current + delta, 0, results.Count - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void EnableFinalSync(SyncEngine syncEngine)
|
public void EnableFinalSync(SyncEngine syncEngine)
|
||||||
|
|||||||
@@ -2241,6 +2241,13 @@ erreichbar. Ergänzt: ein "SCHNELLÜBERBLICK"-Block oberhalb der Buttonliste
|
|||||||
obersten Karten des Kurs-Dashboards, hier nur ohne eigenen Tab-Wechsel. Sowie die beiden
|
obersten Karten des Kurs-Dashboards, hier nur ohne eigenen Tab-Wechsel. Sowie die beiden
|
||||||
fehlenden Buttons "📊 Übersicht" (Tab 0) und "✋ Mitarbeit" (Tab 3).
|
fehlenden Buttons "📊 Übersicht" (Tab 0) und "✋ Mitarbeit" (Tab 3).
|
||||||
|
|
||||||
|
**Spätere UX-Vereinfachung (August 2026):** Dieses Auswahl-Panel wurde wieder entfernt, nachdem
|
||||||
|
die Gruppenansicht selbst bereits denselben Schnellüberblick als Übersicht-Tab anbot und dadurch
|
||||||
|
zwei nahezu identische Bereichsnavigationen nacheinander entstanden. Die Gruppenliste zeigt nun
|
||||||
|
responsive Karten; ein Klick öffnet unmittelbar den Übersicht-Tab der Gruppe. Bearbeiten,
|
||||||
|
Schuljahresübernahme, Archivieren/Reaktivieren und Löschen bleiben über ein Drei-Punkte-Menü an
|
||||||
|
jeder Karte erreichbar. Suche, Schuljahresfilter und Archivansicht bleiben erhalten.
|
||||||
|
|
||||||
### 6.2 Zeiterfassung
|
### 6.2 Zeiterfassung
|
||||||
- [x] **6.2.1** Timer starten/stoppen mit Zuordnung zu Aufgabe oder Kategorie —
|
- [x] **6.2.1** Timer starten/stoppen mit Zuordnung zu Aufgabe oder Kategorie —
|
||||||
`TimeTrackingViewModel`. Bewusst ohne live mitlaufende Sekundenanzeige (keine
|
`TimeTrackingViewModel`. Bewusst ohne live mitlaufende Sekundenanzeige (keine
|
||||||
@@ -2417,6 +2424,12 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
|||||||
Gruppeneigenschaften können erst in einem getrennten Schritt nach der Reaktivierung geändert
|
Gruppeneigenschaften können erst in einem getrennten Schritt nach der Reaktivierung geändert
|
||||||
oder gelöscht werden. Reine Ansichten und das Kopieren einer Unterrichtseinheit als Vorlage
|
oder gelöscht werden. Reine Ansichten und das Kopieren einer Unterrichtseinheit als Vorlage
|
||||||
in eine aktive Gruppe bleiben möglich.
|
in eine aktive Gruppe bleiben möglich.
|
||||||
|
- [x] **7.2.6** Einlagige Lerngruppen-Navigation: Gruppen werden als responsive Karten dargestellt
|
||||||
|
und öffnen mit einem Klick direkt den Übersicht-Tab. Die zuvor vorgeschaltete Detailspalte
|
||||||
|
mit einer zweiten Liste aller Bereiche wurde entfernt; die Tabs im Gruppendetail sind damit
|
||||||
|
die einzige Bereichsnavigation. Das Verwaltungsmenü sitzt direkt an jeder Karte und ist per
|
||||||
|
Automation-Namen zugänglich. `GroupListViewModelTests` prüfen Direktnavigation, kontextuelle
|
||||||
|
Bearbeitung, Suche und Archivierung.
|
||||||
|
|
||||||
### 7.3 Import
|
### 7.3 Import
|
||||||
|
|
||||||
@@ -2528,7 +2541,7 @@ Hervorhebung "eigene Klasse" über `LearningGroup.IsOwnClass`, feste Kartenbreit
|
|||||||
Notenstufe bzw. drei Punkte); Versetzungsgefährdung basiert auf dem jüngsten gespeicherten
|
Notenstufe bzw. drei Punkte); Versetzungsgefährdung basiert auf dem jüngsten gespeicherten
|
||||||
Zeugnisnotenstand (Note 5/6 bzw. höchstens 4 Punkte). Klick öffnet den betroffenen Schüler.
|
Zeugnisnotenstand (Note 5/6 bzw. höchstens 4 Punkte). Klick öffnet den betroffenen Schüler.
|
||||||
- [x] **9.6** Dashboard-Kacheln ein-/ausblendbar und in der Reihenfolge konfigurierbar.
|
- [x] **9.6** Dashboard-Kacheln ein-/ausblendbar und in der Reihenfolge konfigurierbar.
|
||||||
„Dashboard anpassen“ bietet für jede Kachel Sichtbarkeit sowie Hoch-/Runter-Sortierung;
|
„Bereiche anpassen“ bietet für jede Kachel Sichtbarkeit sowie Hoch-/Runter-Sortierung;
|
||||||
die Konfiguration wird lokal in `dashboardsettings.json` gespeichert und das Raster ohne
|
die Konfiguration wird lokal in `dashboardsettings.json` gespeichert und das Raster ohne
|
||||||
Lücken neu angeordnet.
|
Lücken neu angeordnet.
|
||||||
- [x] **9.7** Automatische Aktualisierung beim Zurücknavigieren.
|
- [x] **9.7** Automatische Aktualisierung beim Zurücknavigieren.
|
||||||
@@ -2581,6 +2594,14 @@ gleiche Abfrage (Datum + Stundennummer, bewusst ohne Gruppenbezug, siehe `Substi
|
|||||||
PeriodNumber`-Doku) wie in `TimetableViewModel.BuildToday` für die "Heute"-Ansicht des
|
PeriodNumber`-Doku) wie in `TimetableViewModel.BuildToday` für die "Heute"-Ansicht des
|
||||||
Stundenplans.
|
Stundenplans.
|
||||||
|
|
||||||
|
**Nachtrag Dashboard-Fokus (August 2026):** Oberhalb der konfigurierbaren Karten fasst ein neuer
|
||||||
|
Tagesfokus Unterricht, offene Aufgaben, Handlungsbedarf und Termine der nächsten 30 Tage in vier
|
||||||
|
Kennzahlen zusammen. Wetterdetails sind standardmäßig eingeklappt (amtliche Warnungen öffnen den
|
||||||
|
Bereich automatisch), die linke Inhaltsspalte erhält mehr Breite und leere reine Hinweis-Karten
|
||||||
|
(u.a. Fehlzeiten-Warnung, Förderplan-Wiedervorlage, Korrekturen und Auffälligkeiten) werden trotz
|
||||||
|
aktivierter Dashboard-Konfiguration automatisch ausgeblendet. Inhalte und Direktaktionen bleiben
|
||||||
|
unverändert erhalten; die Seite wird bei ruhiger Datenlage lediglich deutlich kürzer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Sync & Server
|
## 10. Sync & Server
|
||||||
@@ -3439,7 +3460,16 @@ von 4.2.2) der Tab "Kürzel-Katalog" für die Von/Nach-Kürzel des Stundenverlau
|
|||||||
|
|
||||||
- [ ] **14.1** Tastaturbedienung durchgängig: alle Hauptfunktionen ohne Maus erreichbar
|
- [ ] **14.1** Tastaturbedienung durchgängig: alle Hauptfunktionen ohne Maus erreichbar
|
||||||
(Vorbild: Mitarbeit-Schnelleingabe).
|
(Vorbild: Mitarbeit-Schnelleingabe).
|
||||||
- [ ] **14.2** Globale Suche (Schüler, Gruppe, Klausur) über Tastenkürzel.
|
- [x] **14.2** Globale Suche (Schüler, Gruppe, Klausur) über Tastenkürzel.
|
||||||
|
**Umsetzung:** `Strg+K` (Windows/Linux) bzw. `⌘K` (macOS) öffnet aus jeder Hauptansicht eine
|
||||||
|
modale Befehlspalette. `GlobalSearchViewModel` durchsucht lokal und ohne zusätzlichen Index
|
||||||
|
aktive wie inaktive Schüler/Lerngruppen sowie Klausuren und Aufgaben; Treffer springen direkt
|
||||||
|
ins Schülerdetail, Gruppendetail, den Klausuren-Tab oder die Aufgabenverwaltung. Pfeiltasten,
|
||||||
|
Enter und Escape bedienen die Palette vollständig ohne Maus. Bei leerer Suche stehen die
|
||||||
|
Schnellaktionen „Aufgabe anlegen“, „Erinnerung anlegen“ und „Schüler anlegen“ bereit und
|
||||||
|
verwenden die bereits vorhandenen Dialoge samt Validierung. Zusätzlich ist der Einstieg als
|
||||||
|
zugänglich benannte Schaltfläche im Navigationsbereich sichtbar. Tests in
|
||||||
|
`GlobalSearchViewModelTests` decken Ergebnisarten, Navigation und Schnellerfassung ab.
|
||||||
- [~] **14.3** Rückgängig-Funktion für Löschvorgänge (mindestens Bestätigungsdialog überall).
|
- [~] **14.3** Rückgängig-Funktion für Löschvorgänge (mindestens Bestätigungsdialog überall).
|
||||||
|
|
||||||
**Umsetzung:** generischer, nicht-invasiver "Papierkorb light" statt einer
|
**Umsetzung:** generischer, nicht-invasiver "Papierkorb light" statt einer
|
||||||
|
|||||||
Reference in New Issue
Block a user