Fix Untis homework comparison and student selection
This commit is contained in:
1 parent
1291b24478
commit
f3e23ad207
8 files changed
+372
-66
No files matched your search
@@ -1,4 +1,5 @@
|
||||
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||
using LehrerApp.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
@@ -28,4 +29,32 @@ public sealed class UntisNameMatchingTests
|
||||
{
|
||||
Assert.True(UntisNameMatching.NamesMatch("Gerste, Amelia", "Gerste Amelia"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildClassNameIndex_VerwendetDieselbeInstanzWieDieAuswahlliste()
|
||||
{
|
||||
var candidate = new Student { FirstName = "Amelia", LastName = "Gerste" };
|
||||
var repositoryCopy = new Student
|
||||
{
|
||||
Id = candidate.Id, FirstName = candidate.FirstName, LastName = candidate.LastName,
|
||||
};
|
||||
|
||||
var index = UntisNameMatching.BuildClassNameIndex([candidate], [repositoryCopy]);
|
||||
|
||||
Assert.Same(candidate, index[UntisNameMatching.NameKey("Gerste Amelia")]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildClassNameIndex_GleicheNamenInVerschiedenenKlassenBleibenEindeutig()
|
||||
{
|
||||
var classStudent = new Student { FirstName = "Amelia", LastName = "Gerste" };
|
||||
var otherStudent = new Student { FirstName = "Amelia", LastName = "Gerste" };
|
||||
var candidates = new[] { classStudent, otherStudent };
|
||||
|
||||
Assert.Empty(UntisNameMatching.BuildNameIndex(candidates));
|
||||
var index = UntisNameMatching.BuildClassNameIndex(candidates,
|
||||
[new Student { Id = classStudent.Id, FirstName = "Amelia", LastName = "Gerste" }]);
|
||||
|
||||
Assert.Same(classStudent, index[UntisNameMatching.NameKey("Gerste Amelia")]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.WebUntis;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class WebUntisHomeworkComparisonViewModelTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Fehlende HA", "Buch vergessen", "Negativ", true)]
|
||||
[InlineData("Sonstiges", "HA nicht dabei", "Negativ", false)]
|
||||
[InlineData(null, "HA nicht dabei", "Negativ", true)]
|
||||
[InlineData("Hausaufgaben fehlen", null, "Negativ", true)]
|
||||
[InlineData("Verhalten", "Schüler war unruhig", "Negativ", false)]
|
||||
[InlineData("Hausaufgaben", "HA gemacht", "Positiv", false)]
|
||||
[InlineData("Sonstiges", "Schach gespielt", "Negativ", false)]
|
||||
public void IsMissingHomeworkEntry_ErkenntAuchDasWebUntisKuerzel(
|
||||
string? category, string? entryText, string group, bool expected)
|
||||
{
|
||||
var entry = new UntisClassRegisterEventDto("10c", 20260925, "Che", "Gerste Amelia",
|
||||
category, group, entryText);
|
||||
|
||||
Assert.Equal(expected, WebUntisHomeworkComparisonViewModel.IsMissingHomeworkEntry(entry));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_ZeigtFehlendeHaAuchOhneHausaufgabeImFreitext()
|
||||
{
|
||||
const string report = "Klasse\tDatum\tFach\tName\tBenutzer\tEintragskategorie\tKategoriegruppe\tText\r\n" +
|
||||
"10c\t25.09.26\tChe\tGerste Amelia\tlehrkraft\tFehlende HA\tNegativ\tChe: Buch vergessen\r\n" +
|
||||
"10c\t25.09.26\tChe\tGerste Amelia\tlehrkraft\tHausaufgaben\tPositiv\tChe: alles erledigt\r\n";
|
||||
var handler = new QueueHandler(
|
||||
Json("{\"result\":{\"sessionId\":\"s\"}}"),
|
||||
Json("{\"result\":[{\"id\":1,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"),
|
||||
Json("{\"data\":{\"finished\":true,\"error\":false," +
|
||||
"\"reportParams\":\"get=rpt.tmp&name=ClassregEventPerStudent&format=csv\"}}"),
|
||||
new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(Encoding.UTF8.GetBytes(report)),
|
||||
});
|
||||
var settings = TestSupport.BuildWebUntisSettingsService();
|
||||
var credentials = new WebUntisCredentials("schule", "arche.webuntis.com", "lehrkraft", "geheim");
|
||||
settings.SetApiCredentials(credentials);
|
||||
await using var untis = new WebUntisIntegrationService(new HttpClient(handler), settings);
|
||||
await untis.ConnectAsync(credentials);
|
||||
var vm = new WebUntisHomeworkComparisonViewModel(untis, new FakeStudents([]),
|
||||
new FakeGroups([]), new FakeSubjects([]), new FakeMemberships([]),
|
||||
new FakeSessions([]), new FakeEntries(), new SchoolYearService())
|
||||
{
|
||||
StartDate = new DateTimeOffset(2026, 9, 24, 12, 0, 0, TimeSpan.Zero),
|
||||
EndDate = new DateTimeOffset(2026, 9, 26, 12, 0, 0, TimeSpan.Zero),
|
||||
};
|
||||
|
||||
await vm.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal("Gerste Amelia", Assert.Single(vm.Rows).UntisStudentName);
|
||||
Assert.Contains("1 passende Einträge von 2", vm.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadUndApply_TrenntBestehendenStatus_ZielstatusUndLokaleEintraege()
|
||||
{
|
||||
const string report = "Klasse\tDatum\tFach\tName\tBenutzer\tEintragskategorie\tKategoriegruppe\tText\r\n" +
|
||||
"10c\t25.09.26\tChe\tGerste Amelia\tlehrkraft\tFehlende HA\tNegativ\tChe: nicht gemacht\r\n" +
|
||||
"10c\t25.09.26\tChe\tMuster Ben\tlehrkraft\tFehlende HA\tNegativ\tChe: nicht gemacht\r\n";
|
||||
var handler = new QueueHandler(
|
||||
Json("{\"result\":{\"sessionId\":\"s\"}}"),
|
||||
Json("{\"result\":[{\"id\":1,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"),
|
||||
Json("{\"data\":{\"finished\":true,\"error\":false," +
|
||||
"\"reportParams\":\"get=rpt.tmp&name=ClassregEventPerStudent&format=csv\"}}"),
|
||||
new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(Encoding.UTF8.GetBytes(report)),
|
||||
});
|
||||
var settings = TestSupport.BuildWebUntisSettingsService();
|
||||
var credentials = new WebUntisCredentials("schule", "arche.webuntis.com", "lehrkraft", "geheim");
|
||||
settings.SetApiCredentials(credentials);
|
||||
await using var untis = new WebUntisIntegrationService(new HttpClient(handler), settings);
|
||||
await untis.ConnectAsync(credentials);
|
||||
|
||||
var date = new DateOnly(2026, 9, 25);
|
||||
var schoolYear = new SchoolYearService();
|
||||
var chemistry = new Subject { Name = "Chemie", ShortName = "Che" };
|
||||
var biology = new Subject { Name = "Biologie", ShortName = "Bio" };
|
||||
var classGroup = new LearningGroup { Name = "10c", Type = GroupType.Class,
|
||||
SchoolYear = schoolYear.CurrentSchoolYear(date) };
|
||||
var chemistryGroup = new LearningGroup { Name = "Chemie 10c", Type = GroupType.Course,
|
||||
SubjectId = chemistry.Id, SchoolYear = classGroup.SchoolYear };
|
||||
var biologyGroup = new LearningGroup { Name = "Biologie 10c", Type = GroupType.Course,
|
||||
SubjectId = biology.Id, SchoolYear = classGroup.SchoolYear };
|
||||
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 chemistrySession = new ParticipationSession { GroupId = chemistryGroup.Id, Date = date };
|
||||
var biologySession = new ParticipationSession { GroupId = biologyGroup.Id, Date = date };
|
||||
var memberships = new FakeMemberships([
|
||||
new GroupMembership { StudentId = amelia.Id, GroupId = chemistryGroup.Id },
|
||||
new GroupMembership { StudentId = amelia.Id, GroupId = biologyGroup.Id },
|
||||
new GroupMembership { StudentId = ben.Id, GroupId = chemistryGroup.Id },
|
||||
new GroupMembership { StudentId = carl.Id, GroupId = chemistryGroup.Id },
|
||||
]);
|
||||
var participation = new FakeEntries();
|
||||
participation.Add(new ParticipationEntry { SessionId = chemistrySession.Id, StudentId = amelia.Id,
|
||||
Homework = HomeworkStatus.MissingOpen, HomeworkMissing = true });
|
||||
participation.Add(new ParticipationEntry { SessionId = biologySession.Id, StudentId = amelia.Id,
|
||||
Homework = HomeworkStatus.PartiallyCompleted, HomeworkMissing = true });
|
||||
participation.Add(new ParticipationEntry { SessionId = chemistrySession.Id, StudentId = carl.Id,
|
||||
Homework = HomeworkStatus.MissingOverdue, HomeworkMissing = true });
|
||||
|
||||
var vm = new WebUntisHomeworkComparisonViewModel(untis,
|
||||
new FakeStudents([amelia, ben, carl]),
|
||||
new FakeGroups([classGroup, chemistryGroup, biologyGroup]),
|
||||
new FakeSubjects([chemistry, biology]), memberships,
|
||||
new FakeSessions([chemistrySession, biologySession]), participation, schoolYear)
|
||||
{
|
||||
StartDate = new DateTimeOffset(2026, 9, 24, 12, 0, 0, TimeSpan.Zero),
|
||||
EndDate = new DateTimeOffset(2026, 9, 26, 12, 0, 0, TimeSpan.Zero),
|
||||
};
|
||||
|
||||
await vm.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
var existing = Assert.Single(vm.Rows, r => r.AssignedStudent?.Id == amelia.Id);
|
||||
Assert.False(existing.Selected);
|
||||
Assert.Equal(HomeworkDisplay.Label(HomeworkStatus.MissingOpen), existing.LocalStatus);
|
||||
Assert.Equal(HomeworkDisplay.Label(HomeworkStatus.MissingOpen), existing.SelectedStatusName);
|
||||
var newRow = Assert.Single(vm.Rows, r => r.AssignedStudent?.Id == ben.Id);
|
||||
Assert.True(newRow.Selected);
|
||||
Assert.Equal("Noch kein Status erfasst", newRow.LocalStatus);
|
||||
Assert.Equal(2, vm.LocalOnlyRows.Count);
|
||||
Assert.Contains(vm.LocalOnlyRows, r => r.StudentName == amelia.FullName && r.GroupName == biologyGroup.Name);
|
||||
Assert.Contains(vm.LocalOnlyRows, r => r.StudentName == carl.FullName && r.GroupName == chemistryGroup.Name);
|
||||
|
||||
newRow.AssignedStudent = carl;
|
||||
Assert.Single(vm.LocalOnlyRows);
|
||||
Assert.False(newRow.Selected);
|
||||
newRow.AssignedStudent = ben;
|
||||
Assert.Equal(2, vm.LocalOnlyRows.Count);
|
||||
newRow.SelectedStatusName = HomeworkDisplay.Label(HomeworkStatus.SubmittedLate);
|
||||
vm.ApplyCommand.Execute(null);
|
||||
|
||||
var saved = participation.GetBySessionAndStudent(chemistrySession.Id, ben.Id);
|
||||
Assert.Equal(HomeworkStatus.SubmittedLate, saved?.Homework);
|
||||
Assert.False(saved?.HomeworkMissing);
|
||||
Assert.Equal(HomeworkDisplay.Label(HomeworkStatus.SubmittedLate), newRow.LocalStatus);
|
||||
Assert.False(newRow.Selected);
|
||||
Assert.Equal(HomeworkStatus.MissingOpen,
|
||||
participation.GetBySessionAndStudent(chemistrySession.Id, amelia.Id)?.Homework);
|
||||
}
|
||||
|
||||
private static HttpResponseMessage Json(string json) => new(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
};
|
||||
|
||||
private sealed class QueueHandler(params HttpResponseMessage[] responses) : HttpMessageHandler
|
||||
{
|
||||
private readonly Queue<HttpResponseMessage> _responses = new(responses);
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken) => Task.FromResult(_responses.Dequeue());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using LehrerApp.Core.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,4 +22,21 @@ public static class UntisNameMatching
|
||||
.Where(w => w.Length > 0)
|
||||
.Select(w => w.ToLowerInvariant())
|
||||
.OrderBy(w => w, StringComparer.Ordinal));
|
||||
|
||||
/// <summary>Verwendet die Instanzen aus der Auswahlliste. Repository-Abfragen für eine Klasse
|
||||
/// liefern neue Student-Objekte mit derselben Id, die ein ComboBox.SelectedItem nicht erkennt.</summary>
|
||||
public static Dictionary<string, Student> BuildClassNameIndex(IReadOnlyList<Student> candidates,
|
||||
IEnumerable<Student> classStudents)
|
||||
{
|
||||
var classStudentIds = classStudents.Select(s => s.Id).ToHashSet();
|
||||
return BuildNameIndex(candidates.Where(s => classStudentIds.Contains(s.Id)));
|
||||
}
|
||||
|
||||
public static Dictionary<string, Student> BuildNameIndex(IEnumerable<Student> candidates) =>
|
||||
candidates
|
||||
.Select(student => (Key: NameKey(student.FullName), Student: student))
|
||||
.Where(x => x.Key.Length > 0)
|
||||
.GroupBy(x => x.Key)
|
||||
.Where(group => group.Select(x => x.Student.Id).Distinct().Count() == 1)
|
||||
.ToDictionary(group => group.Key, group => group.First().Student);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public partial class WebUntisHomeworkRow : ObservableObject
|
||||
public required string ClassName { get; init; }
|
||||
public required DateOnly Date { get; init; }
|
||||
public required string UntisStudentName { get; init; }
|
||||
public string? CategoryName { get; init; }
|
||||
public string? SubjectLabel { get; init; }
|
||||
public string? Text { get; init; }
|
||||
public required IReadOnlyList<Student> Candidates { get; init; }
|
||||
@@ -26,15 +27,39 @@ public partial class WebUntisHomeworkRow : ObservableObject
|
||||
internal Action<WebUntisHomeworkRow>? OnAssignmentChanged { get; init; }
|
||||
public string DateLabel => Date.ToString("dd.MM.yyyy");
|
||||
public bool CanApply => SessionId is not null;
|
||||
private static readonly HomeworkStatus[] SelectableStatuses =
|
||||
[
|
||||
HomeworkStatus.MissingOpen, HomeworkStatus.MissingOverdue,
|
||||
HomeworkStatus.PartiallyCompleted, HomeworkStatus.PartialMissingOverdue,
|
||||
HomeworkStatus.PartialSubmittedLate, HomeworkStatus.SubmittedLate,
|
||||
HomeworkStatus.Completed,
|
||||
];
|
||||
public static string[] StatusOptions { get; } = SelectableStatuses.Select(s => HomeworkDisplay.Label(s)).ToArray();
|
||||
public HomeworkStatus SelectedStatus =>
|
||||
SelectableStatuses.FirstOrDefault(s => HomeworkDisplay.Label(s) == SelectedStatusName,
|
||||
HomeworkStatus.MissingOpen);
|
||||
|
||||
[ObservableProperty] private Student? _assignedStudent;
|
||||
[ObservableProperty] private string _localStatus = "ohne Zuordnung";
|
||||
[ObservableProperty] private Guid? _sessionId;
|
||||
[ObservableProperty] private bool _selected;
|
||||
[ObservableProperty] private string _selectedStatusName = HomeworkDisplay.Label(HomeworkStatus.MissingOpen);
|
||||
|
||||
partial void OnAssignedStudentChanged(Student? value) => OnAssignmentChanged?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>Lokal erfasste fehlende Hausaufgabe ohne zugeordnete WebUntis-Zeile. Dient nur als
|
||||
/// Kopiervorlage; WebUntis wird aus dem Dialog heraus nicht beschrieben.</summary>
|
||||
public sealed record LocalOnlyHomeworkRow(string StudentName, DateOnly Date, string GroupName,
|
||||
string? SubjectName, HomeworkStatus HomeworkStatus, bool PossibleUntisMatch)
|
||||
{
|
||||
public string DateLabel => Date.ToString("dd.MM.yyyy");
|
||||
public string StatusLabel => HomeworkDisplay.Label(HomeworkStatus);
|
||||
public string? Warning => PossibleUntisMatch ? "WebUntis-Eintrag am selben Tag ohne Fachzuordnung prüfen" : null;
|
||||
public string ClipboardText => $"{DateLabel} – {StudentName} ({GroupName})\n" +
|
||||
$"Fehlende HA{(string.IsNullOrWhiteSpace(SubjectName) ? "" : $" ({SubjectName})")}\n{StatusLabel}";
|
||||
}
|
||||
|
||||
/// <summary>Abgleich negativer Klassenbucheinträge zum Stichwort "Hausaufgabe" (eigene, siehe
|
||||
/// <see cref="WebUntisIntegrationService.GetOwnClassRegisterEventsAsync"/>) gegen
|
||||
/// <see cref="ParticipationEntry.Homework"/> - Dashboard-weit statt pro Lerngruppe, aus demselben
|
||||
@@ -43,10 +68,8 @@ public partial class WebUntisHomeworkRow : ObservableObject
|
||||
/// Schülerkennung noch eine feste Fach-/Lerngruppenzuordnung, aber der Lehrkraft-eigene Text enthält
|
||||
/// praktisch immer das Untis-Fachkürzel - darüber wird die passende Lerngruppe (SubjectId + aktive
|
||||
/// Mitgliedschaft am Eintragsdatum) aufgelöst, mehrdeutige Treffer bleiben unaufgelöst statt zu raten.
|
||||
/// Ein Eintrag setzt lokal ausschließlich <see cref="HomeworkStatus.MissingOpen"/> vor, und auch nur,
|
||||
/// wenn dort noch gar kein Status hinterlegt ist - bereits vorhandene, feinere Erfassungen ("Teilweise
|
||||
/// angefertigt", "nachgereicht" usw.) werden nie automatisch überschrieben, sondern nur zum Vergleich
|
||||
/// danebengestellt (Nutzerwunsch).</summary>
|
||||
/// Als Ziel ist <see cref="HomeworkStatus.MissingOpen"/> vorbelegt und vor der Übernahme änderbar.
|
||||
/// Bereits vorhandene lokale Stati werden nicht automatisch zur Übernahme markiert.</summary>
|
||||
public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
{
|
||||
private readonly WebUntisIntegrationService _untis;
|
||||
@@ -59,6 +82,7 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
private readonly SchoolYearService _schoolYears;
|
||||
|
||||
public ObservableCollection<WebUntisHomeworkRow> Rows { get; } = [];
|
||||
public ObservableCollection<LocalOnlyHomeworkRow> LocalOnlyRows { get; } = [];
|
||||
[ObservableProperty] private DateTimeOffset? _startDate = DateTimeOffset.Now.AddDays(-7);
|
||||
[ObservableProperty] private DateTimeOffset? _endDate = DateTimeOffset.Now;
|
||||
[ObservableProperty] private string _status = "Zeitraum wählen und Klassenbucheinträge laden.";
|
||||
@@ -80,11 +104,11 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
var start = DateOnly.FromDateTime((StartDate ?? DateTimeOffset.Now).LocalDateTime);
|
||||
var end = DateOnly.FromDateTime((EndDate ?? DateTimeOffset.Now).LocalDateTime);
|
||||
if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
|
||||
Busy = true; Rows.Clear();
|
||||
Busy = true; Rows.Clear(); LocalOnlyRows.Clear();
|
||||
try
|
||||
{
|
||||
var ownStudents = _students.GetAll();
|
||||
var globalIndex = BuildNameIndex(ownStudents);
|
||||
var globalIndex = UntisNameMatching.BuildNameIndex(ownStudents);
|
||||
// Wie beim Dokumentations-Abgleich zusätzlich pro Klasse indiziert, um gleiche Namen in
|
||||
// verschiedenen Klassen unterscheiden zu können.
|
||||
var classIndexes = _groups.GetAll()
|
||||
@@ -92,7 +116,8 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
.GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => BuildNameIndex(g.SelectMany(x => _students.GetByGroup(x.Id)).Distinct().ToList()),
|
||||
g => UntisNameMatching.BuildClassNameIndex(ownStudents,
|
||||
g.SelectMany(x => _students.GetByGroup(x.Id))),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var shortNameIndex = _subjects.GetAll()
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.ShortName))
|
||||
@@ -107,11 +132,39 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
.Where(x => x.Date is not null)
|
||||
.OrderBy(x => x.Date).ThenBy(x => x.Entry.StudentName);
|
||||
|
||||
var rowsLoaded = false;
|
||||
|
||||
void RefreshLocalOnlyRows()
|
||||
{
|
||||
LocalOnlyRows.Clear();
|
||||
var covered = Rows.Where(r => r.AssignedStudent is not null && r.SessionId is not null)
|
||||
.Select(r => (StudentId: r.AssignedStudent!.Id, SessionId: r.SessionId!.Value))
|
||||
.ToHashSet();
|
||||
foreach (var student in _students.GetAll(includeInactive: true))
|
||||
foreach (var localEntry in _participation.GetByStudent(student.Id))
|
||||
{
|
||||
var homework = HomeworkDisplay.Effective(localEntry);
|
||||
if (homework is null || !HomeworkDisplay.CountsAsMissing(homework)) continue;
|
||||
var session = _sessions.GetById(localEntry.SessionId);
|
||||
if (session is null || session.Date < start || session.Date > end ||
|
||||
covered.Contains((student.Id, session.Id))) continue;
|
||||
var group = _groups.GetById(session.GroupId);
|
||||
if (group is null) continue;
|
||||
var subjectName = group.SubjectId is { } subjectId ? _subjects.GetById(subjectId)?.ShortName : null;
|
||||
var possibleMatch = Rows.Any(r => r.Date == session.Date && r.SessionId is null &&
|
||||
(r.AssignedStudent?.Id == student.Id ||
|
||||
UntisNameMatching.NamesMatch(r.UntisStudentName, student.FullName)));
|
||||
LocalOnlyRows.Add(new LocalOnlyHomeworkRow(student.FullName, session.Date, group.Name,
|
||||
subjectName, homework.Value, possibleMatch));
|
||||
}
|
||||
}
|
||||
|
||||
void ResolveLocalMatch(WebUntisHomeworkRow row)
|
||||
{
|
||||
if (row.AssignedStudent is not { } student)
|
||||
{
|
||||
row.SessionId = null; row.LocalStatus = "ohne Zuordnung"; row.Selected = false;
|
||||
if (rowsLoaded) RefreshLocalOnlyRows();
|
||||
return;
|
||||
}
|
||||
var group = ResolveGroup(student, row.MatchedSubjectId, row.Date);
|
||||
@@ -123,10 +176,11 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
row.LocalStatus = session is null
|
||||
? row.MatchedSubjectId is null ? "Fachkürzel nicht erkannt"
|
||||
: group is null ? "kein passender Kurs gefunden" : "keine lokale Stunde"
|
||||
: HomeworkDisplay.Label(current);
|
||||
: current is null ? "Noch kein Status erfasst" : HomeworkDisplay.Label(current);
|
||||
// Nur vorbelegen, wenn lokal noch überhaupt nichts erfasst ist - jeder vorhandene
|
||||
// Status (auch ein bereits gesetztes "fehlt") bleibt unangetastet, siehe Klassenkommentar.
|
||||
row.Selected = session is not null && current is null;
|
||||
if (rowsLoaded) RefreshLocalOnlyRows();
|
||||
}
|
||||
|
||||
foreach (var (entry, date) in ordered)
|
||||
@@ -141,6 +195,7 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
var row = new WebUntisHomeworkRow
|
||||
{
|
||||
ClassName = entry.ClassName, Date = date!.Value, UntisStudentName = entry.StudentName,
|
||||
CategoryName = entry.CategoryName,
|
||||
SubjectLabel = subject?.ShortName, Text = entry.Text, Candidates = ownStudents,
|
||||
MatchedSubjectId = subject?.Id, OnAssignmentChanged = ResolveLocalMatch,
|
||||
};
|
||||
@@ -148,12 +203,16 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
row.AssignedStudent = match; // löst OnAssignedStudentChanged aus und setzt SessionId/LocalStatus/Selected
|
||||
}
|
||||
|
||||
rowsLoaded = true;
|
||||
RefreshLocalOnlyRows();
|
||||
|
||||
var unresolvedStudent = Rows.Count(x => x.AssignedStudent is null);
|
||||
var unresolvedSubject = Rows.Count(x => x.MatchedSubjectId is null);
|
||||
Status = $"{Rows.Count} Einträge \"fehlende Hausaufgabe\" erhalten" +
|
||||
Status = $"{Rows.Count} passende Einträge von {entries.Count} eigenen Klassenbucheinträgen" +
|
||||
(unresolvedStudent > 0 ? $", {unresolvedStudent} bitte manuell zuordnen" : "") +
|
||||
(unresolvedSubject > 0 ? $", bei {unresolvedSubject} kein Fachkürzel im Text erkannt" : "") +
|
||||
$". {Rows.Count(x => x.CanApply)} einer lokalen Stunde zuordenbar.";
|
||||
$". {Rows.Count(x => x.CanApply)} einer lokalen Stunde zuordenbar." +
|
||||
$" {LocalOnlyRows.Count} lokale fehlende Hausaufgaben ohne zugeordneten WebUntis-Eintrag.";
|
||||
}
|
||||
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
|
||||
finally { Busy = false; }
|
||||
@@ -168,11 +227,11 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
var studentId = row.AssignedStudent!.Id;
|
||||
var entry = _participation.GetBySessionAndStudent(row.SessionId!.Value, studentId)
|
||||
?? new ParticipationEntry { SessionId = row.SessionId.Value, StudentId = studentId };
|
||||
entry.Homework = HomeworkStatus.MissingOpen;
|
||||
entry.HomeworkMissing = true;
|
||||
entry.Homework = row.SelectedStatus;
|
||||
entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(row.SelectedStatus);
|
||||
entry.UpdatedAt = DateTime.UtcNow;
|
||||
_participation.Save(entry);
|
||||
row.LocalStatus = HomeworkDisplay.Label(HomeworkStatus.MissingOpen);
|
||||
row.LocalStatus = HomeworkDisplay.Label(row.SelectedStatus);
|
||||
row.Selected = false;
|
||||
}
|
||||
Status = $"{selected.Count} Hausaufgaben-Status übernommen.";
|
||||
@@ -194,12 +253,16 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
return candidates.Count == 1 ? candidates[0] : null;
|
||||
}
|
||||
|
||||
// Gleiche Heuristik wie ClassTeacherDetailsViewModel.ContainsHomework, zusätzlich auf negative
|
||||
// Einträge eingeschränkt (eine positive "Hausaufgabe"-Kategorie wäre kein Fehlen-Signal).
|
||||
private static bool IsMissingHomeworkEntry(UntisClassRegisterEventDto entry) =>
|
||||
string.Equals(entry.CategoryGroup, "Negativ", StringComparison.OrdinalIgnoreCase) &&
|
||||
(entry.CategoryName?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
entry.Text?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true);
|
||||
// Die Kategorie ist das verlässliche Signal. Der freie Text kann andere Hinweise enthalten und
|
||||
// wird nur verwendet, wenn WebUntis gar keine Kategorie geliefert hat. "HA" muss ein Wort sein.
|
||||
public static bool IsMissingHomeworkEntry(UntisClassRegisterEventDto entry) =>
|
||||
string.Equals(entry.CategoryGroup?.Trim(), "Negativ", StringComparison.OrdinalIgnoreCase) &&
|
||||
MentionsHomework(string.IsNullOrWhiteSpace(entry.CategoryName) ? entry.Text : entry.CategoryName);
|
||||
|
||||
private static bool MentionsHomework(string? value) =>
|
||||
value?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
(value is not null && Regex.IsMatch(value, @"(?<![\p{L}\p{Nd}])HA(?![\p{L}\p{Nd}])",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
|
||||
|
||||
private static Subject? MatchSubject(string? text, IReadOnlyDictionary<string, Subject> shortNameIndex)
|
||||
{
|
||||
@@ -210,15 +273,6 @@ public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
return null;
|
||||
}
|
||||
|
||||
// Wie bei den übrigen WebUntis-Abgleichen: Reihenfolge und Satzzeichen werden normalisiert,
|
||||
// aber nur innerhalb der Kandidaten eindeutige Namen automatisch zugeordnet.
|
||||
private static Dictionary<string, Student> BuildNameIndex(IReadOnlyList<Student> candidates) =>
|
||||
candidates
|
||||
.Select(student => (Key: UntisNameMatching.NameKey(student.FullName), Student: student))
|
||||
.GroupBy(x => x.Key)
|
||||
.Where(group => group.Select(x => x.Student).Distinct().Count() == 1)
|
||||
.ToDictionary(group => group.Key, group => group.First().Student);
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public partial class WebUntisDocumentationComparisonViewModel : ObservableObject
|
||||
try
|
||||
{
|
||||
var ownStudents = _students.GetAll();
|
||||
var globalIndex = BuildNameIndex(ownStudents);
|
||||
var globalIndex = UntisNameMatching.BuildNameIndex(ownStudents);
|
||||
// Zusätzlich pro Klasse (aktuelles Schuljahr) indiziert: löst den Fall "gleicher Name in
|
||||
// verschiedenen Klassen" auf, den ein rein globaler Namensabgleich nicht unterscheiden könnte.
|
||||
var currentSchoolYear = _schoolYears.CurrentSchoolYear();
|
||||
@@ -91,7 +91,8 @@ public partial class WebUntisDocumentationComparisonViewModel : ObservableObject
|
||||
.GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => BuildNameIndex(g.SelectMany(x => _students.GetByGroup(x.Id)).Distinct().ToList()),
|
||||
g => UntisNameMatching.BuildClassNameIndex(ownStudents,
|
||||
g.SelectMany(x => _students.GetByGroup(x.Id))),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var entries = await _untis.GetOwnClassRegisterEventsAsync(start, end);
|
||||
@@ -173,15 +174,6 @@ public partial class WebUntisDocumentationComparisonViewModel : ObservableObject
|
||||
Status = $"{selected.Count} Einträge aus WebUntis übernommen.";
|
||||
}
|
||||
|
||||
// Die gemeinsame Untis-Normalisierung ignoriert Reihenfolge, Komma und abweichende Leerzeichen.
|
||||
// Mehrdeutige Namen bleiben weiterhin bewusst ohne automatische Vorauswahl.
|
||||
private static Dictionary<string, Student> BuildNameIndex(IReadOnlyList<Student> candidates) =>
|
||||
candidates
|
||||
.Select(student => (Key: UntisNameMatching.NameKey(student.FullName), Student: student))
|
||||
.GroupBy(x => x.Key)
|
||||
.Where(group => group.Select(x => x.Student).Distinct().Count() == 1)
|
||||
.ToDictionary(group => group.Key, group => group.First().Student);
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
xmlns:conv="clr-namespace:LehrerApp.Desktop.Converters"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.WebUntisHomeworkComparisonDialog"
|
||||
x:DataType="vm:WebUntisHomeworkComparisonViewModel"
|
||||
Title="Fehlende Hausaufgaben mit WebUntis abgleichen" Width="1100" Height="680"
|
||||
MinWidth="900" MinHeight="480" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto" Margin="24" RowSpacing="8">
|
||||
Title="Fehlende Hausaufgaben mit WebUntis abgleichen" Width="1250" Height="760"
|
||||
MinWidth="1000" MinHeight="550" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,2*,Auto,Auto,1*,Auto" Margin="24" RowSpacing="8">
|
||||
<StackPanel Grid.Row="0" Spacing="4">
|
||||
<TextBlock Text="Fehlende Hausaufgaben mit WebUntis abgleichen" Classes="dialogtitle"/>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.65"
|
||||
Text="Nur eigene, negative Klassenbucheinträge zum Stichwort „Hausaufgabe“. Fach wird über das Untis-Fachkürzel im Text erkannt. Zeilen mit bereits vorhandenem lokalem Hausaufgaben-Status (z.B. „Teilweise“, „nachgereicht“) werden nie überschrieben, nur zum Vergleich angezeigt."/>
|
||||
Text="Eigene negative Klassenbucheinträge mit der Kategorie „Hausaufgabe“ oder „HA“. Der Zielstatus ist vor der Übernahme änderbar. Bereits lokal erfasste Stati bleiben unmarkiert; unten stehen lokale fehlende Hausaufgaben ohne zugeordneten WebUntis-Eintrag."/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
|
||||
<CalendarDatePicker SelectedDate="{Binding StartDate, Converter={x:Static conv:DateTimeOffsetToDateTimeConverter.Instance}}"/>
|
||||
@@ -19,20 +19,21 @@
|
||||
<Button Content="Klassenbucheinträge laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,70,60,55,1.1*,1.1*,1.4*,1.2*" ColumnSpacing="8" Margin="4,0">
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,70,60,60,1*,1*,1.5*,1.1*,1.1*" ColumnSpacing="8" Margin="4,0">
|
||||
<TextBlock Grid.Column="1" Text="Datum" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="2" Text="Klasse" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="3" Text="Fach" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="4" Text="Name (WebUntis)" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="5" Text="Zuordnung" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="6" Text="Text" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="7" Text="Lokaler Hausaufgaben-Status" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="6" Text="WebUntis-Eintrag" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="7" Text="Übernahme als" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="8" Text="Lokaler Status" FontSize="11" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="3">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WebUntisHomeworkRow">
|
||||
<Grid ColumnDefinitions="Auto,70,60,55,1.1*,1.1*,1.4*,1.2*" ColumnSpacing="8" Margin="0,3">
|
||||
<Grid ColumnDefinitions="Auto,70,60,60,1*,1*,1.5*,1.1*,1.1*" ColumnSpacing="8" Margin="0,3">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding Selected}" IsEnabled="{Binding CanApply}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding ClassName}" VerticalAlignment="Center"/>
|
||||
@@ -41,18 +42,47 @@
|
||||
<ComboBox Grid.Column="5" ItemsSource="{Binding Candidates}" SelectedItem="{Binding AssignedStudent}"
|
||||
DisplayMemberBinding="{Binding FullName}" PlaceholderText="Schüler*in wählen…"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Column="6" Text="{Binding Text}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="7" Text="{Binding LocalStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="6" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding CategoryName}" FontWeight="SemiBold" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding Text}" TextWrapping="Wrap" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="7" ItemsSource="{Binding StatusOptions}" SelectedItem="{Binding SelectedStatusName}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Center"
|
||||
ToolTip.Tip="{Binding SelectedStatusName}"/>
|
||||
<TextBlock Grid.Column="8" Text="{Binding LocalStatus}" TextWrapping="Wrap" VerticalAlignment="Center"
|
||||
ToolTip.Tip="{Binding LocalStatus}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="4" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
||||
<Grid Grid.Row="4" ColumnDefinitions="*,Auto" ColumnSpacing="8">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Status}" FontSize="12" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Schließen" Click="OnClose"/>
|
||||
<Button Grid.Column="2" Content="Markierte übernehmen" Command="{Binding ApplyCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
<Button Grid.Column="1" Content="Markierte übernehmen" Command="{Binding ApplyCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="5" Spacing="2">
|
||||
<TextBlock Text="Lokal erfasste fehlende Hausaufgaben ohne WebUntis-Gegenstück" FontWeight="SemiBold" FontSize="13"/>
|
||||
<TextBlock Text="Zum manuellen Nachtragen im Klassenbuch. Bei ungeklärter Fachzuordnung bitte den WebUntis-Eintrag am selben Tag prüfen."
|
||||
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="6">
|
||||
<ItemsControl ItemsSource="{Binding LocalOnlyRows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:LocalOnlyHomeworkRow">
|
||||
<Grid ColumnDefinitions="80,1.3*,1.2*,0.7*,1.6*,1.6*,Auto" ColumnSpacing="8" Margin="0,3">
|
||||
<TextBlock Grid.Column="0" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding StudentName}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding GroupName}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding SubjectName}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding StatusLabel}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="5" Text="{Binding Warning}" TextWrapping="Wrap" Foreground="DarkOrange" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="6" Content="In Zwischenablage" Click="OnCopyToClipboardClick"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
<Button Grid.Row="7" Content="Schließen" Click="OnClose" HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,5 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
@@ -8,4 +12,13 @@ public partial class WebUntisHomeworkComparisonDialog : Window
|
||||
public WebUntisHomeworkComparisonDialog() => InitializeComponent();
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private async void OnCopyToClipboardClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { DataContext: LocalOnlyHomeworkRow row }) return;
|
||||
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
|
||||
if (clipboard is null) return;
|
||||
await clipboard.SetTextAsync(row.ClipboardText);
|
||||
App.Services.GetRequiredService<NotificationService>().ShowSuccess("In die Zwischenablage kopiert.");
|
||||
}
|
||||
}
|
||||
@@ -1981,9 +1981,10 @@ Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
|
||||
`WebUntisDocumentationComparisonViewModel` (Dashboard-weit, da der WebUntis-"-alle-"-Bericht
|
||||
klassenübergreifend ist) und `WebUntisLessonAbsenceComparisonViewModel` (schreibt wie dort über
|
||||
`IParticipationRepository.GetBySessionAndStudent`/`Save`).
|
||||
- **Filter:** `CategoryGroup == "Negativ"` und `CategoryName`/`Text` enthält "Hausauf" — dieselbe
|
||||
Substring-Heuristik wie `ClassTeacherDetailsViewModel.ContainsHomework`, nur zusätzlich auf
|
||||
negative Einträge eingeschränkt.
|
||||
- **Filter:** `CategoryGroup == "Negativ"` und `CategoryName` enthält "Hausauf" oder das
|
||||
eigenständige Kürzel "HA". Nur wenn die Kategorie fehlt, wird der freie Text als Ersatz geprüft.
|
||||
Das schließt WebUntis-Kategorien wie "Fehlende HA" ein, ohne sonstige negative Einträge wegen
|
||||
beiläufiger Erwähnungen im freien Text einzubeziehen.
|
||||
- **Fach-Erkennung:** Der Klassenbuch-Bericht liefert kein zuverlässiges Fach-Feld, aber laut Nutzer
|
||||
enthält der mitgeschickte Text praktisch immer das Untis-Fachkürzel. Deshalb wird `Text` (Fallback
|
||||
`CategoryName`) gegen `Subject.ShortName` alle Lerngruppen-Fächer abgeglichen (Wortgrenzen-Split,
|
||||
@@ -1996,13 +1997,15 @@ Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
|
||||
statt irgendeine zu wählen (gleiches Vorsichtsprinzip wie beim Namensabgleich). Die
|
||||
`ParticipationSession` der Lerngruppe am Eintragsdatum liefert dann die Stunde für
|
||||
`IParticipationRepository`.
|
||||
- **Kein Überschreiben (Kernanforderung):** eine Zeile wird nur vorausgewählt, wenn
|
||||
`HomeworkDisplay.Effective(entry)` noch `null` ist, also lokal überhaupt noch nichts erfasst wurde
|
||||
— jeder bereits vorhandene Status (auch ein bereits gesetztes "fehlt", nicht nur "Teilweise"/
|
||||
"nachgereicht") bleibt unangetastet und wird nur zum Vergleich daneben angezeigt; die Checkbox
|
||||
bleibt trotzdem bedienbar, falls doch bewusst überschrieben werden soll. Übernommen wird immer nur
|
||||
`HomeworkStatus.MissingOpen` ("Nachreichen offen") — nie automatisch `MissingOverdue`, die weitere
|
||||
Nachverfolgung bleibt laut Nutzer App-intern.
|
||||
- **Übernahme als:** `HomeworkStatus.MissingOpen` ("Nachreichen offen") ist vorbelegt, lässt sich aber
|
||||
pro Zeile über eine ComboBox auf jeden Hausaufgabenstatus ändern. Eine Zeile wird nur vorausgewählt,
|
||||
wenn lokal noch kein Status erfasst wurde. Bereits vorhandene Stati bleiben unmarkiert und sichtbar;
|
||||
nur bewusst markierte Zeilen werden mit dem gewählten Status gespeichert. `HomeworkMissing` wird
|
||||
passend zum Status gesetzt.
|
||||
- **Lokal ohne WebUntis-Gegenstück:** Lokal erfasste fehlende Hausaufgaben im geladenen Zeitraum,
|
||||
denen keine WebUntis-Zeile derselben Schüler*in und lokalen Stunde zugeordnet werden konnte,
|
||||
erscheinen unten als Kopiervorlage für die manuelle Nacherfassung. Bei ungeklärter Fachzuordnung
|
||||
eines WebUntis-Eintrags am selben Tag erscheint ein Hinweis zur Prüfung.
|
||||
- **Bewusst kein Caching:** `GetOwnClassRegisterEventsAsync` wird wie beim bereits bestehenden
|
||||
`WebUntisDocumentationComparisonViewModel` direkt (ungecacht) aufgerufen statt über
|
||||
`UntisReportCacheService`, dessen Cache-Tabellen aktuell klassen-/`GetForeignClassRegisterEventsAsync`-
|
||||
@@ -2010,10 +2013,9 @@ Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
|
||||
eigenständiger, deutlich größerer Umbau gewesen, siehe Nutzer-Rückfrage). Beide "eigene Einträge"-
|
||||
Dialoge lösen pro Öffnen einen eigenen Live-Abruf aus; falls das in der Praxis stört, wäre eine
|
||||
gemeinsame Cache-Erweiterung ein sinnvoller separater Nachtrag.
|
||||
- Aus Konsistenz mit den beiden bestehenden, strukturell identischen Abgleichs-ViewModels bewusst
|
||||
ungetestet gelassen: `WebUntisIntegrationService` ist `sealed` und nicht auf ein Interface
|
||||
gemockt, `WebUntisLessonAbsenceComparisonViewModel`/`WebUntisDocumentationComparisonViewModel`
|
||||
haben aus demselben Grund ebenfalls keine Tests.
|
||||
- Regressionstests für HA-Kürzel, den Ladevorgang, die Zielstatus-Übernahme und lokale Einträge ohne
|
||||
WebUntis-Gegenstück mit synthetischen WebUntis-Klassenbuchberichten in
|
||||
`WebUntisHomeworkComparisonViewModelTests`.
|
||||
|
||||
**Nachtrag zu 4.3, Untis-Hub (September 2026):** Nutzer-Feedback: die vier bestehenden WebUntis-
|
||||
Abgleiche (Fehlzeiten je Lerngruppe, offene Stunden, Klassenbuch-, Hausaufgabenabgleich) liefen
|
||||
|
||||
Reference in new issue
Block a user