feat: PageHeader/Filter/Paginierung auf Betreiber-Accounts, Audit-Log, Meine Anträge, Wiedervorlage ausgerollt

Vervollständigt den Rollout aus dem vorigen Schritt: Betreiber-Accounts
und Audit-Log (Betreiber-Bereich) sowie "Meine Anträge" bekommen
.page-header, Freitext-Filter (matchesQuery) und Paginierung.
Wiedervorlage nur .page-header, bewusst ohne Paginierung (natürlich
begrenzte Liste).

Audit-Log holt weiterhin nur die neuesten 1000 Einträge aus der DB
und paginiert im Go-Code darüber (auditLogFetchLimit) - keine echte
DB-Offset-Paginierung, als bekannte Einschränkung dokumentiert.
This commit is contained in:
noroot
2026-08-31 19:33:27 +02:00
parent 4c60603055
commit 9e2c7f92ff
7 changed files with 178 additions and 42 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"net/http"
"strings"
"github.com/netcell-it/deklarix/internal/rules"
"github.com/netcell-it/deklarix/internal/store"
@@ -334,9 +335,11 @@ type antragListItem struct {
}
type antragListData struct {
Title string
Nav navData
Antraege []antragListItem
Title string
Nav navData
Antraege []antragListItem
Suche string
Pagination pagination
}
// handleAntragList zeigt die eigenen Anträge (Ebene 2 — "Eigene
@@ -351,8 +354,16 @@ func (s *Server) handleAntragList(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Anträge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
data := antragListData{Title: "Meine Anträge", Nav: navFor(r)}
q := strings.TrimSpace(r.URL.Query().Get("q"))
var gefiltert []store.Antrag
for _, a := range list {
if matchesQuery(q, a.Titel, a.Status, a.Haeufigkeit) {
gefiltert = append(gefiltert, a)
}
}
pag := paginationFromRequest(r, len(gefiltert))
data := antragListData{Title: "Meine Anträge", Nav: navFor(r), Suche: q, Pagination: pag}
for _, a := range paginate(gefiltert, pag) {
data.Antraege = append(data.Antraege, antragListItem{
ID: a.ID, Titel: a.Titel, Status: a.Status, Haeufigkeit: a.Haeufigkeit,
CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"),

View File

@@ -2,9 +2,11 @@ package web
import (
"net/http"
"strings"
"time"
"github.com/netcell-it/deklarix/internal/auth"
"github.com/netcell-it/deklarix/internal/store"
)
type betreiberDashboardData struct {
@@ -52,9 +54,11 @@ type betreiberAccountListItem struct {
}
type betreiberAccountListData struct {
Title string
Nav navData
Accounts []betreiberAccountListItem
Title string
Nav navData
Accounts []betreiberAccountListItem
Suche string
Pagination pagination
}
// handleBetreiberAccountList listet alle Mandanten der Plattform.
@@ -64,8 +68,16 @@ func (s *Server) handleBetreiberAccountList(w http.ResponseWriter, r *http.Reque
http.Error(w, "Accounts konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
data := betreiberAccountListData{Title: "Accounts", Nav: navFor(r)}
q := strings.TrimSpace(r.URL.Query().Get("q"))
var gefiltert []store.Account
for _, a := range accounts {
if matchesQuery(q, a.Name) {
gefiltert = append(gefiltert, a)
}
}
pag := paginationFromRequest(r, len(gefiltert))
data := betreiberAccountListData{Title: "Accounts", Nav: navFor(r), Suche: q, Pagination: pag}
for _, a := range paginate(gefiltert, pag) {
data.Accounts = append(data.Accounts, betreiberAccountListItem{
ID: a.ID, Name: a.Name, CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"),
})
@@ -218,20 +230,37 @@ type betreiberAuditEntryView struct {
}
type betreiberAuditLogData struct {
Title string
Nav navData
Entries []betreiberAuditEntryView
Title string
Nav navData
Entries []betreiberAuditEntryView
Suche string
Pagination pagination
}
// auditLogFetchLimit begrenzt, wie viele der neuesten Einträge aus der
// DB geholt werden, bevor im Go-Code gefiltert/paginiert wird — echte
// DB-seitige Offset-Paginierung wäre für ein potenziell sehr groß
// werdendes Audit-Log der sauberere Ansatz, ist hier aber (noch) nicht
// umgesetzt (siehe CLAUDE.md, Offene Punkte).
const auditLogFetchLimit = 1000
// handleBetreiberAuditLog zeigt das plattformweite Audit-Log.
func (s *Server) handleBetreiberAuditLog(w http.ResponseWriter, r *http.Request) {
entries, err := s.store.ListAuditLog(r.Context(), 200)
entries, err := s.store.ListAuditLog(r.Context(), auditLogFetchLimit)
if err != nil {
http.Error(w, "Audit-Log konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
data := betreiberAuditLogData{Title: "Audit-Log", Nav: navFor(r)}
q := strings.TrimSpace(r.URL.Query().Get("q"))
var gefiltert []store.AuditEntry
for _, e := range entries {
if matchesQuery(q, e.Action, e.TargetType, e.Details) {
gefiltert = append(gefiltert, e)
}
}
pag := paginationFromRequest(r, len(gefiltert))
data := betreiberAuditLogData{Title: "Audit-Log", Nav: navFor(r), Suche: q, Pagination: pag}
for _, e := range paginate(gefiltert, pag) {
data.Entries = append(data.Entries, betreiberAuditEntryView{
CreatedAt: e.CreatedAt.Format("02.01.2006 15:04:05"), Action: e.Action,
TargetType: e.TargetType, TargetID: e.TargetID, Details: e.Details,

View File

@@ -4,22 +4,42 @@
<body>
{{template "nav" .Nav}}
<div class="page">
<h1>Meine Anträge</h1>
{{if not .Antraege}}
<p class="hinweis">Noch keine Anträge gestellt.</p>
{{else}}
<ul class="beitraege-liste">
{{range .Antraege}}
<li>
<a href="/antraege/{{.ID}}">
<strong>{{.Titel}}</strong> · {{.Haeufigkeit}} · {{.CreatedAt}}
<span class="status status-{{.Status}}">{{.Status}}</span>
</a>
</li>
<div class="page-header">
<div>
<h1>Meine Anträge</h1>
<p class="hinweis">Eigene Anträge und ihr Bearbeitungsstand.</p>
</div>
<a class="page-header-action" href="/antraege/neu">Neuen Antrag stellen</a>
</div>
<form method="get">
<div class="filter-bar">
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (Titel, Status, Häufigkeit)">
<button type="submit">Filtern</button>
{{if .Suche}}<a href="/antraege">Zurücksetzen</a>{{end}}
</div>
{{if not .Antraege}}
<p class="hinweis">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine Anträge gestellt.{{end}}</p>
{{else}}
<ul class="beitraege-liste">
{{range .Antraege}}
<li>
<a href="/antraege/{{.ID}}">
<strong>{{.Titel}}</strong> · {{.Haeufigkeit}} · {{.CreatedAt}}
<span class="status status-{{.Status}}">{{.Status}}</span>
</a>
</li>
{{end}}
</ul>
<div class="pagination">
<span class="hinweis">Seite {{.Pagination.Page}} von {{.Pagination.TotalPages}} ({{.Pagination.Total}} Einträge)</span>
<span>
{{if .Pagination.HasPrev}}<button type="submit" name="page" value="{{.Pagination.PrevPage}}">&larr; Zurück</button>{{end}}
{{if .Pagination.HasNext}}<button type="submit" name="page" value="{{.Pagination.NextPage}}">Weiter &rarr;</button>{{end}}
</span>
</div>
{{end}}
</ul>
{{end}}
<p><a href="/antraege/neu">Neuen Antrag stellen</a></p>
</form>
</div>
</body>
</html>

View File

@@ -5,9 +5,20 @@
{{template "nav" .Nav}}
<div class="page">
<p><a href="/betreiber">&larr; Plattform</a></p>
<h1>Accounts</h1>
<div class="page-header">
<div>
<h1>Accounts</h1>
<p class="hinweis">Alle Mandanten der Plattform.</p>
</div>
<a class="page-header-action" href="/betreiber/accounts/neu">Firma anlegen</a>
</div>
<form method="get" class="filter-bar">
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (Firmenname)">
<button type="submit">Filtern</button>
{{if .Suche}}<a href="/betreiber/accounts">Zurücksetzen</a>{{end}}
</form>
{{if not .Accounts}}
<p class="hinweis">Noch keine Accounts.</p>
<p class="hinweis">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine Accounts.{{end}}</p>
{{else}}
<ul class="beitraege-liste">
{{range .Accounts}}
@@ -18,8 +29,26 @@
</li>
{{end}}
</ul>
<div class="pagination">
<span class="hinweis">Seite {{.Pagination.Page}} von {{.Pagination.TotalPages}} ({{.Pagination.Total}} Einträge)</span>
<span>
{{if .Pagination.HasPrev}}
<form method="get" style="display:inline">
<input type="hidden" name="q" value="{{.Suche}}">
<input type="hidden" name="page" value="{{.Pagination.PrevPage}}">
<button type="submit">&larr; Zurück</button>
</form>
{{end}}
{{if .Pagination.HasNext}}
<form method="get" style="display:inline">
<input type="hidden" name="q" value="{{.Suche}}">
<input type="hidden" name="page" value="{{.Pagination.NextPage}}">
<button type="submit">Weiter &rarr;</button>
</form>
{{end}}
</span>
</div>
{{end}}
<p><a href="/betreiber/accounts/neu">Firma anlegen</a></p>
</div>
</body>
</html>

View File

@@ -5,9 +5,19 @@
{{template "nav" .Nav}}
<div class="page">
<p><a href="/betreiber">&larr; Plattform</a></p>
<h1>Audit-Log</h1>
<div class="page-header">
<div>
<h1>Audit-Log</h1>
<p class="hinweis">Plattformweit, append-only — zeigt die neuesten Einträge.</p>
</div>
</div>
<form method="get" class="filter-bar">
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (Aktion, Zieltyp, Details)">
<button type="submit">Filtern</button>
{{if .Suche}}<a href="/betreiber/audit-log">Zurücksetzen</a>{{end}}
</form>
{{if not .Entries}}
<p class="hinweis">Noch keine Einträge.</p>
<p class="hinweis">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine Einträge.{{end}}</p>
{{else}}
<ul class="beteiligte">
{{range .Entries}}
@@ -17,6 +27,25 @@
</li>
{{end}}
</ul>
<div class="pagination">
<span class="hinweis">Seite {{.Pagination.Page}} von {{.Pagination.TotalPages}} ({{.Pagination.Total}} Einträge)</span>
<span>
{{if .Pagination.HasPrev}}
<form method="get" style="display:inline">
<input type="hidden" name="q" value="{{.Suche}}">
<input type="hidden" name="page" value="{{.Pagination.PrevPage}}">
<button type="submit">&larr; Zurück</button>
</form>
{{end}}
{{if .Pagination.HasNext}}
<form method="get" style="display:inline">
<input type="hidden" name="q" value="{{.Suche}}">
<input type="hidden" name="page" value="{{.Pagination.NextPage}}">
<button type="submit">Weiter &rarr;</button>
</form>
{{end}}
</span>
</div>
{{end}}
</div>
</body>

View File

@@ -4,11 +4,15 @@
<body>
{{template "nav" .Nav}}
<div class="page">
<h1>Wiedervorlage</h1>
<p class="hinweis">
Genehmigungen, die abgelaufen sind, bald ablaufen, oder deren
Werkzeug sich seit der Entscheidung im Katalog geändert hat.
</p>
<div class="page-header">
<div>
<h1>Wiedervorlage</h1>
<p class="hinweis">
Genehmigungen, die abgelaufen sind, bald ablaufen, oder deren
Werkzeug sich seit der Entscheidung im Katalog geändert hat.
</p>
</div>
</div>
{{if not .Eintraege}}
<p class="keine-findings">Nichts zur Wiedervorlage.</p>
{{else}}