Redesign Klassenlehrer Tab.

This commit is contained in:
2026-08-26 22:50:40 +02:00
parent 265ffe9a74
commit cf8de08501
11 changed files with 730 additions and 207 deletions
@@ -126,6 +126,51 @@ public sealed class ClassTeacherViewModelsTests
Assert.True(ben.HasRecentClassRegisterEntry); Assert.True(ben.HasRecentClassRegisterEntry);
} }
[Fact]
public void RosterBuild_BeschreibtVerspaetungNichtAlsNullFehlstunden()
{
var students = new[] { Student(1001, "Ada Müller") };
var todayAbsences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 26), "Müller Ada", 1001, 0, 20,
["Deu"], [1], ["nicht entsch."], ["Verspätung"], null, null, false),
};
var row = Assert.Single(ClassTeacherRosterRow.Build(students, todayAbsences, [],
new DateOnly(2026, 8, 26)));
Assert.True(row.IsLate);
Assert.True(row.IsUnexcused);
Assert.Equal("20 Min. verspätet · Unentschuldigt", row.StatusText);
Assert.Equal("#FF5A67", row.StatusColor);
}
[Fact]
public void RosterBuild_PriorisiertHandlungsbedarfVorUnauffaelligenSchuelern()
{
var students = new[]
{
Student(1001, "Zora Unauffällig"),
Student(1002, "Ada Fehlzeit"),
Student(1003, "Ben Klassenbuch"),
};
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 26), "Fehlzeit Ada", 1002, 2, 90,
["Deu"], [1, 2], ["nicht entsch."], ["Absent"], null, null, false),
};
var register = new[]
{
new UntisForeignClassRegisterEventDto("10c", 20260825, "Deu", "Klassenbuch Ben", "test",
"Fehlende HA", "Negativ", "Hausaufgaben fehlen"),
};
var rows = ClassTeacherRosterRow.Build(students, absences, register, new DateOnly(2026, 8, 26));
Assert.Equal(["Ada Fehlzeit", "Ben Klassenbuch", "Zora Unauffällig"],
rows.Select(r => r.StudentName));
}
private static UntisStudentRosterCacheEntry Student(int? externKey, string displayName) => private static UntisStudentRosterCacheEntry Student(int? externKey, string displayName) =>
new() { ClassName = "6a", ExternKey = externKey, DisplayName = displayName }; new() { ClassName = "6a", ExternKey = externKey, DisplayName = displayName };
} }
@@ -12,6 +12,15 @@ public sealed record ClassTeacherClassRegisterRow(DateOnly Date, string? Subject
string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text) string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text)
{ {
public string DateLabel => Date.ToString("dd.MM.yyyy"); public string DateLabel => Date.ToString("dd.MM.yyyy");
public string StudentDisplayName => DisplayName(StudentName);
public string CategoryColor => CategoryGroup?.Contains("Negativ", StringComparison.OrdinalIgnoreCase) == true
? "#FF5A67" : "#4C8DFF";
private static string DisplayName(string value)
{
var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return parts.Length < 2 ? value : string.Join(" ", parts.Skip(1).Append(parts[0]));
}
} }
/// <summary>Fehlzeiten eines/einer Schüler*in an einem Tag, über alle Fächer zusammengefasst - /// <summary>Fehlzeiten eines/einer Schüler*in an einem Tag, über alle Fächer zusammengefasst -
@@ -25,6 +34,14 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
string? Note, string? ExcuseNote, bool CountsAsFullDay) string? Note, string? ExcuseNote, bool CountsAsFullDay)
{ {
public string DateLabel => Date.ToString("dd.MM.yyyy"); public string DateLabel => Date.ToString("dd.MM.yyyy");
public string StudentDisplayName
{
get
{
var parts = StudentName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return parts.Length < 2 ? StudentName : string.Join(" ", parts.Skip(1).Append(parts[0]));
}
}
public string SubjectsLabel => string.Join(", ", Subjects); public string SubjectsLabel => string.Join(", ", Subjects);
public string PeriodsLabel => string.Join(", ", PeriodNumbers.Order()); public string PeriodsLabel => string.Join(", ", PeriodNumbers.Order());
public string StatusLabel => string.Join(", ", Statuses); public string StatusLabel => string.Join(", ", Statuses);
@@ -33,6 +50,17 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
/// dezente Hervorhebung statt einer weiteren Text-/Icon-Spalte. /// dezente Hervorhebung statt einer weiteren Text-/Icon-Spalte.
public string RowBackground => CountsAsFullDay ? "#1FFFA000" : "Transparent"; public string RowBackground => CountsAsFullDay ? "#1FFFA000" : "Transparent";
public string? FullDayTooltip => CountsAsFullDay ? "Ganzer Fehltag" : null; public string? FullDayTooltip => CountsAsFullDay ? "Ganzer Fehltag" : null;
public bool IsLate => TotalAbsentPeriods == 0 || AbsenceReasons.Any(r =>
r.Contains("verspät", StringComparison.OrdinalIgnoreCase));
public bool IsUnexcused => Statuses.Any(s =>
s.Contains("nicht entsch", StringComparison.OrdinalIgnoreCase));
public string KindLabel => IsLate ? $"{TotalAbsentMinutes} Min. verspätet" :
CountsAsFullDay ? "Ganzer Fehltag" : $"{TotalAbsentPeriods} Fehlstunde{(TotalAbsentPeriods == 1 ? "" : "n")}";
public string FriendlyStatusLabel => IsUnexcused ? "Unentschuldigt" :
Statuses.Any(s => s.Contains("entsch", StringComparison.OrdinalIgnoreCase)) ? "Entschuldigt" : StatusLabel;
public string StatusColor => IsUnexcused ? "#FF5A67" : IsLate ? "#F59E0B" : "#73C45A";
public string DetailLabel => string.Join(" · ", new[] { ReasonLabel, Note, ExcuseNote }
.Where(value => !string.IsNullOrWhiteSpace(value)));
public static IReadOnlyList<ClassAbsenceDaySummaryRow> GroupByStudentAndDay( public static IReadOnlyList<ClassAbsenceDaySummaryRow> GroupByStudentAndDay(
IEnumerable<UntisClassAbsenceEntryDto> entries) => entries IEnumerable<UntisClassAbsenceEntryDto> entries) => entries
@@ -78,12 +106,18 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
public ObservableCollection<ClassTeacherClassRegisterRow> Entries { get; } = []; public ObservableCollection<ClassTeacherClassRegisterRow> Entries { get; } = [];
public ObservableCollection<ClassAbsenceDaySummaryRow> AbsenceEntries { get; } = []; public ObservableCollection<ClassAbsenceDaySummaryRow> AbsenceEntries { get; } = [];
[ObservableProperty] private DateTimeOffset _startDate = DateTimeOffset.Now.AddDays(-7); [ObservableProperty] private DateTimeOffset _startDate = DateTimeOffset.Now.AddDays(-6);
[ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now; [ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now;
[ObservableProperty] private string _status = "Zeitraum wählen und laden."; [ObservableProperty] private string _status = "Zeitraum wählen und laden.";
[ObservableProperty] private bool _busy; [ObservableProperty] private bool _busy;
/// Von der Übersicht gesetzt (Klick auf eine Roster-Zeile) - leer zeigt alle Schüler*innen. /// Von der Übersicht gesetzt (Klick auf eine Roster-Zeile) - leer zeigt alle Schüler*innen.
[ObservableProperty] private string _studentFilter = ""; [ObservableProperty] private string _studentFilter = "";
[ObservableProperty] private int _quickRangeIndex = 1;
public bool HasEntries => Entries.Count > 0;
public bool HasAbsenceEntries => AbsenceEntries.Count > 0;
public string ActiveFilterLabel => string.IsNullOrWhiteSpace(StudentFilter)
? "Alle Schüler*innen" : StudentFilter;
public ClassTeacherDetailsViewModel(UntisReportCacheService cache) public ClassTeacherDetailsViewModel(UntisReportCacheService cache)
{ {
@@ -93,9 +127,20 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
public void Initialize(string className) public void Initialize(string className)
{ {
_className = className; _className = className;
StudentFilter = "";
Entries.Clear(); Entries.Clear();
AbsenceEntries.Clear(); AbsenceEntries.Clear();
Status = "Zeitraum wählen und laden."; Status = "Zeitraum wählen und laden.";
NotifyListState();
}
partial void OnStudentFilterChanged(string value) => OnPropertyChanged(nameof(ActiveFilterLabel));
partial void OnQuickRangeIndexChanged(int value)
{
var days = value switch { 0 => 0, 1 => 6, 2 => 29, _ => 6 };
EndDate = DateTimeOffset.Now;
StartDate = DateTimeOffset.Now.AddDays(-days);
} }
[RelayCommand] [RelayCommand]
@@ -106,6 +151,16 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
[RelayCommand] [RelayCommand]
private Task Refresh() => LoadInternal(forceRefresh: true); private Task Refresh() => LoadInternal(forceRefresh: true);
[RelayCommand]
private Task ApplyFilter() => LoadInternal(forceRefresh: false);
[RelayCommand]
private async Task ClearStudentFilter()
{
StudentFilter = "";
await LoadInternal(forceRefresh: false);
}
private async Task LoadInternal(bool forceRefresh) private async Task LoadInternal(bool forceRefresh)
{ {
var start = DateOnly.FromDateTime(StartDate.LocalDateTime); var start = DateOnly.FromDateTime(StartDate.LocalDateTime);
@@ -113,7 +168,7 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; } if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
if (string.IsNullOrWhiteSpace(_className)) { Status = "Keine Klasse ausgewählt."; return; } if (string.IsNullOrWhiteSpace(_className)) { Status = "Keine Klasse ausgewählt."; return; }
Busy = true; Entries.Clear(); AbsenceEntries.Clear(); Busy = true; Entries.Clear(); AbsenceEntries.Clear(); NotifyListState();
try try
{ {
var classRegisterTask = _cache.GetClassRegisterEventsAsync(_className, start, end, forceRefresh); var classRegisterTask = _cache.GetClassRegisterEventsAsync(_className, start, end, forceRefresh);
@@ -137,9 +192,10 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " + Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " +
$"{AbsenceEntries.Count} Fehlzeiten-Tage im Zeitraum."; $"{AbsenceEntries.Count} Fehlzeiten-Tage im Zeitraum.";
NotifyListState();
} }
catch (WebUntisIntegrationException ex) { Status = ex.Message; } catch (WebUntisIntegrationException ex) { Status = ex.Message; }
finally { Busy = false; } finally { Busy = false; NotifyListState(); }
} }
// WebUntis liefert Namen je nach Bericht in anderer Reihenfolge als der Schülerreport, aus dem // WebUntis liefert Namen je nach Bericht in anderer Reihenfolge als der Schülerreport, aus dem
@@ -153,4 +209,10 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
private static bool TryDate(int value, out DateOnly date) => private static bool TryDate(int value, out DateOnly date) =>
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date); DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
private void NotifyListState()
{
OnPropertyChanged(nameof(HasEntries));
OnPropertyChanged(nameof(HasAbsenceEntries));
}
} }
@@ -6,62 +6,91 @@ using LehrerApp.Desktop.Services;
namespace LehrerApp.Desktop.ViewModels.ClassTeacher; namespace LehrerApp.Desktop.ViewModels.ClassTeacher;
/// <summary>Eine Zeile der Klassenlehrer-Übersicht ("auf einen Blick", Nutzer-Feedback) - pro
/// Schüler*in ein Ampel-Symbol für "heute laut WebUntis keine gemeldete Fehlzeit" und ein Badge,
/// falls in den letzten Tagen ein Klassenbucheintrag einer anderen Lehrkraft angelegt wurde.
/// Bewusst kein Bezug zu eigenem Unterricht/eigener Anwesenheitserfassung - genau der Fall, den es
/// abzudecken gilt, ist der Tag ohne eigenen Unterricht mit der Klasse.</summary>
public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday, public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday,
string? AbsenceTooltip, bool HasRecentClassRegisterEntry) string? AbsenceTooltip, bool HasRecentClassRegisterEntry)
{ {
public string StatusIcon => HasAbsenceToday ? "⚠" : "✓"; public ClassAbsenceDaySummaryRow? TodayAbsence { get; init; }
public bool HasClassRegisterToday { get; init; }
public bool IsLate => TodayAbsence is { } a &&
(a.TotalAbsentPeriods == 0 || a.AbsenceReasons.Any(IsLateReason));
public bool IsUnexcused => TodayAbsence?.Statuses.Any(s =>
s.Contains("nicht entsch", StringComparison.OrdinalIgnoreCase)) == true;
public bool NeedsAttention => HasAbsenceToday || HasRecentClassRegisterEntry;
public int AttentionRank => IsUnexcused ? 0 : IsLate ? 1 : HasAbsenceToday ? 2 :
HasRecentClassRegisterEntry ? 3 : 4;
public string Initials
{
get
{
var parts = StudentName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return string.Concat(parts.Take(2).Select(p => char.ToUpperInvariant(p[0])));
}
}
public string StatusText
{
get
{
if (TodayAbsence is null)
return HasRecentClassRegisterEntry ? "Neuer Klassenbucheintrag" : "Keine gemeldete Fehlzeit";
var core = IsLate
? $"{TodayAbsence.TotalAbsentMinutes} Min. verspätet"
: TodayAbsence.CountsAsFullDay
? "Ganzer Fehltag"
: $"{TodayAbsence.TotalAbsentPeriods} Fehlstunde{(TodayAbsence.TotalAbsentPeriods == 1 ? "" : "n")}";
return IsUnexcused ? $"{core} · Unentschuldigt" : core;
}
}
public string StatusColor => IsUnexcused ? "#FF5A67" :
HasAbsenceToday ? "#F59E0B" : HasRecentClassRegisterEntry ? "#4C8DFF" : "#73C45A";
public string StatusBarColor => StatusColor;
public string ClassRegisterLabel => HasClassRegisterToday ? "Eintrag heute" :
HasRecentClassRegisterEntry ? "Eintrag diese Woche" : "";
private static bool IsLateReason(string reason) =>
reason.Contains("verspät", StringComparison.OrdinalIgnoreCase);
/// <summary>Führt die WebUntis-Schülerliste der Klasse mit den heutigen Fehlzeiten und den
/// jüngsten Klassenbucheinträgen zusammen. Zuordnung primär über <c>ExternKey</c> (beide
/// stammen aus WebUntis, gleiche Kennung), Fallback auf reihenfolge-unabhängigen Namensabgleich
/// (<see cref="UntisNameMatching"/>) für den Klassenbuch-Bericht, der keinen ExternKey liefert
/// (siehe UntisForeignClassRegisterEventDto) — WebUntis liefert Namen dort in anderer
/// Reihenfolge als im Schülerreport.</summary>
public static IReadOnlyList<ClassTeacherRosterRow> Build( public static IReadOnlyList<ClassTeacherRosterRow> Build(
IReadOnlyList<UntisStudentRosterCacheEntry> students, IReadOnlyList<UntisStudentRosterCacheEntry> students,
IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences, IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences,
IReadOnlyList<UntisForeignClassRegisterEventDto> recentClassRegisterEntries) IReadOnlyList<UntisForeignClassRegisterEventDto> recentClassRegisterEntries,
DateOnly? today = null)
{ {
var absenceByKey = todayAbsences var referenceDate = today ?? todayAbsences.FirstOrDefault()?.Date ?? DateOnly.FromDateTime(DateTime.Today);
.Where(a => a.ExternKey is not null) var absenceByKey = todayAbsences.Where(a => a.ExternKey is not null)
.GroupBy(a => a.ExternKey!.Value) .GroupBy(a => a.ExternKey!.Value).ToDictionary(g => g.Key, g => g.First());
var absenceByName = todayAbsences.GroupBy(a => UntisNameMatching.NameKey(a.StudentName))
.ToDictionary(g => g.Key, g => g.First()); .ToDictionary(g => g.Key, g => g.First());
var absenceByName = todayAbsences var recentByName = recentClassRegisterEntries.GroupBy(e => UntisNameMatching.NameKey(e.StudentName))
.GroupBy(a => UntisNameMatching.NameKey(a.StudentName)) .ToDictionary(g => g.Key, g => g.ToList());
.ToDictionary(g => g.Key, g => g.First());
var recentNames = recentClassRegisterEntries
.Select(e => UntisNameMatching.NameKey(e.StudentName))
.ToHashSet();
return students return students.Select(student =>
.Select(s =>
{ {
var nameKey = UntisNameMatching.NameKey(s.DisplayName); var nameKey = UntisNameMatching.NameKey(student.DisplayName);
var absence = (s.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null) var absence = (student.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null)
?? absenceByName.GetValueOrDefault(nameKey); ?? absenceByName.GetValueOrDefault(nameKey);
return new ClassTeacherRosterRow(s.DisplayName, s.ExternKey, absence is not null, var registerEntries = recentByName.GetValueOrDefault(nameKey) ?? [];
return new ClassTeacherRosterRow(student.DisplayName, student.ExternKey, absence is not null,
absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}", absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}",
recentNames.Contains(nameKey)); registerEntries.Count > 0)
{
TodayAbsence = absence,
HasClassRegisterToday = registerEntries.Any(e => TryDate(e.Date, out var date) && date == referenceDate),
};
}) })
.OrderBy(r => r.StudentName) .OrderBy(r => r.AttentionRank).ThenBy(r => r.StudentName).ToList();
.ToList();
} }
private static bool TryDate(int value, out DateOnly date) =>
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
} }
/// <summary> public sealed record ClassTeacherTrendDay(string DayLabel, int AlertCount, int UnexcusedCount, int LateCount,
/// Top-Level-Bereich "Klassenlehrer" (eigener Sidebar-Eintrag, siehe MainWindowViewModel) - bewusst double AlertBarWidth, double UnexcusedBarWidth, double LateBarWidth);
/// nicht an eine LearningGroup gehängt, da man Klassenlehrer für eine ganze Klasse ist, unabhängig public sealed record ClassTeacherPatternNotice(string StudentName, string Message, string Color);
/// vom eigenen Unterricht (Nutzer-Feedback zur ursprünglichen Umsetzung als Gruppen-Tab). Die
/// Klasse selbst kommt aus den WebUntis-Einstellungen (<see cref="WebUntisSettingsService.HomeroomClassName"/>).
/// Zwei Ebenen: Übersicht (dieses ViewModel, kompaktes Roster) und Details
/// (<see cref="ClassTeacherDetailsViewModel"/>, die bisherigen Rohdaten-Listen) - ein Klick auf
/// eine Roster-Zeile springt gefiltert in die Details.
/// </summary>
public partial class ClassTeacherOverviewViewModel : ObservableObject public partial class ClassTeacherOverviewViewModel : ObservableObject
{ {
private readonly WebUntisSettingsService _settings; private readonly WebUntisSettingsService _settings;
@@ -69,57 +98,132 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
public ClassTeacherDetailsViewModel DetailsTab { get; } public ClassTeacherDetailsViewModel DetailsTab { get; }
public ObservableCollection<ClassTeacherRosterRow> Roster { get; } = []; public ObservableCollection<ClassTeacherRosterRow> Roster { get; } = [];
public ObservableCollection<ClassTeacherRosterRow> PrimaryRoster { get; } = [];
public ObservableCollection<ClassTeacherRosterRow> SecondaryRoster { get; } = [];
public ObservableCollection<ClassTeacherTrendDay> TrendDays { get; } = [];
public ObservableCollection<ClassTeacherPatternNotice> PatternNotices { get; } = [];
[ObservableProperty] private string? _homeroomClassName; [ObservableProperty] private string? _homeroomClassName;
[ObservableProperty] private int _activeTabIndex; [ObservableProperty] private int _activeTabIndex;
[ObservableProperty] private string _status = ""; [ObservableProperty] private string _status = "";
[ObservableProperty] private bool _busy; [ObservableProperty] private bool _busy;
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private int _selectedRosterFilter;
[ObservableProperty] private string _primarySectionTitle = "Heute auffällig";
[ObservableProperty] private string _secondarySectionTitle = "Weitere Schüler*innen";
[ObservableProperty] private int _studentCount;
[ObservableProperty] private int _todayAlertCount;
[ObservableProperty] private int _todayUnexcusedCount;
[ObservableProperty] private int _recentClassRegisterCount;
[ObservableProperty] private int _presentCount;
[ObservableProperty] private int _lateCount;
[ObservableProperty] private int _excusedAbsenceCount;
[ObservableProperty] private int _unexcusedAbsenceCount;
[ObservableProperty] private string _lastUpdatedLabel = "Noch nicht aktualisiert";
public bool HomeroomClassConfigured => !string.IsNullOrWhiteSpace(HomeroomClassName); public bool HomeroomClassConfigured => !string.IsNullOrWhiteSpace(HomeroomClassName);
public bool HasPrimaryRoster => PrimaryRoster.Count > 0;
public bool HasSecondaryRoster => SecondaryRoster.Count > 0;
public bool HasNoFilterResults => !Busy && PrimaryRoster.Count == 0 && SecondaryRoster.Count == 0;
public bool HasPatternNotices => PatternNotices.Count > 0;
public bool AlertsFilterSelected => SelectedRosterFilter == 0;
public bool ClassRegisterFilterSelected => SelectedRosterFilter == 1;
public bool AllFilterSelected => SelectedRosterFilter == 2;
public int TodayUnexcusedPercent => Percent(TodayUnexcusedCount);
public int LatePercent => Percent(LateCount);
public int PresentPercent => Percent(PresentCount);
public int ExcusedAbsencePercent => Percent(ExcusedAbsenceCount);
public int UnexcusedAbsencePercent => Percent(UnexcusedAbsenceCount);
/// Vom Code-Behind/MainWindowViewModel gesetzt: springt in die WebUntis-Einstellungen, wenn noch
/// keine Klasse gewählt ist.
public Func<Task>? OnNavigateToSettings { get; set; } public Func<Task>? OnNavigateToSettings { get; set; }
public Func<Task>? OnNavigateToWorkload { get; set; }
public ClassTeacherOverviewViewModel(WebUntisSettingsService settings, public ClassTeacherOverviewViewModel(WebUntisSettingsService settings,
UntisReportCacheService cache, ClassTeacherDetailsViewModel detailsTab) UntisReportCacheService cache, ClassTeacherDetailsViewModel detailsTab)
{ {
_settings = settings; _cache = cache; DetailsTab = detailsTab; _settings = settings;
_cache = cache;
DetailsTab = detailsTab;
} }
partial void OnHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(HomeroomClassConfigured)); partial void OnHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(HomeroomClassConfigured));
partial void OnSearchTextChanged(string value) => ApplyRosterFilter();
partial void OnSelectedRosterFilterChanged(int value)
{
OnPropertyChanged(nameof(AlertsFilterSelected));
OnPropertyChanged(nameof(ClassRegisterFilterSelected));
OnPropertyChanged(nameof(AllFilterSelected));
ApplyRosterFilter();
}
[RelayCommand] [RelayCommand]
private async Task Load() private async Task Load()
{ {
HomeroomClassName = _settings.HomeroomClassName; HomeroomClassName = _settings.HomeroomClassName;
ActiveTabIndex = 0; ActiveTabIndex = 0;
Roster.Clear(); Roster.Clear(); PrimaryRoster.Clear(); SecondaryRoster.Clear(); TrendDays.Clear(); PatternNotices.Clear();
if (!HomeroomClassConfigured) if (!HomeroomClassConfigured)
{ {
Status = "Noch keine Klasse ausgewählt."; Status = "Noch keine Klasse ausgewählt.";
NotifyRosterState();
return; return;
} }
var className = HomeroomClassName!; var className = HomeroomClassName!;
DetailsTab.Initialize(className); DetailsTab.Initialize(className);
Busy = true; Busy = true;
NotifyRosterState();
try try
{ {
var today = DateOnly.FromDateTime(DateTime.Today); var today = DateOnly.FromDateTime(DateTime.Today);
var start = today.AddDays(-6);
var studentsTask = _cache.GetStudentRosterAsync(className); var studentsTask = _cache.GetStudentRosterAsync(className);
var absencesTask = _cache.GetAbsencesAsync(className, today, today); var absencesTask = _cache.GetAbsencesAsync(className, start, today);
var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, today.AddDays(-7), today); var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, start, today);
await Task.WhenAll(studentsTask, absencesTask, classRegisterTask); await Task.WhenAll(studentsTask, absencesTask, classRegisterTask);
var todayAbsences = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result); var absenceDays = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result);
var todayAbsences = absenceDays.Where(a => a.Date == today).ToList();
foreach (var row in ClassTeacherRosterRow.Build(studentsTask.Result, todayAbsences, foreach (var row in ClassTeacherRosterRow.Build(studentsTask.Result, todayAbsences,
classRegisterTask.Result)) classRegisterTask.Result, today)) Roster.Add(row);
Roster.Add(row);
Status = $"{Roster.Count} Schüler*innen · Stand heute."; StudentCount = Roster.Count;
TodayAlertCount = Roster.Count(r => r.HasAbsenceToday);
TodayUnexcusedCount = Roster.Count(r => r.IsUnexcused);
LateCount = Roster.Count(r => r.IsLate);
PresentCount = Roster.Count(r => !r.HasAbsenceToday);
ExcusedAbsenceCount = Roster.Count(r => r.HasAbsenceToday && !r.IsLate && !r.IsUnexcused);
UnexcusedAbsenceCount = Roster.Count(r => r.HasAbsenceToday && !r.IsLate && r.IsUnexcused);
RecentClassRegisterCount = classRegisterTask.Result.Count;
BuildTrend(absenceDays, start, today);
BuildPatternNotices(absenceDays);
LastUpdatedLabel = $"Zuletzt aktualisiert: Heute, {DateTime.Now:HH:mm}";
Status = $"{StudentCount} Schüler*innen · {TodayAlertCount} heute auffällig";
ApplyRosterFilter();
NotifySummary();
} }
catch (WebUntisIntegrationException ex) { Status = ex.Message; } catch (WebUntisIntegrationException ex) { Status = ex.Message; NotifyRosterState(); }
finally { Busy = false; } finally { Busy = false; NotifyRosterState(); }
}
[RelayCommand] private void ShowAlerts() => SelectedRosterFilter = 0;
[RelayCommand] private void ShowClassRegister() => SelectedRosterFilter = 1;
[RelayCommand] private void ShowAll() => SelectedRosterFilter = 2;
[RelayCommand]
private void OpenClassRegister()
{
DetailsTab.StudentFilter = "";
ActiveTabIndex = 1;
DetailsTab.LoadCommand.Execute(null);
}
[RelayCommand]
private void OpenAbsences()
{
DetailsTab.StudentFilter = "";
ActiveTabIndex = 2;
DetailsTab.LoadCommand.Execute(null);
} }
[RelayCommand] [RelayCommand]
@@ -127,13 +231,87 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
{ {
if (row is null) return; if (row is null) return;
DetailsTab.StudentFilter = row.StudentName; DetailsTab.StudentFilter = row.StudentName;
ActiveTabIndex = 1; ActiveTabIndex = row.HasAbsenceToday ? 2 : 1;
DetailsTab.LoadCommand.Execute(null); DetailsTab.LoadCommand.Execute(null);
} }
[RelayCommand] [RelayCommand] private async Task GoToSettings()
private async Task GoToSettings() { if (OnNavigateToSettings is not null) await OnNavigateToSettings(); }
[RelayCommand] private async Task GoToWorkload()
{ if (OnNavigateToWorkload is not null) await OnNavigateToWorkload(); }
private void ApplyRosterFilter()
{ {
if (OnNavigateToSettings is not null) await OnNavigateToSettings(); PrimaryRoster.Clear(); SecondaryRoster.Clear();
var query = Roster.Where(r => string.IsNullOrWhiteSpace(SearchText) ||
r.StudentName.Contains(SearchText.Trim(), StringComparison.CurrentCultureIgnoreCase));
if (SelectedRosterFilter == 0)
{
PrimarySectionTitle = "Heute auffällig";
foreach (var row in query.Where(r => r.HasAbsenceToday)) PrimaryRoster.Add(row);
}
else if (SelectedRosterFilter == 1)
{
PrimarySectionTitle = "Klassenbucheinträge der letzten 7 Tage";
foreach (var row in query.Where(r => r.HasRecentClassRegisterEntry)) PrimaryRoster.Add(row);
}
else
{
PrimarySectionTitle = "Heute auffällig";
SecondarySectionTitle = "Weitere Schüler*innen";
foreach (var row in query.Where(r => r.NeedsAttention)) PrimaryRoster.Add(row);
foreach (var row in query.Where(r => !r.NeedsAttention)) SecondaryRoster.Add(row);
}
NotifyRosterState();
}
private void BuildTrend(IReadOnlyList<ClassAbsenceDaySummaryRow> absenceDays, DateOnly start, DateOnly end)
{
var counts = Enumerable.Range(0, 7).Select(offset => start.AddDays(offset)).Select(date =>
{
var rows = absenceDays.Where(r => r.Date == date).ToList();
return (Date: date, Alerts: rows.Count,
Unexcused: rows.Count(r => r.Statuses.Any(s => s.Contains("nicht entsch", StringComparison.OrdinalIgnoreCase))),
Late: rows.Count(r => r.TotalAbsentPeriods == 0 || r.AbsenceReasons.Any(a =>
a.Contains("verspät", StringComparison.OrdinalIgnoreCase))));
}).ToList();
var max = Math.Max(1, counts.Max(c => c.Alerts));
foreach (var day in counts)
TrendDays.Add(new ClassTeacherTrendDay(day.Date == end ? "Heute" : day.Date.ToString("ddd"),
day.Alerts, day.Unexcused, day.Late,
112d * day.Alerts / max, 112d * day.Unexcused / max, 112d * day.Late / max));
}
private void BuildPatternNotices(IReadOnlyList<ClassAbsenceDaySummaryRow> absenceDays)
{
foreach (var group in absenceDays.GroupBy(row => UntisNameMatching.NameKey(row.StudentName)))
{
var rows = group.ToList();
var displayName = Roster.FirstOrDefault(row =>
UntisNameMatching.NameKey(row.StudentName) == group.Key)?.StudentName ?? rows[0].StudentDisplayName;
var lateDays = rows.Count(row => row.IsLate);
var unexcusedDays = rows.Count(row => row.IsUnexcused && !row.IsLate);
if (unexcusedDays >= 2)
PatternNotices.Add(new ClassTeacherPatternNotice(displayName,
$"{unexcusedDays} unentschuldigte Fehltage in 7 Tagen", "#FF5A67"));
else if (lateDays >= 2)
PatternNotices.Add(new ClassTeacherPatternNotice(displayName,
$"{lateDays}-mal verspätet in 7 Tagen", "#F59E0B"));
}
OnPropertyChanged(nameof(HasPatternNotices));
}
private int Percent(int value) => StudentCount == 0 ? 0 : (int)Math.Round(100d * value / StudentCount);
private void NotifySummary()
{
OnPropertyChanged(nameof(TodayUnexcusedPercent)); OnPropertyChanged(nameof(LatePercent));
OnPropertyChanged(nameof(PresentPercent)); OnPropertyChanged(nameof(ExcusedAbsencePercent));
OnPropertyChanged(nameof(UnexcusedAbsencePercent));
}
private void NotifyRosterState()
{
OnPropertyChanged(nameof(HasPrimaryRoster)); OnPropertyChanged(nameof(HasSecondaryRoster));
OnPropertyChanged(nameof(HasNoFilterResults));
} }
} }
@@ -150,6 +150,7 @@ public partial class MainWindowViewModel : ObservableObject
{ {
var vm = _services.GetRequiredService<ClassTeacherOverviewViewModel>(); var vm = _services.GetRequiredService<ClassTeacherOverviewViewModel>();
vm.OnNavigateToSettings = () => { NavigateToSettings(SettingsTab.WebUntis); return Task.CompletedTask; }; vm.OnNavigateToSettings = () => { NavigateToSettings(SettingsTab.WebUntis); return Task.CompletedTask; };
vm.OnNavigateToWorkload = () => { NavigateToWorkload(); return Task.CompletedTask; };
vm.LoadCommand.Execute(null); vm.LoadCommand.Execute(null);
return vm; return vm;
} }
@@ -0,0 +1,72 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherAbsencesView"
x:DataType="vm:ClassTeacherDetailsViewModel">
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="16" RowSpacing="10">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<StackPanel>
<TextBlock Text="Fehlzeiten" FontSize="22" FontWeight="SemiBold"/>
<TextBlock Text="Pro Schüler*in und Tag über alle Fächer zusammengefasst" FontSize="12" Opacity="0.55"/>
</StackPanel>
<Button Grid.Column="1" Content="↻ Aus WebUntis aktualisieren" Command="{Binding RefreshCommand}"
IsEnabled="{Binding !Busy}" Background="Transparent" Foreground="#5795F5" VerticalAlignment="Center"/>
</Grid>
<Border Grid.Row="1" Background="#11151B" BorderBrush="#2A3038" BorderThickness="1"
CornerRadius="8" Padding="10">
<Grid RowDefinitions="Auto,Auto" RowSpacing="8">
<Grid Grid.Row="0" ColumnDefinitions="155,Auto,*,Auto,*" ColumnSpacing="8">
<ComboBox Grid.Column="0" SelectedIndex="{Binding QuickRangeIndex, Mode=TwoWay}">
<ComboBoxItem Content="Heute"/><ComboBoxItem Content="Letzte 7 Tage"/><ComboBoxItem Content="Letzte 30 Tage"/>
</ComboBox>
<TextBlock Grid.Column="1" Text="von" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="2" SelectedDate="{Binding StartDate}" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Column="3" Text="bis" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="4" SelectedDate="{Binding EndDate}" HorizontalAlignment="Stretch"/>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
<TextBox Grid.Column="0" Text="{Binding StudentFilter, Mode=TwoWay}" PlaceholderText="Schüler*in filtern…"/>
<Button Grid.Column="1" Content="Anzeigen" Command="{Binding ApplyFilterCommand}" IsEnabled="{Binding !Busy}"/>
<Button Grid.Column="2" Content="Filter löschen" Command="{Binding ClearStudentFilterCommand}"
IsVisible="{Binding StudentFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</Grid>
</Grid>
</Border>
<Grid Grid.Row="2">
<DataGrid ItemsSource="{Binding AbsenceEntries}" AutoGenerateColumns="False" IsReadOnly="True"
GridLinesVisibility="Horizontal" BorderBrush="#2A3038" BorderThickness="1"
CanUserResizeColumns="True" CanUserSortColumns="True" RowHeight="48">
<DataGrid.Columns>
<DataGridTextColumn Header="Datum" Binding="{Binding DateLabel}" Width="105"/>
<DataGridTextColumn Header="Schüler*in" Binding="{Binding StudentDisplayName}" Width="1.3*"/>
<DataGridTemplateColumn Header="Art" Width="1.15*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:ClassAbsenceDaySummaryRow">
<TextBlock Text="{Binding KindLabel}" Foreground="{Binding StatusColor}" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Status" Width="1*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:ClassAbsenceDaySummaryRow">
<Border Background="#1C222A" CornerRadius="5" Padding="7,3" HorizontalAlignment="Left">
<TextBlock Text="{Binding FriendlyStatusLabel}" Foreground="{Binding StatusColor}" FontSize="11"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Fächer" Binding="{Binding SubjectsLabel}" Width="1.2*"/>
<DataGridTextColumn Header="Stunden" Binding="{Binding PeriodsLabel}" Width="0.75*"/>
<DataGridTextColumn Header="Grund / Notiz" Binding="{Binding DetailLabel}" Width="2*"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel IsVisible="{Binding !HasAbsenceEntries}" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="5">
<TextBlock Text="Keine Fehlzeiten im gewählten Zeitraum" FontWeight="SemiBold"/>
<TextBlock Text="Passe Zeitraum oder Schülerfilter an." FontSize="12" Opacity="0.55"/>
</StackPanel>
</Grid>
<TextBlock Grid.Row="3" Text="{Binding Status}" FontSize="11" Opacity="0.6"/>
</Grid>
</UserControl>
@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace LehrerApp.Desktop.Views.ClassTeacher;
public partial class ClassTeacherAbsencesView : UserControl
{
public ClassTeacherAbsencesView() => InitializeComponent();
}
@@ -1,96 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherDetailsView"
x:DataType="vm:ClassTeacherDetailsViewModel">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,1*,Auto,Auto,1*,Auto" Margin="16" RowSpacing="6">
<TextBlock Grid.Row="0" TextWrapping="Wrap" FontSize="12" Opacity="0.65"
Text="Klassenbucheinträge anderer Lehrkräfte und Fehlzeiten der Klasse über alle Fächer (nur Ansicht, keine Übernahme in eigene Daten)."/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
<DatePicker SelectedDate="{Binding StartDate}"/>
<TextBlock Text="bis" VerticalAlignment="Center"/>
<DatePicker SelectedDate="{Binding EndDate}"/>
<Button Content="Laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"
ToolTip.Tip="Lädt aus dem lokalen Cache, sofern der aktuelle Zeitraum in der letzten Stunde schon einmal abgerufen wurde."/>
<Button Content="Jetzt wirklich neu abrufen" Command="{Binding RefreshCommand}" IsEnabled="{Binding !Busy}"
ToolTip.Tip="Ruft WebUntis direkt ab, auch wenn der Cache noch aktuell wäre."/>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8"
IsVisible="{Binding StudentFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding StudentFilter, StringFormat='Gefiltert auf: {0}'}" FontSize="12" VerticalAlignment="Center"/>
<Button Content="Filter zurücksetzen" FontSize="11" Padding="8,3" Click="OnClearStudentFilterClick"/>
</StackPanel>
<TextBlock Grid.Row="3" Text="Klassenbucheinträge anderer Lehrkräfte" FontWeight="SemiBold" FontSize="13" Margin="0,4,0,0"/>
<Grid Grid.Row="4" ColumnDefinitions="70,1*,1.1*,0.8*,1*,1.3*" ColumnSpacing="8" Margin="4,0">
<TextBlock Grid.Column="0" Text="Datum" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="1" Text="Schüler*in" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="2" Text="Fach / Lehrkraft" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="3" Text="Kategorie" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="4" Text="Gruppe" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="5" Text="Text" FontSize="11" Opacity="0.6"/>
</Grid>
<ScrollViewer Grid.Row="5">
<ItemsControl ItemsSource="{Binding Entries}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ClassTeacherClassRegisterRow">
<Grid ColumnDefinitions="70,1*,1.1*,0.8*,1*,1.3*" ColumnSpacing="8" Margin="0,3">
<TextBlock Grid.Column="0" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding StudentName}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<StackPanel Grid.Column="2" Spacing="0">
<TextBlock Text="{Binding Subject}"/>
<TextBlock Text="{Binding TeacherUsername}" FontSize="11" Opacity="0.6"/>
</StackPanel>
<TextBlock Grid.Column="3" Text="{Binding CategoryName}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<TextBlock Grid.Column="4" Text="{Binding CategoryGroup}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<TextBlock Grid.Column="5" Text="{Binding Text}" VerticalAlignment="Center" TextWrapping="Wrap"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<TextBlock Grid.Row="6" Text="Fehlzeiten der Klasse (alle Fächer, pro Schüler*in und Tag zusammengefasst)"
FontWeight="SemiBold" FontSize="13" Margin="0,8,0,0"/>
<Grid Grid.Row="7" ColumnDefinitions="70,1*,1.3*,0.5*,0.6*,0.6*,0.8*,1.5*" ColumnSpacing="8" Margin="4,0">
<TextBlock Grid.Column="0" Text="Datum" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="1" Text="Schüler*in" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="2" Text="Fächer" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="3" Text="Std." FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="4" Text="Fehlstd." FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="5" Text="Fehlmin." FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="6" Text="Status" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="7" Text="Grund / Notiz" FontSize="11" Opacity="0.6"/>
</Grid>
<ScrollViewer Grid.Row="8">
<ItemsControl ItemsSource="{Binding AbsenceEntries}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ClassAbsenceDaySummaryRow">
<Border Background="{Binding RowBackground}" CornerRadius="4" Padding="0,2"
ToolTip.Tip="{Binding FullDayTooltip}">
<Grid ColumnDefinitions="70,1*,1.3*,0.5*,0.6*,0.6*,0.8*,1.5*" ColumnSpacing="8" Margin="4,1">
<TextBlock Grid.Column="0" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding StudentName}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<TextBlock Grid.Column="2" Text="{Binding SubjectsLabel}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<TextBlock Grid.Column="3" Text="{Binding PeriodsLabel}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<TextBlock Grid.Column="4" Text="{Binding TotalAbsentPeriods}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="5" Text="{Binding TotalAbsentMinutes}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="6" Text="{Binding StatusLabel}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<StackPanel Grid.Column="7" Spacing="0">
<TextBlock Text="{Binding ReasonLabel}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding Note}" FontSize="11" Opacity="0.6" TextWrapping="Wrap"
IsVisible="{Binding Note, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<TextBlock Grid.Row="9" Text="{Binding Status}" FontSize="12" TextWrapping="Wrap"/>
</Grid>
</UserControl>
@@ -1,17 +0,0 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.ClassTeacher;
namespace LehrerApp.Desktop.Views.ClassTeacher;
public partial class ClassTeacherDetailsView : UserControl
{
public ClassTeacherDetailsView() => InitializeComponent();
private void OnClearStudentFilterClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not ClassTeacherDetailsViewModel vm) return;
vm.StudentFilter = "";
vm.LoadCommand.Execute(null);
}
}
@@ -5,55 +5,259 @@
x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherOverviewView" x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherOverviewView"
x:DataType="vm:ClassTeacherOverviewViewModel"> x:DataType="vm:ClassTeacherOverviewViewModel">
<UserControl.Styles>
<Style Selector="Border.metric">
<Setter Property="Background" Value="#11151B"/>
<Setter Property="BorderBrush" Value="#2A3038"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,12"/>
<Setter Property="Margin" Value="0,0,10,0"/>
</Style>
<Style Selector="Button.filter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="#343B45"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Padding" Value="16,6"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style Selector="Button.filter.active">
<Setter Property="Background" Value="#173765"/>
<Setter Property="BorderBrush" Value="#3C86F7"/>
<Setter Property="Foreground" Value="#FFFFFF"/>
</Style>
<Style Selector="Button.rosterRow">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Background" Value="#0E1217"/>
<Setter Property="BorderBrush" Value="#262C34"/>
<Setter Property="BorderThickness" Value="1,0,1,1"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="MinHeight" Value="48"/>
</Style>
<Style Selector="Button.rosterRow:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#171D25"/>
</Style>
<Style Selector="Border.sidePanel">
<Setter Property="Background" Value="#11151B"/>
<Setter Property="BorderBrush" Value="#2A3038"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
<Style Selector="TextBlock.sectionLabel">
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="4,10,0,6"/>
</Style>
</UserControl.Styles>
<Grid> <Grid>
<!-- Hinweis, solange in den Einstellungen keine Klasse ausgewählt ist -->
<StackPanel IsVisible="{Binding !HomeroomClassConfigured}" HorizontalAlignment="Center" <StackPanel IsVisible="{Binding !HomeroomClassConfigured}" HorizontalAlignment="Center"
VerticalAlignment="Center" Spacing="12" MaxWidth="360"> VerticalAlignment="Center" Spacing="12" MaxWidth="380">
<TextBlock Text="🎓" FontSize="40" HorizontalAlignment="Center"/> <Border Width="56" Height="56" CornerRadius="28" Background="#173765" HorizontalAlignment="Center">
<TextBlock Text="Noch keine Klasse ausgewählt" FontSize="16" FontWeight="SemiBold" HorizontalAlignment="Center"/> <TextBlock Text="KL" FontSize="18" FontWeight="Bold" Foreground="#66A3FF"
<TextBlock TextWrapping="Wrap" TextAlignment="Center" FontSize="13" Opacity="0.7" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock Text="Noch keine Klasse ausgewählt" FontSize="18" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock TextWrapping="Wrap" TextAlignment="Center" FontSize="13" Opacity="0.65"
Text="Wähle in den WebUntis-Einstellungen die Klasse aus, deren Klassenlehrer/-in du bist."/> Text="Wähle in den WebUntis-Einstellungen die Klasse aus, deren Klassenlehrer/-in du bist."/>
<Button Content="Zu den Einstellungen" Command="{Binding GoToSettingsCommand}" HorizontalAlignment="Center"/> <Button Content="Zu den Einstellungen" Command="{Binding GoToSettingsCommand}" HorizontalAlignment="Center"/>
</StackPanel> </StackPanel>
<TabbedPage IsVisible="{Binding HomeroomClassConfigured}" TabPlacement="Top" <TabbedPage IsVisible="{Binding HomeroomClassConfigured}" TabPlacement="Top"
SelectedIndex="{Binding ActiveTabIndex, Mode=TwoWay}"> SelectedIndex="{Binding ActiveTabIndex, Mode=TwoWay}">
<ContentPage Header="Übersicht"> <ContentPage Header="Übersicht">
<Grid RowDefinitions="Auto,Auto,*" Margin="16" RowSpacing="8"> <Grid RowDefinitions="Auto,Auto,*" Margin="16,12,16,16" RowSpacing="10">
<TextBlock Grid.Row="0" Text="{Binding HomeroomClassName, StringFormat='Klasse {0}'}" <Grid Grid.Row="0" ColumnDefinitions="*,Auto">
FontSize="18" FontWeight="SemiBold"/> <StackPanel Grid.Column="0" Spacing="2">
<TextBlock Grid.Row="1" Text="{Binding Status}" FontSize="12" Opacity="0.7"/> <TextBlock Text="{Binding HomeroomClassName, StringFormat='Klasse {0}'}"
<ScrollViewer Grid.Row="2"> FontSize="24" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding Roster}"> <TextBlock Text="Klassenlehrer-Übersicht" FontSize="13" Opacity="0.58"/>
<ItemsControl.ItemTemplate> </StackPanel>
<DataTemplate x:DataType="vm:ClassTeacherRosterRow"> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<Button HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" <TextBlock Text="{Binding LastUpdatedLabel}" FontSize="11" Opacity="0.55" VerticalAlignment="Center"/>
Background="Transparent" BorderThickness="0" Padding="8,6" <Button Content="↻ Aktualisieren" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"
Command="{Binding $parent[ItemsControl].((vm:ClassTeacherOverviewViewModel)DataContext).ShowDetailsForStudentCommand}" Background="Transparent" BorderThickness="0" Foreground="#5795F5"/>
CommandParameter="{Binding}"> </StackPanel>
<Grid ColumnDefinitions="Auto,*,Auto"> </Grid>
<TextBlock Grid.Column="0" Text="{Binding StatusIcon}" FontSize="16" Width="28"
ToolTip.Tip="{Binding AbsenceTooltip}"/> <Grid Grid.Row="1" ColumnDefinitions="*,*,*,*">
<TextBlock Grid.Column="1" Text="{Binding StudentName}" VerticalAlignment="Center"/> <Border Grid.Column="0" Classes="metric">
<Border Grid.Column="2" Background="#1976D2" CornerRadius="8" Padding="6,1" <Grid ColumnDefinitions="36,*">
IsVisible="{Binding HasRecentClassRegisterEntry}" <TextBlock Text="◉" FontSize="22" Foreground="#4C8DFF" VerticalAlignment="Center"/>
ToolTip.Tip="Klassenbucheintrag einer anderen Lehrkraft in den letzten 7 Tagen"> <StackPanel Grid.Column="1"><TextBlock Text="{Binding StudentCount}" FontSize="22" FontWeight="SemiBold"/><TextBlock Text="Schüler*innen" FontSize="11" Opacity="0.6"/></StackPanel>
<TextBlock Text="Klassenbuch" Foreground="White" FontSize="10"/> </Grid>
</Border> </Border>
</Grid> <Border Grid.Column="1" Classes="metric">
</Button> <Grid ColumnDefinitions="36,*">
</DataTemplate> <TextBlock Text="△" FontSize="24" Foreground="#F59E0B" VerticalAlignment="Center"/>
</ItemsControl.ItemTemplate> <StackPanel Grid.Column="1"><TextBlock Text="{Binding TodayAlertCount}" FontSize="22" FontWeight="SemiBold"/><TextBlock Text="heute auffällig" FontSize="11" Opacity="0.6"/></StackPanel>
</ItemsControl> </Grid>
</ScrollViewer> </Border>
<Border Grid.Column="2" Classes="metric">
<Grid ColumnDefinitions="36,*">
<TextBlock Text="!" FontSize="22" Foreground="#FF5A67" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1"><TextBlock Text="{Binding TodayUnexcusedCount}" FontSize="22" FontWeight="SemiBold"/><TextBlock Text="unentschuldigt" FontSize="11" Opacity="0.6"/></StackPanel>
</Grid>
</Border>
<Border Grid.Column="3" Classes="metric" Margin="0">
<Grid ColumnDefinitions="36,*">
<TextBlock Text="▤" FontSize="21" Foreground="#4C8DFF" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1"><TextBlock Text="{Binding RecentClassRegisterCount}" FontSize="22" FontWeight="SemiBold"/><TextBlock Text="Klassenbucheinträge · 7 Tage" FontSize="11" Opacity="0.6"/></StackPanel>
</Grid>
</Border>
</Grid>
<Grid Grid.Row="2" ColumnDefinitions="*,286" ColumnSpacing="12">
<Grid Grid.Column="0" RowDefinitions="Auto,*">
<Border Grid.Row="0" Background="#11151B" BorderBrush="#2A3038" BorderThickness="1"
CornerRadius="8" Padding="8" Margin="0,0,0,4">
<Grid ColumnDefinitions="Auto,Auto,Auto,*,250" ColumnSpacing="8">
<Button Grid.Column="0" Classes="filter" Classes.active="{Binding AlertsFilterSelected}"
Content="Auffällig" Command="{Binding ShowAlertsCommand}"/>
<Button Grid.Column="1" Classes="filter" Classes.active="{Binding ClassRegisterFilterSelected}"
Content="Klassenbuch" Command="{Binding ShowClassRegisterCommand}"/>
<Button Grid.Column="2" Classes="filter" Classes.active="{Binding AllFilterSelected}"
Content="Alle" Command="{Binding ShowAllCommand}"/>
<TextBox Grid.Column="4" Text="{Binding SearchText, Mode=TwoWay}" PlaceholderText="Schüler*in suchen…"
FontSize="12" MinHeight="32"/>
</Grid>
</Border>
<ScrollViewer Grid.Row="1">
<StackPanel>
<TextBlock Classes="sectionLabel" Text="{Binding PrimarySectionTitle}" IsVisible="{Binding HasPrimaryRoster}"/>
<ItemsControl ItemsSource="{Binding PrimaryRoster}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ClassTeacherRosterRow">
<Button Classes="rosterRow"
Command="{Binding $parent[ItemsControl].((vm:ClassTeacherOverviewViewModel)DataContext).ShowDetailsForStudentCommand}"
CommandParameter="{Binding}">
<Grid ColumnDefinitions="4,42,220,*,150,18" Margin="0">
<Border Grid.Column="0" Background="{Binding StatusBarColor}"/>
<Border Grid.Column="1" Width="28" Height="28" CornerRadius="14" BorderBrush="#4A515C"
BorderThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="{Binding Initials}" FontSize="10" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock Grid.Column="2" Text="{Binding StudentName}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="3" Text="{Binding StatusText}" Foreground="{Binding StatusColor}"
VerticalAlignment="Center" FontSize="12" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="4" Text="{Binding ClassRegisterLabel}" Foreground="#5795F5"
VerticalAlignment="Center" FontSize="11"/>
<TextBlock Grid.Column="5" Text="" FontSize="18" Opacity="0.7" VerticalAlignment="Center"/>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Classes="sectionLabel" Text="{Binding SecondarySectionTitle}" IsVisible="{Binding HasSecondaryRoster}"/>
<ItemsControl ItemsSource="{Binding SecondaryRoster}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ClassTeacherRosterRow">
<Button Classes="rosterRow"
Command="{Binding $parent[ItemsControl].((vm:ClassTeacherOverviewViewModel)DataContext).ShowDetailsForStudentCommand}"
CommandParameter="{Binding}">
<Grid ColumnDefinitions="4,42,220,*,150,18">
<Border Grid.Column="0" Background="{Binding StatusBarColor}"/>
<Border Grid.Column="1" Width="28" Height="28" CornerRadius="14" BorderBrush="#4A515C" BorderThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="{Binding Initials}" FontSize="10" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock Grid.Column="2" Text="{Binding StudentName}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="3" Text="{Binding StatusText}" Foreground="{Binding StatusColor}"
VerticalAlignment="Center" FontSize="12"/>
<TextBlock Grid.Column="4" Text="{Binding ClassRegisterLabel}" Foreground="#5795F5" VerticalAlignment="Center" FontSize="11"/>
<TextBlock Grid.Column="5" Text="" FontSize="18" Opacity="0.7" VerticalAlignment="Center"/>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel IsVisible="{Binding HasNoFilterResults}" HorizontalAlignment="Center" Margin="0,54,0,0" Spacing="6">
<TextBlock Text="Keine passenden Schüler*innen" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock Text="Passe Filter oder Suche an." FontSize="12" Opacity="0.55"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
<ScrollViewer Grid.Column="1">
<StackPanel>
<Border Classes="sidePanel">
<StackPanel Spacing="9">
<TextBlock Text="Tagesüberblick" FontSize="14" FontWeight="SemiBold"/>
<Grid ColumnDefinitions="10,*,Auto,40"><Border Width="7" Height="7" CornerRadius="4" Background="#73C45A"/><TextBlock Grid.Column="1" Text="Anwesend" FontSize="12"/><TextBlock Grid.Column="2" Text="{Binding PresentCount}"/><TextBlock Grid.Column="3" Text="{Binding PresentPercent, StringFormat='{}{0} %'}" Opacity="0.6" HorizontalAlignment="Right"/></Grid>
<Grid ColumnDefinitions="10,*,Auto,40"><Border Width="7" Height="7" CornerRadius="4" Background="#F59E0B"/><TextBlock Grid.Column="1" Text="Verspätet" FontSize="12"/><TextBlock Grid.Column="2" Text="{Binding LateCount}"/><TextBlock Grid.Column="3" Text="{Binding LatePercent, StringFormat='{}{0} %'}" Opacity="0.6" HorizontalAlignment="Right"/></Grid>
<Grid ColumnDefinitions="10,*,Auto,40"><Border Width="7" Height="7" CornerRadius="4" Background="#4C8DFF"/><TextBlock Grid.Column="1" Text="Fehlzeit · entschuldigt" FontSize="12"/><TextBlock Grid.Column="2" Text="{Binding ExcusedAbsenceCount}"/><TextBlock Grid.Column="3" Text="{Binding ExcusedAbsencePercent, StringFormat='{}{0} %'}" Opacity="0.6" HorizontalAlignment="Right"/></Grid>
<Grid ColumnDefinitions="10,*,Auto,40"><Border Width="7" Height="7" CornerRadius="4" Background="#FF5A67"/><TextBlock Grid.Column="1" Text="Fehlzeit · unentschuldigt" FontSize="12"/><TextBlock Grid.Column="2" Text="{Binding UnexcusedAbsenceCount}"/><TextBlock Grid.Column="3" Text="{Binding UnexcusedAbsencePercent, StringFormat='{}{0} %'}" Opacity="0.6" HorizontalAlignment="Right"/></Grid>
</StackPanel>
</Border>
<Border Classes="sidePanel">
<StackPanel Spacing="8">
<TextBlock Text="Trend · letzte 7 Tage" FontSize="14" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding TrendDays}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ClassTeacherTrendDay">
<Grid ColumnDefinitions="42,*,24" Margin="0,2">
<TextBlock Text="{Binding DayLabel}" FontSize="11" Opacity="0.6" VerticalAlignment="Center"/>
<Grid Grid.Column="1" Height="8" VerticalAlignment="Center">
<Border Background="#252B33" CornerRadius="4"/>
<Border Width="{Binding AlertBarWidth}" HorizontalAlignment="Left" Background="#F59E0B" CornerRadius="4"/>
</Grid>
<TextBlock Grid.Column="2" Text="{Binding AlertCount}" FontSize="11" HorizontalAlignment="Right"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Betroffene Schüler*innen pro Tag" FontSize="10" Opacity="0.45"/>
</StackPanel>
</Border>
<Border Classes="sidePanel" IsVisible="{Binding HasPatternNotices}">
<StackPanel Spacing="8">
<TextBlock Text="Muster erkannt" FontSize="14" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding PatternNotices}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ClassTeacherPatternNotice">
<Grid ColumnDefinitions="4,*" Margin="0,3">
<Border Background="{Binding Color}" CornerRadius="2" Margin="0,1,8,1"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding StudentName}" FontSize="12" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Message}" FontSize="11" Opacity="0.62" TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<Border Classes="sidePanel">
<StackPanel Spacing="8">
<TextBlock Text="Nächste Schritte" FontSize="14" FontWeight="SemiBold"/>
<Button Content="Klassenbuch öffnen" Command="{Binding OpenClassRegisterCommand}" HorizontalAlignment="Stretch"/>
<Button Content="Fehlzeiten öffnen" Command="{Binding OpenAbsencesCommand}" HorizontalAlignment="Stretch"/>
<Button Content="Aufgaben &amp; Wiedervorlagen" Command="{Binding GoToWorkloadCommand}"
HorizontalAlignment="Stretch" Background="Transparent"/>
</StackPanel>
</Border>
<TextBlock Text="{Binding Status}" FontSize="11" TextWrapping="Wrap" Opacity="0.6"/>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid> </Grid>
</ContentPage> </ContentPage>
<ContentPage Header="Details"> <ContentPage Header="Klassenbuch">
<views:ClassTeacherDetailsView DataContext="{Binding DetailsTab}"/> <views:ClassTeacherRegisterView DataContext="{Binding DetailsTab}"/>
</ContentPage>
<ContentPage Header="Fehlzeiten">
<views:ClassTeacherAbsencesView DataContext="{Binding DetailsTab}"/>
</ContentPage> </ContentPage>
</TabbedPage> </TabbedPage>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -0,0 +1,58 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherRegisterView"
x:DataType="vm:ClassTeacherDetailsViewModel">
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="16" RowSpacing="10">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<StackPanel>
<TextBlock Text="Klassenbucheinträge" FontSize="22" FontWeight="SemiBold"/>
<TextBlock Text="Einträge anderer Lehrkräfte nur zur Ansicht" FontSize="12" Opacity="0.55"/>
</StackPanel>
<Button Grid.Column="1" Content="↻ Aus WebUntis aktualisieren" Command="{Binding RefreshCommand}"
IsEnabled="{Binding !Busy}" Background="Transparent" Foreground="#5795F5" VerticalAlignment="Center"/>
</Grid>
<Border Grid.Row="1" Background="#11151B" BorderBrush="#2A3038" BorderThickness="1"
CornerRadius="8" Padding="10">
<Grid RowDefinitions="Auto,Auto" RowSpacing="8">
<Grid Grid.Row="0" ColumnDefinitions="155,Auto,*,Auto,*" ColumnSpacing="8">
<ComboBox Grid.Column="0" SelectedIndex="{Binding QuickRangeIndex, Mode=TwoWay}">
<ComboBoxItem Content="Heute"/><ComboBoxItem Content="Letzte 7 Tage"/><ComboBoxItem Content="Letzte 30 Tage"/>
</ComboBox>
<TextBlock Grid.Column="1" Text="von" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="2" SelectedDate="{Binding StartDate}" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Column="3" Text="bis" VerticalAlignment="Center" Opacity="0.6"/>
<DatePicker Grid.Column="4" SelectedDate="{Binding EndDate}" HorizontalAlignment="Stretch"/>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
<TextBox Grid.Column="0" Text="{Binding StudentFilter, Mode=TwoWay}" PlaceholderText="Schüler*in filtern…"/>
<Button Grid.Column="1" Content="Anzeigen" Command="{Binding ApplyFilterCommand}" IsEnabled="{Binding !Busy}"/>
<Button Grid.Column="2" Content="Filter löschen" Command="{Binding ClearStudentFilterCommand}"
IsVisible="{Binding StudentFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</Grid>
</Grid>
</Border>
<Grid Grid.Row="2">
<DataGrid ItemsSource="{Binding Entries}" AutoGenerateColumns="False" IsReadOnly="True"
GridLinesVisibility="Horizontal" BorderBrush="#2A3038" BorderThickness="1"
CanUserResizeColumns="True" CanUserSortColumns="True" RowHeight="46">
<DataGrid.Columns>
<DataGridTextColumn Header="Datum" Binding="{Binding DateLabel}" Width="105"/>
<DataGridTextColumn Header="Schüler*in" Binding="{Binding StudentDisplayName}" Width="1.25*"/>
<DataGridTextColumn Header="Fach" Binding="{Binding Subject}" Width="0.7*"/>
<DataGridTextColumn Header="Lehrkraft" Binding="{Binding TeacherUsername}" Width="0.8*"/>
<DataGridTextColumn Header="Kategorie" Binding="{Binding CategoryName}" Width="1*"/>
<DataGridTextColumn Header="Gruppe" Binding="{Binding CategoryGroup}" Width="0.8*"/>
<DataGridTextColumn Header="Eintrag" Binding="{Binding Text}" Width="2*"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel IsVisible="{Binding !HasEntries}" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="5">
<TextBlock Text="Keine Klassenbucheinträge im gewählten Zeitraum" FontWeight="SemiBold"/>
<TextBlock Text="Passe Zeitraum oder Schülerfilter an." FontSize="12" Opacity="0.55"/>
</StackPanel>
</Grid>
<TextBlock Grid.Row="3" Text="{Binding Status}" FontSize="11" Opacity="0.6"/>
</Grid>
</UserControl>
@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace LehrerApp.Desktop.Views.ClassTeacher;
public partial class ClassTeacherRegisterView : UserControl
{
public ClassTeacherRegisterView() => InitializeComponent();
}