Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e2c7f92ff | ||
|
|
4c60603055 | ||
|
|
bc99858c94 |
81
CLAUDE.md
81
CLAUDE.md
@@ -598,6 +598,87 @@ wie die Formular-Abschnitte angehoben (600/1rem statt 500/0.9375rem) —
|
|||||||
für eine einheitliche Überschriften-Hierarchie statt Zufallsgrößen je
|
für eine einheitliche Überschriften-Hierarchie statt Zufallsgrößen je
|
||||||
nach UA-Stylesheet.
|
nach UA-Stylesheet.
|
||||||
|
|
||||||
|
**enconf-Patterns übertragen (2026-08-31):** enconf (Schwesterprojekt)
|
||||||
|
ist React + Ant Design (`ProTable`/`StatCard`/`PageHeader`-Komponenten)
|
||||||
|
— nicht 1:1 übernehmbar, da Deklarix bewusst kein SPA ist, aber die
|
||||||
|
*Muster* wurden als reines CSS/HTML nachgebaut (recherchiert über die
|
||||||
|
Architect-RAG-Suche, `ac_search_code_global`, da das enconf-Repo selbst
|
||||||
|
außerhalb des Arbeitsverzeichnisses liegt):
|
||||||
|
|
||||||
|
- **`.table-responsive`** (ersetzt `.table-scroll` schrittweise) —
|
||||||
|
enconfs `ProTable.tsx` schaltet auf schmalen Screens von Tabelle auf
|
||||||
|
eine Card-Liste um (`renderMobileCard` via `Grid.useBreakpoint()`),
|
||||||
|
statt horizontal zu scrollen. Nachgebaut rein in CSS: unter 640px
|
||||||
|
werden `<tr>` zu Karten, `<td>` zu Label/Wert-Zeilen (`content:
|
||||||
|
attr(data-label)` im `::before`) — jedes `<td>` braucht dafür ein
|
||||||
|
`data-label="Spaltenname"`-Attribut. Ab 640px normale Tabelle mit
|
||||||
|
Scroll-Fade als Sicherheitsnetz. Bisher angewendet auf beide
|
||||||
|
Werkzeugkatalog-Tabellen; `.table-scroll` (nur Scroll-Fade, keine
|
||||||
|
Card-Umschaltung) bleibt für noch nicht migrierte Tabellen (Register,
|
||||||
|
Nutzer-Import-Ergebnis) bestehen.
|
||||||
|
- **`.page-header`** (Titel+Unterzeile links, primäre Aktion rechts,
|
||||||
|
stapelt auf Mobile) — ersetzt einen nackten `<h1>` mit einem Link
|
||||||
|
irgendwo unten auf der Seite. Bisher auf den Werkzeugkatalog
|
||||||
|
angewendet (Betreiber + Mandant, inkl. "Eigene Werkzeuge"-Abschnitt).
|
||||||
|
- **`.stat-cards`/`.stat-card`** — Kennzahl-Kacheln mit farbigem oberem
|
||||||
|
Akzentrand (blau normal, gelb/rot nur wenn der Wert > 0 ist — keine
|
||||||
|
Farbe ohne Grund) statt einfacher Text-Links; `.stat-card-action` für
|
||||||
|
die primäre Handlungs-Kachel (z. B. "Antrag stellen"). Kein
|
||||||
|
Icon-Set/keine Icon-Bibliothek eingeführt (enconf nutzt
|
||||||
|
`@ant-design/icons`, das ist React-spezifisch) — bewusste
|
||||||
|
Vereinfachung. Angewendet auf die Startseite (`index.html`).
|
||||||
|
|
||||||
|
**Ausgerollt (2026-08-31):** Register, Nutzerverwaltung, Abteilungen,
|
||||||
|
Fälle-Posteingang, Betreiber-Dashboard.
|
||||||
|
|
||||||
|
- **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
|
||||||
websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt`
|
websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt`
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -35,9 +37,11 @@ type nutzerListItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type nutzerListeData struct {
|
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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -334,9 +335,11 @@ type antragListItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type antragListData struct {
|
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"),
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -52,9 +54,11 @@ type betreiberAccountListItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type betreiberAccountListData struct {
|
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"),
|
||||||
})
|
})
|
||||||
@@ -218,20 +230,37 @@ type betreiberAuditEntryView struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type betreiberAuditLogData struct {
|
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,
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -20,9 +21,11 @@ type fallListItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fallListData struct {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ func TestIndexZeigtEigeneOffeneAntraege(t *testing.T) {
|
|||||||
if resp.Code != http.StatusOK {
|
if resp.Code != http.StatusOK {
|
||||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(resp.Body.String(), `href="/antraege">Meine offenen Anträge <span class="status">1</span>`) {
|
body := resp.Body.String()
|
||||||
t.Errorf("expected 1 offenen Antrag auf der Startseite, got: %s", resp.Body.String())
|
if !strings.Contains(body, `href="/antraege"`) || !strings.Contains(body, `stat-card-value">1</span>`) || !strings.Contains(body, "Meine offenen Anträge") {
|
||||||
|
t.Errorf("expected 1 offenen Antrag auf der Startseite, got: %s", body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,8 +29,9 @@ func TestIndexZeigtPosteingangNurFuerFachebene(t *testing.T) {
|
|||||||
if resp.Code != http.StatusOK {
|
if resp.Code != http.StatusOK {
|
||||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(resp.Body.String(), `href="/faelle">Posteingang <span class="status">1</span>`) {
|
body := resp.Body.String()
|
||||||
t.Errorf("expected 1 offenen Fall im Posteingang auf der Startseite, got: %s", resp.Body.String())
|
if !strings.Contains(body, `href="/faelle"`) || !strings.Contains(body, `stat-card-value">1</span>`) || !strings.Contains(body, "Posteingang") {
|
||||||
|
t.Errorf("expected 1 offenen Fall im Posteingang auf der Startseite, got: %s", body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ func TestIndexZeigtPlattformLinkFuerBetreiber(t *testing.T) {
|
|||||||
if resp.Code != http.StatusOK {
|
if resp.Code != http.StatusOK {
|
||||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(resp.Body.String(), `href="/betreiber">Zur Plattform`) {
|
if !strings.Contains(resp.Body.String(), `href="/betreiber"`) || !strings.Contains(resp.Body.String(), "Zur Plattform") {
|
||||||
t.Errorf("expected a Plattform link for betreiber, got: %s", resp.Body.String())
|
t.Errorf("expected a Plattform link for betreiber, got: %s", resp.Body.String())
|
||||||
}
|
}
|
||||||
if strings.Contains(resp.Body.String(), "Antrag stellen") {
|
if strings.Contains(resp.Body.String(), "Antrag stellen") {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ type registerEintragView struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type registerListeData struct {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -792,6 +798,183 @@ tbody tr:hover {
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Responsive Tabellen — Card-Ansicht auf dem Handy ────────────────
|
||||||
|
enconf-Pattern (ProTable.tsx): auf schmalen Screens wird nicht
|
||||||
|
horizontal gescrollt, sondern jede Zeile zu einer eigenen Karte mit
|
||||||
|
Label/Wert-Paaren. Braucht data-label="Spaltenname" auf jedem <td>.
|
||||||
|
Ab 640px bleibt es eine normale Tabelle (mit Scroll-Fade als
|
||||||
|
Sicherheitsnetz bei vielen Spalten). */
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
-webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
|
||||||
|
mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: visible;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-mask-image: none;
|
||||||
|
mask-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive table,
|
||||||
|
.table-responsive tbody,
|
||||||
|
.table-responsive tr,
|
||||||
|
.table-responsive td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive thead {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive tbody tr {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive td {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive td:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-heading);
|
||||||
|
text-align: left;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Seitenkopf (PageHeader-Pattern, enconf) ─────────────────────────
|
||||||
|
Titel + Unterzeile links, primäre Aktion rechts — statt eines
|
||||||
|
nackten <h1> mit einem Link irgendwo unten auf der Seite. */
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .hinweis {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .page-header-action {
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--branding-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .page-header-action:hover {
|
||||||
|
filter: brightness(0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Kennzahl-Kacheln (StatCard-Pattern, enconf Dashboard) ───────────
|
||||||
|
Vorher: einfache Text-Links in einer Liste ohne visuelle Priorität.
|
||||||
|
Jetzt: farbiger oberer Akzentrand + große Zahl + Klick navigiert zur
|
||||||
|
Liste, wie enconfs StatCard-Komponente — nur ohne Icon-Bibliothek. */
|
||||||
|
.stat-cards {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-top: 3px solid var(--branding-primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
padding: 16px 18px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-heading);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-mittel {
|
||||||
|
border-top-color: var(--color-mittel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-hoch {
|
||||||
|
border-top-color: var(--color-hoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-action {
|
||||||
|
background: var(--branding-primary);
|
||||||
|
border-color: var(--branding-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-action .stat-card-label {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ab hier mehr Platz (Tablet) — der Container bekommt spürbaren
|
/* Ab hier mehr Platz (Tablet) — der Container bekommt spürbaren
|
||||||
Rand statt volle Breite, sonst bleibt alles identisch. */
|
Rand statt volle Breite, sonst bleibt alles identisch. */
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
|
|||||||
@@ -4,8 +4,12 @@
|
|||||||
<body>
|
<body>
|
||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Abteilungen</h1>
|
<div class="page-header">
|
||||||
<p class="hinweis">Stammdaten für den Fragebogen (Feld "Abteilung").</p>
|
<div>
|
||||||
|
<h1>Abteilungen</h1>
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -4,22 +4,42 @@
|
|||||||
<body>
|
<body>
|
||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Meine Anträge</h1>
|
<div class="page-header">
|
||||||
{{if not .Antraege}}
|
<div>
|
||||||
<p class="hinweis">Noch keine Anträge gestellt.</p>
|
<h1>Meine Anträge</h1>
|
||||||
{{else}}
|
<p class="hinweis">Eigene Anträge und ihr Bearbeitungsstand.</p>
|
||||||
<ul class="beitraege-liste">
|
</div>
|
||||||
{{range .Antraege}}
|
<a class="page-header-action" href="/antraege/neu">Neuen Antrag stellen</a>
|
||||||
<li>
|
</div>
|
||||||
<a href="/antraege/{{.ID}}">
|
<form method="get">
|
||||||
<strong>{{.Titel}}</strong> · {{.Haeufigkeit}} · {{.CreatedAt}}
|
<div class="filter-bar">
|
||||||
<span class="status status-{{.Status}}">{{.Status}}</span>
|
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (Titel, Status, Häufigkeit)">
|
||||||
</a>
|
<button type="submit">Filtern</button>
|
||||||
</li>
|
{{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}}
|
{{end}}
|
||||||
</ul>
|
</form>
|
||||||
{{end}}
|
|
||||||
<p><a href="/antraege/neu">Neuen Antrag stellen</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,9 +5,20 @@
|
|||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<p><a href="/betreiber">← Plattform</a></p>
|
<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}}
|
{{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">← 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}}
|
{{end}}
|
||||||
<p><a href="/betreiber/accounts/neu">Firma anlegen</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,9 +5,19 @@
|
|||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<p><a href="/betreiber">← Plattform</a></p>
|
<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}}
|
{{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">← 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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<p><a href="/betreiber">← Plattform</a></p>
|
<p><a href="/betreiber">← Plattform</a></p>
|
||||||
<h1>Werkzeugkatalog</h1>
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Werkzeugkatalog</h1>
|
||||||
|
<p class="hinweis">Zentral gepflegt, für alle Mandanten sichtbar.</p>
|
||||||
|
</div>
|
||||||
|
<a class="page-header-action" href="/betreiber/werkzeuge/neu">Werkzeug anlegen</a>
|
||||||
|
</div>
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<input type="search" name="q" value="{{.Filter.Suche}}" placeholder="Suche (Name, Anbieter, Land)">
|
<input type="search" name="q" value="{{.Filter.Suche}}" placeholder="Suche (Name, Anbieter, Land)">
|
||||||
@@ -23,7 +29,7 @@
|
|||||||
{{if not .Werkzeuge}}
|
{{if not .Werkzeuge}}
|
||||||
<p class="hinweis">{{if .Filter.Aktiv}}Keine Treffer für diese Filter.{{else}}Noch keine zentralen Katalogeinträge.{{end}}</p>
|
<p class="hinweis">{{if .Filter.Aktiv}}Keine Treffer für diese Filter.{{else}}Noch keine zentralen Katalogeinträge.{{end}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="table-scroll">
|
<div class="table-responsive">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -35,16 +41,16 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .Werkzeuge}}
|
{{range .Werkzeuge}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/betreiber/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
<td data-label="Name"><a href="/betreiber/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||||
<td>{{.Anbieter}}</td>
|
<td data-label="Anbieter">{{.Anbieter}}</td>
|
||||||
<td>{{.Verarbeitungslaender}}</td>
|
<td data-label="Verarbeitungsländer">{{.Verarbeitungslaender}}</td>
|
||||||
<td>{{if .DPFZertifiziert}}ja{{else}}nein{{end}}</td>
|
<td data-label="DPF">{{if .DPFZertifiziert}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
<td data-label="AVV">{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
<td data-label="Training aus">{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{.Aufbewahrung}}</td>
|
<td data-label="Aufbewahrung">{{.Aufbewahrung}}</td>
|
||||||
<td>{{.Zertifizierungen}}</td>
|
<td data-label="Zertifizierungen">{{.Zertifizierungen}}</td>
|
||||||
<td>{{.GeeigneteZwecke}}</td>
|
<td data-label="Geeignete Zwecke">{{.GeeigneteZwecke}}</td>
|
||||||
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
<td data-label="Zuletzt geprüft">{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -59,7 +65,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</form>
|
</form>
|
||||||
<p><a href="/betreiber/werkzeuge/neu">Werkzeug anlegen</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,20 +4,40 @@
|
|||||||
<body>
|
<body>
|
||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Posteingang</h1>
|
<div class="page-header">
|
||||||
{{if not .Faelle}}
|
<div>
|
||||||
<p class="keine-findings">Keine offenen Anträge — alles bearbeitet.</p>
|
<h1>Posteingang</h1>
|
||||||
{{else}}
|
<p class="hinweis">Alle noch offenen Anträge deines Mandanten.</p>
|
||||||
<ul class="beitraege-liste">
|
</div>
|
||||||
{{range .Faelle}}
|
</div>
|
||||||
<li>
|
<form method="get">
|
||||||
<a href="/faelle/{{.ID}}">
|
<div class="filter-bar">
|
||||||
<strong>{{.Titel}}</strong> · {{.Haeufigkeit}} · eingereicht am {{.CreatedAt}}
|
<input type="search" name="q" value="{{.Suche}}" placeholder="Suche (Titel, Häufigkeit)">
|
||||||
</a>
|
<button type="submit">Filtern</button>
|
||||||
</li>
|
{{if .Suche}}<a href="/faelle">Zurücksetzen</a>{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if not .Faelle}}
|
||||||
|
<p class="keine-findings">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Keine offenen Anträge — alles bearbeitet.{{end}}</p>
|
||||||
|
{{else}}
|
||||||
|
<ul class="beitraege-liste">
|
||||||
|
{{range .Faelle}}
|
||||||
|
<li>
|
||||||
|
<a href="/faelle/{{.ID}}">
|
||||||
|
<strong>{{.Titel}}</strong> · {{.Haeufigkeit}} · eingereicht am {{.CreatedAt}}
|
||||||
|
</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}}
|
{{end}}
|
||||||
</ul>
|
</form>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,15 +6,25 @@
|
|||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Willkommen bei Deklarix</h1>
|
<h1>Willkommen bei Deklarix</h1>
|
||||||
{{if .Nav.IsBetreiber}}
|
{{if .Nav.IsBetreiber}}
|
||||||
<ul class="admin-kacheln">
|
<ul class="stat-cards">
|
||||||
<li><a href="/betreiber">Zur Plattform</a></li>
|
<li><a class="stat-card stat-card-action" href="/betreiber"><span class="stat-card-label">Zur Plattform →</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<ul class="admin-kacheln">
|
<ul class="stat-cards">
|
||||||
<li><a href="/antraege/neu">Antrag stellen</a></li>
|
<li><a class="stat-card stat-card-action" href="/antraege/neu"><span class="stat-card-label">Antrag stellen →</span></a></li>
|
||||||
<li><a href="/antraege">Meine offenen Anträge <span class="status">{{.OffeneAntraege}}</span></a></li>
|
<li>
|
||||||
|
<a class="stat-card{{if .OffeneAntraege}} stat-card-mittel{{end}}" href="/antraege">
|
||||||
|
<span class="stat-card-value">{{.OffeneAntraege}}</span>
|
||||||
|
<span class="stat-card-label">Meine offenen Anträge</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{{if .ZeigePosteingang}}
|
{{if .ZeigePosteingang}}
|
||||||
<li><a href="/faelle">Posteingang <span class="status">{{.OffenerPosteingang}}</span></a></li>
|
<li>
|
||||||
|
<a class="stat-card{{if .OffenerPosteingang}} stat-card-hoch{{end}}" href="/faelle">
|
||||||
|
<span class="stat-card-value">{{.OffenerPosteingang}}</span>
|
||||||
|
<span class="stat-card-label">Posteingang</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -6,12 +6,16 @@
|
|||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Werkzeugkatalog</h1>
|
<h1>Werkzeugkatalog</h1>
|
||||||
|
|
||||||
<h2>Zentraler Katalog</h2>
|
<div class="page-header">
|
||||||
<p class="hinweis">
|
<div>
|
||||||
Wird von der Plattform gepflegt. Ein gesperrtes Werkzeug bleibt im
|
<h2>Zentraler Katalog</h2>
|
||||||
zentralen Katalog unverändert — es wird nur für diesen Mandanten aus
|
<p class="hinweis">
|
||||||
der Werkzeugsuche ausgeblendet.
|
Wird von der Plattform gepflegt. Ein gesperrtes Werkzeug bleibt im
|
||||||
</p>
|
zentralen Katalog unverändert — es wird nur für diesen Mandanten
|
||||||
|
aus der Werkzeugsuche ausgeblendet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form method="get" class="filter-bar">
|
<form method="get" class="filter-bar">
|
||||||
<input type="search" name="q" value="{{.Filter.Suche}}" placeholder="Suche (Name, Anbieter, Land)">
|
<input type="search" name="q" value="{{.Filter.Suche}}" placeholder="Suche (Name, Anbieter, Land)">
|
||||||
<select name="zweck">
|
<select name="zweck">
|
||||||
@@ -27,7 +31,7 @@
|
|||||||
{{if not .Werkzeuge}}
|
{{if not .Werkzeuge}}
|
||||||
<p class="hinweis">{{if .Filter.Aktiv}}Keine Treffer für diese Filter.{{else}}Der zentrale Katalog ist noch leer.{{end}}</p>
|
<p class="hinweis">{{if .Filter.Aktiv}}Keine Treffer für diese Filter.{{else}}Der zentrale Katalog ist noch leer.{{end}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="table-scroll">
|
<div class="table-responsive">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -38,15 +42,15 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .Werkzeuge}}
|
{{range .Werkzeuge}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.Name}}</td>
|
<td data-label="Name">{{.Name}}</td>
|
||||||
<td>{{.Anbieter}}</td>
|
<td data-label="Anbieter">{{.Anbieter}}</td>
|
||||||
<td>{{.Verarbeitungslaender}}</td>
|
<td data-label="Verarbeitungsländer">{{.Verarbeitungslaender}}</td>
|
||||||
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
<td data-label="AVV">{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
<td data-label="Training aus">{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{.Aufbewahrung}}</td>
|
<td data-label="Aufbewahrung">{{.Aufbewahrung}}</td>
|
||||||
<td>{{.GeeigneteZwecke}}</td>
|
<td data-label="Geeignete Zwecke">{{.GeeigneteZwecke}}</td>
|
||||||
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
<td data-label="Zuletzt geprüft">{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||||
<td>
|
<td data-label="Status">
|
||||||
{{if .Gesperrt}}
|
{{if .Gesperrt}}
|
||||||
<span class="status">gesperrt</span>
|
<span class="status">gesperrt</span>
|
||||||
<form method="post" action="/verwaltung/werkzeuge/{{.ID}}/entsperren" style="display:inline">
|
<form method="post" action="/verwaltung/werkzeuge/{{.ID}}/entsperren" style="display:inline">
|
||||||
@@ -87,15 +91,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<h2>Eigene Werkzeuge</h2>
|
<div class="page-header">
|
||||||
<p class="hinweis">
|
<div>
|
||||||
Zusätzliche Katalogeinträge nur für diesen Mandanten — sichtbar nur
|
<h2>Eigene Werkzeuge</h2>
|
||||||
hier, nicht im zentralen Katalog der Plattform.
|
<p class="hinweis">
|
||||||
</p>
|
Zusätzliche Katalogeinträge nur für diesen Mandanten — sichtbar nur
|
||||||
|
hier, nicht im zentralen Katalog der Plattform.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a class="page-header-action" href="/verwaltung/werkzeuge/neu">Eigenes Werkzeug anlegen</a>
|
||||||
|
</div>
|
||||||
{{if not .EigeneWerkzeuge}}
|
{{if not .EigeneWerkzeuge}}
|
||||||
<p class="hinweis">Noch keine eigenen Werkzeuge.</p>
|
<p class="hinweis">Noch keine eigenen Werkzeuge.</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="table-scroll">
|
<div class="table-responsive">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Anbieter</th><th>Verarbeitungsländer</th><th>Zuletzt geprüft</th></tr>
|
<tr><th>Name</th><th>Anbieter</th><th>Verarbeitungsländer</th><th>Zuletzt geprüft</th></tr>
|
||||||
@@ -103,17 +112,16 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .EigeneWerkzeuge}}
|
{{range .EigeneWerkzeuge}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/verwaltung/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
<td data-label="Name"><a href="/verwaltung/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||||
<td>{{.Anbieter}}</td>
|
<td data-label="Anbieter">{{.Anbieter}}</td>
|
||||||
<td>{{.Verarbeitungslaender}}</td>
|
<td data-label="Verarbeitungsländer">{{.Verarbeitungslaender}}</td>
|
||||||
<td>{{.LetztePruefung}}</td>
|
<td data-label="Zuletzt geprüft">{{.LetztePruefung}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<p><a href="/verwaltung/werkzeuge/neu">Eigenes Werkzeug anlegen</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,7 +4,21 @@
|
|||||||
<body>
|
<body>
|
||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Nutzerverwaltung</h1>
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<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">← 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="/verwaltung/nutzer/import">Per CSV importieren</a></p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,36 +4,61 @@
|
|||||||
<body>
|
<body>
|
||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Register</h1>
|
<div class="page-header">
|
||||||
<p class="hinweis">
|
<div>
|
||||||
Jede Genehmigung erzeugt automatisch einen Registereintrag —
|
<h1>Register</h1>
|
||||||
abgelehnte Anträge und Rückfragen stehen nicht im Register.
|
<p class="hinweis">
|
||||||
</p>
|
Jede Genehmigung erzeugt automatisch einen Registereintrag —
|
||||||
<p><a href="/registereintraege/export.csv">CSV-Export</a> · <a href="/registereintraege/export.pdf">PDF-Export</a></p>
|
abgelehnte Anträge und Rückfragen stehen nicht im Register.
|
||||||
{{if not .Eintraege}}
|
</p>
|
||||||
<p class="hinweis">Noch keine Genehmigungen.</p>
|
</div>
|
||||||
{{else}}
|
<span class="hinweis"><a href="/registereintraege/export.csv">CSV-Export</a> · <a href="/registereintraege/export.pdf">PDF-Export</a></span>
|
||||||
<div class="table-scroll">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Zweck</th><th>Abteilung</th><th>Werkzeug</th><th>Datenklasse</th>
|
|
||||||
<th>Einstufung</th><th>Auflagen</th><th>Verantwortliche/r</th>
|
|
||||||
<th>Entschieden am</th><th>Gültig bis</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range .Eintraege}}
|
|
||||||
<tr>
|
|
||||||
<td>{{.Zweck}}</td><td>{{.Abteilung}}</td><td>{{.Werkzeug}}</td><td>{{.Datenklasse}}</td>
|
|
||||||
<td>{{.Einstufung}}</td><td>{{.Auflagen}}</td><td>{{.Verantwortlicher}}</td>
|
|
||||||
<td>{{.EntschiedenAm}}</td><td>{{.GueltigBis}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
<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}}
|
||||||
|
<p class="hinweis">{{if .Suche}}Keine Treffer für diese Suche.{{else}}Noch keine Genehmigungen.{{end}}</p>
|
||||||
|
{{else}}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Zweck</th><th>Abteilung</th><th>Werkzeug</th><th>Datenklasse</th>
|
||||||
|
<th>Einstufung</th><th>Auflagen</th><th>Verantwortliche/r</th>
|
||||||
|
<th>Entschieden am</th><th>Gültig bis</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Eintraege}}
|
||||||
|
<tr>
|
||||||
|
<td data-label="Zweck">{{.Zweck}}</td>
|
||||||
|
<td data-label="Abteilung">{{.Abteilung}}</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>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</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}}">← Zurück</button>{{end}}
|
||||||
|
{{if .Pagination.HasNext}}<button type="submit" name="page" value="{{.Pagination.NextPage}}">Weiter →</button>{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,11 +4,15 @@
|
|||||||
<body>
|
<body>
|
||||||
{{template "nav" .Nav}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Wiedervorlage</h1>
|
<div class="page-header">
|
||||||
<p class="hinweis">
|
<div>
|
||||||
Genehmigungen, die abgelaufen sind, bald ablaufen, oder deren
|
<h1>Wiedervorlage</h1>
|
||||||
Werkzeug sich seit der Entscheidung im Katalog geändert hat.
|
<p class="hinweis">
|
||||||
</p>
|
Genehmigungen, die abgelaufen sind, bald ablaufen, oder deren
|
||||||
|
Werkzeug sich seit der Entscheidung im Katalog geändert hat.
|
||||||
|
</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}}
|
||||||
|
|||||||
Reference in New Issue
Block a user