FEAT-2: Becken pages - list (Gridify search+paging), detail w/ occupancy, form, delete w/ 409 conflict message

This commit is contained in:
2026-06-06 00:08:06 +02:00
parent 16a0016de9
commit 5ee4b9874b
3 changed files with 342 additions and 0 deletions

View File

@@ -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<FormState>(EMPTY)
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({})
const [initializedFor, setInitializedFor] = useState<string | null>(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 <p className="muted">{de.common.loading}</p>
return (
<section className="page">
<h2>{isEdit ? t.form.editTitle : t.form.createTitle}</h2>
<form className="form" onSubmit={onSubmit} noValidate>
<label className="field">
<span>{t.fields.name} *</span>
<input
className="input"
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
aria-invalid={Boolean(errors.name)}
/>
{errors.name && <small className="error-text">{errors.name}</small>}
</label>
<label className="field">
<span>{t.fields.notes}</span>
<textarea
value={form.notes}
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
/>
</label>
{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 ? t.form.saving : t.form.save}
</button>
<Link to={isEdit && id ? `/becken/${id}` : '/becken'} className="btn">
{t.form.cancel}
</Link>
</div>
</form>
</section>
)
}