WIP (unstable): Klassenlehrer-Tab (fremde Klassenbucheinträge + Fehlzeiten der Klasse)
Neuer Tab in der Gruppendetailansicht (LearningGroup.IsClassTeacher-Flag), zeigt Klassenbucheinträge anderer Lehrkräfte sowie Fehlzeiten der ganzen Klasse über alle Fächer, pro Schüler*in und Tag zusammengefasst. Beides rein lesend, kein Übernehmen in lokale Daten. Fehlzeiten-Bericht (AbsencePerStudent) adressiert die Klasse über "KL"+getKlassen-ID, automatisch aufgelöst - noch nicht mit echtem WebUntis-Zugang gegengeprüft (siehe TODO.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -308,6 +308,7 @@ public static class AppBootstrapper
|
||||
services.AddTransient<CompetencyOverviewTabViewModel>();
|
||||
services.AddTransient<SeatingPlanTabViewModel>();
|
||||
services.AddTransient<GroupDocumentationTabViewModel>();
|
||||
services.AddTransient<ClassTeacherTabViewModel>();
|
||||
services.AddTransient<AddGroupDialogViewModel>();
|
||||
services.AddTransient<TrashViewModel>();
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
|
||||
@@ -32,6 +32,12 @@ public sealed record UntisLessonAbsenceDto(string StudentName, int Date, int Abs
|
||||
string? Reason, int? ExternKey, bool ExternKeyInParentheses, string? HandledOn, bool Counts);
|
||||
public sealed record UntisClassRegisterEventDto(string ClassName, int Date, string? Subject,
|
||||
string StudentName, string? CategoryName, string? CategoryGroup, string? Text);
|
||||
public sealed record UntisForeignClassRegisterEventDto(string ClassName, int Date, string? Subject,
|
||||
string StudentName, string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text);
|
||||
public sealed record UntisClassAbsenceEntryDto(string StudentName, int? ExternKey, string ClassName, int Date,
|
||||
int AbsentPeriods, int AbsentMinutes, string? TeacherUsernames, string? Subject, string? AbsenceReason,
|
||||
string? Note, int? EntryId, string? HandledOn, bool Counts, string? ExcuseNote, int? PeriodNumber,
|
||||
string? Status, bool CountsAsFullDay);
|
||||
|
||||
/// <summary>Direkter WebUntis-Client des Desktops. Personenbezogene Antworten und der
|
||||
/// unverschlüsselte CSV-Report passieren zu keinem Zeitpunkt den LehrerApp-Server.</summary>
|
||||
@@ -135,6 +141,65 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings
|
||||
.ToList();
|
||||
}, token);
|
||||
|
||||
// Spiegelbild von GetOwnClassRegisterEventsAsync für das "Klassenlehrer"-Feature (siehe TODO.md):
|
||||
// dieselben "-alle-"-Rohdaten, aber Einträge anderer Lehrkräfte statt der eigenen, zusätzlich auf
|
||||
// die übergebene Klasse eingeschränkt (der "-alle-"-Bericht liefert sonst auch andere Klassen mit,
|
||||
// sobald eine Lehrkraft mehrere Klassen unterrichtet).
|
||||
public Task<IReadOnlyList<UntisForeignClassRegisterEventDto>> GetForeignClassRegisterEventsAsync(
|
||||
string className, DateOnly start, DateOnly end, CancellationToken token = default) => ExecuteAsync(async client =>
|
||||
{
|
||||
var ownUsername = settings.GetApiCredentials()?.Username;
|
||||
var entries = await client.GetClassRegisterEventsReportAsync(Date(start), Date(end), token);
|
||||
return (IReadOnlyList<UntisForeignClassRegisterEventDto>)entries
|
||||
.Where(x => string.Equals(x.ClassName, className, StringComparison.OrdinalIgnoreCase)
|
||||
// Nicht einfach "!=" gegen ownUsername: wäre ownUsername unbekannt (null), würde
|
||||
// ein ebenfalls fehlender TeacherUsername sonst fälschlich als "eigen" erkannt
|
||||
// (Equals(null,null)==true) und rausgefiltert statt als fremd angezeigt zu werden.
|
||||
&& !(ownUsername is not null
|
||||
&& string.Equals(x.TeacherUsername, ownUsername, StringComparison.OrdinalIgnoreCase)))
|
||||
.Select(x => new UntisForeignClassRegisterEventDto(x.ClassName, x.Date, x.Subject, x.StudentName,
|
||||
x.TeacherUsername, x.CategoryName, x.CategoryGroup, x.Text))
|
||||
.ToList();
|
||||
}, token);
|
||||
|
||||
// Fehlzeiten der ganzen Klasse über alle Fächer, Ergänzung zu GetForeignClassRegisterEventsAsync
|
||||
// für das "Klassenlehrer"-Feature. Der Bericht adressiert die Klasse über "KL"+getKlassen-ID statt
|
||||
// eines Namens, siehe ResolveClassIdAsync.
|
||||
public Task<IReadOnlyList<UntisClassAbsenceEntryDto>> GetClassAbsencesAsync(string className,
|
||||
DateOnly start, DateOnly end, CancellationToken token = default) => ExecuteAsync(async client =>
|
||||
{
|
||||
var classId = await ResolveClassIdAsync(client, className, token);
|
||||
var entries = await client.GetClassAbsencesAsync(classId, Date(start), Date(end), token);
|
||||
return (IReadOnlyList<UntisClassAbsenceEntryDto>)entries.Select(x => new UntisClassAbsenceEntryDto(
|
||||
x.StudentName, x.ExternKey, x.ClassName, x.Date, x.AbsentPeriods, x.AbsentMinutes,
|
||||
x.TeacherUsernames, x.Subject, x.AbsenceReason, x.Note, x.EntryId, x.HandledOn, x.Counts,
|
||||
x.ExcuseNote, x.PeriodNumber, x.Status, x.CountsAsFullDay))
|
||||
.ToList();
|
||||
}, token);
|
||||
|
||||
// "KL" + getKlassen-ID, dasselbe Element-Kürzel-Schema wie kl/te/su/ro bei anderen Berichten
|
||||
// (siehe Entities(...) unten). Unbestätigt (kein offizieller Report-Parameter dokumentiert), aber
|
||||
// aus einem echten Browser-Request rückgeschlossen - schlägt die Zuordnung fehl, liefert der
|
||||
// Bericht erkennbar leere/falsche Daten statt eines stillen Fehlers.
|
||||
private static async Task<string> ResolveClassIdAsync(WebUntisClient client, string className,
|
||||
CancellationToken token)
|
||||
{
|
||||
var years = await client.GetSchoolYearsAsync(token);
|
||||
var today = Date(DateOnly.FromDateTime(DateTime.Today));
|
||||
var year = years.FirstOrDefault(y => y.StartDate <= today && y.EndDate >= today)
|
||||
?? years.OrderByDescending(y => y.StartDate).FirstOrDefault()
|
||||
?? throw new WebUntisIntegrationException("WebUntis hat kein Schuljahr geliefert.");
|
||||
var classes = await client.GetClassesAsync(year.UntisId, token);
|
||||
var matches = classes.Where(c => string.Equals(c.Name, className, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
return matches.Count switch
|
||||
{
|
||||
1 => $"KL{matches[0].UntisId}",
|
||||
0 => throw new WebUntisIntegrationException($"WebUntis kennt keine Klasse namens \"{className}\"."),
|
||||
_ => throw new WebUntisIntegrationException(
|
||||
$"Mehrere WebUntis-Klassen heißen \"{className}\" - bitte manuell prüfen."),
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteAsync<T>(Func<WebUntisClient, Task<T>> operation, CancellationToken token)
|
||||
{
|
||||
try { return await operation(await GetClientAsync(token)); }
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Desktop.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
/// <summary>Eine Zeile aus dem "-alle-"-Klassenbuchbericht, die eine andere Lehrkraft angelegt hat.
|
||||
/// Rein zur Ansicht - anders als beim Dokumentations-Abgleich (eigene Einträge) gibt es hier kein
|
||||
/// "Übernehmen", da es keine eigenen Einträge zum lokalen Synchronisieren sind.</summary>
|
||||
public sealed record ClassTeacherClassRegisterRow(DateOnly Date, string? Subject, string StudentName,
|
||||
string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text)
|
||||
{
|
||||
public string DateLabel => Date.ToString("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
/// <summary>Fehlzeiten eines/einer Schüler*in an einem Tag, über alle Fächer zusammengefasst -
|
||||
/// der WebUntis-Bericht liefert eine Zeile pro Fehlstunde, was bei einer ganzen Klasse über mehrere
|
||||
/// Wochen zu unübersichtlich wäre (Nutzer-Feedback: kompakte "auf einen Blick"-Übersicht statt
|
||||
/// Rohdaten). Die Rohzeilen (<see cref="UntisClassAbsenceEntryDto"/>) bleiben trotzdem erreichbar,
|
||||
/// falls später ein Detail-Drill-down pro Schüler*in dazukommt (siehe TODO.md).</summary>
|
||||
public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName, int? ExternKey,
|
||||
int TotalAbsentPeriods, int TotalAbsentMinutes, IReadOnlyList<string> Subjects,
|
||||
IReadOnlyList<int> PeriodNumbers, IReadOnlyList<string> Statuses, IReadOnlyList<string> AbsenceReasons,
|
||||
string? Note, string? ExcuseNote, bool CountsAsFullDay)
|
||||
{
|
||||
public string DateLabel => Date.ToString("dd.MM.yyyy");
|
||||
public string SubjectsLabel => string.Join(", ", Subjects);
|
||||
public string PeriodsLabel => string.Join(", ", PeriodNumbers.Order());
|
||||
public string StatusLabel => string.Join(", ", Statuses);
|
||||
public string ReasonLabel => string.Join("; ", AbsenceReasons);
|
||||
|
||||
public static IReadOnlyList<ClassAbsenceDaySummaryRow> GroupByStudentAndDay(
|
||||
IEnumerable<UntisClassAbsenceEntryDto> entries) => entries
|
||||
.Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null))
|
||||
.Where(x => x.Date is not null)
|
||||
.GroupBy(x => (Date: x.Date!.Value, x.Entry.StudentName, x.Entry.ExternKey))
|
||||
.Select(g => new ClassAbsenceDaySummaryRow(
|
||||
g.Key.Date, g.Key.StudentName, g.Key.ExternKey,
|
||||
g.Sum(x => x.Entry.AbsentPeriods), g.Sum(x => x.Entry.AbsentMinutes),
|
||||
Distinct(g.Select(x => x.Entry.Subject)),
|
||||
g.Select(x => x.Entry.PeriodNumber).Where(p => p is not null).Select(p => p!.Value).Distinct().ToList(),
|
||||
Distinct(g.Select(x => x.Entry.Status)),
|
||||
Distinct(g.Select(x => x.Entry.AbsenceReason)),
|
||||
g.Select(x => x.Entry.Note).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)),
|
||||
g.Select(x => x.Entry.ExcuseNote).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)),
|
||||
g.Any(x => x.Entry.CountsAsFullDay)))
|
||||
.OrderByDescending(r => r.Date).ThenBy(r => r.StudentName)
|
||||
.ToList();
|
||||
|
||||
private static List<string> Distinct(IEnumerable<string?> values) =>
|
||||
values.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v!).Distinct().ToList();
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Klassenlehrer-Tab: zeigt Klassenbucheinträge, die andere Lehrkräfte zu Schülern dieser Klasse
|
||||
/// angelegt haben (WebUntis "-alle-"-Bericht, gefiltert auf fremde statt der eigenen
|
||||
/// Benutzerkennung wie beim bestehenden Dokumentations-Abgleich, siehe
|
||||
/// <see cref="WebUntisIntegrationService.GetForeignClassRegisterEventsAsync"/>), sowie die
|
||||
/// Fehlzeiten der Klasse über alle Fächer (<see cref="WebUntisIntegrationService.GetClassAbsencesAsync"/>).
|
||||
/// Beides rein zur Ansicht, kein Übernehmen in lokale Daten. Nur für Gruppen mit
|
||||
/// <see cref="Core.Models.LearningGroup.IsClassTeacher"/> sichtbar (siehe
|
||||
/// GroupDetailViewModel/GroupDetailView).
|
||||
/// </summary>
|
||||
public partial class ClassTeacherTabViewModel : ObservableObject
|
||||
{
|
||||
private readonly WebUntisIntegrationService _untis;
|
||||
private readonly IGroupRepository _groups;
|
||||
private string _className = "";
|
||||
|
||||
public ObservableCollection<ClassTeacherClassRegisterRow> Entries { get; } = [];
|
||||
public ObservableCollection<ClassAbsenceDaySummaryRow> AbsenceEntries { get; } = [];
|
||||
|
||||
[ObservableProperty] private DateTimeOffset _startDate = DateTimeOffset.Now.AddDays(-7);
|
||||
[ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now;
|
||||
[ObservableProperty] private string _status = "Zeitraum wählen und laden.";
|
||||
[ObservableProperty] private bool _busy;
|
||||
|
||||
public ClassTeacherTabViewModel(WebUntisIntegrationService untis, IGroupRepository groups)
|
||||
{
|
||||
_untis = untis; _groups = groups;
|
||||
}
|
||||
|
||||
public void Initialize(Guid groupId)
|
||||
{
|
||||
_className = _groups.GetById(groupId)?.Name ?? "";
|
||||
Entries.Clear();
|
||||
AbsenceEntries.Clear();
|
||||
Status = "Zeitraum wählen und laden.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Load()
|
||||
{
|
||||
var start = DateOnly.FromDateTime(StartDate.LocalDateTime);
|
||||
var end = DateOnly.FromDateTime(EndDate.LocalDateTime);
|
||||
if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
|
||||
if (string.IsNullOrWhiteSpace(_className)) { Status = "Dieser Gruppe fehlt ein Klassenname."; return; }
|
||||
|
||||
Busy = true; Entries.Clear(); AbsenceEntries.Clear();
|
||||
try
|
||||
{
|
||||
var classRegisterTask = _untis.GetForeignClassRegisterEventsAsync(_className, start, end);
|
||||
var absencesTask = _untis.GetClassAbsencesAsync(_className, start, end);
|
||||
await Task.WhenAll(classRegisterTask, absencesTask);
|
||||
|
||||
var ordered = classRegisterTask.Result
|
||||
.Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null))
|
||||
.Where(x => x.Date is not null)
|
||||
.OrderByDescending(x => x.Date).ThenBy(x => x.Entry.StudentName);
|
||||
foreach (var (entry, date) in ordered)
|
||||
{
|
||||
Entries.Add(new ClassTeacherClassRegisterRow(date!.Value, entry.Subject, entry.StudentName,
|
||||
entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text));
|
||||
}
|
||||
|
||||
foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result))
|
||||
AbsenceEntries.Add(row);
|
||||
|
||||
Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " +
|
||||
$"{AbsenceEntries.Count} Fehlzeiten-Tage im Zeitraum.";
|
||||
}
|
||||
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
|
||||
finally { Busy = false; }
|
||||
}
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
}
|
||||
@@ -251,6 +251,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
|
||||
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
|
||||
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
|
||||
public bool IsClassTeacher => Group?.IsClassTeacher ?? false;
|
||||
public bool IsReadOnly => Group is { IsActive: false };
|
||||
public bool IsEditable => Group is { IsActive: true };
|
||||
public string SubjectName { get; private set; } = "";
|
||||
@@ -258,6 +259,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
partial void OnGroupChanged(LearningGroup? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsDifferentiated));
|
||||
OnPropertyChanged(nameof(IsClassTeacher));
|
||||
OnPropertyChanged(nameof(IsReadOnly));
|
||||
OnPropertyChanged(nameof(IsEditable));
|
||||
NotifyWriteCommands();
|
||||
@@ -279,6 +281,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
|
||||
public SeatingPlanTabViewModel SeatingPlanTab { get; }
|
||||
public GroupDocumentationTabViewModel GroupDocumentationTab { get; }
|
||||
public ClassTeacherTabViewModel ClassTeacherTab { get; }
|
||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||
@@ -295,7 +298,8 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
GroupOverviewViewModel overviewTab, ParticipationTabViewModel participationTab,
|
||||
GradeOverviewTabViewModel gradeOverviewTab,
|
||||
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
|
||||
SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab)
|
||||
SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab,
|
||||
ClassTeacherTabViewModel classTeacherTab)
|
||||
{
|
||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||
_exams = exams; _grades = grades; _tasks = tasks;
|
||||
@@ -306,6 +310,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
CompetencyOverviewTab = competencyOverviewTab;
|
||||
SeatingPlanTab = seatingPlanTab;
|
||||
GroupDocumentationTab = groupDocumentationTab;
|
||||
ClassTeacherTab = classTeacherTab;
|
||||
SeatingPlanTab.OnAssessmentChanged = () =>
|
||||
{
|
||||
ParticipationTab.LoadSessions();
|
||||
@@ -335,6 +340,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
CompetencyOverviewTab.Initialize(Group);
|
||||
SeatingPlanTab.Initialize(Group.Id, IsReadOnly);
|
||||
GroupDocumentationTab.Initialize(Group.Id);
|
||||
ClassTeacherTab.Initialize(Group.Id);
|
||||
}
|
||||
|
||||
private void ReloadExams()
|
||||
@@ -877,6 +883,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private int? _hoursPerWeek;
|
||||
[ObservableProperty] private bool _isOwnClass;
|
||||
[ObservableProperty] private bool _isDifferentiated;
|
||||
[ObservableProperty] private bool _isClassTeacher;
|
||||
[ObservableProperty] private bool _requiresLessonPlanning = true;
|
||||
[ObservableProperty] private int? _webUntisLessonId;
|
||||
[ObservableProperty] private string _nameError = "";
|
||||
@@ -917,6 +924,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
HoursPerWeek = group.HoursPerWeek;
|
||||
IsOwnClass = group.IsOwnClass;
|
||||
IsDifferentiated = group.IsDifferentiated;
|
||||
IsClassTeacher = group.IsClassTeacher;
|
||||
RequiresLessonPlanning = group.RequiresLessonPlanning;
|
||||
WebUntisLessonId = group.WebUntisLessonId;
|
||||
OnPropertyChanged(nameof(DialogTitle));
|
||||
@@ -964,6 +972,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
Result.HoursPerWeek = HoursPerWeek;
|
||||
Result.IsOwnClass = IsOwnClass;
|
||||
Result.IsDifferentiated = IsDifferentiated;
|
||||
Result.IsClassTeacher = IsClassTeacher;
|
||||
Result.RequiresLessonPlanning = RequiresLessonPlanning;
|
||||
Result.WebUntisLessonId = WebUntisLessonId;
|
||||
_groups.Save(Result);
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
<CheckBox Content="Gehört zu meiner Klasse" IsChecked="{Binding IsOwnClass}"
|
||||
ToolTip.Tip="Für die farbliche Hervorhebung im Dashboard-Kalender. Mehrere Gruppen (Klasse + zugehörige Kurse) können markiert werden."/>
|
||||
|
||||
<CheckBox Content="Klassenlehrer/-in dieser Klasse" IsChecked="{Binding IsClassTeacher}"
|
||||
ToolTip.Tip="Blendet den Klassenlehrer-Tab ein (Klassenbucheinträge anderer Lehrkräfte zu dieser Klasse). Sollte nur an genau einer Gruppe gesetzt werden."/>
|
||||
|
||||
<CheckBox Content="Niveaudifferenzierung (E/G/Förder)" IsChecked="{Binding IsDifferentiated}"
|
||||
ToolTip.Tip="Blendet im Schüler-Tab eine Niveau-Zuordnung je Schüler ein und erlaubt in Klausuren die Zuordnung zu einem Niveau."/>
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.ClassTeacherTabView"
|
||||
x:DataType="vm:ClassTeacherTabViewModel">
|
||||
|
||||
<Grid RowDefinitions="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}"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="Klassenbucheinträge anderer Lehrkräfte" FontWeight="SemiBold" FontSize="13" Margin="0,4,0,0"/>
|
||||
<Grid Grid.Row="3" 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="4">
|
||||
<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="5" 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="6" 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="7">
|
||||
<ItemsControl ItemsSource="{Binding AbsenceEntries}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ClassAbsenceDaySummaryRow">
|
||||
<Grid ColumnDefinitions="70,1*,1.3*,0.5*,0.6*,0.6*,0.8*,1.5*" 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"/>
|
||||
<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>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock Grid.Row="8" Text="{Binding Status}" FontSize="12" TextWrapping="Wrap"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class ClassTeacherTabView : UserControl
|
||||
{
|
||||
public ClassTeacherTabView() => InitializeComponent();
|
||||
}
|
||||
@@ -205,6 +205,11 @@
|
||||
<views:GroupDocumentationTabView DataContext="{Binding GroupDocumentationTab}"/>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Klassenlehrer, nur sichtbar für Gruppen mit IsClassTeacher -->
|
||||
<ContentPage Header="Klassenlehrer" IsVisible="{Binding IsClassTeacher}">
|
||||
<views:ClassTeacherTabView DataContext="{Binding ClassTeacherTab}"/>
|
||||
</ContentPage>
|
||||
|
||||
</TabbedPage>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user