FEAT-12a: ForSale scaffolding — status+ForSale, abgabe strings, /abgabe route+nav, sale-ad API stub, detail action, jszip
This commit is contained in:
@@ -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>
|
||||
|
||||
37
gerbil-manager-web/src/api/saleAd.ts
Normal file
37
gerbil-manager-web/src/api/saleAd.ts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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: '📊' },
|
||||
]
|
||||
|
||||
|
||||
11
gerbil-manager-web/src/pages/AbgabePage.tsx
Normal file
11
gerbil-manager-web/src/pages/AbgabePage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user