WIP (unstable): Klassenlehrer-Feature auf Einstellungen/eigenen Bereich umgestellt
Nutzer-Feedback: Klassenlehrer ist eine Rolle für die ganze Klasse, unabhängig vom eigenen Unterricht - die Anbindung an ein LearningGroup.IsClassTeacher-Flag mit Tab in der Gruppendetailansicht war deshalb der falsche Ort. Ersetzt durch: - Klassenauswahl in den WebUntis-Einstellungen (WebUntisSettingsService.HomeroomClassName), über den bestehenden WebUntis-Klassenpicker. - Eigener Sidebar-Bereich "Klassenlehrer" statt Gruppen-Tab. - Zwei Ebenen: kompakte Roster-Übersicht mit Ampel-Symbolen (Fehlzeiten heute, Klassenbuch-Badge) und Details (die bisherigen Rohdaten-Listen, jetzt gefiltert auf einzelne Schüler*innen anspringbar). 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:
@@ -14,13 +14,6 @@ public class LearningGroup
|
|||||||
public bool IsOwnClass { get; set; }
|
public bool IsOwnClass { get; set; }
|
||||||
public bool IsDifferentiated { get; set; }
|
public bool IsDifferentiated { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ich bin Klassenlehrer/-in dieser Klasse. Anders als <see cref="IsOwnClass"/> (rein
|
|
||||||
/// kalenderfarbliche Markierung, mehrfach setzbar für Klasse + zugehörige Kurse) schaltet
|
|
||||||
/// dieses Flag Zugriff auf Daten anderer Lehrkräfte zu dieser Klasse frei (Klassenlehrer-Tab)
|
|
||||||
/// und sollte deshalb nur an genau einer Klasse-Gruppe gesetzt werden.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsClassTeacher { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Default true (alte Datensätze ohne dieses Feld verhalten sich damit unverändert wie bisher
|
/// Default true (alte Datensätze ohne dieses Feld verhalten sich damit unverändert wie bisher
|
||||||
/// - LiteDB liefert den C#-Default für fehlende Felder). Für Gruppen wie Klassenrat oder
|
/// - LiteDB liefert den C#-Default für fehlende Felder). Für Gruppen wie Klassenrat oder
|
||||||
/// Willkommenskreis, die keinen inhaltlichen Verlaufsplan brauchen, in den Stammdaten
|
/// Willkommenskreis, die keinen inhaltlichen Verlaufsplan brauchen, in den Stammdaten
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Tests;
|
namespace LehrerApp.Desktop.Tests;
|
||||||
@@ -62,4 +62,48 @@ public sealed class ClassTeacherViewModelsTests
|
|||||||
Assert.Equal(new DateOnly(2026, 8, 25), rows[0].Date);
|
Assert.Equal(new DateOnly(2026, 8, 25), rows[0].Date);
|
||||||
Assert.Equal(new DateOnly(2026, 8, 24), rows[1].Date);
|
Assert.Equal(new DateOnly(2026, 8, 24), rows[1].Date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RosterBuild_OrdnetFehlzeitPerExternKeyUndKlassenbuchPerNameZu()
|
||||||
|
{
|
||||||
|
var students = new[]
|
||||||
|
{
|
||||||
|
Student(1001, "Ada Müller"),
|
||||||
|
Student(1002, "Ben Schmidt"),
|
||||||
|
Student(1003, "Cem Yilmaz"),
|
||||||
|
};
|
||||||
|
var todayAbsences = new[]
|
||||||
|
{
|
||||||
|
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 25), "Ada Müller", 1001, 2, 90,
|
||||||
|
["Che"], [3, 4], ["nicht entsch."], ["Absent"], null, null, false),
|
||||||
|
};
|
||||||
|
var recentClassRegister = new[]
|
||||||
|
{
|
||||||
|
new UntisForeignClassRegisterEventDto("6a", 20260824, "Deu", "Ben Schmidt", "mueller",
|
||||||
|
"Vergessen", "Organisation", "Buch vergessen"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var roster = ClassTeacherRosterRow.Build(students, todayAbsences, recentClassRegister);
|
||||||
|
|
||||||
|
Assert.Equal(3, roster.Count);
|
||||||
|
var ada = Assert.Single(roster, r => r.StudentName == "Ada Müller");
|
||||||
|
Assert.True(ada.HasAbsenceToday);
|
||||||
|
Assert.Contains("nicht entsch.", ada.AbsenceTooltip);
|
||||||
|
Assert.False(ada.HasRecentClassRegisterEntry);
|
||||||
|
|
||||||
|
var ben = Assert.Single(roster, r => r.StudentName == "Ben Schmidt");
|
||||||
|
Assert.False(ben.HasAbsenceToday);
|
||||||
|
Assert.True(ben.HasRecentClassRegisterEntry);
|
||||||
|
|
||||||
|
var cem = Assert.Single(roster, r => r.StudentName == "Cem Yilmaz");
|
||||||
|
Assert.False(cem.HasAbsenceToday);
|
||||||
|
Assert.False(cem.HasRecentClassRegisterEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UntisStudentDto Student(int externKey, string displayName) => new(
|
||||||
|
UntisId: externKey, ExternKey: externKey, ClassName: "6a", Name: displayName, LongName: null,
|
||||||
|
ForeName: null, DisplayName: displayName, Gender: null, BirthDate: null, BirthDateRaw: null,
|
||||||
|
EntryDate: null, EntryDateRaw: null, ExitDate: null, ExitDateRaw: null, Text: null,
|
||||||
|
MedicalReportDuty: null, Schulpflicht: null, Majority: null,
|
||||||
|
Address: new UntisStudentAddressDto(null, null, null, null, null, null), AttributeIL: null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.Services;
|
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -30,9 +29,7 @@ public sealed class GroupDetailViewModelTests
|
|||||||
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
||||||
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
||||||
new FakeSessions([]), new FakeEntries(), new FakeAspects()),
|
new FakeSessions([]), new FakeEntries(), new FakeAspects()),
|
||||||
new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups),
|
new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups));
|
||||||
new ClassTeacherTabViewModel(
|
|
||||||
new WebUntisIntegrationService(new HttpClient(), TestSupport.BuildWebUntisSettingsService()), groups));
|
|
||||||
|
|
||||||
vm.LoadGroup(group.Id);
|
vm.LoadGroup(group.Id);
|
||||||
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using LehrerApp.Data;
|
|||||||
using LehrerApp.Data.Repositories;
|
using LehrerApp.Data.Repositories;
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using LehrerApp.Desktop.ViewModels.Planning;
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Settings;
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
@@ -297,6 +298,8 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<TimeTrackingViewModel>();
|
services.AddSingleton<TimeTrackingViewModel>();
|
||||||
services.AddSingleton<WorkloadEvaluationViewModel>();
|
services.AddSingleton<WorkloadEvaluationViewModel>();
|
||||||
services.AddSingleton<WorkloadViewModel>();
|
services.AddSingleton<WorkloadViewModel>();
|
||||||
|
services.AddSingleton<ClassTeacherDetailsViewModel>();
|
||||||
|
services.AddSingleton<ClassTeacherOverviewViewModel>();
|
||||||
|
|
||||||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||||||
services.AddTransient<GroupDetailViewModel>();
|
services.AddTransient<GroupDetailViewModel>();
|
||||||
@@ -308,7 +311,6 @@ public static class AppBootstrapper
|
|||||||
services.AddTransient<CompetencyOverviewTabViewModel>();
|
services.AddTransient<CompetencyOverviewTabViewModel>();
|
||||||
services.AddTransient<SeatingPlanTabViewModel>();
|
services.AddTransient<SeatingPlanTabViewModel>();
|
||||||
services.AddTransient<GroupDocumentationTabViewModel>();
|
services.AddTransient<GroupDocumentationTabViewModel>();
|
||||||
services.AddTransient<ClassTeacherTabViewModel>();
|
|
||||||
services.AddTransient<AddGroupDialogViewModel>();
|
services.AddTransient<AddGroupDialogViewModel>();
|
||||||
services.AddTransient<TrashViewModel>();
|
services.AddTransient<TrashViewModel>();
|
||||||
services.AddTransient<SettingsViewModel>();
|
services.AddTransient<SettingsViewModel>();
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ internal class WebUntisSettingsConfig
|
|||||||
public string LastSyncStatus { get; set; } = "";
|
public string LastSyncStatus { get; set; } = "";
|
||||||
public string? EncryptedApiCredentials { get; set; }
|
public string? EncryptedApiCredentials { get; set; }
|
||||||
public int? TeacherUntisId { get; set; }
|
public int? TeacherUntisId { get; set; }
|
||||||
|
public int? HomeroomClassUntisId { get; set; }
|
||||||
|
public string? HomeroomClassName { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record WebUntisCredentials(string School, string Host, string Username, string Password);
|
public sealed record WebUntisCredentials(string School, string Host, string Username, string Password);
|
||||||
@@ -36,6 +38,8 @@ public class WebUntisSettingsService
|
|||||||
public bool IsConfigured => _config.EncryptedIcalUrl is not null;
|
public bool IsConfigured => _config.EncryptedIcalUrl is not null;
|
||||||
public bool ApiIsConfigured => _config.EncryptedApiCredentials is not null;
|
public bool ApiIsConfigured => _config.EncryptedApiCredentials is not null;
|
||||||
public int? TeacherUntisId => _config.TeacherUntisId;
|
public int? TeacherUntisId => _config.TeacherUntisId;
|
||||||
|
public int? HomeroomClassUntisId => _config.HomeroomClassUntisId;
|
||||||
|
public string? HomeroomClassName => _config.HomeroomClassName;
|
||||||
public DateTime? LastSyncAt => _config.LastSyncAt;
|
public DateTime? LastSyncAt => _config.LastSyncAt;
|
||||||
public string LastSyncStatus => _config.LastSyncStatus;
|
public string LastSyncStatus => _config.LastSyncStatus;
|
||||||
|
|
||||||
@@ -83,6 +87,8 @@ public class WebUntisSettingsService
|
|||||||
{
|
{
|
||||||
_config.EncryptedApiCredentials = null;
|
_config.EncryptedApiCredentials = null;
|
||||||
_config.TeacherUntisId = null;
|
_config.TeacherUntisId = null;
|
||||||
|
_config.HomeroomClassUntisId = null;
|
||||||
|
_config.HomeroomClassName = null;
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +98,16 @@ public class WebUntisSettingsService
|
|||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Die Klasse, deren Klassenlehrer/-in man ist - unabhängig von jeder LearningGroup (siehe
|
||||||
|
// TODO.md "Klassenlehrer"-Feature): man ist es für die ganze Klasse, nicht für einen einzelnen
|
||||||
|
// Unterricht/Kurs. HomeroomClassName ist wie TeacherUntisId unverschlüsselt, da kein Geheimnis.
|
||||||
|
public void SetHomeroomClass(int? untisId, string? name)
|
||||||
|
{
|
||||||
|
_config.HomeroomClassUntisId = untisId;
|
||||||
|
_config.HomeroomClassName = name;
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
public void SetLastSync(DateTime at, string status)
|
public void SetLastSync(DateTime at, string status)
|
||||||
{
|
{
|
||||||
_config.LastSyncAt = at;
|
_config.LastSyncAt = at;
|
||||||
|
|||||||
+27
-16
@@ -1,10 +1,9 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using LehrerApp.Core.Interfaces;
|
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
namespace LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||||
|
|
||||||
/// <summary>Eine Zeile aus dem "-alle-"-Klassenbuchbericht, die eine andere Lehrkraft angelegt hat.
|
/// <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
|
/// Rein zur Ansicht - anders als beim Dokumentations-Abgleich (eigene Einträge) gibt es hier kein
|
||||||
@@ -57,19 +56,19 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Klassenlehrer-Tab: zeigt Klassenbucheinträge, die andere Lehrkräfte zu Schülern dieser Klasse
|
/// Details-Ansicht des Klassenlehrer-Bereichs: Klassenbucheinträge, die andere Lehrkräfte zu
|
||||||
/// angelegt haben (WebUntis "-alle-"-Bericht, gefiltert auf fremde statt der eigenen
|
/// Schülern der Klasse angelegt haben (WebUntis "-alle-"-Bericht, gefiltert auf fremde statt der
|
||||||
/// Benutzerkennung wie beim bestehenden Dokumentations-Abgleich, siehe
|
/// eigenen Benutzerkennung wie beim bestehenden Dokumentations-Abgleich, siehe
|
||||||
/// <see cref="WebUntisIntegrationService.GetForeignClassRegisterEventsAsync"/>), sowie die
|
/// <see cref="WebUntisIntegrationService.GetForeignClassRegisterEventsAsync"/>), sowie die
|
||||||
/// Fehlzeiten der Klasse über alle Fächer (<see cref="WebUntisIntegrationService.GetClassAbsencesAsync"/>).
|
/// 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
|
/// Beides rein zur Ansicht, kein Übernehmen in lokale Daten. Bewusst unabhängig von jeder
|
||||||
/// <see cref="Core.Models.LearningGroup.IsClassTeacher"/> sichtbar (siehe
|
/// <see cref="Core.Models.LearningGroup"/> — Klassenlehrer ist man für die ganze Klasse, nicht für
|
||||||
/// GroupDetailViewModel/GroupDetailView).
|
/// einen einzelnen Unterricht; die Klasse kommt aus <see cref="ClassTeacherOverviewViewModel"/>
|
||||||
|
/// (letztlich aus den WebUntis-Einstellungen), nicht aus einer Gruppen-ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ClassTeacherTabViewModel : ObservableObject
|
public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly WebUntisIntegrationService _untis;
|
private readonly WebUntisIntegrationService _untis;
|
||||||
private readonly IGroupRepository _groups;
|
|
||||||
private string _className = "";
|
private string _className = "";
|
||||||
|
|
||||||
public ObservableCollection<ClassTeacherClassRegisterRow> Entries { get; } = [];
|
public ObservableCollection<ClassTeacherClassRegisterRow> Entries { get; } = [];
|
||||||
@@ -79,15 +78,17 @@ public partial class ClassTeacherTabViewModel : ObservableObject
|
|||||||
[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.
|
||||||
|
[ObservableProperty] private string _studentFilter = "";
|
||||||
|
|
||||||
public ClassTeacherTabViewModel(WebUntisIntegrationService untis, IGroupRepository groups)
|
public ClassTeacherDetailsViewModel(WebUntisIntegrationService untis)
|
||||||
{
|
{
|
||||||
_untis = untis; _groups = groups;
|
_untis = untis;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize(Guid groupId)
|
public void Initialize(string className)
|
||||||
{
|
{
|
||||||
_className = _groups.GetById(groupId)?.Name ?? "";
|
_className = className;
|
||||||
Entries.Clear();
|
Entries.Clear();
|
||||||
AbsenceEntries.Clear();
|
AbsenceEntries.Clear();
|
||||||
Status = "Zeitraum wählen und laden.";
|
Status = "Zeitraum wählen und laden.";
|
||||||
@@ -99,7 +100,7 @@ public partial class ClassTeacherTabViewModel : ObservableObject
|
|||||||
var start = DateOnly.FromDateTime(StartDate.LocalDateTime);
|
var start = DateOnly.FromDateTime(StartDate.LocalDateTime);
|
||||||
var end = DateOnly.FromDateTime(EndDate.LocalDateTime);
|
var end = DateOnly.FromDateTime(EndDate.LocalDateTime);
|
||||||
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 = "Dieser Gruppe fehlt ein Klassenname."; return; }
|
if (string.IsNullOrWhiteSpace(_className)) { Status = "Keine Klasse ausgewählt."; return; }
|
||||||
|
|
||||||
Busy = true; Entries.Clear(); AbsenceEntries.Clear();
|
Busy = true; Entries.Clear(); AbsenceEntries.Clear();
|
||||||
try
|
try
|
||||||
@@ -109,6 +110,7 @@ public partial class ClassTeacherTabViewModel : ObservableObject
|
|||||||
await Task.WhenAll(classRegisterTask, absencesTask);
|
await Task.WhenAll(classRegisterTask, absencesTask);
|
||||||
|
|
||||||
var ordered = classRegisterTask.Result
|
var ordered = classRegisterTask.Result
|
||||||
|
.Where(MatchesStudentFilter)
|
||||||
.Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null))
|
.Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null))
|
||||||
.Where(x => x.Date is not null)
|
.Where(x => x.Date is not null)
|
||||||
.OrderByDescending(x => x.Date).ThenBy(x => x.Entry.StudentName);
|
.OrderByDescending(x => x.Date).ThenBy(x => x.Entry.StudentName);
|
||||||
@@ -118,7 +120,8 @@ public partial class ClassTeacherTabViewModel : ObservableObject
|
|||||||
entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text));
|
entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result))
|
var absences = absencesTask.Result.Where(MatchesStudentFilter);
|
||||||
|
foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences))
|
||||||
AbsenceEntries.Add(row);
|
AbsenceEntries.Add(row);
|
||||||
|
|
||||||
Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " +
|
Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " +
|
||||||
@@ -128,6 +131,14 @@ public partial class ClassTeacherTabViewModel : ObservableObject
|
|||||||
finally { Busy = false; }
|
finally { Busy = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool MatchesStudentFilter(UntisForeignClassRegisterEventDto entry) =>
|
||||||
|
string.IsNullOrWhiteSpace(StudentFilter) ||
|
||||||
|
string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private bool MatchesStudentFilter(UntisClassAbsenceEntryDto entry) =>
|
||||||
|
string.IsNullOrWhiteSpace(StudentFilter) ||
|
||||||
|
string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
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 ? "⚠" : "✓";
|
||||||
|
|
||||||
|
/// <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 Namensgleichheit für den
|
||||||
|
/// Klassenbuch-Bericht, der keinen ExternKey liefert (siehe UntisForeignClassRegisterEventDto).</summary>
|
||||||
|
public static IReadOnlyList<ClassTeacherRosterRow> Build(
|
||||||
|
IReadOnlyList<UntisStudentDto> students,
|
||||||
|
IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences,
|
||||||
|
IReadOnlyList<UntisForeignClassRegisterEventDto> recentClassRegisterEntries)
|
||||||
|
{
|
||||||
|
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 => a.StudentName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
var recentNames = recentClassRegisterEntries
|
||||||
|
.Select(e => e.StudentName)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return students
|
||||||
|
.Select(s =>
|
||||||
|
{
|
||||||
|
var absence = absenceByKey.GetValueOrDefault(s.ExternKey)
|
||||||
|
?? absenceByName.GetValueOrDefault(s.DisplayName);
|
||||||
|
return new ClassTeacherRosterRow(s.DisplayName, s.ExternKey, absence is not null,
|
||||||
|
absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}",
|
||||||
|
recentNames.Contains(s.DisplayName));
|
||||||
|
})
|
||||||
|
.OrderBy(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>
|
||||||
|
public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly WebUntisSettingsService _settings;
|
||||||
|
private readonly WebUntisIntegrationService _untis;
|
||||||
|
|
||||||
|
public ClassTeacherDetailsViewModel DetailsTab { get; }
|
||||||
|
public ObservableCollection<ClassTeacherRosterRow> Roster { get; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty] private string? _homeroomClassName;
|
||||||
|
[ObservableProperty] private int _activeTabIndex;
|
||||||
|
[ObservableProperty] private string _status = "";
|
||||||
|
[ObservableProperty] private bool _busy;
|
||||||
|
|
||||||
|
public bool HomeroomClassConfigured => !string.IsNullOrWhiteSpace(HomeroomClassName);
|
||||||
|
|
||||||
|
/// Vom Code-Behind/MainWindowViewModel gesetzt: springt in die WebUntis-Einstellungen, wenn noch
|
||||||
|
/// keine Klasse gewählt ist.
|
||||||
|
public Func<Task>? OnNavigateToSettings { get; set; }
|
||||||
|
|
||||||
|
public ClassTeacherOverviewViewModel(WebUntisSettingsService settings, WebUntisIntegrationService untis,
|
||||||
|
ClassTeacherDetailsViewModel detailsTab)
|
||||||
|
{
|
||||||
|
_settings = settings; _untis = untis; DetailsTab = detailsTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(HomeroomClassConfigured));
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task Load()
|
||||||
|
{
|
||||||
|
HomeroomClassName = _settings.HomeroomClassName;
|
||||||
|
ActiveTabIndex = 0;
|
||||||
|
Roster.Clear();
|
||||||
|
if (!HomeroomClassConfigured)
|
||||||
|
{
|
||||||
|
Status = "Noch keine Klasse ausgewählt.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var className = HomeroomClassName!;
|
||||||
|
DetailsTab.Initialize(className);
|
||||||
|
Busy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var studentsTask = _untis.GetStudentsAsync(className);
|
||||||
|
var absencesTask = _untis.GetClassAbsencesAsync(className, today, today);
|
||||||
|
var classRegisterTask = _untis.GetForeignClassRegisterEventsAsync(className, today.AddDays(-7), today);
|
||||||
|
await Task.WhenAll(studentsTask, absencesTask, classRegisterTask);
|
||||||
|
|
||||||
|
var todayAbsences = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result);
|
||||||
|
foreach (var row in ClassTeacherRosterRow.Build(studentsTask.Result.Students, todayAbsences,
|
||||||
|
classRegisterTask.Result))
|
||||||
|
Roster.Add(row);
|
||||||
|
Status = $"{Roster.Count} Schüler*innen · Stand heute.";
|
||||||
|
}
|
||||||
|
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
|
||||||
|
finally { Busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowDetailsForStudent(ClassTeacherRosterRow? row)
|
||||||
|
{
|
||||||
|
if (row is null) return;
|
||||||
|
DetailsTab.StudentFilter = row.StudentName;
|
||||||
|
ActiveTabIndex = 1;
|
||||||
|
DetailsTab.LoadCommand.Execute(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task GoToSettings()
|
||||||
|
{
|
||||||
|
if (OnNavigateToSettings is not null) await OnNavigateToSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -251,7 +251,6 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
|
// 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.
|
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
|
||||||
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
|
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
|
||||||
public bool IsClassTeacher => Group?.IsClassTeacher ?? false;
|
|
||||||
public bool IsReadOnly => Group is { IsActive: false };
|
public bool IsReadOnly => Group is { IsActive: false };
|
||||||
public bool IsEditable => Group is { IsActive: true };
|
public bool IsEditable => Group is { IsActive: true };
|
||||||
public string SubjectName { get; private set; } = "";
|
public string SubjectName { get; private set; } = "";
|
||||||
@@ -259,7 +258,6 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
partial void OnGroupChanged(LearningGroup? value)
|
partial void OnGroupChanged(LearningGroup? value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsDifferentiated));
|
OnPropertyChanged(nameof(IsDifferentiated));
|
||||||
OnPropertyChanged(nameof(IsClassTeacher));
|
|
||||||
OnPropertyChanged(nameof(IsReadOnly));
|
OnPropertyChanged(nameof(IsReadOnly));
|
||||||
OnPropertyChanged(nameof(IsEditable));
|
OnPropertyChanged(nameof(IsEditable));
|
||||||
NotifyWriteCommands();
|
NotifyWriteCommands();
|
||||||
@@ -281,7 +279,6 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
|
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
|
||||||
public SeatingPlanTabViewModel SeatingPlanTab { get; }
|
public SeatingPlanTabViewModel SeatingPlanTab { get; }
|
||||||
public GroupDocumentationTabViewModel GroupDocumentationTab { get; }
|
public GroupDocumentationTabViewModel GroupDocumentationTab { get; }
|
||||||
public ClassTeacherTabViewModel ClassTeacherTab { get; }
|
|
||||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||||
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
|
||||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||||
@@ -298,8 +295,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
GroupOverviewViewModel overviewTab, ParticipationTabViewModel participationTab,
|
GroupOverviewViewModel overviewTab, ParticipationTabViewModel participationTab,
|
||||||
GradeOverviewTabViewModel gradeOverviewTab,
|
GradeOverviewTabViewModel gradeOverviewTab,
|
||||||
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
|
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
|
||||||
SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab,
|
SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab)
|
||||||
ClassTeacherTabViewModel classTeacherTab)
|
|
||||||
{
|
{
|
||||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||||
_exams = exams; _grades = grades; _tasks = tasks;
|
_exams = exams; _grades = grades; _tasks = tasks;
|
||||||
@@ -310,7 +306,6 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
CompetencyOverviewTab = competencyOverviewTab;
|
CompetencyOverviewTab = competencyOverviewTab;
|
||||||
SeatingPlanTab = seatingPlanTab;
|
SeatingPlanTab = seatingPlanTab;
|
||||||
GroupDocumentationTab = groupDocumentationTab;
|
GroupDocumentationTab = groupDocumentationTab;
|
||||||
ClassTeacherTab = classTeacherTab;
|
|
||||||
SeatingPlanTab.OnAssessmentChanged = () =>
|
SeatingPlanTab.OnAssessmentChanged = () =>
|
||||||
{
|
{
|
||||||
ParticipationTab.LoadSessions();
|
ParticipationTab.LoadSessions();
|
||||||
@@ -340,7 +335,6 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
CompetencyOverviewTab.Initialize(Group);
|
CompetencyOverviewTab.Initialize(Group);
|
||||||
SeatingPlanTab.Initialize(Group.Id, IsReadOnly);
|
SeatingPlanTab.Initialize(Group.Id, IsReadOnly);
|
||||||
GroupDocumentationTab.Initialize(Group.Id);
|
GroupDocumentationTab.Initialize(Group.Id);
|
||||||
ClassTeacherTab.Initialize(Group.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadExams()
|
private void ReloadExams()
|
||||||
@@ -883,7 +877,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
[ObservableProperty] private int? _hoursPerWeek;
|
[ObservableProperty] private int? _hoursPerWeek;
|
||||||
[ObservableProperty] private bool _isOwnClass;
|
[ObservableProperty] private bool _isOwnClass;
|
||||||
[ObservableProperty] private bool _isDifferentiated;
|
[ObservableProperty] private bool _isDifferentiated;
|
||||||
[ObservableProperty] private bool _isClassTeacher;
|
|
||||||
[ObservableProperty] private bool _requiresLessonPlanning = true;
|
[ObservableProperty] private bool _requiresLessonPlanning = true;
|
||||||
[ObservableProperty] private int? _webUntisLessonId;
|
[ObservableProperty] private int? _webUntisLessonId;
|
||||||
[ObservableProperty] private string _nameError = "";
|
[ObservableProperty] private string _nameError = "";
|
||||||
@@ -924,7 +917,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
HoursPerWeek = group.HoursPerWeek;
|
HoursPerWeek = group.HoursPerWeek;
|
||||||
IsOwnClass = group.IsOwnClass;
|
IsOwnClass = group.IsOwnClass;
|
||||||
IsDifferentiated = group.IsDifferentiated;
|
IsDifferentiated = group.IsDifferentiated;
|
||||||
IsClassTeacher = group.IsClassTeacher;
|
|
||||||
RequiresLessonPlanning = group.RequiresLessonPlanning;
|
RequiresLessonPlanning = group.RequiresLessonPlanning;
|
||||||
WebUntisLessonId = group.WebUntisLessonId;
|
WebUntisLessonId = group.WebUntisLessonId;
|
||||||
OnPropertyChanged(nameof(DialogTitle));
|
OnPropertyChanged(nameof(DialogTitle));
|
||||||
@@ -972,7 +964,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
Result.HoursPerWeek = HoursPerWeek;
|
Result.HoursPerWeek = HoursPerWeek;
|
||||||
Result.IsOwnClass = IsOwnClass;
|
Result.IsOwnClass = IsOwnClass;
|
||||||
Result.IsDifferentiated = IsDifferentiated;
|
Result.IsDifferentiated = IsDifferentiated;
|
||||||
Result.IsClassTeacher = IsClassTeacher;
|
|
||||||
Result.RequiresLessonPlanning = RequiresLessonPlanning;
|
Result.RequiresLessonPlanning = RequiresLessonPlanning;
|
||||||
Result.WebUntisLessonId = WebUntisLessonId;
|
Result.WebUntisLessonId = WebUntisLessonId;
|
||||||
_groups.Save(Result);
|
_groups.Save(Result);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using LehrerApp.Desktop.ViewModels.Planning;
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Settings;
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
@@ -31,6 +32,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
public bool IsExamsActive => ActiveNavItem == NavItem.Exams;
|
public bool IsExamsActive => ActiveNavItem == NavItem.Exams;
|
||||||
public bool IsPlannerActive => ActiveNavItem == NavItem.Planner;
|
public bool IsPlannerActive => ActiveNavItem == NavItem.Planner;
|
||||||
public bool IsWorkloadActive => ActiveNavItem == NavItem.Workload;
|
public bool IsWorkloadActive => ActiveNavItem == NavItem.Workload;
|
||||||
|
public bool IsClassTeacherActive => ActiveNavItem == NavItem.ClassTeacher;
|
||||||
public bool IsSettingsActive => ActiveNavItem == NavItem.Settings;
|
public bool IsSettingsActive => ActiveNavItem == NavItem.Settings;
|
||||||
|
|
||||||
public MainWindowViewModel(IServiceProvider services,
|
public MainWindowViewModel(IServiceProvider services,
|
||||||
@@ -71,6 +73,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
case TimetableViewModel vm: vm.Load(); break;
|
case TimetableViewModel vm: vm.Load(); break;
|
||||||
case WorkloadViewModel vm:
|
case WorkloadViewModel vm:
|
||||||
vm.Tasks.Load(); vm.TimeTracking.Load(); vm.Evaluation.Load(); break;
|
vm.Tasks.Load(); vm.TimeTracking.Load(); vm.Evaluation.Load(); break;
|
||||||
|
case ClassTeacherOverviewViewModel vm: vm.LoadCommand.Execute(null); break;
|
||||||
case GroupDetailViewModel { Group: { } group } vm: vm.LoadGroup(group.Id); break;
|
case GroupDetailViewModel { Group: { } group } vm: vm.LoadGroup(group.Id); break;
|
||||||
// Inline-Bearbeitung (IsEditing) nicht überschreiben - anders als die Gruppenansicht
|
// Inline-Bearbeitung (IsEditing) nicht überschreiben - anders als die Gruppenansicht
|
||||||
// laufen Namens-/Geschlechtsänderungen hier nicht über einen Dialog.
|
// laufen Namens-/Geschlechtsänderungen hier nicht über einen Dialog.
|
||||||
@@ -87,6 +90,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
OnPropertyChanged(nameof(IsExamsActive));
|
OnPropertyChanged(nameof(IsExamsActive));
|
||||||
OnPropertyChanged(nameof(IsPlannerActive));
|
OnPropertyChanged(nameof(IsPlannerActive));
|
||||||
OnPropertyChanged(nameof(IsWorkloadActive));
|
OnPropertyChanged(nameof(IsWorkloadActive));
|
||||||
|
OnPropertyChanged(nameof(IsClassTeacherActive));
|
||||||
OnPropertyChanged(nameof(IsSettingsActive));
|
OnPropertyChanged(nameof(IsSettingsActive));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +106,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
|
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
|
||||||
NavItem.Planner => GetTimetable(),
|
NavItem.Planner => GetTimetable(),
|
||||||
NavItem.Workload => GetWorkload(),
|
NavItem.Workload => GetWorkload(),
|
||||||
|
NavItem.ClassTeacher => GetClassTeacherOverview(),
|
||||||
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
||||||
_ => CurrentPage,
|
_ => CurrentPage,
|
||||||
};
|
};
|
||||||
@@ -141,6 +146,14 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
return workload;
|
return workload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ClassTeacherOverviewViewModel GetClassTeacherOverview()
|
||||||
|
{
|
||||||
|
var vm = _services.GetRequiredService<ClassTeacherOverviewViewModel>();
|
||||||
|
vm.OnNavigateToSettings = () => { NavigateToSettings(SettingsTab.WebUntis); return Task.CompletedTask; };
|
||||||
|
vm.LoadCommand.Execute(null);
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
|
||||||
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
||||||
{
|
{
|
||||||
ActiveNavItem = NavItem.Groups;
|
ActiveNavItem = NavItem.Groups;
|
||||||
@@ -175,7 +188,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
public void NavigateToWorkload() => NavigateTo(NavItem.Workload);
|
public void NavigateToWorkload() => NavigateTo(NavItem.Workload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings }
|
public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, ClassTeacher, Settings }
|
||||||
|
|
||||||
public partial class PlaceholderViewModel : ObservableObject
|
public partial class PlaceholderViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -217,7 +217,14 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _untisPassword = "";
|
[ObservableProperty] private string _untisPassword = "";
|
||||||
[ObservableProperty] private string _untisApiStatus = "";
|
[ObservableProperty] private string _untisApiStatus = "";
|
||||||
[ObservableProperty] private bool _untisApiBusy;
|
[ObservableProperty] private bool _untisApiBusy;
|
||||||
|
[ObservableProperty] private string? _untisHomeroomClassName;
|
||||||
|
public string UntisHomeroomClassDisplay => UntisHomeroomClassName is { Length: > 0 } name
|
||||||
|
? $"Ausgewählt: {name}"
|
||||||
|
: "Keine Klasse ausgewählt.";
|
||||||
|
partial void OnUntisHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(UntisHomeroomClassDisplay));
|
||||||
public Func<Task>? OnReviewUntisMapping { get; set; }
|
public Func<Task>? OnReviewUntisMapping { get; set; }
|
||||||
|
/// Vom Code-Behind gesetzt: öffnet den WebUntis-Klassenauswahldialog, liefert null bei Abbruch.
|
||||||
|
public Func<Task<(int UntisId, string Name)?>>? OnPickHomeroomClass { get; set; }
|
||||||
|
|
||||||
// ── Schulweiter Jahresplan (ClassyPlan-iCal) ─────────────────────────────
|
// ── Schulweiter Jahresplan (ClassyPlan-iCal) ─────────────────────────────
|
||||||
|
|
||||||
@@ -559,6 +566,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
UntisUsername = credentials.Username;
|
UntisUsername = credentials.Username;
|
||||||
UntisApiStatus = $"API-Zugang für {credentials.Username} ist lokal verschlüsselt gespeichert.";
|
UntisApiStatus = $"API-Zugang für {credentials.Username} ist lokal verschlüsselt gespeichert.";
|
||||||
}
|
}
|
||||||
|
UntisHomeroomClassName = _untisSettings.HomeroomClassName;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -647,6 +655,23 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
if (OnReviewUntisMapping is not null) await OnReviewUntisMapping();
|
if (OnReviewUntisMapping is not null) await OnReviewUntisMapping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task UntisPickHomeroomClass()
|
||||||
|
{
|
||||||
|
if (OnPickHomeroomClass is null) return;
|
||||||
|
var result = await OnPickHomeroomClass();
|
||||||
|
if (result is null) return;
|
||||||
|
_untisSettings.SetHomeroomClass(result.Value.UntisId, result.Value.Name);
|
||||||
|
LoadUntisSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void UntisClearHomeroomClass()
|
||||||
|
{
|
||||||
|
_untisSettings.SetHomeroomClass(null, null);
|
||||||
|
LoadUntisSettings();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Schulweiter Jahresplan: Laden / Speichern / Entfernen / Jetzt abrufen ─
|
// ── Schulweiter Jahresplan: Laden / Speichern / Entfernen / Jetzt abrufen ─
|
||||||
|
|
||||||
private void LoadAnnualPlanSettings()
|
private void LoadAnnualPlanSettings()
|
||||||
|
|||||||
+17
-11
@@ -1,10 +1,10 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
|
||||||
x:Class="LehrerApp.Desktop.Views.Groups.ClassTeacherTabView"
|
x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherDetailsView"
|
||||||
x:DataType="vm:ClassTeacherTabViewModel">
|
x:DataType="vm:ClassTeacherDetailsViewModel">
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,1*,Auto,Auto,1*,Auto" Margin="16" RowSpacing="6">
|
<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"
|
<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)."/>
|
Text="Klassenbucheinträge anderer Lehrkräfte und Fehlzeiten der Klasse über alle Fächer (nur Ansicht, keine Übernahme in eigene Daten)."/>
|
||||||
|
|
||||||
@@ -15,8 +15,14 @@
|
|||||||
<Button Content="Laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
|
<Button Content="Laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Text="Klassenbucheinträge anderer Lehrkräfte" FontWeight="SemiBold" FontSize="13" Margin="0,4,0,0"/>
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8"
|
||||||
<Grid Grid.Row="3" ColumnDefinitions="70,1*,1.1*,0.8*,1*,1.3*" ColumnSpacing="8" Margin="4,0">
|
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="0" Text="Datum" FontSize="11" Opacity="0.6"/>
|
||||||
<TextBlock Grid.Column="1" Text="Schüler*in" 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="2" Text="Fach / Lehrkraft" FontSize="11" Opacity="0.6"/>
|
||||||
@@ -24,7 +30,7 @@
|
|||||||
<TextBlock Grid.Column="4" Text="Gruppe" 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"/>
|
<TextBlock Grid.Column="5" Text="Text" FontSize="11" Opacity="0.6"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ScrollViewer Grid.Row="4">
|
<ScrollViewer Grid.Row="5">
|
||||||
<ItemsControl ItemsSource="{Binding Entries}">
|
<ItemsControl ItemsSource="{Binding Entries}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:ClassTeacherClassRegisterRow">
|
<DataTemplate x:DataType="vm:ClassTeacherClassRegisterRow">
|
||||||
@@ -44,9 +50,9 @@
|
|||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<TextBlock Grid.Row="5" Text="Fehlzeiten der Klasse (alle Fächer, pro Schüler*in und Tag zusammengefasst)"
|
<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"/>
|
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">
|
<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="0" Text="Datum" FontSize="11" Opacity="0.6"/>
|
||||||
<TextBlock Grid.Column="1" Text="Schüler*in" 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="2" Text="Fächer" FontSize="11" Opacity="0.6"/>
|
||||||
@@ -56,7 +62,7 @@
|
|||||||
<TextBlock Grid.Column="6" Text="Status" 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"/>
|
<TextBlock Grid.Column="7" Text="Grund / Notiz" FontSize="11" Opacity="0.6"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ScrollViewer Grid.Row="7">
|
<ScrollViewer Grid.Row="8">
|
||||||
<ItemsControl ItemsSource="{Binding AbsenceEntries}">
|
<ItemsControl ItemsSource="{Binding AbsenceEntries}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:ClassAbsenceDaySummaryRow">
|
<DataTemplate x:DataType="vm:ClassAbsenceDaySummaryRow">
|
||||||
@@ -79,6 +85,6 @@
|
|||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<TextBlock Grid.Row="8" Text="{Binding Status}" FontSize="12" TextWrapping="Wrap"/>
|
<TextBlock Grid.Row="9" Text="{Binding Status}" FontSize="12" TextWrapping="Wrap"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
|
||||||
|
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.ClassTeacher"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherOverviewView"
|
||||||
|
x:DataType="vm:ClassTeacherOverviewViewModel">
|
||||||
|
|
||||||
|
<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"
|
||||||
|
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}">
|
||||||
|
|
||||||
|
<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}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ClassTeacherRosterRow">
|
||||||
|
<Button HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||||
|
Background="Transparent" BorderThickness="0" Padding="8,6"
|
||||||
|
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"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
|
<ContentPage Header="Details">
|
||||||
|
<views:ClassTeacherDetailsView DataContext="{Binding DetailsTab}"/>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
|
</TabbedPage>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.ClassTeacher;
|
||||||
|
|
||||||
|
public partial class ClassTeacherOverviewView : UserControl
|
||||||
|
{
|
||||||
|
public ClassTeacherOverviewView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -71,9 +71,6 @@
|
|||||||
<CheckBox Content="Gehört zu meiner Klasse" IsChecked="{Binding IsOwnClass}"
|
<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."/>
|
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}"
|
<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."/>
|
ToolTip.Tip="Blendet im Schüler-Tab eine Niveau-Zuordnung je Schüler ein und erlaubt in Klausuren die Zuordnung zu einem Niveau."/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Groups;
|
|
||||||
|
|
||||||
public partial class ClassTeacherTabView : UserControl
|
|
||||||
{
|
|
||||||
public ClassTeacherTabView() => InitializeComponent();
|
|
||||||
}
|
|
||||||
@@ -205,11 +205,6 @@
|
|||||||
<views:GroupDocumentationTabView DataContext="{Binding GroupDocumentationTab}"/>
|
<views:GroupDocumentationTabView DataContext="{Binding GroupDocumentationTab}"/>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Klassenlehrer, nur sichtbar für Gruppen mit IsClassTeacher -->
|
|
||||||
<ContentPage Header="Klassenlehrer" IsVisible="{Binding IsClassTeacher}">
|
|
||||||
<views:ClassTeacherTabView DataContext="{Binding ClassTeacherTab}"/>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||||
xmlns:vw="clr-namespace:LehrerApp.Desktop.Views.Workload"
|
xmlns:vw="clr-namespace:LehrerApp.Desktop.Views.Workload"
|
||||||
xmlns:vmw="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
|
xmlns:vmw="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
|
||||||
|
xmlns:vct="clr-namespace:LehrerApp.Desktop.Views.ClassTeacher"
|
||||||
|
xmlns:vmct="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
|
||||||
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
|
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
|
||||||
x:Class="LehrerApp.Desktop.Views.MainWindow"
|
x:Class="LehrerApp.Desktop.Views.MainWindow"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
@@ -58,6 +60,9 @@
|
|||||||
<DataTemplate DataType="vmw:WorkloadViewModel">
|
<DataTemplate DataType="vmw:WorkloadViewModel">
|
||||||
<vw:WorkloadView/>
|
<vw:WorkloadView/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="vmct:ClassTeacherOverviewViewModel">
|
||||||
|
<vct:ClassTeacherOverviewView/>
|
||||||
|
</DataTemplate>
|
||||||
<DataTemplate DataType="vm:PlaceholderViewModel">
|
<DataTemplate DataType="vm:PlaceholderViewModel">
|
||||||
<views:PlaceholderView/>
|
<views:PlaceholderView/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@@ -209,6 +214,16 @@
|
|||||||
<TextBlock Classes="navlabel" Text="Arbeitszeit"/>
|
<TextBlock Classes="navlabel" Text="Arbeitszeit"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button Classes="navitem" Classes.active="{Binding IsClassTeacherActive}" HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left"
|
||||||
|
CornerRadius="6"
|
||||||
|
Command="{Binding NavigateToCommand}"
|
||||||
|
CommandParameter="{x:Static vm:NavItem.ClassTeacher}">
|
||||||
|
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||||
|
<TextBlock Classes="navicon" Text="🎓" TextAlignment="Center"/>
|
||||||
|
<TextBlock Classes="navlabel" Text="Klassenlehrer"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
<Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
|
<Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Left"
|
HorizontalContentAlignment="Left"
|
||||||
CornerRadius="6"
|
CornerRadius="6"
|
||||||
|
|||||||
@@ -1000,6 +1000,19 @@
|
|||||||
IsVisible="{Binding UntisApiIsConfigured}" IsEnabled="{Binding !UntisApiBusy}"/>
|
IsVisible="{Binding UntisApiIsConfigured}" IsEnabled="{Binding !UntisApiBusy}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="8" IsVisible="{Binding UntisApiIsConfigured}">
|
||||||
|
<Separator Margin="0,8"/>
|
||||||
|
<TextBlock Text="Klassenlehrer" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
Text="Die Klasse, deren Klassenlehrer/-in du bist — unabhängig von deinem eigenen Unterricht. Schaltet den Klassenlehrer-Bereich in der Seitenleiste frei."/>
|
||||||
|
<TextBlock FontSize="13" Text="{Binding UntisHomeroomClassDisplay}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Content="Klasse auswählen…" Command="{Binding UntisPickHomeroomClassCommand}"/>
|
||||||
|
<Button Content="Entfernen" Command="{Binding UntisClearHomeroomClassCommand}"
|
||||||
|
IsVisible="{Binding UntisHomeroomClassName, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<Separator Margin="0,8"/>
|
<Separator Margin="0,8"/>
|
||||||
|
|
||||||
<TextBlock Text="WebUntis-Stundenplan-Abgleich" FontSize="16" FontWeight="SemiBold"/>
|
<TextBlock Text="WebUntis-Stundenplan-Abgleich" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ using Avalonia.Platform.Storage;
|
|||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using LehrerApp.Desktop.ViewModels.Planning;
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Settings;
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
|
using LehrerApp.Desktop.Views.Groups;
|
||||||
using LehrerApp.Desktop.Views.Planning;
|
using LehrerApp.Desktop.Views.Planning;
|
||||||
using LehrerApp.Desktop.Views.Shared;
|
using LehrerApp.Desktop.Views.Shared;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -31,6 +33,7 @@ public partial class SettingsView : UserControl
|
|||||||
vm.OnConfirmRecoveryRestore = ShowRecoveryRestoreConfirmDialog;
|
vm.OnConfirmRecoveryRestore = ShowRecoveryRestoreConfirmDialog;
|
||||||
vm.OnThemeChanged = App.ApplyTheme;
|
vm.OnThemeChanged = App.ApplyTheme;
|
||||||
vm.OnReviewUntisMapping = ShowUntisMappingReviewDialog;
|
vm.OnReviewUntisMapping = ShowUntisMappingReviewDialog;
|
||||||
|
vm.OnPickHomeroomClass = PickHomeroomClassAsync;
|
||||||
_ = vm.LoadSchoolLocationCommand.ExecuteAsync(null);
|
_ = vm.LoadSchoolLocationCommand.ExecuteAsync(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +54,20 @@ public partial class SettingsView : UserControl
|
|||||||
await dialog.ShowDialog<bool>(owner);
|
await dialog.ShowDialog<bool>(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(int UntisId, string Name)?> PickHomeroomClassAsync()
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
|
||||||
|
var untis = App.Services.GetRequiredService<WebUntisIntegrationService>();
|
||||||
|
var selectionVm = new WebUntisClassSelectionViewModel(untis);
|
||||||
|
var selection = new WebUntisClassSelectionDialog { DataContext = selectionVm };
|
||||||
|
await selectionVm.InitializeAsync();
|
||||||
|
if (!await selection.ShowDialog<bool>(owner) || selectionVm.SelectedClass is null) return null;
|
||||||
|
|
||||||
|
return (selectionVm.SelectedClass.UntisId, selectionVm.SelectedClass.Name);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> SaveRecoveryFile(string content)
|
private async Task<bool> SaveRecoveryFile(string content)
|
||||||
{
|
{
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
|||||||
@@ -1277,53 +1277,57 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
|
|||||||
Auch hier keine externe Schülerkennung, nur der Name — Namensabgleich soll deshalb zusätzlich auf
|
Auch hier keine externe Schülerkennung, nur der Name — Namensabgleich soll deshalb zusätzlich auf
|
||||||
Klasse+Fach aus der Zeile scopen (→ genau eine lokale Lerngruppe), nicht global über alle Schüler.
|
Klasse+Fach aus der Zeile scopen (→ genau eine lokale Lerngruppe), nicht global über alle Schüler.
|
||||||
Abgleichsdialog (Diff gegen `Documentation`, Übernehmen in beide Richtungen) noch nicht gebaut.
|
Abgleichsdialog (Diff gegen `Documentation`, Übernehmen in beide Richtungen) noch nicht gebaut.
|
||||||
- [x] **"Klassenlehrer"-Feature, Schritt 1 (Klassenbuch fremder Einträge, August 2026):** Der
|
- [x] **"Klassenlehrer"-Feature — Datenschicht (August 2026):** Zwei WebUntis-Berichte liefern, was
|
||||||
"-alle-"-Bericht enthält für die eigene Klasse auch Einträge, die andere Lehrkräfte in deren
|
für einen Klassenlehrer-Überblick fehlte. `WebUntisIntegrationService.GetForeignClassRegisterEventsAsync`
|
||||||
Fächern angelegt haben (`Benutzer` ≠ eigener Login) — für den reinen Dokumentations-Abgleich
|
ist das Spiegelbild von `GetOwnClassRegisterEventsAsync`: derselbe "-alle-"-Rohbericht
|
||||||
(eigene Einträge synchron halten) werden die herausgefiltert. Neues Flag
|
(`ClassregEventPerStudent`), aber Einträge anderer Lehrkräfte (`Benutzer` ≠ eigener Login) statt
|
||||||
`LearningGroup.IsClassTeacher` ("Klassenlehrer/-in dieser Klasse", Stammdaten-Checkbox, keine
|
der eigenen, auf eine Klasse eingeschränkt — kein neuer Bericht nötig.
|
||||||
Migration nötig) blendet einen neuen Tab "Klassenlehrer" in der Gruppendetailansicht ein —
|
`GetClassAbsencesAsync`/`reports.do?name=AbsencePerStudent&klasseOrStudentgroupId=KL<id>&studentId=-1`
|
||||||
bewusst ein eigenes Flag statt `IsOwnClass` (das ist rein die Kalenderfarb-Markierung und
|
(per Netzwerk-Mitschnitt gefunden) liefert Fehlzeiten der ganzen Klasse über alle Fächer, nicht nur
|
||||||
mehrfach setzbar für Klasse + zugehörige Kurse, würde den Tab sonst versehentlich auch bei
|
eigenen Unterricht. `klasseOrStudentgroupId` ist `"KL"` + die `getKlassen`-interne Klassen-ID
|
||||||
Kursen zeigen). `WebUntisIntegrationService.GetForeignClassRegisterEventsAsync` ist das
|
(dasselbe Element-Kürzel-Schema wie `kl`/`te`/`su`/`ro` bei anderen Berichten) —
|
||||||
Spiegelbild von `GetOwnClassRegisterEventsAsync`: derselbe "-alle-"-Rohbericht, aber Einträge
|
`WebUntisIntegrationService.ResolveClassIdAsync` löst sie pro Aufruf über
|
||||||
anderer Lehrkräfte statt der eigenen, zusätzlich auf die übergebene Klasse eingeschränkt (kein
|
`getSchoolyears`+`getKlassen` und den Klassennamen auf. Das CSV-Format ist **nicht** identisch mit
|
||||||
neuer WebUntis-Bericht nötig). `ClassTeacherTabViewModel`/`ClassTeacherTabView` zeigen die
|
`AbsencePerLesson` (ursprüngliche Vermutung anhand echter Daten widerlegt) — eigenes Spaltenschema,
|
||||||
Liste rein lesend an (Datum, Schüler*in, Fach/Lehrkraft, Kategorie, Text) — kein
|
eine Zeile pro Fehlstunde, eigener Parser `WebUntisClassAbsenceParser`/Modell
|
||||||
"Übernehmen"-Command wie beim Dokumentations-Abgleich, da es fremde Einträge sind, keine
|
`UntisClassAbsenceEntry`. Anders als `AbsencePerLesson` liefert `Status` hier einen Klartext-Wert
|
||||||
eigenen zum lokalen Synchronisieren.
|
direkt (im Sample nur `"nicht entsch."` beobachtet, andere Werte nicht bestätigt), es braucht also
|
||||||
**Nicht Teil dieses Schritts:** Fehlzeiten der Klasse aus fremdem Unterricht — dafür fehlt noch
|
keine Status-Heuristik. Der Header `Text` kommt zweimal vor (eine immer-leere Spalte, dann die
|
||||||
ein per Netzwerk-Mitschnitt zu ermittelnder klassenbezogener Fehlzeiten-Bericht ohne `lessonId`
|
eigentliche Tagesnotiz) — der Dictionary-per-Header-Parse-Stil überschreibt das zufällig richtig,
|
||||||
(vermutlich `klasseId`/`teacherId` statt `lsid`); folgt als eigener Schritt, sobald die
|
im Parser kommentiert statt stillschweigend vorausgesetzt. `Fehltage` (0/1, `CountsAsFullDay`) ist
|
||||||
Query-Parameter bekannt sind. Ebenfalls zurückgestellt: Dokumentation/Behördenschriftverkehr als
|
laut Nutzer-Angabe der auf eine ganze Zahl abgerundete Fehlzeit-Tagesanteil: bleibt 0, bis auch die
|
||||||
eigenständiger, WebUntis-unabhängiger Baustein.
|
|
||||||
- [x] **"Klassenlehrer"-Feature, Schritt 2 (Fehlzeiten aus fremdem Unterricht, August 2026):** Der
|
|
||||||
gesuchte Bericht ist `reports.do?name=AbsencePerStudent&klasseOrStudentgroupId=KL<id>&studentId=-1`
|
|
||||||
(per Netzwerk-Mitschnitt gefunden) — Query-Parameter sind bis auf `name`/`klasseOrStudentgroupId`/
|
|
||||||
`studentId` identisch mit `ClassregEventPerStudent`. `klasseOrStudentgroupId` ist `"KL"` +
|
|
||||||
die `getKlassen`-interne Klassen-ID (dasselbe Element-Kürzel-Schema wie `kl`/`te`/`su`/`ro` bei
|
|
||||||
anderen Berichten) — **kein manuelles Feld nötig**, `WebUntisIntegrationService.ResolveClassIdAsync`
|
|
||||||
löst sie pro Aufruf über `getSchoolyears`+`getKlassen` und den Klassennamen auf (analog zum
|
|
||||||
bestehenden Klassen-Auswahldialog). Das CSV-Format ist **nicht** identisch mit `AbsencePerLesson`
|
|
||||||
(ursprüngliche Vermutung anhand echter Daten widerlegt) — eigenes Spaltenschema, eine Zeile pro
|
|
||||||
Fehlstunde, neuer Parser `WebUntisClassAbsenceParser`/Modell `UntisClassAbsenceEntry`. Anders als
|
|
||||||
`AbsencePerLesson` liefert `Status` hier einen Klartext-Wert direkt (im Sample nur `"nicht
|
|
||||||
entsch."` beobachtet, andere Werte nicht bestätigt), es braucht also keine Status-Heuristik.
|
|
||||||
Der Header `Text` kommt zweimal vor (eine immer-leere Spalte, dann die eigentliche Tagesnotiz) —
|
|
||||||
der bestehende Dictionary-per-Header-Parse-Stil überschreibt das zufällig richtig, im Parser
|
|
||||||
kommentiert statt stillschweigend vorausgesetzt. `Fehltage` (0/1, `CountsAsFullDay`) ist laut
|
|
||||||
Nutzer-Angabe der auf eine ganze Zahl abgerundete Fehlzeit-Tagesanteil: bleibt 0, bis auch die
|
|
||||||
letzte Unterrichtsstunde des Tages als versäumt gilt, erst deren Zeile trägt dann den Wert 1.
|
letzte Unterrichtsstunde des Tages als versäumt gilt, erst deren Zeile trägt dann den Wert 1.
|
||||||
Im Klassenlehrer-Tab (Nutzer-Feedback) **pro Schüler*in und Tag gruppiert** dargestellt statt
|
`ClassAbsenceDaySummaryRow.GroupByStudentAndDay` fasst die Fehlstunden pro Schüler*in/Tag zusammen
|
||||||
einer Zeile pro Fehlstunde — `ClassAbsenceDaySummaryRow.GroupByStudentAndDay` fasst Fehlstunden/
|
(Fächer/Status/Gründe als distinkte Listen) statt eine Zeile pro Fehlstunde zu zeigen.
|
||||||
-minuten zusammen und sammelt Fächer/Status/Gründe als distinkte Listen. Rein Ansicht, kein
|
- [x] **"Klassenlehrer"-Feature — Neuausrichtung (August 2026):** Die erste Umsetzung hing das
|
||||||
Abgleich/Übernehmen in lokale Anwesenheitsdaten; eigene und fremde Fächer erscheinen bewusst
|
Feature an ein `LearningGroup.IsClassTeacher`-Flag mit Tab in der Gruppendetailansicht.
|
||||||
beide ungefiltert.
|
Nutzer-Feedback: konzeptionell falsch — Klassenlehrer ist man für eine ganze Klasse, unabhängig
|
||||||
**Zurückgestellte Idee:** ein Detail-Drill-down pro Schüler*in (z. B. Heatmap der gefehlten
|
vom eigenen Unterricht (die Checkbox saß z. B. auch an "SOL"/"Klassenrat"), und die Übersicht wird
|
||||||
Einzelstunden, Wochenansicht mit Verspätung/vorzeitigem Verlassen) — noch offen, ob im selben Tab
|
gerade an Tagen gebraucht, an denen man selbst keinen Unterricht mit der Klasse hat.
|
||||||
oder über einen eigenen Tab/Dialog "in die Tiefe" erreichbar; die Rohdaten
|
`LearningGroup.IsClassTeacher` wieder entfernt. Stattdessen:
|
||||||
(`UntisClassAbsenceEntryDto`) sind dafür schon vollständig vorhanden, nur noch nicht in der UI
|
- **Klassenauswahl in den Einstellungen** statt an einer Gruppe:
|
||||||
genutzt.
|
`WebUntisSettingsService.HomeroomClassUntisId`/`HomeroomClassName` (unverschlüsselt, gleiches
|
||||||
|
Muster wie `TeacherUntisId`), Auswahl im "Untis-Einbettung"-Tab über den bestehenden
|
||||||
|
`WebUntisClassSelectionViewModel`/`WebUntisClassSelectionDialog` (bisher nur für den
|
||||||
|
WebUntis-Klassenimport in der Kursansicht genutzt).
|
||||||
|
- **Eigener Top-Level-Bereich** "Klassenlehrer" in der Seitenleiste (`NavItem.ClassTeacher`,
|
||||||
|
`ClassTeacherOverviewViewModel`) statt Tab in einer `LearningGroup`-Detailansicht — sichtbar und
|
||||||
|
ladbar unabhängig davon, ob heute eigener Unterricht mit der Klasse stattfindet. Zeigt bei
|
||||||
|
fehlender Klassenauswahl einen Hinweis mit Sprung in die Einstellungen.
|
||||||
|
- **Zwei Ebenen** (Nutzer-Feedback: "auf die Schnelle" statt Rohdaten-Listen als Erstes):
|
||||||
|
"Übersicht" — kompaktes Roster (`ClassTeacherRosterRow.Build`, Zuordnung primär über `ExternKey`
|
||||||
|
für die Fehlzeiten, Namens-Fallback für den Klassenbuch-Bericht ohne `ExternKey`) mit
|
||||||
|
Ampel-Symbol "keine gemeldete Fehlzeit heute laut WebUntis" (unabhängig von eigener
|
||||||
|
Anwesenheitserfassung) und Badge bei Klassenbucheinträgen anderer Lehrkräfte in den letzten 7
|
||||||
|
Tagen; "Details" — die bisherigen Rohdaten-Listen (jetzt `ClassTeacherDetailsViewModel`/
|
||||||
|
`ClassTeacherDetailsView`, aus `ViewModels/Groups`/`Views/Groups` nach
|
||||||
|
`ViewModels/ClassTeacher`/`Views/ClassTeacher` verschoben, `Initialize(string className)` statt
|
||||||
|
`Initialize(Guid groupId)`), aus der Übersicht per Klick auf eine Zeile gefiltert auf
|
||||||
|
eine/n Schüler*in anspringbar (`ClassTeacherDetailsViewModel.StudentFilter`).
|
||||||
|
**Zurückgestellte Ideen:** Verknüpfung Roster-Zeile ↔ lokaler `Student`/Navigation zum
|
||||||
|
Schülerdetail; ein Detail-Drill-down pro Schüler*in (Heatmap gefehlter Einzelstunden,
|
||||||
|
Verspätung/vorzeitiges Verlassen je Woche); eine KI-gestützte Zusammenfassung/Bewertung der
|
||||||
|
Einträge (pro Schüler*in oder gesamt) mit Namens-Anonymisierung/Synonymen vor dem Versand.
|
||||||
|
|
||||||
### 4.4 Wochen-/Tagesansicht
|
### 4.4 Wochen-/Tagesansicht
|
||||||
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
|
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
|
||||||
|
|||||||
Reference in New Issue
Block a user