Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d77b19f61f | ||
|
|
dbeaa45644 |
79
CLAUDE.md
79
CLAUDE.md
@@ -39,8 +39,8 @@ Regelbasiert, nicht ML: die Ableitungsregeln (Datenklasse, KI-VO-
|
|||||||
Einstufung, Anforderungsprofil) liegen als versionierte YAML-Dateien
|
Einstufung, Anforderungsprofil) liegen als versionierte YAML-Dateien
|
||||||
vor, nicht im Code — ein fachlich Verantwortlicher soll sie anpassen
|
vor, nicht im Code — ein fachlich Verantwortlicher soll sie anpassen
|
||||||
können, ohne Go-Code anzufassen. Ein LLM ist v1 höchstens optional zur
|
können, ohne Go-Code anzufassen. Ein LLM ist v1 höchstens optional zur
|
||||||
Zweckerkennung aus dem Freitext vorgesehen (Textgenerierung/Übersetzung/
|
Zweckerkennung aus dem Freitext vorgesehen (die zwölf Zweck-Kategorien
|
||||||
Code/Bild/Transkription/Recherche erkennen), nie für die Einstufung
|
erkennen, siehe `gueltigeZwecke` weiter unten), nie für die Einstufung
|
||||||
selbst — und auch dort nur als Vorschlag, den der Mensch bestätigt.
|
selbst — und auch dort nur als Vorschlag, den der Mensch bestätigt.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -253,10 +253,13 @@ zentralen Katalog zu verändern.
|
|||||||
**Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede
|
**Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede
|
||||||
Zusicherung im Katalog (AVV verfügbar, Verarbeitungsort, Training-Opt-
|
Zusicherung im Katalog (AVV verfügbar, Verarbeitungsort, Training-Opt-
|
||||||
out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für
|
out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für
|
||||||
eine Aussage, die nicht nachweisbar ist. Einträge älter als 180 Tage
|
eine Aussage, die nicht nachweisbar ist. Beide Katalogansichten zeigen
|
||||||
werden im zentralen (`GET /betreiber/werkzeuge`) und im mandanten-
|
eine dreistufige Ampel zum Prüfalter (`pruefStatus()` in
|
||||||
seitigen Katalog (`GET /verwaltung/werkzeuge`) als "nicht aktuell
|
`betreiber_werkzeug_handlers.go`, CSS-Klassen `status-niedrig`/
|
||||||
geprüft" markiert (`werkzeugPruefungVeraltetNach`, 180 Tage).
|
`-mittel`/`-hoch`, dieselben Farbtoken wie die KI-VO-Risikoampel):
|
||||||
|
**aktuell** (grün, < 150 Tage), **bald fällig** (gelb, 150–180 Tage —
|
||||||
|
dasselbe 30-Tage-Vorwarnfenster wie bei der Wiedervorlage), **abgelaufen**
|
||||||
|
(rot, > 180 Tage).
|
||||||
|
|
||||||
`aufbewahrung_tage` ist NULLABLE (Migration 0015) — `NULL` bedeutet
|
`aufbewahrung_tage` ist NULLABLE (Migration 0015) — `NULL` bedeutet
|
||||||
"vom Anbieter nicht beziffert" und ist bewusst von `0` ("belegte
|
"vom Anbieter nicht beziffert" und ist bewusst von `0` ("belegte
|
||||||
@@ -266,13 +269,23 @@ aussahen — `formatAufbewahrung()` zeigt entsprechend "unbekannt" statt
|
|||||||
"0 Tage".
|
"0 Tage".
|
||||||
|
|
||||||
`geeignete_zwecke` ist seit derselben Migration ein kontrolliertes
|
`geeignete_zwecke` ist seit derselben Migration ein kontrolliertes
|
||||||
Vokabular statt Freitext: das Formular zeigt Checkboxen für genau die
|
Vokabular statt Freitext: das Formular zeigt Checkboxen statt eines
|
||||||
sechs Zweck-Kategorien aus dem Fragebogen (Textgenerierung,
|
Freitextfelds (`gueltigeZwecke` in `betreiber_werkzeug_handlers.go`,
|
||||||
Übersetzung, Code, Bild, Transkription, Recherche — `gueltigeZwecke` in
|
serverseitig validiert via `istGueltigerZweck`). Grund: ein künftiger
|
||||||
`betreiber_werkzeug_handlers.go`), serverseitig validiert
|
automatischer Zweckabgleich (Bewertungslogik Schritt 3, weiterhin nicht
|
||||||
(`istGueltigerZweck`). Grund: ein künftiger automatischer Zweckabgleich
|
gebaut) würde an inkonsistentem Freitext ("Text" statt
|
||||||
(Bewertungslogik Schritt 3, weiterhin nicht gebaut) würde an
|
"Textgenerierung") scheitern. Am 2026-08-31 von sechs auf zwölf
|
||||||
inkonsistentem Freitext ("Text" statt "Textgenerierung") scheitern.
|
Kategorien erweitert, nachdem der Nutzer zurecht einwandte, dass sechs
|
||||||
|
Kategorien die tatsächliche Bandbreite an Einsatzgebieten nicht
|
||||||
|
abdecken: Textgenerierung, Übersetzung, Code, Bild, Transkription,
|
||||||
|
Recherche, **Datenanalyse, Video, Audio/Sprache,
|
||||||
|
Kundenservice/Chatbot, Präsentation/Design, Automatisierung/Agenten**.
|
||||||
|
Es gibt keinen einheitlichen Branchenstandard für KI-Funktions-
|
||||||
|
kategorien (regulatorische Taxonomien wie die KI-VO selbst oder OECD/
|
||||||
|
NIST klassifizieren nach Risiko, nicht nach Funktion; Marktplatz-
|
||||||
|
Kategorien wie bei G2 sind Marketing, kein Standard) — diese zwölf
|
||||||
|
sind Deklarix' eigene, bewusst begrenzte Liste für den künftigen
|
||||||
|
Zweckabgleich, keine allgemeine KI-Taxonomie.
|
||||||
|
|
||||||
Neues Feld `subprozessoren` (TEXT[], Migration 0015) macht
|
Neues Feld `subprozessoren` (TEXT[], Migration 0015) macht
|
||||||
Unterauftragsverarbeiter (z. B. Anthropic bei Microsoft 365 Copilot,
|
Unterauftragsverarbeiter (z. B. Anthropic bei Microsoft 365 Copilot,
|
||||||
@@ -316,8 +329,8 @@ bleibt, mit welchem Katalogstand ein Vorschlag erzeugt wurde.
|
|||||||
nicht erfüllen — Grund je Werkzeug festhalten, auch aussortierte
|
nicht erfüllen — Grund je Werkzeug festhalten, auch aussortierte
|
||||||
Werkzeuge werden im Ergebnis mit Begründung gezeigt.
|
Werkzeuge werden im Ergebnis mit Begründung gezeigt.
|
||||||
3. **Zweckabgleich.** Aus `beschreibung`/`ergebnis` den Zweck ableiten
|
3. **Zweckabgleich.** Aus `beschreibung`/`ergebnis` den Zweck ableiten
|
||||||
(Textgenerierung, Übersetzung, Code, Bild, Transkription, Recherche)
|
(die zwölf Kategorien aus `gueltigeZwecke`, siehe Werkzeugkatalog
|
||||||
und gegen `geeignete_zwecke` filtern.
|
weiter unten) und gegen `geeignete_zwecke` filtern.
|
||||||
4. **Rangfolge.** Verbleibende Werkzeuge sortieren: EU-Verarbeitung,
|
4. **Rangfolge.** Verbleibende Werkzeuge sortieren: EU-Verarbeitung,
|
||||||
Training standardmäßig aus, kurze Aufbewahrung, Aktualität der
|
Training standardmäßig aus, kurze Aufbewahrung, Aktualität der
|
||||||
Prüfung.
|
Prüfung.
|
||||||
@@ -464,7 +477,41 @@ kein separates Desktop-Template.
|
|||||||
**Tabellen** (`table`/`th`/`td` in `app.css`) sind seit dieser Änderung
|
**Tabellen** (`table`/`th`/`td` in `app.css`) sind seit dieser Änderung
|
||||||
gestylt (vorher komplett ungestylt, betraf v. a. `register_liste.html`);
|
gestylt (vorher komplett ungestylt, betraf v. a. `register_liste.html`);
|
||||||
breite Tabellen stehen in einem `.table-scroll`-Wrapper (horizontales
|
breite Tabellen stehen in einem `.table-scroll`-Wrapper (horizontales
|
||||||
Scrollen auf dem Handy statt gequetschter Spalten).
|
Scrollen auf dem Handy statt gequetschter Spalten). Seit 2026-08-31 hat
|
||||||
|
`.table-scroll` zusätzlich einen rechten Fade-Rand (`mask-image`) —
|
||||||
|
ohne den sah eine am Fensterrand abgeschnittene breite Tabelle wie ein
|
||||||
|
Rendering-Fehler aus statt wie "hier geht's mit Scrollen weiter" (per
|
||||||
|
Screenshot am Werkzeugkatalog beobachtet: die letzten zwei Spalten
|
||||||
|
waren nur als einzelne Buchstaben sichtbar, ganz ohne Hinweis auf mehr
|
||||||
|
Inhalt).
|
||||||
|
|
||||||
|
**Formulare** (`.form-card`/`.form-section`/`.form-grid`/`.chip-group`
|
||||||
|
in `app.css`, seit 2026-08-31, erstmals angewendet auf
|
||||||
|
`betreiber_werkzeug_form.html`): vorher war jedes Formular eine flache
|
||||||
|
Spalte aus `<label>`+`<input>` ohne Gruppierung — bei einem Formular
|
||||||
|
mit über einem Dutzend Feldern (Werkzeug anlegen) wirkte das "klobig,
|
||||||
|
einfach nur untereinander" (O-Ton Nutzer). Jetzt: `.form-card` (weiße
|
||||||
|
Karte mit Rahmen/Schatten, Ant-Design-Tokens aus `design/enterprise.css`
|
||||||
|
übernommen) enthält mehrere `<fieldset class="form-section">`
|
||||||
|
(thematische Abschnitte mit Titel, per `border-top` getrennt), darin
|
||||||
|
`.form-grid` (2 Spalten ab 640px für kurze Felder wie Name/Anbieter
|
||||||
|
nebeneinander, `.form-full` erzwingt volle Breite für lange Felder).
|
||||||
|
Checkbox-/Radio-Gruppen mit vielen Optionen (Verarbeitungsort, die
|
||||||
|
zwölf Zwecke) sind `.chip-group` — abgerundete Pillen statt einer
|
||||||
|
langen Untereinander-Liste, ausgewählte Chips werden über
|
||||||
|
`:has(input:checked)` blau hervorgehoben (kein JavaScript). Ein
|
||||||
|
Löschen-Button steht nicht mehr einfach unter dem Formular, sondern in
|
||||||
|
einer abgesetzten `.form-gefahrenzone`-Fußzeile der Karte. Dieselben
|
||||||
|
Klassen sind bewusst generisch (nicht an ein Formular gebunden) — noch
|
||||||
|
nicht auf andere Formulare (Login/Registrierung, Fragebogen, übrige
|
||||||
|
Admin-Formulare) angewendet, das wäre der nächste Schritt, falls
|
||||||
|
gewünscht. Beim Umbau auffällig gewordener, vorbestehender Bug: das
|
||||||
|
Lösch-Formular in `betreiber_werkzeug_form.html` war fest auf
|
||||||
|
`/betreiber/werkzeuge/{id}/loeschen` verdrahtet, obwohl dieselbe
|
||||||
|
Vorlage auch vom Mandanten-Formular (`/verwaltung/werkzeuge/...`)
|
||||||
|
genutzt wird — ein Mandant hätte beim Löschen eines eigenen Werkzeugs
|
||||||
|
403 bekommen. Jetzt `{{.ActionBase}}/{{.ID}}/loeschen`, wie die
|
||||||
|
Formular-`action` selbst.
|
||||||
|
|
||||||
**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
|
||||||
|
|||||||
@@ -26,12 +26,17 @@ func istGueltigerVerarbeitungsort(v string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// gueltigeZwecke ist das kontrollierte Vokabular für "geeignete Zwecke"
|
// gueltigeZwecke ist das kontrollierte Vokabular für "geeignete Zwecke"
|
||||||
// — exakt die sechs Zweck-Kategorien aus dem Fragebogen/der geplanten
|
// (siehe CLAUDE.md, Bewertungslogik Schritt 3). Vorher war das Feld
|
||||||
// Zweckabgleich-Stufe (siehe CLAUDE.md, Bewertungslogik Schritt 3).
|
// Freitext; ein Tippfehler ("Text" statt "Textgenerierung") hätte den
|
||||||
// Vorher war das Feld Freitext; ein Tippfehler ("Text" statt
|
// künftigen automatischen Zweckabgleich unbemerkt kaputt gemacht. Am
|
||||||
// "Textgenerierung") hätte den künftigen automatischen Zweckabgleich
|
// 2026-08-31 von sechs auf zwölf Kategorien erweitert (bewusste
|
||||||
// unbemerkt kaputt gemacht.
|
// v1-Scope-Entscheidung überarbeitet, nicht stillschweigend geändert)
|
||||||
var gueltigeZwecke = []string{"Textgenerierung", "Übersetzung", "Code", "Bild", "Transkription", "Recherche"}
|
// — die ursprünglichen sechs deckten nur klassische Wissensarbeit ab,
|
||||||
|
// nicht Datenanalyse/Video/Audio/Kundenservice/Design/Automatisierung.
|
||||||
|
var gueltigeZwecke = []string{
|
||||||
|
"Textgenerierung", "Übersetzung", "Code", "Bild", "Transkription", "Recherche",
|
||||||
|
"Datenanalyse", "Video", "Audio/Sprache", "Kundenservice/Chatbot", "Präsentation/Design", "Automatisierung/Agenten",
|
||||||
|
}
|
||||||
|
|
||||||
func istGueltigerZweck(v string) bool {
|
func istGueltigerZweck(v string) bool {
|
||||||
for _, z := range gueltigeZwecke {
|
for _, z := range gueltigeZwecke {
|
||||||
@@ -67,7 +72,8 @@ type werkzeugListItem struct {
|
|||||||
Zertifizierungen string
|
Zertifizierungen string
|
||||||
GeeigneteZwecke string
|
GeeigneteZwecke string
|
||||||
LetztePruefung string
|
LetztePruefung string
|
||||||
NichtAktuell bool
|
PruefStatusLabel string
|
||||||
|
PruefStatusCSS string
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatAufbewahrung stellt sicher, dass "unbekannt" (Anbieter hat
|
// formatAufbewahrung stellt sicher, dass "unbekannt" (Anbieter hat
|
||||||
@@ -90,10 +96,32 @@ type werkzeugListeData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// werkzeugPruefungVeraltetNach ist die Schwelle, ab der ein
|
// werkzeugPruefungVeraltetNach ist die Schwelle, ab der ein
|
||||||
// Katalogeintrag im Ergebnis als "nicht aktuell geprüft" markiert wird
|
// Katalogeintrag im Ergebnis als "abgelaufen" markiert wird (siehe
|
||||||
// (siehe CLAUDE.md, Werkzeugkatalog) — jede Zusicherung im Katalog muss
|
// CLAUDE.md, Werkzeugkatalog) — jede Zusicherung im Katalog muss
|
||||||
// belegbar UND aktuell sein.
|
// belegbar UND aktuell sein. werkzeugPruefungBaldFaelligNach liegt
|
||||||
const werkzeugPruefungVeraltetNach = 180 * 24 * time.Hour
|
// 30 Tage davor (dasselbe Vorwarnfenster wie die Wiedervorlage,
|
||||||
|
// wiedervorlageSchwelle) — "bald fällig" soll rechtzeitig auffallen,
|
||||||
|
// nicht erst wenn die Frist schon gerissen ist.
|
||||||
|
const (
|
||||||
|
werkzeugPruefungBaldFaelligNach = 150 * 24 * time.Hour
|
||||||
|
werkzeugPruefungVeraltetNach = 180 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// pruefStatus liefert eine Ampel-Kennzeichnung für das Alter einer
|
||||||
|
// letzten Werkzeug-Prüfung: aktuell (grün) / bald fällig (gelb) /
|
||||||
|
// abgelaufen (rot) — die CSS-Klassen sind dieselben wie für die
|
||||||
|
// Risiko-Ampel anderswo im Produkt (status-niedrig/-mittel/-hoch).
|
||||||
|
func pruefStatus(letztePruefung time.Time) (label, css string) {
|
||||||
|
alter := time.Since(letztePruefung)
|
||||||
|
switch {
|
||||||
|
case alter > werkzeugPruefungVeraltetNach:
|
||||||
|
return "abgelaufen", "status-hoch"
|
||||||
|
case alter > werkzeugPruefungBaldFaelligNach:
|
||||||
|
return "bald fällig", "status-mittel"
|
||||||
|
default:
|
||||||
|
return "aktuell", "status-niedrig"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleBetreiberWerkzeugListe zeigt den zentralen Katalog.
|
// handleBetreiberWerkzeugListe zeigt den zentralen Katalog.
|
||||||
func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -104,13 +132,14 @@ func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
data := werkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
data := werkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
||||||
for _, wz := range werkzeuge {
|
for _, wz := range werkzeuge {
|
||||||
|
label, css := pruefStatus(wz.LetztePruefung)
|
||||||
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
|
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
|
||||||
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
||||||
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
||||||
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
||||||
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
||||||
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
||||||
NichtAktuell: time.Since(wz.LetztePruefung) > werkzeugPruefungVeraltetNach,
|
PruefStatusLabel: label, PruefStatusCSS: css,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeuge", data); err != nil {
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeuge", data); err != nil {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ package web
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type mandantWerkzeugItem struct {
|
type mandantWerkzeugItem struct {
|
||||||
@@ -24,7 +23,8 @@ type mandantWerkzeugItem struct {
|
|||||||
Aufbewahrung string
|
Aufbewahrung string
|
||||||
GeeigneteZwecke string
|
GeeigneteZwecke string
|
||||||
LetztePruefung string
|
LetztePruefung string
|
||||||
NichtAktuell bool
|
PruefStatusLabel string
|
||||||
|
PruefStatusCSS string
|
||||||
Gesperrt bool
|
Gesperrt bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +71,14 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
data := mandantWerkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
data := mandantWerkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
||||||
for _, wz := range zentral {
|
for _, wz := range zentral {
|
||||||
|
label, css := pruefStatus(wz.LetztePruefung)
|
||||||
data.Werkzeuge = append(data.Werkzeuge, mandantWerkzeugItem{
|
data.Werkzeuge = append(data.Werkzeuge, mandantWerkzeugItem{
|
||||||
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
||||||
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
||||||
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
||||||
GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
||||||
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
||||||
NichtAktuell: time.Since(wz.LetztePruefung) > werkzeugPruefungVeraltetNach,
|
PruefStatusLabel: label, PruefStatusCSS: css,
|
||||||
Gesperrt: gesperrt[wz.ID],
|
Gesperrt: gesperrt[wz.ID],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -392,6 +392,16 @@ nav button {
|
|||||||
color: var(--color-mittel);
|
color: var(--color-mittel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-niedrig {
|
||||||
|
background: var(--color-niedrig-bg);
|
||||||
|
color: var(--color-niedrig);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-hoch {
|
||||||
|
background: var(--color-hoch-bg);
|
||||||
|
color: var(--color-hoch);
|
||||||
|
}
|
||||||
|
|
||||||
.beitraege-liste {
|
.beitraege-liste {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
@@ -545,6 +555,126 @@ button.entfernen {
|
|||||||
margin: 4px 12px 4px 0;
|
margin: 4px 12px 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Formular-Karten (Enterprise-Look, siehe design/enterprise.css
|
||||||
|
.ant-card/.ant-form-item) ─────────────────────────────────────────
|
||||||
|
Ersetzt "ein Formular = eine flache, ununterbrochene Spalte" durch
|
||||||
|
klar abgegrenzte Abschnitte mit Titel und ein Grid für kurze Felder
|
||||||
|
nebeneinander statt jedes Feld einzeln untereinander. */
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section + .form-section {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section > legend {
|
||||||
|
padding: 0 0 12px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-heading);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid .form-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox-/Radio-Gruppen als Chips statt einer langen
|
||||||
|
Untereinander-Liste — v. a. bei den zwölf Zweck-Kategorien war das
|
||||||
|
vorher eine kaum überblickbare Spalte. */
|
||||||
|
.chip-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-group label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-group input[type="checkbox"],
|
||||||
|
.chip-group input[type="radio"] {
|
||||||
|
width: auto;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-group label:has(input:checked) {
|
||||||
|
border-color: var(--branding-primary);
|
||||||
|
background: rgba(22, 119, 255, 0.08);
|
||||||
|
color: var(--branding-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions form,
|
||||||
|
.form-actions button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-gefahrenzone {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-gefahrenzone form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.fragebogen fieldset input[type="radio"] {
|
.fragebogen fieldset input[type="radio"] {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -595,11 +725,17 @@ tbody tr:hover {
|
|||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ohne dieses Signal sieht eine breite, am Fensterrand abgeschnittene
|
||||||
|
Tabelle wie ein Rendering-Fehler aus statt wie "hier geht's mit
|
||||||
|
Scrollen weiter" — der Fade ist bewusst immer sichtbar (kein JS, das
|
||||||
|
den Scroll-Zustand prüfen könnte), auch am Ende der Tabelle. */
|
||||||
.table-scroll {
|
.table-scroll {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: 16px;
|
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%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ab hier mehr Platz (Tablet) — der Container bekommt spürbaren
|
/* Ab hier mehr Platz (Tablet) — der Container bekommt spürbaren
|
||||||
|
|||||||
@@ -12,61 +12,107 @@
|
|||||||
Pflicht.
|
Pflicht.
|
||||||
</p>
|
</p>
|
||||||
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
|
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
|
||||||
|
<div class="form-card">
|
||||||
<form method="post" action="{{if .ID}}{{.ActionBase}}/{{.ID}}{{else}}{{.ActionBase}}{{end}}">
|
<form method="post" action="{{if .ID}}{{.ActionBase}}/{{.ID}}{{else}}{{.ActionBase}}{{end}}">
|
||||||
|
<fieldset class="form-section">
|
||||||
|
<legend>Grunddaten</legend>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div>
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input type="text" id="name" name="name" value="{{.Name}}" required>
|
<input type="text" id="name" name="name" value="{{.Name}}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<label for="anbieter">Anbieter</label>
|
<label for="anbieter">Anbieter</label>
|
||||||
<input type="text" id="anbieter" name="anbieter" value="{{.Anbieter}}" required>
|
<input type="text" id="anbieter" name="anbieter" value="{{.Anbieter}}" required>
|
||||||
|
</div>
|
||||||
<fieldset>
|
<div class="form-full">
|
||||||
<legend>Verarbeitungsort</legend>
|
<label>Verarbeitungsort</label>
|
||||||
{{$gewaehlt := .Verarbeitungsort}}
|
{{$gewaehlt := .Verarbeitungsort}}
|
||||||
|
<div class="chip-group">
|
||||||
{{range .Verarbeitungsorte}}
|
{{range .Verarbeitungsorte}}
|
||||||
<label><input type="radio" name="verarbeitungsort" value="{{.}}" {{if eq . $gewaehlt}}checked{{end}} required> {{.}}</label>
|
<label><input type="radio" name="verarbeitungsort" value="{{.}}" {{if eq . $gewaehlt}}checked{{end}} required> {{.}}</label>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form-section">
|
||||||
|
<legend>Rechtliches & Datenschutz</legend>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-full">
|
||||||
|
<label>Zusicherungen</label>
|
||||||
|
<div class="chip-group">
|
||||||
<label><input type="checkbox" name="avv_verfuegbar" {{if .AVVVerfuegbar}}checked{{end}}> AVV verfügbar</label>
|
<label><input type="checkbox" name="avv_verfuegbar" {{if .AVVVerfuegbar}}checked{{end}}> AVV verfügbar</label>
|
||||||
<label for="avv_url">AVV-URL</label>
|
|
||||||
<input type="text" id="avv_url" name="avv_url" value="{{.AVVURL}}">
|
|
||||||
|
|
||||||
<label><input type="checkbox" name="training_opt_out" {{if .TrainingOptOut}}checked{{end}}> Training-Opt-out möglich</label>
|
<label><input type="checkbox" name="training_opt_out" {{if .TrainingOptOut}}checked{{end}}> Training-Opt-out möglich</label>
|
||||||
<label><input type="checkbox" name="training_standard" {{if .TrainingStandard}}checked{{end}}> Training standardmäßig aus</label>
|
<label><input type="checkbox" name="training_standard" {{if .TrainingStandard}}checked{{end}}> Training standardmäßig aus</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-full">
|
||||||
|
<label for="avv_url">AVV-URL</label>
|
||||||
|
<input type="text" id="avv_url" name="avv_url" value="{{.AVVURL}}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<label for="aufbewahrung_tage">Aufbewahrung (Tage, leer lassen wenn vom Anbieter nicht beziffert)</label>
|
<label for="aufbewahrung_tage">Aufbewahrung (Tage, leer lassen wenn vom Anbieter nicht beziffert)</label>
|
||||||
<input type="number" id="aufbewahrung_tage" name="aufbewahrung_tage" min="0" value="{{.AufbewahrungTage}}">
|
<input type="number" id="aufbewahrung_tage" name="aufbewahrung_tage" min="0" value="{{.AufbewahrungTage}}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<label for="zertifizierungen">Zertifizierungen (kommagetrennt)</label>
|
<label for="zertifizierungen">Zertifizierungen (kommagetrennt)</label>
|
||||||
<input type="text" id="zertifizierungen" name="zertifizierungen" value="{{.Zertifizierungen}}">
|
<input type="text" id="zertifizierungen" name="zertifizierungen" value="{{.Zertifizierungen}}">
|
||||||
|
</div>
|
||||||
|
<div class="form-full">
|
||||||
<label for="subprozessoren">Subprozessoren (kommagetrennt, z. B. Anthropic, AWS)</label>
|
<label for="subprozessoren">Subprozessoren (kommagetrennt, z. B. Anthropic, AWS)</label>
|
||||||
<input type="text" id="subprozessoren" name="subprozessoren" value="{{.Subprozessoren}}">
|
<input type="text" id="subprozessoren" name="subprozessoren" value="{{.Subprozessoren}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset class="form-section">
|
||||||
<legend>Geeignete Zwecke</legend>
|
<legend>Einsatzbereich</legend>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-full">
|
||||||
|
<label>Geeignete Zwecke</label>
|
||||||
|
<div class="chip-group">
|
||||||
{{range .ZweckeOptionen}}
|
{{range .ZweckeOptionen}}
|
||||||
<label><input type="checkbox" name="geeignete_zwecke" value="{{.}}" {{if index $.ZweckeAusgewaehlt .}}checked{{end}}> {{.}}</label>
|
<label><input type="checkbox" name="geeignete_zwecke" value="{{.}}" {{if index $.ZweckeAusgewaehlt .}}checked{{end}}> {{.}}</label>
|
||||||
{{end}}
|
{{end}}
|
||||||
</fieldset>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-full">
|
||||||
<label for="einschraenkungen">Einschränkungen (kommagetrennt)</label>
|
<label for="einschraenkungen">Einschränkungen (kommagetrennt)</label>
|
||||||
<input type="text" id="einschraenkungen" name="einschraenkungen" value="{{.Einschraenkungen}}">
|
<input type="text" id="einschraenkungen" name="einschraenkungen" value="{{.Einschraenkungen}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form-section">
|
||||||
|
<legend>Beleg</legend>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div>
|
||||||
<label for="letzte_pruefung">Letzte Prüfung</label>
|
<label for="letzte_pruefung">Letzte Prüfung</label>
|
||||||
<input type="date" id="letzte_pruefung" name="letzte_pruefung" value="{{.LetztePruefung}}" required>
|
<input type="date" id="letzte_pruefung" name="letzte_pruefung" value="{{.LetztePruefung}}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<label for="quelle">Quelle (Beleg)</label>
|
<label for="quelle">Quelle (Beleg)</label>
|
||||||
<input type="text" id="quelle" name="quelle" value="{{.Quelle}}" required>
|
<input type="text" id="quelle" name="quelle" value="{{.Quelle}}" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
<button type="submit">Speichern</button>
|
<button type="submit">Speichern</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{if .ID}}
|
{{if .ID}}
|
||||||
<form method="post" action="/betreiber/werkzeuge/{{.ID}}/loeschen" onsubmit="return confirm('Werkzeug wirklich löschen?')">
|
<div class="form-gefahrenzone">
|
||||||
|
<span class="hinweis">Ein gelöschtes Werkzeug kann nicht wiederhergestellt werden.</span>
|
||||||
|
<form method="post" action="{{.ActionBase}}/{{.ID}}/loeschen" onsubmit="return confirm('Werkzeug wirklich löschen?')">
|
||||||
<button type="submit" class="entfernen">Werkzeug löschen</button>
|
<button type="submit" class="entfernen">Werkzeug löschen</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<td>{{.Aufbewahrung}}</td>
|
<td>{{.Aufbewahrung}}</td>
|
||||||
<td>{{.Zertifizierungen}}</td>
|
<td>{{.Zertifizierungen}}</td>
|
||||||
<td>{{.GeeigneteZwecke}}</td>
|
<td>{{.GeeigneteZwecke}}</td>
|
||||||
<td>{{.LetztePruefung}}{{if .NichtAktuell}} <span class="status">nicht aktuell geprüft</span>{{end}}</td>
|
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{.Aufbewahrung}}</td>
|
<td>{{.Aufbewahrung}}</td>
|
||||||
<td>{{.GeeigneteZwecke}}</td>
|
<td>{{.GeeigneteZwecke}}</td>
|
||||||
<td>{{.LetztePruefung}}{{if .NichtAktuell}} <span class="status">nicht aktuell geprüft</span>{{end}}</td>
|
<td>{{.LetztePruefung}} <span class="status {{.PruefStatusCSS}}">{{.PruefStatusLabel}}</span></td>
|
||||||
<td>
|
<td>
|
||||||
{{if .Gesperrt}}
|
{{if .Gesperrt}}
|
||||||
<span class="status">gesperrt</span>
|
<span class="status">gesperrt</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user