diff --git a/gerbil-manager-web/src/pages/BeckenDetailPage.tsx b/gerbil-manager-web/src/pages/BeckenDetailPage.tsx new file mode 100644 index 0000000..452c37d --- /dev/null +++ b/gerbil-manager-web/src/pages/BeckenDetailPage.tsx @@ -0,0 +1,143 @@ +/** + * FEAT-2: Becken-Detailseite — Stammdaten, BELEGUNG (welche Tiere wohnen + * aktuell hier, mit Farbschlag + Link zur Rennmaus), Löschen mit + * Bestätigung; Konflikt (Becken nicht leer) wird deutsch gemeldet. + */ +import { useMemo, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' +import { de } from '../strings/de' +import { ApiError } from '../api/client' +import { deleteEnclosure, getEnclosure } from '../api/enclosures' +import { listGerbils } from '../api/gerbils' +import { listColorVarieties } from '../api/lookups' +import { condition } from '../api/gridify' +import { useApi, useMutation } from '../hooks/useApi' + +export default function BeckenDetailPage() { + const t = de.pages.becken + const { id = '' } = useParams() + const navigate = useNavigate() + const [deleteError, setDeleteError] = useState(null) + + const enclosure = useApi(() => getEnclosure(id), [id]) + // Belegung: alle Tiere, deren aktuelles Becken dieses ist. + const occupants = useApi( + () => + listGerbils({ + filter: condition({ field: 'enclosureId', op: '==', value: id }), + orderBy: 'name', + page: 1, + pageSize: 500, + }), + [id], + ) + const colorVarieties = useApi(() => listColorVarieties(), []) + const colorNameById = useMemo( + () => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])), + [colorVarieties.data], + ) + + const removal = useMutation(() => deleteEnclosure(id)) + + async function onDelete() { + if (!window.confirm(t.delete.confirmMessage)) return + setDeleteError(null) + try { + await removal.run() + navigate('/becken') + } catch (err) { + // Backend meldet Konflikt, wenn noch Tiere im Becken wohnen. + if (err instanceof ApiError && err.status === 409) { + setDeleteError(t.delete.conflict) + } else { + setDeleteError(removal.error ?? de.api.errors.unknown) + } + } + } + + if (enclosure.loading) return

{de.common.loading}

+ if (enclosure.error || !enclosure.data) { + return ( +
+

{enclosure.error ?? t.detail.notFound}

+ + {t.detail.back} + +
+ ) + } + + const e = enclosure.data + const animals = occupants.data?.items ?? [] + const count = occupants.data?.totalCount ?? 0 + + return ( +
+
+
+

{e.name}

+ {occupants.data && ( +

+ {count} {count === 1 ? t.occupancy.countOne : t.occupancy.countMany} +

+ )} +
+
+ + {t.detail.edit} + + + + {t.detail.back} + +
+
+ + {deleteError &&
{deleteError}
} + + {e.notes && ( +
+
+
{t.fields.notes}
+
{e.notes}
+
+
+ )} + +

{t.occupancy.title}

+ {occupants.loading &&

{de.common.loading}

} + {occupants.error && ( +
+ {occupants.error} + +
+ )} + {!occupants.loading && !occupants.error && animals.length === 0 && ( +

{t.occupancy.empty}

+ )} + {animals.length > 0 && ( + + )} +
+ ) +} diff --git a/gerbil-manager-web/src/pages/BeckenFormPage.tsx b/gerbil-manager-web/src/pages/BeckenFormPage.tsx new file mode 100644 index 0000000..46702f7 --- /dev/null +++ b/gerbil-manager-web/src/pages/BeckenFormPage.tsx @@ -0,0 +1,90 @@ +/** FEAT-2: Becken anlegen/bearbeiten — Name (Pflicht) + Notizen. */ +import { useState, type FormEvent } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' +import { de } from '../strings/de' +import { createEnclosure, getEnclosure, updateEnclosure, type CreateEnclosure } from '../api/enclosures' +import { useApi, useMutation } from '../hooks/useApi' + +interface FormState { + name: string + notes: string +} + +const EMPTY: FormState = { name: '', notes: '' } + +/** "" -> null, sonst der Wert. */ +const nn = (s: string): string | null => (s.trim() === '' ? null : s) + +export default function BeckenFormPage() { + const t = de.pages.becken + const navigate = useNavigate() + const { id } = useParams() + const isEdit = Boolean(id) + + const [form, setForm] = useState(EMPTY) + const [errors, setErrors] = useState>>({}) + const [initializedFor, setInitializedFor] = useState(null) + + const existing = useApi(() => (id ? getEnclosure(id) : Promise.resolve(null)), [id]) + + // Vorbefüllen im Bearbeiten-Modus (adjust-state-during-render, wie FEAT-1). + if (existing.data && initializedFor !== existing.data.id) { + setInitializedFor(existing.data.id) + setForm({ name: existing.data.name, notes: existing.data.notes ?? '' }) + } + + const mutation = useMutation((body: CreateEnclosure) => + isEdit && id ? updateEnclosure(id, body) : createEnclosure(body), + ) + + async function onSubmit(e: FormEvent) { + e.preventDefault() + if (form.name.trim() === '') { + setErrors({ name: t.validation.nameRequired }) + return + } + setErrors({}) + const saved = await mutation.run({ name: form.name.trim(), notes: nn(form.notes) }) + navigate(`/becken/${saved.id}`) + } + + if (isEdit && existing.loading) return

{de.common.loading}

+ + return ( +
+

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

+ +
+ + +