feat: Fehlzeiten Calender
This commit is contained in:
@@ -18,6 +18,10 @@ public enum ClassTeacherStatusKind { Ok, Info, Warning, Danger }
|
||||
/// Schlagwort gezielt dämpfen kann, siehe <see cref="ClassTeacherRosterRow.MatchDomains"/>.</summary>
|
||||
public enum VorgangScoreDomain { Attendance, Lateness, Classbook }
|
||||
|
||||
/// <summary>Auswahlbereich des kompakten Monatskalenders. Ein leerer Schülername steht für die
|
||||
/// bisherige Klassen-Gesamtansicht.</summary>
|
||||
public sealed record ClassTeacherCalendarScope(string? StudentName, string DisplayName);
|
||||
|
||||
public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday,
|
||||
string? AbsenceTooltip, bool HasRecentClassRegisterEntry)
|
||||
{
|
||||
@@ -424,6 +428,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
public ObservableCollection<ClassTeacherPatternNotice> PatternNotices { get; } = [];
|
||||
public ObservableCollection<ClassTeacherOpenExcuseRow> OpenExcuses { get; } = [];
|
||||
public ObservableCollection<ClassTeacherCompactCalendarDay> CompactMonthDays { get; } = [];
|
||||
public ObservableCollection<ClassTeacherCalendarScope> CalendarScopes { get; } = [];
|
||||
|
||||
[ObservableProperty] private string? _homeroomClassName;
|
||||
[ObservableProperty] private int _activeTabIndex;
|
||||
@@ -453,6 +458,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
/// den "Klassenbuch öffnen"-Button gehängt statt in einer eigenen Kennzahlkarte.
|
||||
[ObservableProperty] private int _ownDocumentationFollowUpCount;
|
||||
[ObservableProperty] private int _ownDocumentationCriticalCount;
|
||||
[ObservableProperty] private ClassTeacherCalendarScope? _selectedCalendarScope;
|
||||
|
||||
// Zuletzt per Load() geholte WebUntis-Rohdaten, für RefreshFromLocalDataOnly() - damit ein
|
||||
// eingehendes Sync-Ereignis die Ansicht neu aufbauen kann, ohne selbst WebUntis anzufragen.
|
||||
@@ -501,8 +507,17 @@ 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 string CompactMonthLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
var month = DateOnly.FromDateTime(DateTime.Today).ToString("MMMM yyyy",
|
||||
System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
|
||||
return SelectedCalendarScope?.StudentName is { Length: > 0 }
|
||||
? $"{month} · {SelectedCalendarScope.DisplayName}"
|
||||
: $"{month} · gesamte Klasse";
|
||||
}
|
||||
}
|
||||
|
||||
public Func<Task>? OnNavigateToSettings { get; set; }
|
||||
public Func<Task>? OnNavigateToWorkload { get; set; }
|
||||
@@ -534,6 +549,11 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
partial void OnOpenExcuseOverflowCountChanged(int value) => OnPropertyChanged(nameof(HasOpenExcuseOverflow));
|
||||
partial void OnOwnDocumentationFollowUpCountChanged(int value) => OnPropertyChanged(nameof(HasOwnDocumentationAlerts));
|
||||
partial void OnOwnDocumentationCriticalCountChanged(int value) => OnPropertyChanged(nameof(HasOwnDocumentationAlerts));
|
||||
partial void OnSelectedCalendarScopeChanged(ClassTeacherCalendarScope? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(CompactMonthLabel));
|
||||
if (CalendarScopes.Count > 0) RebuildCompactMonthFromCachedData();
|
||||
}
|
||||
partial void OnSearchTextChanged(string value) => ApplyRosterFilter();
|
||||
partial void OnSelectedRosterFilterChanged(int value)
|
||||
{
|
||||
@@ -548,7 +568,8 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
ActiveTabIndex = 0;
|
||||
Roster.Clear(); PrimaryRoster.Clear(); SecondaryRoster.Clear(); TrendDays.Clear(); PatternNotices.Clear();
|
||||
OpenExcuses.Clear(); CompactMonthDays.Clear(); OpenExcuseOverflowCount = 0;
|
||||
OpenExcuses.Clear(); CompactMonthDays.Clear(); CalendarScopes.Clear(); SelectedCalendarScope = null;
|
||||
OpenExcuseOverflowCount = 0;
|
||||
}
|
||||
|
||||
// Wird beim Navigieren auf diese Seite aufgerufen statt LoadCommand: baut nur den lokalen
|
||||
@@ -678,6 +699,12 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
holidayWeekdaysExcluded, termStart, classRegisterEvents,
|
||||
_patternScoreSettings.Load(), closedVorgaengeByNameKey)) Roster.Add(row);
|
||||
|
||||
CalendarScopes.Add(new ClassTeacherCalendarScope(null, "Gesamte Klasse"));
|
||||
foreach (var student in students.OrderBy(s => s.DisplayName))
|
||||
CalendarScopes.Add(new ClassTeacherCalendarScope(student.DisplayName,
|
||||
ClassTeacherOverviewViewModel.DisplayStudentName(student.DisplayName)));
|
||||
SelectedCalendarScope = CalendarScopes[0];
|
||||
|
||||
StudentCount = Roster.Count;
|
||||
TodayAlertCount = Roster.Count(r => r.HasAbsenceToday);
|
||||
TodayUnexcusedCount = Roster.Count(r => r.IsUnexcused);
|
||||
@@ -724,7 +751,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
private void OpenMonthlyCalendar()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
DetailsTab.StudentFilter = "";
|
||||
DetailsTab.StudentFilter = SelectedCalendarScope?.StudentName ?? "";
|
||||
DetailsTab.CalendarMonth = new DateOnly(today.Year, today.Month, 1);
|
||||
ActiveTabIndex = 2;
|
||||
DetailsTab.ShowCalendarCommand.Execute(null);
|
||||
@@ -735,15 +762,29 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
CompactMonthDays.Clear();
|
||||
foreach (var day in BuildCompactMonthDays(new DateOnly(today.Year, today.Month, 1),
|
||||
absences, registerEntries, today))
|
||||
absences, registerEntries, today, SelectedCalendarScope?.StudentName))
|
||||
CompactMonthDays.Add(day);
|
||||
}
|
||||
|
||||
private void RebuildCompactMonthFromCachedData()
|
||||
{
|
||||
if (_lastAbsences is null || _lastClassRegisterEvents is null) return;
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
BuildCompactMonth(ClassAbsenceDaySummaryRow.GroupByStudentAndDay(_lastAbsences),
|
||||
_lastClassRegisterEvents, today);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ClassTeacherCompactCalendarDay> BuildCompactMonthDays(DateOnly month,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> absences,
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries, DateOnly today)
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto> registerEntries, DateOnly today,
|
||||
string? studentName = null)
|
||||
{
|
||||
var first = new DateOnly(month.Year, month.Month, 1);
|
||||
if (!string.IsNullOrWhiteSpace(studentName))
|
||||
{
|
||||
absences = absences.Where(a => UntisNameMatching.NamesMatch(a.StudentName, studentName)).ToList();
|
||||
registerEntries = registerEntries.Where(e => UntisNameMatching.NamesMatch(e.StudentName, studentName)).ToList();
|
||||
}
|
||||
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)
|
||||
@@ -768,6 +809,12 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static string DisplayStudentName(string value)
|
||||
{
|
||||
var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.Length < 2 ? value : string.Join(" ", parts.Skip(1).Append(parts[0]));
|
||||
}
|
||||
|
||||
private static int SignalPriority(ClassTeacherCalendarEventKind kind) => kind switch
|
||||
{
|
||||
ClassTeacherCalendarEventKind.Unexcused => 0,
|
||||
|
||||
Reference in New Issue
Block a user