From 4c606030551f7494666295508d468ede86d0c98e Mon Sep 17 00:00:00 2001 From: noroot Date: Mon, 31 Aug 2026 16:12:41 +0200 Subject: [PATCH] feat: PageHeader/Filter/Paginierung auf Register, Nutzerverwaltung, Abteilungen, Posteingang, Betreiber-Dashboard ausgerollt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register und Fälle-Posteingang bekommen .page-header, Freitext-Filter und Paginierung; Register zusätzlich .table-responsive statt .table-scroll. Nutzerverwaltung bekommt Filter+Paginierung (bleibt Karten-Liste). Abteilungen nur .page-header (bewusst ohne Paginierung, typischerweise kleine Listen). Betreiber-Dashboard von .admin-kacheln auf .stat-cards umgestellt. Neuer gemeinsamer Helfer matchesQuery() in pagination.go für die Freitext-Filter aller Listen-Seiten. Dabei einen bereits live gewesenen CSS-Bug gefunden und behoben: .filter-bar direkt auf einem
verlor gegen "form { flex-direction: column }" (gleiche Spezifität, nur form setzte die Property), wodurch die Filterleiste senkrecht statt waagerecht stapelte — betraf die im letzten Schritt ausgelieferte Mandanten-Werkzeugkatalog-Seite. Jetzt robust via explizitem flex-direction: row in .filter-bar selbst. --- CLAUDE.md | 39 ++++++++- internal/web/admin_handlers.go | 20 ++++- internal/web/fachebene_handlers.go | 22 +++-- internal/web/pagination.go | 20 +++++ internal/web/register_handlers.go | 21 ++++- internal/web/static/app.css | 6 ++ internal/web/templates/abteilung_liste.html | 8 +- .../web/templates/betreiber_dashboard.html | 23 ++++- internal/web/templates/faelle_liste.html | 46 +++++++--- internal/web/templates/nutzer_liste.html | 38 ++++++++- internal/web/templates/register_liste.html | 83 ++++++++++++------- 11 files changed, 260 insertions(+), 66 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 180ac0c..0d56f12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -628,9 +628,42 @@ außerhalb des Arbeitsverzeichnisses liegt): `@ant-design/icons`, das ist React-spezifisch) — bewusste Vereinfachung. Angewendet auf die Startseite (`index.html`). -Noch nicht auf weitere Tabellen/Seiten übertragen (Register, -Nutzerliste, Abteilungen, Fälle-Posteingang, Betreiber-Dashboard) — -nächste Kandidaten, falls das Muster fortgesetzt werden soll. +**Ausgerollt (2026-08-31):** Register, Nutzerverwaltung, Abteilungen, +Fälle-Posteingang, Betreiber-Dashboard. + +- **Register** (`register_liste.html`) und **Fälle-Posteingang** + (`faelle_liste.html`) haben jetzt `.page-header` + Freitext-Filter + (`q`) + Paginierung. Register zusätzlich `.table-responsive` (war + vorher `.table-scroll`) mit `data-label` je ``. +- **Nutzerverwaltung** (`nutzer_liste.html`) hat `.page-header` + + Freitext-Filter (E-Mail/Rolle) + Paginierung — blieb `.beitraege-liste` + (schon Karten-artig), keine `.table-responsive`-Umstellung nötig. +- **Abteilungen** (`abteilung_liste.html`) bekam nur `.page-header` — + bewusst ohne Paginierung, Abteilungslisten sind typischerweise klein + (Handvoll Stammdaten pro Mandant). +- **Betreiber-Dashboard** (`betreiber_dashboard.html`) von + `.admin-kacheln` auf `.stat-cards` umgestellt (analog zu `index.html`). + +Neue gemeinsame Helfer in `pagination.go`: `matchesQuery(q string, +felder ...string) bool` — eine einzige Freitext-Filter-Basis für alle +Listen-Seiten, damit nicht jede ihre eigene Such-Logik erfindet. + +**Beim Ausrollen gefundener, bereits live gewesener Bug:** `.filter-bar` +direkt auf einem ``-Element (statt auf einem `
` innerhalb +des Formulars) verlor gegen die allgemeine `form { flex-direction: +column }`-Regel — beide Selektoren haben für die Property +`flex-direction` dieselbe Spezifität, aber nur `form` setzte sie +explizit, `.filter-bar` überließ sie dem Initialwert. Ergebnis: die +Filterleiste stapelte senkrecht statt in einer Zeile. Betraf bereits +die im vorigen Schritt ausgelieferte Mandanten-Werkzeugkatalog-Seite. +Behoben durch explizites `flex-direction: row` in `.filter-bar` selbst +— jetzt unabhängig davon robust, ob die Klasse auf einem `` oder +einem `
` sitzt. + +Noch nicht übertragen: Betreiber-Accounts, Betreiber-Audit-Log, +Wiedervorlage, "Meine Anträge" — nächste Kandidaten, falls das Muster +fortgesetzt werden soll (Audit-Log wird langfristig am ehesten groß +genug für Paginierung). **Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein diff --git a/internal/web/admin_handlers.go b/internal/web/admin_handlers.go index 776fd9b..c70185e 100644 --- a/internal/web/admin_handlers.go +++ b/internal/web/admin_handlers.go @@ -8,8 +8,10 @@ package web import ( "net/http" + "strings" "github.com/netcell-it/deklarix/internal/auth" + "github.com/netcell-it/deklarix/internal/store" ) // mandantenRollen sind die Rollen, die ein Admin innerhalb des eigenen @@ -35,9 +37,11 @@ type nutzerListItem struct { } type nutzerListeData struct { - Title string - Nav navData - Nutzer []nutzerListItem + Title string + Nav navData + Nutzer []nutzerListItem + Suche string + Pagination pagination } // handleNutzerListe zeigt alle Logins des eigenen Mandanten. @@ -48,8 +52,16 @@ func (s *Server) handleNutzerListe(w http.ResponseWriter, r *http.Request) { http.Error(w, "Nutzer konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } - data := nutzerListeData{Title: "Nutzerverwaltung", Nav: navFor(r)} + q := strings.TrimSpace(r.URL.Query().Get("q")) + var gefiltert []store.User for _, u := range users { + if matchesQuery(q, u.Email, u.Role) { + gefiltert = append(gefiltert, u) + } + } + pag := paginationFromRequest(r, len(gefiltert)) + data := nutzerListeData{Title: "Nutzerverwaltung", Nav: navFor(r), Suche: q, Pagination: pag} + for _, u := range paginate(gefiltert, pag) { data.Nutzer = append(data.Nutzer, nutzerListItem{ ID: u.ID, Email: u.Email, Role: u.Role, Active: u.Active, IstSelbst: u.ID == self.ID, }) diff --git a/internal/web/fachebene_handlers.go b/internal/web/fachebene_handlers.go index d9e4c9c..79d9bd3 100644 --- a/internal/web/fachebene_handlers.go +++ b/internal/web/fachebene_handlers.go @@ -7,6 +7,7 @@ package web import ( "encoding/json" "net/http" + "strings" "time" "github.com/netcell-it/deklarix/internal/store" @@ -20,9 +21,11 @@ type fallListItem struct { } type fallListData struct { - Title string - Nav navData - Faelle []fallListItem + Title string + Nav navData + Faelle []fallListItem + Suche string + Pagination pagination } // handleFaelleListe zeigt den Posteingang: alle noch offenen (Status @@ -34,16 +37,25 @@ func (s *Server) handleFaelleListe(w http.ResponseWriter, r *http.Request) { http.Error(w, "Anträge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } - data := fallListData{Title: "Posteingang", Nav: navFor(r)} + q := strings.TrimSpace(r.URL.Query().Get("q")) + var offene []fallListItem for _, a := range antraege { if a.Status != "eingereicht" { continue } - data.Faelle = append(data.Faelle, fallListItem{ + if !matchesQuery(q, a.Titel, a.Haeufigkeit) { + continue + } + offene = append(offene, fallListItem{ ID: a.ID, Titel: a.Titel, Haeufigkeit: a.Haeufigkeit, CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"), }) } + pag := paginationFromRequest(r, len(offene)) + data := fallListData{ + Title: "Posteingang", Nav: navFor(r), + Faelle: paginate(offene, pag), Suche: q, Pagination: pag, + } if err := s.templates.ExecuteTemplate(w, "faelle-liste", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } diff --git a/internal/web/pagination.go b/internal/web/pagination.go index 246ceab..d25a0ed 100644 --- a/internal/web/pagination.go +++ b/internal/web/pagination.go @@ -8,6 +8,7 @@ package web import ( "net/http" "strconv" + "strings" ) const defaultPerPage = 20 @@ -65,6 +66,25 @@ func (p pagination) bounds() (start, end int) { return start, end } +// matchesQuery prüft, ob q (Freitext-Suche, ohne Beachtung von +// Groß-/Kleinschreibung) in mindestens einem der übergebenen Felder +// vorkommt — leeres q trifft immer zu. Gemeinsame Basis für die +// Freitext-Filter mehrerer Listen-Seiten (Register, Nutzerverwaltung, +// Posteingang, ...), damit nicht jede Liste ihre eigene Such-Logik +// erfindet. +func matchesQuery(q string, felder ...string) bool { + if q == "" { + return true + } + q = strings.ToLower(q) + for _, f := range felder { + if strings.Contains(strings.ToLower(f), q) { + return true + } + } + return false +} + // paginate schneidet den Ausschnitt für die aktuelle Seite aus einer // bereits gefilterten/sortierten Liste heraus — generisch, damit jede // Listen-Seite (Werkzeuge, Register, ...) dieselbe Funktion nutzt statt diff --git a/internal/web/register_handlers.go b/internal/web/register_handlers.go index 22b4eb6..65a9c27 100644 --- a/internal/web/register_handlers.go +++ b/internal/web/register_handlers.go @@ -27,9 +27,11 @@ type registerEintragView struct { } type registerListeData struct { - Title string - Nav navData - Eintraege []registerEintragView + Title string + Nav navData + Eintraege []registerEintragView + Suche string + Pagination pagination } func (s *Server) registerEintraegeFuerAccount(r *http.Request) ([]registerEintragView, error) { @@ -61,7 +63,18 @@ func (s *Server) handleRegisterListe(w http.ResponseWriter, r *http.Request) { http.Error(w, "Register konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } - data := registerListeData{Title: "Register", Nav: navFor(r), Eintraege: views} + q := strings.TrimSpace(r.URL.Query().Get("q")) + var gefiltert []registerEintragView + for _, e := range views { + if matchesQuery(q, e.Zweck, e.Abteilung, e.Werkzeug, e.Verantwortlicher) { + gefiltert = append(gefiltert, e) + } + } + pag := paginationFromRequest(r, len(gefiltert)) + data := registerListeData{ + Title: "Register", Nav: navFor(r), Eintraege: paginate(gefiltert, pag), + Suche: q, Pagination: pag, + } if err := s.templates.ExecuteTemplate(w, "register-liste", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 5d467d1..31fe75d 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -755,6 +755,12 @@ tbody tr:hover { pagination.go. */ .filter-bar { display: flex; + /* explizit setzen, nicht dem initial-value überlassen: .filter-bar + landet manchmal direkt auf einem , und "form { flex-direction: + column }" (oben) hat dieselbe Spezifität für DIESE Property, wenn + .filter-bar sie nicht selbst deklariert — das Formular würde dann + senkrecht stapeln statt in einer Zeile. */ + flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; diff --git a/internal/web/templates/abteilung_liste.html b/internal/web/templates/abteilung_liste.html index 6747d64..e37ba96 100644 --- a/internal/web/templates/abteilung_liste.html +++ b/internal/web/templates/abteilung_liste.html @@ -4,8 +4,12 @@ {{template "nav" .Nav}}
-

Abteilungen

-

Stammdaten für den Fragebogen (Feld "Abteilung").

+ {{if .Error}}

{{.Error}}

{{end}} {{if not .Abteilungen}}

Noch keine Abteilungen.

diff --git a/internal/web/templates/betreiber_dashboard.html b/internal/web/templates/betreiber_dashboard.html index 41f281c..d4d8db2 100644 --- a/internal/web/templates/betreiber_dashboard.html +++ b/internal/web/templates/betreiber_dashboard.html @@ -5,10 +5,25 @@ {{template "nav" .Nav}} diff --git a/internal/web/templates/faelle_liste.html b/internal/web/templates/faelle_liste.html index 380565c..e4c637c 100644 --- a/internal/web/templates/faelle_liste.html +++ b/internal/web/templates/faelle_liste.html @@ -4,20 +4,40 @@ {{template "nav" .Nav}}
-

Posteingang

-{{if not .Faelle}} -

Keine offenen Anträge — alles bearbeitet.

-{{else}} - -{{end}} +
diff --git a/internal/web/templates/nutzer_liste.html b/internal/web/templates/nutzer_liste.html index d2bdc8d..a4a2db9 100644 --- a/internal/web/templates/nutzer_liste.html +++ b/internal/web/templates/nutzer_liste.html @@ -4,7 +4,21 @@ {{template "nav" .Nav}}
-

Nutzerverwaltung

+ +
+ + + {{if .Suche}}Zurücksetzen{{end}} +
+{{if not .Nutzer}} +

{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine weiteren Nutzer.{{end}}

+{{else}}
    {{range .Nutzer}}
  • @@ -25,7 +39,27 @@
  • {{end}}
-

Nutzer anlegen · Per CSV importieren

+ +{{end}} +

Per CSV importieren

diff --git a/internal/web/templates/register_liste.html b/internal/web/templates/register_liste.html index e5be1bc..ac3abc3 100644 --- a/internal/web/templates/register_liste.html +++ b/internal/web/templates/register_liste.html @@ -4,36 +4,61 @@ {{template "nav" .Nav}}
-

Register

-

- Jede Genehmigung erzeugt automatisch einen Registereintrag — - abgelehnte Anträge und Rückfragen stehen nicht im Register. -

-

CSV-Export · PDF-Export

-{{if not .Eintraege}} -

Noch keine Genehmigungen.

-{{else}} -
- - - - - - - - - - {{range .Eintraege}} - - - - - - {{end}} - -
ZweckAbteilungWerkzeugDatenklasseEinstufungAuflagenVerantwortliche/rEntschieden amGültig bis
{{.Zweck}}{{.Abteilung}}{{.Werkzeug}}{{.Datenklasse}}{{.Einstufung}}{{.Auflagen}}{{.Verantwortlicher}}{{.EntschiedenAm}}{{.GueltigBis}}
+ -{{end}} +
+
+ + + {{if .Suche}}Zurücksetzen{{end}} +
+ + {{if not .Eintraege}} +

{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine Genehmigungen.{{end}}

+ {{else}} +
+ + + + + + + + + + {{range .Eintraege}} + + + + + + + + + + + + {{end}} + +
ZweckAbteilungWerkzeugDatenklasseEinstufungAuflagenVerantwortliche/rEntschieden amGültig bis
{{.Zweck}}{{.Abteilung}}{{.Werkzeug}}{{.Datenklasse}}{{.Einstufung}}{{.Auflagen}}{{.Verantwortlicher}}{{.EntschiedenAm}}{{.GueltigBis}}
+
+ + {{end}} +