From d3550bbc1689543cdd24971e7ebac09ab725a01a Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 00:29:52 +0200 Subject: [PATCH] FEAT-3: Wurf create/edit form (parent pickers, auto +35d go-home date, validation) Co-Authored-By: Claude Opus 4.8 (1M context) --- gerbil-manager-web/src/pages/WurfFormPage.tsx | 232 +++++++++++++++++- 1 file changed, 230 insertions(+), 2 deletions(-) diff --git a/gerbil-manager-web/src/pages/WurfFormPage.tsx b/gerbil-manager-web/src/pages/WurfFormPage.tsx index 71317c1..3b58bab 100644 --- a/gerbil-manager-web/src/pages/WurfFormPage.tsx +++ b/gerbil-manager-web/src/pages/WurfFormPage.tsx @@ -1,10 +1,238 @@ +import { useMemo, useState, type FormEvent } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' import { de } from '../strings/de' +import { createLitter, getLitter, updateLitter } from '../api/litters' +import { listGerbils } from '../api/gerbils' +import type { CreateLitter } from '../api/types' +import { useApi, useMutation } from '../hooks/useApi' +import AnimalPicker from '../components/AnimalPicker' + +interface FormState { + name: string + date: string + fatherId: string + fatherName: string + motherId: string + motherName: string + totalBorn: string + expectedGoHomeDate: string + notes: string +} + +const EMPTY: FormState = { + name: '', + date: '', + fatherId: '', + fatherName: '', + motherId: '', + motherName: '', + totalBorn: '', + expectedGoHomeDate: '', + notes: '', +} + +/** Add n days to an ISO date ("YYYY-MM-DD"), in UTC to avoid TZ drift. */ +function addDays(iso: string, n: number): string { + const [y, m, d] = iso.split('-').map(Number) + if (!y || !m || !d) return '' + const ms = Date.UTC(y, m - 1, d) + n * 86_400_000 + return new Date(ms).toISOString().slice(0, 10) +} + +const nn = (s: string): string | null => (s.trim() === '' ? null : s) export default function WurfFormPage() { - // Fleshed out in a later FEAT-3 increment (parents picker, auto go-home date). + const t = de.pages.litters + const navigate = useNavigate() + const { id } = useParams() + const isEdit = Boolean(id) + + const [form, setForm] = useState(EMPTY) + const [errors, setErrors] = useState>>({}) + const [goHomeTouched, setGoHomeTouched] = useState(false) + const [initializedFor, setInitializedFor] = useState(null) + + const existing = useApi(() => (id ? getLitter(id) : Promise.resolve(null)), [id]) + // Resolve parent names when editing (litter stores only ids). + const gerbils = useApi(() => listGerbils({ page: 1, pageSize: 1000, orderBy: 'name' }), []) + const nameById = useMemo(() => { + const map = new Map() + for (const g of gerbils.data?.items ?? []) map.set(g.id, g.name) + return map + }, [gerbils.data]) + + // Prefill in edit mode (adjust state during render, guarded). + if (existing.data && initializedFor !== existing.data.id) { + setInitializedFor(existing.data.id) + const l = existing.data + setForm({ + name: l.name, + date: l.date ?? '', + fatherId: l.fatherId ?? '', + fatherName: l.fatherId ? (nameById.get(l.fatherId) ?? '') : '', + motherId: l.motherId ?? '', + motherName: l.motherId ? (nameById.get(l.motherId) ?? '') : '', + totalBorn: l.totalBorn != null ? String(l.totalBorn) : '', + expectedGoHomeDate: l.expectedGoHomeDate ?? '', + notes: l.notes ?? '', + }) + setGoHomeTouched(true) + } + + const set = (key: K, value: FormState[K]) => + setForm((f) => ({ ...f, [key]: value })) + + // Wurfdatum change: auto-suggest go-home +35 days unless the user edited it. + const onDateChange = (date: string) => { + setForm((f) => ({ + ...f, + date, + expectedGoHomeDate: goHomeTouched ? f.expectedGoHomeDate : date ? addDays(date, 35) : '', + })) + } + + const mutation = useMutation((body: CreateLitter) => + isEdit && id ? updateLitter(id, body) : createLitter(body), + ) + + function validate(): boolean { + const next: Partial> = {} + if (form.name.trim() === '') next.name = t.validation.nameRequired + if (form.date.trim() === '') next.date = t.validation.dateRequired + setErrors(next) + return Object.keys(next).length === 0 + } + + async function onSubmit(e: FormEvent) { + e.preventDefault() + if (!validate()) return + const totalBornNum = form.totalBorn.trim() === '' ? null : Number(form.totalBorn) + const body: CreateLitter = { + name: form.name.trim(), + date: form.date, + fatherId: nn(form.fatherId), + motherId: nn(form.motherId), + totalBorn: totalBornNum != null && Number.isFinite(totalBornNum) ? totalBornNum : null, + expectedGoHomeDate: nn(form.expectedGoHomeDate), + notes: nn(form.notes), + } + const saved = await mutation.run(body) + navigate(`/wuerfe/${saved.id}`) + } + + if (isEdit && existing.loading) return

{de.common.loading}

+ return (
-

{de.pages.litters.form.createTitle}

+

{isEdit ? t.form.editTitle : t.form.createTitle}

+ +
+ + + + +
+ {t.fields.father} + {form.fatherName ? ( +

+ {form.fatherName} + +

+ ) : ( + setForm((f) => ({ ...f, fatherId: g.id, fatherName: g.name }))} + /> + )} +
+ +
+ {t.fields.mother} + {form.motherName ? ( +

+ {form.motherName} + +

+ ) : ( + setForm((f) => ({ ...f, motherId: g.id, motherName: g.name }))} + /> + )} +
+ + + + + +