Merge feature/gehege-rename: Becken→Gehege + Terrarium-Icon + Start-Nav raus, /rennmaeuse = Landing

- UI-Begriff 'Becken' → 'Gehege' (de.ts, Becken-Pages, Abgabe/Hilfe), Route /becken→/gehege+Redirect
- Nav-Icon Badewanne → Terrarium; 'Start'-Nav-Eintrag entfernt
- Index-Route '/' → Navigate /rennmaeuse (Landing Page), HomePage.tsx geloescht
- e2e navigation/becken-kontakte angepasst
Frontend-only, kein EF. [god-QA: disjunkt zu main-Commits, Home-Assertions nur in navigation.spec (selbst gefixt), live-data.spec referenziert kein '/']

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 17:01:09 +02:00
10 changed files with 45 additions and 56 deletions

View File

@@ -7,7 +7,7 @@ const tk = de.pages.kontakte
test.describe('Becken', () => { test.describe('Becken', () => {
test('Belegung: Detail zeigt Bewohner mit Link zur Tierakte', async ({ page }) => { test('Belegung: Detail zeigt Bewohner mit Link zur Tierakte', async ({ page }) => {
skipUnlessMock() skipUnlessMock()
await page.goto('/becken') await page.goto('/gehege')
await page.getByRole('link', { name: /Großbecken/ }).click() await page.getByRole('link', { name: /Großbecken/ }).click()
await expect(page.getByRole('heading', { name: 'Großbecken' })).toBeVisible() await expect(page.getByRole('heading', { name: 'Großbecken' })).toBeVisible()
await expect(page.getByText(`2 ${tb.occupancy.countMany}`)).toBeVisible() await expect(page.getByText(`2 ${tb.occupancy.countMany}`)).toBeVisible()
@@ -18,7 +18,7 @@ test.describe('Becken', () => {
test('Löschen eines belegten Beckens meldet den 409-Konflikt auf Deutsch', async ({ page }) => { test('Löschen eines belegten Beckens meldet den 409-Konflikt auf Deutsch', async ({ page }) => {
skipUnlessMock() skipUnlessMock()
await page.goto('/becken') await page.goto('/gehege')
await page.getByRole('link', { name: /Großbecken/ }).click() await page.getByRole('link', { name: /Großbecken/ }).click()
acceptNextDialog(page) acceptNextDialog(page)
await page.getByRole('button', { name: tb.delete.action }).click() await page.getByRole('button', { name: tb.delete.action }).click()

View File

@@ -4,7 +4,7 @@ import { de, expect, gotoSection, test } from './fixtures'
test('alle Bereiche sind über die Navigation erreichbar', async ({ page }) => { test('alle Bereiche sind über die Navigation erreichbar', async ({ page }) => {
await page.goto('/') await page.goto('/')
await expect(page.getByRole('heading', { name: de.app.title })).toBeVisible() await expect(page.getByRole('heading', { name: de.app.title })).toBeVisible()
await expect(page.getByRole('heading', { name: de.pages.home.title })).toBeVisible() await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
await gotoSection(page, de.nav.gerbils) await gotoSection(page, de.nav.gerbils)
await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible() await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
@@ -30,5 +30,5 @@ test('unbekannte Route zeigt die deutsche 404-Seite', async ({ page }) => {
await page.goto('/diese-seite-gibt-es-nicht') await page.goto('/diese-seite-gibt-es-nicht')
await expect(page.getByText(de.pages.notFound.message)).toBeVisible() await expect(page.getByText(de.pages.notFound.message)).toBeVisible()
await page.getByRole('link', { name: de.pages.notFound.backHome }).click() await page.getByRole('link', { name: de.pages.notFound.backHome }).click()
await expect(page.getByRole('heading', { name: de.pages.home.title })).toBeVisible() await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
}) })

View File

@@ -1,4 +1,4 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom' import { BrowserRouter, Navigate, Route, Routes, useParams } from 'react-router-dom'
import AppShell from './components/AppShell' import AppShell from './components/AppShell'
import BeckenPage from './pages/BeckenPage' import BeckenPage from './pages/BeckenPage'
import BeckenDetailPage from './pages/BeckenDetailPage' import BeckenDetailPage from './pages/BeckenDetailPage'
@@ -8,7 +8,6 @@ import GerbilDetailPage from './pages/GerbilDetailPage'
import GerbilFormPage from './pages/GerbilFormPage' import GerbilFormPage from './pages/GerbilFormPage'
import GenetikPage from './pages/GenetikPage' import GenetikPage from './pages/GenetikPage'
import AbgabePage from './pages/AbgabePage' import AbgabePage from './pages/AbgabePage'
import HomePage from './pages/HomePage'
import KontaktePage from './pages/KontaktePage' import KontaktePage from './pages/KontaktePage'
import KontaktDetailPage from './pages/KontaktDetailPage' import KontaktDetailPage from './pages/KontaktDetailPage'
import KontaktFormPage from './pages/KontaktFormPage' import KontaktFormPage from './pages/KontaktFormPage'
@@ -28,12 +27,17 @@ import WebseiteVorschauPage from './pages/WebseiteVorschauPage'
import AnfragenPage from './pages/AnfragenPage' import AnfragenPage from './pages/AnfragenPage'
import AnfrageDetailPage from './pages/AnfrageDetailPage' import AnfrageDetailPage from './pages/AnfrageDetailPage'
function BeckenRedirect() {
const { '*': splat } = useParams()
return <Navigate to={`/gehege${splat ? `/${splat}` : ''}`} replace />
}
export default function App() { export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route element={<AppShell />}> <Route element={<AppShell />}>
<Route index element={<HomePage />} /> <Route index element={<Navigate to="/rennmaeuse" replace />} />
<Route path="rennmaeuse"> <Route path="rennmaeuse">
<Route index element={<GerbilsPage />} /> <Route index element={<GerbilsPage />} />
<Route path="neu" element={<GerbilFormPage />} /> <Route path="neu" element={<GerbilFormPage />} />
@@ -48,7 +52,9 @@ export default function App() {
<Route path=":id/bearbeiten" element={<WurfFormPage />} /> <Route path=":id/bearbeiten" element={<WurfFormPage />} />
</Route> </Route>
{/* FEAT-2: Kontakte ersetzt das Züchter-Platzhalter-Routing (Breeder -> Contact) */} {/* FEAT-2: Kontakte ersetzt das Züchter-Platzhalter-Routing (Breeder -> Contact) */}
<Route path="becken"> {/* GEHEGE-RENAME: /becken → /gehege; redirect preserves bookmarks */}
<Route path="becken/*" element={<BeckenRedirect />} />
<Route path="gehege">
<Route index element={<BeckenPage />} /> <Route index element={<BeckenPage />} />
<Route path="neu" element={<BeckenFormPage />} /> <Route path="neu" element={<BeckenFormPage />} />
<Route path=":id" element={<BeckenDetailPage />} /> <Route path=":id" element={<BeckenDetailPage />} />

View File

@@ -13,13 +13,12 @@ interface NavItem {
// Primary items are always visible in the phone bottom bar; secondary items // Primary items are always visible in the phone bottom bar; secondary items
// move into the "Mehr" sheet on phones and appear inline in the desktop sidebar. // move into the "Mehr" sheet on phones and appear inline in the desktop sidebar.
const PRIMARY: NavItem[] = [ const PRIMARY: NavItem[] = [
{ to: '/', label: de.nav.home, icon: '🏠', end: true },
{ to: '/rennmaeuse', label: de.nav.gerbils, icon: '🐭' }, { to: '/rennmaeuse', label: de.nav.gerbils, icon: '🐭' },
{ to: '/wuerfe', label: de.nav.litters, icon: '🍼' }, { to: '/wuerfe', label: de.nav.litters, icon: '🍼' },
{ to: '/genetik', label: de.nav.genetics, icon: '🧬' }, { to: '/genetik', label: de.nav.genetics, icon: '🧬' },
] ]
const SECONDARY: NavItem[] = [ const SECONDARY: NavItem[] = [
{ to: '/becken', label: de.nav.enclosures, icon: '🛁' }, { to: '/gehege', label: de.nav.enclosures, icon: '🏜️' },
{ to: '/kontakte', label: de.nav.contacts, icon: '📇' }, { to: '/kontakte', label: de.nav.contacts, icon: '📇' },
{ to: '/abgabe', label: de.nav.forSale, icon: '🏡' }, { to: '/abgabe', label: de.nav.forSale, icon: '🏡' },
// INBOX-1: Anfragen-Posteingang // INBOX-1: Anfragen-Posteingang

View File

@@ -44,7 +44,7 @@ export default function BeckenDetailPage() {
setDeleteError(null) setDeleteError(null)
const result = await removal.run() const result = await removal.run()
if (result.ok) { if (result.ok) {
navigate('/becken') navigate('/gehege')
} else if (result.cause instanceof ApiError && result.cause.status === 409) { } else if (result.cause instanceof ApiError && result.cause.status === 409) {
// Backend meldet Konflikt, wenn noch Tiere im Becken wohnen. // Backend meldet Konflikt, wenn noch Tiere im Becken wohnen.
setDeleteError(t.delete.conflict) setDeleteError(t.delete.conflict)
@@ -58,7 +58,7 @@ export default function BeckenDetailPage() {
return ( return (
<section className="page"> <section className="page">
<p className="muted">{enclosure.error ?? t.detail.notFound}</p> <p className="muted">{enclosure.error ?? t.detail.notFound}</p>
<Link to="/becken" className="btn"> <Link to="/gehege" className="btn">
{t.detail.back} {t.detail.back}
</Link> </Link>
</section> </section>
@@ -81,7 +81,7 @@ export default function BeckenDetailPage() {
)} )}
</div> </div>
<div className="head-actions"> <div className="head-actions">
<Link to={`/becken/${e.id}/bearbeiten`} className="btn btn--primary"> <Link to={`/gehege/${e.id}/bearbeiten`} className="btn btn--primary">
{t.detail.edit} {t.detail.edit}
</Link> </Link>
<button <button
@@ -92,7 +92,7 @@ export default function BeckenDetailPage() {
> >
{t.delete.action} {t.delete.action}
</button> </button>
<Link to="/becken" className="btn"> <Link to="/gehege" className="btn">
{t.detail.back} {t.detail.back}
</Link> </Link>
</div> </div>

View File

@@ -45,7 +45,7 @@ export default function BeckenFormPage() {
} }
setErrors({}) setErrors({})
const result = await mutation.run({ name: form.name.trim(), notes: nn(form.notes) }) const result = await mutation.run({ name: form.name.trim(), notes: nn(form.notes) })
if (result.ok) navigate(`/becken/${result.value.id}`) if (result.ok) navigate(`/gehege/${result.value.id}`)
// On failure mutation.error drives the inline alert; run() never throws. // On failure mutation.error drives the inline alert; run() never throws.
} }
@@ -81,7 +81,7 @@ export default function BeckenFormPage() {
<button type="submit" className="btn btn--primary" disabled={mutation.pending}> <button type="submit" className="btn btn--primary" disabled={mutation.pending}>
{mutation.pending ? t.form.saving : t.form.save} {mutation.pending ? t.form.saving : t.form.save}
</button> </button>
<Link to={isEdit && id ? `/becken/${id}` : '/becken'} className="btn"> <Link to={isEdit && id ? `/gehege/${id}` : '/gehege'} className="btn">
{t.form.cancel} {t.form.cancel}
</Link> </Link>
</div> </div>

View File

@@ -35,7 +35,7 @@ export default function BeckenPage() {
</p> </p>
)} )}
</div> </div>
<Link to="/becken/neu" className="btn btn--primary"> <Link to="/gehege/neu" className="btn btn--primary">
+ {t.newButton} + {t.newButton}
</Link> </Link>
</header> </header>
@@ -72,7 +72,7 @@ export default function BeckenPage() {
<ul className="card-list"> <ul className="card-list">
{items.map((e) => ( {items.map((e) => (
<li key={e.id}> <li key={e.id}>
<Link to={`/becken/${e.id}`} className="gerbil-card"> <Link to={`/gehege/${e.id}`} className="gerbil-card">
<span className="gerbil-card__name">{e.name}</span> <span className="gerbil-card__name">{e.name}</span>
<span className="gerbil-card__meta">{e.notes ?? ''}</span> <span className="gerbil-card__meta">{e.notes ?? ''}</span>
</Link> </Link>

View File

@@ -38,12 +38,12 @@ const sections: Section[] = [
<p>Die Navigation erreichst du unten (Smartphone) oder links (Laptop). Mit <strong>{nav.more}"</strong> öffnest du alle weiteren Bereiche.</p> <p>Die Navigation erreichst du unten (Smartphone) oder links (Laptop). Mit <strong>{nav.more}"</strong> öffnest du alle weiteren Bereiche.</p>
<p>Empfohlene Reihenfolge beim ersten Mal:</p> <p>Empfohlene Reihenfolge beim ersten Mal:</p>
<ol> <ol>
<li>Becken anlegen (wo leben deine Tiere?)</li> <li>Gehege anlegen (wo leben deine Tiere?)</li>
<li>Rennmäuse anlegen mit Foto</li> <li>Rennmäuse anlegen mit Foto</li>
<li>Würfe eintragen</li> <li>Würfe eintragen</li>
<li>Genetik erkunden</li> <li>Genetik erkunden</li>
</ol> </ol>
<p className="hilfe-hint">Tipp: Lege zuerst die Becken an — dann kannst du beim Erfassen der Tiere direkt das richtige Becken auswählen.</p> <p className="hilfe-hint">Tipp: Lege zuerst die Gehege an — dann kannst du beim Erfassen der Tiere direkt das richtige Gehege auswählen.</p>
</> </>
), ),
}, },
@@ -53,7 +53,7 @@ const sections: Section[] = [
content: ( content: (
<> <>
<p>Unter <strong>„{nav.gerbils}"</strong> siehst du alle deine Rennmäuse. Mit <strong>{g.newButton}"</strong> legst du ein neues Tier an.</p> <p>Unter <strong>„{nav.gerbils}"</strong> siehst du alle deine Rennmäuse. Mit <strong>{g.newButton}"</strong> legst du ein neues Tier an.</p>
<p>Pflichtfelder sind <strong>„{g.fields.name}"</strong> und <strong>{g.fields.gender}"</strong>. Optional kannst du Geburtsdatum, Farbschlag, Genotyp, Becken und Herkunft eintragen.</p> <p>Pflichtfelder sind <strong>„{g.fields.name}"</strong> und <strong>{g.fields.gender}"</strong>. Optional kannst du Geburtsdatum, Farbschlag, Genotyp, Gehege und Herkunft eintragen.</p>
<p>Auf der Detailseite findest du drei Tabs:</p> <p>Auf der Detailseite findest du drei Tabs:</p>
<ul> <ul>
<li><strong>„{g.detail.tabs.photos}"</strong> Fotos hochladen; das erste Foto wird als Profilfoto angezeigt.</li> <li><strong>„{g.detail.tabs.photos}"</strong> Fotos hochladen; das erste Foto wird als Profilfoto angezeigt.</li>
@@ -69,9 +69,9 @@ const sections: Section[] = [
title: t.sections.becken, title: t.sections.becken,
content: ( content: (
<> <>
<p>Unter <strong>{nav.enclosures}"</strong> verwaltest du deine Gehege. Mit <strong>„{b.newButton}"</strong> legst du ein neues Becken an (nur Name ist Pflicht).</p> <p>Unter <strong>{nav.enclosures}"</strong> verwaltest du deine Gehege. Mit <strong>„{b.newButton}"</strong> legst du ein neues Gehege an (nur Name ist Pflicht).</p>
<p>Die Detailseite zeigt dir die aktuellen <strong>{b.occupancy.title}"</strong> — mit Links direkt zur Tierakte. So siehst du auf einen Blick, wer wo lebt.</p> <p>Die Detailseite zeigt dir die aktuellen <strong>{b.occupancy.title}"</strong> — mit Links direkt zur Tierakte. So siehst du auf einen Blick, wer wo lebt.</p>
<p>Ein Becken lässt sich nur löschen, wenn es leer ist — das schützt vor versehentlichem Datenverlust.</p> <p>Ein Gehege lässt sich nur löschen, wenn es leer ist — das schützt vor versehentlichem Datenverlust.</p>
<p>Unter <strong>„{nav.contacts}"</strong> kannst du Züchter, Käufer und andere Kontakte anlegen. Diese lassen sich dann als Herkunft oder Abnehmer bei Tieren eintragen.</p> <p>Unter <strong>„{nav.contacts}"</strong> kannst du Züchter, Käufer und andere Kontakte anlegen. Diese lassen sich dann als Herkunft oder Abnehmer bei Tieren eintragen.</p>
</> </>
), ),

View File

@@ -1,10 +0,0 @@
import { de } from '../strings/de'
export default function HomePage() {
return (
<section>
<h2>{de.pages.home.title}</h2>
<p>{de.pages.home.intro}</p>
</section>
)
}

View File

@@ -9,13 +9,12 @@ export const de = {
tagline: 'Verwaltung für Rennmäuse, Würfe und Züchter', tagline: 'Verwaltung für Rennmäuse, Würfe und Züchter',
}, },
nav: { nav: {
home: 'Start',
gerbils: 'Rennmäuse', gerbils: 'Rennmäuse',
litters: 'Würfe', litters: 'Würfe',
breeders: 'Züchter', breeders: 'Züchter',
genetics: 'Genetik', genetics: 'Genetik',
// FEAT-2 (Oscar): Becken + Kontakte // FEAT-2 (Oscar): Becken + Kontakte (GEHEGE-RENAME: Becken → Gehege)
enclosures: 'Becken', enclosures: 'Gehege',
contacts: 'Kontakte', contacts: 'Kontakte',
// FEAT-7 (Kelly): Statistik // FEAT-7 (Kelly): Statistik
statistics: 'Statistik', statistics: 'Statistik',
@@ -37,11 +36,6 @@ export const de = {
website: 'Webseite', website: 'Webseite',
}, },
pages: { pages: {
home: {
title: 'Willkommen',
intro:
'Hier verwaltest du deine Rennmäuse, Würfe und Züchter. Wähle einen Bereich aus der Navigation.',
},
gerbils: { gerbils: {
title: 'Rennmäuse', title: 'Rennmäuse',
// Liste // Liste
@@ -82,7 +76,7 @@ export const de = {
causeOfDeath: 'Todesursache', causeOfDeath: 'Todesursache',
goHomeDate: 'Abgabedatum', goHomeDate: 'Abgabedatum',
litter: 'Wurf', litter: 'Wurf',
enclosure: 'Becken', enclosure: 'Gehege',
colorVariety: 'Farbschlag', colorVariety: 'Farbschlag',
origin: 'Herkunft', origin: 'Herkunft',
receiver: 'Abnehmer', receiver: 'Abnehmer',
@@ -253,8 +247,8 @@ export const de = {
markMultiAction: 'Ausgewählte zur Abgabe stellen', markMultiAction: 'Ausgewählte zur Abgabe stellen',
grouping: { grouping: {
title: 'Gruppen', title: 'Gruppen',
byEnclosure: 'Nach Becken vorgeschlagen', byEnclosure: 'Nach Gehege vorgeschlagen',
ungrouped: 'Ohne Becken', ungrouped: 'Ohne Gehege',
moveTo: 'Verschieben nach', moveTo: 'Verschieben nach',
newGroup: 'Neue Gruppe', newGroup: 'Neue Gruppe',
groupLabel: 'Gruppe', groupLabel: 'Gruppe',
@@ -298,12 +292,12 @@ export const de = {
notConfigured: 'KI-Funktion: API-Schlüssel noch nicht konfiguriert.', notConfigured: 'KI-Funktion: API-Schlüssel noch nicht konfiguriert.',
}, },
}, },
// ── FEAT-2 (Oscar): Becken (Enclosures) ── // ── FEAT-2 (Oscar): Gehege (Enclosures) — GEHEGE-RENAME: Becken → Gehege ──
becken: { becken: {
title: 'Becken', title: 'Gehege',
newButton: 'Neues Becken', newButton: 'Neues Gehege',
empty: 'Keine Becken gefunden.', empty: 'Keine Gehege gefunden.',
countLabel: 'Becken', countLabel: 'Gehege',
searchPlaceholder: 'Name suchen …', searchPlaceholder: 'Name suchen …',
fields: { fields: {
name: 'Name', name: 'Name',
@@ -311,18 +305,18 @@ export const de = {
}, },
occupancy: { occupancy: {
title: 'Bewohner', title: 'Bewohner',
empty: 'Dieses Becken ist derzeit leer.', empty: 'Dieses Gehege ist derzeit leer.',
countOne: 'Tier', countOne: 'Tier',
countMany: 'Tiere', countMany: 'Tiere',
}, },
detail: { detail: {
edit: 'Bearbeiten', edit: 'Bearbeiten',
back: 'Zurück zur Liste', back: 'Zurück zur Liste',
notFound: 'Dieses Becken wurde nicht gefunden.', notFound: 'Dieses Gehege wurde nicht gefunden.',
}, },
form: { form: {
createTitle: 'Neues Becken anlegen', createTitle: 'Neues Gehege anlegen',
editTitle: 'Becken bearbeiten', editTitle: 'Gehege bearbeiten',
save: 'Speichern', save: 'Speichern',
cancel: 'Abbrechen', cancel: 'Abbrechen',
saving: 'Speichern …', saving: 'Speichern …',
@@ -332,9 +326,9 @@ export const de = {
}, },
delete: { delete: {
action: 'Löschen', action: 'Löschen',
confirmMessage: 'Soll dieses Becken wirklich gelöscht werden?', confirmMessage: 'Soll dieses Gehege wirklich gelöscht werden?',
conflict: conflict:
'Becken ist nicht leer — bitte zuerst alle Tiere in ein anderes Becken umsetzen.', 'Gehege ist nicht leer — bitte zuerst alle Tiere in ein anderes Gehege umsetzen.',
}, },
}, },
// ── FEAT-4 (Kelly): Stammbaum (Ahnentafel) ── // ── FEAT-4 (Kelly): Stammbaum (Ahnentafel) ──
@@ -731,7 +725,7 @@ export const de = {
sections: { sections: {
ersteSchritte: 'Erste Schritte', ersteSchritte: 'Erste Schritte',
tiere: 'Rennmäuse anlegen & Fotos', tiere: 'Rennmäuse anlegen & Fotos',
becken: 'Becken', becken: 'Gehege',
wuerfe: 'Würfe & Jungtiere', wuerfe: 'Würfe & Jungtiere',
genetik: 'Probeverpaarung & Genetik', genetik: 'Probeverpaarung & Genetik',
stammbaum: 'Stammbaum', stammbaum: 'Stammbaum',