Compare commits

...

4 Commits

Author SHA1 Message Date
0e7ec5ab61 BESTAND-FILTER follow-up: residency toggle on the Gerbil create/edit form
Completes residency end-to-end: a 'Gehört zum eigenen Bestand' checkbox on the
Rennmaus-Formular (default on for new animals) lets the user mark a tier as an
external ancestor by hand — previously isResident was only set at import. Wired
through FormState + formFromGerbil (defaults true) + the CreateGerbil payload.

German via de.ts (LF). e2e: edit Krümel, untick Bestand, save -> detail shows
the 'Extern' badge (proves form field -> payload -> persistence). Gate green:
build, eslint, vitest 78, e2e 86.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:12:32 +02:00
12604fba7c data(import): D4 Victoria Welby resolved (ee[f]) — C's mother → C gets both parents — Julian 2026-06-06 12:12:23 +02:00
311c5461fd data(import): Chelsea (merge to *02.04.2021, correctDob) + Vestra (DD) resolved — Julian 2026-06-06 12:11:41 +02:00
3634d6ef9a data(import): D3 Silvain resolved (ee, Pp) — Julian 2026-06-06 12:09:41 +02:00
5 changed files with 64 additions and 5 deletions

View File

@@ -93,10 +93,10 @@ Bitte je Tier sagen, **welcher Wert stimmt** (die Quellen widersprechen sich bei
| WildFire v.d. K.C. (*05.10.2017) | P-Locus: **P-****PP** | ✅ **PP** — Julian | | WildFire v.d. K.C. (*05.10.2017) | P-Locus: **P-****PP** | ✅ **PP** — Julian |
| Zuleika v.d. K.C. (*24.10.2015) | D-Locus: **D-****DD** | ✅ **DD, Ee, Gg, PP** (`aa c[chm]c[h] DD Ee Gg PP spsp`) — Julian | | Zuleika v.d. K.C. (*24.10.2015) | D-Locus: **D-****DD** | ✅ **DD, Ee, Gg, PP** (`aa c[chm]c[h] DD Ee Gg PP spsp`) — Julian |
| Milka of LennyLengo (*09.12.2018) | C-Locus: **C-****Cc[h]** · E-Locus: **E-****EE** | ✅ **Cc[h], EE** (`aa Cc[h] dd EE Gg P- Spsp`) — Julian | | Milka of LennyLengo (*09.12.2018) | C-Locus: **C-****Cc[h]** · E-Locus: **E-****EE** | ✅ **Cc[h], EE** (`aa Cc[h] dd EE Gg P- Spsp`) — Julian |
| Silvain v.d. K.C. (*27.03.2022) | E-Locus: **Ee****ee** · P-Locus: **P-****Pp** | ⏳ offen | | Silvain v.d. K.C. (*27.03.2022) | E-Locus: **Ee****ee** · P-Locus: **P-****Pp** | **ee, Pp** (`aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp`) — Julian |
| Ichika v.d. K.C. (*19.04.2020) | E-Locus: **ee****ee[f]** | ✅ **ee[f]** (Beibehalten-Regel: `[f]` war vorhanden) — Julian | | Ichika v.d. K.C. (*19.04.2020) | E-Locus: **ee****ee[f]** | ✅ **ee[f]** (Beibehalten-Regel: `[f]` war vorhanden) — Julian |
| Daja of Little Rose (*16.05.2021) | Scheckung: **mit `spsp`****ohne** | ✅ **mit `spsp`** (Beibehalten-Regel) — Julian | | Daja of Little Rose (*16.05.2021) | Scheckung: **mit `spsp`****ohne** | ✅ **mit `spsp`** (Beibehalten-Regel) — Julian |
| Chelsea v.d. K.C. | ⚠️ **Kein Genotyp-Konflikt**es gibt **zwei** „Chelsea v.d. K.C." mit verschiedenem Geburtsdatum: **\*02.04.2021** und **\*15.10.2021**. Zwei verschiedene Tiere, oder ist ein Datum falsch? | ⏳ offen | | Chelsea v.d. K.C. | ⚠️ **Kein Genotyp-Konflikt**zwei „Chelsea" mit verschiedenem Datum (\*02.04.2021 / \*15.10.2021) | ✅ **ein Tier, Geburtsdatum 02.04.2021** (15.10.2021 war falsch → zusammengeführt) — Julian |
### D4 · **Marker** unterschiedlich (`WP` / `DP` / `WFNZ` / „hörend" mal vorhanden, mal nicht) — welcher gilt? ### D4 · **Marker** unterschiedlich (`WP` / `DP` / `WFNZ` / „hörend" mal vorhanden, mal nicht) — welcher gilt?
> ✅ **REGEL (Julian 2026-06-06):** „Wenn irgendwo etwas vorhanden war, das anderswo fehlte → **immer beibehalten**." Gilt generell für Marker/Flags und Angaben wie `spsp` oder `[f]` (Vorhandensein gewinnt über Fehlen). Wird zur Standard-Regel im Importer → löst alle „mit/ohne"-Fälle automatisch (z. B. Daja `spsp`, Ichika `[f]`). Greift NICHT bei echten Wert-Widersprüchen (z. B. `DD`↔`D-`, `Ee`↔`ee`) — die brauchen weiter deine Entscheidung. > ✅ **REGEL (Julian 2026-06-06):** „Wenn irgendwo etwas vorhanden war, das anderswo fehlte → **immer beibehalten**." Gilt generell für Marker/Flags und Angaben wie `spsp` oder `[f]` (Vorhandensein gewinnt über Fehlen). Wird zur Standard-Regel im Importer → löst alle „mit/ohne"-Fälle automatisch (z. B. Daja `spsp`, Ichika `[f]`). Greift NICHT bei echten Wert-Widersprüchen (z. B. `DD`↔`D-`, `Ee`↔`ee`) — die brauchen weiter deine Entscheidung.
@@ -105,8 +105,8 @@ Bitte je Tier sagen, **welcher Wert stimmt** (die Quellen widersprechen sich bei
| Tier | Konkreter Konflikt — was stimmt? | Status | | Tier | Konkreter Konflikt — was stimmt? | Status |
|---|---|---| |---|---|---|
| Vestra von den Schlossmäusen (*08.02.2019) | D-Locus: **D-****DD** (WP gleich in beiden) | ⏳ offen | | Vestra von den Schlossmäusen (*08.02.2019) | D-Locus: **D-****DD** (WP gleich in beiden) | **DD** — Julian |
| Victoria Welby gen. Welby v.d. K.C. (*16.01.2023) | E-Locus: **Ee[f]****ee[f]** (Fuchs ja/nein; DP gleich in beiden) — **das ist die Mutter von „C"!** Sobald geklärt, bekommt C auch seine Mutter. | ⏳ offen | | Victoria Welby gen. Welby v.d. K.C. (*16.01.2023) | E-Locus: **Ee[f]****ee[f]** **Mutter von „C"!** | ✅ **ee[f]** (`Aa CC D- ee[f] Gg pp Spsp [DP]`) — Julian → C bekommt damit seine Mutter |
| Hedwig of BGB (*30.10.2019) | (WP/DP/hörend) | ✅ auto-gelöst — sind jetzt Flags, kein Konflikt mehr | | Hedwig of BGB (*30.10.2019) | (WP/DP/hörend) | ✅ auto-gelöst — sind jetzt Flags, kein Konflikt mehr |
| Pitari gen. Piti v.d. K.C. (*16.05.2021) | (DP) | ✅ auto-gelöst — DP ist jetzt ein Flag | | Pitari gen. Piti v.d. K.C. (*16.05.2021) | (DP) | ✅ auto-gelöst — DP ist jetzt ein Flag |
| Little Hero of Black Forest (*22.02.2018) | (WFNZ ± spsp) | ✅ kein Genotyp-Konflikt mehr (WFNZ = Flag) | | Little Hero of Black Forest (*22.02.2018) | (WFNZ ± spsp) | ✅ kein Genotyp-Konflikt mehr (WFNZ = Flag) |

View File

@@ -27,3 +27,17 @@ test('Toggle „Externe Ahnen einblenden“ zeigt externe Tiere mit Extern-Marki
// Der Bestand bleibt weiterhin sichtbar. // Der Bestand bleibt weiterhin sichtbar.
await expect(page.locator('.gerbil-row', { hasText: 'Krümel' })).toBeVisible() await expect(page.locator('.gerbil-row', { hasText: 'Krümel' })).toBeVisible()
}) })
test('Bearbeiten-Formular kann ein Tier als extern markieren (Bestand-Häkchen)', async ({ page }) => {
skipUnlessMock()
// Krümel gehört zum Bestand -> Häkchen entfernen und speichern.
await page.goto('/rennmaeuse/kruemel/bearbeiten')
const check = page.getByRole('checkbox', { name: t.form.isResidentLabel })
await expect(check).toBeChecked()
await check.uncheck()
await page.getByRole('button', { name: t.form.save }).click()
// Auf der Detailseite ist Krümel jetzt als „Extern“ markiert.
await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible()
await expect(page.getByText(t.externalBadge, { exact: true })).toBeVisible()
})

View File

@@ -24,6 +24,7 @@ interface FormState {
receiverContactId: string receiverContactId: string
genotype: string genotype: string
notes: string notes: string
isResident: boolean
} }
const EMPTY: FormState = { const EMPTY: FormState = {
@@ -41,6 +42,7 @@ const EMPTY: FormState = {
receiverContactId: '', receiverContactId: '',
genotype: '', genotype: '',
notes: '', notes: '',
isResident: true,
} }
function formFromGerbil(g: { function formFromGerbil(g: {
@@ -58,6 +60,7 @@ function formFromGerbil(g: {
receiverContactId: string | null receiverContactId: string | null
genotype: string | null genotype: string | null
notes: string | null notes: string | null
isResident?: boolean | null
}): FormState { }): FormState {
return { return {
name: g.name, name: g.name,
@@ -74,6 +77,7 @@ function formFromGerbil(g: {
receiverContactId: g.receiverContactId ?? '', receiverContactId: g.receiverContactId ?? '',
genotype: g.genotype ?? '', genotype: g.genotype ?? '',
notes: g.notes ?? '', notes: g.notes ?? '',
isResident: g.isResident ?? true,
} }
} }
@@ -166,6 +170,7 @@ export default function GerbilFormPage() {
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,
} }
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}`)
@@ -355,6 +360,15 @@ 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 field--check" title={t.form.isResidentHint}>
<span>{t.form.isResidentLabel}</span>
<input
type="checkbox"
checked={form.isResident}
onChange={(e) => set('isResident', e.target.checked)}
/>
</label>
{mutation.error && <div className="alert alert--error">{mutation.error}</div>} {mutation.error && <div className="alert alert--error">{mutation.error}</div>}
<div className="form-actions"> <div className="form-actions">

View File

@@ -116,6 +116,9 @@ export const de = {
none: '— keine Angabe —', none: '— keine Angabe —',
genotypeHint: genotypeHint:
'Optional. Format z. B. „Aa CC Dd EE GG Pp Spsp rere“. Unbekannte Allele als „-“.', 'Optional. Format z. B. „Aa CC Dd EE GG Pp Spsp rere“. Unbekannte Allele als „-“.',
// BESTAND-FILTER: Zugehörigkeit zum eigenen Bestand (sonst externe Ahne)
isResidentLabel: 'Gehört zum eigenen Bestand',
isResidentHint: 'Abwählen für externe Ahnen, die nur für den Stammbaum erfasst sind.',
save: 'Speichern', save: 'Speichern',
cancel: 'Abbrechen', cancel: 'Abbrechen',
saving: 'Speichern …', saving: 'Speichern …',

View File

@@ -1,5 +1,5 @@
{ {
"_doc": "Human conflict resolutions for the import quarantine (HUMANQUESTION section D / C6). The importer consumes this to UN-QUARANTINE an animal: for a matching (name + dob) it accepts the given authoritative field(s) — `genotype`, `farbschlag`, and/or `dateOfDeath` (DD.MM.YYYY) — and skips the conflict. Key match = normalize(name) + dob, same identity as dedup. Maintained by god (Michael) as Julian/his wife answer the D-conflicts; originals (xlsx) stay read-only.", "_doc": "Human conflict resolutions for the import quarantine (HUMANQUESTION section D / C6). The importer consumes this to UN-QUARANTINE an animal: for a matching (name + dob) it accepts the given authoritative field(s) — `genotype`, `farbschlag`, and/or `dateOfDeath` (DD.MM.YYYY) — and skips the conflict. Special field `correctDob` (DD.MM.YYYY): the matched (name + dob) record is a DUPLICATE with a WRONG birthdate — remap its DOB to `correctDob` BEFORE dedup so it merges into the canonical same-named animal. Key match = normalize(name) + dob, same identity as dedup. Maintained by god (Michael) as Julian/his wife answer the D-conflicts; originals (xlsx) stay read-only.",
"resolutions": [ "resolutions": [
{ {
"name": "Firefly von den Kleinen Chaoten", "name": "Firefly von den Kleinen Chaoten",
@@ -56,6 +56,34 @@
"decision": "C-locus = Cc[h], E-locus = EE", "decision": "C-locus = Cc[h], E-locus = EE",
"genotype": "aa Cc[h] dd EE Gg P- Spsp", "genotype": "aa Cc[h] dd EE Gg P- Spsp",
"source": "Julian 2026-06-06 — HUMANQUESTION D3" "source": "Julian 2026-06-06 — HUMANQUESTION D3"
},
{
"name": "Silvain von den Kleinen Chaoten",
"dob": "27.03.2022",
"decision": "E-locus = ee, P-locus = Pp",
"genotype": "aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp",
"source": "Julian 2026-06-06 — HUMANQUESTION D3"
},
{
"name": "Chelsea von den Kleinen Chaoten",
"dob": "15.10.2021",
"decision": "duplicate with wrong birthdate — same animal as Chelsea *02.04.2021; merge into it",
"correctDob": "02.04.2021",
"source": "Julian 2026-06-06 — HUMANQUESTION D3 (Chelsea Dublette)"
},
{
"name": "Vestra von den Schlossmäusen",
"dob": "08.02.2019",
"decision": "D-locus = DD",
"genotype": "Aa Cc[chm] DD EE GG PP Spsp [WP]",
"source": "Julian 2026-06-06 — HUMANQUESTION D4"
},
{
"name": "Victoria Welby gen. Welby von den Kleinen Chaoten",
"dob": "16.01.2023",
"decision": "E-locus = ee[f] (Fuchs). NOTE: this is the mother of animal 'C' (c-29042024) — un-quarantining her links C's second parent.",
"genotype": "Aa CC D- ee[f] Gg pp Spsp [DP]",
"source": "Julian 2026-06-06 — HUMANQUESTION D4"
} }
] ]
} }