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 <form> 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.
96 lines
2.7 KiB
Go
96 lines
2.7 KiB
Go
// pagination.go — wiederverwendbare serverseitige Paginierung für
|
|
// Listen-Seiten (Werkzeugkatalog, Register, ...). Bewusst ohne
|
|
// JavaScript: Seiten-/Filterwechsel läuft über ein einziges GET-Formular,
|
|
// das Query-Parameter (q, zweck, page, ...) setzt — siehe CLAUDE.md,
|
|
// Frontend.
|
|
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const defaultPerPage = 20
|
|
|
|
// pagination beschreibt die aktuelle Seite einer Liste. Templates lesen
|
|
// daraus Seitenzahl, Gesamtzahl und ob Vor/Zurück-Buttons sichtbar sein
|
|
// sollen.
|
|
type pagination struct {
|
|
Page int
|
|
PerPage int
|
|
Total int
|
|
TotalPages int
|
|
HasPrev bool
|
|
HasNext bool
|
|
PrevPage int
|
|
NextPage int
|
|
}
|
|
|
|
// paginationFromRequest liest "page" aus der Query (Standard: 1, nie
|
|
// kleiner 1, nie größer als die letzte tatsächlich vorhandene Seite).
|
|
// per_page ist bewusst nicht vom Nutzer einstellbar (kein Query-Param
|
|
// dafür) — eine feste Seitengröße hält die Filter-URLs einfach und
|
|
// verhindert versehentlich riesige Anfragen.
|
|
func paginationFromRequest(r *http.Request, total int) pagination {
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
perPage := defaultPerPage
|
|
totalPages := (total + perPage - 1) / perPage
|
|
if totalPages < 1 {
|
|
totalPages = 1
|
|
}
|
|
if page > totalPages {
|
|
page = totalPages
|
|
}
|
|
return pagination{
|
|
Page: page, PerPage: perPage, Total: total, TotalPages: totalPages,
|
|
HasPrev: page > 1, HasNext: page < totalPages,
|
|
PrevPage: page - 1, NextPage: page + 1,
|
|
}
|
|
}
|
|
|
|
// bounds liefert den Start-/End-Index für die aktuelle Seite innerhalb
|
|
// der (bereits gefilterten) Gesamtliste.
|
|
func (p pagination) bounds() (start, end int) {
|
|
start = (p.Page - 1) * p.PerPage
|
|
end = start + p.PerPage
|
|
if end > p.Total {
|
|
end = p.Total
|
|
}
|
|
if start > end {
|
|
start = end
|
|
}
|
|
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
|
|
// eigene Slice-Arithmetik zu wiederholen.
|
|
func paginate[T any](items []T, p pagination) []T {
|
|
start, end := p.bounds()
|
|
return items[start:end]
|
|
}
|