This commit is contained in:
@@ -379,35 +379,42 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
continue;
|
||||
}
|
||||
|
||||
var events = new List<ClassTeacherCalendarEvent>();
|
||||
foreach (var absence in absences.Where(a => a.Date == date).OrderBy(a => a.StudentDisplayName))
|
||||
{
|
||||
var (code, label, color, kind) = absence.IsUnexcused
|
||||
? ("U", "Unentschuldigt gefehlt", "#C62828", ClassTeacherCalendarEventKind.Unexcused)
|
||||
: absence.IsLate
|
||||
? ("V", "Verspätet", "#EF6C00", ClassTeacherCalendarEventKind.Late)
|
||||
: absence.FriendlyStatusLabel.Contains("Entschuldigt", StringComparison.OrdinalIgnoreCase)
|
||||
? ("E", "Entschuldigt gefehlt", "#2E7D32", ClassTeacherCalendarEventKind.Excused)
|
||||
: ("A", "Abwesend", "#1565C0", ClassTeacherCalendarEventKind.Absent);
|
||||
events.Add(new ClassTeacherCalendarEvent(absence.StudentDisplayName, code, label, color, kind));
|
||||
}
|
||||
|
||||
foreach (var entry in registerEntries.Where(e => e.Date == date).OrderBy(e => e.StudentDisplayName))
|
||||
{
|
||||
var isHomework = ContainsHomework(entry);
|
||||
if (isHomework && !includeHomework) continue;
|
||||
if (!entry.IsDangerStatus && !isHomework) continue;
|
||||
events.Add(new ClassTeacherCalendarEvent(entry.StudentDisplayName,
|
||||
isHomework ? "H" : "!", isHomework ? "Hausaufgaben" : "Negativer Klassenbucheintrag",
|
||||
isHomework ? "#6A1B9A" : "#8E24AA",
|
||||
isHomework ? ClassTeacherCalendarEventKind.Homework : ClassTeacherCalendarEventKind.ClassRegister));
|
||||
}
|
||||
|
||||
result.Add(new ClassTeacherCalendarDay(date, events));
|
||||
result.Add(new ClassTeacherCalendarDay(date,
|
||||
BuildDayEvents(date, absences, registerEntries, includeHomework)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ClassTeacherCalendarEvent> BuildDayEvents(DateOnly date,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> absences,
|
||||
IReadOnlyList<ClassTeacherClassRegisterRow> registerEntries, bool includeHomework)
|
||||
{
|
||||
var events = new List<ClassTeacherCalendarEvent>();
|
||||
foreach (var absence in absences.Where(a => a.Date == date).OrderBy(a => a.StudentDisplayName))
|
||||
{
|
||||
var (code, label, color, kind) = absence.IsUnexcused
|
||||
? ("U", "Unentschuldigt gefehlt", "#C62828", ClassTeacherCalendarEventKind.Unexcused)
|
||||
: absence.IsLate
|
||||
? ("V", "Verspätet", "#EF6C00", ClassTeacherCalendarEventKind.Late)
|
||||
: absence.FriendlyStatusLabel.Contains("Entschuldigt", StringComparison.OrdinalIgnoreCase)
|
||||
? ("E", "Entschuldigt gefehlt", "#2E7D32", ClassTeacherCalendarEventKind.Excused)
|
||||
: ("A", "Abwesend", "#1565C0", ClassTeacherCalendarEventKind.Absent);
|
||||
events.Add(new ClassTeacherCalendarEvent(absence.StudentDisplayName, code, label, color, kind));
|
||||
}
|
||||
|
||||
foreach (var entry in registerEntries.Where(e => e.Date == date).OrderBy(e => e.StudentDisplayName))
|
||||
{
|
||||
var isHomework = ContainsHomework(entry);
|
||||
if (isHomework && !includeHomework) continue;
|
||||
if (!entry.IsDangerStatus && !isHomework) continue;
|
||||
events.Add(new ClassTeacherCalendarEvent(entry.StudentDisplayName,
|
||||
isHomework ? "H" : "!", isHomework ? "Hausaufgaben" : "Negativer Klassenbucheintrag",
|
||||
isHomework ? "#6A1B9A" : "#8E24AA",
|
||||
isHomework ? ClassTeacherCalendarEventKind.Homework : ClassTeacherCalendarEventKind.ClassRegister));
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
private static bool ContainsHomework(ClassTeacherClassRegisterRow entry) =>
|
||||
entry.CategoryName?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
entry.Text?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
@@ -392,6 +392,14 @@ public sealed record ClassTeacherOpenExcuseRow(string StudentName, DateOnly Date
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Eine einzelne, auf ein Minimum verdichtete Monatszelle im Übersichts-Widget.</summary>
|
||||
public sealed record ClassTeacherCompactCalendarDay(DateOnly Date, string SignalCode,
|
||||
string SignalColorHex, string Tooltip, bool IsToday)
|
||||
{
|
||||
public string DayNumber => Date.Day.ToString();
|
||||
public bool HasSignal => !string.IsNullOrEmpty(SignalCode);
|
||||
}
|
||||
|
||||
public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
private readonly WebUntisSettingsService _settings;
|
||||
@@ -415,6 +423,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
public ObservableCollection<ClassTeacherTrendDay> TrendDays { get; } = [];
|
||||
public ObservableCollection<ClassTeacherPatternNotice> PatternNotices { get; } = [];
|
||||
public ObservableCollection<ClassTeacherOpenExcuseRow> OpenExcuses { get; } = [];
|
||||
public ObservableCollection<ClassTeacherCompactCalendarDay> CompactMonthDays { get; } = [];
|
||||
|
||||
[ObservableProperty] private string? _homeroomClassName;
|
||||
[ObservableProperty] private int _activeTabIndex;
|
||||
@@ -492,6 +501,8 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
public double UnexcusedAbsenceFraction => StudentCount == 0 ? 0 : (double)UnexcusedAbsenceCount / StudentCount;
|
||||
public string DayOverviewTooltip => $"{PresentCount} anwesend · {LateCount} verspätet · " +
|
||||
$"{ExcusedAbsenceCount} entschuldigt · {UnexcusedAbsenceCount} unentschuldigt";
|
||||
public string CompactMonthLabel => DateOnly.FromDateTime(DateTime.Today).ToString("MMMM yyyy",
|
||||
System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
|
||||
|
||||
public Func<Task>? OnNavigateToSettings { get; set; }
|
||||
public Func<Task>? OnNavigateToWorkload { get; set; }
|
||||
@@ -537,7 +548,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
ActiveTabIndex = 0;
|
||||
Roster.Clear(); PrimaryRoster.Clear(); SecondaryRoster.Clear(); TrendDays.Clear(); PatternNotices.Clear();
|
||||
OpenExcuses.Clear(); OpenExcuseOverflowCount = 0;
|
||||
OpenExcuses.Clear(); CompactMonthDays.Clear(); OpenExcuseOverflowCount = 0;
|
||||
}
|
||||
|
||||
// Wird beim Navigieren auf diese Seite aufgerufen statt LoadCommand: baut nur den lokalen
|
||||
@@ -676,6 +687,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
UnexcusedAbsenceCount = Roster.Count(r => r.HasAbsenceToday && !r.IsLate && r.IsUnexcused);
|
||||
RecentClassRegisterCount = recentRegisterEntries.Count;
|
||||
BuildTrend(absenceDaysYear, trendDays);
|
||||
BuildCompactMonth(absenceDaysYear, classRegisterEvents, today);
|
||||
BuildPatternNotices(absenceDaysYear, sevenDayStart);
|
||||
BuildWeekdayPatternNotices(absenceDaysYear);
|
||||
BuildAttendanceParticipationNotices();
|
||||
@@ -708,6 +720,64 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
DetailsTab.LoadCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenMonthlyCalendar()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
DetailsTab.StudentFilter = "";
|
||||
DetailsTab.CalendarMonth = new DateOnly(today.Year, today.Month, 1);
|
||||
ActiveTabIndex = 2;
|
||||
DetailsTab.ShowCalendarCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void BuildCompactMonth(IReadOnlyList<ClassAbsenceDaySummaryRow> absences,
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries, DateOnly today)
|
||||
{
|
||||
CompactMonthDays.Clear();
|
||||
foreach (var day in BuildCompactMonthDays(new DateOnly(today.Year, today.Month, 1),
|
||||
absences, registerEntries, today))
|
||||
CompactMonthDays.Add(day);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ClassTeacherCompactCalendarDay> BuildCompactMonthDays(DateOnly month,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> absences,
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries, DateOnly today)
|
||||
{
|
||||
var first = new DateOnly(month.Year, month.Month, 1);
|
||||
var registerRows = registerEntries
|
||||
.Select(e => (Entry: e, Valid: TryDate(e.Date, out var date), Date: date))
|
||||
.Where(x => x.Valid && x.Date.Year == first.Year && x.Date.Month == first.Month)
|
||||
.Select(x => new ClassTeacherClassRegisterRow(x.Date, x.Entry.Subject, x.Entry.StudentName,
|
||||
x.Entry.TeacherUsername, x.Entry.CategoryName, x.Entry.CategoryGroup, x.Entry.Text))
|
||||
.ToList();
|
||||
var result = new List<ClassTeacherCompactCalendarDay>();
|
||||
var days = DateTime.DaysInMonth(first.Year, first.Month);
|
||||
|
||||
for (var number = 1; number <= days; number++)
|
||||
{
|
||||
var date = new DateOnly(first.Year, first.Month, number);
|
||||
var events = ClassTeacherDetailsViewModel.BuildDayEvents(date, absences, registerRows,
|
||||
includeHomework: false);
|
||||
var strongest = events.OrderBy(e => SignalPriority(e.Kind)).FirstOrDefault();
|
||||
var tooltip = events.Count == 0
|
||||
? $"{date:dd.MM.yyyy}: unauffällig"
|
||||
: $"{date:dd.MM.yyyy}\n" + string.Join("\n", events.Select(e => e.Tooltip));
|
||||
result.Add(new ClassTeacherCompactCalendarDay(date, strongest?.Code ?? "",
|
||||
strongest?.ColorHex ?? "#00000000", tooltip, date == today));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int SignalPriority(ClassTeacherCalendarEventKind kind) => kind switch
|
||||
{
|
||||
ClassTeacherCalendarEventKind.Unexcused => 0,
|
||||
ClassTeacherCalendarEventKind.ClassRegister => 1,
|
||||
ClassTeacherCalendarEventKind.Absent => 2,
|
||||
ClassTeacherCalendarEventKind.Late => 3,
|
||||
ClassTeacherCalendarEventKind.Excused => 4,
|
||||
_ => 5,
|
||||
};
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowDetailsForStudent(ClassTeacherRosterRow? row)
|
||||
{
|
||||
|
||||
@@ -98,6 +98,19 @@
|
||||
<Style Selector="Border.statusFill.danger">
|
||||
<Setter Property="Background" Value="{DynamicResource AppStatusDangerBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Border.compactDay">
|
||||
<Setter Property="Width" Value="28"/>
|
||||
<Setter Property="Height" Value="28"/>
|
||||
<Setter Property="CornerRadius" Value="6"/>
|
||||
<Setter Property="Background" Value="{DynamicResource AppChipBackgroundBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppCardBorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Margin" Value="2"/>
|
||||
</Style>
|
||||
<Style Selector="Border.compactDay.today">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppAccentTextBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<UserControl.Resources>
|
||||
@@ -409,6 +422,48 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Verdichtete Monatsansicht: exakt eine Zelle je Kalendertag. Ohne Ereignis
|
||||
bleibt nur die Tageszahl sichtbar; bei Auffälligkeiten ersetzt das stärkste
|
||||
Statuskürzel die Zahl. Alle Details bleiben per Tooltip und Drill-down erhalten. -->
|
||||
<Border Classes="sidePanel">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Monat auf einen Blick" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding CompactMonthLabel}" FontSize="11" Opacity="0.55"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Öffnen ›" Command="{Binding OpenMonthlyCalendarCommand}"
|
||||
Background="Transparent" BorderThickness="0" Padding="6,2"
|
||||
Foreground="{DynamicResource AppAccentTextBrush}"/>
|
||||
</Grid>
|
||||
<ItemsControl ItemsSource="{Binding CompactMonthDays}" HorizontalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><UniformGrid Columns="7"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ClassTeacherCompactCalendarDay">
|
||||
<Border Classes="compactDay" Classes.today="{Binding IsToday}"
|
||||
ToolTip.Tip="{Binding Tooltip}">
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding DayNumber}" FontSize="11" Opacity="0.7"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="{Binding !HasSignal}"/>
|
||||
<Border Background="{Binding SignalColorHex}" CornerRadius="5"
|
||||
IsVisible="{Binding HasSignal}">
|
||||
<TextBlock Text="{Binding SignalCode}" Foreground="White" FontSize="11"
|
||||
FontWeight="Bold" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="U unentsch. · ! Klassenbuch · A abwesend · V verspätet · E entsch."
|
||||
FontSize="9" Opacity="0.5" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="sidePanel" IsVisible="{Binding HasOpenExcuses}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Offene Entschuldigungen" FontSize="14" FontWeight="SemiBold"/>
|
||||
|
||||
Reference in New Issue
Block a user