diff --git a/CLAUDE.md b/CLAUDE.md index 597a0fa..54aa8df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -253,10 +253,13 @@ zentralen Katalog zu verändern. **Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede Zusicherung im Katalog (AVV verfügbar, Verarbeitungsort, Training-Opt- out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für -eine Aussage, die nicht nachweisbar ist. Einträge älter als 180 Tage -werden im zentralen (`GET /betreiber/werkzeuge`) und im mandanten- -seitigen Katalog (`GET /verwaltung/werkzeuge`) als "nicht aktuell -geprüft" markiert (`werkzeugPruefungVeraltetNach`, 180 Tage). +eine Aussage, die nicht nachweisbar ist. Beide Katalogansichten zeigen +eine dreistufige Ampel zum Prüfalter (`pruefStatus()` in +`betreiber_werkzeug_handlers.go`, CSS-Klassen `status-niedrig`/ +`-mittel`/`-hoch`, dieselben Farbtoken wie die KI-VO-Risikoampel): +**aktuell** (grün, < 150 Tage), **bald fällig** (gelb, 150–180 Tage — +dasselbe 30-Tage-Vorwarnfenster wie bei der Wiedervorlage), **abgelaufen** +(rot, > 180 Tage). `aufbewahrung_tage` ist NULLABLE (Migration 0015) — `NULL` bedeutet "vom Anbieter nicht beziffert" und ist bewusst von `0` ("belegte diff --git a/internal/web/betreiber_werkzeug_handlers.go b/internal/web/betreiber_werkzeug_handlers.go index 82b63b3..bc07962 100644 --- a/internal/web/betreiber_werkzeug_handlers.go +++ b/internal/web/betreiber_werkzeug_handlers.go @@ -67,7 +67,8 @@ type werkzeugListItem struct { Zertifizierungen string GeeigneteZwecke string LetztePruefung string - NichtAktuell bool + PruefStatusLabel string + PruefStatusCSS string } // formatAufbewahrung stellt sicher, dass "unbekannt" (Anbieter hat @@ -90,10 +91,32 @@ type werkzeugListeData struct { } // werkzeugPruefungVeraltetNach ist die Schwelle, ab der ein -// Katalogeintrag im Ergebnis als "nicht aktuell geprüft" markiert wird -// (siehe CLAUDE.md, Werkzeugkatalog) — jede Zusicherung im Katalog muss -// belegbar UND aktuell sein. -const werkzeugPruefungVeraltetNach = 180 * 24 * time.Hour +// Katalogeintrag im Ergebnis als "abgelaufen" markiert wird (siehe +// CLAUDE.md, Werkzeugkatalog) — jede Zusicherung im Katalog muss +// belegbar UND aktuell sein. werkzeugPruefungBaldFaelligNach liegt +// 30 Tage davor (dasselbe Vorwarnfenster wie die Wiedervorlage, +// wiedervorlageSchwelle) — "bald fällig" soll rechtzeitig auffallen, +// nicht erst wenn die Frist schon gerissen ist. +const ( + werkzeugPruefungBaldFaelligNach = 150 * 24 * time.Hour + werkzeugPruefungVeraltetNach = 180 * 24 * time.Hour +) + +// pruefStatus liefert eine Ampel-Kennzeichnung für das Alter einer +// letzten Werkzeug-Prüfung: aktuell (grün) / bald fällig (gelb) / +// abgelaufen (rot) — die CSS-Klassen sind dieselben wie für die +// Risiko-Ampel anderswo im Produkt (status-niedrig/-mittel/-hoch). +func pruefStatus(letztePruefung time.Time) (label, css string) { + alter := time.Since(letztePruefung) + switch { + case alter > werkzeugPruefungVeraltetNach: + return "abgelaufen", "status-hoch" + case alter > werkzeugPruefungBaldFaelligNach: + return "bald fällig", "status-mittel" + default: + return "aktuell", "status-niedrig" + } +} // handleBetreiberWerkzeugListe zeigt den zentralen Katalog. func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Request) { @@ -104,13 +127,14 @@ func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Req } data := werkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)} for _, wz := range werkzeuge { + label, css := pruefStatus(wz.LetztePruefung) data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{ ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort, AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard, Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage), Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "), - LetztePruefung: wz.LetztePruefung.Format("02.01.2006"), - NichtAktuell: time.Since(wz.LetztePruefung) > werkzeugPruefungVeraltetNach, + LetztePruefung: wz.LetztePruefung.Format("02.01.2006"), + PruefStatusLabel: label, PruefStatusCSS: css, }) } if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeuge", data); err != nil { diff --git a/internal/web/mandant_werkzeug_handlers.go b/internal/web/mandant_werkzeug_handlers.go index 219c1de..7f1bdb8 100644 --- a/internal/web/mandant_werkzeug_handlers.go +++ b/internal/web/mandant_werkzeug_handlers.go @@ -11,7 +11,6 @@ package web import ( "net/http" "strings" - "time" ) type mandantWerkzeugItem struct { @@ -24,7 +23,8 @@ type mandantWerkzeugItem struct { Aufbewahrung string GeeigneteZwecke string LetztePruefung string - NichtAktuell bool + PruefStatusLabel string + PruefStatusCSS string Gesperrt bool } @@ -71,14 +71,15 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque data := mandantWerkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)} for _, wz := range zentral { + label, css := pruefStatus(wz.LetztePruefung) data.Werkzeuge = append(data.Werkzeuge, mandantWerkzeugItem{ ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort, AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard, - Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage), - GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "), - LetztePruefung: wz.LetztePruefung.Format("02.01.2006"), - NichtAktuell: time.Since(wz.LetztePruefung) > werkzeugPruefungVeraltetNach, - Gesperrt: gesperrt[wz.ID], + Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage), + GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "), + LetztePruefung: wz.LetztePruefung.Format("02.01.2006"), + PruefStatusLabel: label, PruefStatusCSS: css, + Gesperrt: gesperrt[wz.ID], }) } for _, wz := range eigene { diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 38be898..b8b8f13 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -392,6 +392,16 @@ nav button { color: var(--color-mittel); } +.status-niedrig { + background: var(--color-niedrig-bg); + color: var(--color-niedrig); +} + +.status-hoch { + background: var(--color-hoch-bg); + color: var(--color-hoch); +} + .beitraege-liste { list-style: none; margin: 0 0 16px; @@ -595,11 +605,17 @@ tbody tr:hover { background: var(--color-bg); } +/* Ohne dieses Signal sieht eine breite, am Fensterrand abgeschnittene + Tabelle wie ein Rendering-Fehler aus statt wie "hier geht's mit + Scrollen weiter" — der Fade ist bewusst immer sichtbar (kein JS, das + den Scroll-Zustand prüfen könnte), auch am Ende der Tabelle. */ .table-scroll { overflow-x: auto; box-shadow: var(--shadow); border-radius: var(--radius-md); margin-bottom: 16px; + -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%); + mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%); } /* Ab hier mehr Platz (Tablet) — der Container bekommt spürbaren diff --git a/internal/web/templates/betreiber_werkzeuge.html b/internal/web/templates/betreiber_werkzeuge.html index f0b4f9a..c56b544 100644 --- a/internal/web/templates/betreiber_werkzeuge.html +++ b/internal/web/templates/betreiber_werkzeuge.html @@ -29,7 +29,7 @@