2 Commits
Author SHA1 Message Date
admin 89a6376fb6 Improve desktop UX and accessibility 2026-08-29 20:56:55 +02:00
admin b7a105af73 Dashboard und Schnellnavigation fix 2026-08-29 19:35:05 +02:00
32 changed files with 606 additions and 99 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ public interface IHasAttachments
List<DocumentAttachment> Attachments { get; } List<DocumentAttachment> Attachments { get; }
} }
// Hinten angefügt, damit die numerischen Werte bereits gespeicherter LiteDB-Einträge stabil bleiben. // Hinten angefügt, damit die numerischen Werte bereits gespeicherter LiteDB-Einträge stabil bleiben.
public enum DocumentationType { Conversation, Incident, SupportPlan, Absence, ParentCall, ParentLetter } public enum DocumentationType { Conversation, Incident, SupportPlan, Absence, ParentCall, ParentLetter, Planning }
public enum SupportStatus { Active, Completed, Paused } public enum SupportStatus { Active, Completed, Paused }
public class WorkTask public class WorkTask
@@ -59,12 +59,12 @@ public sealed class DashboardViewModelTests
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null, DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null, FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
FakeSessions? sessions = null, FakeEntries? entries = null, FakeSessions? sessions = null, FakeEntries? entries = null,
FakeAnnualPlanEvents? annualPlanEvents = null) FakeAnnualPlanEvents? annualPlanEvents = null, List<LearningGroup>? allGroups = null)
{ {
lessons ??= new FakeLessons(); lessons ??= new FakeLessons();
lessons.Add(lesson); lessons.Add(lesson);
return new DashboardViewModel( return new DashboardViewModel(
new FakeGroups([group]), new FakeSubjects([]), lessons, new FakeGroups(allGroups ?? [group]), new FakeSubjects([]), lessons,
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(), exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]), reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
tasks ?? new FakeWorkTasks(), sessions ?? new FakeSessions([]), entries ?? new FakeEntries(), tasks ?? new FakeWorkTasks(), sessions ?? new FakeSessions([]), entries ?? new FakeEntries(),
@@ -242,6 +242,26 @@ public sealed class DashboardViewModelTests
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test"); Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test");
} }
[Fact]
public void Kalenderauswahl_SortiertKurseDesTagesVorDenAlphabetischenRest()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var selectedDate = today.AddDays(1);
var alphabeticallyFirst = new LearningGroup { Name = "10a" };
var selectedDayGroup = new LearningGroup { Name = "WAT 10c" };
var lessons = new FakeLessons();
lessons.Add(new Lesson { GroupId = selectedDayGroup.Id, Date = selectedDate });
var vm = BuildVm(alphabeticallyFirst,
new Lesson { GroupId = alphabeticallyFirst.Id, Date = today }, lessons: lessons,
allGroups: [alphabeticallyFirst, selectedDayGroup]);
vm.SelectCalendarDayCommand.Execute(vm.CalendarDays.Single(d => d.Date == selectedDate));
Assert.Equal(selectedDayGroup.Id, vm.CurrentGroups[0].GroupId);
Assert.True(vm.CurrentGroups[0].IsOnSelectedDay);
Assert.Equal(alphabeticallyFirst.Id, vm.CurrentGroups[1].GroupId);
}
[Fact] [Fact]
public void Kalender_ZeigtMitarbeitssitzungenAlsEigenenTermintyp() public void Kalender_ZeigtMitarbeitssitzungenAlsEigenenTermintyp()
{ {
@@ -247,6 +247,25 @@ public sealed class DocumentationDialogViewModelTests
Assert.Equal(groupId, vm.Result.GroupId); Assert.Equal(groupId, vm.Result.GroupId);
} }
[Fact]
public void GesamteLerngruppe_SaveSpeichertOhneSchuelerbezug()
{
var groupId = Guid.NewGuid();
var wholeGroup = new StudentOption(Guid.Empty, "Gesamte Lerngruppe");
var vm = new DocumentationDialogViewModel(Guid.Empty, null, new FakeAttachmentStorage(),
[wholeGroup], groupId)
{
SelectedStudent = wholeGroup, Title = "Klausur planen", TypeName = "Planung / Erinnerung",
};
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Equal(Guid.Empty, vm.Result!.StudentId);
Assert.Equal(groupId, vm.Result.GroupId);
Assert.Equal(DocumentationType.Planning, vm.Result.Type);
}
[Fact] [Fact]
public void MitStudentOptions_Bearbeiten_SchuelerIstVorausgewaehlt() public void MitStudentOptions_Bearbeiten_SchuelerIstVorausgewaehlt()
{ {
@@ -29,6 +29,20 @@ public sealed class ExamsOverviewViewModelTests
Assert.Equal("Neue Klausur", vm.Rows[1].Title); Assert.Equal("Neue Klausur", vm.Rows[1].Title);
} }
[Fact]
public void LeereListe_BietetAbsprungZuLerngruppen()
{
var vm = BuildVm([], []);
var navigated = false;
vm.OnNavigateToGroups = () => navigated = true;
vm.LoadCommand.Execute(null);
vm.GoToGroupsCommand.Execute(null);
Assert.True(vm.HasNoExams);
Assert.True(navigated);
}
[Fact] [Fact]
public void Load_ParallelkurseWerdenAlsGeschwisterErkannt() public void Load_ParallelkurseWerdenAlsGeschwisterErkannt()
{ {
@@ -14,7 +14,8 @@ public sealed class GlobalSearchViewModelTests
Assert.Collection(vm.Results, Assert.Collection(vm.Results,
item => Assert.Equal(GlobalSearchAction.NewTask, item.Action), item => Assert.Equal(GlobalSearchAction.NewTask, item.Action),
item => Assert.Equal(GlobalSearchAction.NewReminder, item.Action), item => Assert.Equal(GlobalSearchAction.NewReminder, item.Action),
item => Assert.Equal(GlobalSearchAction.NewStudent, item.Action)); item => Assert.Equal(GlobalSearchAction.NewStudent, item.Action),
item => Assert.Equal(GlobalSearchAction.NewGroupDocumentation, item.Action));
Assert.Same(vm.Results[0], vm.SelectedResult); Assert.Same(vm.Results[0], vm.SelectedResult);
} }
@@ -67,8 +68,36 @@ public sealed class GlobalSearchViewModelTests
Assert.True(reminder); Assert.True(reminder);
} }
[Fact]
public void Suche_FindetAktiveGruppeUeberFachUndZeigtFachImUntertitel()
{
var subject = new Subject { Name = "Wirtschaft-Arbeit-Technik", ShortName = "WAT" };
var group = new LearningGroup { Name = "10c", SubjectId = subject.Id, SchoolYear = "2026/27", GradeLevel = 10 };
var vm = BuildVm(groups: [group], subjects: [subject]);
vm.Query = "wat";
var result = Assert.Single(vm.Results, x => x.Kind == GlobalSearchResultKind.Group);
Assert.Contains("WAT", result.Subtitle);
}
[Fact]
public void Suche_UeberspringtArchivierteSchuelerUndGruppenSamtDerenKlausuren()
{
var archivedGroup = new LearningGroup { Name = "Testkurs Vorjahr", IsActive = false };
var archivedStudent = new Student { FirstName = "Test", LastName = "Archiv", IsActive = false };
var exam = new Exam { GroupId = archivedGroup.Id, Title = "Testklausur" };
var vm = BuildVm([archivedStudent], [archivedGroup], [exam]);
vm.Query = "Test";
Assert.DoesNotContain(vm.Results, x => x.Kind is GlobalSearchResultKind.Student
or GlobalSearchResultKind.Group or GlobalSearchResultKind.Exam);
}
private static GlobalSearchViewModel BuildVm(List<Student>? students = null, private static GlobalSearchViewModel BuildVm(List<Student>? students = null,
List<LearningGroup>? groups = null, List<Exam>? exams = null, FakeWorkTasks? tasks = null) => List<LearningGroup>? groups = null, List<Exam>? exams = null, FakeWorkTasks? tasks = null,
List<Subject>? subjects = null) =>
new(new FakeStudents(students ?? []), new FakeGroups(groups ?? []), new(new FakeStudents(students ?? []), new FakeGroups(groups ?? []),
new FakeExams(exams ?? []), tasks ?? new FakeWorkTasks()); new FakeSubjects(subjects ?? []), new FakeExams(exams ?? []), tasks ?? new FakeWorkTasks());
} }
@@ -0,0 +1,26 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Students;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class GroupDocumentationQuickViewModelTests
{
[Fact]
public void Save_ErstelltPlanungFuerGesamteLerngruppe()
{
var group = new GroupDocumentationOption(Guid.NewGuid(), "10c · WAT");
var vm = new GroupDocumentationQuickViewModel([group])
{
SelectedGroup = group, Title = "Klausur ankündigen", Content = "Termin abstimmen",
};
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Equal(Guid.Empty, vm.Result!.StudentId);
Assert.Equal(group.Id, vm.Result.GroupId);
Assert.Equal(DocumentationType.Planning, vm.Result.Type);
Assert.True(vm.Result.ExcludeFromWebUntisSync);
}
}
@@ -36,6 +36,25 @@ public sealed class GroupDocumentationTabViewModelTests
Assert.Equal("Alt", vm.Entries[1].Model.Title); Assert.Equal("Alt", vm.Entries[1].Model.Title);
} }
[Fact]
public void Initialize_ZeigtGruppenweitenEintragAuchOhneSchueler()
{
var group = new LearningGroup { Name = "10c" };
var docs = new FakeDocumentation();
docs.Add(new Documentation
{
StudentId = Guid.Empty, GroupId = group.Id, Type = DocumentationType.Planning,
Title = "Klausur planen", Date = new DateOnly(2026, 9, 1),
});
var vm = BuildVm([], [group], docs);
vm.Initialize(group.Id);
var entry = Assert.Single(vm.Entries);
Assert.Equal("Gesamte Lerngruppe", entry.StudentName);
Assert.Equal("Planung / Erinnerung", entry.TypeLabel);
}
[Fact] [Fact]
public void Initialize_EintragAusAndererGruppe_WirdMitangezeigtAberAlsFremdMarkiert() public void Initialize_EintragAusAndererGruppe_WirdMitangezeigtAberAlsFremdMarkiert()
{ {
@@ -51,6 +51,18 @@ public class StudentListViewModelTests
Assert.Equal("M", vm.Students.Single(s => s.Id == _ben.Id).AvatarLabel); Assert.Equal("M", vm.Students.Single(s => s.Id == _ben.Id).AvatarLabel);
} }
[Fact]
public void SucheOhneTreffer_LiefertHilfreichenLeerzustand()
{
var vm = BuildViewModel();
vm.SearchText = "nicht vorhanden";
Assert.True(vm.HasNoStudents);
Assert.False(vm.HasStudents);
Assert.Contains("Suche", vm.EmptyListMessage);
}
private StudentListViewModel BuildViewModel() => new( private StudentListViewModel BuildViewModel() => new(
new FakeStudents([_anna, _ben]), new FakeStudents([_anna, _ben]),
new FakeGroups([_group]), new FakeGroups([_group]),
+23
View File
@@ -7,6 +7,17 @@
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://LehrerApp.Desktop/Styles/SemanticBrushes.axaml"/> <ResourceInclude Source="avares://LehrerApp.Desktop/Styles/SemanticBrushes.axaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<!-- Plattformunabhängige Vektoricons für die Hauptnavigation (14.10). -->
<StreamGeometry x:Key="IconSearch">M10,2 A8,8 0 1 0 10,18 A8,8 0 1 0 10,2 M10,5 A5,5 0 1 1 10,15 A5,5 0 1 1 10,5 M15,14 L22,21 L20,23 L13,16 Z</StreamGeometry>
<StreamGeometry x:Key="IconDashboard">M3,3 H10 V10 H3 Z M14,3 H21 V7 H14 Z M14,11 H21 V21 H14 Z M3,14 H10 V21 H3 Z</StreamGeometry>
<StreamGeometry x:Key="IconGroups">M3,8 L12,3 L21,8 V10 H3 Z M5,11 H8 V18 H5 Z M10,11 H14 V18 H10 Z M16,11 H19 V18 H16 Z M3,20 H21 V22 H3 Z</StreamGeometry>
<StreamGeometry x:Key="IconStudents">M12,3 A4,4 0 1 0 12,11 A4,4 0 1 0 12,3 M4,21 C4,16 7,13 12,13 C17,13 20,16 20,21 Z</StreamGeometry>
<StreamGeometry x:Key="IconExams">M5,2 H15 L20,7 V22 H5 Z M7,5 V19 H18 V9 H13 V4 H7 Z M9,11 H16 V13 H9 Z M9,15 H16 V17 H9 Z</StreamGeometry>
<StreamGeometry x:Key="IconCalendar">M3,4 H21 V22 H3 Z M5,10 V20 H19 V10 Z M7,2 H9 V7 H7 Z M15,2 H17 V7 H15 Z M7,12 H10 V15 H7 Z M12,12 H15 V15 H12 Z M7,17 H10 V19 H7 Z M12,17 H15 V19 H12 Z</StreamGeometry>
<StreamGeometry x:Key="IconClock">M12,2 A10,10 0 1 0 12,22 A10,10 0 1 0 12,2 M12,5 A7,7 0 1 1 12,19 A7,7 0 1 1 12,5 M11,7 H13 V12 L17,14 L16,16 L11,13 Z</StreamGeometry>
<StreamGeometry x:Key="IconClassTeacher">M2,8 L12,3 L22,8 L12,13 Z M6,11 V16 C9,19 15,19 18,16 V11 M22,8 V15</StreamGeometry>
<StreamGeometry x:Key="IconSettings">M3,5 H21 V7 H3 Z M8,3 A3,3 0 1 0 8,9 A3,3 0 1 0 8,3 M3,11 H21 V13 H3 Z M16,9 A3,3 0 1 0 16,15 A3,3 0 1 0 16,9 M3,17 H21 V19 H3 Z M10,15 A3,3 0 1 0 10,21 A3,3 0 1 0 10,15</StreamGeometry>
<StreamGeometry x:Key="IconRefresh">M12,3 A9,9 0 0 1 21,12 H18 A6,6 0 0 0 8,7 L11,10 H3 V2 L6,5 A9,9 0 0 1 12,3 M12,21 A9,9 0 0 1 3,12 H6 A6,6 0 0 0 16,17 L13,14 H21 V22 L18,19 A9,9 0 0 1 12,21</StreamGeometry>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
@@ -24,5 +35,17 @@
<Setter Property="Opacity" Value="0.4"/> <Setter Property="Opacity" Value="0.4"/>
<Setter Property="FontSize" Value="13"/> <Setter Property="FontSize" Value="13"/>
</Style> </Style>
<Style Selector="Button.touchTarget">
<Setter Property="MinWidth" Value="40"/>
<Setter Property="MinHeight" Value="40"/>
</Style>
<Style Selector="Button:focus-visible">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="BorderThickness" Value="2"/>
</Style>
<Style Selector="TextBox:focus-visible, ComboBox:focus-visible">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="BorderThickness" Value="2"/>
</Style>
</Application.Styles> </Application.Styles>
</Application> </Application>
+26
View File
@@ -154,6 +154,9 @@ 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"
var examsOverview = Services.GetRequiredService<ViewModels.Exams.ExamsOverviewViewModel>();
examsOverview.OnNavigateToGroups = () => main.NavigateToCommand.Execute(NavItem.Groups);
// Globale Suche/Schnellerfassung (14.2): Navigation bleibt im MainWindow-VM, die // Globale Suche/Schnellerfassung (14.2): Navigation bleibt im MainWindow-VM, die
// vorhandenen Dialog-Helfer übernehmen Eingabe und Validierung. // vorhandenen Dialog-Helfer übernehmen Eingabe und Validierung.
var search = Services.GetRequiredService<GlobalSearchViewModel>(); var search = Services.GetRequiredService<GlobalSearchViewModel>();
@@ -170,6 +173,7 @@ public class App : Application
}; };
search.OnQuickAddTask = startAsReminder => ShowQuickTaskDialog(startAsReminder, dash); search.OnQuickAddTask = startAsReminder => ShowQuickTaskDialog(startAsReminder, dash);
search.OnQuickAddStudent = ShowAddStudentDialog; search.OnQuickAddStudent = ShowAddStudentDialog;
search.OnQuickAddGroupDocumentation = () => ShowQuickGroupDocumentationDialog(dash);
// StudentList → StudentDetail + Anlegen // StudentList → StudentDetail + Anlegen
var sl = Services.GetRequiredService<StudentListViewModel>(); var sl = Services.GetRequiredService<StudentListViewModel>();
@@ -203,4 +207,26 @@ public class App : Application
dashboard.RefreshCommand.Execute(null); dashboard.RefreshCommand.Execute(null);
Services.GetRequiredService<WorkTaskListViewModel>().Load(); Services.GetRequiredService<WorkTaskListViewModel>().Load();
} }
private static async Task ShowQuickGroupDocumentationDialog(DashboardViewModel dashboard)
{
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime
{ MainWindow: { } owner }) return;
var subjects = Services.GetRequiredService<ISubjectRepository>().GetAll()
.ToDictionary(s => s.Id);
var options = Services.GetRequiredService<IGroupRepository>().GetAll()
.Where(g => g.IsActive)
.OrderBy(g => g.Name)
.Select(g => new GroupDocumentationOption(g.Id,
g.SubjectId is { } subjectId && subjects.TryGetValue(subjectId, out var subject)
? $"{g.Name} · {(string.IsNullOrWhiteSpace(subject.ShortName) ? subject.Name : subject.ShortName)}"
: g.Name));
var vm = new GroupDocumentationQuickViewModel(options);
var dialog = new Views.Students.GroupDocumentationQuickDialog { DataContext = vm };
if (!await dialog.ShowDialog<bool>(owner) || vm.Result is null) return;
Services.GetRequiredService<IDocumentationRepository>().Save(vm.Result);
dashboard.RefreshCommand.Execute(null);
}
} }
@@ -56,6 +56,7 @@ public partial class DashboardViewModel : ObservableObject
[ObservableProperty] private string _currentSchoolYear = ""; [ObservableProperty] private string _currentSchoolYear = "";
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today); [ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
[ObservableProperty] private string _selectedDayLabel = ""; [ObservableProperty] private string _selectedDayLabel = "";
[ObservableProperty] private DateOnly _selectedCalendarDate = DateOnly.FromDateTime(DateTime.Today);
[ObservableProperty] private bool _isDashboardSettingsOpen; [ObservableProperty] private bool _isDashboardSettingsOpen;
[ObservableProperty] private bool _isWeatherPanelVisible; [ObservableProperty] private bool _isWeatherPanelVisible;
[ObservableProperty] private string _weatherSummary = ""; [ObservableProperty] private string _weatherSummary = "";
@@ -196,7 +197,7 @@ public partial class DashboardViewModel : ObservableObject
IsHighPriority = t.Priority == TaskPriority.High }); IsHighPriority = t.Priority == TaskPriority.High });
CurrentGroups.Clear(); CurrentGroups.Clear();
foreach (var g in groups.Values.OrderBy(g => g.Name)) foreach (var g in groups.Values)
CurrentGroups.Add(new() CurrentGroups.Add(new()
{ {
GroupId = g.Id, GroupId = g.Id,
@@ -749,9 +750,35 @@ public partial class DashboardViewModel : ObservableObject
{ {
if (day is null) return; if (day is null) return;
foreach (var cell in CalendarDays) cell.IsSelected = cell == day; foreach (var cell in CalendarDays) cell.IsSelected = cell == day;
SelectedCalendarDate = day.Date;
SelectedDayLabel = day.Date.ToString("dddd, d. MMMM", De); SelectedDayLabel = day.Date.ToString("dddd, d. MMMM", De);
SelectedDayEvents.Clear(); SelectedDayEvents.Clear();
foreach (var item in day.Events) SelectedDayEvents.Add(item); foreach (var item in day.Events) SelectedDayEvents.Add(item);
SortCurrentGroups(day.Date);
}
private void SortCurrentGroups(DateOnly date)
{
var schoolHolidays = _schoolHolidays.GetAll();
var publicHolidays = _publicHolidays.GetHolidays(date.Year, _calendarSettings.State)
.Select(h => h.Date).ToHashSet();
var isFreeDay = IsFreeDay(date, schoolHolidays, publicHolidays);
var cancelledPeriods = _substitutions.GetByDate(date)
.Where(s => s.Kind == SubstitutionKind.Cancelled)
.Select(s => s.PeriodNumber).ToHashSet();
foreach (var chip in CurrentGroups)
{
var hasLesson = _lessons.GetByGroupAndDate(chip.GroupId, date).Count > 0;
var hasActiveSlot = !isFreeDay && _timetableSlots.GetByGroup(chip.GroupId)
.Any(s => s.Weekday == date.DayOfWeek && !cancelledPeriods.Contains(s.PeriodNumber));
chip.IsOnSelectedDay = hasLesson || hasActiveSlot;
}
var sorted = CurrentGroups.OrderByDescending(g => g.IsOnSelectedDay)
.ThenBy(g => g.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
CurrentGroups.Clear();
foreach (var chip in sorted) CurrentGroups.Add(chip);
} }
[RelayCommand] [RelayCommand]
@@ -891,7 +918,13 @@ public class LessonItem
public bool HasRoom => !string.IsNullOrWhiteSpace(Room); public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
} }
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } public bool IsHighPriority { get; set; } } public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } public bool IsHighPriority { get; set; } }
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; } public class GroupChip
{
public Guid GroupId { get; set; }
public string Name { get; set; } = "";
public string Subject { get; set; } = "";
public bool IsOnSelectedDay { get; set; }
}
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ──────────────────────── // ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
@@ -43,6 +43,8 @@ public partial class ExamsOverviewViewModel : ObservableObject
public Func<Exam, Task>? OnGradeExam { get; set; } public Func<Exam, Task>? OnGradeExam { get; set; }
public Func<Exam, Task>? OnEvaluateExam { get; set; } public Func<Exam, Task>? OnEvaluateExam { get; set; }
public Action<Guid>? OnNavigateToGroup { get; set; } public Action<Guid>? OnNavigateToGroup { get; set; }
public Action? OnNavigateToGroups { get; set; }
public bool HasNoExams => Rows.Count == 0;
public ExamsOverviewViewModel(IExamRepository exams, IExamResultRepository examResults, public ExamsOverviewViewModel(IExamRepository exams, IExamResultRepository examResults,
IGroupRepository groups, IGroupMembershipRepository memberships, GradingService grading, IGroupRepository groups, IGroupMembershipRepository memberships, GradingService grading,
@@ -85,6 +87,7 @@ public partial class ExamsOverviewViewModel : ObservableObject
Rows.Add(row); Rows.Add(row);
EmptyHint = Rows.Count == 0 ? "Keine Klausuren angelegt." : ""; EmptyHint = Rows.Count == 0 ? "Keine Klausuren angelegt." : "";
OnPropertyChanged(nameof(HasNoExams));
SelectedRow = selectedId is { } id SelectedRow = selectedId is { } id
? Rows.FirstOrDefault(r => r.Exam.Id == id) ?? Rows.FirstOrDefault() ? Rows.FirstOrDefault(r => r.Exam.Id == id) ?? Rows.FirstOrDefault()
: Rows.FirstOrDefault(); : Rows.FirstOrDefault();
@@ -173,6 +176,9 @@ public partial class ExamsOverviewViewModel : ObservableObject
OnNavigateToGroup?.Invoke(SelectedRow.GroupId); OnNavigateToGroup?.Invoke(SelectedRow.GroupId);
} }
[RelayCommand]
private void GoToGroups() => OnNavigateToGroups?.Invoke();
[RelayCommand] [RelayCommand]
private void ToggleApproval() => ToggleDate(SelectedRow?.Exam, e => e.ApprovalGrantedAt, private void ToggleApproval() => ToggleDate(SelectedRow?.Exam, e => e.ApprovalGrantedAt,
(e, v) => e.ApprovalGrantedAt = v); (e, v) => e.ApprovalGrantedAt = v);
@@ -17,6 +17,7 @@ public partial class GlobalSearchViewModel : ObservableObject
private readonly IStudentRepository _students; private readonly IStudentRepository _students;
private readonly IGroupRepository _groups; private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private readonly IExamRepository _exams; private readonly IExamRepository _exams;
private readonly IWorkTaskRepository _tasks; private readonly IWorkTaskRepository _tasks;
@@ -30,13 +31,15 @@ public partial class GlobalSearchViewModel : ObservableObject
public Action<GlobalSearchResult>? OnNavigate { get; set; } public Action<GlobalSearchResult>? OnNavigate { get; set; }
public Func<bool, Task>? OnQuickAddTask { get; set; } public Func<bool, Task>? OnQuickAddTask { get; set; }
public Func<Task>? OnQuickAddStudent { get; set; } public Func<Task>? OnQuickAddStudent { get; set; }
public Func<Task>? OnQuickAddGroupDocumentation { get; set; }
public Action? OnClose { get; set; } public Action? OnClose { get; set; }
public GlobalSearchViewModel(IStudentRepository students, IGroupRepository groups, public GlobalSearchViewModel(IStudentRepository students, IGroupRepository groups,
IExamRepository exams, IWorkTaskRepository tasks) ISubjectRepository subjects, IExamRepository exams, IWorkTaskRepository tasks)
{ {
_students = students; _students = students;
_groups = groups; _groups = groups;
_subjects = subjects;
_exams = exams; _exams = exams;
_tasks = tasks; _tasks = tasks;
RefreshResults(); RefreshResults();
@@ -59,23 +62,34 @@ public partial class GlobalSearchViewModel : ObservableObject
if (query.Length > 0) if (query.Length > 0)
{ {
var groups = _groups.GetAll(includeInactive: true); var groups = _groups.GetAll().Where(g => g.IsActive).ToList();
var groupNames = groups.ToDictionary(g => g.Id, g => g.Name); var groupNames = groups.ToDictionary(g => g.Id, g => g.Name);
var subjects = _subjects.GetAll().ToDictionary(s => s.Id);
Subject? GroupSubject(LearningGroup group) => group.SubjectId is { } subjectId
? subjects.GetValueOrDefault(subjectId)
: null;
static string SubjectLabel(Subject? subject) => subject is null ? ""
: string.IsNullOrWhiteSpace(subject.ShortName)
|| subject.Name.Equals(subject.ShortName, StringComparison.CurrentCultureIgnoreCase)
? subject.Name
: $"{subject.ShortName} {subject.Name}";
var candidates = new List<GlobalSearchResult>(); var candidates = new List<GlobalSearchResult>();
candidates.AddRange(_students.GetAll(includeInactive: true) candidates.AddRange(_students.GetAll().Where(s => s.IsActive)
.Where(s => Matches(s.FullName, query)) .Where(s => Matches(s.FullName, query))
.Select(s => GlobalSearchResult.ForStudent(s))); .Select(s => GlobalSearchResult.ForStudent(s)));
candidates.AddRange(groups candidates.AddRange(groups
.Where(g => Matches($"{g.Name} {g.SchoolYear} {g.GradeLevel}", query)) .Where(g => Matches($"{g.Name} {GroupSubject(g)?.Name} {GroupSubject(g)?.ShortName} {g.SchoolYear} {g.GradeLevel}", query))
.Select(GlobalSearchResult.ForGroup)); .Select(g => GlobalSearchResult.ForGroup(g, SubjectLabel(GroupSubject(g)))));
candidates.AddRange(_exams.GetAll() candidates.AddRange(_exams.GetAll()
.Where(e => groupNames.ContainsKey(e.GroupId))
.Where(e => Matches($"{e.Title} {groupNames.GetValueOrDefault(e.GroupId)}", query)) .Where(e => Matches($"{e.Title} {groupNames.GetValueOrDefault(e.GroupId)}", query))
.Select(e => GlobalSearchResult.ForExam(e, groupNames.GetValueOrDefault(e.GroupId) ?? ""))); .Select(e => GlobalSearchResult.ForExam(e, groupNames.GetValueOrDefault(e.GroupId) ?? "")));
candidates.AddRange(_tasks.GetAll() candidates.AddRange(_tasks.GetAll()
.Where(t => t.GroupId is null || groupNames.ContainsKey(t.GroupId.Value))
.Where(t => Matches($"{t.Title} {t.Notes} {groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty)}", query)) .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) ?? ""))); .Select(t => GlobalSearchResult.ForTask(t, groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty) ?? "")));
@@ -99,6 +113,7 @@ public partial class GlobalSearchViewModel : ObservableObject
GlobalSearchResult.ForAction(GlobalSearchAction.NewTask, "Aufgabe anlegen", "Mit Fälligkeit, Gruppe und Priorität", ""), 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.NewReminder, "Erinnerung anlegen", "Kurze Notiz ohne Zeiterfassung", "◷"),
GlobalSearchResult.ForAction(GlobalSearchAction.NewStudent, "Schüler anlegen", "Neue Stammdaten erfassen", ""), GlobalSearchResult.ForAction(GlobalSearchAction.NewStudent, "Schüler anlegen", "Neue Stammdaten erfassen", ""),
GlobalSearchResult.ForAction(GlobalSearchAction.NewGroupDocumentation, "Lerngruppen-Eintrag", "Planung, Klausur oder Erinnerung dokumentieren", "N"),
}; };
foreach (var action in actions.Where(a => query.Length == 0 || Matches(a.Title, query))) foreach (var action in actions.Where(a => query.Length == 0 || Matches(a.Title, query)))
@@ -124,6 +139,9 @@ public partial class GlobalSearchViewModel : ObservableObject
case GlobalSearchAction.NewStudent: case GlobalSearchAction.NewStudent:
if (OnQuickAddStudent is not null) await OnQuickAddStudent(); if (OnQuickAddStudent is not null) await OnQuickAddStudent();
break; break;
case GlobalSearchAction.NewGroupDocumentation:
if (OnQuickAddGroupDocumentation is not null) await OnQuickAddGroupDocumentation();
break;
default: default:
OnNavigate?.Invoke(result); OnNavigate?.Invoke(result);
break; break;
@@ -134,7 +152,7 @@ public partial class GlobalSearchViewModel : ObservableObject
} }
public enum GlobalSearchResultKind { Action, Student, Group, Exam, Task } public enum GlobalSearchResultKind { Action, Student, Group, Exam, Task }
public enum GlobalSearchAction { None, NewTask, NewReminder, NewStudent } public enum GlobalSearchAction { None, NewTask, NewReminder, NewStudent, NewGroupDocumentation }
public sealed class GlobalSearchResult public sealed class GlobalSearchResult
{ {
@@ -171,10 +189,13 @@ public sealed class GlobalSearchResult
Title = student.FullName, Subtitle = student.IsActive ? "Aktiv" : "Inaktiv", Icon = "P", Title = student.FullName, Subtitle = student.IsActive ? "Aktiv" : "Inaktiv", Icon = "P",
}; };
public static GlobalSearchResult ForGroup(LearningGroup group) => new() public static GlobalSearchResult ForGroup(LearningGroup group, string subjectName) => new()
{ {
Kind = GlobalSearchResultKind.Group, EntityId = group.Id, GroupId = group.Id, Kind = GlobalSearchResultKind.Group, EntityId = group.Id, GroupId = group.Id,
Title = group.Name, Subtitle = $"{group.SchoolYear} · Stufe {group.GradeLevel}", Icon = "G", Title = group.Name,
Subtitle = string.Join(" · ", new[] { subjectName, group.SchoolYear, $"Stufe {group.GradeLevel}" }
.Where(x => !string.IsNullOrWhiteSpace(x))),
Icon = "G",
}; };
public static GlobalSearchResult ForExam(Exam exam, string groupName) => new() public static GlobalSearchResult ForExam(Exam exam, string groupName) => new()
@@ -25,6 +25,7 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
private List<StudentOption> _groupStudents = []; private List<StudentOption> _groupStudents = [];
public static readonly StudentOption AllStudentsOption = new(Guid.Empty, "Alle Schüler"); public static readonly StudentOption AllStudentsOption = new(Guid.Empty, "Alle Schüler");
public static readonly StudentOption WholeGroupOption = new(Guid.Empty, "Gesamte Lerngruppe");
public ObservableCollection<DocumentationItem> Entries { get; } = []; public ObservableCollection<DocumentationItem> Entries { get; } = [];
public ObservableCollection<StudentOption> StudentFilterOptions { get; } = [AllStudentsOption]; public ObservableCollection<StudentOption> StudentFilterOptions { get; } = [AllStudentsOption];
@@ -62,8 +63,6 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
private void Load() private void Load()
{ {
Entries.Clear(); Entries.Clear();
if (_groupStudents.Count == 0) return;
var studentNameById = _groupStudents.ToDictionary(s => s.Id, s => s.Name); var studentNameById = _groupStudents.ToDictionary(s => s.Id, s => s.Name);
var groupNameCache = new Dictionary<Guid, string>(); var groupNameCache = new Dictionary<Guid, string>();
string GroupLabel(Guid id) string GroupLabel(Guid id)
@@ -80,6 +79,10 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
var all = relevantStudentIds var all = relevantStudentIds
.SelectMany(id => _docs.GetByStudent(id)) .SelectMany(id => _docs.GetByStudent(id))
.Concat(SelectedStudentFilter.Id == Guid.Empty
? _docs.GetAll().Where(d => d.StudentId == Guid.Empty && d.GroupId == _groupId)
: [])
.DistinctBy(d => d.Id)
.Where(d => !OnlyThisGroup || d.GroupId == _groupId) .Where(d => !OnlyThisGroup || d.GroupId == _groupId)
.OrderByDescending(d => d.IsDraft) .OrderByDescending(d => d.IsDraft)
.ThenByDescending(d => d.Date) .ThenByDescending(d => d.Date)
@@ -90,7 +93,10 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
{ {
var isOwnGroup = d.GroupId is null || d.GroupId == _groupId; var isOwnGroup = d.GroupId is null || d.GroupId == _groupId;
var otherGroupLabel = isOwnGroup ? "" : GroupLabel(d.GroupId!.Value); var otherGroupLabel = isOwnGroup ? "" : GroupLabel(d.GroupId!.Value);
Entries.Add(new DocumentationItem(d, studentNameById.GetValueOrDefault(d.StudentId, ""), var studentName = d.StudentId == Guid.Empty
? WholeGroupOption.Name
: studentNameById.GetValueOrDefault(d.StudentId, "");
Entries.Add(new DocumentationItem(d, studentName,
isOwnGroup, otherGroupLabel)); isOwnGroup, otherGroupLabel));
} }
} }
@@ -100,7 +106,7 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
[RelayCommand] [RelayCommand]
private async Task AddDocumentation() private async Task AddDocumentation()
{ {
if (OnEditDocumentation is null || _groupStudents.Count == 0) return; if (OnEditDocumentation is null) return;
var result = await OnEditDocumentation(_groupId, _groupStudents, null); var result = await OnEditDocumentation(_groupId, _groupStudents, null);
if (result is null) return; if (result is null) return;
_docs.Save(result); _docs.Save(result);
@@ -17,7 +17,7 @@ public record StudentOption(Guid Id, string Name);
public static class DocumentationTypeDisplay public static class DocumentationTypeDisplay
{ {
public static string[] Options { get; } = public static string[] Options { get; } =
["Gespräch", "Vorkommnis", "Förderplan", "Fehlzeit", "Elternanruf", "Elternbrief"]; ["Gespräch", "Vorkommnis", "Förderplan", "Fehlzeit", "Elternanruf", "Elternbrief", "Planung / Erinnerung"];
public static string Label(DocumentationType t) => t switch public static string Label(DocumentationType t) => t switch
{ {
@@ -27,6 +27,7 @@ public static class DocumentationTypeDisplay
DocumentationType.Absence => "Fehlzeit", DocumentationType.Absence => "Fehlzeit",
DocumentationType.ParentCall => "Elternanruf", DocumentationType.ParentCall => "Elternanruf",
DocumentationType.ParentLetter => "Elternbrief", DocumentationType.ParentLetter => "Elternbrief",
DocumentationType.Planning => "Planung / Erinnerung",
_ => "", _ => "",
}; };
@@ -37,6 +38,7 @@ public static class DocumentationTypeDisplay
"Fehlzeit" => DocumentationType.Absence, "Fehlzeit" => DocumentationType.Absence,
"Elternanruf" => DocumentationType.ParentCall, "Elternanruf" => DocumentationType.ParentCall,
"Elternbrief" => DocumentationType.ParentLetter, "Elternbrief" => DocumentationType.ParentLetter,
"Planung / Erinnerung" => DocumentationType.Planning,
_ => DocumentationType.Conversation, _ => DocumentationType.Conversation,
}; };
} }
@@ -301,7 +303,7 @@ public partial class DocumentationDialogViewModel : ObservableObject
LetterSentDateError = ""; LetterResponseDateError = ""; LetterSentDateError = ""; LetterResponseDateError = "";
var valid = true; var valid = true;
if (CanPickStudent && SelectedStudent is null) { StudentError = "Schüler auswählen."; valid = false; } if (CanPickStudent && SelectedStudent is null) { StudentError = "Bezug auswählen."; valid = false; }
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; } if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
@@ -0,0 +1,46 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Models;
using System.Globalization;
namespace LehrerApp.Desktop.ViewModels.Students;
public sealed record GroupDocumentationOption(Guid Id, string DisplayName);
/// Kompakte Erfassung einer gruppenweiten Planungsnotiz aus der globalen Befehlspalette.
public partial class GroupDocumentationQuickViewModel(IEnumerable<GroupDocumentationOption> groups)
: ObservableObject
{
public List<GroupDocumentationOption> Groups { get; } = groups.ToList();
[ObservableProperty] private GroupDocumentationOption? _selectedGroup;
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _content = "";
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
[ObservableProperty] private string _groupError = "";
[ObservableProperty] private string _titleError = "";
[ObservableProperty] private string _dateError = "";
public Documentation? Result { get; private set; }
[RelayCommand]
private void Save()
{
GroupError = TitleError = DateError = "";
if (SelectedGroup is null) GroupError = "Lerngruppe auswählen.";
if (string.IsNullOrWhiteSpace(Title)) TitleError = "Titel erforderlich.";
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
DateError = "Format TT.MM.JJJJ.";
if (GroupError.Length > 0 || TitleError.Length > 0 || DateError.Length > 0) return;
Result = new Documentation
{
StudentId = Guid.Empty,
GroupId = SelectedGroup!.Id,
Type = DocumentationType.Planning,
Date = date,
Title = Title.Trim(),
Content = Content.Trim(),
ExcludeFromWebUntisSync = true,
};
}
}
@@ -64,9 +64,15 @@ public partial class StudentListViewModel : ObservableObject
[ObservableProperty] private string _searchText = ""; [ObservableProperty] private string _searchText = "";
[ObservableProperty] private bool _showInactive; [ObservableProperty] private bool _showInactive;
[ObservableProperty] private StudentListItem? _selectedStudent; [ObservableProperty] private StudentListItem? _selectedStudent;
[ObservableProperty] private bool _isImporting;
public ObservableCollection<StudentListItem> Students { get; } = []; public ObservableCollection<StudentListItem> Students { get; } = [];
public string CountSummary => $"{Students.Count} Schüler gesamt"; public string CountSummary => $"{Students.Count} Schüler gesamt";
public bool HasNoStudents => Students.Count == 0;
public bool HasStudents => !HasNoStudents;
public string EmptyListMessage => string.IsNullOrWhiteSpace(SearchText)
? ShowInactive ? "Noch keine Schüler vorhanden." : "Noch keine aktiven Schüler vorhanden."
: "Keine Schüler passen zur aktuellen Suche.";
public StudentListViewModel(IStudentRepository students, IGroupRepository groups, public StudentListViewModel(IStudentRepository students, IGroupRepository groups,
IGroupMembershipRepository memberships) IGroupMembershipRepository memberships)
@@ -105,6 +111,9 @@ public partial class StudentListViewModel : ObservableObject
} }
foreach (var s in f) Students.Add(new StudentListItem(s)); foreach (var s in f) Students.Add(new StudentListItem(s));
OnPropertyChanged(nameof(CountSummary)); OnPropertyChanged(nameof(CountSummary));
OnPropertyChanged(nameof(HasNoStudents));
OnPropertyChanged(nameof(HasStudents));
OnPropertyChanged(nameof(EmptyListMessage));
} }
public Func<Task>? OnAddStudent { get; set; } public Func<Task>? OnAddStudent { get; set; }
@@ -20,17 +20,17 @@
<StackPanel Spacing="20"> <StackPanel Spacing="20">
<!-- Begrüßung --> <!-- Begrüßung -->
<Grid ColumnDefinitions="*,Auto"> <Grid RowDefinitions="Auto,Auto">
<StackPanel> <StackPanel>
<TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/> <TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/>
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/> <TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8"> <WrapPanel Grid.Row="1" Orientation="Horizontal" ItemSpacing="8" LineSpacing="8" Margin="0,10,0,0">
<Button Content="Klassenbuch abgleichen…" Click="OnCompareWebUntisDocumentationClick" <Button Content="Klassenbuch abgleichen…" Click="OnCompareWebUntisDocumentationClick"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<Button Content="Bereiche anpassen" Command="{Binding ToggleDashboardSettingsCommand}" <Button Content="Bereiche anpassen" Command="{Binding ToggleDashboardSettingsCommand}"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
</StackPanel> </WrapPanel>
</Grid> </Grid>
<!-- Der Tagesfokus beantwortet zuerst die vier Fragen, die beim Öffnen der App zählen: <!-- Der Tagesfokus beantwortet zuerst die vier Fragen, die beim Öffnen der App zählen:
@@ -72,9 +72,13 @@
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<CheckBox Content="{Binding Title}" IsChecked="{Binding IsVisible}" VerticalAlignment="Center"/> <CheckBox Content="{Binding Title}" IsChecked="{Binding IsVisible}" VerticalAlignment="Center"/>
<Button Content="↑" Padding="6,2" <Button Content="↑" Padding="6,2"
ToolTip.Tip="Bereich nach oben verschieben"
AutomationProperties.Name="Dashboard-Bereich nach oben verschieben"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardUpCommand}" Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardUpCommand}"
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
<Button Content="↓" Padding="6,2" <Button Content="↓" Padding="6,2"
ToolTip.Tip="Bereich nach unten verschieben"
AutomationProperties.Name="Dashboard-Bereich nach unten verschieben"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardDownCommand}" Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardDownCommand}"
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
</StackPanel> </StackPanel>
@@ -250,8 +254,10 @@
Width="130" Margin="12,0"/> Width="130" Margin="12,0"/>
<Button Grid.Column="2" Content="Heute" FontSize="11" Padding="8,2" <Button Grid.Column="2" Content="Heute" FontSize="11" Padding="8,2"
Command="{Binding CalendarTodayCommand}" Margin="0,0,4,0"/> Command="{Binding CalendarTodayCommand}" Margin="0,0,4,0"/>
<Button Grid.Column="3" Content="" Padding="8,2" Command="{Binding PrevMonthCommand}"/> <Button Grid.Column="3" Content="" Padding="8,2" Command="{Binding PrevMonthCommand}"
<Button Grid.Column="4" Content="" Padding="8,2" Command="{Binding NextMonthCommand}" Margin="4,0,0,0"/> ToolTip.Tip="Vorheriger Monat" AutomationProperties.Name="Vorheriger Monat"/>
<Button Grid.Column="4" Content="" Padding="8,2" Command="{Binding NextMonthCommand}" Margin="4,0,0,0"
ToolTip.Tip="Nächster Monat" AutomationProperties.Name="Nächster Monat"/>
</Grid> </Grid>
<ItemsControl ItemsSource="{Binding CalendarWeekdayHeaders}"> <ItemsControl ItemsSource="{Binding CalendarWeekdayHeaders}">
@@ -586,10 +592,10 @@
<StackPanel> <StackPanel>
<TextBlock Text="MEINE LERNGRUPPEN" FontSize="11" FontWeight="Bold" <TextBlock Text="MEINE LERNGRUPPEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/> Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding CurrentGroups}"> <ItemsControl ItemsSource="{Binding CurrentGroups}" HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/> <WrapPanel Orientation="Horizontal" ItemSpacing="8" LineSpacing="8"/>
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
@@ -597,9 +603,15 @@
<Button Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenGroupCommand}" <Button Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenGroupCommand}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Background="{DynamicResource SystemAccentColorLight2}" Background="{DynamicResource SystemAccentColorLight2}"
CornerRadius="6" Padding="12,6" Margin="0,0,8,8"> CornerRadius="6" Padding="12,6" MinWidth="150"
ToolTip.Tip="Kurs öffnen">
<StackPanel> <StackPanel>
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="13"/> <StackPanel Orientation="Horizontal" Spacing="6">
<Border Width="6" Height="6" CornerRadius="3"
Background="{DynamicResource SystemAccentColor}"
IsVisible="{Binding IsOnSelectedDay}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="13"/>
</StackPanel>
<TextBlock Text="{Binding Subject}" FontSize="11" Opacity="0.7" <TextBlock Text="{Binding Subject}" FontSize="11" Opacity="0.7"
IsVisible="{Binding Subject, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding Subject, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
@@ -182,8 +182,14 @@
<!-- Liste aller Klausuren, nach Priorität sortiert (nicht nach Datum) ─────────── --> <!-- Liste aller Klausuren, nach Priorität sortiert (nicht nach Datum) ─────────── -->
<ScrollViewer Grid.Row="2" Margin="20,0,20,16"> <ScrollViewer Grid.Row="2" Margin="20,0,20,16">
<StackPanel> <StackPanel>
<TextBlock Text="{Binding EmptyHint}" Classes="emptyhint" Margin="0,20,0,0" <StackPanel Margin="0,28,0,0" Spacing="10" HorizontalAlignment="Center"
IsVisible="{Binding EmptyHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding HasNoExams}">
<TextBlock Text="Noch keine Klausuren im aktuellen Schuljahr." Classes="emptyhint"
FontSize="15" TextAlignment="Center"/>
<TextBlock Text="Klausuren werden im jeweiligen Kurs angelegt." FontSize="12" Opacity="0.55"/>
<Button Content="Zu den Lerngruppen" Command="{Binding GoToGroupsCommand}"
HorizontalAlignment="Center"/>
</StackPanel>
<ItemsControl ItemsSource="{Binding Rows}"> <ItemsControl ItemsSource="{Binding Rows}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ExamListRowViewModel"> <DataTemplate x:DataType="vm:ExamListRowViewModel">
@@ -13,9 +13,9 @@
<Border Grid.Row="0" Padding="20,16" <Border Grid.Row="0" Padding="20,16"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto"> <Grid RowDefinitions="Auto,Auto">
<shared:PageHeader Grid.Column="0" Title="{Binding GroupTitle}" Subtitle="{Binding GroupSubtitle}"/> <shared:PageHeader Title="{Binding GroupTitle}" Subtitle="{Binding GroupSubtitle}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8"> <WrapPanel Grid.Row="1" Orientation="Horizontal" ItemSpacing="8" LineSpacing="8" Margin="0,10,0,0">
<TextBlock VerticalAlignment="Center" Opacity="0.6" FontSize="13"> <TextBlock VerticalAlignment="Center" Opacity="0.6" FontSize="13">
<Run Text="{Binding StudentCount}"/> <Run Text="{Binding StudentCount}"/>
<Run Text=" Schüler"/> <Run Text=" Schüler"/>
@@ -34,7 +34,7 @@
<Button Content="Austragung zurücknehmen" Command="{Binding ReinstateStudentCommand}" <Button Content="Austragung zurücknehmen" Command="{Binding ReinstateStudentCommand}"
IsVisible="{Binding SelectedStudent.HasExitDate}"/> IsVisible="{Binding SelectedStudent.HasExitDate}"/>
<Button Content=" Klausur" Command="{Binding AddExamCommand}" IsEnabled="{Binding IsEditable}"/> <Button Content=" Klausur" Command="{Binding AddExamCommand}" IsEnabled="{Binding IsEditable}"/>
</StackPanel> </WrapPanel>
</Grid> </Grid>
</Border> </Border>
@@ -28,8 +28,15 @@ public partial class GroupDocumentationTabView : UserControl
var owner = TopLevel.GetTopLevel(this) as Window; var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null; if (owner is null) return null;
var options = new List<StudentOption> { GroupDocumentationTabViewModel.WholeGroupOption };
options.AddRange(studentOptions);
var vm = new DocumentationDialogViewModel(editing?.StudentId ?? Guid.Empty, editing, var vm = new DocumentationDialogViewModel(editing?.StudentId ?? Guid.Empty, editing,
App.Services.GetRequiredService<IAttachmentStorage>(), studentOptions, groupId); App.Services.GetRequiredService<IAttachmentStorage>(), options, groupId);
if (editing is null)
{
vm.SelectedStudent = GroupDocumentationTabViewModel.WholeGroupOption;
vm.TypeName = DocumentationTypeDisplay.Label(DocumentationType.Planning);
}
var dialog = new DocumentationDialog { DataContext = vm }; var dialog = new DocumentationDialog { DataContext = vm };
var saved = await dialog.ShowDialog<bool>(owner); var saved = await dialog.ShowDialog<bool>(owner);
if (!saved) vm.DiscardUnsavedAttachments(); if (!saved) vm.DiscardUnsavedAttachments();
@@ -11,13 +11,14 @@
<Border Grid.Row="0" Padding="20,16" <Border Grid.Row="0" Padding="20,16"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto,Auto"> <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto,Auto">
<shared:PageHeader Grid.Column="0" Title="Lerngruppen" Subtitle="{Binding ListSummary}"/> <shared:PageHeader Grid.ColumnSpan="3" Title="Lerngruppen" Subtitle="{Binding ListSummary}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding SchoolYears}" <ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding SchoolYears}"
SelectedItem="{Binding SelectedSchoolYear}" SelectedItem="{Binding SelectedSchoolYear}"
Width="100" Margin="0,0,8,0" VerticalAlignment="Center"/> Width="110" Margin="0,10,8,0" VerticalAlignment="Center"
<Button Grid.Column="2" Content=" Neue Gruppe" AutomationProperties.Name="Schuljahr auswählen"/>
Command="{Binding AddGroupCommand}" VerticalAlignment="Center"/> <Button Grid.Row="1" Grid.Column="2" Content=" Neue Gruppe"
Command="{Binding AddGroupCommand}" VerticalAlignment="Center" Margin="0,10,0,0"/>
</Grid> </Grid>
</Border> </Border>
@@ -76,7 +77,7 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Grid.Column="2" Content="⋯" Width="36" Height="32" Margin="0,10,10,0" <Button Grid.Column="2" Content="⋯" Width="40" Height="40" Margin="0,10,10,0"
Padding="0" VerticalAlignment="Top" Padding="0" VerticalAlignment="Top"
ToolTip.Tip="Lerngruppe verwalten" ToolTip.Tip="Lerngruppe verwalten"
AutomationProperties.Name="{Binding ManageAutomationName}"> AutomationProperties.Name="{Binding ManageAutomationName}">
+31 -23
View File
@@ -22,7 +22,7 @@
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Title="LehrerApp" Title="LehrerApp"
Width="1280" Height="800" Width="1280" Height="800"
MinWidth="900" MinHeight="600"> MinWidth="640" MinHeight="480">
<Panel> <Panel>
<!-- <!--
@@ -81,14 +81,14 @@
FontFamily explizit auf die farbige Emoji-Schrift gepinnt: Windows kann für FontFamily explizit auf die farbige Emoji-Schrift gepinnt: Windows kann für
Emoji-Codepoints je nach Fallback-Auflösung sonst auf "Segoe UI Symbol" Emoji-Codepoints je nach Fallback-Auflösung sonst auf "Segoe UI Symbol"
(einfarbig/schwarz) statt "Segoe UI Emoji" (farbig) ausweichen. --> (einfarbig/schwarz) statt "Segoe UI Emoji" (farbig) ausweichen. -->
<Style Selector="TextBlock.navicon"> <Style Selector="PathIcon.navicon">
<Setter Property="FontSize" Value="15"/>
<Setter Property="Width" Value="24"/> <Setter Property="Width" Value="24"/>
<Setter Property="FontFamily" Value="Segoe UI Emoji,Segoe UI Symbol,Segoe UI"/> <Setter Property="Height" Value="24"/>
</Style> </Style>
<Style Selector="Button.navitem"> <Style Selector="Button.navitem">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="10,8"/> <Setter Property="Padding" Value="10,8"/>
<Setter Property="MinHeight" Value="40"/>
</Style> </Style>
<Style Selector="Button.navitem.active"> <Style Selector="Button.navitem.active">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/> <Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
@@ -98,9 +98,9 @@
<Style Selector="DockPanel.compact TextBlock.navlabel"> <Style Selector="DockPanel.compact TextBlock.navlabel">
<Setter Property="IsVisible" Value="False"/> <Setter Property="IsVisible" Value="False"/>
</Style> </Style>
<Style Selector="DockPanel.compact TextBlock.navicon"> <Style Selector="DockPanel.compact PathIcon.navicon">
<Setter Property="FontSize" Value="20"/>
<Setter Property="Width" Value="28"/> <Setter Property="Width" Value="28"/>
<Setter Property="Height" Value="28"/>
</Style> </Style>
<Style Selector="DockPanel.compact Button.navitem"> <Style Selector="DockPanel.compact Button.navitem">
<Setter Property="HorizontalContentAlignment" Value="Center"/> <Setter Property="HorizontalContentAlignment" Value="Center"/>
@@ -157,7 +157,7 @@
ToolTip.Tip="Suchen und schnell erfassen (Strg/⌘+K)" ToolTip.Tip="Suchen und schnell erfassen (Strg/⌘+K)"
AutomationProperties.Name="Suchen und schnell erfassen"> AutomationProperties.Name="Suchen und schnell erfassen">
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Classes="navicon" Text="⌕" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconSearch}"/>
<TextBlock Grid.Column="1" Classes="navlabel" Text="Suchen / Erfassen"/> <TextBlock Grid.Column="1" Classes="navlabel" Text="Suchen / Erfassen"/>
<TextBlock Grid.Column="2" Classes="navlabel" Text="⌘K" FontSize="10" Opacity="0.45" <TextBlock Grid.Column="2" Classes="navlabel" Text="⌘K" FontSize="10" Opacity="0.45"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
@@ -169,9 +169,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Dashboard}"> CommandParameter="{x:Static vm:NavItem.Dashboard}"
ToolTip.Tip="Dashboard (Strg/⌘+1)" AutomationProperties.Name="Dashboard">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📊" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconDashboard}"/>
<TextBlock Classes="navlabel" Text="Dashboard"/> <TextBlock Classes="navlabel" Text="Dashboard"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -183,9 +184,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Groups}"> CommandParameter="{x:Static vm:NavItem.Groups}"
ToolTip.Tip="Lerngruppen (Strg/⌘+2)" AutomationProperties.Name="Lerngruppen">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="🏫" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconGroups}"/>
<TextBlock Classes="navlabel" Text="Lerngruppen"/> <TextBlock Classes="navlabel" Text="Lerngruppen"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -193,9 +195,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Students}"> CommandParameter="{x:Static vm:NavItem.Students}"
ToolTip.Tip="Schüler (Strg/⌘+3)" AutomationProperties.Name="Schüler">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="👤" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconStudents}"/>
<TextBlock Classes="navlabel" Text="Schüler"/> <TextBlock Classes="navlabel" Text="Schüler"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -203,9 +206,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Exams}"> CommandParameter="{x:Static vm:NavItem.Exams}"
ToolTip.Tip="Klausuren (Strg/⌘+4)" AutomationProperties.Name="Klausuren">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📝" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconExams}"/>
<TextBlock Classes="navlabel" Text="Klausuren"/> <TextBlock Classes="navlabel" Text="Klausuren"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -213,9 +217,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Planner}"> CommandParameter="{x:Static vm:NavItem.Planner}"
ToolTip.Tip="Planung (Strg/⌘+5)" AutomationProperties.Name="Planung">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📅" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconCalendar}"/>
<TextBlock Classes="navlabel" Text="Planung"/> <TextBlock Classes="navlabel" Text="Planung"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -227,9 +232,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Workload}"> CommandParameter="{x:Static vm:NavItem.Workload}"
ToolTip.Tip="Arbeitszeit (Strg/⌘+6)" AutomationProperties.Name="Arbeitszeit">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⏱" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconClock}"/>
<TextBlock Classes="navlabel" Text="Arbeitszeit"/> <TextBlock Classes="navlabel" Text="Arbeitszeit"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -237,9 +243,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.ClassTeacher}"> CommandParameter="{x:Static vm:NavItem.ClassTeacher}"
ToolTip.Tip="Klassenlehrer (Strg/⌘+7)" AutomationProperties.Name="Klassenlehrer">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="🎓" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconClassTeacher}"/>
<TextBlock Classes="navlabel" Text="Klassenlehrer"/> <TextBlock Classes="navlabel" Text="Klassenlehrer"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -247,9 +254,10 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
CornerRadius="6" CornerRadius="6"
Command="{Binding NavigateToCommand}" Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Settings}"> CommandParameter="{x:Static vm:NavItem.Settings}"
ToolTip.Tip="Einstellungen (Strg/⌘+8)" AutomationProperties.Name="Einstellungen">
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10"> <StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⚙️" TextAlignment="Center"/> <PathIcon Classes="navicon" Data="{StaticResource IconSettings}"/>
<TextBlock Classes="navlabel" Text="Einstellungen"/> <TextBlock Classes="navlabel" Text="Einstellungen"/>
</StackPanel> </StackPanel>
</Button> </Button>
@@ -36,6 +36,13 @@ public partial class MainWindow : Window
return; return;
} }
if (commandModifier && TryGetNavigationShortcut(e.Key, out var destination))
{
vm.NavigateToCommand.Execute(destination);
e.Handled = true;
return;
}
if (!vm.IsCommandPaletteOpen) return; if (!vm.IsCommandPaletteOpen) return;
if (e.Key == Key.Escape) if (e.Key == Key.Escape)
{ {
@@ -54,6 +61,23 @@ public partial class MainWindow : Window
} }
} }
private static bool TryGetNavigationShortcut(Key key, out NavItem destination)
{
destination = key switch
{
Key.D1 or Key.NumPad1 => NavItem.Dashboard,
Key.D2 or Key.NumPad2 => NavItem.Groups,
Key.D3 or Key.NumPad3 => NavItem.Students,
Key.D4 or Key.NumPad4 => NavItem.Exams,
Key.D5 or Key.NumPad5 => NavItem.Planner,
Key.D6 or Key.NumPad6 => NavItem.Workload,
Key.D7 or Key.NumPad7 => NavItem.ClassTeacher,
Key.D8 or Key.NumPad8 => NavItem.Settings,
_ => default,
};
return key is >= Key.D1 and <= Key.D8 or >= Key.NumPad1 and <= Key.NumPad8;
}
private void OnOpenCommandPaletteClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) private void OnOpenCommandPaletteClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
if (DataContext is MainWindowViewModel vm) if (DataContext is MainWindowViewModel vm)
@@ -154,9 +154,11 @@
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto"> <Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
<Button Grid.Column="0" Content="" FontWeight="Bold" Padding="10,4" <Button Grid.Column="0" Content="" FontWeight="Bold" Padding="10,4"
Command="{Binding PreviousWeekCommand}" ToolTip.Tip="Vorherige Woche"/> Command="{Binding PreviousWeekCommand}" ToolTip.Tip="Vorherige Woche"
AutomationProperties.Name="Vorherige Woche"/>
<Button Grid.Column="1" Content="" FontWeight="Bold" Padding="10,4" Margin="4,0,0,0" <Button Grid.Column="1" Content="" FontWeight="Bold" Padding="10,4" Margin="4,0,0,0"
Command="{Binding NextWeekCommand}" ToolTip.Tip="Nächste Woche"/> Command="{Binding NextWeekCommand}" ToolTip.Tip="Nächste Woche"
AutomationProperties.Name="Nächste Woche"/>
<TextBlock Grid.Column="2" FontSize="16" FontWeight="SemiBold" <TextBlock Grid.Column="2" FontSize="16" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="12,0,0,0"> VerticalAlignment="Center" Margin="12,0,0,0">
<Run Text="Woche "/><Run Text="{Binding WeekRangeLabel}"/> <Run Text="Woche "/><Run Text="{Binding WeekRangeLabel}"/>
@@ -166,7 +168,8 @@
<Button Grid.Column="4" Content="Ausnahme eintragen" Margin="0,0,8,0" <Button Grid.Column="4" Content="Ausnahme eintragen" Margin="0,0,8,0"
Command="{Binding AddSubstitutionCommand}"/> Command="{Binding AddSubstitutionCommand}"/>
<Button Grid.Column="5" Content="⚙️" Command="{Binding OpenSettingsCommand}" <Button Grid.Column="5" Content="⚙️" Command="{Binding OpenSettingsCommand}"
ToolTip.Tip="Einstellungen (Ferien, Aufsichten, Stundenraster)"/> ToolTip.Tip="Einstellungen (Ferien, Aufsichten, Stundenraster)"
AutomationProperties.Name="Stundenplan-Einstellungen"/>
</Grid> </Grid>
<Border Background="#33F59E0B" BorderBrush="#F59E0B" BorderThickness="1" <Border Background="#33F59E0B" BorderBrush="#F59E0B" BorderThickness="1"
@@ -281,7 +284,7 @@
<Button Classes="weekCellMenuTrigger" Content="⋮" <Button Classes="weekCellMenuTrigger" Content="⋮"
HorizontalAlignment="Right" VerticalAlignment="Top" HorizontalAlignment="Right" VerticalAlignment="Top"
IsVisible="{Binding HasGroupId}" IsVisible="{Binding HasGroupId}"
ToolTip.Tip="Weitere Ziele…"> ToolTip.Tip="Weitere Ziele…" AutomationProperties.Name="Weitere Ziele">
<Button.Flyout> <Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedRight"> <MenuFlyout Placement="BottomEdgeAlignedRight">
<MenuItem Header="▶ Unterrichtsansicht" IsVisible="{Binding HasLesson}" <MenuItem Header="▶ Unterrichtsansicht" IsVisible="{Binding HasLesson}"
@@ -404,7 +407,8 @@
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Center" Background="Transparent" HorizontalContentAlignment="Center" Background="Transparent"
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).EditCellCommand}" Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).EditCellCommand}"
CommandParameter="{Binding}"/> CommandParameter="{Binding}"
AutomationProperties.Name="Stundenplan-Eintrag hinzufügen"/>
<Border Background="#D85A30" CornerRadius="8" Padding="5,1" <Border Background="#D85A30" CornerRadius="8" Padding="5,1"
IsVisible="{Binding HasBadge}" IsVisible="{Binding HasBadge}"
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="2"> HorizontalAlignment="Right" VerticalAlignment="Top" Margin="2">
@@ -13,10 +13,10 @@
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/> <TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
<StackPanel Spacing="4" IsVisible="{Binding CanPickStudent}"> <StackPanel Spacing="4" IsVisible="{Binding CanPickStudent}">
<TextBlock Text="Schüler *" FontSize="12" Opacity="0.7"/> <TextBlock Text="Bezug *" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding StudentOptions}" SelectedItem="{Binding SelectedStudent}" <ComboBox ItemsSource="{Binding StudentOptions}" SelectedItem="{Binding SelectedStudent}"
DisplayMemberBinding="{Binding Name}" HorizontalAlignment="Stretch" DisplayMemberBinding="{Binding Name}" HorizontalAlignment="Stretch"
PlaceholderText="Schüler wählen"/> PlaceholderText="Schüler oder gesamte Lerngruppe wählen"/>
<TextBlock Text="{Binding StudentError}" Foreground="Red" FontSize="11" <TextBlock Text="{Binding StudentError}" Foreground="Red" FontSize="11"
IsVisible="{Binding StudentError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding StudentError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
@@ -0,0 +1,44 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
x:Class="LehrerApp.Desktop.Views.Students.GroupDocumentationQuickDialog"
x:DataType="vm:GroupDocumentationQuickViewModel"
Title="Lerngruppen-Eintrag" Width="480" SizeToContent="Height"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<StackPanel Spacing="12">
<TextBlock Text="Lerngruppen-Eintrag" Classes="dialogtitle"/>
<TextBlock Text="Planung, Klausur oder Erinnerung für eine ganze Lerngruppe festhalten."
TextWrapping="Wrap" Opacity="0.7"/>
<StackPanel Spacing="4">
<TextBlock Text="Lerngruppe *" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Groups}" SelectedItem="{Binding SelectedGroup}"
DisplayMemberBinding="{Binding DisplayName}" PlaceholderText="Lerngruppe wählen"/>
<TextBlock Text="{Binding GroupError}" Foreground="Red" FontSize="11"
IsVisible="{Binding GroupError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<Grid ColumnDefinitions="*,12,140">
<StackPanel Spacing="4">
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Title}" PlaceholderText="z. B. Klausur ankündigen"/>
<TextBlock Text="{Binding TitleError}" Foreground="Red" FontSize="11"
IsVisible="{Binding TitleError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding DateText}"/>
<TextBlock Text="{Binding DateError}" Foreground="Red" FontSize="11"
IsVisible="{Binding DateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="Notiz" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Content}" AcceptsReturn="True" Height="90" TextWrapping="Wrap"/>
</StackPanel>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,20,0,0">
<Button Content="Abbrechen" Click="OnCancel"/>
<Button Content="Speichern" Classes="accent" Click="OnSave"/>
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,19 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Students;
namespace LehrerApp.Desktop.Views.Students;
public partial class GroupDocumentationQuickDialog : Window
{
public GroupDocumentationQuickDialog() => InitializeComponent();
private void OnSave(object? sender, RoutedEventArgs e)
{
if (DataContext is not GroupDocumentationQuickViewModel vm) return;
vm.SaveCommand.Execute(null);
if (vm.Result is not null) Close(true);
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
}
@@ -8,18 +8,21 @@
<Border Grid.Row="0" Padding="20,16" <Border Grid.Row="0" Padding="20,16"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto,Auto,Auto"> <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto,Auto,Auto">
<shared:PageHeader Grid.Column="0" Title="Schüler" Subtitle="{Binding CountSummary}"/> <shared:PageHeader Grid.ColumnSpan="4" Title="Schüler" Subtitle="{Binding CountSummary}"/>
<CheckBox Grid.Column="1" Content="Inaktive anzeigen" <CheckBox Grid.Row="1" Grid.Column="1" Content="Inaktive anzeigen"
IsChecked="{Binding ShowInactive}" IsChecked="{Binding ShowInactive}"
VerticalAlignment="Center" Margin="0,0,12,0"/> VerticalAlignment="Center" Margin="0,10,12,0"/>
<Button Grid.Column="2" Content="⇩ Importieren…" Click="OnImportClick" <Button Grid.Row="1" Grid.Column="2" Content="⇩ Importieren…" Click="OnImportClick"
VerticalAlignment="Center" Margin="0,0,8,0"/> IsEnabled="{Binding !IsImporting}"
<Button Grid.Column="3" Content=" Neuer Schüler" VerticalAlignment="Center" Margin="0,10,8,0"
Command="{Binding AddStudentCommand}" VerticalAlignment="Center"/> AutomationProperties.Name="Schüler importieren"/>
<Button Grid.Row="1" Grid.Column="3" Content=" Neuer Schüler"
Command="{Binding AddStudentCommand}" VerticalAlignment="Center" Margin="0,10,0,0"/>
</Grid> </Grid>
</Border> </Border>
<DockPanel Grid.Row="1"> <Grid Grid.Row="1">
<DockPanel IsVisible="{Binding HasStudents}">
<TextBox DockPanel.Dock="Top" Text="{Binding SearchText}" <TextBox DockPanel.Dock="Top" Text="{Binding SearchText}"
PlaceholderText="Name oder Lerngruppe suchen…" Margin="16,10,16,4"/> PlaceholderText="Name oder Lerngruppe suchen…" Margin="16,10,16,4"/>
<DataGrid ItemsSource="{Binding Students}" <DataGrid ItemsSource="{Binding Students}"
@@ -45,5 +48,22 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</DockPanel> </DockPanel>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10"
IsVisible="{Binding HasNoStudents}">
<TextBlock Text="{Binding EmptyListMessage}" Classes="emptyhint" FontSize="15"
TextAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="8"
IsVisible="{Binding SearchText, Converter={x:Static StringConverters.IsNullOrEmpty}}">
<Button Content=" Ersten Schüler anlegen" Command="{Binding AddStudentCommand}"/>
<Button Content="⇩ Schülerliste importieren" Click="OnImportClick"/>
</StackPanel>
</StackPanel>
<Border Background="#70000000" IsVisible="{Binding IsImporting}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10">
<ProgressBar IsIndeterminate="True" Width="220" Height="5"/>
<TextBlock Text="Schülerliste wird analysiert …" Foreground="White"/>
</StackPanel>
</Border>
</Grid>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -36,6 +36,7 @@ public partial class StudentListView : UserControl
}); });
if (files.Count == 0) return; if (files.Count == 0) return;
list.IsImporting = true;
try try
{ {
await using var source = await files[0].OpenReadAsync(); await using var source = await files[0].OpenReadAsync();
@@ -67,5 +68,9 @@ public partial class StudentListView : UserControl
{ {
App.Services.GetRequiredService<NotificationService>().ShowError(ex.Message); App.Services.GetRequiredService<NotificationService>().ShowError(ex.Message);
} }
finally
{
list.IsImporting = false;
}
} }
} }
+10 -3
View File
@@ -14,10 +14,17 @@
Foreground="#D32F2F"/> Foreground="#D32F2F"/>
<TextBlock Text="Sync angehalten" FontSize="10" Foreground="#D32F2F" Opacity="0.8"/> <TextBlock Text="Sync angehalten" FontSize="10" Foreground="#D32F2F" Opacity="0.8"/>
</StackPanel> </StackPanel>
<Button Grid.Column="1" Content="↻" FontSize="14" <Button Grid.Column="1" Classes="touchTarget"
Command="{Binding SyncNowCommand}" Command="{Binding SyncNowCommand}"
IsVisible="{Binding CanAttemptSync}" IsVisible="{Binding CanAttemptSync}"
Background="Transparent" Padding="6,4" Background="Transparent" Padding="8"
ToolTip.Tip="Jetzt synchronisieren"/> ToolTip.Tip="Jetzt synchronisieren" AutomationProperties.Name="Jetzt synchronisieren">
<Grid>
<PathIcon Data="{StaticResource IconRefresh}" Width="18" Height="18"
IsVisible="{Binding !IsSyncing}"/>
<ProgressBar Width="22" Height="4" IsIndeterminate="True"
IsVisible="{Binding IsSyncing}"/>
</Grid>
</Button>
</Grid> </Grid>
</UserControl> </UserControl>
+51 -12
View File
@@ -2077,6 +2077,13 @@ dadurch faktisch schon (Quick-Input-Dialog bzw. "Offene Entschuldigungen" im Das
Auswahl gar nicht erst an. "Gespräch begleiten" (Elternanruf) und Anhänge funktionieren im Auswahl gar nicht erst an. "Gespräch begleiten" (Elternanruf) und Anhänge funktionieren im
Gruppen-Tab identisch zum Schüler-Tab, da beide dieselbe `DocumentationItem`/ Gruppen-Tab identisch zum Schüler-Tab, da beide dieselbe `DocumentationItem`/
`DocumentationDialog`-Infrastruktur verwenden. `DocumentationDialog`-Infrastruktur verwenden.
- [x] **5.1.8** Gruppenweite Planungs- und Erinnerungseinträge ohne Schülerbezug. Im
Dokumentationsdialog einer Lerngruppe steht jetzt „Gesamte Lerngruppe“ als eigener Bezug
zur Wahl; solche Einträge verwenden den angehängten Typ `DocumentationType.Planning`, die
bestehende `GroupId` und bewusst `Guid.Empty` als kompatiblen „kein Schüler“-Marker. Sie
erscheinen auch in Gruppen ohne Schüler und werden eindeutig als „Gesamte Lerngruppe“
beschriftet. Die globale Suche bietet zusätzlich die Schnellaktion „Lerngruppen-Eintrag“
mit einem kompakten Formular für Gruppe, Datum, Titel und Notiz.
### 5.2 Fehlzeiten (als Auswertung des bestehenden Anwesenheits-Trackings, siehe oben) ### 5.2 Fehlzeiten (als Auswertung des bestehenden Anwesenheits-Trackings, siehe oben)
- [x] **5.2.1** Schnelle Abwesenheitserfassung je Stunde — bereits vorhanden über - [x] **5.2.1** Schnelle Abwesenheitserfassung je Stunde — bereits vorhanden über
@@ -2644,6 +2651,13 @@ Bereich automatisch), die linke Inhaltsspalte erhält mehr Breite und leere rein
aktivierter Dashboard-Konfiguration automatisch ausgeblendet. Inhalte und Direktaktionen bleiben aktivierter Dashboard-Konfiguration automatisch ausgeblendet. Inhalte und Direktaktionen bleiben
unverändert erhalten; die Seite wird bei ruhiger Datenlage lediglich deutlich kürzer. unverändert erhalten; die Seite wird bei ruhiger Datenlage lediglich deutlich kürzer.
**Nachtrag Kursliste (August 2026):** Die Kachel „Meine Lerngruppen“ nutzt weiterhin ein echtes
mehrzeiliges `WrapPanel`, jetzt mit explizitem Zeilen-/Elementabstand und Mindestbreite statt einer
abschneidbaren Ein-Zeilen-Darstellung. Bei Auswahl eines Kalendertags werden Gruppen mit einer
tatsächlichen Lesson oder einem an diesem Tag aktiven Stundenplan-Slot zuerst angezeigt und mit
einem Akzentpunkt markiert; innerhalb dieser Gruppe sowie für den Rest gilt alphabetische Sortierung.
Ferien, Feiertage und vollständig ausgefallene Stunden werden dabei berücksichtigt.
--- ---
## 10. Sync & Server ## 10. Sync & Server
@@ -3500,15 +3514,21 @@ von 4.2.2) der Tab "Kürzel-Katalog" für die Von/Nach-Kürzel des Stundenverlau
## 14. UX-Querschnitt ## 14. UX-Querschnitt
- [ ] **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). **Hauptnavigation umgesetzt:** `Strg/⌘+1…8` öffnet
Dashboard, Lerngruppen, Schüler, Klausuren, Planung, Arbeitszeit, Klassenlehrer und
Einstellungen direkt; `Strg/⌘+K`, Pfeiltasten, Enter und Escape bedienen weiterhin die
globale Suche. Die vollständige Tastaturprüfung aller Fachdialoge bleibt offen.
- [x] **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 **Umsetzung:** `Strg+K` (Windows/Linux) bzw. `⌘K` (macOS) öffnet aus jeder Hauptansicht eine
modale Befehlspalette. `GlobalSearchViewModel` durchsucht lokal und ohne zusätzlichen Index modale Befehlspalette. `GlobalSearchViewModel` durchsucht lokal und ohne zusätzlichen Index
aktive wie inaktive Schüler/Lerngruppen sowie Klausuren und Aufgaben; Treffer springen direkt ausschließlich aktive Schüler/Lerngruppen sowie deren Klausuren und Aufgaben; Treffer springen direkt
ins Schülerdetail, Gruppendetail, den Klausuren-Tab oder die Aufgabenverwaltung. Pfeiltasten, 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 Enter und Escape bedienen die Palette vollständig ohne Maus. Gruppentreffer werden
Schnellaktionen „Aufgabe anlegen“, „Erinnerung anlegen“ und „Schüler anlegen“ bereit und zusätzlich über den zugeordneten Fachnamen gefunden und zeigen diesen
im Untertitel, damit gleichnamige Klassen/Kurse eindeutig bleiben. Bei leerer Suche stehen die
Schnellaktionen „Aufgabe anlegen“, „Erinnerung anlegen“, „Schüler anlegen“ und
„Lerngruppen-Eintrag“ bereit und
verwenden die bereits vorhandenen Dialoge samt Validierung. Zusätzlich ist der Einstieg als verwenden die bereits vorhandenen Dialoge samt Validierung. Zusätzlich ist der Einstieg als
zugänglich benannte Schaltfläche im Navigationsbereich sichtbar. Tests in zugänglich benannte Schaltfläche im Navigationsbereich sichtbar. Tests in
`GlobalSearchViewModelTests` decken Ergebnisarten, Navigation und Schnellerfassung ab. `GlobalSearchViewModelTests` decken Ergebnisarten, Navigation und Schnellerfassung ab.
@@ -3553,8 +3573,15 @@ von 4.2.2) der Tab "Kürzel-Katalog" für die Von/Nach-Kürzel des Stundenverlau
[TrashTests.cs](LehrerApp.Data.Tests/TrashTests.cs) (Löschen/Wiederherstellen je [TrashTests.cs](LehrerApp.Data.Tests/TrashTests.cs) (Löschen/Wiederherstellen je
Repository, Sortierung, Bereinigung), 6 in Repository, Sortierung, Bereinigung), 6 in
[TrashViewModelTests.cs](LehrerApp.Desktop.Tests/TrashViewModelTests.cs). [TrashViewModelTests.cs](LehrerApp.Desktop.Tests/TrashViewModelTests.cs).
- [ ] **14.4** Ladeanzeigen bei längeren Operationen (Import, Sync, Export). - [~] **14.4** Ladeanzeigen bei längeren Operationen (Import, Sync, Export).
- [ ] **14.5** Leere Zustände mit Handlungsaufforderung statt leerer Tabellen. Schülerimporte blockieren die Liste während der asynchronen Dateianalyse mit Fortschritts-
anzeige und Status; der Sync-Button wechselt während des Abgleichs vom Vektoricon auf eine
indeterminierte Fortschrittsanzeige. Bereits vorhandene WebUntis-/KI-Ladevorgänge bleiben
erhalten. Eine einheitliche Anzeige für sämtliche PDF-/CSV-Exporte ist noch offen.
- [~] **14.5** Leere Zustände mit Handlungsaufforderung statt leerer Tabellen. Lerngruppen hatten
bereits „Erste Lerngruppe anlegen“; Schüler bieten jetzt „Ersten Schüler anlegen“ und
„Schülerliste importieren“, die Klausuren-Hauptseite erklärt den Anlageort und springt zu
den Lerngruppen. Fachspezifische Untertabellen werden schrittweise ergänzt.
- [~] **14.6** Fenstergröße und Spaltenbreiten über Sitzungen hinweg merken. - [~] **14.6** Fenstergröße und Spaltenbreiten über Sitzungen hinweg merken.
**Umsetzung (Fenstergröße):** **Umsetzung (Fenstergröße):**
@@ -3576,14 +3603,23 @@ von 4.2.2) der Tab "Kürzel-Katalog" für die Von/Nach-Kürzel des Stundenverlau
stabile Spalten-Identität, an der eine gespeicherte Breite verlässlich hängen könnte — stabile Spalten-Identität, an der eine gespeicherte Breite verlässlich hängen könnte —
eine generische Lösung ist damit kein "billiger" Zusatz, sondern ein eigener, größerer eine generische Lösung ist damit kein "billiger" Zusatz, sondern ein eigener, größerer
Umbau. Nicht in diesem Durchgang umgesetzt. Umbau. Nicht in diesem Durchgang umgesetzt.
- [ ] **14.7** Bedienung auf Touch-Geräten prüfen (Tablet im Unterricht). - [~] **14.7** Bedienung auf Touch-Geräten prüfen (Tablet im Unterricht). Hauptnavigation und
- [ ] **14.8** Responsive Layout und Windows-DPI prüfen (kleine Notebook-Auflösungen sowie Sync-Aktion haben mindestens 40 px große Ziele; besonders kleine Karten-/Menüaktionen wurden
vergrößert. Ein Test auf echter Tablet-Hardware bleibt offen.
- [~] **14.8** Responsive Layout und Windows-DPI prüfen (kleine Notebook-Auflösungen sowie
125/150/200 % Skalierung; starre Master-Detail-Spalten bei Bedarf stapeln). Der kompakte 125/150/200 % Skalierung; starre Master-Detail-Spalten bei Bedarf stapeln). Der kompakte
Drawer berücksichtigt bereits die schmalere verfügbare Breite mit reduziertem Außen-/ Drawer berücksichtigt bereits die schmalere verfügbare Breite mit reduziertem Außen-/
Innenabstand und einer eigenen Iconfläche, damit Windows-Emoji nicht abgeschnitten werden. Innenabstand und einer eigenen Iconfläche, damit Windows-Emoji nicht abgeschnitten werden.
- [ ] **14.9** Barrierefreiheit prüfen: Automation-Namen für Icon-Buttons, sichtbare Fokusrahmen, Das Hauptfenster kann jetzt bis 640×480 verkleinert werden und wechselt dadurch tatsächlich
in den Overlay-Drawer; zuvor verhinderte `MinWidth=900` exakt diesen Zustand. Aktionsleisten
in Dashboard, Schüler-, Gruppenliste und Gruppendetail umbrechen bzw. liegen unter dem Titel.
Ein echter Windows-DPI-Test bleibt offen.
- [~] **14.9** Barrierefreiheit prüfen: Automation-Namen für Icon-Buttons, sichtbare Fokusrahmen,
Kontraste und Status nicht ausschließlich über Farbe/Emoji vermitteln. Kontraste und Status nicht ausschließlich über Farbe/Emoji vermitteln.
- [ ] **14.10** Plattformübergreifend konsistentes SVG-/`PathIcon`-Set statt systemabhängiger Sichtbare Akzent-Fokusrahmen sind zentral für Buttons, Textfelder und Comboboxen definiert;
Hauptnavigation, Kalender-/Dashboard-Sortierung und zentrale Stundenplan-Iconaktionen haben
sprechende Automation-Namen und Tooltips. Die vollständige Prüfung aller Dialoge bleibt offen.
- [~] **14.10** Plattformübergreifend konsistentes SVG-/`PathIcon`-Set statt systemabhängiger
Emoji-Darstellung einführen. **Konkreter Bericht (Nutzer-Feedback):** Drawer-Icons erscheinen Emoji-Darstellung einführen. **Konkreter Bericht (Nutzer-Feedback):** Drawer-Icons erscheinen
auf einem Windows-PC gegen 22 Uhr einfarbig schwarz statt farbig — passend zum bekannten auf einem Windows-PC gegen 22 Uhr einfarbig schwarz statt farbig — passend zum bekannten
Windows-Verhalten, dass Emoji-Codepunkte je nach Font-Fallback statt der farbigen Windows-Verhalten, dass Emoji-Codepunkte je nach Font-Fallback statt der farbigen
@@ -3593,7 +3629,10 @@ von 4.2.2) der Tab "Kürzel-Katalog" für die Von/Nach-Kürzel des Stundenverlau
explizit `FontFamily="Segoe UI Emoji,Segoe UI Symbol,Segoe UI"` — auf anderen Plattformen explizit `FontFamily="Segoe UI Emoji,Segoe UI Symbol,Segoe UI"` — auf anderen Plattformen
folgenlos, da unbekannte Fontnamen einfach übersprungen werden. Nur eine Abmilderung für die folgenlos, da unbekannte Fontnamen einfach übersprungen werden. Nur eine Abmilderung für die
Drawer-Navigation, kein Nachweis der Ursache und keine Lösung für die übrigen Emoji im Rest Drawer-Navigation, kein Nachweis der Ursache und keine Lösung für die übrigen Emoji im Rest
der App — die eigentliche, dauerhafte Lösung bleibt dieser Punkt (echtes Icon-Set). der App. **Hauptnavigation jetzt dauerhaft gelöst:** Suche und alle acht Navigationsziele
verwenden zentral hinterlegte `StreamGeometry`/`PathIcon`-Ressourcen; auch der Sync-Button
verwendet ein Vektoricon. Fachaktionen im restlichen UI enthalten teilweise weiterhin Emoji
und werden in einem späteren, separaten Austausch migriert.
- [x] **14.11** Aktiven Navigationspunkt in der Seitenleiste sichtbar hervorheben; Zustand wird - [x] **14.11** Aktiven Navigationspunkt in der Seitenleiste sichtbar hervorheben; Zustand wird
über `MainWindowViewModel.ActiveNavItem` gesteuert. über `MainWindowViewModel.ActiveNavItem` gesteuert.
- [x] **14.12** Tab "Übersicht" im Kurs (`GroupDetailView`, bislang nur Platzhaltertext) gefüllt — - [x] **14.12** Tab "Übersicht" im Kurs (`GroupDetailView`, bislang nur Platzhaltertext) gefüllt —