Zeigt die Fälligkeit der vier bestehenden, manuell ausgelösten WebUntis-Abgleiche (Fehlzeiten je Lerngruppe, offene Stunden, Klassenbuch-, Hausaufgabenabgleich) in einem neuen Hub-Fenster, ohne selbst WebUntis anzufragen - nur gespeicherte Zeitstempel werden ausgewertet. Konsolidiert die bisher verstreuten Einstiegspunkte (Sidebar-Button, Dashboard-Buttons) in ein neues WebUntis-Menü plus einen kompakten Gesundheits-Indikator auf dem Dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,14 @@ public interface IUntisCacheFetchStateRepository
|
||||
UntisCacheFetchState? Get(string className, UntisCacheKind kind);
|
||||
void Save(UntisCacheFetchState state);
|
||||
}
|
||||
/// Zuletzt-Lauf-Status der Untis-Hub-Jobs (siehe TODO.md) - je (Kind, GroupId), GroupId null bei
|
||||
/// den dashboard-weiten Jobs.
|
||||
public interface IUntisHubJobStateRepository
|
||||
{
|
||||
UntisHubJobState? Get(UntisHubJobKind kind, Guid? groupId);
|
||||
List<UntisHubJobState> GetAll();
|
||||
void Save(UntisHubJobState state);
|
||||
}
|
||||
/// Vom Nutzer bestätigte Zuordnungen WebUntis-Wochenmuster → LearningGroup.
|
||||
public interface IUntisSlotMappingRepository
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace LehrerApp.Core.Models;
|
||||
|
||||
public enum UntisHubJobKind
|
||||
{
|
||||
FehlzeitenKurz,
|
||||
FehlzeitenLang,
|
||||
OffenePeriods,
|
||||
Klassenbuchabgleich,
|
||||
Hausaufgabenabgleich,
|
||||
}
|
||||
|
||||
/// <summary>Wann welcher WebUntis-Abgleich ("Untis-Hub"-Job, siehe TODO.md) zuletzt lief und mit
|
||||
/// welchem Ergebnis - ein Datensatz pro (<see cref="Kind"/>, <see cref="GroupId"/>). Bei den drei
|
||||
/// dashboard-weiten Jobs (<see cref="UntisHubJobKind.OffenePeriods"/>,
|
||||
/// <see cref="UntisHubJobKind.Klassenbuchabgleich"/>, <see cref="UntisHubJobKind.Hausaufgabenabgleich"/>)
|
||||
/// ist <see cref="GroupId"/> null; die beiden Fehlzeiten-Kadenzen sind je Lerngruppe getrennt.</summary>
|
||||
public class UntisHubJobState
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public UntisHubJobKind Kind { get; set; }
|
||||
public Guid? GroupId { get; set; }
|
||||
public DateTime? LastRunAt { get; set; }
|
||||
public string? LastResultSummary { get; set; }
|
||||
}
|
||||
@@ -72,6 +72,7 @@ public class LiteDbContext : IDisposable
|
||||
public ILiteCollection<UntisAbsenceCacheEntry> UntisAbsenceCache => _db.GetCollection<UntisAbsenceCacheEntry>("untis_absence_cache");
|
||||
public ILiteCollection<UntisClassRegisterCacheEntry> UntisClassRegisterCache => _db.GetCollection<UntisClassRegisterCacheEntry>("untis_classregister_cache");
|
||||
public ILiteCollection<UntisCacheFetchState> UntisCacheFetchStates => _db.GetCollection<UntisCacheFetchState>("untis_cache_fetch_state");
|
||||
public ILiteCollection<UntisHubJobState> UntisHubJobStates => _db.GetCollection<UntisHubJobState>("untis_hub_job_states");
|
||||
public ILiteCollection<UntisStudentRosterCacheEntry> UntisStudentRosterCache => _db.GetCollection<UntisStudentRosterCacheEntry>("untis_student_roster_cache");
|
||||
public ILiteCollection<AnnualPlanEvent> AnnualPlanEvents => _db.GetCollection<AnnualPlanEvent>("annual_plan_events");
|
||||
public ILiteCollection<TrashedItem> TrashedItems => _db.GetCollection<TrashedItem>("trash");
|
||||
|
||||
@@ -899,6 +899,16 @@ public class UntisCacheFetchStateRepository(LiteDbContext db) : IUntisCacheFetch
|
||||
public void Save(UntisCacheFetchState state) => db.UntisCacheFetchStates.Upsert(state);
|
||||
}
|
||||
|
||||
public class UntisHubJobStateRepository(LiteDbContext db) : IUntisHubJobStateRepository
|
||||
{
|
||||
public UntisHubJobState? Get(UntisHubJobKind kind, Guid? groupId) =>
|
||||
db.UntisHubJobStates.FindOne(s => s.Kind == kind && s.GroupId == groupId);
|
||||
|
||||
public List<UntisHubJobState> GetAll() => db.UntisHubJobStates.FindAll().ToList();
|
||||
|
||||
public void Save(UntisHubJobState state) => db.UntisHubJobStates.Upsert(state);
|
||||
}
|
||||
|
||||
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
||||
{
|
||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||
|
||||
@@ -74,7 +74,8 @@ public sealed class DashboardViewModelTests
|
||||
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
||||
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(), NewCalendarSettings(),
|
||||
substitutions ?? new FakeSubstitutionEntries(), timeEntries ?? new FakeTimeEntries(), annualPlanEvents);
|
||||
substitutions ?? new FakeSubstitutionEntries(), timeEntries ?? new FakeTimeEntries(),
|
||||
TestSupport.BuildUntisHubService(), TestSupport.BuildWebUntisIntegrationService(), annualPlanEvents);
|
||||
}
|
||||
|
||||
/// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
|
||||
|
||||
@@ -39,6 +39,14 @@ public static class TestSupport
|
||||
return new WebUntisSettingsService(tempPath);
|
||||
}
|
||||
|
||||
/// Nicht konfiguriert (kein API-Login hinterlegt) - genügt für Tests, die nur den Konstruktor
|
||||
/// bedienen müssen und keinen echten WebUntis-Zugriff auslösen.
|
||||
public static WebUntisIntegrationService BuildWebUntisIntegrationService() =>
|
||||
new(new HttpClient(), BuildWebUntisSettingsService());
|
||||
|
||||
public static UntisHubService BuildUntisHubService(List<LearningGroup>? groups = null) =>
|
||||
new(new FakeGroups(groups ?? []), new FakeUntisHubJobStates(), new SchoolYearService());
|
||||
|
||||
/// Analog zu den übrigen dateibasierten Feed-Einstellungen: eigenes Temp-Verzeichnis.
|
||||
public static AnnualPlanSettingsService BuildAnnualPlanSettingsService()
|
||||
{
|
||||
@@ -525,6 +533,19 @@ public class FakeUntisCacheFetchStates : IUntisCacheFetchStateRepository
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeUntisHubJobStates : IUntisHubJobStateRepository
|
||||
{
|
||||
private readonly List<UntisHubJobState> _all = [];
|
||||
public UntisHubJobState? Get(UntisHubJobKind kind, Guid? groupId) =>
|
||||
_all.FirstOrDefault(s => s.Kind == kind && s.GroupId == groupId);
|
||||
public List<UntisHubJobState> GetAll() => _all.ToList();
|
||||
public void Save(UntisHubJobState state)
|
||||
{
|
||||
_all.RemoveAll(s => s.Kind == state.Kind && s.GroupId == state.GroupId);
|
||||
_all.Add(state);
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeWorkTasks : IWorkTaskRepository
|
||||
{
|
||||
private readonly List<WorkTask> _all = [];
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class UntisHubServiceTests
|
||||
{
|
||||
private static readonly DateTime UtcNow = new(2026, 9, 10, 8, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private static LearningGroup Group(string name, int? webUntisLessonId, bool isActive = true) =>
|
||||
new() { Name = name, WebUntisLessonId = webUntisLessonId, IsActive = isActive };
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_ProGruppeMitLessonId_ZweiFehlzeitenzeilenPlusDreiGlobaleZeilen()
|
||||
{
|
||||
var group = Group("9a", 42);
|
||||
|
||||
var rows = UntisHubService.BuildRows([group], [], UtcNow);
|
||||
|
||||
Assert.Equal(5, rows.Count);
|
||||
Assert.Equal(2, rows.Count(r => r.GroupId == group.Id));
|
||||
Assert.Contains(rows, r => r.Kind == UntisHubJobKind.FehlzeitenKurz && r.GroupId == group.Id);
|
||||
Assert.Contains(rows, r => r.Kind == UntisHubJobKind.FehlzeitenLang && r.GroupId == group.Id);
|
||||
Assert.Contains(rows, r => r.Kind == UntisHubJobKind.OffenePeriods && r.GroupId == null);
|
||||
Assert.Contains(rows, r => r.Kind == UntisHubJobKind.Klassenbuchabgleich && r.GroupId == null);
|
||||
Assert.Contains(rows, r => r.Kind == UntisHubJobKind.Hausaufgabenabgleich && r.GroupId == null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_NieGeprueft_GiltAlsUeberfaellig()
|
||||
{
|
||||
var rows = UntisHubService.BuildRows([], [], UtcNow);
|
||||
|
||||
Assert.All(rows, r => Assert.Equal(UntisHubDueState.Overdue, r.DueState));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_FehlzeitenKurzVorSechsTagen_GiltAlsOk()
|
||||
{
|
||||
var group = Group("9a", 42);
|
||||
var state = new UntisHubJobState
|
||||
{
|
||||
Kind = UntisHubJobKind.FehlzeitenKurz, GroupId = group.Id, LastRunAt = UtcNow.AddDays(-6),
|
||||
};
|
||||
|
||||
var rows = UntisHubService.BuildRows([group], [state], UtcNow);
|
||||
|
||||
var row = rows.Single(r => r.Kind == UntisHubJobKind.FehlzeitenKurz);
|
||||
Assert.Equal(UntisHubDueState.Ok, row.DueState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_FehlzeitenKurzVorSechzehnTagen_GiltAlsFaellig()
|
||||
{
|
||||
var group = Group("9a", 42);
|
||||
var state = new UntisHubJobState
|
||||
{
|
||||
Kind = UntisHubJobKind.FehlzeitenKurz, GroupId = group.Id, LastRunAt = UtcNow.AddDays(-16),
|
||||
};
|
||||
|
||||
var rows = UntisHubService.BuildRows([group], [state], UtcNow);
|
||||
|
||||
var row = rows.Single(r => r.Kind == UntisHubJobKind.FehlzeitenKurz);
|
||||
Assert.Equal(UntisHubDueState.Due, row.DueState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_FehlzeitenKurzVorZweiundzwanzigTagen_GiltAlsUeberfaellig()
|
||||
{
|
||||
var group = Group("9a", 42);
|
||||
var state = new UntisHubJobState
|
||||
{
|
||||
Kind = UntisHubJobKind.FehlzeitenKurz, GroupId = group.Id, LastRunAt = UtcNow.AddDays(-22),
|
||||
};
|
||||
|
||||
var rows = UntisHubService.BuildRows([group], [state], UtcNow);
|
||||
|
||||
var row = rows.Single(r => r.Kind == UntisHubJobKind.FehlzeitenKurz);
|
||||
Assert.Equal(UntisHubDueState.Overdue, row.DueState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_FehlzeitenLangHatDeutlichLaengereKadenzAlsKurz()
|
||||
{
|
||||
var group = Group("9a", 42);
|
||||
var lastRunAt = UtcNow.AddDays(-40);
|
||||
var states = new List<UntisHubJobState>
|
||||
{
|
||||
new() { Kind = UntisHubJobKind.FehlzeitenKurz, GroupId = group.Id, LastRunAt = lastRunAt },
|
||||
new() { Kind = UntisHubJobKind.FehlzeitenLang, GroupId = group.Id, LastRunAt = lastRunAt },
|
||||
};
|
||||
|
||||
var rows = UntisHubService.BuildRows([group], states, UtcNow);
|
||||
|
||||
Assert.Equal(UntisHubDueState.Overdue, rows.Single(r => r.Kind == UntisHubJobKind.FehlzeitenKurz).DueState);
|
||||
Assert.Equal(UntisHubDueState.Ok, rows.Single(r => r.Kind == UntisHubJobKind.FehlzeitenLang).DueState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_GruppeOhneWebUntisLessonId_WirdVonAufrufseiteAusgeschlossen()
|
||||
{
|
||||
// GetRows() filtert vorab auf WebUntisLessonId != null (siehe UntisHubService.GetRows) -
|
||||
// BuildRows selbst bekommt bereits nur die eligible-Gruppen übergeben.
|
||||
var eligible = new List<LearningGroup> { Group("9a", 42) };
|
||||
|
||||
var rows = UntisHubService.BuildRows(eligible, [], UtcNow);
|
||||
|
||||
Assert.Equal(2, rows.Count(r => r.GroupId != null));
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<IUntisClassRegisterCacheRepository, UntisClassRegisterCacheRepository>();
|
||||
services.AddSingleton<IUntisCacheFetchStateRepository, UntisCacheFetchStateRepository>();
|
||||
services.AddSingleton<IUntisStudentRosterCacheRepository, UntisStudentRosterCacheRepository>();
|
||||
services.AddSingleton<IUntisHubJobStateRepository, UntisHubJobStateRepository>();
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────
|
||||
services.AddSingleton<GradingService>();
|
||||
@@ -248,6 +249,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton(sp => new SchoolWeatherService(new HttpClient(), syncSettings));
|
||||
services.AddSingleton(sp => new WebUntisIntegrationService(new HttpClient(), untisSettings));
|
||||
services.AddSingleton<UntisReportCacheService>();
|
||||
services.AddSingleton<UntisHubService>();
|
||||
// War dieses Gerät schon eingeloggt, aber sync.key fehlt(e), wurde gerade eben (unten)
|
||||
// stillschweigend ein neuer, unabhängiger Schlüssel erzeugt - bisher unter dem ALTEN
|
||||
// Schlüssel synchronisierte Server-Daten sind für dieses Gerät dann nicht mehr lesbar.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Desktop.Views;
|
||||
using LehrerApp.Desktop.Views.Groups;
|
||||
using LehrerApp.Desktop.Views.Students;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Services;
|
||||
|
||||
/// <summary>Führt einen der vier bestehenden WebUntis-Abgleiche (unverändert, über ihre bestehenden
|
||||
/// Dialoge) aus und vermerkt das Ergebnis im <see cref="UntisHubService"/> - genutzt sowohl vom
|
||||
/// Untis-Hub-Dialog ("Jetzt prüfen" je Zeile) als auch von den WebUntis-Menüpunkten in
|
||||
/// <c>MainWindow</c>, damit beide Einstiegspunkte denselben Fälligkeitsstand pflegen.</summary>
|
||||
public static class UntisHubActions
|
||||
{
|
||||
public static async Task RunFehlzeitenAsync(Window owner, LearningGroup group, UntisHubJobKind kind,
|
||||
DateOnly start, DateOnly end, UntisHubService hub)
|
||||
{
|
||||
var vm = new WebUntisLessonAbsenceComparisonViewModel(group,
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>())
|
||||
{ StartDate = start.ToDateTime(TimeOnly.MinValue), EndDate = end.ToDateTime(TimeOnly.MinValue) };
|
||||
var loaded = TrackLoad(vm, v => v.Busy);
|
||||
await new WebUntisLessonAbsenceComparisonDialog { DataContext = vm }.ShowDialog(owner);
|
||||
if (loaded()) hub.RecordRun(kind, group.Id, vm.Status);
|
||||
}
|
||||
|
||||
public static async Task RunKlassenbuchAsync(Window owner, UntisHubService hub)
|
||||
{
|
||||
var vm = new WebUntisDocumentationComparisonViewModel(
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<IDocumentationRepository>(),
|
||||
App.Services.GetRequiredService<SchoolYearService>());
|
||||
var loaded = TrackLoad(vm, v => v.Busy);
|
||||
await new WebUntisDocumentationComparisonDialog { DataContext = vm }.ShowDialog(owner);
|
||||
if (loaded()) hub.RecordRun(UntisHubJobKind.Klassenbuchabgleich, null, vm.Status);
|
||||
}
|
||||
|
||||
public static async Task RunHausaufgabenAsync(Window owner, UntisHubService hub)
|
||||
{
|
||||
var vm = new WebUntisHomeworkComparisonViewModel(
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<ISubjectRepository>(),
|
||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>(),
|
||||
App.Services.GetRequiredService<SchoolYearService>());
|
||||
var loaded = TrackLoad(vm, v => v.Busy);
|
||||
await new WebUntisHomeworkComparisonDialog { DataContext = vm }.ShowDialog(owner);
|
||||
if (loaded()) hub.RecordRun(UntisHubJobKind.Hausaufgabenabgleich, null, vm.Status);
|
||||
}
|
||||
|
||||
public static async Task RunOffenePeriodsAsync(Window owner, UntisHubService hub)
|
||||
{
|
||||
var dialog = new OpenUntisPeriodsDialog();
|
||||
await dialog.ShowDialog(owner);
|
||||
hub.RecordRun(UntisHubJobKind.OffenePeriods, null, dialog.LastStatus);
|
||||
}
|
||||
|
||||
/// <summary>Erkennt nicht-invasiv (ohne die Vergleichs-ViewModels zu ändern), ob im Dialog
|
||||
/// tatsächlich ein Ladeversuch stattfand: <c>Busy</c> wechselt in <c>Load()</c> immer erst auf
|
||||
/// true und im <c>finally</c>-Block zurück auf false, egal ob erfolgreich oder mit Fehler
|
||||
/// abgebrochen - genau dieser Übergang wird hier beobachtet.</summary>
|
||||
private static Func<bool> TrackLoad<T>(T vm, Func<T, bool> isBusy) where T : ObservableObject
|
||||
{
|
||||
var loaded = false;
|
||||
var wasBusy = false;
|
||||
vm.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName != "Busy") return;
|
||||
var busy = isBusy(vm);
|
||||
if (wasBusy && !busy) loaded = true;
|
||||
wasBusy = busy;
|
||||
};
|
||||
return () => loaded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.Services;
|
||||
|
||||
public enum UntisHubDueState { Ok, Due, Overdue }
|
||||
|
||||
/// <summary>Eine Zeile im Untis-Hub: eine Job-Instanz (Fehlzeiten-Kadenz je Lerngruppe, oder einer
|
||||
/// der drei dashboard-weiten Jobs) mit ihrer aktuellen Fälligkeit.</summary>
|
||||
public sealed record UntisHubJobRow(
|
||||
UntisHubJobKind Kind, Guid? GroupId, string GroupName,
|
||||
DateTime? LastRunAt, string? LastResultSummary, UntisHubDueState DueState, string DueLabel);
|
||||
|
||||
/// <summary>
|
||||
/// Zeigt, welche der bestehenden, rein manuell ausgelösten WebUntis-Abgleiche (Fehlzeiten pro
|
||||
/// Lerngruppe, offene Stunden, Klassenbuch-/Hausaufgabenabgleich) fällig sind - ohne selbst
|
||||
/// WebUntis anzufragen (Nutzer-Feedback: "nicht bei WebUntis auffallen", siehe
|
||||
/// <see cref="UntisReportCacheService"/>). Die eigentlichen Abgleiche laufen weiterhin über die
|
||||
/// bestehenden Vergleichsdialoge (siehe <see cref="UntisHubActions"/>); dieser Service verwaltet
|
||||
/// nur die Fälligkeits-Zeitstempel dazu.
|
||||
/// </summary>
|
||||
public sealed class UntisHubService(
|
||||
IGroupRepository groups, IUntisHubJobStateRepository jobStates, SchoolYearService schoolYears)
|
||||
{
|
||||
private static readonly TimeSpan FehlzeitenKurzDue = TimeSpan.FromDays(14);
|
||||
private static readonly TimeSpan FehlzeitenKurzOverdue = TimeSpan.FromDays(21);
|
||||
private static readonly TimeSpan FehlzeitenLangDue = TimeSpan.FromDays(60);
|
||||
private static readonly TimeSpan FehlzeitenLangOverdue = TimeSpan.FromDays(90);
|
||||
private static readonly TimeSpan GlobalDue = TimeSpan.FromDays(14);
|
||||
private static readonly TimeSpan GlobalOverdue = TimeSpan.FromDays(21);
|
||||
|
||||
private static readonly (UntisHubJobKind Kind, string Label)[] GlobalJobs =
|
||||
[
|
||||
(UntisHubJobKind.OffenePeriods, "Offene Stunden"),
|
||||
(UntisHubJobKind.Klassenbuchabgleich, "Klassenbuchabgleich"),
|
||||
(UntisHubJobKind.Hausaufgabenabgleich, "Hausaufgabenabgleich"),
|
||||
];
|
||||
|
||||
public List<UntisHubJobRow> GetRows()
|
||||
{
|
||||
var eligibleGroups = groups.GetBySchoolYear(schoolYears.CurrentSchoolYear())
|
||||
.Where(g => g.WebUntisLessonId is not null)
|
||||
.OrderBy(g => g.Name)
|
||||
.ToList();
|
||||
return BuildRows(eligibleGroups, jobStates.GetAll(), DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public void RecordRun(UntisHubJobKind kind, Guid? groupId, string? summary) =>
|
||||
jobStates.Save(new UntisHubJobState
|
||||
{
|
||||
Id = jobStates.Get(kind, groupId)?.Id ?? Guid.NewGuid(),
|
||||
Kind = kind, GroupId = groupId, LastRunAt = DateTime.UtcNow, LastResultSummary = summary,
|
||||
});
|
||||
|
||||
/// Reine Entscheidungslogik ohne Repository-Zugriff (gleiches Muster wie
|
||||
/// <see cref="UntisReportCacheService.Plan"/>): aus den fälligkeitsrelevanten Lerngruppen und den
|
||||
/// zuletzt gespeicherten Job-Zuständen wird die vollständige Hub-Zeilenliste gebaut - zwei
|
||||
/// Fehlzeiten-Zeilen je Gruppe plus die drei dashboard-weiten Zeilen.
|
||||
public static List<UntisHubJobRow> BuildRows(
|
||||
IReadOnlyList<LearningGroup> eligibleGroups, IReadOnlyList<UntisHubJobState> states, DateTime utcNow)
|
||||
{
|
||||
UntisHubJobState? State(UntisHubJobKind kind, Guid? groupId) =>
|
||||
states.FirstOrDefault(s => s.Kind == kind && s.GroupId == groupId);
|
||||
|
||||
var rows = new List<UntisHubJobRow>();
|
||||
foreach (var group in eligibleGroups)
|
||||
{
|
||||
rows.Add(Row(UntisHubJobKind.FehlzeitenKurz, group.Id, group.Name,
|
||||
State(UntisHubJobKind.FehlzeitenKurz, group.Id), FehlzeitenKurzDue, FehlzeitenKurzOverdue, utcNow));
|
||||
rows.Add(Row(UntisHubJobKind.FehlzeitenLang, group.Id, group.Name,
|
||||
State(UntisHubJobKind.FehlzeitenLang, group.Id), FehlzeitenLangDue, FehlzeitenLangOverdue, utcNow));
|
||||
}
|
||||
foreach (var (kind, label) in GlobalJobs)
|
||||
rows.Add(Row(kind, null, label, State(kind, null), GlobalDue, GlobalOverdue, utcNow));
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static UntisHubJobRow Row(UntisHubJobKind kind, Guid? groupId, string groupName,
|
||||
UntisHubJobState? state, TimeSpan due, TimeSpan overdue, DateTime utcNow)
|
||||
{
|
||||
var (dueState, dueLabel) = DueStatus(state?.LastRunAt, due, overdue, utcNow);
|
||||
return new UntisHubJobRow(kind, groupId, groupName, state?.LastRunAt, state?.LastResultSummary,
|
||||
dueState, dueLabel);
|
||||
}
|
||||
|
||||
private static (UntisHubDueState, string) DueStatus(
|
||||
DateTime? lastRunAt, TimeSpan due, TimeSpan overdue, DateTime utcNow)
|
||||
{
|
||||
if (lastRunAt is null) return (UntisHubDueState.Overdue, "noch nie geprüft");
|
||||
var age = utcNow - lastRunAt.Value;
|
||||
var days = (int)age.TotalDays;
|
||||
if (age >= overdue) return (UntisHubDueState.Overdue, $"fällig seit {days} Tagen");
|
||||
if (age >= due) return (UntisHubDueState.Due, $"fällig seit {days} Tagen");
|
||||
return (UntisHubDueState.Ok, days == 0 ? "gerade eben geprüft" : $"vor {days} Tag(en) geprüft");
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
private readonly ITimeEntryRepository _timeEntries;
|
||||
private readonly IAnnualPlanEventRepository? _annualPlanEvents;
|
||||
private readonly SchoolWeatherService? _schoolWeather;
|
||||
private readonly UntisHubService _untisHub;
|
||||
private readonly WebUntisIntegrationService _webUntis;
|
||||
|
||||
private const int OpenExcuseMaxAgeDays = 21;
|
||||
private const int SupportPlanDueWithinDays = 14;
|
||||
@@ -143,6 +145,10 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public string AttentionSummary => AttentionCount == 1 ? "1 offener Punkt" : $"{AttentionCount} offene Punkte";
|
||||
public string UpcomingSummary => UpcomingCount == 1 ? "1 Termin" : $"{UpcomingCount} Termine";
|
||||
|
||||
[ObservableProperty] private string _webUntisHealthLabel = "";
|
||||
[ObservableProperty] private bool _isWebUntisHealthWarning;
|
||||
[ObservableProperty] private bool _isWebUntisHealthVisible;
|
||||
|
||||
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
||||
IExamRepository exams, IExamResultRepository examResults, IGradeRepository grades,
|
||||
IReportGradeRepository reportGrades, IGroupMembershipRepository memberships,
|
||||
@@ -153,6 +159,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
DashboardSettingsService dashboardSettings, ISchoolHolidayRepository schoolHolidays,
|
||||
PublicHolidayService publicHolidays, SchoolCalendarSettingsService calendarSettings,
|
||||
ISubstitutionEntryRepository substitutions, ITimeEntryRepository timeEntries,
|
||||
UntisHubService untisHub, WebUntisIntegrationService webUntis,
|
||||
IAnnualPlanEventRepository? annualPlanEvents = null,
|
||||
AnnualPlanSyncService? annualPlanSync = null, SchoolWeatherService? schoolWeather = null)
|
||||
{
|
||||
@@ -167,12 +174,28 @@ public partial class DashboardViewModel : ObservableObject
|
||||
_substitutions = substitutions;
|
||||
_annualPlanEvents = annualPlanEvents;
|
||||
_schoolWeather = schoolWeather;
|
||||
_untisHub = untisHub;
|
||||
_webUntis = webUntis;
|
||||
if (annualPlanSync is not null)
|
||||
{
|
||||
annualPlanSync.DataChanged += () => Avalonia.Threading.Dispatcher.UIThread.Post(LoadCalendar);
|
||||
}
|
||||
LoadDashboardCards();
|
||||
Load();
|
||||
RefreshWebUntisHealth();
|
||||
}
|
||||
|
||||
/// <summary>Liest nur den gespeicherten Fälligkeitsstand der Untis-Hub-Jobs (kein
|
||||
/// WebUntis-Zugriff, siehe <see cref="UntisHubService.GetRows"/>) - aufgerufen bei jedem
|
||||
/// Dashboard-Refresh und erneut, nachdem der Nutzer den Hub geöffnet/einen Abgleich gemacht hat.</summary>
|
||||
public void RefreshWebUntisHealth()
|
||||
{
|
||||
IsWebUntisHealthVisible = _webUntis.IsAvailable;
|
||||
if (!IsWebUntisHealthVisible) return;
|
||||
var rows = _untisHub.GetRows();
|
||||
var due = rows.Count(r => r.DueState != UntisHubDueState.Ok);
|
||||
IsWebUntisHealthWarning = due > 0;
|
||||
WebUntisHealthLabel = due > 0 ? $"WebUntis ⚠ {due} fällig" : "WebUntis ✓";
|
||||
}
|
||||
|
||||
private DashboardCardOption Card(string key) => DashboardCards.First(c => c.Key == key);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.UntisHub;
|
||||
|
||||
/// <summary>Eine Zeile im Untis-Hub-Fenster - reine Anzeige-Projektion von
|
||||
/// <see cref="UntisHubJobRow"/>, neu aufgebaut bei jedem <see cref="UntisHubViewModel.Load"/>.</summary>
|
||||
public sealed class UntisHubRowViewModel
|
||||
{
|
||||
public UntisHubJobKind Kind { get; }
|
||||
public Guid? GroupId { get; }
|
||||
public string GroupName { get; }
|
||||
public string JobLabel { get; }
|
||||
public string DueLabel { get; }
|
||||
public string? LastResultSummary { get; }
|
||||
public bool IsWarning { get; }
|
||||
public bool IsDanger { get; }
|
||||
|
||||
public UntisHubRowViewModel(UntisHubJobRow row)
|
||||
{
|
||||
Kind = row.Kind; GroupId = row.GroupId; GroupName = row.GroupName;
|
||||
JobLabel = Label(row.Kind); DueLabel = row.DueLabel; LastResultSummary = row.LastResultSummary;
|
||||
IsWarning = row.DueState == UntisHubDueState.Due;
|
||||
IsDanger = row.DueState == UntisHubDueState.Overdue;
|
||||
}
|
||||
|
||||
private static string Label(UntisHubJobKind kind) => kind switch
|
||||
{
|
||||
UntisHubJobKind.FehlzeitenKurz => "Fehlzeiten (kurzfristig)",
|
||||
UntisHubJobKind.FehlzeitenLang => "Fehlzeiten (seit Schuljahresbeginn)",
|
||||
UntisHubJobKind.OffenePeriods => "Offene Stunden",
|
||||
UntisHubJobKind.Klassenbuchabgleich => "Klassenbuchabgleich",
|
||||
UntisHubJobKind.Hausaufgabenabgleich => "Hausaufgabenabgleich",
|
||||
_ => kind.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>ViewModel des Untis-Hub-Fensters (siehe TODO.md) - zeigt nur den gespeicherten
|
||||
/// Fälligkeitsstand an (<see cref="UntisHubService.GetRows"/>, rein lesend aus LiteDB). Das
|
||||
/// tatsächliche Ausführen eines Jobs (inkl. WebUntis-Anfrage) übernimmt die Code-Behind-Klasse über
|
||||
/// <see cref="UntisHubActions"/>, weil dafür ein Fenster-Owner für <c>ShowDialog</c> gebraucht wird.</summary>
|
||||
public partial class UntisHubViewModel : ObservableObject
|
||||
{
|
||||
private readonly UntisHubService _hub;
|
||||
private readonly IGroupRepository _groups;
|
||||
|
||||
public ObservableCollection<UntisHubRowViewModel> Rows { get; } = [];
|
||||
[ObservableProperty] private bool _isAvailable;
|
||||
[ObservableProperty] private string _status = "";
|
||||
|
||||
public UntisHubViewModel(UntisHubService hub, IGroupRepository groups, WebUntisIntegrationService untis)
|
||||
{
|
||||
_hub = hub; _groups = groups;
|
||||
IsAvailable = untis.IsAvailable;
|
||||
Load();
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
Rows.Clear();
|
||||
if (!IsAvailable)
|
||||
{
|
||||
Status = "WebUntis ist nicht konfiguriert (siehe Einstellungen).";
|
||||
return;
|
||||
}
|
||||
foreach (var row in _hub.GetRows()) Rows.Add(new UntisHubRowViewModel(row));
|
||||
var overdue = Rows.Count(r => r.IsDanger);
|
||||
var due = Rows.Count(r => r.IsWarning);
|
||||
Status = overdue > 0 || due > 0
|
||||
? $"{overdue + due} von {Rows.Count} Prüfungen fällig ({overdue} überfällig)."
|
||||
: $"Alle {Rows.Count} Prüfungen aktuell.";
|
||||
}
|
||||
|
||||
public LearningGroup? FindGroup(Guid id) => _groups.GetById(id);
|
||||
}
|
||||
@@ -9,6 +9,12 @@
|
||||
<Setter Property="Foreground" Value="Red"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
<Style Selector="Button.webuntishealth">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppStatusOkBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Button.webuntishealth.warning">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppStatusWarningBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Border.daycell.selected">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
@@ -26,9 +32,11 @@
|
||||
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<WrapPanel Grid.Row="1" Orientation="Horizontal" ItemSpacing="8" LineSpacing="8" Margin="0,10,0,0">
|
||||
<Button Content="Klassenbuch abgleichen…" Click="OnCompareWebUntisDocumentationClick"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Content="Fehlende Hausaufgaben abgleichen…" Click="OnCompareWebUntisHomeworkClick"
|
||||
<!-- Fasst die vier WebUntis-Abgleiche zusammen, die jetzt nur noch im Menü "WebUntis"
|
||||
erreichbar sind (siehe TODO.md, Untis-Hub) - Klick öffnet den Hub. -->
|
||||
<Button Content="{Binding WebUntisHealthLabel}" Click="OnOpenUntisHubClick"
|
||||
IsVisible="{Binding IsWebUntisHealthVisible}"
|
||||
Classes="webuntishealth" Classes.warning="{Binding IsWebUntisHealthWarning}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Content="Bereiche anpassen" Command="{Binding ToggleDashboardSettingsCommand}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Desktop.Views.Groups;
|
||||
using LehrerApp.Desktop.Views.Students;
|
||||
using LehrerApp.Desktop.Views.UntisHub;
|
||||
using LehrerApp.Desktop.Views.Workload;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -32,32 +27,11 @@ public partial class DashboardView : UserControl
|
||||
return await WorkTaskDialogHelper.ShowDialog(owner, startAsReminder: startAsReminder);
|
||||
}
|
||||
|
||||
private async void OnCompareWebUntisDocumentationClick(object? sender, RoutedEventArgs e)
|
||||
private async void OnOpenUntisHubClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return;
|
||||
var dialogVm = new WebUntisDocumentationComparisonViewModel(
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<IDocumentationRepository>(),
|
||||
App.Services.GetRequiredService<SchoolYearService>());
|
||||
await new WebUntisDocumentationComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async void OnCompareWebUntisHomeworkClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return;
|
||||
var dialogVm = new WebUntisHomeworkComparisonViewModel(
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<ISubjectRepository>(),
|
||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>(),
|
||||
App.Services.GetRequiredService<SchoolYearService>());
|
||||
await new WebUntisHomeworkComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
|
||||
await new UntisHubDialog().ShowDialog(owner);
|
||||
if (DataContext is DashboardViewModel vm) vm.RefreshWebUntisHealth();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
darunter als Overlay-Drawer mit Hamburger-Button (automatisch).
|
||||
Kein eigener Code nötig.
|
||||
-->
|
||||
<DockPanel>
|
||||
<Menu DockPanel.Dock="Top">
|
||||
<MenuItem Header="WebUntis">
|
||||
<MenuItem Header="Untis-Hub…" Click="OnOpenUntisHub"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Offene Stunden…" Click="OnOpenUntisPeriods"/>
|
||||
<MenuItem Header="Klassenbuchabgleich…" Click="OnCompareUntisKlassenbuch"/>
|
||||
<MenuItem Header="Hausaufgabenabgleich…" Click="OnCompareUntisHausaufgaben"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<DrawerPage x:Name="RootDrawer"
|
||||
DrawerLength="220"
|
||||
DrawerBehavior="Auto"
|
||||
@@ -250,15 +260,6 @@
|
||||
<TextBlock Classes="navlabel" Text="Klassenlehrer"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" CornerRadius="6"
|
||||
Click="OnOpenUntisPeriods"
|
||||
ToolTip.Tip="Offene Untis-Stunden" AutomationProperties.Name="Offene Untis-Stunden">
|
||||
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
|
||||
<PathIcon Classes="navicon" Data="{StaticResource IconOpenPeriods}"/>
|
||||
<TextBlock Classes="navlabel" Text="Offene Untis-Stunden"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
CornerRadius="6"
|
||||
@@ -276,6 +277,7 @@
|
||||
</DrawerPage.Drawer>
|
||||
|
||||
</DrawerPage>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Globale Suche und Schnellerfassung (14.2). Bewusst als Overlay auf der aktuellen Seite:
|
||||
Der Nutzer behält den Kontext und kann mit Escape ohne Navigation zurückkehren. -->
|
||||
|
||||
@@ -3,7 +3,9 @@ using Avalonia.Input;
|
||||
using Avalonia.Threading;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels;
|
||||
using LehrerApp.Desktop.Views.UntisHub;
|
||||
using LehrerApp.Sync;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views;
|
||||
|
||||
@@ -23,9 +25,24 @@ public partial class MainWindow : Window
|
||||
KeyDown += OnWindowKeyDown;
|
||||
}
|
||||
|
||||
private async void OnOpenUntisHub(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
await new UntisHubDialog().ShowDialog(this);
|
||||
}
|
||||
|
||||
private async void OnOpenUntisPeriods(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
await new OpenUntisPeriodsDialog().ShowDialog(this);
|
||||
await UntisHubActions.RunOffenePeriodsAsync(this, App.Services.GetRequiredService<UntisHubService>());
|
||||
}
|
||||
|
||||
private async void OnCompareUntisKlassenbuch(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
await UntisHubActions.RunKlassenbuchAsync(this, App.Services.GetRequiredService<UntisHubService>());
|
||||
}
|
||||
|
||||
private async void OnCompareUntisHausaufgaben(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
await UntisHubActions.RunHausaufgabenAsync(this, App.Services.GetRequiredService<UntisHubService>());
|
||||
}
|
||||
|
||||
private void OnWindowKeyDown(object? sender, KeyEventArgs e)
|
||||
|
||||
@@ -23,6 +23,10 @@ public sealed class OpenUntisPeriodsDialog : Window
|
||||
private UntisOpenPeriodsMeta? _meta;
|
||||
private int _yearId;
|
||||
|
||||
/// <summary>Für den Untis-Hub (<see cref="UntisHubActions.RunOffenePeriodsAsync"/>): letzter
|
||||
/// Status-Text nach dem automatischen Laden beim Öffnen.</summary>
|
||||
public string? LastStatus => _status.Text;
|
||||
|
||||
public OpenUntisPeriodsDialog()
|
||||
{
|
||||
Title = "Offene WebUntis-Stunden";
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.UntisHub"
|
||||
x:Class="LehrerApp.Desktop.Views.UntisHub.UntisHubDialog"
|
||||
x:DataType="vm:UntisHubViewModel"
|
||||
Title="Untis-Hub"
|
||||
Width="900" Height="600" MinWidth="640" MinHeight="400"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Window.Styles>
|
||||
<Style Selector="TextBlock.duestatus">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppStatusOkBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="TextBlock.duestatus.warning">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppStatusWarningBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="TextBlock.duestatus.danger">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppStatusDangerBrush}"/>
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<DockPanel Margin="20">
|
||||
<StackPanel DockPanel.Dock="Top" Spacing="10" Margin="0,0,0,14">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="Untis-Hub" FontSize="22" FontWeight="SemiBold"/>
|
||||
<Button Grid.Column="1" Content="Aktualisieren" Click="OnRefreshClick"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding Status}" FontSize="12" Opacity="0.7" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid ItemsSource="{Binding Rows}" AutoGenerateColumns="False" IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal" BorderBrush="{DynamicResource AppCardBorderBrush}"
|
||||
BorderThickness="1" CanUserResizeColumns="True" CanUserSortColumns="True" RowHeight="46"
|
||||
IsVisible="{Binding IsAvailable}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Bereich" Binding="{Binding GroupName}" Width="1.3*"/>
|
||||
<DataGridTextColumn Header="Prüfung" Binding="{Binding JobLabel}" Width="1.3*"/>
|
||||
<DataGridTemplateColumn Header="Status" Width="1.1*">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:UntisHubRowViewModel">
|
||||
<TextBlock Text="{Binding DueLabel}" Classes="duestatus"
|
||||
Classes.warning="{Binding IsWarning}" Classes.danger="{Binding IsDanger}"
|
||||
VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Header="Letztes Ergebnis" Binding="{Binding LastResultSummary}" Width="2*"/>
|
||||
<DataGridTemplateColumn Header="" Width="130">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:UntisHubRowViewModel">
|
||||
<Button Content="Jetzt prüfen" Click="OnCheckClick" DataContext="{Binding}"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<TextBlock IsVisible="{Binding !IsAvailable}" Text="WebUntis ist nicht konfiguriert. Bitte zuerst in den Einstellungen hinterlegen."
|
||||
FontSize="14" Opacity="0.7" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,58 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.UntisHub;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.UntisHub;
|
||||
|
||||
public partial class UntisHubDialog : Window
|
||||
{
|
||||
public UntisHubDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new UntisHubViewModel(
|
||||
App.Services.GetRequiredService<UntisHubService>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>());
|
||||
}
|
||||
|
||||
private void OnRefreshClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is UntisHubViewModel vm) vm.Load();
|
||||
}
|
||||
|
||||
private async void OnCheckClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not UntisHubViewModel vm || sender is not Button { DataContext: UntisHubRowViewModel row })
|
||||
return;
|
||||
var hub = App.Services.GetRequiredService<UntisHubService>();
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
|
||||
switch (row.Kind)
|
||||
{
|
||||
case UntisHubJobKind.FehlzeitenKurz or UntisHubJobKind.FehlzeitenLang:
|
||||
var group = row.GroupId is { } id ? vm.FindGroup(id) : null;
|
||||
if (group is null) return;
|
||||
var schoolYears = App.Services.GetRequiredService<SchoolYearService>();
|
||||
var start = row.Kind == UntisHubJobKind.FehlzeitenKurz
|
||||
? today.AddDays(-30)
|
||||
: schoolYears.SchoolYearStart(schoolYears.CurrentSchoolYear());
|
||||
await UntisHubActions.RunFehlzeitenAsync(this, group, row.Kind, start, today, hub);
|
||||
break;
|
||||
case UntisHubJobKind.OffenePeriods:
|
||||
await UntisHubActions.RunOffenePeriodsAsync(this, hub);
|
||||
break;
|
||||
case UntisHubJobKind.Klassenbuchabgleich:
|
||||
await UntisHubActions.RunKlassenbuchAsync(this, hub);
|
||||
break;
|
||||
case UntisHubJobKind.Hausaufgabenabgleich:
|
||||
await UntisHubActions.RunHausaufgabenAsync(this, hub);
|
||||
break;
|
||||
}
|
||||
vm.Load();
|
||||
}
|
||||
}
|
||||
@@ -1995,6 +1995,34 @@ Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
|
||||
gemockt, `WebUntisLessonAbsenceComparisonViewModel`/`WebUntisDocumentationComparisonViewModel`
|
||||
haben aus demselben Grund ebenfalls keine Tests.
|
||||
|
||||
**Nachtrag zu 4.3, Untis-Hub (September 2026):** Nutzer-Feedback: die vier bestehenden WebUntis-
|
||||
Abgleiche (Fehlzeiten je Lerngruppe, offene Stunden, Klassenbuch-, Hausaufgabenabgleich) liefen
|
||||
bislang alle rein manuell und ohne jede Übersicht, wann welcher zuletzt lief — bei bewusst fehlendem
|
||||
Caching (s.o.) besteht sonst das Risiko, denselben Bericht aus Unsicherheit zu oft abzurufen ("bei
|
||||
WebUntis auffallen").
|
||||
- Neuer `UntisHubService` ([LehrerApp.Desktop/Services/UntisHubService.cs](LehrerApp.Desktop/Services/UntisHubService.cs))
|
||||
verwaltet nur Fälligkeits-Zeitstempel (`UntisHubJobState`, LiteDB-Collection
|
||||
`untis_hub_job_states`) — **kein** eigener WebUntis-Zugriff, reine Lesefunktion aus der
|
||||
lokalen DB. Der Fehlzeitenabgleich bekommt zwei Kadenzen je Lerngruppe mit gesetzter
|
||||
`WebUntisLessonId` (kurzfristig alle 14 Tage/fällig, 21 Tage/überfällig; langfristig seit
|
||||
Schuljahresbeginn alle 60/90 Tage), die drei dashboard-weiten Abgleiche je 14/21 Tage.
|
||||
- `UntisHubActions` ([LehrerApp.Desktop/Services/UntisHubActions.cs](LehrerApp.Desktop/Services/UntisHubActions.cs))
|
||||
führt die vier bestehenden Vergleichsdialoge unverändert aus und erkennt nicht-invasiv (über den
|
||||
`Busy`-Übergang der jeweiligen ViewModels) einen tatsächlichen Ladeversuch, um danach den
|
||||
Fälligkeitsstand zu aktualisieren — egal ob über den Hub oder das Menü ausgelöst.
|
||||
- Neues Fenster `UntisHubDialog` ([LehrerApp.Desktop/Views/UntisHub/](LehrerApp.Desktop/Views/UntisHub/))
|
||||
zeigt alle Job-Zeilen tabellarisch (Bereich, Prüfung, Fälligkeit, letztes Ergebnis, "Jetzt
|
||||
prüfen") — rein manuell, keine automatischen Hintergrund-Requests.
|
||||
- **Einstiegspunkte konsolidiert:** Nutzer wollte keine weiteren Einzelfunktionen in der Sidebar.
|
||||
`MainWindow` bekam dafür ihre erste klassische Menüleiste (bisher gab es keine) mit Menüpunkt
|
||||
"WebUntis" (Untis-Hub, Offene Stunden, Klassenbuchabgleich, Hausaufgabenabgleich); der bisherige
|
||||
Sidebar-Eintrag "Offene Untis-Stunden" sowie die beiden Dashboard-Buttons "Klassenbuch
|
||||
abgleichen…"/"Fehlende Hausaufgaben abgleichen…" (siehe Nachtrag oben) sind dafür entfallen.
|
||||
Auf dem Dashboard bleibt statt der Buttons nur noch ein kompakter Status-Badge ("WebUntis ✓" /
|
||||
"WebUntis ⚠ N fällig", nur sichtbar wenn WebUntis konfiguriert ist), der den Hub öffnet.
|
||||
Der Fehlzeitenabgleich-Button in der einzelnen Lerngruppe (`GroupDetailView`) blieb unverändert,
|
||||
da kontextgebunden.
|
||||
|
||||
### 4.4 Wochen-/Tagesansicht
|
||||
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
|
||||
("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.
|
||||
|
||||
Reference in New Issue
Block a user