WIP (unstable): WebUntis-iCal-Abgleich für Vertretungen/Ausfälle
Erkennt Vertretungen, Ausfälle und Zusatzaufsichten aus dem persönlichen WebUntis-iCal-Feed und schreibt sie automatisch als SubstitutionEntry. Bekannter offener Bug: es tauchen weiterhin falsche Vertretungen für Stunden auf, die real unverändert sind — wird in einem Folge-Commit untersucht, deshalb vorerst auf diesem Branch statt main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -890,6 +890,246 @@ war zudem redundant (das Bearbeiten-Tab ist direkt anklickbar) und fühlte sich
|
||||
- `TimetableViewModel.ShowEditorCommand` entfernt (kein Aufrufer mehr) statt umbenannt — es tat
|
||||
ohnehin nur `ActiveTabIndex = 1`, was jetzt nirgends mehr gebraucht wird.
|
||||
|
||||
**Nachtrag zu 4.3, neunte Iteration (WebUntis-iCal-Abgleich):** Nutzer-Feedback: der eigene
|
||||
Stundenplan ist zwar schon in der App hinterlegt, aber das Schulsystem (WebUntis) veröffentlicht
|
||||
zusätzlich einen persönlichen iCal-Feed mit Echtzeit-Änderungen (Vertretung, Ausfall,
|
||||
Raumänderung). Wunsch: die App soll diesen Feed periodisch selbst abrufen, in zwei Stufen — erst
|
||||
eine "fuzzy logic", die die regulären WebUntis-Fächer dem eigenen Stundenplan zuordnet und
|
||||
"bewacht", dass keines davon plötzlich nicht mehr passt, dann ein Abgleich gegen einen lokalen
|
||||
Schnappschuss, um konkrete Änderungen zu erkennen und als `SubstitutionEntry` zu übernehmen.
|
||||
|
||||
Vor dem Entwurf wurde der echte iCal-Feed des Nutzers einmalig testweise abgerufen (danach
|
||||
sofort wieder gelöscht), um nicht blind gegen die RFC-5545-Spezifikation zu entwickeln: iCal4j-
|
||||
generiert, keine RRULE-Wiederholung (jede Wochenstunde ist bereits ein eigenes VEVENT über ein
|
||||
Schuljahr, UID pro Wochen-Slot stabil), kein Line-Folding, keine VALARM-Blöcke — ein
|
||||
selbstgeschriebener schlanker Parser reicht, keine neue NuGet-Abhängigkeit (Nutzerentscheidung,
|
||||
Alternative wäre `Ical.Net` gewesen). Alle gesampelten Termine hatten `STATUS:CONFIRMED` (Feed
|
||||
kurz nach Schuljahresbeginn abgerufen) — das Design verlässt sich deshalb primär auf den
|
||||
Schnappschuss-Abgleich, nicht auf eine bestimmte WebUntis-Kodierung von Vertretungen.
|
||||
|
||||
- **`LehrerApp.Core/Services/IcsParser.cs`**: minimaler RFC-5545-Teilparser (VEVENT-Blöcke,
|
||||
`KEY;PARAM=VAL:VALUE`-Zeilen, Escaping, defensives Line-Unfolding und UTC-`Z`-Handling trotz im
|
||||
echten Feed nicht beobachtet) → `List<UntisIcsEvent>`.
|
||||
- **`LehrerApp.Core/Services/UntisMatchingService.cs`** (Stufe 1): leitet aus einem vollen Fetch
|
||||
das reguläre Wochenmuster ab (häufigste Fach/Klassen-Kombination je Wochentag+Uhrzeit über alle
|
||||
Wochen), löst Uhrzeit → Stundennummer über `PeriodScheduleService` auf und Klassen-Token →
|
||||
`LearningGroup` (exakt, dann normalisiert; bei kombinierten Klassen wird die Gruppe mit
|
||||
vorhandenem `TimetableSlot` bevorzugt). Lehrkraft-Kürzel wird nicht hartkodiert, sondern als
|
||||
häufigstes letztes Wort in `DESCRIPTION` erkannt. Liefert zusätzlich die Gegenrichtung:
|
||||
vorhandene `TimetableSlot`s ohne passendes iCal-Muster.
|
||||
- **`LehrerApp.Core/Services/UntisDiffService.cs`** (Stufe 2): vergleicht einen neuen Fetch gegen
|
||||
den letzten lokalen Schnappschuss je iCal-UID — abweichendes Fach/Klasse → `SubstitutionEntry`
|
||||
(`Kind=Lesson`), `STATUS:CANCELLED` oder ein im Lookahead-Fenster (14 Tage) verschwundener
|
||||
Termin → `Kind=Cancelled`. Nur Termine mit einer **bestätigten** `UntisSlotMapping` erzeugen
|
||||
automatisch Einträge — unbestätigte Muster fließen nur in die Abweichungs-Zählung ein.
|
||||
- **Neue Modelle** (`LehrerApp.Core/Models/UntisSync.cs`): `UntisSnapshotEntry` (lokale
|
||||
Sicherungskopie je Termin, für den Abgleich) und `UntisSlotMapping` (vom Nutzer bestätigte
|
||||
Zuordnung Wochenmuster → Gruppe, inkl. aufgelöster Stundennummer). `SubstitutionEntry` um
|
||||
`ExternalId` (iCal-UID) erweitert — macht wiederholte Abgleich-Läufe idempotent (Update statt
|
||||
Duplikat, neue `ISubstitutionEntryRepository.GetByExternalId`). Beide neuen Repositories feuern
|
||||
bewusst **keinen** `db.OnChange` (siehe Kommentar in `AllRepositories.cs`) — der Schnappschuss
|
||||
ist reines lokales Abgleich-Zwischenmaterial, die Zuordnung hängt an der pro Gerät hinterlegten
|
||||
WebUntis-URL, beides ergibt über Sync keinen Mehrwert.
|
||||
- **`WebUntisSettingsService`** (`LehrerApp.Desktop/Services/`): exaktes Abbild von
|
||||
`AiSettingsService`/`SyncSettingsService` — die iCal-URL trägt ein eingebettetes Auth-Token und
|
||||
wird deshalb wie ein Passwort behandelt (AES-256-GCM über `SyncCrypto`, eigener
|
||||
dateirechte-geschützter Schlüssel, nie Klartext in der JSON-Konfigurationsdatei).
|
||||
- **`UntisSyncService`** (`LehrerApp.Desktop/Services/`): Timer/Gate/Dispose-Muster wie
|
||||
`SyncEngine` (60-Minuten-Intervall, `SemaphoreSlim(1,1)` mit `WaitAsync(0)` statt Warteschlange),
|
||||
nur registriert, wenn URL hinterlegt und aktiviert (`AppBootstrapper.cs`, gleiches
|
||||
bedingte-Registrierung-Muster wie beim Sync-Server). Der reine Verarbeitungskern
|
||||
(`ProcessIcsText`) ist ohne HTTP-Zugriff gehalten und direkt mit vorgefertigtem ICS-Text
|
||||
testbar (public statt internal, da diese Codebasis kein `InternalsVisibleTo` nutzt).
|
||||
- **Settings-Tab "Stundenplan-Abgleich"**: iCal-URL-Eingabe (maskiert), Aktivieren, "Jetzt
|
||||
abrufen", Status letzter Abgleich, "Zuordnung prüfen…" öffnet `UntisMappingReviewDialog` — Liste
|
||||
der erkannten Wochenmuster mit vorgeschlagener Gruppe (vorbefüllt bei sicherem Match), die der
|
||||
Nutzer bestätigt oder ändert, bevor automatische Vertretungen dafür geschrieben werden (bewusste
|
||||
Design-Entscheidung: die erstmalige Zuordnung ist fehleranfällig, laufende Tages-Änderungen
|
||||
danach nicht mehr — sie landen im ohnehin jederzeit von Hand korrigierbaren
|
||||
`SubstitutionEntry`-Mechanismus).
|
||||
- **Abweichungs-Banner** im Stundenplan (`TimetableViewModel.HasUntisMismatch`): vergleicht nur
|
||||
den bereits lokal bestätigten Zuordnungsstand gegen die aktuellen `TimetableSlot`s (kein
|
||||
erneuter iCal-Abruf beim Seitenaufruf) — bleibt komplett verborgen, solange der Abgleich nicht
|
||||
aktiviert ist.
|
||||
- Wie beim Sync-Server deckt ein `AppBootstrapper.RestartApplication()` das Registrieren von
|
||||
`UntisSyncService` ab (nur einmalig beim Start bedingt registriert, kein
|
||||
Live-Re-Registrierungspfad).
|
||||
|
||||
**Nachtrag zu 4.3, zehnte Iteration (Bugfix Zuordnungs-Gedächtnis + Aufsichten):**
|
||||
Nutzer-Feedback nach erstem echtem Ausprobieren: *"Kann es sein, dass er meine Verbesserungen gar
|
||||
nicht einspeichert"* — Ursache war, dass `UntisMappingReviewDialogViewModel.LoadAsync` beim
|
||||
erneuten Öffnen die Zeilen immer frisch aus der Mustererkennung aufgebaut hat, ohne zuvor
|
||||
bestätigte `UntisSlotMapping`s zu berücksichtigen — eine manuelle Korrektur war zwar tatsächlich
|
||||
gespeichert, wurde beim nächsten Öffnen aber von der reinen Algorithmus-Vermutung überschrieben
|
||||
angezeigt. Zusätzlich legte jedes Speichern für denselben Slot ein **neues** `UntisSlotMapping`
|
||||
mit neuer Id an, statt das vorhandene zu aktualisieren — bei zwei Mappings für denselben
|
||||
(Weekday,StartTime)-Schlüssel hätte `UntisDiffService.Diff` beim nächsten Poll mit einer
|
||||
`ArgumentException` abgebrochen (unbehandelt im Timer-Callback, hätte den gesamten Prozess
|
||||
beendet).
|
||||
- `UntisMappingReviewDialogViewModel` bekommt `IUntisSlotMappingRepository` injiziert, lädt
|
||||
bestehende Zuordnungen vor dem Aufbau der Zeilen und übergibt sie an `UntisMappingRow`, das die
|
||||
vorherige Bestätigung bevorzugt vor dem reinen Algorithmus-Vorschlag vorbefüllt.
|
||||
- `UntisMappingRow.BuildMapping()` (neu) trägt beim erneuten Bestätigen desselben Slots die
|
||||
vorhandene Id weiter, statt immer `Guid.NewGuid()` zu vergeben — Speichern ist jetzt ein
|
||||
echtes Update, kein Duplikat.
|
||||
- `UntisDiffService` baut die Mapping-Lookup-Tabelle zusätzlich defensiv per `GroupBy` statt
|
||||
direktem `ToDictionary` (das zuletzt angelegte Mapping gewinnt), falls doch einmal mehrere
|
||||
Zeilen für denselben Slot existieren — kein Crash mehr, nur ein stillschweigend ignoriertes
|
||||
Altmapping. `UntisSyncService.PollAsync` fängt außerdem jede Ausnahme aus der Verarbeitung
|
||||
selbst ab (vorher nur der HTTP-Abruf) und trägt sie in den Sync-Status ein, statt den
|
||||
Timer-Callback unbehandelt abstürzen zu lassen.
|
||||
|
||||
Zweites Feedback: *"Zwei Termine sind meine Aufsichten, die nicht zugeordnet werden können. Vom
|
||||
Zeitraster und von der Dauer her, könnten die erfasst werden, oder es muss noch diese Option
|
||||
geben."* — Termine ohne Klassenbezug (Aufsicht/Springstunde, DESCRIPTION nur Lehrkraft-Kürzel)
|
||||
liegen typischerweise in einer Pause zwischen zwei Unterrichtsstunden, nicht auf einer
|
||||
konfigurierten Stunden-Startzeit — `UntisMatchingService.ResolvePeriodNumber` (exakte oder nur
|
||||
minutengenau tolerante Übereinstimmung) konnte sie deshalb grundsätzlich nie auflösen, und der
|
||||
Review-Dialog bot dafür auch keine passende Bestätigungsoption (nur eine Lerngruppen-Auswahl).
|
||||
- `UntisSlotMapping` um `Kind` (`SubstitutionKind`, Standard `Lesson`) und `AfterPeriod` erweitert;
|
||||
`GroupId`/`PeriodNumber` sind jetzt nullable (nur bei `Kind=Lesson` gesetzt).
|
||||
- `UntisMatchingService.BuildMatches`: Muster ohne Klassenanteil bekommen statt einer
|
||||
Stundennummer ein `AfterPeriod` — die Pause direkt vor der Startzeit (letzte Stunde, deren Ende
|
||||
≤ Startzeit liegt; 0 vor der ersten Stunde) — anders als bei Unterrichtsstunden **immer**
|
||||
auflösbar, da eine Pause per Definition zwischen/vor Stunden liegt statt exakt auf einer
|
||||
Startzeit.
|
||||
- `UntisMappingReviewDialog`: Zeilen ohne Klassenbezug zeigen statt der Lerngruppen-ComboBox eine
|
||||
Checkbox "Als Aufsicht bestätigen".
|
||||
- `UntisDiffService`: Kind-abhängige Behandlung — bei `Supervision` erzeugt ein verschwundener
|
||||
oder als `STATUS:CANCELLED` markierter Termin einen `SubstitutionEntry { Kind = Supervision,
|
||||
AfterPeriod = ... }` statt `Cancelled` (das bleibt Unterrichtsstunden vorbehalten, da die
|
||||
Anzeige dafür Gruppe/Fach aus einem `TimetableSlot` herleitet, den es für Aufsichten nicht
|
||||
gibt). Feingranulare "noch da, aber Ort geändert"-Erkennung für Aufsichten bewusst nicht
|
||||
gebaut (kein zusätzliches Location-Feld auf dem Mapping) — reicht für den gemeldeten Fall
|
||||
(Aufsicht verschwindet/wird übernommen) und hält den Umfang klein.
|
||||
|
||||
**Nachtrag zu 4.3, elfte Iteration (Bugfix Doppelstunden):** Nutzer-Feedback nach dem
|
||||
Zuordnen aller Muster: *"Ich habe aber jetzt alles zugeordnet, und trotzdem erhalte ich die
|
||||
Warnung, dass 16 Stunden ohne Untis-Zuordnung sind."* Ursache: WebUntis fasst eine Doppelstunde
|
||||
(zwei aufeinanderfolgende Stunden desselben Fachs/derselben Gruppe) zu einem EINZIGEN VEVENT über
|
||||
beide Stundenzeiten hinweg zusammen (z.B. 07:50–09:20 für Stunde 1+2), während der Stundenplan der
|
||||
App dafür zwei separate `TimetableSlot`-Einträge haben kann. `UntisMatchingService` löste bisher
|
||||
nur eine einzelne Stundennummer aus der Startzeit auf — die zweite Stunde jeder Doppelstunde
|
||||
konnte dadurch nie als zugeordnet gelten, unabhängig davon, was im Review-Dialog bestätigt wurde
|
||||
(sie tauchte dort als eigene Zeile gar nicht erst auf).
|
||||
- `UntisSlotMatch`/`UntisSlotMapping` um `CoveredPeriods` (`List<int>`) erweitert — alle Stunden,
|
||||
die ein WebUntis-Termin überdeckt (via neuer `UntisMatchingService.ResolveCoveredPeriods`:
|
||||
alle konfigurierten Stundenraster-Einträge, die vollständig innerhalb [Start, Ende) des Termins
|
||||
liegen). `PeriodNumber` bleibt als erste/primäre Stunde erhalten (u.a. für
|
||||
`UntisDiffService`, das weiterhin nur die erste Stunde einer Doppelstunde in einen
|
||||
`SubstitutionEntry` schreibt — bewusste Vereinfachung, siehe unten).
|
||||
- `UntisMatchingService.BuildMatches`/`TimetableViewModel.LoadUntisMismatch`: der
|
||||
"zugeordnet"-Abgleich prüft jetzt gegen ALLE `CoveredPeriods` einer bestätigten Zuordnung, nicht
|
||||
nur gegen die erste Stunde.
|
||||
- `UntisMappingReviewDialog` zeigt Doppelstunden-Zeilen als "1.–2. Stunde (07:50, Doppelstunde)"
|
||||
statt nur der ersten Stunde, damit sichtbar ist, dass eine Bestätigung beide Stunden abdeckt.
|
||||
- Bewusst nicht angegangen: `UntisDiffService` schreibt bei einer geänderten/ausgefallenen
|
||||
Doppelstunde weiterhin nur einen `SubstitutionEntry` für die erste Stunde (ein Eintrag kann nur
|
||||
eine `PeriodNumber` tragen) — für den gemeldeten Fall (Zuordnungs-Zählung im Stundenplan-Banner)
|
||||
nicht relevant, bleibt als bekannte Einschränkung dokumentiert statt den Umfang zu sprengen.
|
||||
|
||||
**Nachtrag zu 4.3, zwölfte Iteration (verifiziert: einmalige Vertretungsaufsicht meldet sich nur
|
||||
einmal ab):** Nutzer-Sorge, nachdem eine einmalige Vertretungsaufsicht für eine einzelne Woche im
|
||||
Review-Dialog als Aufsicht bestätigt wurde: *"Die fehlt natürlich in der Woche drauf wieder. Wird
|
||||
dann bis zum Ende des gültigen Stundenplans diese Stunde als entfallene Aufsicht geführt?"* — Kein
|
||||
Codefehler, aber ein berechtigter Verdacht angesichts einer dauerhaft bestätigten
|
||||
`UntisSlotMapping`; per neuem Regressionstest
|
||||
(`UntisSyncServiceTests.ProcessIcsText_EinmaligeVertretungsaufsicht_MeldetEntfallenNurEinmal`)
|
||||
verifiziert, dass die "entfallen"-Erkennung in `UntisDiffService` an die konkrete, tatsächlich im
|
||||
Snapshot gesehene Zeile EINES Datums gekoppelt ist, nicht an eine dauerhaft erwartete
|
||||
wöchentliche Wiederholung der Zuordnung: sobald eine verschwundene Zeile einmal gemeldet wurde,
|
||||
wird ihre Snapshot-Zeile gelöscht (`SnapshotIdsToDelete`) — für künftige Wochen entsteht ohne
|
||||
einen neuen, tatsächlich von WebUntis gemeldeten Termin an diesem Slot gar keine neue
|
||||
Snapshot-Zeile mehr, die erneut "verschwinden" könnte. Einzige bekannte Randnotiz: die bestätigte
|
||||
`UntisSlotMapping` selbst bleibt als (harmloser) verwaister Datensatz in der Datenbank stehen, da
|
||||
ihr Muster nach dem einmaligen Vorkommen bei einem erneuten Abruf nicht mehr auftaucht und der
|
||||
Review-Dialog dafür deshalb auch keine Zeile zum Entfernen mehr anbietet — bislang nicht als
|
||||
eigenständiges Problem gemeldet, deshalb kein eigener Aufräum-Mechanismus gebaut.
|
||||
|
||||
**Nachtrag zu 4.3, dreizehnte Iteration (Sichtbarkeit im Wochenraster + zusätzliche Aufsichten +
|
||||
Fach in der Gruppenauswahl):** Nutzer-Feedback: *"Wäre es doch auch schön, wenn das im
|
||||
Stundenplan für die nächste Woche irgendwie erkenntlich ist [...] Auch die Extra-Aufsicht ist
|
||||
dann nicht im Plan. So macht doch der Sync nur so halb Sinn."*
|
||||
- **Sichtbarkeit verifiziert, kein Code nötig:** Das Wochenraster (`TimetableViewModel.
|
||||
BuildWeekOverview`) liest `SubstitutionEntry` bereits für die jeweils angezeigte Woche
|
||||
(`WeekOffset`), unabhängig davon, ob der Eintrag von Hand oder automatisch über den
|
||||
WebUntis-Abgleich entstanden ist — beides landet in derselben Tabelle. Ein Ausfall ersetzt die
|
||||
reguläre Kachel vollständig (Aufschrift "Ausfall" + `Description` direkt sichtbar als Text,
|
||||
keine Extra-Hover-Lösung nötig, siehe `WeekCellItem.ForCancelled`/`TimetableView.axaml`).
|
||||
Per neuem Test (`Load_AusfallInDerFolgewoche_ErscheintImWochenrasterNachNavigation`,
|
||||
`WeekOffset` über `NextWeekCommand` statt direkter Zuweisung, da nur die Befehle `Load()` erneut
|
||||
auslösen) erstmals mit `WeekOffset != 0` bestätigt — vorher gab es dafür keinen Test.
|
||||
- **Echte Lücke gefunden und behoben:** `UntisDiffService` erzeugte für eine bestätigte
|
||||
Aufsichts-Zuordnung bisher nur bei `STATUS:CANCELLED` oder Verschwinden einen Eintrag — das
|
||||
reguläre Vorkommen selbst (die Vertretungsaufsicht in der Woche, in der sie tatsächlich
|
||||
stattfindet) blieb unsichtbar. Neue Logik: `UntisDiffService.Diff` bekommt optional
|
||||
`existingSupervisionDuties` (`ISupervisionDutyRepository`, über `UntisSyncService`
|
||||
durchgereicht) — eine bestätigte Supervision-Zuordnung OHNE passende reguläre `SupervisionDuty`
|
||||
an Wochentag+Pause gilt selbst schon als meldenswerte (zusätzliche) Vertretung und erzeugt bei
|
||||
jedem tatsächlichen Vorkommen sofort einen `SubstitutionEntry` (idempotent über die iCal-UID
|
||||
als `ExternalId`). Mit passender regulärer Duty bleibt es wie zuvor bei reiner
|
||||
Abweichungs-Erkennung (Cancelled/verschwunden).
|
||||
- **Fach in der Kursauswahl:** Nutzer-Feedback: *"Meine Klasse habe ich 3-mal. Ohne das Fach
|
||||
dabei, kann ich nicht sicher die richtige Lerngruppe hier auswählen."* Gleiche Begründung wie
|
||||
bei `TimetableSlotDialogViewModel` ("10c (Chemie)" bei Namenskonflikt) — hier über eine neue
|
||||
kleine Anzeige-Hülle `UntisGroupOption { LearningGroup Group; string DisplayLabel; }` gelöst
|
||||
(bewusst immer mit Fach statt nur bei erkanntem Konflikt, da dieser Dialog direkt an
|
||||
`LearningGroup`-Objekte statt an Label-Strings bindet — einfacher als eine
|
||||
Konfliktbevorzugungs-Logik nachzubauen). `UntisMappingReviewDialogViewModel` bekommt dafür
|
||||
`ISubjectRepository` injiziert.
|
||||
|
||||
**Nachtrag zu 4.3, vierzehnte Iteration (Bugfix: von Anfang an fehlende Stunden):** Nutzer-
|
||||
Feedback: *"Am 27.08. fällt eine Stunde NAT in der 8c aus. Dieser Ausfall steht nicht im Plan
|
||||
[...] die Klasse ist dort weg, also der Unterricht wird definitiv nicht stattfinden."* Ursache:
|
||||
das gesamte Diffing war bis dahin rein **reaktiv** — es erkannte nur Termine, die zwischen zwei
|
||||
Abrufen aus dem Feed **verschwanden** (vorher gesehen, jetzt weg). Ein Ausfall, den WebUntis von
|
||||
Anfang an nie als Termin gelistet hatte (weil er schon beim allerersten Abruf der App feststand),
|
||||
hinterließ nie einen Schnappschuss-Eintrag, der hätte "verschwinden" können — für das
|
||||
Schnappschuss-Diffing sah es aus, als hätte es diese Stunde nie gegeben, es gab also nichts zu
|
||||
erkennen.
|
||||
- `UntisDiffService.Diff` bekommt eine zweite, **aktive** Prüfung für bestätigte
|
||||
`Lesson`-Zuordnungen: für jedes vom Feed bereits abgedeckte künftige Datum des Zuordnungs-
|
||||
Wochentags (bis zum jüngsten im aktuellen Fetch gesehenen Datum, `newEvents.Max(Date)` —
|
||||
begrenzt auf das, was WebUntis tatsächlich schon veröffentlicht hat, damit nicht Tage jenseits
|
||||
des Feed-Horizonts fälschlich als Ausfall gelten) wird geprüft, ob ein passender Termin
|
||||
existiert; fehlt er, wird unabhängig vom Schnappschuss-Verlauf ein `SubstitutionEntry` erzeugt
|
||||
(idempotent über einen aus Zuordnung+Datum abgeleiteten Schlüssel statt einer iCal-UID, die es
|
||||
für eine fehlende Stunde naturgemäß nie gab).
|
||||
- Die bisherige rein reaktive Schnappschuss-Erkennung für `Lesson`-Zuordnungen entfällt dadurch
|
||||
(sie ist jetzt ein Sonderfall der aktiven Prüfung) — für Aufsichten (`Supervision`) bleibt sie
|
||||
unverändert bestehen, da dort kein fester wöchentlicher Anspruch existiert (siehe Nachtrag zur
|
||||
dreizehnten Iteration).
|
||||
- Neuer Parameter `freeDates` (Ferien/Feiertage) verhindert, dass die aktive Prüfung während
|
||||
Schulferien fälschlich Ausfälle meldet — `UntisSyncService` berechnet ihn aus
|
||||
`ISchoolHolidayRepository`/`PublicHolidayService`/`SchoolCalendarSettingsService`, dieselbe
|
||||
Logik wie `TimetableViewModel.IsFreeDay`, hier bewusst separat gehalten statt geteilt, da
|
||||
`UntisDiffService` (LehrerApp.Core) absichtlich frei von Desktop-ViewModel-Abhängigkeiten
|
||||
bleibt.
|
||||
- Der am 26.08. vom Nutzer vermutete Sonderfall (eigener Unterrichtsausfall, möglicherweise durch
|
||||
einen manuell eingetragenen Sondereinsatz überschrieben) wurde nicht weiter untersucht — vom
|
||||
Nutzer selbst als plausible, nicht fehlerhafte Erklärung eingeordnet.
|
||||
|
||||
**Nachtrag zu 4.3, fünfzehnte Iteration (Bugfix: manuelle Zuordnung bei kombinierten Kursen
|
||||
wirkungslos):** Nutzer-Feedback: *"Der Mathematik E-Kurs Dienstag 3.&4. Stunde ist ein Kurs aus
|
||||
den Klassen 10a, 10b und 10c. Die Logik möchte ihn immer meiner Klasse 10c alleine geben. Ich
|
||||
habe das aufgelöst und den Mathematik E-Kurs ausgewählt. Diese manuelle Verknüpfung ist aber
|
||||
jetzt scheinbar vergessen und es taucht jedes Mal die Vertretung auf."* Ursache: beim Bestätigen
|
||||
wird `UntisSlotMapping.ClassToken` kompakt ohne Leerzeichen gespeichert (`"10a;10b;10c"`),
|
||||
WebUntis trennt die Klassen in `DESCRIPTION` aber mit `"; "` (Semikolon + Leerzeichen, z.B.
|
||||
`"10a; 10b; 10c; Gastro HED"`, siehe echtes Beispiel im Planungsdokument). Der Abweichungs-
|
||||
Vergleich in `UntisDiffService.HasDeviated` war ein reiner Teilstring-Vergleich
|
||||
(`evt.Description.Contains(mapping.ClassToken)`) — der schlug dadurch für **jede** kombinierte/
|
||||
differenzierte Gruppe (mehr als ein Klassen-Token) strukturell fehl, unabhängig davon, ob die
|
||||
manuell gewählte Gruppe stimmte: jede einzelne Bestätigung eines Kurses mit mehreren Klassen
|
||||
wurde bei jedem Poll erneut als "abweichend" gemeldet.
|
||||
- Fix: `HasDeviated` entfernt vor dem Teilstring-Vergleich alle Leerzeichen aus der rohen
|
||||
`DESCRIPTION` (gleiches Prinzip wie `UntisMatchingService.Normalize` beim
|
||||
Gruppennamen-Abgleich) — formatunabhängig, verlässt sich nicht auf eine bestimmte
|
||||
WebUntis-Trennzeichen-Konvention.
|
||||
- Betraf ausschließlich kombinierte Gruppen (mehr als ein Klassen-Token) — einzelne Klassen
|
||||
(`"10c"` in `"10c HED"`) waren nie betroffen, da dort kein Trennzeichen im Spiel ist; deshalb
|
||||
ist der Fehler dem Nutzer erst bei diesem speziellen Kurs aufgefallen.
|
||||
|
||||
### 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