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:
noroot
2026-08-31 12:28:57 +02:00
parent 4358dbaa0d
commit dbeaa45644
6 changed files with 64 additions and 20 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -29,7 +29,7 @@
<td>{{.Aufbewahrung}}</td>
<td>{{.Zertifizierungen}}</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>
{{end}}
</tbody>

View File

@@ -33,7 +33,7 @@
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
<td>{{.Aufbewahrung}}</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>
{{if .Gesperrt}}
<span class="status">gesperrt</span>