Compare commits

...

2 Commits

Author SHA1 Message Date
noroot
9e2c7f92ff 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.
2026-08-31 19:33:27 +02:00
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
17 changed files with 434 additions and 104 deletions

View File

@@ -628,9 +628,56 @@ außerhalb des Arbeitsverzeichnisses liegt):
`@ant-design/icons`, das ist React-spezifisch) — bewusste `@ant-design/icons`, das ist React-spezifisch) — bewusste
Vereinfachung. Angewendet auf die Startseite (`index.html`). Vereinfachung. Angewendet auf die Startseite (`index.html`).
Noch nicht auf weitere Tabellen/Seiten übertragen (Register, **Ausgerollt (2026-08-31):** Register, Nutzerverwaltung, Abteilungen,
Nutzerliste, Abteilungen, Fälle-Posteingang, Betreiber-Dashboard) — Fälle-Posteingang, Betreiber-Dashboard.
nächste Kandidaten, falls das Muster fortgesetzt werden soll.
- **Register** (`register_liste.html`) und **Fälle-Posteingang**
(`faelle_liste.html`) haben jetzt `.page-header` + Freitext-Filter
(`q`) + Paginierung. Register zusätzlich `.table-responsive` (war
vorher `.table-scroll`) mit `data-label` je `<td>`.
- **Nutzerverwaltung** (`nutzer_liste.html`) hat `.page-header` +
Freitext-Filter (E-Mail/Rolle) + Paginierung — blieb `.beitraege-liste`
(schon Karten-artig), keine `.table-responsive`-Umstellung nötig.
- **Abteilungen** (`abteilung_liste.html`) bekam nur `.page-header`
bewusst ohne Paginierung, Abteilungslisten sind typischerweise klein
(Handvoll Stammdaten pro Mandant).
- **Betreiber-Dashboard** (`betreiber_dashboard.html`) von
`.admin-kacheln` auf `.stat-cards` umgestellt (analog zu `index.html`).
Neue gemeinsame Helfer in `pagination.go`: `matchesQuery(q string,
felder ...string) bool` — eine einzige Freitext-Filter-Basis für alle
Listen-Seiten, damit nicht jede ihre eigene Such-Logik erfindet.
**Beim Ausrollen gefundener, bereits live gewesener Bug:** `.filter-bar`
direkt auf einem `<form>`-Element (statt auf einem `<div>` innerhalb
des Formulars) verlor gegen die allgemeine `form { flex-direction:
column }`-Regel — beide Selektoren haben für die Property
`flex-direction` dieselbe Spezifität, aber nur `form` setzte sie
explizit, `.filter-bar` überließ sie dem Initialwert. Ergebnis: die
Filterleiste stapelte senkrecht statt in einer Zeile. Betraf bereits
die im vorigen Schritt ausgelieferte Mandanten-Werkzeugkatalog-Seite.
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.
**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 **Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per
Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein

View File

@@ -8,8 +8,10 @@ package web
import ( import (
"net/http" "net/http"
"strings"
"github.com/netcell-it/deklarix/internal/auth" "github.com/netcell-it/deklarix/internal/auth"
"github.com/netcell-it/deklarix/internal/store"
) )
// mandantenRollen sind die Rollen, die ein Admin innerhalb des eigenen // mandantenRollen sind die Rollen, die ein Admin innerhalb des eigenen
@@ -38,6 +40,8 @@ type nutzerListeData struct {
Title string Title string
Nav navData Nav navData
Nutzer []nutzerListItem Nutzer []nutzerListItem
Suche string
Pagination pagination
} }
// handleNutzerListe zeigt alle Logins des eigenen Mandanten. // handleNutzerListe zeigt alle Logins des eigenen Mandanten.
@@ -48,8 +52,16 @@ func (s *Server) handleNutzerListe(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Nutzer konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Nutzer konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return return
} }
data := nutzerListeData{Title: "Nutzerverwaltung", Nav: navFor(r)} q := strings.TrimSpace(r.URL.Query().Get("q"))
var gefiltert []store.User
for _, u := range users { for _, u := range users {
if matchesQuery(q, u.Email, u.Role) {
gefiltert = append(gefiltert, u)
}
}
pag := paginationFromRequest(r, len(gefiltert))
data := nutzerListeData{Title: "Nutzerverwaltung", Nav: navFor(r), Suche: q, Pagination: pag}
for _, u := range paginate(gefiltert, pag) {
data.Nutzer = append(data.Nutzer, nutzerListItem{ data.Nutzer = append(data.Nutzer, nutzerListItem{
ID: u.ID, Email: u.Email, Role: u.Role, Active: u.Active, IstSelbst: u.ID == self.ID, ID: u.ID, Email: u.Email, Role: u.Role, Active: u.Active, IstSelbst: u.ID == self.ID,
}) })

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"github.com/netcell-it/deklarix/internal/rules" "github.com/netcell-it/deklarix/internal/rules"
"github.com/netcell-it/deklarix/internal/store" "github.com/netcell-it/deklarix/internal/store"
@@ -337,6 +338,8 @@ type antragListData struct {
Title string Title string
Nav navData Nav navData
Antraege []antragListItem Antraege []antragListItem
Suche string
Pagination pagination
} }
// handleAntragList zeigt die eigenen Anträge (Ebene 2 — "Eigene // 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) http.Error(w, "Anträge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return 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 { 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{ data.Antraege = append(data.Antraege, antragListItem{
ID: a.ID, Titel: a.Titel, Status: a.Status, Haeufigkeit: a.Haeufigkeit, ID: a.ID, Titel: a.Titel, Status: a.Status, Haeufigkeit: a.Haeufigkeit,
CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"), CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"),

View File

@@ -2,9 +2,11 @@ package web
import ( import (
"net/http" "net/http"
"strings"
"time" "time"
"github.com/netcell-it/deklarix/internal/auth" "github.com/netcell-it/deklarix/internal/auth"
"github.com/netcell-it/deklarix/internal/store"
) )
type betreiberDashboardData struct { type betreiberDashboardData struct {
@@ -55,6 +57,8 @@ type betreiberAccountListData struct {
Title string Title string
Nav navData Nav navData
Accounts []betreiberAccountListItem Accounts []betreiberAccountListItem
Suche string
Pagination pagination
} }
// handleBetreiberAccountList listet alle Mandanten der Plattform. // 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) http.Error(w, "Accounts konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return return
} }
data := betreiberAccountListData{Title: "Accounts", Nav: navFor(r)} q := strings.TrimSpace(r.URL.Query().Get("q"))
var gefiltert []store.Account
for _, a := range accounts { 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{ data.Accounts = append(data.Accounts, betreiberAccountListItem{
ID: a.ID, Name: a.Name, CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"), ID: a.ID, Name: a.Name, CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"),
}) })
@@ -221,17 +233,34 @@ type betreiberAuditLogData struct {
Title string Title string
Nav navData Nav navData
Entries []betreiberAuditEntryView 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. // handleBetreiberAuditLog zeigt das plattformweite Audit-Log.
func (s *Server) handleBetreiberAuditLog(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
http.Error(w, "Audit-Log konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Audit-Log konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return 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 { 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{ data.Entries = append(data.Entries, betreiberAuditEntryView{
CreatedAt: e.CreatedAt.Format("02.01.2006 15:04:05"), Action: e.Action, CreatedAt: e.CreatedAt.Format("02.01.2006 15:04:05"), Action: e.Action,
TargetType: e.TargetType, TargetID: e.TargetID, Details: e.Details, TargetType: e.TargetType, TargetID: e.TargetID, Details: e.Details,

View File

@@ -7,6 +7,7 @@ package web
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/netcell-it/deklarix/internal/store" "github.com/netcell-it/deklarix/internal/store"
@@ -23,6 +24,8 @@ type fallListData struct {
Title string Title string
Nav navData Nav navData
Faelle []fallListItem Faelle []fallListItem
Suche string
Pagination pagination
} }
// handleFaelleListe zeigt den Posteingang: alle noch offenen (Status // handleFaelleListe zeigt den Posteingang: alle noch offenen (Status
@@ -34,16 +37,25 @@ func (s *Server) handleFaelleListe(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Anträge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Anträge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return return
} }
data := fallListData{Title: "Posteingang", Nav: navFor(r)} q := strings.TrimSpace(r.URL.Query().Get("q"))
var offene []fallListItem
for _, a := range antraege { for _, a := range antraege {
if a.Status != "eingereicht" { if a.Status != "eingereicht" {
continue continue
} }
data.Faelle = append(data.Faelle, fallListItem{ if !matchesQuery(q, a.Titel, a.Haeufigkeit) {
continue
}
offene = append(offene, fallListItem{
ID: a.ID, Titel: a.Titel, Haeufigkeit: a.Haeufigkeit, ID: a.ID, Titel: a.Titel, Haeufigkeit: a.Haeufigkeit,
CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"), CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"),
}) })
} }
pag := paginationFromRequest(r, len(offene))
data := fallListData{
Title: "Posteingang", Nav: navFor(r),
Faelle: paginate(offene, pag), Suche: q, Pagination: pag,
}
if err := s.templates.ExecuteTemplate(w, "faelle-liste", data); err != nil { if err := s.templates.ExecuteTemplate(w, "faelle-liste", data); err != nil {
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
} }

View File

@@ -8,6 +8,7 @@ package web
import ( import (
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
const defaultPerPage = 20 const defaultPerPage = 20
@@ -65,6 +66,25 @@ func (p pagination) bounds() (start, end int) {
return 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 // paginate schneidet den Ausschnitt für die aktuelle Seite aus einer
// bereits gefilterten/sortierten Liste heraus — generisch, damit jede // bereits gefilterten/sortierten Liste heraus — generisch, damit jede
// Listen-Seite (Werkzeuge, Register, ...) dieselbe Funktion nutzt statt // Listen-Seite (Werkzeuge, Register, ...) dieselbe Funktion nutzt statt

View File

@@ -30,6 +30,8 @@ type registerListeData struct {
Title string Title string
Nav navData Nav navData
Eintraege []registerEintragView Eintraege []registerEintragView
Suche string
Pagination pagination
} }
func (s *Server) registerEintraegeFuerAccount(r *http.Request) ([]registerEintragView, error) { func (s *Server) registerEintraegeFuerAccount(r *http.Request) ([]registerEintragView, error) {
@@ -61,7 +63,18 @@ func (s *Server) handleRegisterListe(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Register konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Register konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return return
} }
data := registerListeData{Title: "Register", Nav: navFor(r), Eintraege: views} q := strings.TrimSpace(r.URL.Query().Get("q"))
var gefiltert []registerEintragView
for _, e := range views {
if matchesQuery(q, e.Zweck, e.Abteilung, e.Werkzeug, e.Verantwortlicher) {
gefiltert = append(gefiltert, e)
}
}
pag := paginationFromRequest(r, len(gefiltert))
data := registerListeData{
Title: "Register", Nav: navFor(r), Eintraege: paginate(gefiltert, pag),
Suche: q, Pagination: pag,
}
if err := s.templates.ExecuteTemplate(w, "register-liste", data); err != nil { if err := s.templates.ExecuteTemplate(w, "register-liste", data); err != nil {
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
} }

View File

@@ -755,6 +755,12 @@ tbody tr:hover {
pagination.go. */ pagination.go. */
.filter-bar { .filter-bar {
display: flex; display: flex;
/* explizit setzen, nicht dem initial-value überlassen: .filter-bar
landet manchmal direkt auf einem <form>, und "form { flex-direction:
column }" (oben) hat dieselbe Spezifität für DIESE Property, wenn
.filter-bar sie nicht selbst deklariert — das Formular würde dann
senkrecht stapeln statt in einer Zeile. */
flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 8px; gap: 8px;

View File

@@ -4,8 +4,12 @@
<body> <body>
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<div class="page-header">
<div>
<h1>Abteilungen</h1> <h1>Abteilungen</h1>
<p class="hinweis">Stammdaten für den Fragebogen (Feld "Abteilung").</p> <p class="hinweis">Stammdaten für den Fragebogen (Feld "Abteilung").</p>
</div>
</div>
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}} {{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
{{if not .Abteilungen}} {{if not .Abteilungen}}
<p class="hinweis">Noch keine Abteilungen.</p> <p class="hinweis">Noch keine Abteilungen.</p>

View File

@@ -4,9 +4,22 @@
<body> <body>
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<div class="page-header">
<div>
<h1>Meine Anträge</h1> <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}} {{if not .Antraege}}
<p class="hinweis">Noch keine Anträge gestellt.</p> <p class="hinweis">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine Anträge gestellt.{{end}}</p>
{{else}} {{else}}
<ul class="beitraege-liste"> <ul class="beitraege-liste">
{{range .Antraege}} {{range .Antraege}}
@@ -18,8 +31,15 @@
</li> </li>
{{end}} {{end}}
</ul> </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}} {{end}}
<p><a href="/antraege/neu">Neuen Antrag stellen</a></p> </form>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -5,9 +5,20 @@
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<p><a href="/betreiber">&larr; Plattform</a></p> <p><a href="/betreiber">&larr; Plattform</a></p>
<div class="page-header">
<div>
<h1>Accounts</h1> <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}} {{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}} {{else}}
<ul class="beitraege-liste"> <ul class="beitraege-liste">
{{range .Accounts}} {{range .Accounts}}
@@ -18,8 +29,26 @@
</li> </li>
{{end}} {{end}}
</ul> </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}} {{end}}
<p><a href="/betreiber/accounts/neu">Firma anlegen</a></p>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -5,9 +5,19 @@
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<p><a href="/betreiber">&larr; Plattform</a></p> <p><a href="/betreiber">&larr; Plattform</a></p>
<div class="page-header">
<div>
<h1>Audit-Log</h1> <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}} {{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}} {{else}}
<ul class="beteiligte"> <ul class="beteiligte">
{{range .Entries}} {{range .Entries}}
@@ -17,6 +27,25 @@
</li> </li>
{{end}} {{end}}
</ul> </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}} {{end}}
</div> </div>
</body> </body>

View File

@@ -5,10 +5,25 @@
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<h1>Plattform</h1> <h1>Plattform</h1>
<ul class="admin-kacheln"> <ul class="stat-cards">
<li><a href="/betreiber/accounts">Accounts <span class="status">{{.AccountCount}}</span></a></li> <li>
<li><a href="/betreiber/werkzeuge">Werkzeugkatalog <span class="status">{{.WerkzeugCount}}</span></a></li> <a class="stat-card" href="/betreiber/accounts">
<li><a href="/betreiber/audit-log">Audit-Log <span class="status">{{.RecentAuditCount}} neueste</span></a></li> <span class="stat-card-value">{{.AccountCount}}</span>
<span class="stat-card-label">Accounts</span>
</a>
</li>
<li>
<a class="stat-card" href="/betreiber/werkzeuge">
<span class="stat-card-value">{{.WerkzeugCount}}</span>
<span class="stat-card-label">Werkzeugkatalog</span>
</a>
</li>
<li>
<a class="stat-card" href="/betreiber/audit-log">
<span class="stat-card-value">{{.RecentAuditCount}}</span>
<span class="stat-card-label">Audit-Log (neueste)</span>
</a>
</li>
</ul> </ul>
</div> </div>
</body> </body>

View File

@@ -4,9 +4,21 @@
<body> <body>
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<div class="page-header">
<div>
<h1>Posteingang</h1> <h1>Posteingang</h1>
<p class="hinweis">Alle noch offenen Anträge deines Mandanten.</p>
</div>
</div>
<form method="get">
<div class="filter-bar">
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (Titel, Häufigkeit)">
<button type="submit">Filtern</button>
{{if .Suche}}<a href="/faelle">Zurücksetzen</a>{{end}}
</div>
{{if not .Faelle}} {{if not .Faelle}}
<p class="keine-findings">Keine offenen Anträge — alles bearbeitet.</p> <p class="keine-findings">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Keine offenen Anträge — alles bearbeitet.{{end}}</p>
{{else}} {{else}}
<ul class="beitraege-liste"> <ul class="beitraege-liste">
{{range .Faelle}} {{range .Faelle}}
@@ -17,7 +29,15 @@
</li> </li>
{{end}} {{end}}
</ul> </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}} {{end}}
</form>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -4,7 +4,21 @@
<body> <body>
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<div class="page-header">
<div>
<h1>Nutzerverwaltung</h1> <h1>Nutzerverwaltung</h1>
<p class="hinweis">Logins deines Mandanten — ein Login wird deaktiviert statt gelöscht.</p>
</div>
<a class="page-header-action" href="/verwaltung/nutzer/neu">Nutzer anlegen</a>
</div>
<form method="get" class="filter-bar">
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (E-Mail, Rolle)">
<button type="submit">Filtern</button>
{{if .Suche}}<a href="/verwaltung/nutzer">Zurücksetzen</a>{{end}}
</form>
{{if not .Nutzer}}
<p class="hinweis">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine weiteren Nutzer.{{end}}</p>
{{else}}
<ul class="beitraege-liste"> <ul class="beitraege-liste">
{{range .Nutzer}} {{range .Nutzer}}
<li> <li>
@@ -25,7 +39,27 @@
</li> </li>
{{end}} {{end}}
</ul> </ul>
<p><a href="/verwaltung/nutzer/neu">Nutzer anlegen</a> · <a href="/verwaltung/nutzer/import">Per CSV importieren</a></p> <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="/verwaltung/nutzer/import">Per CSV importieren</a></p>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -4,16 +4,27 @@
<body> <body>
{{template "nav" .Nav}} {{template "nav" .Nav}}
<div class="page"> <div class="page">
<div class="page-header">
<div>
<h1>Register</h1> <h1>Register</h1>
<p class="hinweis"> <p class="hinweis">
Jede Genehmigung erzeugt automatisch einen Registereintrag — Jede Genehmigung erzeugt automatisch einen Registereintrag —
abgelehnte Anträge und Rückfragen stehen nicht im Register. abgelehnte Anträge und Rückfragen stehen nicht im Register.
</p> </p>
<p><a href="/registereintraege/export.csv">CSV-Export</a> · <a href="/registereintraege/export.pdf">PDF-Export</a></p> </div>
<span class="hinweis"><a href="/registereintraege/export.csv">CSV-Export</a> · <a href="/registereintraege/export.pdf">PDF-Export</a></span>
</div>
<form method="get">
<div class="filter-bar">
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (Zweck, Abteilung, Werkzeug, Verantwortliche/r)">
<button type="submit">Filtern</button>
{{if .Suche}}<a href="/registereintraege">Zurücksetzen</a>{{end}}
</div>
{{if not .Eintraege}} {{if not .Eintraege}}
<p class="hinweis">Noch keine Genehmigungen.</p> <p class="hinweis">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine Genehmigungen.{{end}}</p>
{{else}} {{else}}
<div class="table-scroll"> <div class="table-responsive">
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -25,15 +36,29 @@
<tbody> <tbody>
{{range .Eintraege}} {{range .Eintraege}}
<tr> <tr>
<td>{{.Zweck}}</td><td>{{.Abteilung}}</td><td>{{.Werkzeug}}</td><td>{{.Datenklasse}}</td> <td data-label="Zweck">{{.Zweck}}</td>
<td>{{.Einstufung}}</td><td>{{.Auflagen}}</td><td>{{.Verantwortlicher}}</td> <td data-label="Abteilung">{{.Abteilung}}</td>
<td>{{.EntschiedenAm}}</td><td>{{.GueltigBis}}</td> <td data-label="Werkzeug">{{.Werkzeug}}</td>
<td data-label="Datenklasse">{{.Datenklasse}}</td>
<td data-label="Einstufung">{{.Einstufung}}</td>
<td data-label="Auflagen">{{.Auflagen}}</td>
<td data-label="Verantwortliche/r">{{.Verantwortlicher}}</td>
<td data-label="Entschieden am">{{.EntschiedenAm}}</td>
<td data-label="Gültig bis">{{.GueltigBis}}</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
</div> </div>
<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}} {{end}}
</form>
</div> </div>
</body> </body>
</html> </html>

View File

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