feat: Anhänge je Stunde, Klausur-Sitzplan mischen, Gefährdungsbeurteilungs-Assistent

Lesson bekommt dieselbe Anhang-Infrastruktur wie Documentation (Material,
Arbeitsblätter, Experimentunterlagen), samt Fix einer Sync-Lücke, die Anhang-
Dateibytes bisher nur für Documentation statt generisch übertragen hat
(IHasAttachments). Sitzplan-Tab bekommt einen "Plätze mischen"-Button für
Klausursitzpläne. Neu: mehrschrittiger Gefährdungsbeurteilungs-Assistent mit
optionalem KI-Entwurf (ai-backend/gbu.php) und PDF-Export, Format bewusst als
JSON-Anhang statt eigener Datenbank-Entität. Details und Architekturentscheidungen
in TODO.md (4.2, 7.1.5, 10.1.8).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 17:13:14 +02:00
co-authored by Claude Sonnet 5
parent 331033db5c
commit 038337997f
29 changed files with 1886 additions and 22 deletions
+108
View File
@@ -518,6 +518,83 @@ leer ist) und rollt von dort vorwärts auf den nächsten passenden Wochentag. Fe
Stundenplan-Eintrag für die Gruppe, bleibt "heute" als Fallback erhalten (keine Verhaltens-
änderung für Gruppen ohne Stundenplan).
**Nachtrag — Anhänge je Stunde (Nutzerwunsch, fachspezifisch):** Chemieunterricht plant mit
Experimenten, zu denen eine schriftliche Gefährdungsbeurteilung gehört; außerdem gab es bisher
gar keine Möglichkeit, ein Arbeitsblatt/Material als Datei an eine Stunde zu hängen (die
`Material`-Spalte im Verlaufsplan ist bewusst nur Freitext, siehe 4.2.2). Statt zweier separater
Felder ein einziges, generisches `Lesson.Attachments`
(`List<DocumentAttachment>`) — dieselbe Anhang-Infrastruktur, die `Documentation` (5.1) bereits
nutzt (`IAttachmentStorage`, `LiteAttachmentStorage`), deckt beides ab: Gefährdungsbeurteilungen
genauso wie normale Arbeitsblätter/Scans. Im Stunden-Dialog
([LessonDialog.axaml](LehrerApp.Desktop/Views/Groups/LessonDialog.axaml)) neuer Abschnitt
"Anhänge" nach demselben Muster wie im Dokumentationsdialog (Button " Datei" über den
Avalonia-Dateiauswahldialog, Liste mit Datei/Größe/"Speichern"/"×"). `LessonRepository.Delete`
räumt beim (harten) Löschen einer Stunde die zugehörigen Anhänge aus der Attachment-Ablage auf,
analog zu `LiteDbContext.CascadeHardDeleteDocumentation`. Neue Tests: `LessonDialogViewModelTests`
(Hochladen inkl. Größenlimit, Entfernen, Vorbelegung beim Bearbeiten, Übernahme ins
Speicherergebnis), `RepositoryTests.LessonRepository_DeleteRaeumtAnhaengeAusDerAttachmentAblageAuf`.
Eine KI-gestützte Vorformulierung der Gefährdungsbeurteilung (ebenfalls Nutzerwunsch) ist bewusst
zurückgestellt — dafür bräuchte es einen neuen `ai-backend`-Endpunkt mit eigenem Systemprompt
(analog zu `explain.php`, 4.5.21) und eine sorgfältig formulierte, gut sichtbare
Rechtssicherheits-Einschränkung im Dialog; als eigener, separat zu planender Schritt vorgesehen.
**Nachtrag — Gefährdungsbeurteilungs-Assistent (Umsetzung des oben zurückgestellten Schritts):**
Vor der Umsetzung stand eine Architekturfrage: eigene LiteDB-Entität mit Repository/Sync-Wiring,
oder etwas Leichtgewichtigeres? Entschieden für Letzteres — die Prüfung, ob Anhang-Dateibytes
überhaupt zwischen Geräten synchronisieren (siehe Nachtrag zu 10.1.8, dort im selben Zug behoben),
ergab, dass die eigentliche Lücke eine Ebene tiefer lag, nicht am Fehlen einer eigenen Entität.
**Format:** [`HazardAssessment`](LehrerApp.Core/Models/HazardAssessment.cs) ist ein reines,
JSON-serialisierbares Modell (Titel, Klassenstufe/Kurs, Datum, Art des Versuchs
[Lehrer-/Schüler-/Demonstrationsversuch], Durchführung, Gefahrstoffe mit GHS-Piktogrammen/H-/
P-Sätzen/Menge, Gefährdungen, Schutzmaßnahmen, Erste Hilfe, Entsorgung) — **keine** eigene
LiteDB-Collection/Repository. Wird als JSON serialisiert und über die bestehende
Anhang-Infrastruktur an die `Lesson` gehängt, Dateiname endet auf `.gbu.json`
(`AttachmentItem.IsHazardAssessment`/`HazardAssessmentSuffix`). Bewusst **kein** eingebauter
Katalog amtlicher H-/P-Satz-Texte im Code — Fehlerrisiko bei sicherheitsrelevanten Angaben, die
Lehrkraft trägt die Prüfung gegen das Sicherheitsdatenblatt.
**Wizard:** neuer mehrschrittiger Assistent
([HazardAssessmentWizardDialog.axaml](LehrerApp.Desktop/Views/Groups/HazardAssessmentWizardDialog.axaml),
`HazardAssessmentWizardViewModel`) mit 5 Schritten (Basisdaten, Gefahrstoffe, Gefährdungen &
Schutzmaßnahmen, Erste Hilfe & Entsorgung, Zusammenfassung). Persistiert nicht selbst — liefert
nur `Result`, der Aufrufer (`LessonDialog`) serialisiert zu JSON und hängt es über die bereits
vorhandenen `AddAttachment`/`RemoveAttachmentCommand` an. Im Stunden-Dialog neuer Button
"🧪 Gefährdungsbeurteilung" neben " Datei"; `.gbu.json`-Anhänge zeigen in der Anhangliste
"Öffnen" (Wizard im Bearbeitungsmodus, ersetzt den Anhang beim Speichern) und "PDF" statt des
generischen "Speichern".
**KI-Entwurf:** neuer Endpunkt `ai-backend/gbu.php` (gleicher Aufbau wie `explain.php`: Auth,
Guthabenprüfung, fester Systemprompt, `ai_backend_call_and_charge`) erkennt aus Thema und
Verlaufsplan der Stunde das Experiment und liefert einen strukturierten Entwurf. Systemprompt
weist die KI ausdrücklich an, unsichere H-/P-Sätze/Mengenangaben **nicht zu erfinden**, sondern
mit einem Prüfhinweis zu kennzeichnen, und bei der Einstufung Lehrer-/Schülerversuch im Zweifel
vorsichtig (Lehrerversuch) zu sein. `AiPlanningService.RequestHazardAssessmentDraftAsync` (neue
DTOs `AiHazardAssessmentRequest`/`AiHazardAssessmentResponse` in `AiPlanningDtos.cs`) baut den
Kontext lokal aus dem gerade im Dialog bearbeiteten (auch noch nicht gespeicherten) Stand, nicht
erst nach dem Speichern. Button "🤖 KI-Entwurf erstellen" im Wizard, direkt daneben ein **fest
sichtbarer** Rechtssicherheits-Hinweis (nicht erst nach Fehlern eingeblendet): "Kein
rechtssicheres Dokument — bitte jede Angabe eigenverantwortlich prüfen." `HazardAssessment.
IsAiAssisted` wird gesetzt und erscheint als deutlich hervorgehobener Warnhinweis im PDF-Export,
sobald KI-Anteile beteiligt waren.
Deployment-Hinweis: `ai-backend/` hat **kein** Auto-Deploy (anders als `LehrerApp.Api`, siehe
[docker/README.md](docker/README.md)) — nach diesem Änderungsdurchgang muss `gbu.php` manuell auf
den PHP-Server hochgeladen werden (siehe [ai-backend/README.md](ai-backend/README.md), Abschnitt
"Update für bereits deployte Installationen").
**PDF-Export:** `PdfExportService.BuildHazardAssessmentPdf` (neues `HazardAssessmentPrintData`)
druckt Durchführung, Gefahrstofftabelle, Gefährdungen/Schutzmaßnahmen als Liste, Erste
Hilfe/Entsorgung/Hinweise — bei `IsAiAssisted` zusätzlich ein auffälliges Warnbanner oben im
Dokument.
Neue Tests: [HazardAssessmentWizardViewModelTests.cs](LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs)
(Navigation, Validierung, Gefahrstoff-/Gefährdungs-/Schutzmaßnahmen-Verwaltung, Vorbelegung beim
Bearbeiten, `ExperimentKindDisplay`/`GhsPictogramDisplay`), zwei neue Fälle in
`PdfExportServiceTests.cs`. Die HTTP-Anfrage von `RequestHazardAssessmentDraftAsync` selbst ist
wie bei `RequestExplanationAsync`/`RequestPlanAsync` nicht Teil der automatisierten Tests (braucht
einen echten Endpunkt, siehe Kommentar in `AiPlanningServiceTests.cs`).
### 4.3 Stundenplan
- [x] **4.3.1** Neues Modell `TimetableSlot` (Gruppe, Wochentag, Stunde, Raum) + Repository —
[Planning.cs](LehrerApp.Core/Models/Planning.cs),
@@ -1388,6 +1465,17 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
(Laden, Umschalten inkl. Persistieren, Sperre für belegte Plätze, verhinderte Zuweisung auf
ausgeblendete Plätze, `ShowSeat`/`CanToggleHidden` je nach Modus).
**Nachtrag (Nutzerwunsch): Klausur-Sitzplan mischen.** Neuer Button "🎲 Plätze für Klausur
mischen" neben Bearbeiten/Löschen — erzeugt aus dem aktuell gewählten Plan einen neuen,
unabhängigen Sitzplan mit gleichem Raster/Raum, aber zufällig vertauschten Insassen der
belegten Plätze (Fisher-Yates über `Random.Shared.Shuffle`, Platzkoordinaten bleiben
unverändert, nur wer wo sitzt wird neu gewürfelt). Der Ursprungsplan bleibt unangetastet
erhalten, damit er für den nächsten regulären Unterricht weiter nutzbar ist; der neu
angelegte Plan ist wie jeder andere `SeatingPlan`-Datensatz sofort über den bestehenden
Sitzplan-PDF-Export (11.4) druckbar, ohne weitere Anpassung. Test:
`SeatingPlanViewModelTests.ShuffleSeats_ErzeugtNeuenPlanMitVertauschtenPlaetzenUndBehaeltDenUrsprungsplan`
plus ein `CanExecute`-Test für die Bearbeitungsmodus-Sperre.
### 7.2 Gruppen
- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/
`ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit,
@@ -1764,6 +1852,26 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba
Anhänge nach Anwenden eines `Documentation`-Ereignisses). Original-`StorageId` bleibt beim
Download erhalten (roher `db.Attachments.Upload`-Aufruf statt `IAttachmentStorage.Upload`,
das immer eine neue Id vergäbe).
**Nachtrag — Verallgemeinerung auf `IHasAttachments` (Bug, gefunden bei 4.2-Nachtrag
"Anhänge je Stunde"):** `SyncEventPublisher.Publish` und `EventApplier` (Gate +
Deserialisierung in `DownloadMissingAttachmentsAsync`) waren hart auf den Modelltyp
`Documentation` verdrahtet (`if (payload is Documentation doc)` bzw.
`evt.EntityType == nameof(Documentation)`). Als `Lesson` ein eigenes `Attachments`-Feld
bekam, hätte das bedeutet: die Metadaten synchronisieren (Lesson selbst synchronisiert
ohnehin vollständig), aber die eigentlichen Datei-Bytes nie — ein an einem Gerät
angehängtes Arbeitsblatt/eine Gefährdungsbeurteilung wäre auf dem zweiten Gerät nur ein
Verweis auf eine nicht existierende `StorageId` gewesen. Neues Marker-Interface
`IHasAttachments` ([Workload.cs](LehrerApp.Core/Models/Workload.cs), Property
`List<DocumentAttachment> Attachments`), implementiert von `Documentation` und `Lesson`.
`SyncEventPublisher.Publish` prüft jetzt `payload is IHasAttachments` statt des konkreten
Typs; `EventApplier` bekommt eine kleine, pro Entitätstyp erweiterbare
`AttachmentDeserializers`-Tabelle (analog zur bestehenden `Handlers`-Tabelle) statt der
festen `Documentation`-Deserialisierung. `AttachmentSyncer` (Upload-Seite) war bereits
generisch (kennt nur `StorageId`s, keinen Entitätstyp) und musste nicht geändert werden.
Neue Tests: `EventApplierTests.ApplyAsync_LessonMitFehlendemAnhang_LaedtIhnUeberHttpNach`,
neue Datei [SyncEventPublisherTests.cs](LehrerApp.Sync.Tests/SyncEventPublisherTests.cs)
(Documentation, Lesson, Entität ohne Anhänge).
- [x] **10.1.9** Schärfere Kollisionskontrolle beim Push (Konzeptgespräch nach dem
Pull-Watermark-Bugfix, siehe Nachtrag zu 10.1.7): statt der bisherigen 30-Sekunden-
Heuristik ("hat ein anderes Gerät kürzlich dieselbe Entität angefasst") trägt jedes