refactor: Dashboard-Handlungsbedarf zu einer Karte zusammenfassen

Fasst die sieben Kacheln, die tatsaechlich "wo muss ich reagieren" beantworten
(Entschuldigungen, Fehlzeiten-Warnung, Foerderplan-Wiedervorlage, Korrekturen,
ungeplante Stunden, Auffaelligkeiten, Unterrichtszeit-Nacherfassung), zu einer
Karte "Handlungsbedarf" mit Filter-Chips zusammen statt sieben eigener
Sichtbarkeits-Schalter.

Neues gemeinsames Modell AttentionItem/AttentionGroup/AttentionAction traegt nur
Anzeigedaten - die Datenbeschaffung bleibt unveraendert in den jeweiligen
DashboardViewModel.LoadXxx-Methoden, die am Ende ein AttentionItem statt ihrer
eigenen Item-Klasse erzeugen. RebuildAttention() gruppiert nach Art (feste
Reihenfolge) und wendet Filter an, ohne die Repos erneut abzufragen.

SupportPlanDueItem, CorrectionProgressItem, UnplannedLessonItem und
DashboardAlertItem entfallen (nur dashboard-intern verwendet); OpenExcuseItem
und AttendanceWarningItem bleiben bestehen, da GroupOverviewViewModel sie
weiterhin nutzt. Fuenf Navigations-Commands wurden durch ein einziges
OpenAttentionItemCommand ersetzt.

Bewusste Verhaltensaenderung: LoadAlerts dupliziert Fehlzeiten-Ueberschreitungen
nicht mehr in die Auffaelligkeiten-Gruppe, da dieselbe Zahl sonst zweimal in
derselben Karte erschiene (vorher durch zwei getrennte Kacheln nicht sichtbar).

Details und Testanpassungen siehe TODO.md, Abschnitt 9 (Dashboard).
This commit is contained in:
2026-09-11 00:40:58 +02:00
parent c3ce1a7204
commit 6af4bee1f0
6 changed files with 447 additions and 367 deletions
@@ -20,6 +20,28 @@
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
</Style>
<!-- Handlungsbedarf-Karte: Schweregrad-Streifen (nur Auffälligkeiten) und die rot
hervorgehobene Fehlzeitenquote — ersetzen die früheren fest kodierten Hex-Farben
(#D32F2F/#F59E0B, Foreground="Red") durch die validierte Status-Palette. -->
<Style Selector="Border.severitybar">
<Setter Property="Width" Value="4"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="Border.severitybar.high">
<Setter Property="Background" Value="{DynamicResource AppStatusDangerBrush}"/>
</Style>
<Style Selector="Border.severitybar.medium">
<Setter Property="Background" Value="{DynamicResource AppStatusWarningBrush}"/>
</Style>
<Style Selector="TextBlock.attentionTrailing">
<Setter Property="FontSize" Value="10"/>
<Setter Property="Opacity" Value="0.6"/>
</Style>
<Style Selector="TextBlock.attentionTrailing.warning">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Opacity" Value="1"/>
<Setter Property="Foreground" Value="{DynamicResource AppStatusDangerBrush}"/>
</Style>
</UserControl.Styles>
<ScrollViewer Padding="24">
@@ -142,7 +164,8 @@
<TextBlock Text="HEUTE UND HANDLUNGSBEDARF" FontSize="11" FontWeight="Bold" Opacity="0.5"
Margin="2,2,0,-8"/>
<Grid ColumnDefinitions="3*,2*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<!-- 7 Kacheln (today/tasks/calendar/examload/upcoming/groups/attention) auf 2 Spalten -> 4 Zeilen. -->
<Grid ColumnDefinitions="3*,2*" RowDefinitions="Auto,Auto,Auto,Auto">
<!-- Heutige Stunden -->
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
@@ -366,70 +389,6 @@
</StackPanel>
</Border>
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
<Border Grid.Column="{Binding ExcusesCard.Column}" Grid.Row="{Binding ExcusesCard.Row}"
IsVisible="{Binding ExcusesCard.EffectiveIsVisible}" Margin="{Binding ExcusesCard.Margin}" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="OFFENE ENTSCHULDIGUNGEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding OpenExcuses}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:OpenExcuseItem">
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,4">
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<TextBlock FontSize="11" Opacity="0.6">
<Run Text="{Binding GroupName}"/>
<Run Text=" · "/>
<Run Text="{Binding DateDisplay}"/>
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Content="Entschuldigt" FontSize="11" Padding="7,3"
Command="{Binding MarkExcusedCommand}" Margin="0,0,4,0"/>
<Button Grid.Column="2" Content="Unentschuldigt" FontSize="11" Padding="7,3"
Command="{Binding MarkUnexcusedCommand}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine offenen Entschuldigungen." Classes="emptyhint"
IsVisible="{Binding !OpenExcuses.Count}"/>
</StackPanel>
</Border>
<!-- Fehlzeiten-Warnung (5.2.3) -->
<Border Grid.Column="{Binding AttendanceCard.Column}" Grid.Row="{Binding AttendanceCard.Row}"
IsVisible="{Binding AttendanceCard.EffectiveIsVisible}" Margin="{Binding AttendanceCard.Margin}" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="FEHLZEITEN-WARNUNG" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding AttendanceWarnings}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:AttendanceWarningItem">
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
<Button Grid.Column="0" Content="{Binding StudentName}" FontSize="13"
HorizontalAlignment="Left" HorizontalContentAlignment="Left"
Background="Transparent" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenStudentAttendanceCommand}"
CommandParameter="{Binding}"/>
<TextBlock Grid.Column="1" Foreground="Red" FontSize="12" VerticalAlignment="Center">
<Run Text="{Binding AbsenceRatePercent}"/>
<Run Text=" %"/>
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine Fehlzeiten über dem Schwellenwert." Classes="emptyhint"
IsVisible="{Binding !AttendanceWarnings.Count}"/>
</StackPanel>
</Border>
<!-- Klausurwochen (Nutzer-Feedback): eigene Klausurlast über alle Kurse hinweg -->
<Border Grid.Column="{Binding ExamLoadCard.Column}" Grid.Row="{Binding ExamLoadCard.Row}"
IsVisible="{Binding ExamLoadCard.EffectiveIsVisible}" Margin="{Binding ExamLoadCard.Margin}" VerticalAlignment="Top"
@@ -454,63 +413,6 @@
</StackPanel>
</Border>
<!-- Unterrichtszeit nacherfassen (Nutzer-Feedback) -->
<Border Grid.Column="{Binding MissingTeachingTimeCard.Column}" Grid.Row="{Binding MissingTeachingTimeCard.Row}"
IsVisible="{Binding MissingTeachingTimeCard.EffectiveIsVisible}" Margin="{Binding MissingTeachingTimeCard.Margin}" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="UNTERRICHTSZEIT NACHERFASSEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding MissingTeachingTimeEntries}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:MissingTeachingTimeItem">
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="13"
VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Erfassen" FontSize="12" Padding="10,4"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).AddMissingTeachingTimeCommand}"
CommandParameter="{Binding}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine fehlende Unterrichtszeit-Erfassung." Classes="emptyhint"
IsVisible="{Binding !MissingTeachingTimeEntries.Count}"/>
</StackPanel>
</Border>
<!-- Förderplan-Wiedervorlage (5.3.2) -->
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
IsVisible="{Binding SupportCard.EffectiveIsVisible}" Margin="{Binding SupportCard.Margin}" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="FÖRDERPLAN-WIEDERVORLAGE" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding SupportPlanReviews}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:SupportPlanDueItem">
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
<StackPanel Grid.Column="0">
<Button Content="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"
HorizontalAlignment="Left" HorizontalContentAlignment="Left"
Background="Transparent" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenStudentSupportPlanCommand}"
CommandParameter="{Binding}"/>
<TextBlock Text="{Binding Title}" FontSize="11" Opacity="0.6"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding ReviewDateDisplay}" FontSize="12"
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine fälligen Überprüfungen." Classes="emptyhint"
IsVisible="{Binding !SupportPlanReviews.Count}"/>
</StackPanel>
</Border>
<!-- Anstehende Termine (9.3) -->
<Border Grid.Column="{Binding UpcomingCard.Column}" Grid.Row="{Binding UpcomingCard.Row}"
IsVisible="{Binding UpcomingCard.EffectiveIsVisible}" Margin="{Binding UpcomingCard.Margin}" VerticalAlignment="Top"
@@ -548,100 +450,101 @@
</StackPanel>
</Border>
<!-- Offene Korrekturen (9.4) -->
<Border Grid.Column="{Binding CorrectionsCard.Column}" Grid.Row="{Binding CorrectionsCard.Row}"
IsVisible="{Binding CorrectionsCard.EffectiveIsVisible}" Margin="{Binding CorrectionsCard.Margin}" VerticalAlignment="Top"
<!-- Handlungsbedarf: zusammengefasste Karte für die frueheren sieben Kacheln
(Entschuldigungen, Fehlzeiten, Foerderplan, Korrekturen, ungeplante Stunden,
Auffaelligkeiten, Unterrichtszeit-Nacherfassung) — siehe AttentionItem.cs.
Filter-Chips ersetzen die vorherige Sichtbarkeit je Einzelkachel. x:Name traegt den
Weg zum DashboardViewModel durch zwei ItemsControl-Verschachtelungen (Gruppe -> Item)
hindurch, ohne $parent[ItemsControl] mit Ancestor-Level zaehlen zu muessen. -->
<Border Grid.Column="{Binding AttentionCard.Column}" Grid.Row="{Binding AttentionCard.Row}"
IsVisible="{Binding AttentionCard.EffectiveIsVisible}" Margin="{Binding AttentionCard.Margin}" 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>
<StackPanel Spacing="10">
<TextBlock Text="HANDLUNGSBEDARF" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
<!-- Ungeplante Stunden -->
<Border Grid.Column="{Binding UnplannedCard.Column}" Grid.Row="{Binding UnplannedCard.Row}"
IsVisible="{Binding UnplannedCard.EffectiveIsVisible}" Margin="{Binding UnplannedCard.Margin}" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="UNGEPLANTE STUNDEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding UnplannedLessons}">
<ItemsControl ItemsSource="{Binding AttentionFilters}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><WrapPanel Orientation="Horizontal" ItemSpacing="6" LineSpacing="6"/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:UnplannedLessonItem">
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,5"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenUnplannedLessonCommand}"
CommandParameter="{Binding}">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding Display}" FontSize="13"
TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="12"
VerticalAlignment="Center" Opacity="0.6"/>
</Grid>
</Button>
<DataTemplate x:DataType="vm:AttentionFilterOption">
<ToggleButton Content="{Binding Label}" IsChecked="{Binding IsActive}"
FontSize="11" Padding="8,3"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine ungeplanten Stunden." Classes="emptyhint"
IsVisible="{Binding !UnplannedLessons.Count}"/>
</StackPanel>
</Border>
<!-- Auffälligkeiten (9.5) -->
<Border Grid.Column="{Binding AlertsCard.Column}" Grid.Row="{Binding AlertsCard.Row}"
IsVisible="{Binding AlertsCard.EffectiveIsVisible}" Margin="{Binding AlertsCard.Margin}" 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 x:Name="AttentionGroupsList" ItemsSource="{Binding Attention}">
<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 x:DataType="vm:AttentionGroup">
<StackPanel Margin="0,6,0,0" Spacing="4">
<TextBlock FontSize="11" FontWeight="SemiBold" Opacity="0.7">
<Run Text="{Binding Header}"/>
<Run Text=" ("/>
<Run Text="{Binding Count}"/>
<Run Text=")"/>
</TextBlock>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:AttentionItem">
<StackPanel Margin="0,4">
<Grid ColumnDefinitions="4,*">
<Border Grid.Column="0" Classes="severitybar"
Classes.high="{Binding IsHighSeverity}"
Classes.medium="{Binding IsMediumSeverity}" Margin="0,0,9,0"/>
<!-- Bewusst immer als Button (statt je nach IsClickable ein anderes Root-Element):
ein einheitliches Template für alle sieben Arten. Bei Excuse/MissingTeachingTime
ist Navigate null, der Klick auf die Zeile selbst ist dann folgenlos — die
eigentliche Aktion liegt dort in Actions (Entschuldigt/Erfassen) darunter. -->
<Button Grid.Column="1" Background="Transparent" BorderThickness="0" Padding="0"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{Binding #AttentionGroupsList.((vm:DashboardViewModel)DataContext).OpenAttentionItemCommand}"
CommandParameter="{Binding}">
<StackPanel Spacing="3">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="1" Text="{Binding TrailingText}"
Classes="attentionTrailing" Classes.warning="{Binding IsWarningTrailing}"
VerticalAlignment="Center" Margin="8,0,0,0"
IsVisible="{Binding HasTrailingText}"/>
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="12"
VerticalAlignment="Center" Margin="8,0,0,0"
Classes.overdue="{Binding IsOverdue}"
IsVisible="{Binding HasDate}"/>
</Grid>
<TextBlock Text="{Binding Subtitle}" FontSize="11" Opacity="0.6"
IsVisible="{Binding HasSubtitle}"/>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding ProgressPercent}"
Height="6" IsVisible="{Binding HasProgress}"/>
<TextBlock Text="{Binding ProgressLabel}" FontSize="10" Opacity="0.65"
IsVisible="{Binding HasProgress}"/>
</StackPanel>
</Button>
</Grid>
<ItemsControl ItemsSource="{Binding Actions}" Margin="13,4,0,0"
IsVisible="{Binding HasActions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="4"/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:AttentionAction">
<Button Content="{Binding Label}" FontSize="11" Padding="7,3"
Command="{Binding Command}" CommandParameter="{Binding Parameter}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine Auffälligkeiten erkannt." Classes="emptyhint"
IsVisible="{Binding !Alerts.Count}"/>
<TextBlock Text="Keine offenen Punkte." Classes="emptyhint"
IsVisible="{Binding !Attention.Count}"/>
</StackPanel>
</Border>