From e4b7558abd27d980c0cacf1a4e5e42a1d6495771 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 01:07:43 +0200 Subject: [PATCH] =?UTF-8?q?FEAT-12a:=20ForSale=20scaffolding=20=E2=80=94?= =?UTF-8?q?=20status+ForSale,=20abgabe=20strings,=20/abgabe=20route+nav,?= =?UTF-8?q?=20sale-ad=20API=20stub,=20detail=20action,=20jszip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gerbil-manager-web/package-lock.json | 164 ++++++++++++++++++ gerbil-manager-web/package.json | 2 + gerbil-manager-web/src/App.tsx | 2 + gerbil-manager-web/src/api/saleAd.ts | 37 ++++ gerbil-manager-web/src/api/types.ts | 6 +- .../src/components/AppShell.tsx | 1 + gerbil-manager-web/src/pages/AbgabePage.tsx | 11 ++ .../src/pages/GerbilDetailPage.tsx | 18 +- gerbil-manager-web/src/strings/de.ts | 57 ++++++ 9 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 gerbil-manager-web/src/api/saleAd.ts create mode 100644 gerbil-manager-web/src/pages/AbgabePage.tsx diff --git a/gerbil-manager-web/package-lock.json b/gerbil-manager-web/package-lock.json index b6c48f6..85dc9ff 100644 --- a/gerbil-manager-web/package-lock.json +++ b/gerbil-manager-web/package-lock.json @@ -8,6 +8,7 @@ "name": "gerbil-manager-web", "version": "0.0.0", "dependencies": { + "jszip": "^3.10.1", "react": "^19.2.6", "react-d3-tree": "^3.6.6", "react-dom": "^19.2.6", @@ -15,6 +16,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.60.0", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -603,6 +605,22 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -1528,6 +1546,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2136,6 +2160,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2146,6 +2176,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2169,6 +2205,12 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2229,6 +2271,18 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2253,6 +2307,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2694,6 +2757,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2741,6 +2810,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -2780,6 +2896,12 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2893,6 +3015,21 @@ "react-dom": ">=18" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -2927,6 +3064,12 @@ "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2949,6 +3092,12 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3003,6 +3152,15 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3167,6 +3325,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/gerbil-manager-web/package.json b/gerbil-manager-web/package.json index 7b59a50..655940d 100644 --- a/gerbil-manager-web/package.json +++ b/gerbil-manager-web/package.json @@ -12,6 +12,7 @@ "test:watch": "vitest" }, "dependencies": { + "jszip": "^3.10.1", "react": "^19.2.6", "react-d3-tree": "^3.6.6", "react-dom": "^19.2.6", @@ -19,6 +20,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.60.0", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/gerbil-manager-web/src/App.tsx b/gerbil-manager-web/src/App.tsx index 5243e54..99b054f 100644 --- a/gerbil-manager-web/src/App.tsx +++ b/gerbil-manager-web/src/App.tsx @@ -7,6 +7,7 @@ import GerbilsPage from './pages/GerbilsPage' import GerbilDetailPage from './pages/GerbilDetailPage' import GerbilFormPage from './pages/GerbilFormPage' import GenetikPage from './pages/GenetikPage' +import AbgabePage from './pages/AbgabePage' import HomePage from './pages/HomePage' import KontaktePage from './pages/KontaktePage' import KontaktDetailPage from './pages/KontaktDetailPage' @@ -51,6 +52,7 @@ export default function App() { } /> } /> + } /> } /> } /> diff --git a/gerbil-manager-web/src/api/saleAd.ts b/gerbil-manager-web/src/api/saleAd.ts new file mode 100644 index 0000000..766cf34 --- /dev/null +++ b/gerbil-manager-web/src/api/saleAd.ts @@ -0,0 +1,37 @@ +/** + * Sale-ad (Abgabe-Inserat) AI assist — FEAT-12a frontend stub. + * + * The backend endpoint (POST /gerbils/sale-ad, Claude-powered) is FEAT-12a-backend + * and lands only once Julian provides an API key. Until then this call 404s/503s + * and the UI shows a German "API-Schlüssel noch nicht konfiguriert" disabled state. + * + * REQUEST SHAPE (defined here; backend must match): + * { + * animals: [{ name, farbschlag, dateOfBirth, notes }], + * statusLine: string, // e.g. "FREI" / "LOCKER RESERVIERT Anna" + * hints: string // free-text style/wishes from the user + * } + * -> { text: string } // the assembled/improved German listing + */ +import { api } from './client' + +export interface SaleAdAnimal { + name: string + farbschlag: string | null + dateOfBirth: string | null + notes: string | null +} + +export interface SaleAdRequest { + animals: SaleAdAnimal[] + statusLine: string + hints: string +} + +export interface SaleAdResponse { + text: string +} + +export function generateSaleAd(request: SaleAdRequest): Promise { + return api.post('/gerbils/sale-ad', request) +} diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts index 91312be..e9f62a4 100644 --- a/gerbil-manager-web/src/api/types.ts +++ b/gerbil-manager-web/src/api/types.ts @@ -10,9 +10,9 @@ export type Gender = 'unknown' | 'male' | 'female' export const GENDERS: Gender[] = ['unknown', 'male', 'female'] -/** Gerbil.Status — C# enum { Active, Deceased, GivenAway }. */ -export type GerbilStatus = 'Active' | 'Deceased' | 'GivenAway' -export const GERBIL_STATUSES: GerbilStatus[] = ['Active', 'Deceased', 'GivenAway'] +/** Gerbil.Status — C# enum { Active, Deceased, GivenAway, ForSale }. */ +export type GerbilStatus = 'Active' | 'Deceased' | 'GivenAway' | 'ForSale' +export const GERBIL_STATUSES: GerbilStatus[] = ['Active', 'ForSale', 'Deceased', 'GivenAway'] /** ISO date string "YYYY-MM-DD" (maps to C# DateOnly). */ export type DateOnlyString = string diff --git a/gerbil-manager-web/src/components/AppShell.tsx b/gerbil-manager-web/src/components/AppShell.tsx index 8fedf11..dc45d86 100644 --- a/gerbil-manager-web/src/components/AppShell.tsx +++ b/gerbil-manager-web/src/components/AppShell.tsx @@ -21,6 +21,7 @@ const PRIMARY: NavItem[] = [ const SECONDARY: NavItem[] = [ { to: '/becken', label: de.nav.enclosures, icon: '🛁' }, { to: '/kontakte', label: de.nav.contacts, icon: '📇' }, + { to: '/abgabe', label: de.nav.forSale, icon: '🏡' }, { to: '/statistik', label: de.nav.statistics, icon: '📊' }, ] diff --git a/gerbil-manager-web/src/pages/AbgabePage.tsx b/gerbil-manager-web/src/pages/AbgabePage.tsx new file mode 100644 index 0000000..659958f --- /dev/null +++ b/gerbil-manager-web/src/pages/AbgabePage.tsx @@ -0,0 +1,11 @@ +import { de } from '../strings/de' + +export default function AbgabePage() { + // Fleshed out in the next FEAT-12a increments (grouping + listing composer + export). + return ( +
+

{de.pages.abgabe.title}

+

{de.pages.abgabe.subtitle}

+
+ ) +} diff --git a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx index 68d89d7..fbeae2b 100644 --- a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx +++ b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx @@ -1,9 +1,9 @@ import { useMemo, useState, type ReactNode } from 'react' import { Link, useParams } from 'react-router-dom' import { de } from '../strings/de' -import { getGerbil } from '../api/gerbils' +import { getGerbil, updateGerbil } from '../api/gerbils' import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups' -import { useApi } from '../hooks/useApi' +import { useApi, useMutation } from '../hooks/useApi' import { formatDate, genderLabel, statusLabel } from '../format/labels' import { fromDisplayString, genotypeToFarbschlag, toDisplayString } from '../genetics' import FarbschlagImage from '../components/FarbschlagImage' @@ -42,6 +42,7 @@ export default function GerbilDetailPage() { const [tab, setTab] = useState('photos') const gerbil = useApi(() => getGerbil(id), [id]) + const forSale = useMutation(() => updateGerbil(id, { status: 'ForSale' })) const colorVarieties = useApi(() => listColorVarieties(), []) const enclosures = useApi(() => listEnclosures(), []) const contacts = useApi(() => listContacts(), []) @@ -96,6 +97,19 @@ export default function GerbilDetailPage() { {t.detail.testMating} + {g.status !== 'ForSale' && ( + + )} {de.pages.stammbaum.openButton} diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts index edf9f18..81a0a36 100644 --- a/gerbil-manager-web/src/strings/de.ts +++ b/gerbil-manager-web/src/strings/de.ts @@ -21,6 +21,8 @@ export const de = { statistics: 'Statistik', // FEAT-7 (Kevin): „Mehr“-Overflow auf dem Smartphone more: 'Mehr', + // FEAT-12a (Kevin): Abgabe + forSale: 'Abgabe', openMenu: 'Menü öffnen', closeMenu: 'Menü schließen', mainNavigation: 'Hauptnavigation', @@ -119,6 +121,7 @@ export const de = { Active: 'Aktiv', Deceased: 'Verstorben', GivenAway: 'Abgegeben', + ForSale: 'Zur Abgabe', }, }, // ── FEAT-3 (Kevin): Zucht — Würfe + Zuchtpaare ── @@ -221,6 +224,60 @@ export const de = { noOffspring: 'Keine lebend geborenen Nachkommen möglich.', warningsTitle: 'Hinweise', }, + // ── FEAT-12a (Kevin): Abgabe (For-Sale / Inserate) ── + abgabe: { + title: 'Abgabe', + subtitle: 'Abgabetiere gruppieren und Inserate erstellen', + empty: 'Keine Tiere zur Abgabe. Stelle Tiere über „Zur Abgabe stellen“ ein.', + markAction: 'Zur Abgabe stellen', + markMultiAction: 'Ausgewählte zur Abgabe stellen', + grouping: { + title: 'Gruppen', + byEnclosure: 'Nach Becken vorgeschlagen', + ungrouped: 'Ohne Becken', + moveTo: 'Verschieben nach', + newGroup: 'Neue Gruppe', + groupLabel: 'Gruppe', + }, + // Inserat-Komposer (Vorlage der Züchterin) + listing: { + headingFor: (n: number, gender: string) => `Gruppe ${n} (${gender})`, + genderMale: 'männlich', + genderFemale: 'weiblich', + genderMixed: 'gemischt', + status: 'Status', + statusFree: 'FREI', + statusLooseReserved: 'LOCKER RESERVIERT', + statusReserved: 'RESERVIERT', + reservedName: 'Name (bei Reservierung)', + tagline: 'Überschrift (emotionaler Einzeiler)', + taglinePlaceholder: 'z. B. „Großer und kleiner Bruder“ – Männliches Rennmaus-Duo sucht ein Zuhause', + bornOn: 'geboren am', + personality: 'Charakter', + personalityPlaceholder: 'Ein paar Sätze zum Charakter des Tieres …', + photos: 'Fotos', + noPhotos: 'Für dieses Tier sind keine Fotos hinterlegt.', + }, + // Export / Paste-Paket + export: { + copyText: 'Text kopieren', + copied: 'Text in die Zwischenablage kopiert.', + copyFailed: 'Kopieren fehlgeschlagen.', + downloadPhotos: 'Fotos herunterladen', + zipping: 'Fotos werden gepackt …', + noPhotosSelected: 'Keine Fotos ausgewählt.', + zipFailed: 'Fotos konnten nicht gepackt werden.', + finish: 'Abgabe abschließen', + finishHint: 'Vertrag erstellen (folgt in einem späteren Schritt).', + }, + // KI-Assistent (Backend folgt mit API-Schlüssel) + ai: { + improve: 'Text mit KI verbessern', + generating: 'KI erstellt den Text …', + hintsLabel: 'Hinweise für die KI (optional)', + notConfigured: 'KI-Funktion: API-Schlüssel noch nicht konfiguriert.', + }, + }, // ── FEAT-2 (Oscar): Becken (Enclosures) ── becken: { title: 'Becken',