diff --git a/gerbil-manager-web/src/api/saleAd.ts b/gerbil-manager-web/src/api/saleAd.ts
index 766cf34..11027a4 100644
--- a/gerbil-manager-web/src/api/saleAd.ts
+++ b/gerbil-manager-web/src/api/saleAd.ts
@@ -20,6 +20,9 @@ export interface SaleAdAnimal {
farbschlag: string | null
dateOfBirth: string | null
notes: string | null
+ /** FEAT-14: German trait LABELS (human-readable for the prompt), not keys. */
+ traits?: string[]
+ characterNote?: string | null
}
export interface SaleAdRequest {
diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts
index a59aa3e..ebdedff 100644
--- a/gerbil-manager-web/src/api/types.ts
+++ b/gerbil-manager-web/src/api/types.ts
@@ -34,6 +34,12 @@ export interface Gerbil {
/** Compact GEN-1 genotype string, e.g. "Aa CC Dd EE GG Pp Spsp rere" (or "?"-wildcards). */
genotype: string | null
notes: string | null
+ /**
+ * FEAT-14: Charakterbogen. Selected trait KEYS (see de.character.traits).
+ * Optional until FEAT-14a backend exposes the fields; treat absent as [].
+ */
+ characterTraits?: string[] | null
+ characterNote?: string | null
}
/** Payload for POST /gerbils. */
@@ -52,6 +58,8 @@ export interface CreateGerbil {
receiverContactId?: string | null
genotype?: string | null
notes?: string | null
+ characterTraits?: string[] | null
+ characterNote?: string | null
}
/** Payload for PUT /gerbils/{id} (all optional / partial update). */
diff --git a/gerbil-manager-web/src/components/Charakterbogen.tsx b/gerbil-manager-web/src/components/Charakterbogen.tsx
new file mode 100644
index 0000000..f8a7ad1
--- /dev/null
+++ b/gerbil-manager-web/src/components/Charakterbogen.tsx
@@ -0,0 +1,62 @@
+import { de } from '../strings/de'
+import { ALL_TRAITS } from '../format/traits'
+import './charakterbogen.css'
+
+export interface CharakterbogenProps {
+ /** Selected trait keys. */
+ traits: string[]
+ note: string
+ onTraitsChange: (traits: string[]) => void
+ onNoteChange: (note: string) => void
+}
+
+/**
+ * FEAT-14: character sheet — a checkbox grid of traits + a free note.
+ * Controlled & reusable: rendered on the animal detail page (persisted) and in
+ * the Abgabe listing composer (feeds the AI sale-text). German labels from
+ * de.character.traits; the stored value is the trait KEY.
+ */
+export default function Charakterbogen({
+ traits,
+ note,
+ onTraitsChange,
+ onNoteChange,
+}: CharakterbogenProps) {
+ const t = de.character
+ const selected = new Set(traits)
+
+ const toggle = (key: string) => {
+ const next = new Set(selected)
+ if (next.has(key)) next.delete(key)
+ else next.add(key)
+ // Preserve the vocabulary order for stable output.
+ onTraitsChange(ALL_TRAITS.filter((tr) => next.has(tr.key)).map((tr) => tr.key))
+ }
+
+ return (
+
+ )
+}
diff --git a/gerbil-manager-web/src/components/charakterbogen.css b/gerbil-manager-web/src/components/charakterbogen.css
new file mode 100644
index 0000000..9c8193d
--- /dev/null
+++ b/gerbil-manager-web/src/components/charakterbogen.css
@@ -0,0 +1,41 @@
+/* FEAT-14 Charakterbogen — trait checkbox grid (mobile-first). */
+
+.charakterbogen .trait-grid {
+ list-style: none;
+ margin: 0 0 0.75rem;
+ padding: 0;
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 0.3rem;
+}
+
+@media (min-width: 480px) {
+ .charakterbogen .trait-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+@media (min-width: 768px) {
+ .charakterbogen .trait-grid {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+}
+
+.trait-chip {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.4rem 0.6rem;
+ border: 1px solid var(--color-border);
+ border-radius: 0.5rem;
+ background: var(--color-surface);
+ cursor: pointer;
+ min-height: 44px;
+}
+
+.trait-chip input {
+ width: 1.1rem;
+ height: 1.1rem;
+ min-height: 0;
+ flex: 0 0 auto;
+}
diff --git a/gerbil-manager-web/src/format/traits.ts b/gerbil-manager-web/src/format/traits.ts
new file mode 100644
index 0000000..000de82
--- /dev/null
+++ b/gerbil-manager-web/src/format/traits.ts
@@ -0,0 +1,15 @@
+/** FEAT-14: map character trait KEYS (stored) <-> German LABELS (de.character.traits). */
+import { de } from '../strings/de'
+
+export const ALL_TRAITS = de.character.traits
+
+const LABEL_BY_KEY = new Map(de.character.traits.map((t) => [t.key, t.label]))
+
+export function traitLabel(key: string): string {
+ return LABEL_BY_KEY.get(key) ?? key
+}
+
+/** Selected keys -> German labels (for display + the AI sale-text prompt). */
+export function traitLabels(keys: readonly string[] | null | undefined): string[] {
+ return (keys ?? []).map(traitLabel)
+}
diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts
index 7083af4..394fe84 100644
--- a/gerbil-manager-web/src/strings/de.ts
+++ b/gerbil-manager-web/src/strings/de.ts
@@ -625,6 +625,34 @@ export const de = {
},
unknownFarbschlag: 'Unbekannter Farbschlag',
},
+ // ── FEAT-14 (Kevin): Charakterbogen — Eigenschaften + Notiz, speist den KI-Verkaufstext ──
+ // Traits: stabile KEYS (gespeichert) + deutsche LABELS (UI + KI-Prompt).
+ // Erweitern = eine Zeile in der Liste; Julian verfeinert die Auswahl.
+ character: {
+ sectionTitle: 'Charakter & Eigenschaften',
+ noteLabel: 'Notizen zum Charakter',
+ notePlaceholder: 'Ein paar Sätze zum Charakter dieses Tieres …',
+ save: 'Charakter speichern',
+ saved: 'Charakter gespeichert.',
+ none: 'Noch keine Eigenschaften ausgewählt.',
+ traits: [
+ { key: 'zutraulich', label: 'zutraulich' },
+ { key: 'handzahm', label: 'handzahm' },
+ { key: 'neugierig', label: 'neugierig' },
+ { key: 'aufgeschlossen', label: 'aufgeschlossen' },
+ { key: 'ruhig', label: 'ruhig / ausgeglichen' },
+ { key: 'lebhaft', label: 'lebhaft / aktiv' },
+ { key: 'verschmust', label: 'verschmust' },
+ { key: 'eigenstaendig', label: 'eigenständig' },
+ { key: 'anfaengergeeignet', label: 'anfängergeeignet' },
+ { key: 'futterfreudig', label: 'futterfreudig' },
+ { key: 'buddelt', label: 'buddelt gern' },
+ { key: 'klettert', label: 'klettert gern' },
+ { key: 'laufrad', label: 'läuft gern im Laufrad' },
+ { key: 'vertraeglich', label: 'gut verträglich' },
+ { key: 'schreckhaft', label: 'schreckhaft' },
+ ],
+ },
} as const
export type Strings = typeof de