HELP-1: in-app German user guide (/hilfe, accordion, 9 sections)

HilfePage.tsx: accordion via <details>/<summary> (native, accessible, mobile-first);
9 sections in du-Form: Erste Schritte, Rennmaeuse, Becken, Wuerfe, Genetik,
Stammbaum, Abgabe-Ablauf, Statistik, Datensicherung. All UI labels imported from
de.ts (rename-safe); SCHECKE/Rex warnings verbatim from genetics.warnings.

de.ts: de.nav.help + de.hilfe (title, subtitle, 9 section keys)
AppShell.tsx: Hilfe (heart question mark) added to SECONDARY nav (Mehr-sheet on phone,
sidebar on desktop)
App.tsx: Route path=hilfe -> <HilfePage />
hilfe.css: accordion, hint boxes, warning colour; piggybacks on existing CSS vars

e2e/hilfe.spec.ts: 3 smoke tests x 2 viewports = 6 new tests
  -- Hilfe reachable via nav
  -- First accordion section expands on click
  -- SCHECKE warning visible in Genetik section

Full evidence: dotnet test 18/18, npm test 47/47, npm run build ok,
npm run e2e 54/54 (48 existing + 6 new hilfe tests).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 07:47:45 +02:00
parent 42718606a9
commit 98060b890b
6 changed files with 358 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
/**
* HELP-1: Hilfe-Seite — Smoke-Test (erreichbar + Accordion öffnet sich).
* Kein Mock-API nötig (rein statische Seite).
*/
import { de, expect, gotoSection, test } from './fixtures'
const th = de.hilfe
test.describe('Hilfe', () => {
test('Hilfe-Seite ist über die Navigation erreichbar', async ({ page }) => {
await gotoSection(page, de.nav.help)
await expect(page.getByRole('heading', { name: th.title })).toBeVisible()
await expect(page.getByText(th.subtitle)).toBeVisible()
})
test('Akkordeon-Abschnitt öffnet sich per Tippen', async ({ page }) => {
await page.goto('/hilfe')
const firstSummary = page.getByText(th.sections.ersteSchritte)
await expect(firstSummary).toBeVisible()
// Abschnitt ist zunächst geschlossen — Inhalt nicht sichtbar
const sectionBody = page.getByText('Empfohlene Reihenfolge')
await expect(sectionBody).not.toBeVisible()
// Tippen öffnet den Abschnitt
await firstSummary.click()
await expect(sectionBody).toBeVisible()
})
test('SCHECKE-Warnung ist im Genetik-Abschnitt enthalten', async ({ page }) => {
await page.goto('/hilfe')
// Öffne Genetik-Abschnitt
await page.getByText(th.sections.genetik).click()
await expect(page.getByText('Schecke × Schecke', { exact: false })).toBeVisible()
})
})

View File

@@ -18,6 +18,7 @@ import WurfFormPage from './pages/WurfFormPage'
import NotFoundPage from './pages/NotFoundPage' import NotFoundPage from './pages/NotFoundPage'
import StammbaumPage from './pages/StammbaumPage' import StammbaumPage from './pages/StammbaumPage'
import StatistikPage from './pages/StatistikPage' import StatistikPage from './pages/StatistikPage'
import HilfePage from './pages/HilfePage'
export default function App() { export default function App() {
return ( return (
@@ -54,6 +55,7 @@ export default function App() {
<Route path="genetik" element={<GenetikPage />} /> <Route path="genetik" element={<GenetikPage />} />
<Route path="abgabe" element={<AbgabePage />} /> <Route path="abgabe" element={<AbgabePage />} />
<Route path="statistik" element={<StatistikPage />} /> <Route path="statistik" element={<StatistikPage />} />
<Route path="hilfe" element={<HilfePage />} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -23,6 +23,7 @@ const SECONDARY: NavItem[] = [
{ 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: '🏡' },
{ to: '/statistik', label: de.nav.statistics, icon: '📊' }, { to: '/statistik', label: de.nav.statistics, icon: '📊' },
{ to: '/hilfe', label: de.nav.help, icon: '❓' },
] ]
const linkClass = ({ isActive }: { isActive: boolean }) => const linkClass = ({ isActive }: { isActive: boolean }) =>

View File

@@ -0,0 +1,196 @@
/**
* HELP-1: In-App-Anleitung für die Züchterin (nicht-technisch, du-Form).
* Accordion via <details>/<summary> — kein JS-State nötig, barrierefrei, mobiltauglich.
* Texte referenzieren nur Bezeichnungen aus de.ts (bleiben bei Umbenennung korrekt).
*/
import { de } from '../strings/de'
import './hilfe.css'
const t = de.hilfe
const nav = de.nav
const g = de.pages.gerbils
const l = de.pages.litters
const b = de.pages.becken
const st = de.pages.stammbaum
const ge = de.pages.genetik
const ab = de.pages.abgabe
const stat = de.pages.statistik
const scheckeWarning = de.genetics.warnings.SCHECKE_LETHAL
const rexWarning = de.genetics.warnings.REX_SEMI_LETHAL
interface Section {
id: string
title: string
content: React.ReactNode
}
const sections: Section[] = [
{
id: 'erste-schritte',
title: t.sections.ersteSchritte,
content: (
<>
<p>Willkommen im <strong>{de.app.title}</strong>! Diese Anleitung führt dich durch die wichtigsten 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>
<ol>
<li>Becken anlegen (wo leben deine Tiere?)</li>
<li>Rennmäuse anlegen mit Foto</li>
<li>Würfe eintragen</li>
<li>Genetik erkunden</li>
</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>
</>
),
},
{
id: 'tiere',
title: t.sections.tiere,
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>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>Auf der Detailseite findest du drei Tabs:</p>
<ul>
<li><strong>„{g.detail.tabs.photos}"</strong> Fotos hochladen; das erste Foto wird als Profilfoto angezeigt.</li>
<li><strong>{g.detail.tabs.health}"</strong> — Tierarztbesuche, Behandlungen und Untersuchungen eintragen.</li>
<li><strong>„{g.detail.tabs.weight}"</strong> Gewicht in Gramm erfassen; nach zwei Einträgen erscheint eine Verlaufskurve.</li>
</ul>
<p>Mit den Filtern oben in der Liste kannst du nach Status, Geschlecht und Farbschlag filtern oder nach Name suchen.</p>
</>
),
},
{
id: 'becken',
title: t.sections.becken,
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>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>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>
</>
),
},
{
id: 'wuerfe',
title: t.sections.wuerfe,
content: (
<>
<p>Unter <strong>{nav.litters}"</strong> erfasst du Würfe. Mit <strong>„{l.newButton}"</strong> trägst du Vater, Mutter, Wurfdatum und Wurfstärke ein.</p>
<p>Das <strong>{l.fields.expectedGoHomeDate}"</strong> wird automatisch auf 35 Tage nach dem Wurfdatum vorgeschlagen — du kannst es anpassen.</p>
<p>Auf der Wurf-Detailseite siehst du unter <strong>„{l.detail.registered}"</strong> die bereits erfassten Jungtiere. Mit <strong>{l.detail.registerJuvenile}"</strong> legst du ein neues Tier an, das direkt mit diesem Wurf verknüpft ist.</p>
<p>Im Tab <strong>„{l.tabs.pairs}"</strong> siehst du alle Verpaarungen aus deinen Würfen auf einen Blick mit Anzahl Würfe und Datum des letzten Wurfes.</p>
<p className="hilfe-hint">Tipp: Wenn du auf der Genetik-Seite eine Probeverpaarung berechnest, kannst du mit <strong>Probeverpaarung"</strong> direkt zur Genetikseite springen und das Tierpaar vorauswählen.</p>
</>
),
},
{
id: 'genetik',
title: t.sections.genetik,
content: (
<>
<p>Unter <strong>„{nav.genetics}"</strong> berechnest du, welche Farbschläge bei einer Verpaarung möglich sind.</p>
<p>Wähle <strong>{ge.father}"</strong> und <strong>„{ge.mother}"</strong> aus deinen Tieren oder gib die Genotypen manuell ein. Klicke dann auf <strong>{ge.run}"</strong>.</p>
<p>Das Ergebnis zeigt alle möglichen Farbschläge mit Wahrscheinlichkeit — du kannst zwischen Anzeige nach Farbschlag und nach Genotyp wechseln.</p>
<p><strong>Wichtige Warnungen:</strong></p>
<ul>
<li className="hilfe-warning"><strong>Schecke × Schecke:</strong> {scheckeWarning}</li>
<li className="hilfe-warning"><strong>Rex × Rex:</strong> {rexWarning}</li>
</ul>
<p>Diese Warnungen erscheinen automatisch, wenn die entsprechende Kombination erkannt wird. Bitte beachte sie beim Zuchtentscheid.</p>
<p>Den Genotyp eines Tieres trägst du auf der Tier-Detailseite unter <strong>„{g.detail.masterData}"</strong> ein. Format: z. B. <code>Aa CC Dd EE GG Pp Spsp rere</code>. Unbekannte Stellen als <code>?</code>.</p>
</>
),
},
{
id: 'stammbaum',
title: t.sections.stammbaum,
content: (
<>
<p>Auf der Tier-Detailseite öffnest du mit <strong>{st.openButton}"</strong> den interaktiven Stammbaum.</p>
<p><strong>Navigieren:</strong> {st.tapHint}</p>
<p><strong>Zoomen:</strong> Mit den Schaltflächen <strong>„{st.zoomIn}"</strong> / <strong>{st.zoomOut}"</strong> oder per Pinch-Geste auf dem Smartphone. <strong>„{st.zoomFit}"</strong> passt die Ansicht an den Bildschirm an.</p>
<p><strong>Drucken / PDF:</strong> Mit <strong>{st.print}"</strong> öffnest du die Druckansicht (Ahnentafel). Wähle im Druckdialog „Als PDF speichern", um eine Datei zu erzeugen.</p>
<p>Der <strong>{st.inbreeding.label}"</strong> wird automatisch für das angezeigte Tier berechnet und oben angezeigt.</p>
<p className="hilfe-hint">Tipp: Je mehr Elterntiere und Würfe du erfasst hast, desto tiefer reicht der Stammbaum.</p>
</>
),
},
{
id: 'abgabe',
title: t.sections.abgabe,
content: (
<>
<p>Der Abgabe-Ablauf läuft in mehreren Schritten:</p>
<ol>
<li>
<strong>Tier zur Abgabe stellen:</strong> Öffne die Tierakte und klicke auf <strong>„{ab.markAction}"</strong>. Das Tier wechselt in den Status <em>{de.pages.gerbils.statusLabels.ForSale}"</em>.
</li>
<li>
<strong>Gruppen bilden:</strong> Auf der <strong>„{nav.forSale}"</strong>-Seite werden Tiere nach Becken vorgruppiert. Du kannst Tiere zwischen Gruppen verschieben oder neue Gruppen anlegen.
</li>
<li>
<strong>Inserat erstellen:</strong> Fülle für jede Gruppe den emotionalen Einzeiler und den Charakter-Text aus.
</li>
<li>
<strong>Text kopieren &amp; Fotos herunterladen:</strong> Mit <strong>{ab.export.copyText}"</strong> kommt der fertige Inseratstext in die Zwischenablage. Mit <strong>„{ab.export.downloadPhotos}"</strong> lädst du alle Fotos der Gruppe als ZIP herunter.
</li>
<li>
<strong>KI-Unterstützung (optional):</strong> Mit <strong>{ab.ai.improve}"</strong> kann der Text automatisch verbessert werden — dazu ist ein API-Schlüssel nötig.
</li>
</ol>
<p className="hilfe-hint">Tipp: Bearbeite jede Gruppe einzeln — Text und Fotos für eine Gruppe gleichzeitig zusammenstellen, dann weiter zur nächsten.</p>
</>
),
},
{
id: 'statistik',
title: t.sections.statistik,
content: (
<>
<p>Unter <strong>„{nav.statistics}"</strong> findest du Auswertungen über deinen Bestand:</p>
<ul>
<li><strong>{stat.littersPerYear}"</strong> — wie viele Würfe pro Jahr.</li>
<li><strong>„{stat.avgLitterSize}"</strong> durchschnittliche Wurfstärke pro Jahr.</li>
<li><strong>{stat.farbschlag}"</strong> — wie viele aktive Tiere welchen Farbschlag haben.</li>
<li><strong>„{stat.population}"</strong> wie sich dein Bestand über die Zeit entwickelt hat.</li>
<li><strong>{stat.losses}"</strong> — verstorbene Tiere nach Jahr.</li>
</ul>
<p>Die Statistiken werden automatisch aus deinen Daten berechnet. Je vollständiger deine Einträge (Geburtsdaten, Farbschläge, Wurfstärken), desto aussagekräftiger die Auswertungen.</p>
</>
),
},
{
id: 'datensicherung',
title: t.sections.datensicherung,
content: (
<>
<p>Deine Daten liegen sicher auf Julian's Server zu Hause — kein Cloud-Konto nötig.</p>
<p>Julian erstellt automatisch jeden Nacht um 3 Uhr ein Backup der Datenbank und aller Fotos. Bei einem Problem kann er den Stand eines beliebigen Tages wiederherstellen.</p>
<p className="hilfe-hint">Wenn du versehentlich Daten gelöscht hast, wende dich an Julian. Er kann aus einem der letzten 7 Tages-Backups wiederherstellen.</p>
</>
),
},
]
export default function HilfePage() {
return (
<section className="page hilfe-page">
<h2>{t.title}</h2>
<p className="hilfe-subtitle">{t.subtitle}</p>
<div className="hilfe-accordion">
{sections.map((s) => (
<details key={s.id} className="hilfe-section">
<summary className="hilfe-section-summary">
<span className="hilfe-section-title">{s.title}</span>
<span className="hilfe-section-chevron" aria-hidden="true">▾</span>
</summary>
<div className="hilfe-section-body">{s.content}</div>
</details>
))}
</div>
</section>
)
}

View File

@@ -0,0 +1,105 @@
.hilfe-page {
max-width: 720px;
}
.hilfe-subtitle {
color: var(--color-muted, #666);
margin-bottom: 1.5rem;
}
/* Accordion */
.hilfe-accordion {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.hilfe-section {
border: 1px solid var(--color-border, #ddd);
border-radius: 6px;
background: var(--color-surface, #fff);
}
.hilfe-section-summary {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1rem;
cursor: pointer;
list-style: none;
user-select: none;
gap: 0.5rem;
}
.hilfe-section-summary::-webkit-details-marker {
display: none;
}
.hilfe-section-summary:hover {
background: var(--color-hover, #f5f5f5);
border-radius: 6px;
}
.hilfe-section[open] > .hilfe-section-summary {
border-bottom: 1px solid var(--color-border, #ddd);
border-radius: 6px 6px 0 0;
}
.hilfe-section-title {
font-weight: 600;
font-size: 1rem;
}
.hilfe-section-chevron {
font-size: 1.1rem;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.hilfe-section[open] .hilfe-section-chevron {
transform: rotate(180deg);
}
.hilfe-section-body {
padding: 1rem 1rem 1.25rem;
}
.hilfe-section-body p {
margin: 0 0 0.75rem;
line-height: 1.6;
}
.hilfe-section-body p:last-child {
margin-bottom: 0;
}
.hilfe-section-body ul,
.hilfe-section-body ol {
margin: 0.25rem 0 0.75rem 1.25rem;
line-height: 1.6;
}
.hilfe-section-body li {
margin-bottom: 0.4rem;
}
.hilfe-section-body code {
font-family: monospace;
background: var(--color-hover, #f5f5f5);
padding: 0.1em 0.35em;
border-radius: 3px;
font-size: 0.9em;
}
/* Hint boxes */
.hilfe-hint {
background: var(--color-info-bg, #eff6ff);
border-left: 3px solid var(--color-info, #3b82f6);
padding: 0.5rem 0.75rem;
border-radius: 0 4px 4px 0;
font-size: 0.93rem;
}
.hilfe-warning {
color: var(--color-warning-text, #92400e);
}

View File

@@ -26,6 +26,8 @@ export const de = {
openMenu: 'Menü öffnen', openMenu: 'Menü öffnen',
closeMenu: 'Menü schließen', closeMenu: 'Menü schließen',
mainNavigation: 'Hauptnavigation', mainNavigation: 'Hauptnavigation',
// HELP-1
help: 'Hilfe',
}, },
pages: { pages: {
home: { home: {
@@ -473,6 +475,22 @@ export const de = {
}, },
}, },
}, },
// ── HELP-1: In-App-Anleitung ──
hilfe: {
title: 'Hilfe & Anleitung',
subtitle: 'Kurzanleitung für den Rennmaus-Manager',
sections: {
ersteSchritte: 'Erste Schritte',
tiere: 'Rennmäuse anlegen & Fotos',
becken: 'Becken',
wuerfe: 'Würfe & Jungtiere',
genetik: 'Probeverpaarung & Genetik',
stammbaum: 'Stammbaum',
abgabe: 'Abgabe-Ablauf',
statistik: 'Statistik',
datensicherung: 'Datensicherung',
},
},
api: { api: {
errors: { errors: {
network: 'Verbindung zum Server fehlgeschlagen. Bitte später erneut versuchen.', network: 'Verbindung zum Server fehlgeschlagen. Bitte später erneut versuchen.',