feat: enconf-Layout-Patterns übertragen — responsive Card-Tabellen, PageHeader, StatCards
Über die Architect-RAG-Suche (ac_search_code_global) drei UX-Muster aus dem enconf-Schwesterprojekt (React/Ant Design) identifiziert und als reines CSS/HTML nachgebaut, da Deklarix bewusst kein SPA ist: - .table-responsive: Tabellen werden unter 640px zu einer Card-Liste statt horizontal zu scrollen (enconfs ProTable-Verhalten), via data-label-Attribut je <td>. Angewendet auf beide Werkzeugkatalog-Tabellen. - .page-header: Titel+Unterzeile links, primäre Aktion rechts, statt eines nackten <h1> mit Link irgendwo unten. - .stat-cards: Kennzahl-Kacheln mit farbigem Akzentrand (nur farbig bei Wert > 0) statt einfacher Text-Links, auf der Startseite.
This commit is contained in:
@@ -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 <span class="status">1</span>`) {
|
||||
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</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 {
|
||||
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>`) {
|
||||
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</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 {
|
||||
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") {
|
||||
|
||||
@@ -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 <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
|
||||
Rand statt volle Breite, sonst bleibt alles identisch. */
|
||||
@media (min-width: 640px) {
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
{{template "nav" .Nav}}
|
||||
<div class="page">
|
||||
<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">
|
||||
<div class="filter-bar">
|
||||
<input type="search" name="q" value="{{.Filter.Suche}}" placeholder="Suche (Name, Anbieter, Land)">
|
||||
@@ -23,7 +29,7 @@
|
||||
{{if not .Werkzeuge}}
|
||||
<p class="hinweis">{{if .Filter.Aktiv}}Keine Treffer für diese Filter.{{else}}Noch keine zentralen Katalogeinträge.{{end}}</p>
|
||||
{{else}}
|
||||
<div class="table-scroll">
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -35,16 +41,16 @@
|
||||
<tbody>
|
||||
{{range .Werkzeuge}}
|
||||
<tr>
|
||||
<td><a href="/betreiber/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||
<td>{{.Anbieter}}</td>
|
||||
<td>{{.Verarbeitungslaender}}</td>
|
||||
<td>{{if .DPFZertifiziert}}ja{{else}}nein{{end}}</td>
|
||||
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||
<td>{{.Aufbewahrung}}</td>
|
||||
<td>{{.Zertifizierungen}}</td>
|
||||
<td>{{.GeeigneteZwecke}}</td>
|
||||
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||
<td data-label="Name"><a href="/betreiber/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||
<td data-label="Anbieter">{{.Anbieter}}</td>
|
||||
<td data-label="Verarbeitungsländer">{{.Verarbeitungslaender}}</td>
|
||||
<td data-label="DPF">{{if .DPFZertifiziert}}ja{{else}}nein{{end}}</td>
|
||||
<td data-label="AVV">{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||
<td data-label="Training aus">{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||
<td data-label="Aufbewahrung">{{.Aufbewahrung}}</td>
|
||||
<td data-label="Zertifizierungen">{{.Zertifizierungen}}</td>
|
||||
<td data-label="Geeignete Zwecke">{{.GeeigneteZwecke}}</td>
|
||||
<td data-label="Zuletzt geprüft">{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
@@ -59,7 +65,6 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</form>
|
||||
<p><a href="/betreiber/werkzeuge/neu">Werkzeug anlegen</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,15 +6,25 @@
|
||||
<div class="page">
|
||||
<h1>Willkommen bei Deklarix</h1>
|
||||
{{if .Nav.IsBetreiber}}
|
||||
<ul class="admin-kacheln">
|
||||
<li><a href="/betreiber">Zur Plattform</a></li>
|
||||
<ul class="stat-cards">
|
||||
<li><a class="stat-card stat-card-action" href="/betreiber"><span class="stat-card-label">Zur Plattform →</span></a></li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<ul class="admin-kacheln">
|
||||
<li><a href="/antraege/neu">Antrag stellen</a></li>
|
||||
<li><a href="/antraege">Meine offenen Anträge <span class="status">{{.OffeneAntraege}}</span></a></li>
|
||||
<ul class="stat-cards">
|
||||
<li><a class="stat-card stat-card-action" href="/antraege/neu"><span class="stat-card-label">Antrag stellen →</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}}
|
||||
<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}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
<div class="page">
|
||||
<h1>Werkzeugkatalog</h1>
|
||||
|
||||
<h2>Zentraler Katalog</h2>
|
||||
<p class="hinweis">
|
||||
Wird von der Plattform gepflegt. Ein gesperrtes Werkzeug bleibt im
|
||||
zentralen Katalog unverändert — es wird nur für diesen Mandanten aus
|
||||
der Werkzeugsuche ausgeblendet.
|
||||
</p>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Zentraler Katalog</h2>
|
||||
<p class="hinweis">
|
||||
Wird von der Plattform gepflegt. Ein gesperrtes Werkzeug bleibt im
|
||||
zentralen Katalog unverändert — es wird nur für diesen Mandanten
|
||||
aus der Werkzeugsuche ausgeblendet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="get" class="filter-bar">
|
||||
<input type="search" name="q" value="{{.Filter.Suche}}" placeholder="Suche (Name, Anbieter, Land)">
|
||||
<select name="zweck">
|
||||
@@ -27,7 +31,7 @@
|
||||
{{if not .Werkzeuge}}
|
||||
<p class="hinweis">{{if .Filter.Aktiv}}Keine Treffer für diese Filter.{{else}}Der zentrale Katalog ist noch leer.{{end}}</p>
|
||||
{{else}}
|
||||
<div class="table-scroll">
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -38,15 +42,15 @@
|
||||
<tbody>
|
||||
{{range .Werkzeuge}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.Anbieter}}</td>
|
||||
<td>{{.Verarbeitungslaender}}</td>
|
||||
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||
<td>{{.Aufbewahrung}}</td>
|
||||
<td>{{.GeeigneteZwecke}}</td>
|
||||
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||
<td>
|
||||
<td data-label="Name">{{.Name}}</td>
|
||||
<td data-label="Anbieter">{{.Anbieter}}</td>
|
||||
<td data-label="Verarbeitungsländer">{{.Verarbeitungslaender}}</td>
|
||||
<td data-label="AVV">{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||
<td data-label="Training aus">{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||
<td data-label="Aufbewahrung">{{.Aufbewahrung}}</td>
|
||||
<td data-label="Geeignete Zwecke">{{.GeeigneteZwecke}}</td>
|
||||
<td data-label="Zuletzt geprüft">{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||
<td data-label="Status">
|
||||
{{if .Gesperrt}}
|
||||
<span class="status">gesperrt</span>
|
||||
<form method="post" action="/verwaltung/werkzeuge/{{.ID}}/entsperren" style="display:inline">
|
||||
@@ -87,15 +91,20 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h2>Eigene Werkzeuge</h2>
|
||||
<p class="hinweis">
|
||||
Zusätzliche Katalogeinträge nur für diesen Mandanten — sichtbar nur
|
||||
hier, nicht im zentralen Katalog der Plattform.
|
||||
</p>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Eigene Werkzeuge</h2>
|
||||
<p class="hinweis">
|
||||
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}}
|
||||
<p class="hinweis">Noch keine eigenen Werkzeuge.</p>
|
||||
{{else}}
|
||||
<div class="table-scroll">
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Anbieter</th><th>Verarbeitungsländer</th><th>Zuletzt geprüft</th></tr>
|
||||
@@ -103,17 +112,16 @@
|
||||
<tbody>
|
||||
{{range .EigeneWerkzeuge}}
|
||||
<tr>
|
||||
<td><a href="/verwaltung/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||
<td>{{.Anbieter}}</td>
|
||||
<td>{{.Verarbeitungslaender}}</td>
|
||||
<td>{{.LetztePruefung}}</td>
|
||||
<td data-label="Name"><a href="/verwaltung/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||
<td data-label="Anbieter">{{.Anbieter}}</td>
|
||||
<td data-label="Verarbeitungsländer">{{.Verarbeitungslaender}}</td>
|
||||
<td data-label="Zuletzt geprüft">{{.LetztePruefung}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
<p><a href="/verwaltung/werkzeuge/neu">Eigenes Werkzeug anlegen</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user