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:
22
CLAUDE.md
22
CLAUDE.md
@@ -660,10 +660,24 @@ Behoben durch explizites `flex-direction: row` in `.filter-bar` selbst
|
||||
— jetzt unabhängig davon robust, ob die Klasse auf einem `<form>` oder
|
||||
einem `<div>` sitzt.
|
||||
|
||||
Noch nicht übertragen: Betreiber-Accounts, Betreiber-Audit-Log,
|
||||
Wiedervorlage, "Meine Anträge" — nächste Kandidaten, falls das Muster
|
||||
fortgesetzt werden soll (Audit-Log wird langfristig am ehesten groß
|
||||
genug für Paginierung).
|
||||
**Ebenfalls ausgerollt (2026-08-31, zweite Runde):** Betreiber-Accounts
|
||||
(`.page-header` + Filter über Firmenname + Paginierung), Betreiber-
|
||||
Audit-Log (`.page-header` + Filter über Aktion/Zieltyp/Details +
|
||||
Paginierung), "Meine Anträge" (`.page-header` + Filter über Titel/
|
||||
Status/Häufigkeit + Paginierung), Wiedervorlage (nur `.page-header`,
|
||||
bewusst ohne Paginierung — natürlich begrenzt auf "braucht gerade
|
||||
Aufmerksamkeit").
|
||||
|
||||
**Bekannte Einschränkung beim Audit-Log:** `ListAuditLog` holt weiterhin
|
||||
nur die neuesten `auditLogFetchLimit` (1000) Einträge aus der DB, dann
|
||||
wird in Go gefiltert/paginiert (`auditLogFetchLimit`-Konstante in
|
||||
`betreiber_handlers.go`) — keine echte DB-seitige Offset-Paginierung.
|
||||
Für ein potenziell sehr groß werdendes, append-only Audit-Log ist das
|
||||
mittelfristig nicht die saubere Lösung (Einträge jenseits der 1000
|
||||
neuesten sind über die Such-/Paginierungs-UI nicht erreichbar), aber
|
||||
proportional zum aktuellen Datenvolumen und konsistent mit dem
|
||||
In-Memory-Paginierungsmuster der übrigen Listen. Bei echtem Bedarf:
|
||||
`store.ListAuditLog` um `offset`/echtes SQL-`LIMIT`/`OFFSET` erweitern.
|
||||
|
||||
**Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per
|
||||
Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}}">← Zurück</button>{{end}}
|
||||
{{if .Pagination.HasNext}}<button type="submit" name="page" value="{{.Pagination.NextPage}}">Weiter →</button>{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
<p><a href="/antraege/neu">Neuen Antrag stellen</a></p>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,9 +5,20 @@
|
||||
{{template "nav" .Nav}}
|
||||
<div class="page">
|
||||
<p><a href="/betreiber">← 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">← 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 →</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<p><a href="/betreiber/accounts/neu">Firma anlegen</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,9 +5,19 @@
|
||||
{{template "nav" .Nav}}
|
||||
<div class="page">
|
||||
<p><a href="/betreiber">← 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">← 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 →</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user