feat: Ampel-Kennzeichnung für Prüfalter im Werkzeugkatalog, Scroll-Fade für breite Tabellen
Katalogtabellen zeigten bisher nur "nicht aktuell geprüft" als Binärflag; jetzt eine dreistufige Ampel (aktuell/bald fällig/ abgelaufen) mit denselben Farbtoken wie die KI-VO-Risikoampel. Zusätzlich: .table-scroll bekommt einen rechten Fade-Rand, damit eine am Fensterrand abgeschnittene breite Tabelle als "hier geht's mit Scrollen weiter" erkennbar ist statt wie ein Rendering-Fehler auszusehen.
This commit is contained in:
11
CLAUDE.md
11
CLAUDE.md
@@ -253,10 +253,13 @@ zentralen Katalog zu verändern.
|
|||||||
**Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede
|
**Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede
|
||||||
Zusicherung im Katalog (AVV verfügbar, Verarbeitungsort, Training-Opt-
|
Zusicherung im Katalog (AVV verfügbar, Verarbeitungsort, Training-Opt-
|
||||||
out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für
|
out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für
|
||||||
eine Aussage, die nicht nachweisbar ist. Einträge älter als 180 Tage
|
eine Aussage, die nicht nachweisbar ist. Beide Katalogansichten zeigen
|
||||||
werden im zentralen (`GET /betreiber/werkzeuge`) und im mandanten-
|
eine dreistufige Ampel zum Prüfalter (`pruefStatus()` in
|
||||||
seitigen Katalog (`GET /verwaltung/werkzeuge`) als "nicht aktuell
|
`betreiber_werkzeug_handlers.go`, CSS-Klassen `status-niedrig`/
|
||||||
geprüft" markiert (`werkzeugPruefungVeraltetNach`, 180 Tage).
|
`-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
|
`aufbewahrung_tage` ist NULLABLE (Migration 0015) — `NULL` bedeutet
|
||||||
"vom Anbieter nicht beziffert" und ist bewusst von `0` ("belegte
|
"vom Anbieter nicht beziffert" und ist bewusst von `0` ("belegte
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ type werkzeugListItem struct {
|
|||||||
Zertifizierungen string
|
Zertifizierungen string
|
||||||
GeeigneteZwecke string
|
GeeigneteZwecke string
|
||||||
LetztePruefung string
|
LetztePruefung string
|
||||||
NichtAktuell bool
|
PruefStatusLabel string
|
||||||
|
PruefStatusCSS string
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatAufbewahrung stellt sicher, dass "unbekannt" (Anbieter hat
|
// formatAufbewahrung stellt sicher, dass "unbekannt" (Anbieter hat
|
||||||
@@ -90,10 +91,32 @@ type werkzeugListeData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// werkzeugPruefungVeraltetNach ist die Schwelle, ab der ein
|
// werkzeugPruefungVeraltetNach ist die Schwelle, ab der ein
|
||||||
// Katalogeintrag im Ergebnis als "nicht aktuell geprüft" markiert wird
|
// Katalogeintrag im Ergebnis als "abgelaufen" markiert wird (siehe
|
||||||
// (siehe CLAUDE.md, Werkzeugkatalog) — jede Zusicherung im Katalog muss
|
// CLAUDE.md, Werkzeugkatalog) — jede Zusicherung im Katalog muss
|
||||||
// belegbar UND aktuell sein.
|
// belegbar UND aktuell sein. werkzeugPruefungBaldFaelligNach liegt
|
||||||
const werkzeugPruefungVeraltetNach = 180 * 24 * time.Hour
|
// 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.
|
// handleBetreiberWerkzeugListe zeigt den zentralen Katalog.
|
||||||
func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Request) {
|
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)}
|
data := werkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
||||||
for _, wz := range werkzeuge {
|
for _, wz := range werkzeuge {
|
||||||
|
label, css := pruefStatus(wz.LetztePruefung)
|
||||||
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
|
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
|
||||||
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
||||||
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
||||||
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
||||||
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
||||||
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
||||||
NichtAktuell: time.Since(wz.LetztePruefung) > werkzeugPruefungVeraltetNach,
|
PruefStatusLabel: label, PruefStatusCSS: css,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeuge", data); err != nil {
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeuge", data); err != nil {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ package web
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type mandantWerkzeugItem struct {
|
type mandantWerkzeugItem struct {
|
||||||
@@ -24,7 +23,8 @@ type mandantWerkzeugItem struct {
|
|||||||
Aufbewahrung string
|
Aufbewahrung string
|
||||||
GeeigneteZwecke string
|
GeeigneteZwecke string
|
||||||
LetztePruefung string
|
LetztePruefung string
|
||||||
NichtAktuell bool
|
PruefStatusLabel string
|
||||||
|
PruefStatusCSS string
|
||||||
Gesperrt bool
|
Gesperrt bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,14 +71,15 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
data := mandantWerkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
data := mandantWerkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
||||||
for _, wz := range zentral {
|
for _, wz := range zentral {
|
||||||
|
label, css := pruefStatus(wz.LetztePruefung)
|
||||||
data.Werkzeuge = append(data.Werkzeuge, mandantWerkzeugItem{
|
data.Werkzeuge = append(data.Werkzeuge, mandantWerkzeugItem{
|
||||||
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
||||||
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
||||||
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
||||||
GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
||||||
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
||||||
NichtAktuell: time.Since(wz.LetztePruefung) > werkzeugPruefungVeraltetNach,
|
PruefStatusLabel: label, PruefStatusCSS: css,
|
||||||
Gesperrt: gesperrt[wz.ID],
|
Gesperrt: gesperrt[wz.ID],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, wz := range eigene {
|
for _, wz := range eigene {
|
||||||
|
|||||||
@@ -392,6 +392,16 @@ nav button {
|
|||||||
color: var(--color-mittel);
|
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 {
|
.beitraege-liste {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
@@ -595,11 +605,17 @@ tbody tr:hover {
|
|||||||
background: var(--color-bg);
|
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 {
|
.table-scroll {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: 16px;
|
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
|
/* Ab hier mehr Platz (Tablet) — der Container bekommt spürbaren
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<td>{{.Aufbewahrung}}</td>
|
<td>{{.Aufbewahrung}}</td>
|
||||||
<td>{{.Zertifizierungen}}</td>
|
<td>{{.Zertifizierungen}}</td>
|
||||||
<td>{{.GeeigneteZwecke}}</td>
|
<td>{{.GeeigneteZwecke}}</td>
|
||||||
<td>{{.LetztePruefung}}{{if .NichtAktuell}} <span class="status">nicht aktuell geprüft</span>{{end}}</td>
|
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{.Aufbewahrung}}</td>
|
<td>{{.Aufbewahrung}}</td>
|
||||||
<td>{{.GeeigneteZwecke}}</td>
|
<td>{{.GeeigneteZwecke}}</td>
|
||||||
<td>{{.LetztePruefung}}{{if .NichtAktuell}} <span class="status">nicht aktuell geprüft</span>{{end}}</td>
|
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||||
<td>
|
<td>
|
||||||
{{if .Gesperrt}}
|
{{if .Gesperrt}}
|
||||||
<span class="status">gesperrt</span>
|
<span class="status">gesperrt</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user