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).
76 lines
2.2 KiB
Go
76 lines
2.2 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"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// 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]
|
|
}
|