FEAT-12a: ForSale scaffolding — status+ForSale, abgabe strings, /abgabe route+nav, sale-ad API stub, detail action, jszip

This commit is contained in:
2026-06-06 01:07:43 +02:00
parent 188b33618e
commit e4b7558abd
9 changed files with 293 additions and 5 deletions

View File

@@ -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() {
<Route path=":id/bearbeiten" element={<KontaktFormPage />} />
</Route>
<Route path="genetik" element={<GenetikPage />} />
<Route path="abgabe" element={<AbgabePage />} />
<Route path="statistik" element={<StatistikPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>

View File

@@ -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<SaleAdResponse> {
return api.post<SaleAdResponse>('/gerbils/sale-ad', request)
}

View File

@@ -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

View File

@@ -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: '📊' },
]

View File

@@ -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 (
<section className="page">
<h2>{de.pages.abgabe.title}</h2>
<p className="muted">{de.pages.abgabe.subtitle}</p>
</section>
)
}

View File

@@ -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<DetailTab>('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() {
<Link to={`/genetik?parent=${g.id}`} className="btn">
{t.detail.testMating}
</Link>
{g.status !== 'ForSale' && (
<button
type="button"
className="btn"
disabled={forSale.pending}
onClick={async () => {
const r = await forSale.run()
if (r.ok) gerbil.reload()
}}
>
{de.pages.abgabe.markAction}
</button>
)}
<Link to={`/rennmaeuse/${g.id}/stammbaum`} className="btn">
{de.pages.stammbaum.openButton}
</Link>

View File

@@ -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',