feat: Filter und Paginierung für den Werkzeugkatalog, Claude Code als Katalogeintrag

Beide Katalog-Ansichten (Betreiber + Mandant) haben jetzt eine
Filterleiste (Freitext über Name/Anbieter/Verarbeitungsländer,
Zweck-Dropdown) und serverseitige Paginierung (20/Seite) über ein
GET-Formular, kein JavaScript. pagination.go ist generisch gehalten
(Go-Generics) für Wiederverwendung in weiteren Listen.

Claude Code als eigener Katalogeintrag ergänzt (nicht identisch mit
Claude for Work: läuft lokal, hat Dateizugriff, lokale Sitzungs-
protokolle mit eigenem Aufbewahrungsrisiko).
This commit is contained in:
noroot
2026-08-31 15:09:23 +02:00
parent 08c7721165
commit 3aecbb446c
8 changed files with 390 additions and 44 deletions

View File

@@ -14,6 +14,51 @@ import (
"github.com/netcell-it/deklarix/internal/store"
)
// werkzeugFilter bündelt die Filterkriterien für die Katalog-Listen
// (Betreiber wie Mandant) — ein einziges GET-Formular setzt "q"
// (Freitext über Name/Anbieter/Länder) und optional "zweck".
type werkzeugFilter struct {
Suche string
Zweck string
}
func werkzeugFilterFromRequest(r *http.Request) werkzeugFilter {
return werkzeugFilter{
Suche: strings.TrimSpace(r.URL.Query().Get("q")),
Zweck: r.URL.Query().Get("zweck"),
}
}
// Aktiv meldet, ob mindestens ein Filter gesetzt ist — steuert, ob ein
// "Zurücksetzen"-Link angezeigt wird. Exportiert, weil html/template
// nur exportierte Methoden per Reflection aufrufen kann.
func (f werkzeugFilter) Aktiv() bool {
return f.Suche != "" || f.Zweck != ""
}
func (f werkzeugFilter) matches(wz store.Werkzeug) bool {
if f.Zweck != "" {
gefunden := false
for _, z := range wz.GeeigneteZwecke {
if z == f.Zweck {
gefunden = true
break
}
}
if !gefunden {
return false
}
}
if f.Suche != "" {
q := strings.ToLower(f.Suche)
heuhaufen := strings.ToLower(wz.Name + " " + wz.Anbieter + " " + strings.Join(wz.Verarbeitungslaender, " "))
if !strings.Contains(heuhaufen, q) {
return false
}
}
return true
}
// istGueltigesLand validiert gegen weltLaenderliste (laenderliste.go)
// — dieselbe kontrollierte-Vokabular-Logik wie bei istGueltigerZweck,
// nur für Verarbeitungsländer statt Zwecke.
@@ -92,9 +137,12 @@ func formatAufbewahrung(tage *int) string {
}
type werkzeugListeData struct {
Title string
Nav navData
Werkzeuge []werkzeugListItem
Title string
Nav navData
Werkzeuge []werkzeugListItem
Filter werkzeugFilter
ZweckeOptionen []string
Pagination pagination
}
// werkzeugPruefungVeraltetNach ist die Schwelle, ab der ein
@@ -132,8 +180,21 @@ func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Req
http.Error(w, "Werkzeugkatalog konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
data := werkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
filter := werkzeugFilterFromRequest(r)
var gefiltert []store.Werkzeug
for _, wz := range werkzeuge {
if filter.matches(wz) {
gefiltert = append(gefiltert, wz)
}
}
pag := paginationFromRequest(r, len(gefiltert))
seite := paginate(gefiltert, pag)
data := werkzeugListeData{
Title: "Werkzeugkatalog", Nav: navFor(r),
Filter: filter, ZweckeOptionen: gueltigeZwecke, Pagination: pag,
}
for _, wz := range seite {
label, css := pruefStatus(wz.LetztePruefung)
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungslaender: strings.Join(wz.Verarbeitungslaender, ", "),