Merge feature/form-fields: Herkunft (originBreeder) + 'Gehoerlos' (isDeaf) im Tier-Formular editierbar
GerbilFormPage: originBreeder-Freitextfeld + isDeaf-Tristate-Select (Ja/Nein/Unbekannt), round-trip laden/speichern. types.ts isDeaf, de.ts Labels. +4 e2e round-trip. Komponiert mit CR-2 PATCH-Semantik (ausgelassene Felder bleiben). vitest 82, e2e 146, build clean. Kein EF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -149,3 +149,26 @@ test('Tier bearbeiten — Notizen ändern', async ({ page }) => {
|
|||||||
await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible()
|
await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible()
|
||||||
await expect(page.getByText('Sehr zutraulich.')).toBeVisible()
|
await expect(page.getByText('Sehr zutraulich.')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Tier bearbeiten — originBreeder Freitext round-trip (FORM-FIELDS-1)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
// Fridolin hat originBreeder='Zoohandlung Meier', kein originContactId → Freitext-Feld vorbelegt
|
||||||
|
await page.goto('/rennmaeuse/fridolin/bearbeiten')
|
||||||
|
await expect(page.getByLabel(t.fields.originBreeder)).toHaveValue('Zoohandlung Meier')
|
||||||
|
await page.getByLabel(t.fields.originBreeder).fill('Neue Musterzucht')
|
||||||
|
await page.getByRole('button', { name: t.form.save, exact: true }).click()
|
||||||
|
// Detailseite: Herkunft zeigt den neuen Wert (kein Contact → originBreeder-Text)
|
||||||
|
await expect(page.getByText('Neue Musterzucht')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Tier bearbeiten — isDeaf Tristate round-trip (FORM-FIELDS-1)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
await page.goto('/rennmaeuse/kruemel/bearbeiten')
|
||||||
|
// Standard: Unbekannt (null → value='')
|
||||||
|
await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('')
|
||||||
|
await page.getByLabel(t.fields.isDeaf).selectOption({ label: t.form.isDeafYes })
|
||||||
|
await page.getByRole('button', { name: t.form.save, exact: true }).click()
|
||||||
|
// Zurück zur Bearbeiten-Seite: Wert muss erhalten sein
|
||||||
|
await page.goto('/rennmaeuse/kruemel/bearbeiten')
|
||||||
|
await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('true')
|
||||||
|
})
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export interface Gerbil {
|
|||||||
* as resident (backend default true).
|
* as resident (backend default true).
|
||||||
*/
|
*/
|
||||||
isResident?: boolean
|
isResident?: boolean
|
||||||
|
/** FORM-FIELDS-1: true=gehörlos, false=hörend, null=unbekannt. */
|
||||||
|
isDeaf?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Payload for POST /gerbils. */
|
/** Payload for POST /gerbils. */
|
||||||
@@ -76,6 +78,7 @@ export interface CreateGerbil {
|
|||||||
characterTraits?: string[] | null
|
characterTraits?: string[] | null
|
||||||
characterNote?: string | null
|
characterNote?: string | null
|
||||||
isResident?: boolean
|
isResident?: boolean
|
||||||
|
isDeaf?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Payload for PUT /gerbils/{id} (all optional / partial update). */
|
/** Payload for PUT /gerbils/{id} (all optional / partial update). */
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ interface FormState {
|
|||||||
enclosureId: string
|
enclosureId: string
|
||||||
litterId: string
|
litterId: string
|
||||||
originContactId: string
|
originContactId: string
|
||||||
|
originBreeder: string
|
||||||
receiverContactId: string
|
receiverContactId: string
|
||||||
genotype: string
|
genotype: string
|
||||||
notes: string
|
notes: string
|
||||||
isResident: boolean
|
isResident: boolean
|
||||||
|
isDeaf: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY: FormState = {
|
const EMPTY: FormState = {
|
||||||
@@ -39,10 +41,12 @@ const EMPTY: FormState = {
|
|||||||
enclosureId: '',
|
enclosureId: '',
|
||||||
litterId: '',
|
litterId: '',
|
||||||
originContactId: '',
|
originContactId: '',
|
||||||
|
originBreeder: '',
|
||||||
receiverContactId: '',
|
receiverContactId: '',
|
||||||
genotype: '',
|
genotype: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
isResident: true,
|
isResident: true,
|
||||||
|
isDeaf: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
function formFromGerbil(g: {
|
function formFromGerbil(g: {
|
||||||
@@ -57,10 +61,12 @@ function formFromGerbil(g: {
|
|||||||
enclosureId: string | null
|
enclosureId: string | null
|
||||||
litterId: string | null
|
litterId: string | null
|
||||||
originContactId: string | null
|
originContactId: string | null
|
||||||
|
originBreeder?: string | null
|
||||||
receiverContactId: string | null
|
receiverContactId: string | null
|
||||||
genotype: string | null
|
genotype: string | null
|
||||||
notes: string | null
|
notes: string | null
|
||||||
isResident?: boolean | null
|
isResident?: boolean | null
|
||||||
|
isDeaf?: boolean | null
|
||||||
}): FormState {
|
}): FormState {
|
||||||
return {
|
return {
|
||||||
name: g.name,
|
name: g.name,
|
||||||
@@ -74,10 +80,12 @@ function formFromGerbil(g: {
|
|||||||
enclosureId: g.enclosureId ?? '',
|
enclosureId: g.enclosureId ?? '',
|
||||||
litterId: g.litterId ?? '',
|
litterId: g.litterId ?? '',
|
||||||
originContactId: g.originContactId ?? '',
|
originContactId: g.originContactId ?? '',
|
||||||
|
originBreeder: g.originBreeder ?? '',
|
||||||
receiverContactId: g.receiverContactId ?? '',
|
receiverContactId: g.receiverContactId ?? '',
|
||||||
genotype: g.genotype ?? '',
|
genotype: g.genotype ?? '',
|
||||||
notes: g.notes ?? '',
|
notes: g.notes ?? '',
|
||||||
isResident: g.isResident ?? true,
|
isResident: g.isResident ?? true,
|
||||||
|
isDeaf: g.isDeaf ?? null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +175,12 @@ export default function GerbilFormPage() {
|
|||||||
enclosureId: nn(form.enclosureId),
|
enclosureId: nn(form.enclosureId),
|
||||||
litterId: nn(form.litterId),
|
litterId: nn(form.litterId),
|
||||||
originContactId: nn(form.originContactId),
|
originContactId: nn(form.originContactId),
|
||||||
|
originBreeder: nn(form.originBreeder),
|
||||||
receiverContactId: nn(form.receiverContactId),
|
receiverContactId: nn(form.receiverContactId),
|
||||||
genotype: nn(form.genotype),
|
genotype: nn(form.genotype),
|
||||||
notes: nn(form.notes),
|
notes: nn(form.notes),
|
||||||
isResident: form.isResident,
|
isResident: form.isResident,
|
||||||
|
isDeaf: form.isDeaf,
|
||||||
}
|
}
|
||||||
const result = await mutation.run(body)
|
const result = await mutation.run(body)
|
||||||
if (result.ok) navigate(`/rennmaeuse/${result.value.id}`)
|
if (result.ok) navigate(`/rennmaeuse/${result.value.id}`)
|
||||||
@@ -326,6 +336,16 @@ export default function GerbilFormPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="field">
|
||||||
|
<span>{t.fields.originBreeder}</span>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={form.originBreeder}
|
||||||
|
placeholder="z. B. Zucht der Kleinen Chaoten"
|
||||||
|
onChange={(e) => set('originBreeder', e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>{t.fields.receiver}</span>
|
<span>{t.fields.receiver}</span>
|
||||||
<select
|
<select
|
||||||
@@ -360,6 +380,21 @@ export default function GerbilFormPage() {
|
|||||||
<textarea value={form.notes} onChange={(e) => set('notes', e.target.value)} />
|
<textarea value={form.notes} onChange={(e) => set('notes', e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="field">
|
||||||
|
<span>{t.fields.isDeaf}</span>
|
||||||
|
<select
|
||||||
|
value={form.isDeaf === null ? '' : String(form.isDeaf)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value
|
||||||
|
set('isDeaf', v === '' ? null : v === 'true')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">{t.form.isDeafUnknown}</option>
|
||||||
|
<option value="true">{t.form.isDeafYes}</option>
|
||||||
|
<option value="false">{t.form.isDeafNo}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="field field--check" title={t.form.isResidentHint}>
|
<label className="field field--check" title={t.form.isResidentHint}>
|
||||||
<span>{t.form.isResidentLabel}</span>
|
<span>{t.form.isResidentLabel}</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export const de = {
|
|||||||
enclosure: 'Gehege',
|
enclosure: 'Gehege',
|
||||||
colorVariety: 'Farbschlag',
|
colorVariety: 'Farbschlag',
|
||||||
origin: 'Herkunft',
|
origin: 'Herkunft',
|
||||||
|
originBreeder: 'Herkunft (Freitext)',
|
||||||
|
isDeaf: 'Gehörlos',
|
||||||
receiver: 'Abnehmer',
|
receiver: 'Abnehmer',
|
||||||
genotype: 'Genotyp',
|
genotype: 'Genotyp',
|
||||||
notes: 'Notizen',
|
notes: 'Notizen',
|
||||||
@@ -114,6 +116,9 @@ export const de = {
|
|||||||
// BESTAND-FILTER: Zugehörigkeit zum eigenen Bestand (sonst externe Ahne)
|
// BESTAND-FILTER: Zugehörigkeit zum eigenen Bestand (sonst externe Ahne)
|
||||||
isResidentLabel: 'Gehört zum eigenen Bestand',
|
isResidentLabel: 'Gehört zum eigenen Bestand',
|
||||||
isResidentHint: 'Abwählen für externe Ahnen, die nur für den Stammbaum erfasst sind.',
|
isResidentHint: 'Abwählen für externe Ahnen, die nur für den Stammbaum erfasst sind.',
|
||||||
|
isDeafUnknown: 'Unbekannt',
|
||||||
|
isDeafYes: 'Ja',
|
||||||
|
isDeafNo: 'Nein',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
saving: 'Speichern …',
|
saving: 'Speichern …',
|
||||||
|
|||||||
Reference in New Issue
Block a user