feat: T6 wave 5 – readiness checks and report, compliance and medicine UI, label UI
- Readiness: all 18 checks (medicine lots, treatment log gaps, labels, harvest withdrawal), medicine subjects masked without veterinary-medicines:read, report PDF (counts only), summary cached 60 s, 409 codes READINESS_*. - Frontend: /compliance page with acknowledge/revoke, dashboard line and nav; veterinary medicine catalogue, lots, movement book with inventory and disposal; label designer check panel, print card with gap list and proof print, food business master data, product legal name. - Service worker: cacheability is path-based (the backend sends no-store on every answer); compliance, medicine, restriction, notice and label check/print paths and restricted document kinds are never cached. - ARCHITECTURE.md §2, §12.10–12.11, Annahmen 86–90. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b912d29166
commit
b4746a2b17
149 files changed
+11869
-320
No files matched your search
+65
-7
@@ -76,7 +76,7 @@ Zentrale Schnellaktion "+ Neu" (Abschnitt 22): permanent sichtbarer Floating-But
|
||||
|
||||
**Offline-Seiten:** Ohne Netz zeigen `/offline` (Start: geladene Stände), `/offline/apiary?id=…` und `/offline/colony?id=…` die Daten aus den Offline-Paketen (IndexedDB). QR- und NFC-Deep-Links (`/apiaries/<id>`, `/colonies/<id>`) weicht der Service Worker ohne Netz auf diese Seiten aus; der Weg zurück („Mit Netz öffnen“) übernimmt `from` nur bei gleicher Herkunft (same-origin). Formulare lesen Völker und Königinnen aus dem Paket, wenn die API nicht antwortet (`withPackFallback`, `networkMode: "always"`).
|
||||
|
||||
**API-Client und Service Worker.** `apiFetch` und `apiFetchFormData` nehmen die Option `refreshOn401: false`: Ein 401 wird dann weder erneuert noch wiederholt, die deutsche Meldung des Servers kommt unverändert an. Alle Nachweis-Aufrufe (Step-up, Abschnitt 11.13) setzen sie (Annahme 56). Der Service Worker legt `/api/v1/profile/telegram`, `/api/v1/profile/notification-settings` und `/api/v1/system` nie im Cache ab (`NEVER_CACHED_API_PATHS` in `public/sw-routes.js`; eine Änderung hebt `SW_ROUTES_VERSION` in `public/sw.js`).
|
||||
**API-Client und Service Worker.** `apiFetch` und `apiFetchFormData` nehmen die Option `refreshOn401: false`: Ein 401 wird dann weder erneuert noch wiederholt, die deutsche Meldung des Servers kommt unverändert an. Alle Nachweis-Aufrufe (Step-up, Abschnitt 11.13) setzen sie (Annahme 56). Der Service Worker legt `/api/v1/profile/telegram`, `/api/v1/profile/notification-settings` und `/api/v1/system` nie im Cache ab (`NEVER_CACHED_API_PATHS` in `public/sw-routes.js`; eine Änderung hebt `SW_ROUTES_VERSION` in `public/sw.js`, aktuell der Hash `0681ff4b095d`; ein Spec prüft, dass die Version zum Inhalt von `sw-routes.js` passt, jede Änderung an der Datei braucht also eine neue Version). Seit T6 kommen dazu: `/api/v1/compliance` (Prüfbereitschaft, Anzeigen-Einstellungen), `authority-notices`, `apiary-restrictions`, `apiaries/:id/restrictions`, `veterinary-medicines`, `veterinary-medicine-lots`, die Etikett-Prüfung und der Druck (`label-templates/check`, `label-templates/:id/check…`, `label-templates/:id/print…`; sie tragen die Anschrift des Lebensmittelunternehmers), das Einzeldokument `documents/:id` (die Art steht nicht im Pfad) und die Dokumentlisten der drei eingeschränkten Arten `MEDICINE_LOT`, `AUTHORITY_NOTICE` und `APIARY_RESTRICTION` (die Art steht in der Query `/documents?entityType=…`, daher prüft `isCacheableApiUrl` die URL samt Query; Listen der Durchsicht-Fotos und `documents/:id/download` bleiben offline verfügbar, das Präfix `/api/v1/documents` wird nicht als Ganzes gesperrt). **Der globale Header `Cache-Control: no-store` des Backends ist bewusst kein Signal für den Worker:** Jede API-Antwort trägt ihn (Abschnitt 9, `noStoreMiddleware`), ein Header-Check würde das gesamte Offline-Verhalten abschalten. `isKeepableApiAnswer` prüft nur Erfolg und JSON; ob etwas behalten wird, entscheidet allein der Pfad (Annahme 87).
|
||||
|
||||
**Stand-Seite und Standrunde:** `apiaries/[id]` zeigt die lebenden Völker mit Ampel und „Runde starten“; nach dem Speichern führt „Weiter mit …“ zum nächsten Volk. Der Rundenzustand liegt je Benutzer lokal und wird beim Abmelden gelöscht.
|
||||
|
||||
@@ -206,6 +206,11 @@ Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige
|
||||
83. **Der Ort im Briefkopf kommt aus der Postleitzahl-Zeile**: Aus „…, 34117 Kassel“ wird „Kassel“; ohne Treffer steht nur das Datum, der Ort bleibt leer (kein `FEHLT`). Kosten bei falscher Annahme: Der Ort fehlt neben dem Datum. Abschnitt 12.8.
|
||||
84. **Die Summe im Anzeigen-PDF ist das gespeicherte `totalColonies`**: Weicht es von der Summe der Stände ab, steht „Gesamtzahl berichtigt (Summe der Stände: n)“; es wird nicht live neu gezählt. Kosten bei falscher Annahme: Ein veraltetes `totalColonies` erscheint im Ausdruck, bis `recount` im Entwurf läuft. Abschnitt 12.8.
|
||||
85. **Keine der ersten sieben Prüfregeln ist quittierbar**: Weder Pflicht- noch Prüfen-Punkte dieser Stufe lassen sich wegquittieren (Spec 5.7); die Quittung ist Mechanik für spätere weiche Regeln (Task 13). Kosten bei falscher Annahme: Der Inhaber muss die Ursache beheben statt zu quittieren. Abschnitt 12.10.
|
||||
86. **Die Kurzfassung der Prüfbereitschaft kann bis zu 60 s veraltet sein**: `GET /compliance/readiness/summary` (Dashboard-Zeile) wird je Prozess 60 s aus dem Speicher bedient und nach Quittieren oder Zurücknehmen im selben Prozess sofort verworfen. Änderungen an den Daten und Quittungen eines anderen Prozesses erscheinen dort bis zu 60 s später; Liste, PDF-Bericht und Quittierpfad rechnen immer live. Kosten bei falscher Annahme: Die Dashboard-Zeile zeigt kurz einen alten Stand. Abschnitt 12.10.
|
||||
87. **Der Service Worker entscheidet über das Zwischenspeichern nach dem Pfad, nicht nach `Cache-Control`**: Das Backend sendet `Cache-Control: no-store` an jede API-Antwort (Abschnitt 9); würde der Worker den Header als Signal lesen, verwürfe er das gesamte Offline-Verhalten. Nie gespeichert wird, was `NEVER_CACHED_API_PATHS` und die Dokumentarten-Regel benennen; ein neuer schutzwürdiger Pfad muss dort eingetragen werden und hebt `SW_ROUTES_VERSION`. Kosten bei falscher Annahme: Ein vergessener Pfad kann offline gespeichert werden, obwohl der Server `no-store` sendet. Abschnitt 2 und 12.3.
|
||||
88. **„1.000“ wird als mehrdeutig abgelehnt**: Die Mengenfelder der Tierarzneimittel-Oberfläche lesen das Komma als Dezimalzeichen und lehnen einen einzelnen Punkt mit genau drei Nachkommastellen („1.000“, „12.345“) sowie jeden Text mit mehreren Trennzeichen ab, weil er Tausender oder Dezimalstelle meinen könnte. Kosten bei falscher Annahme: Wer „1.000“ als eins meint, muss „1“ oder „1,0“ schreiben. Abschnitt 12.11.
|
||||
89. **Eine neue, ungespeicherte Etikettvorlage prüft nichts, bis sie einen Wert hat**: Der Check-Endpunkt verlangt die Mindest-x-Höhe; das Frontend kennt keinen eigenen Vorgabewert (kein Rechtswert im Code). Eine neue Vorlage startet mit leerem Feld, sendet beim Anlegen keinen Wert (der Server setzt seinen Standard) und zeigt das Panel erst nach Eingabe oder nach dem ersten Speichern. Kosten bei falscher Annahme: Im Designer fehlt bei einer ganz neuen Vorlage die Prüfung, bis der Wert gesetzt ist. Abschnitt 12.11.
|
||||
90. **Das Prüfbericht-PDF druckt Zählungen, keine Namen**: Es nennt je Punkt nur die Zahl der betroffenen Objekte, nie Stände, Völker, Mittel, Packungen oder Aktenzeichen, damit der Bericht ohne Rechteprüfung der Namen weitergegeben werden kann. Kosten bei falscher Annahme: Wer die Objekte braucht, öffnet die Seite `/compliance` mit den Rechten. Abschnitt 12.10.
|
||||
|
||||
## 7. Kubernetes
|
||||
|
||||
@@ -731,7 +736,7 @@ Logs nennen nur Zustellungs-IDs, nie Token, Chat-ID oder Text.
|
||||
|
||||
Prüfbereitschaft für Tierarzneimittel, Etiketten und Behörden. Rechtswerte (Fristen, Paragraphen, Ziffernhöhen, x-Höhe, Landesrecht Bayern) stehen nie im Code, sondern sind Einstellungen mit dem Vermerk „Annahme – vom Inhaber zu prüfen“; sie sind vor dem Rollout gegen gesetze-im-internet.de und EUR-Lex abzugleichen. Belege, Rechnungsnummern, Anschriften, Koordinaten und Aktenzeichen sehen nur Inhaber von `compliance:read` bzw. `veterinary-medicines:read`; sie erscheinen nie in öffentlichen Endpunkten, im Shop-Feed, in Webhooks oder in Ereignis-Payloads. Entscheidungen des Inhabers vom 2026-10-15: eigene Meldungs-Kategorie `COMPLIANCE`, Stand vor Volk beim Sperren, kein Step-up für die Ausnahme vom Sperrbezirk.
|
||||
|
||||
**Stand:** Umgesetzt sind Task 1 (Schema, Rechte, Sperren, Backup), 2 (Tierarzneimittel), 3 (Behandlung aus Packung), 4 (Dokumente), 5 (Etikett-Pflichtangaben), 6 (Druck-Sperre und Probedruck), 7 (Ereignisse), 8 (Sperrbezirk und Sperr-Umbau der Völker) und 9 (Behördenpaket I), 10 (Anzeigen-PDFs), 11 (Hinweise und Tagesauswertung) und 12 (Prüfbereitschaft I). Task 13 bis 19 (weitere Prüfregeln, Oberfläche) folgen und ergänzen diesen Abschnitt.
|
||||
**Stand:** Umgesetzt sind Task 1 (Schema, Rechte, Sperren, Backup), 2 (Tierarzneimittel), 3 (Behandlung aus Packung), 4 (Dokumente), 5 (Etikett-Pflichtangaben), 6 (Druck-Sperre und Probedruck), 7 (Ereignisse), 8 (Sperrbezirk und Sperr-Umbau der Völker) und 9 (Behördenpaket I), 10 (Anzeigen-PDFs), 11 (Hinweise und Tagesauswertung) und 12 (Prüfbereitschaft I), 13 (Prüfbereitschaft II), 14 (Oberfläche Prüfbereitschaft), 15 (Oberfläche Tierarzneimittel) und 17 (Oberfläche Etikett). Task 1 bis 15 und 17 sind erledigt; Task 16, 18 und 19 (Behandlungsmaske, Anzeigen- und Sperrbezirk-Oberfläche, Abschluss) folgen und ergänzen diesen Abschnitt.
|
||||
|
||||
### 12.1 Schema und Rechte
|
||||
|
||||
@@ -777,7 +782,7 @@ Eine Behandlung aus einer Packung bucht mit `treatments:create` ab, es gibt kein
|
||||
|
||||
Für `MEDICINE_LOT`, `AUTHORITY_NOTICE` und `APIARY_RESTRICTION` gilt zusätzlich zum Dokumentenrecht das Fachrecht des Datensatzes. Die Zuordnung steht in `DOCUMENT_ENTITY_PERMISSIONS` (`documents.service.ts`): Packung lesen `veterinary-medicines:read`, schreiben `veterinary-medicines:update`; Behördenanzeige und Sperrbezirk lesen `compliance:read`, schreiben `compliance:manage`. „Schreiben“ umfasst Hochladen, Umhängen, Umbenennen und Löschen; beim Umhängen zählen die alte und die neue Art. `DocumentsService.assertEntityAccess(userId, entityType, mode)` wirft 403 ohne das Recht; Arten ohne Eintrag (Stand, Volk, Königin, Durchsicht, Honigcharge) prüfen nichts zusätzlich. Die Prüfung kommt vor jeder Datenbankabfrage (`findAll`, `create`) bzw. nach dem Laden des Dokuments und vor dem Lesen der Datei (`readFile`, `update`, `delete`), daher 403 vor 404 und kein Existenz-Orakel für Unberechtigte. Wer nur `documents:read` hat, sieht Belege, Anzeigen und Sperrbescheide weder in der Liste noch per Download-ID. `compliance:manage` schließt `compliance:read` nicht ein (Annahme 69). Speicherordner sind `medicine_lot`, `authority_notice` und `apiary_restriction`; MIME-Allowlist und Signaturprüfung gelten unverändert. Ein Fehler beim Löschen der Datei wird mit Dokument-Id und `loggableError()` protokolliert, Zeile, Audit und Antwort bleiben. Dokumente gelöschter Datensätze bleiben bestehen (Annahme 70). Das Recht wirkt über den 30-s-Cache des Resolvers verzögert, wie überall.
|
||||
|
||||
**Zwischenspeicher.** Alle API-Antworten tragen `Cache-Control: no-store` (Abschnitt 9, `noStoreMiddleware`); die e2e-Tests halten das für Liste und Download fest. Der Service Worker beachtet `no-store` bisher nicht (`isKeepableApiAnswer` in `public/sw.js` behält JSON-Antworten per `cache.put`). Task 14 muss deshalb entweder `no-store` dort berücksichtigen oder Pfadregeln für Listen mit den drei Arten ergänzen; das Präfix `/api/v1/documents` wird nicht ganz gesperrt, sonst fehlten die Durchsichtsfotos offline. Nie gecacht werden die Compliance-, Arzneimittel- und Sperrbezirk-Pfade (`NEVER_CACHED_API_PATHS`).
|
||||
**Zwischenspeicher.** Alle API-Antworten tragen `Cache-Control: no-store` (Abschnitt 9, `noStoreMiddleware`); die e2e-Tests halten das für Liste und Download fest. Der Service Worker beachtet `no-store` bewusst **nicht**, weil es jede Antwort trägt (Abschnitt 2, Annahme 87). Stattdessen schließen Pfadregeln die Listen der drei Arten (`/documents?entityType=…`) und `documents/:id` aus; das Präfix `/api/v1/documents` wird nicht ganz gesperrt, sonst fehlten die Durchsichtsfotos offline. Nie gecacht werden auch die Compliance-, Arzneimittel- und Sperrbezirk-Pfade (`NEVER_CACHED_API_PATHS`, seit Task 14).
|
||||
|
||||
### 12.4 Etikett-Pflichtangaben
|
||||
|
||||
@@ -861,17 +866,70 @@ Task 11 verbindet die Anzeigen mit dem Ereignis-Rückgrat (Abschnitt 10). `Compl
|
||||
|
||||
### 12.10 Prüfbereitschaft I
|
||||
|
||||
Task 12 liefert den Rahmen der Prüfbereitschaft mit den ersten sieben Regeln; die übrigen elf Regeln folgen mit Task 13.
|
||||
Task 12 liefert den Rahmen der Prüfbereitschaft mit den ersten sieben Regeln, Task 13 ergänzt die übrigen elf (zusammen die 18 der Spec, Ergänzungen am Ende dieses Abschnitts).
|
||||
- **Modul und Worker-Schutz.** `ReadinessModule` (`src/compliance/readiness/`) ist **nur in `AppModule`**. Der Controller hängt am `PermissionResolverService` und darf nicht in den Worker-Graphen. `readiness-worker-graph.spec.ts` läuft über die `imports` des `WorkerModule` und prüft, dass weder `ReadinessModule` noch `ReadinessController` darin liegen.
|
||||
- **`READINESS_RULES_TOKEN`.** Die Regelliste (`READINESS_RULES` in `rules/index.ts`) wird unter dem Token bereitgestellt; `ReadinessService` und `AcknowledgementsService` bekommen sie per Konstruktor. Unit-Specs übergeben Fake-Regeln direkt, die e2e überschreibt den Provider (`[...READINESS_RULES, softRule]`), ohne die geteilte Liste zu verändern. `rules/index.spec.ts` prüft eindeutige Schlüssel und eindeutige `subjectId` je Regel.
|
||||
- **Sieben reine Regeln.** Jede Regel ist eine pure Funktion `evaluate(ctx)`: `holder-data`, `food-business-data` (Pflicht), `vet-office` (Prüfen), `registration` (keine `SENT`-Erstanzeige), `herd-change` (Abweichung zum Bezugsstand laut Einstellung, nie ohne Bezugsstand), `tsk-report` (Stichtag erreicht ohne `SENT`-Zählung, „überfällig“ nach `tskReportDueDay`; das Vorjahr bleibt offen, solange sein Entwurf existiert) und `restriction-active` (ein Punkt mit allen gesperrten Ständen). Eine Regel ohne Befund ergibt genau einen erledigten Punkt (`DONE`, `subjectId` `global`). **Keine dieser sieben Regeln ist quittierbar** (Spec 5.7, Annahme 85); die Quittierung ist als Mechanik gebaut (`acknowledgeable`) und für weiche Regeln aus Task 13 gedacht, ein Test mit einer echten quittierbaren Regel gehört dorthin.
|
||||
- **Sieben reine Regeln.** Jede Regel ist eine pure Funktion `evaluate(ctx)`: `holder-data`, `food-business-data` (Pflicht), `vet-office` (Prüfen), `registration` (keine `SENT`-Erstanzeige), `herd-change` (Abweichung zum Bezugsstand laut Einstellung, nie ohne Bezugsstand), `tsk-report` (Stichtag erreicht ohne `SENT`-Zählung, „überfällig“ nach `tskReportDueDay`; das Vorjahr bleibt offen, solange sein Entwurf existiert) und `restriction-active` (ein Punkt mit allen gesperrten Ständen). Eine Regel ohne Befund ergibt genau einen erledigten Punkt (`DONE`, `subjectId` `global`). **Keine dieser sieben Regeln ist quittierbar** (Spec 5.7, Annahme 85); die Quittierung ist als Mechanik gebaut (`acknowledgeable`) und wird erst von den weichen Regeln aus Task 13 genutzt (vier quittierbare Regeln, siehe unten).
|
||||
- **Live-Auswertung, nur Quittungen gespeichert.** `GET /compliance/readiness` lädt einen Kontext (Stammdaten, Einstellungen, Bestandsstatus, Anzeigen, aktive Sperren, jeweils begrenzt) und führt jede Regel aus; gespeichert wird allein `ComplianceAcknowledgement`. Tage (`today`, Stichtag, „überfällig“) sind Berliner Kalendertage; die Sperre zählt über `isRestrictionActive`, dieselbe Funktion wie der Wächter an den Völker-Pfaden (12.7), damit Anzeige und Sperre nie auseinanderlaufen.
|
||||
- **Fingerabdruck.** sha256 über kanonisches JSON der für den Befund maßgeblichen Werte; Listen (z. B. `apiariesAdded/Removed/Relocated` bei `herd-change`) werden vorher **sortiert**, damit dieselben Daten in anderer Reihenfolge denselben Abdruck ergeben (Test).
|
||||
- **Quittung, Rücknahme, veraltet.** Quittiert wird mit Notiz (10 bis 2000 Zeichen); der Fingerabdruck muss zum jetzt berechneten Punkt passen (sonst 409). Eine Quittung gilt nur, solange sie nicht zurückgenommen ist **und** ihr Fingerabdruck noch passt: Eine **zurückgenommene oder veraltete Quittung öffnet den Punkt wieder**. Quittieren und Zurücknehmen schreiben Audit (`CREATE`/`UPDATE` an `ComplianceAcknowledgement`); das Zurücknehmen sperrt die Zeile und setzt `revokedAt` in SQL.
|
||||
- **Rot und Schwere.** Jeder offene Punkt ist rot; die Schwere steht als Wort („Pflicht“ oder „Prüfen“) daneben. Gesamtstatus `READY`, wenn kein Punkt offen ist, sonst `OPEN`.
|
||||
- **Höchstens 50 Subjekte.** Betroffene Objekte stehen in `subjects` (höchstens 50), der Rest wird in `more` gezählt.
|
||||
- **Maskierung je `subjectPermission`.** Titel und Detailtexte enthalten nie Namen, Anschriften, Aktenzeichen oder Notizen. Hat der Aufrufer das Recht der Regel (`subjectPermission`, hier `apiaries:read`) nicht, bleiben `subjects` leer und `more` zählt sie; `summary` wertet ohne Subjekte aus. Der Stammdaten-Punkt nennt nur die Lücke („Name“, „Anschrift“), nie den Wert. **Die Zählungen bleiben sichtbar** („N Stände liegen im Sperrbezirk“), auch maskiert; die Seite verlangt ohnehin `compliance:read`, nur die Namen hängen am Recht (Annahme 81). Die Arzneimittel-Regeln aus Task 13 setzen `subjectPermission: 'veterinary-medicines:read'` und halten Mittel, Packung, Lieferant und Tierarzt aus `detail` und `title` heraus.
|
||||
- **Maskierung je `subjectPermission`.** Titel und Detailtexte enthalten nie Namen, Anschriften, Aktenzeichen oder Notizen. Hat der Aufrufer das Recht der Regel (`subjectPermission`, hier `apiaries:read`) nicht, bleiben `subjects` leer und `more` zählt sie; `summary` wertet ohne Subjekte aus. Der Stammdaten-Punkt nennt nur die Lücke („Name“, „Anschrift“), nie den Wert. **Die Zählungen bleiben sichtbar** („N Stände liegen im Sperrbezirk“), auch maskiert; die Seite verlangt ohnehin `compliance:read`, nur die Namen hängen am Recht (Annahme 81). Die Arzneimittel-Regeln aus Task 13 halten Mittel, Packung, Lieferant und Tierarzt aus `detail` und `title` heraus; welches Recht die Namen freigibt, steht je Regel in der Tabelle unten.
|
||||
- **Lesepfade schreiben nicht.** `loadReadinessContext` liest `SiteSettings` per `findUnique`; fehlt die Zeile, gilt ein leerer In-Memory-Standard (alle Felder `null`, `holder-data` meldet die Lücken), kein Upsert. `ComplianceSettings` kommt aus `herdStatusIn` (12.8), das nur anlegt, wenn die von der Migration gesäte Zeile fehlt.
|
||||
- **Registrierungs-Bezug separat.** Die jüngste `SENT`-Erstanzeige (nach `sentOn`, `createdAt`, `id`) und der jüngste `DRAFT` werden in **eigenen Abfragen** gelesen, nicht aus einer begrenzten Gesamtliste; nur der TSK-Teil ist auf 100 begrenzt. Der Bezugsstand für `herd-change` kommt unverändert aus `herdStatusIn` (`findBaselineIn`).
|
||||
- **Endpunkte.** `GET /compliance/readiness`, `GET /compliance/readiness/summary` (`compliance:read`); `POST /compliance/acknowledgements` (`@Idempotent`) und `POST /compliance/acknowledgements/:id/revoke` (`compliance:manage`). BEEKEEPER und ADMIN alles, STAFF und READONLY nur Lesen, API_CLIENT nichts.
|
||||
- Hinweis für die Oberfläche (Task 14): Die Zuordnung „Schwere → Wort“ und „`OPEN` → rot mit Symbol und Wort“ braucht einen eigenen Test.
|
||||
- Oberfläche: Die Zuordnung „Schwere → Wort“ und „`OPEN` → rot mit Symbol und Wort“ hat seit Task 14 einen eigenen Test (`labels.spec.ts`, Abschnitt 12.11).
|
||||
|
||||
**Task 13: die 18 Prüfpunkte.** Die Liste `READINESS_RULES` hat jetzt 18 Regeln in fünf Bereichen; `rules/index.spec.ts` prüft Bereich, Schwere, `acknowledgeable` und `subjectPermission` aller 18. Alle Regeln sind weiter reine Funktionen auf dem Kontext (`loadReadinessContext(db, now, { withdrawal })` mit den Feldern `treatments`, `moves`, `lots`, `medicines`, `templates`, `harvests`; `ReadinessModule` importiert dafür `WithdrawalModule`).
|
||||
|
||||
| Bereich | Regel (Schlüssel) | Schwere | quittierbar | Namen frei mit |
|
||||
|---|---|---|---|---|
|
||||
| Stammdaten | `holder-data`, `food-business-data` | Pflicht | – | – |
|
||||
| Stammdaten | `vet-office` | Prüfen | – | – |
|
||||
| Behörden | `registration`, `herd-change`, `tsk-report`, `restriction-active` | Pflicht | – | `apiaries:read` bei `herd-change`, `restriction-active` |
|
||||
| Tierarzneimittel | `treatment-log-gaps` (ein Punkt je Kalenderjahr) | Pflicht | – | `treatments:read` |
|
||||
| Tierarzneimittel | `lot-gaps`, `lot-prescription` | Pflicht | – | `veterinary-medicines:read` |
|
||||
| Tierarzneimittel | `treatment-location-uncertain` | Prüfen | – | `treatments:read` |
|
||||
| Tierarzneimittel | `medicine-withdrawal-missing`, `lot-negative` | Prüfen | – | `veterinary-medicines:read` |
|
||||
| Tierarzneimittel | `lot-expired-stock` (je Packung), `treatment-expired-lot` (je Behandlung) | Prüfen | ja | `veterinary-medicines:read` |
|
||||
| Etiketten | `label-template-gaps` | Pflicht | – | – |
|
||||
| Etiketten | `label-template-form` | Prüfen | ja | – |
|
||||
| Ernte | `harvest-withdrawal` (je Ernte) | Prüfen | ja | `treatments:read` |
|
||||
|
||||
- **Gleiche Funktionen wie das PDF.** `treatment-log-gaps` nutzt `resolveSourceValues`, `treatmentLogGaps` und `groupTreatmentLog` des Bestandsbuchs (12.2): Rückblick sind das laufende und fünf vorige Kalenderjahre (Jahr der UTC-Zeit wie im PDF; ein Eintrag um Mitternacht zum Jahreswechsel kann im Nachbarjahr stehen). Eine lückenhafte Packung steht einmal als Subjekt `LOT` (Label „Packung der Behandlungen“), Behandlungen ohne Packung als Subjekt `TREATMENT` mit dem Volk als `parentId`; Mengen- und Wartezeitlücken bleiben auf der Behandlung, die Tierarzt-Lücke gibt es nur bei verschreibungspflichtigem Mittel. `treatment-location-uncertain` meldet die `*`-Zeilen (`apiaryOnDate(...).uncertain`). `medicine-withdrawal-missing` zählt 0 Tage als erfasst. `lot-expired-stock` nimmt den Rest in den Fingerabdruck auf: Ändert sich der Rest, öffnet sich die Quittung wieder.
|
||||
- **Etiketten.** `label-template-gaps` und `label-template-form` prüfen jede aktive Vorlage mit demselben `checkLabel` wie der Druck (12.4, 12.6; `buildPlaceholderValues`, `expectedParticulars`, Ziffernregeln nur aus `ComplianceSettings`), je Vorlage einmal gemessen (`label-checks.ts`). `label-template-form` meldet nur Warnungen, sein Fingerabdruck sind die Warn-Codes. **Das Ziel der Prüfung ist die jüngste Abfüllung mit Restbestand**: Der Lader liest die Abfüllungen seitenweise (200, neueste zuerst) und hört bei der ersten mit Restbestand (`summarizeStock`) auf; gibt es keine, gilt die jüngste, sonst `SAMPLE_BOTTLING` (Beispielwerte). Ungültige `netQuantityDigitRules` ergeben in der Prüfbereitschaft „keine Regel“ (die Höhe der Füllmenge wird nicht beurteilt), der Druck lehnt sie dagegen mit 409 ab.
|
||||
- **Ernte.** `harvest-withdrawal` rechnet keine Fenster selbst: Der Lader ruft `WithdrawalCheckService.conflictsForHarvests` für die Ernten der letzten 24 Monate auf, der Text kommt aus `withdrawalRangeText` (längstes Fenster).
|
||||
- **Arzneimittel-Namen maskiert.** Regeln mit `subjectPermission: 'veterinary-medicines:read'` zeigen Packungen und Behandlungen ohne dieses Recht nur gezählt (`subjects` leer, `more` zählt). Titel, Text und Subjekt-Labels enthalten **nie Produktnamen** oder Mittelnamen; ein Label heißt allgemein („Packung der Behandlungen“), die Zuordnung zum Mittel steht erst hinter dem Beheben-Link. Die Namen der Lücken (`missing`) sind deutsche Feldnamen (Lieferant, Beleg, Menge, Wartezeit, Tierarzt) bzw. die Titel der Etikett-Befunde. Neue Subjekt-Typen: `TREATMENT`, `LOT`, `MEDICINE`, `LABEL_TEMPLATE`, `HARVEST`.
|
||||
- **Prüfbericht als PDF.** `GET /compliance/readiness/report.pdf` (`compliance:read`) liefert `pruefbericht.pdf` (A4 hoch, `no-store`, `renderReadinessReportPdf` in `readiness-report.ts`). Inhalt: Kopf mit Halter (`FEHLT`, wenn leer), Datum, Gesamtstatus und dem Satz „Jeder offene Punkt ist zu behandeln.“, je Bereich eine Tabelle mit Status (Offen, Erledigt, Quittiert), Schwere als Wort, Titel, Text, Norm (130 pt breit, „Fertigpackungsverordnung“ in einer Zeile) und bei Quittungen Notiz, Nutzer und Datum. **Gedruckt werden nur Zählungen** („n Objekte“), nie Namen von Objekten (Annahme 90); Quittungsnotizen werden nach 600 Zeichen mit „…“ gekürzt. Die Auswertung läuft mit den Rechten des Aufrufers. Ein Aufrufer mit `compliance:read` ohne `veterinary-medicines:read` bekommt 200, und das PDF enthält weder Mittelnamen noch Behandlungsprodukte (e2e).
|
||||
- **Kurzfassung gecacht.** `ReadinessService.summary(now)` hält das Ergebnis je Prozess 60 Sekunden (`SUMMARY_TTL_MS`, Zeitbasis ist das übergebene `now`). `AcknowledgementsService.create` (nach Erfolg) und `revoke` rufen `invalidateSummary()` auf; ein abgelehntes Quittieren lässt den Cache stehen. Liste (`GET /compliance/readiness`), PDF-Bericht und Quittierpfad rechnen immer live. Änderungen an den Daten und Quittungen anderer Prozesse zeigen sich in der Kurzfassung bis zu 60 Sekunden später (Annahme 86).
|
||||
- **409-Codes beim Quittieren.** Die Antwort hat den Körper `{ code, message }` (Meldungen unverändert): `READINESS_FINGERPRINT_STALE` (Fingerabdruck passt nicht mehr; „Der Punkt hat sich geändert. Bitte neu laden.“), `READINESS_ITEM_NOT_OPEN` (Punkt weg oder erledigt; gleiche Meldung) bei `POST /compliance/acknowledgements`, und `READINESS_ACK_REVOKED` („Die Quittung ist schon zurückgenommen.“) bei `…/:id/revoke`. Die Konstanten stehen in `acknowledgements.service.ts`; die Oberfläche wertet die Codes aus (12.11), der Text ist nur Rückfall.
|
||||
- **Lastgrenze.** Eine Auswertung lädt alle Packungen, sechs Jahre Behandlungen und die Ernte-Konflikte (`windowsOf` liest alle Behandlungen der beteiligten Völker). Das ist für den Betrieb unkritisch und der Grund für den 60-s-Cache der Kurzfassung (Dashboard); bei sehr vielen Daten sind Index oder weitere Zwischenspeicher zu prüfen. Einzelpunkte je Behandlung oder Ernte können lang werden, die Oberfläche klappt sie ein.
|
||||
|
||||
### 12.11 Oberfläche (Frontend)
|
||||
|
||||
Die Oberfläche des Compliance-Blocks entsteht in mehreren Tasks; dieser Stand umfasst Prüfbereitschaft (Task 14), Tierarzneimittel (Task 15) und Etikett (Task 17). Behandlungsmaske (Task 16) sowie Anzeigen, Sperrbezirke und Einstellungen (Task 18, 19) folgen. Alle Aktionen sind online-only: Auslöser sind offline deaktiviert und nennen den sichtbaren Grund „Nur mit Verbindung“ (`aria-describedby`). Der Service-Worker-Ausschluss steht in Abschnitt 2.
|
||||
|
||||
**Prüfbereitschaft (Task 14).** Code in `lib/compliance/` (`types`, `api`, `fix-links`, `errors`, `labels`, `use-readiness`; Query-Schlüssel `['compliance', …]`) und `app/(app)/compliance/`.
|
||||
- **Seite `/compliance`** zeigt `GET /compliance/readiness`. Oben der Status: „Prüfbereit“ (grün) oder „n offen“ (rot), jeweils mit Symbol und Wort. **Jeder offene Punkt ist rot** (Token `destructive`) und trägt Symbol, das Wort „Offen“, die **Schwere als Wort „Pflicht“ oder „Prüfen“** und den Titel; dazu ein Satz Detail und klein die Norm. Betroffene Objekte stehen eingeklappt mit einem „Beheben“-Link je Objekt, „und n weitere“ für den Rest; fehlt das Recht auf die Namen, steht nur „n betroffen“. Erledigte Punkte stehen eingeklappt unter „Erledigt (n)“, Quittierte ruhig mit Nutzer, Datum und Notiz. Die Zuordnung Schwere → Wort und offen → rot liegt allein in `labels.ts` und hat einen eigenen Test. Hauptaktion ist „Nächsten Punkt lösen“ (erster offener Pflicht-Punkt); ist dessen Ziel noch nicht gebaut, heißt sie „Nächsten Punkt ansehen“ und springt auf die Karte (`#item-<Schlüssel>-<Subjekt>`). Bei grünem Status steht dort „Bestandsbuch {Jahr} drucken“. Hinter „Mehr“ liegen der Bericht (PDF über `downloadReadinessReport`, als Blob mit Bearer-Header, Dateiname `pruefbericht.pdf`), das Bestandsbuch, Anzeigen, Sperrbezirke und Einstellungen. Downloads sind offline aus.
|
||||
- **Beheben-Links aus einer Stelle:** `fixLinkFor(itemKey, subject)` in `fix-links.ts` bildet Subjekte auf Seiten ab (Typen ohne Beachtung der Groß-/Kleinschreibung). Die Ziele `/compliance/notices(/[id])`, `/compliance/restrictions` und `/compliance/settings` gibt es erst mit Task 18/19; bis dahin blendet die Menge `ROUTE_AVAILABLE` solche Links aus (`available: false`), und die jeweilige Task trägt ihre Route dort ein. Eine Behandlung verlinkt auf `/colonies/{parentId}`, Stammdaten auf `/settings/automation?tab=social-media` (Reiter „Stammdaten & Web“). `herd-change` führt zur Anzeige (`/compliance/notices`), nicht zur Sperrenliste; `restriction-active` ohne Subjekt zur Sperrenliste.
|
||||
- **Quittieren und Zurücknehmen** (`acknowledge-dialog.tsx`): nur bei `acknowledgeable` und offenem Punkt bzw. quittiertem Punkt, nur mit `compliance:manage`. Notiz 10 bis 2000 Zeichen, vor dem Senden geprüft; Zurücknehmen über `ConfirmDialog`. Fehler laufen über `readinessConflictText`, der die drei Codes `READINESS_FINGERPRINT_STALE`, `READINESS_ITEM_NOT_OPEN` und `READINESS_ACK_REVOKED` mit je eigenem deutschem Text auswertet (der Servertext ist nur Rückfall); bei jedem 409 laden alle `compliance`-Queries neu. Nach Erfolg und Neuladen geht der Fokus auf die Kartenüberschrift (`tabIndex=-1`), eine `role="status"`-Region sagt „Punkt quittiert“ bzw. „Quittung zurückgenommen“. `lib/compliance/errors.ts` liest außerdem `DRAFT_EXISTS` und `RESTRICTED_ZONE`, alles andere läuft über `saveErrorText`; es gibt keine zweite Fehlerlogik.
|
||||
- **Dashboard-Zeile** (`components/today/compliance-line.tsx`): „n Punkte offen in der Prüfbereitschaft“, nur mit `compliance:read` und `openCount > 0`, aus `GET /compliance/readiness/summary` (60 s gecacht, Annahme 86). Schlägt das Laden fehl (kein Netzfehler; offline bleibt es still), steht eine **Fehlerzeile** „Prüfbereitschaft nicht geladen.“ mit „Erneut versuchen“.
|
||||
- **Navigation:** `nav-config.ts` hat „Prüfbereitschaft“ (Symbol `ShieldCheck`, Gruppe „Betrieb“, Recht `compliance:read`).
|
||||
|
||||
**Tierarzneimittel (Task 15).** Seiten unter `app/(app)/medicines/`, Daten aus `lib/medicines/api.ts`; kein eigener Nav-Eintrag, der Einstieg ist die Karte „Tierarzneimittel“ auf `/storage` (nur mit `veterinary-medicines:read`).
|
||||
- **Seiten.** `/medicines` (Katalog mit Rest und nächstem Verfall, Reiter Aktiv/Deaktiviert; bei leerem Katalog „Aus deinen Behandlungen übernehmen?“ mit `GET …/suggestions`: „Übernehmen“ öffnet nur das Formular, gespeichert wird erst beim Absenden), `/medicines/[id]` (Angaben, Packungen, Hauptknopf „Packung erfassen“) und `/medicines/lots/[id]` (Rest, Angaben, Bewegungsbuch, Beleg, Hauptknopf „Inventur“). Hauptknöpfe: „Mittel anlegen“ (`create`), „Packung erfassen“ (`create`), „Inventur“ (`update`); Entsorgung, Bearbeiten und (De-)Aktivieren sind Nebenaktionen mit `update`. Ist das Mittel inaktiv, ist „Packung erfassen“ mit sichtbarem Grund „Das Mittel ist deaktiviert.“ gesperrt (Annahme 61). Mit Packungen entfällt das Einheitsfeld (Annahme 62), `unit` wird nie gesendet; geleerte Felder gehen als `null` hinaus.
|
||||
- **Dialoge.** Mittel, Packung, Inventur (zeigt die Differenz live in einem `aria-live`-Bereich, Buchen bei Differenz 0 gesperrt) und Entsorgung (Menge, Pflichtgrund; über den Rest hinaus erlaubt, mit roter Warnung samt Symbol und Wort „Negativ“). Server-Meldungen („Das Mittel ist deaktiviert.“, „Die Einheit des Mittels passt nicht zur Packung.“) erscheinen im Dialog. Ein negativer Rest erscheint überall rot mit Warnsymbol und Wort (`RestText`).
|
||||
- **`use-attempt-key`** (`lib/medicines/use-attempt-key.ts`, Ansatz der Abfüllung): Jeder Anlege- und Buchungsaufruf trägt einen `Idempotency-Key`. Der Schlüssel bleibt nur, wenn der Body unverändert ist und der letzte Versuch am Netzwerk scheiterte; jede Serverantwort ohne Erfolg und jede Änderung des Bodys erneuert ihn (sonst würde der Server mit dem Schlüssel einen alten Body wiedergeben). Er gilt in allen vier Dialogen.
|
||||
- **`parseDecimal`: Tausenderpunkt-Regel.** Das Komma ist immer das deutsche Dezimalzeichen („1,000“ ist 1). Ein Punkt gilt nur als Dezimalpunkt, wenn er das einzige Trennzeichen ist und nicht wie ein Tausenderpunkt aussieht (`^\d*[1-9]\d*\.\d{3}$`). „1,5“, „1.5“ und „0.001“ gehen; **„1.000“**, „12.345“ und jeder Text mit mehr als einem Trenner (auch „1.234,5“) sind ungültig mit „Bitte ohne Tausenderpunkt eingeben, z. B. 1000 oder 1,5“ (Annahme 88). Gilt in allen Mengenfeldern; sie sind Textfelder mit `inputMode="decimal"`.
|
||||
- **Bewegungsbuch** (`lib/medicines/book.ts`): Eingang als erste Zeile, laufender Stand auf Tausendstel gerundet, neueste zuerst, Datum ist der Berliner Tag der Buchung. **Hinweis bei Abweichung:** Weicht der letzte rechnerische Stand von `lot.remaining` des Servers ab, steht unter der Tabelle „Stand weicht vom Server ab – bitte neu laden“.
|
||||
- **Seite ohne Leserecht.** Die drei Seiten laden ohne `veterinary-medicines:read` nichts (`enabled: false`) und zeigen „Dazu fehlt dir das Recht.“ ohne Erneut-Knopf (`components/medicines/no-read-access.tsx`, Link zurück zum Lager).
|
||||
- **Belege.** `DocumentUploadCard` (Art `MEDICINE_LOT`, Titel „Beleg“) hat die Eigenschaft `canWrite` (Vorgabe `true`); die Packungsseite übergibt `canWrite={canUpdate}`. Ohne sie entfallen Hochladen, Umbenennen und Löschen, Herunterladen bleibt. `DocumentEntityType` kommt aus `DOCUMENT_ENTITY_TYPES` und kennt `MEDICINE_LOT`, `AUTHORITY_NOTICE`, `APIARY_RESTRICTION`.
|
||||
- **Hinweis Bestandsbuch (Annahme 73).** Auf der Standseite ist der Menüeintrag „Bestandsbuch (PDF)“ ohne `veterinary-medicines:read` deaktiviert (und nicht fokussierbar, weil die Menükomponente `aria-disabled` nicht zulässt); dauerhaft steht darunter die Zeile „Das Bestandsbuch braucht das Recht „Tierarzneimittel lesen“.“. Für Task 16: `fetchSelectableLots(medicineId, includeLotId?)` liefert nur Packungen aktiver Mittel mit Rest > 0.
|
||||
|
||||
**Etikett (Task 17).** Das Frontend entscheidet nichts über Pflichtangaben; es zeigt, was das Backend (12.4, 12.6) prüft.
|
||||
- **Prüfpanel im Designer** (`components/labels/label-compliance-panel.tsx`): Der Designer sendet 500 ms nach der letzten Änderung nur die Text-Elemente an `POST /label-templates/check` (Hook `useDebouncedLabelCheck` in `lib/label-templates/check.ts`; nur Text-Änderungen lösen aus, Eingaben außerhalb des technischen Bereichs lösen keinen Aufruf aus). Ein neuer Entwurf und das Aushängen **brechen den laufenden Aufruf ab** (`AbortSignal`); bei einem ungültigen Entwurf bleibt das letzte Ergebnis stehen, ist aber als „Veraltet: …“ markiert (`stale`). Das Panel zeigt die sechs Pflichtangaben mit Symbol und „Erfüllt“/„Fehlt“, Hinweise getrennt und mit „Hinweis:“ beginnend; die Live-Region (`aria-live="polite"`) enthält nur die Summenzeile.
|
||||
- **Keine Rechtszahlen im Frontend.** Mindest-x-Höhe und Ziffernhöhen kommen ausschließlich aus `requirements` der Prüfantwort („Die Prüfung verlangt aktuell mindestens X mm“); der Hinweistext am Feld „Mindest-x-Höhe (mm)“ nennt keine Zahl, `DEFAULT_MIN_X_HEIGHT_MM` gibt es nicht. Der Pflichtangaben-Block (`compliance-block.ts`, „Pflichtangaben-Block einfügen“, mit Rückgängig) rechnet die Schriftgrößen aus `requirements`. Die Schranken 0,5 bis 5 mm des Feldes sind der technische Bereich des DTO, kein Rechtswert. Eine neue Vorlage startet mit leerem Feld und sendet kein `minXHeightMm` (der Server setzt seinen Standard); bis ein Wert da ist, läuft keine Prüfung (Annahme 89).
|
||||
- **Druckkarte der Abfüllung** (`bottling/[id]/print-labels-card.tsx`): Vor dem Anzeigen läuft `GET /label-templates/:id/check/:bottlingBatchId` (`gcTime: 0`, keine Persistenz, `cache: "no-store"`). Bei Lücken zeigt sie die **rote Lückenliste** (Region, Symbol und „Fehlt:“) mit Links zu Stammdaten, Produkt und Designer (der Produktlink entfällt ohne Produkt an der Abfüllung); „Etiketten als PDF“ ist dann `aria-disabled` (fokussierbar, Grund per `aria-describedby`). Der **Probedruck** („Probedruck (MUSTER)“, `proof=true`) bleibt immer möglich; sein Dateiname enthält „muster“, der des echten Drucks nicht. Antwortet der Druck mit 409 `LABEL_PARTICULARS_MISSING`, zeigt die Karte die Liste `gaps` des Servers und **setzt den Fokus darauf**. 409 `COMPLIANCE_SETTINGS_INVALID` und 403 erscheinen als deutsche Meldung (`labelCheckErrorText`). Ein zweiter Klick während der Wartezeit löst keinen zweiten Aufruf aus. Ohne `label-templates:read` steht nur ein Hinweis, ohne `:print` gibt es keine Druckknöpfe, ohne Netz „Nur mit Verbindung“. Prüf- und Druckaufrufe laufen mit `cache: "no-store"` (`apiFetchBlob` reicht `init` durch).
|
||||
- **Stammdaten.** Im Reiter „Stammdaten & Web“ (Verwaltung > Automatisierung) stehen `foodBusinessName` (200) und `foodBusinessAddress` (300); **leere oder nur aus Leerzeichen bestehende Felder werden als `null` gesendet** (`value.trim() || null`), damit sie serverseitig löschen. „Vom Bienenhalter übernehmen“ füllt nur die Felder, ohne zu speichern. Der Text nennt, dass sie weder im Shop noch auf der Herkunftsseite erscheinen.
|
||||
- **Produkt `legalName`.** Das Feld „Bezeichnung auf dem Etikett (z. B. Blütenhonig)“ steht im Typ `Product`, im Neu-Formular, im Bearbeiten-Dialog und auf der Detailseite; Platzhalter-Schlüssel und deutsche Labels liegen in `lib/label-templates/placeholder-keys.ts` (Wächter-Spec gegen `test/fixtures/placeholder-keys.json`).
|
||||
Reference in new issue
Block a user