diff --git a/CLAUDE.md b/CLAUDE.md index 78f7ff7..180ac0c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -598,6 +598,40 @@ wie die Formular-Abschnitte angehoben (600/1rem statt 500/0.9375rem) — für eine einheitliche Überschriften-Hierarchie statt Zufallsgrößen je 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 `` zu Karten, `` zu Label/Wert-Zeilen (`content: + attr(data-label)` im `::before`) — jedes `` 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 `

` 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`). + +Noch nicht auf weitere Tabellen/Seiten übertragen (Register, +Nutzerliste, Abteilungen, Fälle-Posteingang, Betreiber-Dashboard) — +nächste Kandidaten, falls das Muster fortgesetzt werden soll. + **Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt` diff --git a/internal/web/handlers_test.go b/internal/web/handlers_test.go index c329aba..4af0dde 100644 --- a/internal/web/handlers_test.go +++ b/internal/web/handlers_test.go @@ -14,8 +14,9 @@ func TestIndexZeigtEigeneOffeneAntraege(t *testing.T) { if resp.Code != http.StatusOK { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } - if !strings.Contains(resp.Body.String(), `href="/antraege">Meine offenen Anträge 1`) { - t.Errorf("expected 1 offenen Antrag auf der Startseite, got: %s", resp.Body.String()) + body := resp.Body.String() + if !strings.Contains(body, `href="/antraege"`) || !strings.Contains(body, `stat-card-value">1`) || !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 { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } - if !strings.Contains(resp.Body.String(), `href="/faelle">Posteingang 1`) { - t.Errorf("expected 1 offenen Fall im Posteingang auf der Startseite, got: %s", resp.Body.String()) + body := resp.Body.String() + if !strings.Contains(body, `href="/faelle"`) || !strings.Contains(body, `stat-card-value">1`) || !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 { 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()) } if strings.Contains(resp.Body.String(), "Antrag stellen") { diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 70dcd24..5d467d1 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -792,6 +792,183 @@ tbody tr:hover { 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 . + 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

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 Rand statt volle Breite, sonst bleibt alles identisch. */ @media (min-width: 640px) { diff --git a/internal/web/templates/betreiber_werkzeuge.html b/internal/web/templates/betreiber_werkzeuge.html index 8bfea07..3d3e1a8 100644 --- a/internal/web/templates/betreiber_werkzeuge.html +++ b/internal/web/templates/betreiber_werkzeuge.html @@ -5,7 +5,13 @@ {{template "nav" .Nav}}

← Plattform

-

Werkzeugkatalog

+
@@ -23,7 +29,7 @@ {{if not .Werkzeuge}}

{{if .Filter.Aktiv}}Keine Treffer für diese Filter.{{else}}Noch keine zentralen Katalogeinträge.{{end}}

{{else}} -
+
@@ -35,16 +41,16 @@ {{range .Werkzeuge}} - - - - - - - - - - + + + + + + + + + + {{end}} @@ -59,7 +65,6 @@ {{end}} -

Werkzeug anlegen

diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html index be31a39..b252d46 100644 --- a/internal/web/templates/index.html +++ b/internal/web/templates/index.html @@ -6,15 +6,25 @@

Willkommen bei Deklarix

{{if .Nav.IsBetreiber}} -
{{.Name}}{{.Anbieter}}{{.Verarbeitungslaender}}{{if .DPFZertifiziert}}ja{{else}}nein{{end}}{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}{{if .TrainingStandard}}ja{{else}}nein{{end}}{{.Aufbewahrung}}{{.Zertifizierungen}}{{.GeeigneteZwecke}}{{.LetztePruefung}} {{.PruefStatusLabel}}{{.Name}}{{.Anbieter}}{{.Verarbeitungslaender}}{{if .DPFZertifiziert}}ja{{else}}nein{{end}}{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}{{if .TrainingStandard}}ja{{else}}nein{{end}}{{.Aufbewahrung}}{{.Zertifizierungen}}{{.GeeigneteZwecke}}{{.LetztePruefung}} {{.PruefStatusLabel}}
@@ -38,15 +42,15 @@ {{range .Werkzeuge}} - - - - - - - - - + + + + + + + +
{{.Name}}{{.Anbieter}}{{.Verarbeitungslaender}}{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}{{if .TrainingStandard}}ja{{else}}nein{{end}}{{.Aufbewahrung}}{{.GeeigneteZwecke}}{{.LetztePruefung}} {{.PruefStatusLabel}} + {{.Name}}{{.Anbieter}}{{.Verarbeitungslaender}}{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}{{if .TrainingStandard}}ja{{else}}nein{{end}}{{.Aufbewahrung}}{{.GeeigneteZwecke}}{{.LetztePruefung}} {{.PruefStatusLabel}} {{if .Gesperrt}} gesperrt @@ -87,15 +91,20 @@ {{end}} -

Eigene Werkzeuge

-

- Zusätzliche Katalogeinträge nur für diesen Mandanten — sichtbar nur - hier, nicht im zentralen Katalog der Plattform. -

+ {{if not .EigeneWerkzeuge}}

Noch keine eigenen Werkzeuge.

{{else}} -
+
@@ -103,17 +112,16 @@ {{range .EigeneWerkzeuge}} - - - - + + + + {{end}}
NameAnbieterVerarbeitungsländerZuletzt geprüft
{{.Name}}{{.Anbieter}}{{.Verarbeitungslaender}}{{.LetztePruefung}}{{.Name}}{{.Anbieter}}{{.Verarbeitungslaender}}{{.LetztePruefung}}
{{end}} -

Eigenes Werkzeug anlegen