Automate overdue homework and show outstanding work in teaching mode
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
This commit is contained in:
1 parent
f3e23ad207
commit
4b73d2bea2
10 files changed
+314
-2
No files matched your search
@@ -0,0 +1,40 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class HomeworkOverdueServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Advance_Nach14Kalendertagen_NurOffeneNachreichungen()
|
||||
{
|
||||
var today = new DateOnly(2026, 9, 25);
|
||||
var group = new LearningGroup { IsActive = true };
|
||||
var archived = new LearningGroup { IsActive = false };
|
||||
var dueSession = new ParticipationSession { GroupId = group.Id, Date = today.AddDays(-14) };
|
||||
var recentSession = new ParticipationSession { GroupId = group.Id, Date = today.AddDays(-13) };
|
||||
var archivedSession = new ParticipationSession { GroupId = archived.Id, Date = today.AddDays(-30) };
|
||||
var open = new ParticipationEntry { SessionId = dueSession.Id, Homework = HomeworkStatus.MissingOpen };
|
||||
var legacy = new ParticipationEntry { SessionId = dueSession.Id, HomeworkMissing = true };
|
||||
var submitted = new ParticipationEntry { SessionId = dueSession.Id, Homework = HomeworkStatus.SubmittedLate };
|
||||
var partial = new ParticipationEntry { SessionId = dueSession.Id, Homework = HomeworkStatus.PartiallyCompleted };
|
||||
var recent = new ParticipationEntry { SessionId = recentSession.Id, Homework = HomeworkStatus.MissingOpen };
|
||||
var oldArchived = new ParticipationEntry { SessionId = archivedSession.Id, Homework = HomeworkStatus.MissingOpen };
|
||||
var entries = new FakeEntries();
|
||||
foreach (var entry in new[] { open, legacy, submitted, partial, recent, oldArchived }) entries.Add(entry);
|
||||
var service = new HomeworkOverdueService(new FakeGroups([group, archived]),
|
||||
new FakeSessions([dueSession, recentSession, archivedSession]), entries);
|
||||
|
||||
Assert.Equal(2, service.Advance(today));
|
||||
Assert.Equal(HomeworkStatus.MissingOverdue, open.Homework);
|
||||
Assert.Equal(HomeworkStatus.MissingOverdue, legacy.Homework);
|
||||
Assert.True(open.HomeworkMissing);
|
||||
Assert.True(legacy.HomeworkMissing);
|
||||
Assert.Equal(HomeworkStatus.SubmittedLate, submitted.Homework);
|
||||
Assert.Equal(HomeworkStatus.PartiallyCompleted, partial.Homework);
|
||||
Assert.Equal(HomeworkStatus.MissingOpen, recent.Homework);
|
||||
Assert.Equal(HomeworkStatus.MissingOpen, oldArchived.Homework);
|
||||
Assert.Equal(0, service.Advance(today));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class TeachingModeOutstandingHomeworkViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Refresh_ZeigtJungeOffeneNachreichungenUndFiltertAktuellAbwesende()
|
||||
{
|
||||
var date = new DateOnly(2026, 9, 25);
|
||||
var group = new LearningGroup { Name = "10c Chemie" };
|
||||
var amelia = new Student { FirstName = "Amelia", LastName = "Gerste" };
|
||||
var ben = new Student { FirstName = "Ben", LastName = "Muster" };
|
||||
var carl = new Student { FirstName = "Carl", LastName = "Beispiel" };
|
||||
var oldMember = new Student { FirstName = "Dana", LastName = "Alt" };
|
||||
var previous = new ParticipationSession { GroupId = group.Id, Date = date.AddDays(-5) };
|
||||
var older = new ParticipationSession { GroupId = group.Id, Date = date.AddDays(-130) };
|
||||
var current = new ParticipationSession { GroupId = group.Id, Date = date };
|
||||
var memberships = new FakeMemberships([
|
||||
new GroupMembership { StudentId = amelia.Id, GroupId = group.Id },
|
||||
new GroupMembership { StudentId = ben.Id, GroupId = group.Id },
|
||||
new GroupMembership { StudentId = carl.Id, GroupId = group.Id },
|
||||
new GroupMembership { StudentId = oldMember.Id, GroupId = group.Id,
|
||||
Period = MembershipPeriod.Custom, JoinedAt = date.AddDays(-30), LeftAt = date.AddDays(-1) },
|
||||
]);
|
||||
var entries = new FakeEntries();
|
||||
entries.Add(new ParticipationEntry { SessionId = previous.Id, StudentId = amelia.Id,
|
||||
Homework = HomeworkStatus.MissingOverdue });
|
||||
var benEntry = new ParticipationEntry { SessionId = previous.Id, StudentId = ben.Id,
|
||||
Homework = HomeworkStatus.MissingOpen };
|
||||
entries.Add(benEntry);
|
||||
entries.Add(new ParticipationEntry { SessionId = previous.Id, StudentId = carl.Id,
|
||||
Homework = HomeworkStatus.SubmittedLate });
|
||||
entries.Add(new ParticipationEntry { SessionId = previous.Id, StudentId = oldMember.Id,
|
||||
Homework = HomeworkStatus.MissingOverdue });
|
||||
entries.Add(new ParticipationEntry { SessionId = older.Id, StudentId = ben.Id,
|
||||
Homework = HomeworkStatus.MissingOverdue });
|
||||
entries.Add(new ParticipationEntry { SessionId = current.Id, StudentId = ben.Id,
|
||||
Attendance = AttendanceStatus.Present });
|
||||
entries.Add(new ParticipationEntry { SessionId = current.Id, StudentId = amelia.Id,
|
||||
Attendance = AttendanceStatus.ExcusePending });
|
||||
|
||||
var vm = new TeachingModeOutstandingHomeworkViewModel(group.Id, date,
|
||||
new FakeStudents([amelia, ben, carl, oldMember]), memberships,
|
||||
new FakeSessions([previous, older, current]), entries);
|
||||
vm.SetCurrentSessionId(current.Id);
|
||||
vm.IsExpanded = true;
|
||||
|
||||
Assert.Equal(ben.FullName, Assert.Single(vm.Rows).StudentName);
|
||||
Assert.Equal("Offene Nachreichungen (1)", vm.Header);
|
||||
vm.IncludeAbsentStudents = true;
|
||||
Assert.Equal(2, vm.Rows.Count);
|
||||
Assert.Contains(vm.Rows, r => r.StudentName == amelia.FullName &&
|
||||
r.Status == HomeworkStatus.MissingOverdue);
|
||||
|
||||
vm.IncludeAbsentStudents = false;
|
||||
var currentAmelia = entries.GetBySessionAndStudent(current.Id, amelia.Id)!;
|
||||
currentAmelia.Attendance = AttendanceStatus.Present;
|
||||
entries.Save(currentAmelia);
|
||||
vm.Refresh();
|
||||
Assert.Equal(2, vm.Rows.Count);
|
||||
|
||||
benEntry.Homework = HomeworkStatus.SubmittedLate;
|
||||
entries.Save(benEntry);
|
||||
vm.Refresh();
|
||||
Assert.Equal(amelia.FullName, Assert.Single(vm.Rows).StudentName);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public class App : Application
|
||||
private static bool _exitHandlerAttached;
|
||||
private static Task _initialPolls = Task.CompletedTask;
|
||||
private static readonly CancellationTokenSource StartupCancellation = new();
|
||||
private static DispatcherTimer? _homeworkOverdueTimer;
|
||||
|
||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
@@ -100,6 +101,7 @@ public class App : Application
|
||||
splash?.SetProgress(60, "Papierkorb aufräumen …");
|
||||
await Task.Run(() => Services.GetRequiredService<ITrashRepository>()
|
||||
.PurgeOlderThan(DateTime.UtcNow.AddDays(-30)));
|
||||
await AdvanceHomeworkAsync();
|
||||
|
||||
if (!_exitHandlerAttached)
|
||||
{
|
||||
@@ -126,6 +128,7 @@ public class App : Application
|
||||
main.EnableFinalSync(syncEngine);
|
||||
desktop.MainWindow = main;
|
||||
main.Show();
|
||||
StartHomeworkOverdueTimer();
|
||||
splash?.SetProgress(100, "Bereit");
|
||||
windowToClose?.Close();
|
||||
AppBootstrapper.Logger.Info($"Start: Hauptfenster nach {timer.ElapsedMilliseconds} ms geöffnet.");
|
||||
@@ -152,6 +155,30 @@ public class App : Application
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task AdvanceHomeworkAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var changed = await Task.Run(() => Services.GetRequiredService<HomeworkOverdueService>()
|
||||
.Advance(DateOnly.FromDateTime(DateTime.Today)));
|
||||
if (changed > 0) AppBootstrapper.Logger.Info($"{changed} offene Hausaufgaben als nicht nachgereicht markiert.");
|
||||
}
|
||||
catch (Exception ex) { AppBootstrapper.Logger.Error("Hausaufgaben-Fristprüfung fehlgeschlagen.", ex); }
|
||||
}
|
||||
|
||||
private static void StartHomeworkOverdueTimer()
|
||||
{
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromHours(1) };
|
||||
timer.Tick += async (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
await AdvanceHomeworkAsync();
|
||||
if (_serviceProvider is not null) timer.Start();
|
||||
};
|
||||
_homeworkOverdueTimer = timer;
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
/// <summary>Wechselt die Darstellung sofort, ohne Neustart (12.4) — Avalonia stylt den
|
||||
/// gesamten sichtbaren Baum automatisch neu, sobald sich <see cref="ThemeVariant"/>
|
||||
/// ändert.</summary>
|
||||
@@ -165,6 +192,7 @@ public class App : Application
|
||||
|
||||
private static void DisposeServices()
|
||||
{
|
||||
_homeworkOverdueTimer?.Stop();
|
||||
var serviceProvider = Interlocked.Exchange(ref _serviceProvider, null);
|
||||
if (serviceProvider is null) return;
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<GradingService>();
|
||||
services.AddSingleton<CompetencyAnalysisService>();
|
||||
services.AddSingleton<SchoolYearService>();
|
||||
services.AddSingleton<HomeworkOverdueService>();
|
||||
services.AddSingleton<GroupRolloverService>();
|
||||
services.AddSingleton<PublicHolidayService>();
|
||||
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.Services;
|
||||
|
||||
/// <summary>Schließt nicht nachgereichte Hausaufgaben 14 Kalendertage nach der Unterrichtsstunde.
|
||||
/// Nur noch offene Nachreichungen werden geändert; andere Stati und archivierte Gruppen bleiben
|
||||
/// unangetastet.</summary>
|
||||
public sealed class HomeworkOverdueService(
|
||||
IGroupRepository groups, IParticipationSessionRepository sessions,
|
||||
IParticipationRepository participation)
|
||||
{
|
||||
public const int GracePeriodDays = 14;
|
||||
|
||||
public int Advance(DateOnly today)
|
||||
{
|
||||
var cutoff = today.AddDays(-GracePeriodDays);
|
||||
var activeGroupIds = groups.GetAll().Where(g => g.IsActive).Select(g => g.Id).ToHashSet();
|
||||
var changed = 0;
|
||||
foreach (var session in sessions.GetAll()
|
||||
.Where(s => s.Date <= cutoff && activeGroupIds.Contains(s.GroupId)))
|
||||
foreach (var entry in participation.GetBySession(session.Id))
|
||||
{
|
||||
if (entry.Homework != HomeworkStatus.MissingOpen &&
|
||||
!(entry.Homework is null && entry.HomeworkMissing)) continue;
|
||||
entry.Homework = HomeworkStatus.MissingOverdue;
|
||||
entry.HomeworkMissing = true;
|
||||
participation.Save(entry);
|
||||
changed++;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
public sealed record OutstandingHomeworkRow(string StudentName, DateOnly Date, HomeworkStatus Status)
|
||||
{
|
||||
public string DateLabel => Date.ToString("dd.MM.yyyy");
|
||||
public string StatusLabel => HomeworkDisplay.Label(Status);
|
||||
}
|
||||
|
||||
/// <summary>Jüngste noch fehlende Hausaufgaben der aktuellen Lerngruppe. Liest bei jedem Öffnen
|
||||
/// und auf Wunsch erneut, damit die Anwesenheit der laufenden Stunde berücksichtigt wird.</summary>
|
||||
public partial class TeachingModeOutstandingHomeworkViewModel(
|
||||
Guid groupId, DateOnly lessonDate,
|
||||
IStudentRepository students, IGroupMembershipRepository memberships,
|
||||
IParticipationSessionRepository sessions, IParticipationRepository participation) : ObservableObject
|
||||
{
|
||||
private const int LookbackDays = 120;
|
||||
private const int MaxRows = 20;
|
||||
private Guid? _currentSessionId;
|
||||
|
||||
public ObservableCollection<OutstandingHomeworkRow> Rows { get; } = [];
|
||||
public string Header => $"Offene Nachreichungen ({Rows.Count})";
|
||||
public bool HasRows => Rows.Count > 0;
|
||||
|
||||
[ObservableProperty] private bool _isExpanded;
|
||||
[ObservableProperty] private bool _includeAbsentStudents;
|
||||
|
||||
partial void OnIsExpandedChanged(bool value)
|
||||
{
|
||||
if (value) Refresh();
|
||||
}
|
||||
|
||||
partial void OnIncludeAbsentStudentsChanged(bool value) => Refresh();
|
||||
|
||||
public void SetCurrentSessionId(Guid? sessionId) => _currentSessionId = sessionId;
|
||||
|
||||
[RelayCommand]
|
||||
public void Refresh()
|
||||
{
|
||||
Rows.Clear();
|
||||
var groupMemberships = memberships.GetByGroup(groupId);
|
||||
var roster = students.GetByGroup(groupId)
|
||||
.Where(s => groupMemberships.FirstOrDefault(m => m.StudentId == s.Id) is not { } membership ||
|
||||
GroupMembershipService.IsActiveOn(membership, lessonDate))
|
||||
.ToDictionary(s => s.Id);
|
||||
|
||||
var absentIds = _currentSessionId is { } sessionId
|
||||
? participation.GetBySession(sessionId)
|
||||
.Where(e => e.Attendance is not null and not AttendanceStatus.Present
|
||||
and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate)
|
||||
.Select(e => e.StudentId).ToHashSet()
|
||||
: [];
|
||||
|
||||
var rows = sessions.GetByGroup(groupId)
|
||||
.Where(s => s.Date < lessonDate && s.Date >= lessonDate.AddDays(-LookbackDays))
|
||||
.SelectMany(session => participation.GetBySession(session.Id)
|
||||
.Select(entry => (session.Date, Entry: entry)))
|
||||
.Where(x => roster.ContainsKey(x.Entry.StudentId) &&
|
||||
(IncludeAbsentStudents || !absentIds.Contains(x.Entry.StudentId)))
|
||||
.Select(x => (x.Date, x.Entry.StudentId, Status: HomeworkDisplay.Effective(x.Entry)))
|
||||
.Where(x => x.Status is { } status && HomeworkDisplay.CountsAsMissing(status))
|
||||
.OrderByDescending(x => x.Date)
|
||||
.ThenBy(x => roster[x.StudentId].FullName)
|
||||
.Take(MaxRows);
|
||||
|
||||
foreach (var row in rows)
|
||||
Rows.Add(new OutstandingHomeworkRow(roster[row.StudentId].FullName, row.Date, row.Status!.Value));
|
||||
OnPropertyChanged(nameof(Header));
|
||||
OnPropertyChanged(nameof(HasRows));
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,12 @@ public class TeachingModeViewModel
|
||||
/// der richtigen Sitzung starten.</summary>
|
||||
public ParticipationTabViewModel Participation { get; }
|
||||
public TeachingModeHomeworkViewModel Homework { get; }
|
||||
public TeachingModeOutstandingHomeworkViewModel? OutstandingHomework { get; }
|
||||
|
||||
public TeachingModeViewModel(Lesson lesson, LearningGroup group,
|
||||
IAlternativeLessonPathRepository alternativePaths, ILessonRepository lessons,
|
||||
SeatingPlanTabViewModel seatingPlan, ParticipationTabViewModel participation)
|
||||
SeatingPlanTabViewModel seatingPlan, ParticipationTabViewModel participation,
|
||||
TeachingModeOutstandingHomeworkViewModel? outstandingHomework = null)
|
||||
{
|
||||
GroupName = group.Name;
|
||||
Timeline = new TeachingTimelineViewModel(lesson, lessons, !group.IsActive);
|
||||
@@ -52,6 +54,16 @@ public class TeachingModeViewModel
|
||||
if (SeatingPlan.SelectedSession is { } linkedSession)
|
||||
Participation.SelectedSession =
|
||||
Participation.Sessions.FirstOrDefault(s => s.Id == linkedSession.Id);
|
||||
OutstandingHomework = outstandingHomework;
|
||||
OutstandingHomework?.SetCurrentSessionId(SeatingPlan.SelectedSession?.Id);
|
||||
OutstandingHomework?.Refresh();
|
||||
SeatingPlan.OnAssessmentChanged = () => OutstandingHomework?.Refresh();
|
||||
SeatingPlan.PropertyChanged += (_, args) =>
|
||||
{
|
||||
if (args.PropertyName != nameof(SeatingPlan.SelectedSession)) return;
|
||||
OutstandingHomework?.SetCurrentSessionId(SeatingPlan.SelectedSession?.Id);
|
||||
OutstandingHomework?.Refresh();
|
||||
};
|
||||
|
||||
// 4.5.4: dieselbe "letzte Stunde vor dieser"-Suche wie das Stundenplan-Badge
|
||||
// "Hausaufgabe kontrollieren" (TimetableViewModel.HasUnhandledHomework) - 120 Tage
|
||||
|
||||
@@ -157,6 +157,35 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Expander Header="{Binding OutstandingHomework.Header}"
|
||||
IsExpanded="{Binding OutstandingHomework.IsExpanded}">
|
||||
<StackPanel Spacing="8" Margin="0,6,0,0">
|
||||
<TextBlock Text="Jüngste fehlende Hausaufgaben der Lerngruppe (letzte 120 Tage)."
|
||||
FontSize="11" Opacity="0.65" TextWrapping="Wrap"/>
|
||||
<CheckBox Content="Abwesende dieser Stunde anzeigen"
|
||||
IsChecked="{Binding OutstandingHomework.IncludeAbsentStudents}" FontSize="12"/>
|
||||
<Button Content="Aktualisieren" Command="{Binding OutstandingHomework.RefreshCommand}"
|
||||
HorizontalAlignment="Left" FontSize="11" Padding="6,4"/>
|
||||
<TextBlock Text="Keine offenen Nachreichungen für die angezeigten Schüler."
|
||||
IsVisible="{Binding !OutstandingHomework.HasRows}" FontSize="12" TextWrapping="Wrap"/>
|
||||
<ItemsControl ItemsSource="{Binding OutstandingHomework.Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:OutstandingHomeworkRow">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="5" Padding="8,6" Margin="0,0,0,5">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" TextWrapping="Wrap"/>
|
||||
<TextBlock FontSize="11" Opacity="0.75" TextWrapping="Wrap">
|
||||
<Run Text="{Binding DateLabel}"/><Run Text=" · "/><Run Text="{Binding StatusLabel}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Hausaufgabe (diese Stunde)" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBox Text="{Binding Homework.CurrentHomework, Mode=TwoWay}" AcceptsReturn="True"
|
||||
|
||||
@@ -26,13 +26,19 @@ public partial class TeachingModeWindow : Window
|
||||
}
|
||||
var group = App.Services.GetRequiredService<IGroupRepository>().GetById(lesson.GroupId);
|
||||
if (group is null) return;
|
||||
var outstandingHomework = new TeachingModeOutstandingHomeworkViewModel(group.Id, lesson.Date,
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>());
|
||||
var window = new TeachingModeWindow
|
||||
{
|
||||
DataContext = new TeachingModeViewModel(lesson, group,
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
||||
App.Services.GetRequiredService<ILessonRepository>(),
|
||||
App.Services.GetRequiredService<SeatingPlanTabViewModel>(),
|
||||
App.Services.GetRequiredService<ParticipationTabViewModel>())
|
||||
App.Services.GetRequiredService<ParticipationTabViewModel>(),
|
||||
outstandingHomework)
|
||||
};
|
||||
OpenWindows.Add(lesson.Id, window);
|
||||
window.Closed += (_, _) => OpenWindows.Remove(lesson.Id);
|
||||
@@ -100,6 +106,7 @@ public partial class TeachingModeWindow : Window
|
||||
var dialog = new AttendanceHomeworkQuickInputDialog { DataContext = quickVm };
|
||||
await dialog.ShowDialog(this);
|
||||
(DataContext as TeachingModeViewModel)?.SeatingPlan.ReloadSeatBadgesFromRepository();
|
||||
(DataContext as TeachingModeViewModel)?.OutstandingHomework?.Refresh();
|
||||
}
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
|
||||
@@ -2017,6 +2017,21 @@ Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
|
||||
WebUntis-Gegenstück mit synthetischen WebUntis-Klassenbuchberichten in
|
||||
`WebUntisHomeworkComparisonViewModelTests`.
|
||||
|
||||
**Nachtrag zu 4.3/4.5, automatische Nachreichfrist und Unterrichtsansicht (September 2026):**
|
||||
- `HomeworkOverdueService` wandelt `MissingOpen` 14 Kalendertage nach dem Datum der lokalen
|
||||
`ParticipationSession` in `MissingOverdue` um, auch für ältere Einträge mit ausschließlich
|
||||
`HomeworkMissing=true`. Andere Stati und archivierte Gruppen bleiben unverändert. Die Prüfung
|
||||
läuft beim Programmstart sowie stündlich bei geöffneter App und ist wiederholbar ohne weitere
|
||||
Änderungen.
|
||||
- Der Unterrichtsmodus enthält links ein ausklappbares Panel für bis zu 20 jüngste noch fehlende
|
||||
Hausaufgaben der Lerngruppe aus den letzten 120 Tagen. Bereits nachgereichte Hausaufgaben und
|
||||
nicht mehr aktive Mitglieder werden ausgeblendet. Schülerinnen und Schüler mit einer als abwesend
|
||||
erfassten Anwesenheit in der aktuellen Sitzung sind standardmäßig ausgeblendet und können über
|
||||
eine Checkbox eingeblendet werden. Das Panel lädt beim Öffnen und per Aktualisieren-Button neu;
|
||||
nach dem Schnelldialog für Anwesenheit/Hausaufgaben ebenfalls.
|
||||
- Regressionstests prüfen die 14-Tage-Grenze, unveränderte andere Stati und archivierte Gruppen
|
||||
sowie die Panel-Filterung nach aktueller Anwesenheit, Mitgliedschaft und Nachreichstatus.
|
||||
|
||||
**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
|
||||
|
||||
Reference in new issue
Block a user