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