Redesign Klassenlehrer Tab.
This commit is contained in:
@@ -126,6 +126,51 @@ public sealed class ClassTeacherViewModelsTests
|
||||
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) =>
|
||||
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)
|
||||
{
|
||||
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 -
|
||||
@@ -25,6 +34,14 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
|
||||
string? Note, string? ExcuseNote, bool CountsAsFullDay)
|
||||
{
|
||||
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 PeriodsLabel => string.Join(", ", PeriodNumbers.Order());
|
||||
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.
|
||||
public string RowBackground => CountsAsFullDay ? "#1FFFA000" : "Transparent";
|
||||
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(
|
||||
IEnumerable<UntisClassAbsenceEntryDto> entries) => entries
|
||||
@@ -78,12 +106,18 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
public ObservableCollection<ClassTeacherClassRegisterRow> Entries { 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 string _status = "Zeitraum wählen und laden.";
|
||||
[ObservableProperty] private bool _busy;
|
||||
/// Von der Übersicht gesetzt (Klick auf eine Roster-Zeile) - leer zeigt alle Schüler*innen.
|
||||
[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)
|
||||
{
|
||||
@@ -93,9 +127,20 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
public void Initialize(string className)
|
||||
{
|
||||
_className = className;
|
||||
StudentFilter = "";
|
||||
Entries.Clear();
|
||||
AbsenceEntries.Clear();
|
||||
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]
|
||||
@@ -106,6 +151,16 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
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)
|
||||
{
|
||||
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 (string.IsNullOrWhiteSpace(_className)) { Status = "Keine Klasse ausgewählt."; return; }
|
||||
|
||||
Busy = true; Entries.Clear(); AbsenceEntries.Clear();
|
||||
Busy = true; Entries.Clear(); AbsenceEntries.Clear(); NotifyListState();
|
||||
try
|
||||
{
|
||||
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, " +
|
||||
$"{AbsenceEntries.Count} Fehlzeiten-Tage im Zeitraum.";
|
||||
NotifyListState();
|
||||
}
|
||||
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
|
||||
@@ -153,4 +209,10 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
|
||||
private static bool TryDate(int value, out DateOnly 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;
|
||||
|
||||
/// <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,
|
||||
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(
|
||||
IReadOnlyList<UntisStudentRosterCacheEntry> students,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences,
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto> recentClassRegisterEntries)
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto> recentClassRegisterEntries,
|
||||
DateOnly? today = null)
|
||||
{
|
||||
var absenceByKey = todayAbsences
|
||||
.Where(a => a.ExternKey is not null)
|
||||
.GroupBy(a => a.ExternKey!.Value)
|
||||
var referenceDate = today ?? todayAbsences.FirstOrDefault()?.Date ?? DateOnly.FromDateTime(DateTime.Today);
|
||||
var absenceByKey = todayAbsences.Where(a => a.ExternKey is not null)
|
||||
.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());
|
||||
var absenceByName = todayAbsences
|
||||
.GroupBy(a => UntisNameMatching.NameKey(a.StudentName))
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
var recentNames = recentClassRegisterEntries
|
||||
.Select(e => UntisNameMatching.NameKey(e.StudentName))
|
||||
.ToHashSet();
|
||||
var recentByName = recentClassRegisterEntries.GroupBy(e => UntisNameMatching.NameKey(e.StudentName))
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
return students
|
||||
.Select(s =>
|
||||
return students.Select(student =>
|
||||
{
|
||||
var nameKey = UntisNameMatching.NameKey(s.DisplayName);
|
||||
var absence = (s.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null)
|
||||
var nameKey = UntisNameMatching.NameKey(student.DisplayName);
|
||||
var absence = (student.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null)
|
||||
?? 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}",
|
||||
recentNames.Contains(nameKey));
|
||||
registerEntries.Count > 0)
|
||||
{
|
||||
TodayAbsence = absence,
|
||||
HasClassRegisterToday = registerEntries.Any(e => TryDate(e.Date, out var date) && date == referenceDate),
|
||||
};
|
||||
})
|
||||
.OrderBy(r => r.StudentName)
|
||||
.ToList();
|
||||
}
|
||||
.OrderBy(r => r.AttentionRank).ThenBy(r => r.StudentName).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Top-Level-Bereich "Klassenlehrer" (eigener Sidebar-Eintrag, siehe MainWindowViewModel) - bewusst
|
||||
/// nicht an eine LearningGroup gehängt, da man Klassenlehrer für eine ganze Klasse ist, unabhängig
|
||||
/// 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>
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
}
|
||||
|
||||
public sealed record ClassTeacherTrendDay(string DayLabel, int AlertCount, int UnexcusedCount, int LateCount,
|
||||
double AlertBarWidth, double UnexcusedBarWidth, double LateBarWidth);
|
||||
public sealed record ClassTeacherPatternNotice(string StudentName, string Message, string Color);
|
||||
|
||||
public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
private readonly WebUntisSettingsService _settings;
|
||||
@@ -69,57 +98,132 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
|
||||
public ClassTeacherDetailsViewModel DetailsTab { 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 int _activeTabIndex;
|
||||
[ObservableProperty] private string _status = "";
|
||||
[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 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>? OnNavigateToWorkload { get; set; }
|
||||
|
||||
public ClassTeacherOverviewViewModel(WebUntisSettingsService settings,
|
||||
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 OnSearchTextChanged(string value) => ApplyRosterFilter();
|
||||
partial void OnSelectedRosterFilterChanged(int value)
|
||||
{
|
||||
OnPropertyChanged(nameof(AlertsFilterSelected));
|
||||
OnPropertyChanged(nameof(ClassRegisterFilterSelected));
|
||||
OnPropertyChanged(nameof(AllFilterSelected));
|
||||
ApplyRosterFilter();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Load()
|
||||
{
|
||||
HomeroomClassName = _settings.HomeroomClassName;
|
||||
ActiveTabIndex = 0;
|
||||
Roster.Clear();
|
||||
Roster.Clear(); PrimaryRoster.Clear(); SecondaryRoster.Clear(); TrendDays.Clear(); PatternNotices.Clear();
|
||||
if (!HomeroomClassConfigured)
|
||||
{
|
||||
Status = "Noch keine Klasse ausgewählt.";
|
||||
NotifyRosterState();
|
||||
return;
|
||||
}
|
||||
|
||||
var className = HomeroomClassName!;
|
||||
DetailsTab.Initialize(className);
|
||||
Busy = true;
|
||||
NotifyRosterState();
|
||||
try
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var start = today.AddDays(-6);
|
||||
var studentsTask = _cache.GetStudentRosterAsync(className);
|
||||
var absencesTask = _cache.GetAbsencesAsync(className, today, today);
|
||||
var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, today.AddDays(-7), today);
|
||||
var absencesTask = _cache.GetAbsencesAsync(className, start, today);
|
||||
var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, start, today);
|
||||
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,
|
||||
classRegisterTask.Result))
|
||||
Roster.Add(row);
|
||||
Status = $"{Roster.Count} Schüler*innen · Stand heute.";
|
||||
classRegisterTask.Result, today)) Roster.Add(row);
|
||||
|
||||
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; }
|
||||
finally { Busy = false; }
|
||||
catch (WebUntisIntegrationException ex) { Status = ex.Message; NotifyRosterState(); }
|
||||
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]
|
||||
@@ -127,13 +231,87 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
if (row is null) return;
|
||||
DetailsTab.StudentFilter = row.StudentName;
|
||||
ActiveTabIndex = 1;
|
||||
ActiveTabIndex = row.HasAbsenceToday ? 2 : 1;
|
||||
DetailsTab.LoadCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task GoToSettings()
|
||||
[RelayCommand] 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>();
|
||||
vm.OnNavigateToSettings = () => { NavigateToSettings(SettingsTab.WebUntis); return Task.CompletedTask; };
|
||||
vm.OnNavigateToWorkload = () => { NavigateToWorkload(); return Task.CompletedTask; };
|
||||
vm.LoadCommand.Execute(null);
|
||||
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: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>
|
||||
<!-- Hinweis, solange in den Einstellungen keine Klasse ausgewählt ist -->
|
||||
<StackPanel IsVisible="{Binding !HomeroomClassConfigured}" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" Spacing="12" MaxWidth="360">
|
||||
<TextBlock Text="🎓" FontSize="40" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Noch keine Klasse ausgewählt" FontSize="16" FontWeight="SemiBold" HorizontalAlignment="Center"/>
|
||||
<TextBlock TextWrapping="Wrap" TextAlignment="Center" FontSize="13" Opacity="0.7"
|
||||
VerticalAlignment="Center" Spacing="12" MaxWidth="380">
|
||||
<Border Width="56" Height="56" CornerRadius="28" Background="#173765" HorizontalAlignment="Center">
|
||||
<TextBlock Text="KL" FontSize="18" FontWeight="Bold" Foreground="#66A3FF"
|
||||
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."/>
|
||||
<Button Content="Zu den Einstellungen" Command="{Binding GoToSettingsCommand}" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<TabbedPage IsVisible="{Binding HomeroomClassConfigured}" TabPlacement="Top"
|
||||
SelectedIndex="{Binding ActiveTabIndex, Mode=TwoWay}">
|
||||
|
||||
<ContentPage Header="Übersicht">
|
||||
<Grid RowDefinitions="Auto,Auto,*" Margin="16" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Text="{Binding HomeroomClassName, StringFormat='Klasse {0}'}"
|
||||
FontSize="18" FontWeight="SemiBold"/>
|
||||
<TextBlock Grid.Row="1" Text="{Binding Status}" FontSize="12" Opacity="0.7"/>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<ItemsControl ItemsSource="{Binding Roster}">
|
||||
<Grid RowDefinitions="Auto,Auto,*" Margin="16,12,16,16" RowSpacing="10">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="{Binding HomeroomClassName, StringFormat='Klasse {0}'}"
|
||||
FontSize="24" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Klassenlehrer-Übersicht" FontSize="13" Opacity="0.58"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding LastUpdatedLabel}" FontSize="11" Opacity="0.55" VerticalAlignment="Center"/>
|
||||
<Button Content="↻ Aktualisieren" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"
|
||||
Background="Transparent" BorderThickness="0" Foreground="#5795F5"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,*,*,*">
|
||||
<Border Grid.Column="0" Classes="metric">
|
||||
<Grid ColumnDefinitions="36,*">
|
||||
<TextBlock Text="◉" FontSize="22" Foreground="#4C8DFF" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1"><TextBlock Text="{Binding StudentCount}" FontSize="22" FontWeight="SemiBold"/><TextBlock Text="Schüler*innen" FontSize="11" Opacity="0.6"/></StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Grid.Column="1" Classes="metric">
|
||||
<Grid ColumnDefinitions="36,*">
|
||||
<TextBlock Text="△" FontSize="24" Foreground="#F59E0B" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1"><TextBlock Text="{Binding TodayAlertCount}" FontSize="22" FontWeight="SemiBold"/><TextBlock Text="heute auffällig" FontSize="11" Opacity="0.6"/></StackPanel>
|
||||
</Grid>
|
||||
</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 HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||
Background="Transparent" BorderThickness="0" Padding="8,6"
|
||||
<Button Classes="rosterRow"
|
||||
Command="{Binding $parent[ItemsControl].((vm:ClassTeacherOverviewViewModel)DataContext).ShowDetailsForStudentCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding StatusIcon}" FontSize="16" Width="28"
|
||||
ToolTip.Tip="{Binding AbsenceTooltip}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding StudentName}" VerticalAlignment="Center"/>
|
||||
<Border Grid.Column="2" Background="#1976D2" CornerRadius="8" Padding="6,1"
|
||||
IsVisible="{Binding HasRecentClassRegisterEntry}"
|
||||
ToolTip.Tip="Klassenbucheintrag einer anderen Lehrkraft in den letzten 7 Tagen">
|
||||
<TextBlock Text="Klassenbuch" Foreground="White" FontSize="10"/>
|
||||
<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 & 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>
|
||||
</ContentPage>
|
||||
|
||||
<ContentPage Header="Details">
|
||||
<views:ClassTeacherDetailsView DataContext="{Binding DetailsTab}"/>
|
||||
<ContentPage Header="Klassenbuch">
|
||||
<views:ClassTeacherRegisterView DataContext="{Binding DetailsTab}"/>
|
||||
</ContentPage>
|
||||
<ContentPage Header="Fehlzeiten">
|
||||
<views:ClassTeacherAbsencesView DataContext="{Binding DetailsTab}"/>
|
||||
</ContentPage>
|
||||
|
||||
</TabbedPage>
|
||||
</Grid>
|
||||
</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();
|
||||
}
|
||||
Reference in New Issue
Block a user