FEAT-2: Becken pages - list (Gridify search+paging), detail w/ occupancy, form, delete w/ 409 conflict message
This commit is contained in:
90
gerbil-manager-web/src/pages/BeckenFormPage.tsx
Normal file
90
gerbil-manager-web/src/pages/BeckenFormPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user