feat: complete dashboard chapter

This commit is contained in:
2026-08-17 01:14:31 +02:00
parent fe36f51886
commit ad40d11d6e
8 changed files with 759 additions and 57 deletions
+3 -2
View File
@@ -99,7 +99,8 @@ public class App : Application
var dash = Services.GetRequiredService<DashboardViewModel>();
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
dash.OnNavigateToStudent = id => main.NavigateToStudent(id);
dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 2); // Tab "Mitarbeit"
dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 3); // Tab "Mitarbeit"
dash.OnNavigateToExam = id => main.NavigateToGroupDetail(id, 4); // Tab "Klausuren"
// StudentList → StudentDetail + Anlegen
var sl = Services.GetRequiredService<StudentListViewModel>();
@@ -109,7 +110,7 @@ public class App : Application
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
var timetable = Services.GetRequiredService<TimetableViewModel>();
timetable.OnNavigateToSettings = () => main.NavigateToSettings(7);
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5);
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 6);
}
private static async Task ShowAddStudentDialog()
+1
View File
@@ -154,6 +154,7 @@ public static class AppBootstrapper
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
services.AddSingleton(_ => new PeriodScheduleService(appData));
services.AddSingleton(_ => new WorkloadSettingsService(appData));
services.AddSingleton(_ => new DashboardSettingsService(appData));
services.AddSingleton(_ => new LetterTemplateService(appData));
// ── KI-Unterstützung (4.5.9, optional nur wenn in den Einstellungen aktiviert) ──────
@@ -16,6 +16,10 @@ public partial class DashboardViewModel : ObservableObject
private readonly ISubjectRepository _subjects;
private readonly ILessonRepository _lessons;
private readonly IExamRepository _exams;
private readonly IExamResultRepository _examResults;
private readonly IGradeRepository _grades;
private readonly IReportGradeRepository _reportGrades;
private readonly IGroupMembershipRepository _memberships;
private readonly IWorkTaskRepository _tasks;
private readonly IParticipationSessionRepository _participationSessions;
private readonly IParticipationRepository _participationEntries;
@@ -25,14 +29,18 @@ public partial class DashboardViewModel : ObservableObject
private readonly PeriodScheduleService _periodSchedule;
private readonly AttendanceBalanceService _attendanceBalance;
private readonly SchoolYearService _sy;
private readonly DashboardSettingsService _dashboardSettings;
private const int OpenExcuseMaxAgeDays = 21;
private const int SupportPlanDueWithinDays = 14;
private const int UpcomingWithinDays = 30;
[ObservableProperty] private string _greeting = "";
[ObservableProperty] private string _currentDate = "";
[ObservableProperty] private string _currentSchoolYear = "";
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
[ObservableProperty] private string _selectedDayLabel = "";
[ObservableProperty] private bool _isDashboardSettingsOpen;
public string CalendarMonthLabel => CalendarMonth.ToString("MMMM yyyy", De);
@@ -43,6 +51,11 @@ public partial class DashboardViewModel : ObservableObject
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
public ObservableCollection<CorrectionProgressItem> OpenCorrections { get; } = [];
public ObservableCollection<DashboardAlertItem> Alerts { get; } = [];
public ObservableCollection<CalendarEventItem> SelectedDayEvents { get; } = [];
public ObservableCollection<DashboardCardOption> DashboardCards { get; } = [];
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
// Navigation-Callback wird von App.axaml.cs verdrahtet
@@ -52,21 +65,40 @@ public partial class DashboardViewModel : ObservableObject
// OnNavigateToGroup (Lerngruppen-Kacheln, Tab "Übersicht"), da der Sprung von einer konkreten
// Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt.
public Action<Guid>? OnNavigateToLesson { get; set; }
public Action<Guid>? OnNavigateToExam { get; set; }
public DashboardCardOption TodayCard => Card("today");
public DashboardCardOption TasksCard => Card("tasks");
public DashboardCardOption CalendarCard => Card("calendar");
public DashboardCardOption ExcusesCard => Card("excuses");
public DashboardCardOption UpcomingCard => Card("upcoming");
public DashboardCardOption CorrectionsCard => Card("corrections");
public DashboardCardOption AlertsCard => Card("alerts");
public DashboardCardOption AttendanceCard => Card("attendance");
public DashboardCardOption SupportCard => Card("support");
public DashboardCardOption GroupsCard => Card("groups");
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
IExamRepository exams, IExamResultRepository examResults, IGradeRepository grades,
IReportGradeRepository reportGrades, IGroupMembershipRepository memberships,
IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
IParticipationRepository participationEntries, IStudentRepository students,
IDocumentationRepository documentation, ITimetableSlotRepository timetableSlots,
PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy)
PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy,
DashboardSettingsService dashboardSettings)
{
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
_participationSessions = participationSessions; _participationEntries = participationEntries;
_students = students; _documentation = documentation;
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
_attendanceBalance = attendanceBalance; _sy = sy;
_attendanceBalance = attendanceBalance; _sy = sy; _dashboardSettings = dashboardSettings;
LoadDashboardCards();
Load();
}
private DashboardCardOption Card(string key) => DashboardCards.First(c => c.Key == key);
private static DateOnly FirstOfMonth(DateTime d) => new(d.Year, d.Month, 1);
private void Load()
@@ -123,6 +155,9 @@ public partial class DashboardViewModel : ObservableObject
LoadOpenExcuses(groups.Values.ToList(), today);
LoadAttendanceWarnings(today);
LoadSupportPlanReviews(today);
LoadUpcomingDates(groups, today);
LoadOpenCorrections(groups, today);
LoadAlerts(groups, today);
}
// ── Fehlzeiten-Warnung (5.2.3) ────────────────────────────────────────────
@@ -173,6 +208,174 @@ public partial class DashboardViewModel : ObservableObject
}
}
// ── Anstehende Termine (9.3) ─────────────────────────────────────────────
private void LoadUpcomingDates(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
{
UpcomingDates.Clear();
var dueBy = today.AddDays(UpcomingWithinDays);
var items = new List<UpcomingDateItem>();
foreach (var group in groups.Values)
foreach (var exam in _exams.GetByGroup(group.Id)
.Where(e => e.Date >= today && e.Date <= dueBy && e.Status == ExamStatus.Planned))
items.Add(new UpcomingDateItem(UpcomingDateKind.Exam, exam.Date, exam.Title,
group.Name, group.Id, null, today));
foreach (var task in _tasks.GetByStatus(WorkTaskStatus.Open)
.Concat(_tasks.GetByStatus(WorkTaskStatus.InProgress))
.Where(t => t.DueDate.HasValue && t.DueDate.Value <= dueBy))
items.Add(new UpcomingDateItem(UpcomingDateKind.Deadline, task.DueDate!.Value,
task.Title, task.GroupId is Guid groupId && groups.TryGetValue(groupId, out var group)
? group.Name : "Aufgabe", task.GroupId, null, today));
foreach (var doc in _documentation.GetAll()
.Where(d => d.Type == DocumentationType.SupportPlan
&& d.SupportData is { Status: SupportStatus.Active, ReviewDate: not null }
&& d.SupportData.ReviewDate.Value <= dueBy))
{
var student = _students.GetById(doc.StudentId);
if (student is not null)
items.Add(new UpcomingDateItem(UpcomingDateKind.SupportPlan,
doc.SupportData!.ReviewDate!.Value, doc.Title, student.FullName,
doc.GroupId, doc.StudentId, today));
}
foreach (var item in items.OrderBy(i => i.Date).ThenBy(i => i.Title).Take(8))
UpcomingDates.Add(item);
}
// ── Offene Korrekturen (9.4) ─────────────────────────────────────────────
private void LoadOpenCorrections(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
{
OpenCorrections.Clear();
foreach (var group in groups.Values)
foreach (var exam in _exams.GetByGroup(group.Id)
.Where(e => e.Status is ExamStatus.Conducted or ExamStatus.Graded)
.OrderBy(e => e.Date))
{
var expected = _memberships.GetByGroup(group.Id)
.Count(m => GroupMembershipService.IsActiveOn(m, exam.Date));
var evaluated = _examResults.GetByExam(exam.Id)
.Count(r => r.Absent || !string.IsNullOrWhiteSpace(r.Grade) || r.Points.Count > 0);
OpenCorrections.Add(new CorrectionProgressItem(exam.Id, group.Id, exam.Title,
group.Name, exam.Date, Math.Min(evaluated, expected), expected, today));
}
}
// ── Auffälligkeiten (9.5) ────────────────────────────────────────────────
private void LoadAlerts(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
{
Alerts.Clear();
foreach (var warning in AttendanceWarnings)
Alerts.Add(new DashboardAlertItem(warning.StudentId, null, warning.StudentName,
"Fehlzeiten", $"Fehlzeitenquote {warning.AbsenceRatePercent:0.#} %", AlertSeverity.High));
foreach (var group in groups.Values)
{
var groupReportGrades = _reportGrades.GetByGroup(group.Id);
foreach (var membership in _memberships.GetByGroup(group.Id)
.Where(m => GroupMembershipService.IsActiveOn(m, today)))
{
var student = _students.GetById(membership.StudentId);
if (student is null) continue;
var values = _grades.GetByStudentAndGroup(student.Id, group.Id)
.OrderBy(g => g.Date)
.Select(g => int.TryParse(g.Value, out var value) ? (int?)value : null)
.Where(v => v.HasValue).Select(v => v!.Value).ToList();
if (values.Count >= 4)
{
var previous = values.TakeLast(4).Take(2).Average();
var recent = values.TakeLast(2).Average();
var declined = group.GradingSystem == GradingSystem.Grades1To6
? recent - previous >= 1.0
: previous - recent >= 3.0;
if (declined)
Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
"Notenabfall", $"{group.Name}: zuletzt {recent:0.0}, zuvor {previous:0.0}",
AlertSeverity.Medium));
}
var latestReport = groupReportGrades
.Where(r => r.StudentId == student.Id)
.OrderByDescending(r => r.UpdatedAt).FirstOrDefault();
var effective = latestReport?.OverrideValue ?? latestReport?.CalculatedValue;
if (int.TryParse(effective, out var reportValue)
&& (group.GradingSystem == GradingSystem.Grades1To6 ? reportValue >= 5 : reportValue <= 4))
Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
"Versetzungsgefährdung", $"{group.Name}: aktueller Stand {reportValue}",
AlertSeverity.High));
}
}
}
// ── Konfigurierbare Kacheln (9.6) ────────────────────────────────────────
private void LoadDashboardCards()
{
DashboardCards.Clear();
foreach (var setting in _dashboardSettings.Load())
{
var option = new DashboardCardOption(setting.Key, CardTitle(setting.Key), setting.IsVisible);
option.OnVisibilityChanged = SaveAndApplyCardLayout;
DashboardCards.Add(option);
}
ApplyCardLayout();
}
private static string CardTitle(string key) => key switch
{
"today" => "Heute", "tasks" => "Offene Aufgaben", "calendar" => "Kalender",
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
"corrections" => "Offene Korrekturen", "alerts" => "Auffälligkeiten",
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
"groups" => "Meine Lerngruppen", _ => key,
};
private void ApplyCardLayout()
{
var visibleIndex = 0;
foreach (var card in DashboardCards)
{
var index = card.IsVisible ? visibleIndex++ : 0;
card.Row = index / 2;
card.Column = index % 2;
}
}
private void SaveAndApplyCardLayout()
{
ApplyCardLayout();
_dashboardSettings.Save(DashboardCards.Select((c, i) => new DashboardCardSetting
{ Key = c.Key, IsVisible = c.IsVisible, Order = i }));
}
[RelayCommand] private void ToggleDashboardSettings() =>
IsDashboardSettingsOpen = !IsDashboardSettingsOpen;
[RelayCommand]
private void MoveCardUp(DashboardCardOption? card)
{
if (card is null) return;
var index = DashboardCards.IndexOf(card);
if (index <= 0) return;
DashboardCards.Move(index, index - 1);
SaveAndApplyCardLayout();
}
[RelayCommand]
private void MoveCardDown(DashboardCardOption? card)
{
if (card is null) return;
var index = DashboardCards.IndexOf(card);
if (index < 0 || index >= DashboardCards.Count - 1) return;
DashboardCards.Move(index, index + 1);
SaveAndApplyCardLayout();
}
[RelayCommand] private void OpenStudentAttendance(AttendanceWarningItem? item)
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
@@ -240,8 +443,8 @@ public partial class DashboardViewModel : ObservableObject
var agg = Agg(lesson.Date);
agg.HasLesson = true;
if (g.IsOwnClass) agg.IsOwnClassDay = true;
agg.Details.Add($"Unterricht: {g.Name}" +
(string.IsNullOrWhiteSpace(lesson.Topic) ? "" : $" {lesson.Topic}"));
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Lesson, lesson.Date,
g.Name, lesson.Topic, g.Id));
}
foreach (var exam in _exams.GetByGroup(g.Id).Where(e => e.Date >= gridStart && e.Date <= gridEnd))
@@ -249,7 +452,8 @@ public partial class DashboardViewModel : ObservableObject
var agg = Agg(exam.Date);
agg.HasExam = true;
if (g.IsOwnClass) agg.IsOwnClassDay = true;
agg.Details.Add($"Klausur: {exam.Title} ({g.Name})");
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Exam, exam.Date,
exam.Title, g.Name, g.Id));
}
}
@@ -261,6 +465,26 @@ public partial class DashboardViewModel : ObservableObject
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.IsOwnClassDay ?? false,
agg?.Details ?? []));
}
SelectCalendarDay(CalendarDays.FirstOrDefault(d => d.Date == today && d.IsCurrentMonth)
?? CalendarDays.First(d => d.IsCurrentMonth));
}
[RelayCommand]
private void SelectCalendarDay(CalendarDayCell? day)
{
if (day is null) return;
foreach (var cell in CalendarDays) cell.IsSelected = cell == day;
SelectedDayLabel = day.Date.ToString("dddd, d. MMMM", De);
SelectedDayEvents.Clear();
foreach (var item in day.Events) SelectedDayEvents.Add(item);
}
[RelayCommand]
private void OpenCalendarEvent(CalendarEventItem? item)
{
if (item is null) return;
if (item.Kind == CalendarEventKind.Exam) OnNavigateToExam?.Invoke(item.GroupId);
else OnNavigateToLesson?.Invoke(item.GroupId);
}
[RelayCommand]
@@ -289,6 +513,19 @@ public partial class DashboardViewModel : ObservableObject
[RelayCommand] private void OpenGroup(GroupChip? c) { if (c is not null) OnNavigateToGroup?.Invoke(c.GroupId); }
[RelayCommand] private void OpenLesson(LessonItem? l) { if (l is not null) OnNavigateToLesson?.Invoke(l.GroupId); }
[RelayCommand] private void OpenUpcomingDate(UpcomingDateItem? item)
{
if (item?.StudentId is Guid studentId) OnNavigateToStudent?.Invoke(studentId);
else if (item?.GroupId is Guid groupId)
{
if (item.Kind == UpcomingDateKind.Exam) OnNavigateToExam?.Invoke(groupId);
else OnNavigateToGroup?.Invoke(groupId);
}
}
[RelayCommand] private void OpenCorrection(CorrectionProgressItem? item)
{ if (item is not null) OnNavigateToExam?.Invoke(item.GroupId); }
[RelayCommand] private void OpenAlert(DashboardAlertItem? item)
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
[RelayCommand] private void Refresh() => Load();
private class DayAgg
@@ -296,7 +533,7 @@ public partial class DashboardViewModel : ObservableObject
public bool HasLesson;
public bool HasExam;
public bool IsOwnClassDay;
public List<string> Details { get; } = [];
public List<CalendarEventItem> Details { get; } = [];
}
}
@@ -369,8 +606,10 @@ public class SupportPlanDueItem
}
}
public class CalendarDayCell
public partial class CalendarDayCell : ObservableObject
{
[ObservableProperty] private bool _isSelected;
public DateOnly Date { get; }
public int DayNumber { get; }
public bool IsCurrentMonth { get; }
public bool IsToday { get; }
@@ -378,16 +617,103 @@ public class CalendarDayCell
public bool HasExam { get; }
public bool IsOwnClassDay { get; }
public string Tooltip { get; }
public IReadOnlyList<CalendarEventItem> Events { get; }
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
bool hasLesson, bool hasExam, bool isOwnClassDay, List<string> details)
bool hasLesson, bool hasExam, bool isOwnClassDay, List<CalendarEventItem> details)
{
Date = date;
DayNumber = date.Day;
IsCurrentMonth = isCurrentMonth;
IsToday = isToday;
HasLesson = hasLesson;
HasExam = hasExam;
IsOwnClassDay = isOwnClassDay;
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy") : string.Join("\n", details);
Events = details;
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy")
: string.Join("\n", details.Select(d => $"{d.KindLabel}: {d.Title}"));
}
}
public enum CalendarEventKind { Lesson, Exam }
public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, string title,
string subtitle, Guid groupId)
{
public CalendarEventKind Kind { get; } = kind;
public DateOnly Date { get; } = date;
public string Title { get; } = title;
public string Subtitle { get; } = subtitle;
public Guid GroupId { get; } = groupId;
public string KindLabel => Kind == CalendarEventKind.Exam ? "Klausur" : "Unterricht";
}
public enum UpcomingDateKind { Exam, SupportPlan, Deadline }
public sealed class UpcomingDateItem(UpcomingDateKind kind, DateOnly date, string title,
string subtitle, Guid? groupId, Guid? studentId, DateOnly today)
{
public UpcomingDateKind Kind { get; } = kind;
public DateOnly Date { get; } = date;
public string Title { get; } = title;
public string Subtitle { get; } = subtitle;
public Guid? GroupId { get; } = groupId;
public Guid? StudentId { get; } = studentId;
public bool IsOverdue { get; } = date < today;
public string DateDisplay => Date.ToString("dd.MM.");
public string KindLabel => Kind switch
{
UpcomingDateKind.Exam => "Klausur",
UpcomingDateKind.SupportPlan => "Förderplan",
_ => "Frist",
};
}
public sealed class CorrectionProgressItem(Guid examId, Guid groupId, string title, string groupName,
DateOnly date, int completed, int total, DateOnly today)
{
public Guid ExamId { get; } = examId;
public Guid GroupId { get; } = groupId;
public string Title { get; } = title;
public string GroupName { get; } = groupName;
public DateOnly Date { get; } = date;
public int Completed { get; } = completed;
public int Total { get; } = total;
public int Percent => Total == 0 ? 0 : (int)Math.Round(Completed * 100.0 / Total);
public string ProgressDisplay => $"{Completed} von {Total} Arbeiten bewertet";
public string DateDisplay => Date.ToString("dd.MM.yyyy");
public bool IsOverdue => Date < today.AddDays(-7) && Completed < Total;
}
public enum AlertSeverity { Medium, High }
public sealed class DashboardAlertItem(Guid studentId, Guid? groupId, string studentName,
string kindLabel, string detail, AlertSeverity severity)
{
public Guid StudentId { get; } = studentId;
public Guid? GroupId { get; } = groupId;
public string StudentName { get; } = studentName;
public string KindLabel { get; } = kindLabel;
public string Detail { get; } = detail;
public AlertSeverity Severity { get; } = severity;
public string SeverityColor => Severity == AlertSeverity.High ? "#D32F2F" : "#F59E0B";
}
public partial class DashboardCardOption : ObservableObject
{
[ObservableProperty] private bool _isVisible;
[ObservableProperty] private int _row;
[ObservableProperty] private int _column;
public string Key { get; }
public string Title { get; }
public Action? OnVisibilityChanged { get; set; }
public DashboardCardOption(string key, string title, bool isVisible)
{
Key = key;
Title = title;
_isVisible = isVisible;
}
partial void OnIsVisibleChanged(bool value) => OnVisibilityChanged?.Invoke();
}
@@ -9,21 +9,56 @@
<Setter Property="Foreground" Value="Red"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Border.daycell.selected">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
</Style>
</UserControl.Styles>
<ScrollViewer Padding="24">
<StackPanel Spacing="20">
<!-- Begrüßung -->
<StackPanel>
<TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/>
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<StackPanel>
<TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/>
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
</StackPanel>
<Button Grid.Column="1" Content="Dashboard anpassen" Command="{Binding ToggleDashboardSettingsCommand}"
VerticalAlignment="Center"/>
</Grid>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto">
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8"
Padding="12" IsVisible="{Binding IsDashboardSettingsOpen}">
<ItemsControl ItemsSource="{Binding DashboardCards}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><WrapPanel Orientation="Horizontal"/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DashboardCardOption">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="1" CornerRadius="6" Padding="8" Margin="4">
<StackPanel Orientation="Horizontal" Spacing="6">
<CheckBox Content="{Binding Title}" IsChecked="{Binding IsVisible}" VerticalAlignment="Center"/>
<Button Content="↑" Padding="6,2"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardUpCommand}"
CommandParameter="{Binding}"/>
<Button Content="↓" Padding="6,2"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardDownCommand}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<!-- Heutige Stunden -->
<Border Grid.Column="0" Grid.Row="0" Margin="0,0,8,8"
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
IsVisible="{Binding TodayCard.IsVisible}" Margin="0,0,8,8"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
@@ -63,7 +98,8 @@
</Border>
<!-- Offene Aufgaben -->
<Border Grid.Column="1" Grid.Row="0" Margin="8,0,0,8"
<Border Grid.Column="{Binding TasksCard.Column}" Grid.Row="{Binding TasksCard.Row}"
IsVisible="{Binding TasksCard.IsVisible}" Margin="8,0,0,8"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
@@ -88,7 +124,8 @@
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
Lerngruppen-Liste darunter ihn nicht nach unten verdrängt. -->
<Border Grid.Column="0" Grid.Row="1" Margin="0,0,8,8" HorizontalAlignment="Left"
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
IsVisible="{Binding CalendarCard.IsVisible}" Margin="0,0,8,8"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel Spacing="8">
@@ -150,20 +187,24 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CalendarDayCell">
<Border Classes="daycell" Classes.ownclass="{Binding IsOwnClassDay}"
Classes.haslesson="{Binding HasLesson}"
Classes.outside="{Binding !IsCurrentMonth}"
Width="32" Height="32" Margin="1" CornerRadius="6"
ToolTip.Tip="{Binding Tooltip}">
<Grid>
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
Text="{Binding DayNumber}" FontSize="12"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Ellipse Width="5" Height="5" Fill="#E53935" Margin="0,0,0,3"
HorizontalAlignment="Center" VerticalAlignment="Bottom"
IsVisible="{Binding HasExam}"/>
</Grid>
</Border>
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="1"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).SelectCalendarDayCommand}"
CommandParameter="{Binding}" ToolTip.Tip="{Binding Tooltip}">
<Border Classes="daycell" Classes.ownclass="{Binding IsOwnClassDay}"
Classes.haslesson="{Binding HasLesson}"
Classes.outside="{Binding !IsCurrentMonth}"
Classes.selected="{Binding IsSelected}"
Width="32" Height="32" CornerRadius="6">
<Grid>
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
Text="{Binding DayNumber}" FontSize="12"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Ellipse Width="5" Height="5" Fill="#E53935" Margin="0,0,0,3"
HorizontalAlignment="Center" VerticalAlignment="Bottom"
IsVisible="{Binding HasExam}"/>
</Grid>
</Border>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -183,11 +224,35 @@
<TextBlock Text="Meine Klasse" FontSize="10" Opacity="0.6"/>
</StackPanel>
</StackPanel>
<Separator Margin="0,4"/>
<TextBlock Text="{Binding SelectedDayLabel}" FontWeight="SemiBold" FontSize="12"/>
<ItemsControl ItemsSource="{Binding SelectedDayEvents}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CalendarEventItem">
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,3"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenCalendarEventCommand}"
CommandParameter="{Binding}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding KindLabel}" FontSize="10" Opacity="0.55" Width="58"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Title}" FontSize="12" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Subtitle}" FontSize="10" Opacity="0.6"/>
</StackPanel>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine Termine an diesem Tag." Classes="emptyhint"
IsVisible="{Binding !SelectedDayEvents.Count}"/>
</StackPanel>
</Border>
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
<Border Grid.Column="1" Grid.Row="1" Margin="8,0,0,8" VerticalAlignment="Top"
<Border Grid.Column="{Binding ExcusesCard.Column}" Grid.Row="{Binding ExcusesCard.Row}"
IsVisible="{Binding ExcusesCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
@@ -220,7 +285,8 @@
</Border>
<!-- Fehlzeiten-Warnung (5.2.3) -->
<Border Grid.Column="0" Grid.Row="2" Margin="0,0,8,8" VerticalAlignment="Top"
<Border Grid.Column="{Binding AttendanceCard.Column}" Grid.Row="{Binding AttendanceCard.Row}"
IsVisible="{Binding AttendanceCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
@@ -249,7 +315,8 @@
</Border>
<!-- Förderplan-Wiedervorlage (5.3.2) -->
<Border Grid.Column="1" Grid.Row="2" Margin="8,0,0,8" VerticalAlignment="Top"
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
IsVisible="{Binding SupportCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
@@ -278,8 +345,113 @@
</StackPanel>
</Border>
<!-- Meine Lerngruppen: wächst mit der Zeit, deshalb ganz unten und volle Breite -->
<Border Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2"
<!-- Anstehende Termine (9.3) -->
<Border Grid.Column="{Binding UpcomingCard.Column}" Grid.Row="{Binding UpcomingCard.Row}"
IsVisible="{Binding UpcomingCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="ANSTEHENDE TERMINE" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding UpcomingDates}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:UpcomingDateItem">
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,4"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenUpcomingDateCommand}"
CommandParameter="{Binding}">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Background="{DynamicResource SystemAccentColorLight2}" CornerRadius="4"
Padding="5,2" VerticalAlignment="Center">
<TextBlock Text="{Binding KindLabel}" FontSize="10"/>
</Border>
<StackPanel Grid.Column="1" Margin="8,0">
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding Subtitle}" FontSize="11" Opacity="0.6"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding DateDisplay}" FontSize="12"
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine Termine in den nächsten 30 Tagen." Classes="emptyhint"
IsVisible="{Binding !UpcomingDates.Count}"/>
</StackPanel>
</Border>
<!-- Offene Korrekturen (9.4) -->
<Border Grid.Column="{Binding CorrectionsCard.Column}" Grid.Row="{Binding CorrectionsCard.Row}"
IsVisible="{Binding CorrectionsCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="OFFENE KORREKTUREN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding OpenCorrections}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CorrectionProgressItem">
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,5"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenCorrectionCommand}"
CommandParameter="{Binding}">
<StackPanel Spacing="3">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"/>
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="11" Opacity="0.6"/>
</Grid>
<TextBlock Text="{Binding GroupName}" FontSize="11" Opacity="0.6"/>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Percent}" Height="6"/>
<TextBlock Text="{Binding ProgressDisplay}" FontSize="10" Opacity="0.65"/>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine offenen Korrekturen." Classes="emptyhint"
IsVisible="{Binding !OpenCorrections.Count}"/>
</StackPanel>
</Border>
<!-- Auffälligkeiten (9.5) -->
<Border Grid.Column="{Binding AlertsCard.Column}" Grid.Row="{Binding AlertsCard.Row}"
IsVisible="{Binding AlertsCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="AUFFÄLLIGKEITEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding Alerts}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DashboardAlertItem">
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,4"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenAlertCommand}"
CommandParameter="{Binding}">
<Grid ColumnDefinitions="4,*">
<Border Background="{Binding SeverityColor}" CornerRadius="2" Margin="0,0,9,0"/>
<StackPanel Grid.Column="1">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"/>
<TextBlock Grid.Column="1" Text="{Binding KindLabel}" FontSize="10" Opacity="0.6"/>
</Grid>
<TextBlock Text="{Binding Detail}" FontSize="11" Opacity="0.65"/>
</StackPanel>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine Auffälligkeiten erkannt." Classes="emptyhint"
IsVisible="{Binding !Alerts.Count}"/>
</StackPanel>
</Border>
<!-- Meine Lerngruppen -->
<Border Grid.Column="{Binding GroupsCard.Column}" Grid.Row="{Binding GroupsCard.Row}"
IsVisible="{Binding GroupsCard.IsVisible}"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
@@ -117,31 +117,36 @@
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="📝 Klausuren"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="3"/>
CommandParameter="4"/>
<Button Content="🔢 Notenübersicht"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="4"/>
CommandParameter="5"/>
<Button Content="📅 Unterrichtsplanung"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="5"/>
CommandParameter="6"/>
<Button Content="🎯 Kompetenzübersicht"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="6"/>
CommandParameter="7"/>
<Button Content="📋 Dokumentation"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="7"/>
CommandParameter="8"/>
</StackPanel>
</StackPanel>