Files
GerbilManager/gerbil-manager-web/src/pages/EinstellungenPage.tsx
Gulum b83e96f552 feat: Rennmausakte, Zucht-Suffix, Gehege-Bilder, Toasts, Infinite-Scroll
Detailseite (Rennmausakte):
- Neugestaltung: Hero-Foto + Overlay, Schnellfakten-Pills, Karten-Sektionen,
  Charakter als Chips, getabte Fotos/Gesundheit/Gewicht.
- Eltern (Vater/Mutter) als Links; Charakter-Karte klappt bei Status
  Abgabe/Verstorben/Abgegeben ein (nur Zucht/Liebhaber offen).
- Gehege wird bei abgegebenen/verstorbenen Tieren ausgeblendet (Detail + Formular).

Listen:
- Infinite Scroll auf allen Listen (Rennmäuse, Würfe, Gehege, Verträge,
  Anfragen, Kontakte) via useInfiniteList/useInfiniteSentinel; stabile
  Sortierung mit id-Tiebreaker (keine doppelten Keys), Back-to-top-Button.
- Kontakte: Rolle-Filter (Züchter/Abnehmer) als Quick-Chips + Sticky-Header.

Zucht-Nachname:
- Namens-Anhängsel der Zucht in den Einstellungen + je Züchter-Kontakt
  (Backend-Spalten + Migration); eigene Tiere zeigen „Name + Suffix".
- „Eigene Zucht" ist die Standard-Herkunft neuer Tiere.

Weiteres:
- Gehege-Bilder: Upload/Galerie auf der Gehege-Detailseite (Backend
  EnclosurePhoto + Endpoints + Migration, geteilte Dateiablage).
- Toast-Rückmeldungen für alle Speichern-Aktionen.
- Checkboxen durch mobile-freundliche Toggle-Schalter ersetzt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:35:30 +02:00

113 lines
3.5 KiB
TypeScript

/**
* FEAT-13: Einstellungen — Zuchtprofil (Verkäufer-Block der Abgabeverträge).
* Einzelzeilen-Settings im Backend (/settings/breeder-profile); die Züchterin
* pflegt ihre Daten hier statt in einer JSON-Datei.
*/
import { useState, type FormEvent } from 'react'
import { de } from '../strings/de'
import {
EMPTY_BREEDER_PROFILE,
getBreederProfile,
isBreederProfileComplete,
putBreederProfile,
type BreederProfile,
} from '../api/settings'
import { useApi, useMutation } from '../hooks/useApi'
import { useToast } from '../components/toast'
// EXPORT-1 (Oscar): Datenexport-Karte
import DatenexportCard from '../components/DatenexportCard'
export default function EinstellungenPage() {
const t = de.pages.einstellungen
const tz = t.zuchtprofil
const toast = useToast()
const [form, setForm] = useState<BreederProfile>(EMPTY_BREEDER_PROFILE)
const [initialized, setInitialized] = useState(false)
const [saved, setSaved] = useState(false)
const existing = useApi(() => getBreederProfile(), [])
if (existing.data && !initialized) {
setInitialized(true)
setForm(existing.data)
}
const set = <K extends keyof BreederProfile>(key: K, value: string) => {
setSaved(false)
setForm((f) => ({ ...f, [key]: value }))
}
const mutation = useMutation((profile: BreederProfile) => putBreederProfile(profile))
async function onSubmit(e: FormEvent) {
e.preventDefault()
const result = await mutation.run(form)
if (result.ok) {
setSaved(true)
toast.success(de.common.saved)
} else {
toast.error(result.error)
}
}
if (existing.loading) return <p className="muted">{de.common.loading}</p>
if (existing.error) {
return (
<section className="page">
<h2>{t.title}</h2>
<div className="alert alert--error">
<span>{existing.error}</span>
<button type="button" className="btn" onClick={existing.reload}>
{de.common.retry}
</button>
</div>
</section>
)
}
const field = (
key: keyof BreederProfile,
label: string,
hint?: string,
type: string = 'text',
) => (
<label className="field">
<span>{label}</span>
<input className="input" type={type} value={form[key]} onChange={(e) => set(key, e.target.value)} />
{hint && <small className="muted">{hint}</small>}
</label>
)
return (
<section className="page">
<h2>{t.title}</h2>
<h3>{tz.title}</h3>
<p className="muted">{tz.intro}</p>
{!isBreederProfileComplete(form) && <div className="alert alert--error">{tz.incompleteHint}</div>}
<form className="form" onSubmit={onSubmit} noValidate>
{field('zuchtName', tz.fields.zuchtName)}
{field('nameSuffix', tz.fields.nameSuffix, tz.fields.nameSuffixHint)}
{field('name', tz.fields.name, tz.fields.nameHint)}
{field('address', tz.fields.address, tz.fields.addressHint)}
{field('city', tz.fields.city)}
{field('phone', tz.fields.phone, undefined, 'tel')}
{field('email', tz.fields.email, undefined, 'email')}
{field('homepage', tz.fields.homepage, undefined, 'url')}
{mutation.error && <div className="alert alert--error">{mutation.error}</div>}
<div className="form-actions">
<button type="submit" className="btn btn--primary" disabled={mutation.pending}>
{mutation.pending ? tz.saving : tz.save}
</button>
{saved && <span className="muted">{tz.saved}</span>}
</div>
</form>
<DatenexportCard />
</section>
)
}