Files
deklarix/internal/web/pagination.go
noroot 4c60603055 feat: PageHeader/Filter/Paginierung auf Register, Nutzerverwaltung, Abteilungen, Posteingang, Betreiber-Dashboard ausgerollt
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.
2026-08-31 16:12:41 +02:00

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]
}